#!/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, *, t_type='hping3', count=100000,
                 host='127.0.0.1', host_alias=None):
        self.type = t_type
        self.count = count
        self.host = host
        self.host_alias = host_alias
        self.max_parallel = 100
        self.results = []
        self.metrics = []

    # ---------------- hping3 ----------------
    def hping3_test(self):
        """Вызов hping3"""
        # По тз надо отправить 100к пакетов максимально быстро.
        # При -i < u50 может возвращать 100%loss, но по tcpdump видно, что ответы дошли, просто поздно.
        # Поэтому попробуем вычислить оптимальный интервал.
        # Но при i>u30 тест будет долгим, а должен быть быстрым. Поэтому режем count.
        for i in [10, 20, 30, 40, 50]:
            r = 1
            try:
                cmd = ["hping3", "-q", "-1", "-i", "u{i}", "-c", str(self.count)]
                out = subprocess.run(cmd, text=True, capture_output=True, check=True, timeout=5,).stderr
                # retocde=0 вернёт даже при 97%loss
                r = out.returncode
            except (subprocess.CalledProcessError):
                pass
            # значит, даже 50 - мало, и надо выходить, 
            if r > 1:
                raise RuntimeError('Не удалось определить оптимальный интервал для hping3')
        # При i>u30, отправка более 50к пакетов - это долго
        if i > 30 and self.count > 50000:
            self.count = 50000
        cmd = ["hping3", "-1", "-i", f"u{i}", "-c", str(self.count), "-q"]
        cmd.append(self.host)

        try:
            # Статистика hping пишется в stderr, остальной вывод в stdout
            logger.debug(' '.join(cmd))
            out = subprocess.run(
                cmd,
                text=True,
                capture_output=True,
                check=True,
                timeout=5,
            ).stderr
            res = out
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
            res = None

        if res:
            return self._parse_hping3_output(res)

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

            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"round-trip min/avg/max = " \
                f"{rtt['min']:.1f}/{rtt['avg']:.3f}/"\
                f"{rtt['max']:.1f}"

        ratio = (sent - received) / sent * 100

        ret = {
            "host": self.host,
            "type": 'hping3',
            "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"Тест flood ping {item['packets']} пакетов к {host}, {item['type']},"
        #              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 = 3.0  # percent
        self.metrics.append({
            'title': f"Тест flood 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 = None

        result = self.hping3_test()

        if result:
            result['human'] = (
                f"{result['packets']} {result['type']} requests transmitted,"
                f" {result['received']} received, {result['ratio']}% requests 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="flood ping к указанному хосту "
                                "с помощью hping3")
    p.add_argument("--count", type=int, default=100000,
                   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()
