"""Представление индикатора IPv4-адреса и правила его поиска."""

from __future__ import annotations

import ipaddress
import re
import logging
from dataclasses import dataclass, field
from typing import Any, FrozenSet, Mapping

from .analysis import IndicatorAnalysisMixin, IndicatorVerdict, default_verdict
from .reason_texts import (
    ABUSEIPDB_CDN_REASON,
    ABUSEIPDB_NOT_CDN_REASON,
    ABUSEIPDB_NOT_WHITELISTED_REASON,
    ABUSEIPDB_PRIVATE_REASON,
    ABUSEIPDB_PUBLIC_REASON,
    ABUSEIPDB_WHITELISTED_REASON,
    ASRANK_RANK_REASON_TEMPLATE,
    ASRANK_RANK_UNKNOWN_REASON_TEMPLATE,
    ASRANK_TOP_REASON,
    IP_ANALYSIS_OK_REASON,
    NO_ASN_REASON,
    NO_PASSIVE_DOMAINS_REASON,
    NO_TOP1M_SHORT_LIVED_BLOCK_REASON,
    OTX_NO_PASSIVE_DNS_BLOCK_REASON,
    OTX_NO_PASSIVE_DNS_MONITOR_REASON,
    PASSIVE_DNS_FOUND_REASON,
    PASSIVE_DNS_HAS_TOP1M_REASON,
    PASSIVE_DNS_LONG_LIVED_REASON,
    PASSIVE_DNS_NO_TOP1M_REASON,
    PASSIVE_POPULAR_ZONES_REASON,
    PASSIVE_POPULAR_ZONES_SAFE_REASON_TEMPLATE,
    SHORT_LIVED_MONITOR_REASON,
    TOP1M_SHORT_LIVED_REASON,
    TOP1M_PASSIVE_TOP_REASON,
)
from .patterns import PatternDefinition
from .domain import DOMAINIndicator

logger = logging.getLogger(__name__)

_OBFUSCATED_DOT = r"(?:\.|\[\.]|\[\]|\(dot\)|\[dot\]|\(\.\))"

_IPV4_PATTERN = re.compile(
    (
        r"\b((?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5]){DOT}"
        ""
        r"(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5]){DOT}"
        ""
        r"(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5]){DOT}"
        ""
        r"(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5]))\b"
        ""
    ).replace("{DOT}", _OBFUSCATED_DOT),
    re.IGNORECASE,
)


@dataclass(frozen=True)
class IPAddressIndicator(IndicatorAnalysisMixin):
    """Индикатор IPv4-адреса."""

    value: str
    kinds: FrozenSet[str] = field(default_factory=frozenset)

    def pre_analysis(self) -> dict[str, Any]:
        """Отсутствие быстрых правил перед опросом провайдеров."""

        logger.info(
            "Для IP %s нет быстрых правил, запускаем опрос провайдеров", self.value
        )
        return {}

    async def analysis(self, features: Mapping[str, Any] | None = None) -> dict[str, Any]:
        """Запустить анализ IP с учётом фич провайдеров."""

        provider_features = features or {}
        result: dict[str, Any] = {"features": provider_features}
        reasons: list[dict[str, Any]] = []

        abuse_features = provider_features.get("abuseipdb") or {}
        if abuse_features.get("isPublic") is False:
            logger.info(
                "IP %s непубличный по AbuseIPDB, пропускаем дальнейший анализ", self.value
            )
            reasons.append({"text": ABUSEIPDB_PRIVATE_REASON})
            result["verdicts"] = [IndicatorVerdict.IGNORE]
            result["reasons"] = reasons
            return result

        reasons.append({"text": ABUSEIPDB_PUBLIC_REASON})

        if abuse_features.get("isWhitelisted") is False:
            logger.info(
                "IP %s не в белом списке AbuseIPDB, пропускаем дальнейший анализ", self.value
            )
            reasons.append({"text": ABUSEIPDB_NOT_WHITELISTED_REASON})
            result["verdicts"] = [IndicatorVerdict.IGNORE]
            result["reasons"] = reasons
            return result

        reasons.append({"text": ABUSEIPDB_WHITELISTED_REASON})

        usage_type = str(abuse_features.get("usageType", "")).lower()
        if usage_type == "content delivery network":
            reasons.append({"text": ABUSEIPDB_CDN_REASON})
            result["verdicts"] = [IndicatorVerdict.IGNORE]
            result["reasons"] = reasons
            return result
        reasons.append({"text": ABUSEIPDB_NOT_CDN_REASON})

        dbip_features = provider_features.get("dbip") or {}
        asn_number = dbip_features.get("asn_number")
        if asn_number is not None:
            from ..providers.asrank import ASRankProvider

            asrank_rank = ASRankProvider().get_rank(asn_number)
            dbip_features["asrank_rank"] = asrank_rank

            if asrank_rank is not None and asrank_rank < 100:
                reasons.append({"text": ASRANK_TOP_REASON})

            if asrank_rank is not None:
                reasons.append(
                    {
                        "text": ASRANK_RANK_REASON_TEMPLATE.format(
                            asn=asn_number, rank=asrank_rank
                        )
                    }
                )
            else:
                reasons.append(
                    {"text": ASRANK_RANK_UNKNOWN_REASON_TEMPLATE.format(asn=asn_number)}
                )
        else:
            reasons.append({"text": NO_ASN_REASON})

        otx_features = provider_features.get("otx") or {}
        passive_dns = otx_features.get("passive_dns") or []
        if not passive_dns:
            result["verdicts"] = [IndicatorVerdict.BLOCK, IndicatorVerdict.MONITOR]
            result["reasons"] = reasons + [
                {"text": OTX_NO_PASSIVE_DNS_BLOCK_REASON},
                {"text": OTX_NO_PASSIVE_DNS_MONITOR_REASON},
            ]
            return result

        reasons.append({"text": PASSIVE_DNS_FOUND_REASON})

        hostnames = [entry.get("hostname") for entry in passive_dns if entry.get("hostname")]

        from ..providers.top1m import Top1MProvider
        from ..providers.utils import fetch_provider_enrichment
        from ..providers.whois import WhoisProvider

        logger.info("Обогащаем %d доменов из passive DNS через топ-1М", len(hostnames))

        top1m_provider = Top1MProvider()
        top1m_features = await fetch_provider_enrichment(
            hostnames,
            provider_name="Top1M",
            provider=top1m_provider,
            context_getter=top1m_provider.get_context,
            indicator_factory=DOMAINIndicator,
        )
        otx_features["top1m"] = top1m_features

        selected_domains: list[str] = []
        tld_rank_counter = 0
        top1m_flag = False

        for entry in passive_dns:
            domain = entry.get("hostname")
            if not domain:
                continue

            domain_features = top1m_features.get(domain, {})
            tld_rank = domain_features.get("top1m_tld_rank")
            if tld_rank is not None and tld_rank < 100:
                tld_rank_counter += 1

            rank = domain_features.get("top1m_rank")
            if rank is not None:
                top1m_flag = True
                if rank < 100000:
                    result["verdicts"] = [IndicatorVerdict.IGNORE]
                    result["reasons"] = reasons + [{"text": TOP1M_PASSIVE_TOP_REASON}]
                    return result

            if tld_rank_counter >= 50:
                result["verdicts"] = [IndicatorVerdict.IGNORE]
                result["reasons"] = reasons + [{"text": PASSIVE_POPULAR_ZONES_REASON}]
                return result

            selected_domains.append(domain)

        reasons.append(
            {
                "text": PASSIVE_POPULAR_ZONES_SAFE_REASON_TEMPLATE.format(
                    count=tld_rank_counter
                )
            }
        )

        if not selected_domains:
            verdict = default_verdict(NO_PASSIVE_DOMAINS_REASON)
            result["verdicts"] = verdict["verdicts"]
            result["reasons"] = reasons + verdict["reasons"]
            return result

        logger.info(
            "Дополняем WHOIS для %d выбранных доменов из passive DNS", len(selected_domains)
        )

        whois_provider = WhoisProvider()
        whois_features = await fetch_provider_enrichment(
            selected_domains,
            provider_name="WHOIS",
            provider=whois_provider,
            context_getter=whois_provider.get_context,
            indicator_factory=DOMAINIndicator,
        )
        otx_features["whois"] = whois_features

        def _is_short_lived(domain: str) -> bool:
            reg_period = (whois_features.get(domain) or {}).get("whois_reg_period_days")
            return reg_period is not None and reg_period < 365 * 2

        all_short_lived = all(_is_short_lived(domain) for domain in selected_domains)

        if top1m_flag:
            reasons.append({"text": PASSIVE_DNS_HAS_TOP1M_REASON})
        else:
            reasons.append({"text": PASSIVE_DNS_NO_TOP1M_REASON})

        if top1m_flag and all_short_lived:
            result["verdicts"] = [IndicatorVerdict.BLOCK]
            result["reasons"] = reasons + [{"text": TOP1M_SHORT_LIVED_REASON}]
            return result

        if not top1m_flag and all_short_lived:
            result["verdicts"] = [IndicatorVerdict.BLOCK, IndicatorVerdict.MONITOR]
            result["reasons"] = reasons + [
                {"text": NO_TOP1M_SHORT_LIVED_BLOCK_REASON},
                {"text": SHORT_LIVED_MONITOR_REASON},
            ]
            return result

        reasons.append({"text": PASSIVE_DNS_LONG_LIVED_REASON})
        verdict = default_verdict(IP_ANALYSIS_OK_REASON)
        result["verdicts"] = verdict["verdicts"]
        result["reasons"] = reasons + verdict["reasons"]
        return result

    @property
    def category(self) -> str:
        """Категория индикатора."""

        return "ip"


def _rearm_dots(value: str) -> str:
    return re.sub(_OBFUSCATED_DOT, ".", value)


def _validate_ipv4(value: str) -> bool:
    try:
        return isinstance(ipaddress.ip_address(value), ipaddress.IPv4Address)
    except ValueError:
        return False


def build_patterns() -> tuple[PatternDefinition, ...]:
    """Вернуть правило выделения IPv4-адресов."""

    return (
        PatternDefinition(
            name="ipv4",
            regex=_IPV4_PATTERN,
            factory=lambda value, kinds: IPAddressIndicator(value=value, kinds=kinds),
            kinds=frozenset({"ip", "ipv4"}),
            rearm=_rearm_dots,
            normalize=lambda value: value,
            validator=_validate_ipv4,
        ),
    )
