"""
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
"""

import abc
import re
from datetime import datetime  # noqa
from typing import Dict, List, Optional, Pattern, Sequence, Tuple, Union  # noqa

import pyparsing as pp
import typepy
from typepy import DateTime

from ._common import _to_unicode
from ._interface import PingParserInterface
from ._logger import logger
from ._stats import PingStats
from ._typing import IcmpReplies
from .error import ParseError, ParseErrorReason


class IcmpReplyKey:
    TIMESTAMP = "timestamp"
    SEQUENCE_NO = "icmp_seq"
    TTL = "ttl"
    TIME = "time"
    DUPLICATE = "duplicate"


class PingParser(PingParserInterface):

    _IPADDR_PATTERN = r"(\d{1,3}\.){3}\d{1,3}"
    _ICMP_SEQ_PATTERN = r"icmp_seq=(?P<icmp_seq>\d+)"
    _TTL_PATTERN = r"ttl=(?P<ttl>\d+)"
    _TIME_PATTERN = r"time=(?P<time>[0-9\.]+)"

    @abc.abstractproperty
    def _parser_name(self) -> str:  # pragma: no cover
        pass

    @abc.abstractproperty
    def _icmp_reply_pattern(self) -> str:  # pragma: no cover
        pass

    @property
    def _duplicate_packet_pattern(self) -> str:
        return r".+ \(DUP!\)$"

    @abc.abstractproperty
    def _stats_headline_pattern(self) -> str:  # pragma: no cover
        pass

    @abc.abstractproperty
    def _is_support_packet_duplicate(self) -> bool:  # pragma: no cover
        pass

    def _parse_icmp_reply(self, ping_lines: Sequence[str]) -> IcmpReplies:
        icmp_reply_regexp = re.compile(self._icmp_reply_pattern, re.IGNORECASE)
        duplicate_packet_regexp = re.compile(self._duplicate_packet_pattern)
        icmp_reply_list = []

        for line in ping_lines:
            match = icmp_reply_regexp.search(line)
            if not match:
                continue

            results = match.groupdict()
            reply = {}  # type: Dict[str, Union[bool, float, int, datetime]]

            if results.get(IcmpReplyKey.TIMESTAMP):
                reply[IcmpReplyKey.TIMESTAMP] = DateTime(
                    results[IcmpReplyKey.TIMESTAMP].lstrip("[").rstrip("]")
                ).force_convert()

            if results.get(IcmpReplyKey.SEQUENCE_NO):
                reply[IcmpReplyKey.SEQUENCE_NO] = int(results[IcmpReplyKey.SEQUENCE_NO])

            if results.get(IcmpReplyKey.TTL):
                reply[IcmpReplyKey.TTL] = int(results[IcmpReplyKey.TTL])

            if results.get(IcmpReplyKey.TIME):
                reply[IcmpReplyKey.TIME] = float(results[IcmpReplyKey.TIME])

            if duplicate_packet_regexp.search(line):
                reply[IcmpReplyKey.DUPLICATE] = True
            else:
                reply[IcmpReplyKey.DUPLICATE] = False

            icmp_reply_list.append(reply)

        return icmp_reply_list

    def _preprocess_parse_stats(self, lines: Sequence[str]) -> Tuple[str, str, Sequence[str]]:
        logger.debug("parsing as {:s} ping result format".format(self._parser_name))

        stats_headline_idx = self.__find_stats_headline_idx(
            lines, re.compile(self._stats_headline_pattern)
        )
        body_line_list = lines[stats_headline_idx + 1 :]
        self.__validate_stats_body(body_line_list)

        packet_info_line = body_line_list[0]

        return (lines[stats_headline_idx], packet_info_line, body_line_list)

    def _parse_destination(self, stats_headline: str) -> str:
        return stats_headline.lstrip("--- ").rstrip(" ping statistics ---")

    def __find_stats_headline_idx(self, lines: Sequence[str], re_stats_header: Pattern) -> int:
        for i, line in enumerate(lines):
            if re_stats_header.search(line):
                break
        else:
            raise ParseError(reason=ParseErrorReason.HEADER_NOT_FOUND)

        return i

    def __validate_stats_body(self, body_line_list: Sequence[str]) -> None:
        if typepy.is_empty_sequence(body_line_list):
            raise ParseError(reason=ParseErrorReason.EMPTY_STATISTICS)

    def _parse_duplicate(self, line: str) -> Optional[int]:
        if not self._is_support_packet_duplicate:
            return None

        packet_pattern = (
            pp.SkipTo(pp.Word("+" + pp.nums) + pp.Literal("duplicates,"))
            + pp.Word("+" + pp.nums)
            + pp.Literal("duplicates,")
        )

        try:
            duplicate_parse_list = packet_pattern.parseString(_to_unicode(line))
        except pp.ParseException:
            return 0

        return int(duplicate_parse_list[-2].strip("+"))


class NullPingParser(PingParser):
    @property
    def _parser_name(self) -> str:
        return "null"

    @property
    def _icmp_reply_pattern(self) -> str:
        return ""

    @property
    def _stats_headline_pattern(self) -> str:
        return ""

    @property
    def _is_support_packet_duplicate(self) -> bool:  # pragma: no cover
        return False

    def parse(self, ping_message: List[str]) -> PingStats:  # pragma: no cover
        return PingStats()

    def _preprocess_parse_stats(
        self, lines: Sequence[str]
    ) -> Tuple[str, str, List[str]]:  # pragma: no cover
        return ("", "", [])


class LinuxPingParser(PingParser):
    @property
    def _parser_name(self) -> str:
        return "Linux"

    @property
    def _icmp_reply_pattern(self) -> str:
        return (
            r"(?P<timestamp>\[[0-9\.]+\])?\s?.+ from .+?: "
            + self._ICMP_SEQ_PATTERN
            + " "
            + self._TTL_PATTERN
            + " "
            + self._TIME_PATTERN
        )

    @property
    def _stats_headline_pattern(self) -> str:
        return "--- .* ping statistics ---"

    @property
    def _is_support_packet_duplicate(self) -> bool:
        return True

    def parse(self, ping_message: List[str]) -> PingStats:
        icmp_replies = self._parse_icmp_reply(ping_message)
        stats_headline, packet_info_line, body_line_list = self._preprocess_parse_stats(
            lines=ping_message
        )
        packet_pattern = (
            pp.Word(pp.nums)
            + pp.Literal("packets transmitted,")
            + pp.Word(pp.nums)
            + pp.Literal("received,")
        )

        destination = self._parse_destination(stats_headline)
        duplicates = self._parse_duplicate(packet_info_line)

        parse_list = packet_pattern.parseString(_to_unicode(packet_info_line))
        packet_transmit = int(parse_list[0])
        packet_receive = int(parse_list[2])

        is_valid_data = True
        try:
            rtt_line = body_line_list[1]
        except IndexError:
            is_valid_data = False

        if not is_valid_data or typepy.is_null_string(rtt_line):
            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        rtt_pattern = (
            pp.Literal("rtt min/avg/max/mdev =")
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + pp.Word(pp.nums + "ms")
        )
        parse_list = rtt_pattern.parseString(_to_unicode(rtt_line))

        return PingStats(
            destination=destination,
            packet_transmit=packet_transmit,
            packet_receive=packet_receive,
            duplicates=duplicates,
            rtt_min=float(parse_list[1]),
            rtt_avg=float(parse_list[3]),
            rtt_max=float(parse_list[5]),
            rtt_mdev=float(parse_list[7]),
            icmp_replies=icmp_replies,
        )


class WindowsPingParser(PingParser):
    @property
    def _parser_name(self) -> str:
        return "Windows"

    @property
    def _icmp_reply_pattern(self) -> str:
        return " from  .+?: " + self._TTL_PATTERN + " " + self._TIME_PATTERN

    @property
    def _stats_headline_pattern(self) -> str:
        return "^Ping statistics for "

    @property
    def _is_support_packet_duplicate(self) -> bool:
        return False

    def parse(self, ping_message: List[str]) -> PingStats:
        icmp_replies = self._parse_icmp_reply(ping_message)
        stats_headline, packet_info_line, body_line_list = self._preprocess_parse_stats(
            lines=ping_message
        )
        packet_pattern = (
            pp.Literal("Packets: Sent = ")
            + pp.Word(pp.nums)
            + pp.Literal(", Received = ")
            + pp.Word(pp.nums)
        )

        destination = self._parse_destination(stats_headline)
        duplicates = self._parse_duplicate(packet_info_line)

        parse_list = packet_pattern.parseString(_to_unicode(packet_info_line))
        packet_transmit = int(parse_list[1])
        packet_receive = int(parse_list[3])

        is_valid_data = True
        try:
            rtt_line = body_line_list[2].strip()
        except IndexError:
            is_valid_data = False

        if not is_valid_data or typepy.is_null_string(rtt_line):
            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        rtt_pattern = (
            pp.Literal("Minimum = ")
            + pp.Word(pp.nums)
            + pp.Literal("ms, Maximum = ")
            + pp.Word(pp.nums)
            + pp.Literal("ms, Average = ")
            + pp.Word(pp.nums)
        )
        parse_list = rtt_pattern.parseString(_to_unicode(rtt_line))

        return PingStats(
            destination=destination,
            packet_transmit=packet_transmit,
            packet_receive=packet_receive,
            duplicates=duplicates,
            rtt_min=float(parse_list[1]),
            rtt_avg=float(parse_list[5]),
            rtt_max=float(parse_list[3]),
            icmp_replies=icmp_replies,
        )

    def _parse_destination(self, stats_headline):
        return stats_headline.lstrip("Ping statistics for ").rstrip(":")


class MacOsPingParser(PingParser):
    @property
    def _parser_name(self) -> str:
        return "macOS"

    @property
    def _icmp_reply_pattern(self) -> str:
        return (
            " from .+?: "
            + self._ICMP_SEQ_PATTERN
            + " "
            + self._TTL_PATTERN
            + " "
            + self._TIME_PATTERN
        )

    @property
    def _stats_headline_pattern(self) -> str:
        return "--- .* ping statistics ---"

    @property
    def _is_support_packet_duplicate(self) -> bool:
        return True

    def parse(self, ping_message: List[str]) -> PingStats:
        icmp_replies = self._parse_icmp_reply(ping_message)
        stats_headline, packet_info_line, body_line_list = self._preprocess_parse_stats(
            lines=ping_message
        )
        packet_pattern = (
            pp.Word(pp.nums)
            + pp.Literal("packets transmitted,")
            + pp.Word(pp.nums)
            + pp.Literal("packets received,")
        )

        destination = self._parse_destination(stats_headline)
        duplicates = self._parse_duplicate(packet_info_line)

        parse_list = packet_pattern.parseString(_to_unicode(packet_info_line))
        packet_transmit = int(parse_list[0])
        packet_receive = int(parse_list[2])

        is_valid_data = True
        try:
            rtt_line = body_line_list[1]
        except IndexError:
            is_valid_data = False

        if not is_valid_data or typepy.is_null_string(rtt_line):
            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        rtt_pattern = (
            pp.Literal("round-trip min/avg/max/stddev =")
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + pp.Word(pp.nums + "ms")
        )
        parse_list = rtt_pattern.parseString(_to_unicode(rtt_line))

        return PingStats(
            destination=destination,
            packet_transmit=packet_transmit,
            packet_receive=packet_receive,
            duplicates=duplicates,
            rtt_min=float(parse_list[1]),
            rtt_avg=float(parse_list[3]),
            rtt_max=float(parse_list[5]),
            rtt_mdev=float(parse_list[7]),
            icmp_replies=icmp_replies,
        )


class AlpineLinuxPingParser(LinuxPingParser):
    @property
    def _parser_name(self) -> str:
        return "AlpineLinux"

    @property
    def _icmp_reply_pattern(self) -> str:
        return (
            " from .+?: " + r"seq=(?P<icmp_seq>\d+) " + self._TTL_PATTERN + " " + self._TIME_PATTERN
        )

    @property
    def _is_support_packet_duplicate(self) -> bool:
        return True

    def parse(self, ping_message: List[str]) -> PingStats:
        icmp_replies = self._parse_icmp_reply(ping_message)
        stats_headline, packet_info_line, body_line_list = self._preprocess_parse_stats(
            lines=ping_message
        )
        packet_pattern = (
            pp.Word(pp.nums)
            + pp.Literal("packets transmitted,")
            + pp.Word(pp.nums)
            + pp.Literal("packets received,")
        )

        destination = self._parse_destination(stats_headline)
        duplicates = self._parse_duplicate(packet_info_line)

        parse_list = packet_pattern.parseString(_to_unicode(packet_info_line))
        packet_transmit = int(parse_list[0])
        packet_receive = int(parse_list[2])

        is_valid_data = True
        try:
            rtt_line = body_line_list[1]
        except IndexError:
            is_valid_data = False

        if not is_valid_data or typepy.is_null_string(rtt_line):
            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        rtt_pattern = (
            pp.Literal("round-trip min/avg/max =")
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + pp.Word(pp.nums + "ms")
        )
        parse_list = rtt_pattern.parseString(_to_unicode(rtt_line))

        return PingStats(
            destination=destination,
            packet_transmit=packet_transmit,
            packet_receive=packet_receive,
            duplicates=duplicates,
            rtt_min=float(parse_list[1]),
            rtt_avg=float(parse_list[3]),
            rtt_max=float(parse_list[5]),
            icmp_replies=icmp_replies,
        )

    def _parse_duplicate(self, line: str) -> int:
        packet_pattern = (
            pp.SkipTo(pp.Word(pp.nums) + pp.Literal("duplicates,"))
            + pp.Word(pp.nums)
            + pp.Literal("duplicates,")
        )
        try:
            duplicate_parse_list = packet_pattern.parseString(_to_unicode(line))
        except pp.ParseException:
            return 0

        return int(duplicate_parse_list[-2])