#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details."""

import argparse
import re
import sys

import lib.args
import lib.base
import lib.shell
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN

__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026040801'

DESCRIPTION = """Sends ICMP ECHO_REQUEST packets to a network host using the system's built-in ping
command. Reports round-trip time (min, avg, max) and packet loss percentage. Alerts
on packet loss or high latency."""

DEFAULT_COUNT = 5  # icmp packets
DEFAULT_INTERVAL = 0.2  # seconds
DEFAULT_DEADLINE = 5  # seconds
DEFAULT_HOSTNAME = '127.0.0.1'


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(description=DESCRIPTION)

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version=f'%(prog)s: v{__version__} by {__author__}',
    )

    parser.add_argument(
        '--always-ok',
        help=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--count',
        help='Number of ECHO_REQUEST packets to send. Default: %(default)s',
        default=DEFAULT_COUNT,
        dest='COUNT',
        type=int,
    )

    parser.add_argument(
        '-H',
        '--hostname',
        help='Hostname or IP address to ping. Default: %(default)s',
        dest='HOSTNAME',
        default=DEFAULT_HOSTNAME,
    )

    parser.add_argument(
        '--interval',
        help='Interval between sending each packet, in seconds. '
        'Accepts real numbers with dot as decimal separator (regardless of locale). '
        'Default: %(default)s',
        default=DEFAULT_INTERVAL,
        dest='INTERVAL',
        type=float,
    )

    parser.add_argument(
        '-t',
        '--timeout',
        help='Timeout in seconds before ping exits regardless of how many packets '
        'have been sent or received. '
        'Default: %(default)s',
        default=DEFAULT_DEADLINE,
        dest='DEADLINE',
        type=int,
    )

    args, _ = parser.parse_known_args()
    return args


def main():
    """The main function. This is where the magic happens."""

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    # ping -c 5 -i 0.2 -w 5 -q 192.0.2.10
    cmd = (
        f'ping -c {args.COUNT} -i {args.INTERVAL} -w {args.DEADLINE} -q {args.HOSTNAME}'
    )

    # fetch data
    stdout, stderr, retc = lib.base.coe(lib.shell.shell_exec(cmd))
    if stderr or retc == 2:
        lib.base.cu(stderr)
    # stdout:
    #   PING 192.0.2.10 (192.0.2.10) 56(84) bytes of data.
    #
    #   --- 192.0.2.10 ping statistics ---
    #   5 packets transmitted, 5 received, 0% packet loss, time 803ms
    #   rtt min/avg/max/mdev = 6.724/13.682/15.856/3.488 ms

    # If ping does not receive any reply packets at all it will exit with code 1.
    # If a packet count and deadline are both specified, and fewer than count packets are received
    # by the time the deadline has arrived, it will also exit with code 1.
    # On other error it exits with code 2. Otherwise it exits with code 0.

    # Since we want to be as tolerant as possible, if we send burst pings and at least
    # one packet makes its way back, we assume the host is alive. So we don't rely on the
    # return code of `ping` (any longer).
    # See https://github.com/Linuxfabrik/monitoring-plugins/issues/691 for details.

    # init some vars
    # Throwing CRIT instead of WARN beacuse of the fact that this check will mainly be used
    # for checking host-liveliness [OK=UP, CRIT=DOWN].
    state = STATE_CRIT if re.search(r'\b0 received', stdout) else STATE_OK
    msg = '' if state == STATE_OK else 'Destination host unreachable. '
    perfdata = ''

    # analyze data:
    result = stdout.splitlines()
    if not result[0] or not result[3]:
        lib.base.cu('Unexpected output from ping.')

    # line 0:
    # "PING %hostname (%ip) [from %sinip %device: ]%datalen(%datalen+28)
    # 'PING www.linuxfabrik.ch (192.0.2.10) 56(84) bytes of data.'
    ping = re.search(r'G (.*?)\(', result[0])  # regex: 45 steps

    # build the message
    msg += f'PING {ping.group(1).strip()}' + ': '

    # line 3:
    # "%ntransmitted packets transmitted, %nreceived received[, +%nrepeats duplicates][, +%nchecksum corrupted][, +%nerrors errors][, %packetloss% packet loss, time %timems]"
    # '5 packets transmitted, 5 received, 0% packet loss, time 803ms'
    matches = re.search(
        r'(\d+) packets transmitted, (\d+) received(, \+?(\d+) duplicates)?(, \+?(\d+) checksum corrupted)?(, \+?(\d+) errors)?, (\d+(?:\.\d+)?)% packet loss, time (\d+)',  # regex: 92 steps
        result[3],
    )
    msg += result[3] + '. '
    perfdata += lib.base.get_perfdata(
        'transmitted',
        matches.group(1),
        uom=None,
        warn=None,
        crit=None,
        _min=0,
        _max=None,
    )
    perfdata += lib.base.get_perfdata(
        'received',
        matches.group(2),
        uom=None,
        warn=None,
        crit=None,
        _min=0,
        _max=None,
    )
    perfdata += lib.base.get_perfdata(
        'duplicates',
        matches.group(4).replace('+', '') if matches.group(4) else 0,
        uom=None,
        warn=None,
        crit=None,
        _min=0,
        _max=None,
    )
    perfdata += lib.base.get_perfdata(
        'checksum_corrupted',
        matches.group(6).replace('+', '') if matches.group(6) else 0,
        uom=None,
        warn=None,
        crit=None,
        _min=0,
        _max=None,
    )
    perfdata += lib.base.get_perfdata(
        'errors',
        matches.group(8).replace('+', '') if matches.group(8) else 0,
        uom=None,
        warn=None,
        crit=None,
        _min=0,
        _max=None,
    )
    perfdata += lib.base.get_perfdata(
        'packet_loss',
        matches.group(9),
        uom='%',
        warn=None,
        crit=None,
        _min=0,
        _max=100,
    )
    perfdata += lib.base.get_perfdata(
        'time',
        matches.group(10),
        uom='ms',
        warn=None,
        crit=None,
        _min=0,
        _max=None,
    )

    # line 4:
    # 'rtt min/avg/max/mdev = 8.926/11.367/17.350/3.184 ms'
    if result[4] and not result[4].startswith('pipe '):
        # host is reachable
        matches = re.search(
            r'= (\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)', result[4]
        )  # regex: 26 steps
        msg += result[4]
        perfdata += lib.base.get_perfdata(
            'rtt_min',
            matches.group(1),
            uom='ms',
            warn=None,
            crit=None,
            _min=0,
            _max=None,
        )
        perfdata += lib.base.get_perfdata(
            'rtt_avg',
            matches.group(2),
            uom='ms',
            warn=None,
            crit=None,
            _min=0,
            _max=None,
        )
        perfdata += lib.base.get_perfdata(
            'rtt_max',
            matches.group(3),
            uom='ms',
            warn=None,
            crit=None,
            _min=0,
            _max=None,
        )
        perfdata += lib.base.get_perfdata(
            'rtt_mdev',
            matches.group(4),
            uom='ms',
            warn=None,
            crit=None,
            _min=0,
            _max=None,
        )

    # over and out
    lib.base.oao(msg, state, perfdata, always_ok=args.ALWAYS_OK)


if __name__ == '__main__':
    try:
        main()
    except Exception:
        lib.base.cu()
