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

from __future__ import annotations

import asyncio
from collections.abc import Mapping
from contextlib import AbstractAsyncContextManager, asynccontextmanager
from datetime import date, datetime
import logging
import socket
from typing import Any, AsyncIterator
from urllib.parse import urlparse

from whois import WhoisEntry, extract_domain
from whois.exceptions import PywhoisError
from whois.whois import NICClient

from ..iocs import IOCType
from .base import BaseProvider, FeatureValue
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__ = ("client",)

    def __init__(self, proxy_url: str | None, timeout: float) -> None:
        self.client = None
        if proxy_url:
            try:
                self.client = ProxyWhoisClient(proxy_url, timeout)
            except Exception:
                logger.exception("Не удалось подготовить WHOIS-клиент через прокси")


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

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

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

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

        @asynccontextmanager
        async def _context() -> AsyncIterator[WhoisContext]:
            if self._context is None:
                self._context = WhoisContext(self.http_proxy, self.http_timeout)
            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_client: ProxyWhoisClient | None = None
        if isinstance(context, WhoisContext):
            lookup_client = context.client
        if lookup_client is None:
            logger.error("WHOIS невозможен: прокси не настроен или не инициализирован")
            return False

        try:
            raw_response = await asyncio.wait_for(
                asyncio.to_thread(
                    lookup_client.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


class ProxyWhoisClient:
    """Простой клиент WHOIS, работающий через HTTP(S)-прокси."""

    __slots__ = ("proxy_host", "proxy_port", "timeout")

    def __init__(self, proxy_url: str, timeout: float) -> None:
        self.proxy_host, self.proxy_port = _parse_proxy_url(proxy_url)
        self.timeout = timeout

    def lookup(self, domain: str) -> WhoisEntry:
        """Получить ответ WHOIS, используя туннель CONNECT."""

        query = extract_domain(domain)
        normalized_query = query.encode("idna").decode("utf-8")

        logger.debug(
            "Открываем WHOIS через прокси %s:%s для домена %s",
            self.proxy_host,
            self.proxy_port,
            normalized_query,
        )

        nic_client = ProxyNICClient(
            self.proxy_host,
            self.proxy_port,
            timeout=self.timeout,
        )
        whois_server = nic_client.choose_server(normalized_query)
        if not whois_server:
            raise PywhoisError(
                f"Не удалось подобрать WHOIS-сервер для {normalized_query}"
            )

        response_text = nic_client.whois_via_proxy(
            normalized_query,
            whois_server,
            NICClient.WHOIS_RECURSE,
            quiet=True,
        )
        if not response_text:
            raise PywhoisError(
                f"WHOIS-сервер {whois_server} не вернул данных для {normalized_query}"
            )

        return WhoisEntry.load(normalized_query, response_text)


class ProxyNICClient(NICClient):
    """Клиент WHOIS, пробрасывающий запросы через CONNECT."""

    __slots__ = ("proxy_host", "proxy_port", "timeout")

    def __init__(self, proxy_host: str, proxy_port: int, *, timeout: float) -> None:
        super().__init__()
        self.proxy_host = proxy_host
        self.proxy_port = proxy_port
        self.timeout = timeout

    def whois_via_proxy(
        self,
        query: str,
        hostname: str,
        flags: int,
        many_results: bool = False,
        quiet: bool = False,
    ) -> str:
        """Отправить запрос WHOIS на указанный сервер через прокси."""

        response = b""
        sock = self._open_tunnel(hostname)
        try:
            prepared_query = self._prepare_query(hostname, query, many_results)
            sock.sendall(prepared_query + b"\r\n")

            while True:
                chunk = sock.recv(4096)
                response += chunk
                if not chunk:
                    break

            response_str = response.decode("utf-8", "replace")
            if 'with "=xxx"' in response_str:
                return self.whois_via_proxy(
                    query,
                    hostname,
                    flags,
                    True,
                    quiet=quiet,
                )

            next_host = None
            if flags & NICClient.WHOIS_RECURSE and next_host is None:
                next_host = self.findwhois_server(response_str, hostname, query)
            if next_host:
                response_str += self.whois_via_proxy(
                    query,
                    next_host,
                    0,
                    quiet=quiet,
                )
        except socket.error as error:
            if not quiet:
                logger.error(
                    "Ошибка сокета при обращении к WHOIS %s через %s:%s: %s",
                    hostname,
                    self.proxy_host,
                    self.proxy_port,
                    error,
                )
            raise
        finally:
            sock.close()

        return response_str

    def _open_tunnel(self, hostname: str) -> socket.socket:
        connect_request = (
            f"CONNECT {hostname}:43 HTTP/1.1\r\n"
            f"Host: {hostname}:43\r\n"
            "\r\n"
        ).encode()

        sock = socket.create_connection(
            (self.proxy_host, self.proxy_port), timeout=self.timeout
        )
        sock.settimeout(self.timeout)
        sock.sendall(connect_request)

        proxy_response = sock.recv(4096)
        status_line = proxy_response.split(b"\r\n", 1)[0]
        if b" 200" not in status_line:
            sock.close()
            raise ConnectionError(
                f"Прокси {self.proxy_host}:{self.proxy_port} отклонил туннель: {status_line!r}"
            )

        return sock

    def _prepare_query(self, hostname: str, query: str, many_results: bool) -> bytes:
        if hostname == NICClient.DENICHOST:
            query_bytes = "-T dn,ace -C UTF-8 " + query
        elif hostname == NICClient.DK_HOST:
            query_bytes = " --show-handles " + query
        elif hostname.endswith(".jp"):
            query_bytes = query + "/e"
        elif hostname.endswith(NICClient.QNICHOST_TAIL) and many_results:
            query_bytes = "=" + query
        else:
            query_bytes = query
        return query_bytes.encode("utf-8")


def _parse_proxy_url(proxy_url: str) -> tuple[str, int]:
    parsed = urlparse(proxy_url)
    if not parsed.hostname or not parsed.port:
        raise ValueError(f"Неверный адрес прокси: {proxy_url}")

    return parsed.hostname, parsed.port


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
