#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Скрипт теста сети с помощью hping3 и/или ping"""


import argparse
import subprocess
import json
import logging

logger = logging.getLogger(__name__)


class TestRunner:
    """Класс для запуска теста"""
    def __init__(self, *, count=100000, host='127.0.0.1', host_alias=None):
        self.count = count
        self.host = host
        self.host_alias = host_alias
        self.max_parallel = 100
        self.results = []
        self.metrics = []

    def _run_ping(self, raw=True, interval=0.0, with_count=True):
        cmd = ["ping", "-w", "3", "-q",]
        if with_count:
            cmd += ["-c", str(self.count)]

        if raw:
            cmd += ["-f", "-e", "0"]
        else:
            cmd += ["-i", str(interval)]

        cmd.append(self.host)

        try:
            logger.debug(' '.join(cmd))
            out = subprocess.run(
                cmd,
                text=True,
                capture_output=True,
                check=True,
                timeout=5,
            ).stdout
            return out
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
            return None

    def ping_test(self):
        """Выполнит тест с помощью ping raw socket
        либо просто с коротким интервалом между пакетами, если raw sock невозможен"""
        # с интервалом .003 и -w 3 можно успеть отправить максимум 1000 пакетов без ошибки
        self.count = min(self.count, 1000)
        res = self._run_ping(raw=False, interval=0.003)

        if not res:
            # Могло завершиться по таймауту и не успеть отправить все пакеты
            logger.warning("fallback ping -w 3 -i 0.003, no count!")
            res = self._run_ping(raw=False, interval=0.003, with_count=False)

        if res:
            return self._parse_ping_output(res)

    def _parse_ping_output(self, text):
        """Парсит вывод ping и складывает в dict"""
        # parse rtt line
        rtt = {}
        for line in text.splitlines():
            if "rtt min/avg/max/mdev" in line:
                values = line.split("=")[1].strip().split()[0]
                mn, avg, mx, mdev = map(float, values.split("/"))
                rtt = {'min': mn, 'avg': avg, 'max': mx, 'mdev': mdev}

            if "packets transmitted" in line:
                parts = line.split(",")
                sent = int(parts[0].split()[0])
                received = int(parts[1].split()[0])

        if rtt:
            rtt['human'] = f"ping rtt min/avg/max/mdev = " \
                f"{rtt['min']:.1f}/{rtt['avg']:.3f}/"\
                f"{rtt['max']:.1f}/{rtt['mdev']:.3f} ms"

        ratio = (sent - received) / sent * 100

        ret = {
            "host": self.host,
            "packets": sent,
            "received": received,
            "ratio": round(ratio, 1),
            "rtt": rtt,
        }
        return ret

    def get_metrics(self):
        """Соберёт метрики для json"""
        if not self.results:
            return
        item = self.results[0]
        host = f"{self.host_alias} ({item['host']})" if self.host_alias else item['host']
        val = item['rtt']['avg']
        ref = 1  # milliseconds
        self.metrics.append({
            'title': f"Тест latency ping {item['packets']} пакетов к {host}, ping,"
                     f" rtt (не более {ref:.3f} msec)",
            'value': val,
            'reference': ref,  # cutoff value
            # 'format': lambda x: f'{x:.3f} ms',
            'format': '{x:.3f} msec',
            'condition': 'less',  # condition type
        })
        # val = item['ratio']
        # ref = 1.0  # percent
        # self.metrics.append({
        #     'title': f"Тест latency ping {item['packets']} пакетов к {host}, {item['type']},"
        #              f" потери (не более {ref:.1f}%)",
        #     'value': val,
        #     'reference': ref,  # cutoff
        #     # 'format': lambda x: f'{x:.1f} %',
        #     'format': '{x:.1f}%',
        #     'condition': 'less',  # condition type
        # })

    # ---------------- run ----------------
    def run_test(self):
        result = self.ping_test()

        if result:
            result['human'] = (
                f"{result['packets']} packets transmitted,"
                f" {result['received']} received, {result['ratio']}% failed"
            )
            self.results.append(result)
            self.get_metrics()


def configure_logging(debug=False):
    """Настройка логов и вывода результатов для запуска без враппера (т.е. без --json)"""
    level = logging.DEBUG if debug else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s - [%(levelname)s] - %(module)s.%(funcName)s - %(message)s",
    )


def parse_args():
    """Разберёт аргументы"""
    p = argparse.ArgumentParser(description="Обычный ping к указанному хосту "
                                "с помощью ping")
    p.add_argument("--count", type=int, default=100,
                   help='Количество пакетов для отправки к хосту')
    p.add_argument("--host", type=str, help="Хост для проверки связи")
    p.add_argument("--alias", type=str, default=None,
                   help="Алиас имени хоста (например, если --host=IP)")
    p.add_argument("--json", action="store_true", help="Вывод в json")
    p.add_argument("--debug", action="store_true", default=False)

    args = p.parse_args()

    return args


def main():
    """main"""
    args = parse_args()
    configure_logging(debug=args.debug)

    runner = TestRunner(count=args.count, host=args.host, host_alias=args.alias)

    runner.run_test()

    if not runner.metrics:
        msg = 'Все варианты ping завершились ошибками. ' \
              ' Запустите скрипт с флагом --debug, ' \
              ' попробуйте выполнить команды вручную и понять, почему'
        raise RuntimeError(msg)

    if args.json:
        print(json.dumps(runner.metrics, indent=2, ensure_ascii=False))
    else:
        results = runner.results[0]
        rtt = results.get('rtt', None)
        if rtt:
            logger.info("%s", rtt['human'])
        logger.info("%s", results['human'])


if __name__ == "__main__":
    main()
