"""Провайдер WHOIS, возвращающий сырые данные библиотеки ``whois``."""

from __future__ import annotations

import asyncio
from collections.abc import Mapping
from contextlib import AbstractAsyncContextManager, asynccontextmanager
from datetime import date, datetime
from functools import partial
import logging
from typing import Any, AsyncIterator, Callable

import httpx
import whois
from whois.exceptions import PywhoisError

from ..iocs import IOCType
from ..http_client import build_sync_http_client
from .base import BaseProvider, FeatureValue
from .systemdns import SystemDNSProvider
from .utils import (
    calculate_days_delta,
    first_non_null,
    normalize_latin_letters,
    normalize_status,
    normalize_text,
    parse_date,
)

logger = logging.getLogger(__name__)


class WhoisContext:
    """Контекст, предоставляющий функцию для WHOIS-запросов."""

    __slots__ = ("lookup",)

    def __init__(self, timeout: float) -> None:
        resolver = WhoisServerResolver(timeout=timeout)
        self.lookup: Callable[[str], Any] = partial(
            _run_whois_with_resolver,
            resolver=resolver,
            timeout=timeout,
        )


class WhoisServerResolver:
    """Разрешает адреса WHOIS-серверов через DoH."""

    def __init__(self, *, timeout: float) -> None:
        self.timeout = timeout
        self._doh_endpoint = f"{SystemDNSProvider.DOH_BASE_URL}{SystemDNSProvider.DOH_ENDPOINT}"

    def resolve_ipv4(self, hostname: str) -> str | None:
        logger.debug("Получаем IPv4 WHOIS-сервера %s через DoH", hostname)
        try:
            with build_sync_http_client(timeout=self.timeout) as client:
                response = client.get(
                    self._doh_endpoint,
                    params={"name": hostname, "type": "A"},
                    headers={
                        "Accept": "application/dns-json",
                        "User-Agent": "ioc-checker/systemdns",
                    },
                )
                response.raise_for_status()
                answers = response.json().get("Answer") or []
        except httpx.HTTPError:
            logger.warning(
                "Не удалось выполнить DoH-запрос для WHOIS-сервера %s", hostname
            )
            return None
        except Exception:
            logger.exception(
                "Ошибка при разрешении WHOIS-сервера %s через DoH", hostname
            )
            return None

        for answer in answers:
            ipv4 = answer.get("data")
            if ipv4:
                logger.info(
                    "WHOIS-сервер %s разрешён в %s, подключаемся по IP", hostname, ipv4
                )
                return ipv4

        logger.warning("DoH не вернул IPv4 для WHOIS-сервера %s", hostname)
        return None


class ResolvedNICClient(whois.NICClient):
    """NIC-клиент, перенаправляющий подключения на IPv4 адреса."""

    resolver: WhoisServerResolver | None = None

    def whois(
        self,
        query: str,
        hostname: str,
        flags: int,
        many_results: bool = False,
        quiet: bool = False,
        timeout: int = 10,
        ignore_socket_errors: bool = True,
    ) -> str:
        resolved_host = self._resolve_hostname(hostname)
        if resolved_host != hostname:
            logger.info(
                "Используем IPv4 %s вместо имени WHOIS-сервера %s", resolved_host, hostname
            )
        return super().whois(
            query,
            resolved_host,
            flags,
            many_results=many_results,
            quiet=quiet,
            timeout=timeout,
            ignore_socket_errors=ignore_socket_errors,
        )

    def _resolve_hostname(self, hostname: str) -> str:
        if self.resolver is None:
            return hostname

        ipv4 = self.resolver.resolve_ipv4(hostname)
        if ipv4:
            return ipv4

        logger.warning(
            "Не удалось получить IPv4 для WHOIS-сервера %s, используем исходное имя",
            hostname,
        )
        return hostname


def _run_whois_with_resolver(
    domain: str, *, resolver: WhoisServerResolver, timeout: float
) -> dict[str, Any]:
    original_nic_client = whois.NICClient
    ResolvedNICClient.resolver = resolver
    whois.NICClient = ResolvedNICClient
    try:
        return whois.whois(
            domain,
            command=False,
            inc_raw=False,
            quiet=True,
            convert_punycode=True,
            timeout=int(timeout),
        )
    finally:
        whois.NICClient = original_nic_client


class WhoisProvider(BaseProvider):
    """Получает информацию WHOIS для доменных индикаторов."""

    def __init__(self) -> None:
        """Создать провайдера WHOIS без требований к токену."""
        super().__init__("whois", requires_token=False)
        self._context = WhoisContext(self.http_timeout)

    def get_context(
        self, token: str | None = None
    ) -> AbstractAsyncContextManager[WhoisContext]:
        """Вернуть переиспользуемый контекст для WHOIS-запросов."""

        logger.debug("Создаём контекст WHOIS для повторного использования запросов")

        @asynccontextmanager
        async def _context() -> AsyncIterator[WhoisContext]:
            yield self._context

        return _context()

    async def fetch(self, indicator: IOCType, context: Any) -> Any | bool:
        """Запросить WHOIS и вернуть необработанный ответ библиотеки."""

        if indicator.category != "domain":
            logger.info(
                "WHOIS не поддерживает индикатор %s (%s)",
                indicator.value,
                indicator.category,
            )
            return False

        logger.info("Запрашиваем WHOIS для домена %s", indicator)

        lookup: Callable[[str], Any]
        if isinstance(context, WhoisContext):
            lookup = context.lookup
        else:
            logger.debug(
                "WHOIS вызывается без подготовленного контекста, используем запасной вариант"
            )
            lookup = WhoisContext(self.http_timeout).lookup

        try:
            raw_response = await asyncio.wait_for(
                asyncio.to_thread(
                    lookup,
                    indicator.value,
                ),
                timeout=self.http_timeout,
            )
        except PywhoisError as error:
            logger.info(
                "WHOIS не содержит данных для домена %s: %s",
                indicator,
                error,
            )
            return {}
        except Exception:
            logger.exception(
                "Не удалось получить WHOIS-данные для домена %s", indicator
            )
            return False

        serializable_response = _make_serializable(raw_response)
        logger.debug(
            "Подготовили ответ WHOIS к сохранению: %s", type(serializable_response)
        )

        return serializable_response

    def build_features(
        self,
        payload: Any,
        *,
        indicator: IOCType | None = None,
    ) -> dict[str, FeatureValue]:
        """Подготовить фичи на основе ответа WHOIS."""

        domain_name = first_non_null(payload.get("domain_name"))
        registrar = first_non_null(payload.get("registrar"))
        creation_date = parse_date(payload.get("creation_date"))
        expiration_date = parse_date(payload.get("expiration_date"))
        # status_str = payload.get("status")
        # emails = normalize_text(payload.get("emails"))
        # org = normalize_text(payload.get("org"))

        whois_exists = bool(
            domain_name
            or registrar
            or creation_date
            or expiration_date
            # or status_str
        )

        indicator_value = first_non_null(getattr(indicator, "value", indicator))
        whois_domain_match = bool(
            indicator_value
            and domain_name
            and domain_name.lower() == indicator_value.lower()
        )
        reg_period = calculate_days_delta(
            creation_date, expiration_date, label="регистрации домена"
        )

        # status_upper = status_str.upper()
        features = {
            "whois_exists": whois_exists and whois_domain_match,
            "whois_domain_match": whois_domain_match,
            # "whois_has_dates": whois_has_dates,
            "whois_reg_period_days": reg_period if whois_domain_match else None,
            "whois_registrar": (
                (
                    normalize_latin_letters(registrar.split()[0].lower())
                    if registrar
                    else str(None)
                )
                if whois_domain_match
                else str(None)
            ),
            # "whois_has_status": int(bool(status_str)),
            # "whois_status_registered": int("REGISTERED" in status_upper),
            # "whois_status_delegated": int("DELEGATED" in status_upper),
            # "whois_status_verified": int("VERIFIED" in status_upper),
            # "whois_status_not_delegated": int("NOT DELEGATED" in status_upper),
            # "whois_has_email": int(bool(emails)),
            # "whois_has_org": int(bool(org)),
        }

        return features


def _make_serializable(value: Any) -> Any:
    """Преобразовать результат WHOIS в JSON-совместимую структуру."""

    if isinstance(value, (datetime, date)):
        return value.isoformat()

    if isinstance(value, Mapping):
        return {key: _make_serializable(item) for key, item in value.items()}

    if isinstance(value, (list, tuple, set, frozenset)):
        return [_make_serializable(item) for item in value]

    return value
