"""Общие вспомогательные функции для расчёта фичей провайдеров."""

from __future__ import annotations

import logging
import re
import asyncio
from collections.abc import Mapping, Sequence
from contextlib import AbstractAsyncContextManager
from datetime import date, datetime, timezone
from numbers import Number
from typing import Any, Callable

import pandas as pd

from ..config import settings
from ..database import cache_result, get_cached_result
from ..iocs import IOCType
from .base import BaseProvider, FeatureValue

logger = logging.getLogger(__name__)

_STATUS_CODE_RE = re.compile(r"status code\s+(\d{3})", re.IGNORECASE)


def ensure_mapping(payload: Any, *, source: str) -> Mapping[str, Any]:
    """Вернуть отображение, подходящее для извлечения фич."""

    if isinstance(payload, Mapping):
        return payload
    logger.debug("Неожиданный формат данных %s: %r", source, payload)
    return {}


def coerce_optional_text(value: Any) -> str | None:
    """Привести значение к строке или вернуть ``None``."""

    if value is None:
        return None
    if isinstance(value, str):
        text = value.strip()
        return text or None
    text = str(value).strip()
    return text or None


def first_non_null(value: Any) -> Any:
    """Вернуть первый непустой элемент из последовательности либо исходное значение."""

    if isinstance(value, (list, tuple, set, frozenset)):
        for item in value:
            if item is not None:
                return item
        return None
    return value


def normalize_text(value: Any) -> str:
    """Нормализовать произвольное текстовое значение."""

    candidate = first_non_null(value)
    text = coerce_optional_text(candidate)
    return text or ""


def normalize_latin_letters(value: str) -> str:
    """Оставить в строке только латинские буквы."""

    normalized = "".join(char for char in value if "a" <= char <= "z")
    logger.debug("Нормализуем строку до латиницы: %s → %s", value, normalized)
    return normalized


def normalize_status(value: Any) -> str:
    """Привести статус к строковому представлению."""

    if isinstance(value, (list, tuple, set, frozenset)):
        parts = [str(item) for item in value if item]
        return ", ".join(parts)
    return normalize_text(value)


def parse_date(raw_date: Any) -> datetime | None:
    """Безопасно преобразовать дату WHOIS или схожих сервисов к ``datetime``."""

    candidate = first_non_null(raw_date)
    if isinstance(candidate, datetime):
        return _normalize_datetime(candidate)
    if isinstance(candidate, date):
        combined = datetime.combine(
            candidate,
            datetime.min.time(),
            tzinfo=timezone.utc,
        )
        logger.debug("Преобразуем дату без таймзоны в UTC: %s", combined)
        return combined
    if isinstance(candidate, str):
        text = candidate.strip()
        if not text:
            return None

        try:
            parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
            return _normalize_datetime(parsed)
        except ValueError:
            cleaned_text, tz_hint = _strip_unrecognized_timezone(text)
            if tz_hint:
                logger.debug("Отбрасываем неподдерживаемый часовой пояс %s из даты %s", tz_hint, text)

            parsed = pd.to_datetime(cleaned_text, utc=True, errors="coerce")
            if pd.isna(parsed):
                logger.debug("Не удалось распарсить дату: %s", text)
                return None
            if isinstance(parsed, pd.Timestamp):
                normalized = parsed.to_pydatetime()
                if normalized.tzinfo is None:
                    normalized = normalized.replace(tzinfo=timezone.utc)
                else:
                    normalized = normalized.astimezone(timezone.utc)
                logger.debug("Дата pandas нормализована к UTC: %s", normalized)
                return normalized
    return None


def calculate_days_delta(
    start: datetime | None, end: datetime | None, *, label: str
) -> int | None:
    """Посчитать продолжительность в днях между двумя датами."""

    if start and end:
        days = (end - start).days
        logger.debug("Рассчитали срок %s в днях: %s", label, days)
        return days

    logger.debug("Недостаточно данных для срока %s: %s - %s", label, start, end)
    return None


def _normalize_datetime(value: datetime) -> datetime:
    """Нормализовать ``datetime`` к UTC для последующих расчётов."""

    if value.tzinfo is None:
        normalized = value.replace(tzinfo=timezone.utc)
    else:
        normalized = value.astimezone(timezone.utc)

    logger.debug("Нормализуем дату %s к UTC: %s", value, normalized)
    return normalized


def _strip_unrecognized_timezone(text: str) -> tuple[str, str | None]:
    """Удалить финальный текстовый часовой пояс, который не знает pandas."""

    parts = text.rsplit(" ", maxsplit=1)
    if len(parts) != 2:
        return text, None

    body, tz_suffix = parts
    if not tz_suffix.isalpha():
        return text, None

    return body, tz_suffix


async def fetch_provider_enrichment(
    values: Sequence[str] | set[str],
    *,
    provider_name: str,
    provider: BaseProvider,
    context_getter: Callable[[], AbstractAsyncContextManager[Any]],
    indicator_factory: Callable[[str], IOCType],
) -> dict[str, Any]:
    """Запросить внешний провайдер для набора значений."""

    results: dict[str, Any] = {}
    tasks: list[asyncio.Task[None]] = []
    unique_values = sorted(set(values))
    concurrency = settings.enrichment_concurrency

    logger.debug(
        "Параллельное обогащение %d значений через %s с лимитом %d",
        len(unique_values),
        provider_name,
        concurrency,
    )

    async with context_getter() as provider_context:
        semaphore = asyncio.Semaphore(concurrency)

        async def _fetch_one(raw_value: str) -> None:
            indicator = indicator_factory(raw_value)
            cached = await get_cached_result(indicator.value, provider.name)
            if cached:
                logger.debug(
                    "Используем кэш %s для %s", provider.name, indicator.value
                )
                _, _cached_type, _cached_provider, cached_payload = cached
                features = provider.build_features(
                    cached_payload, indicator=indicator
                )
                results[raw_value] = features
                return

            async with semaphore:
                payload = await provider.fetch(indicator, provider_context)
                if not payload:
                    return
                features = provider.build_features(payload, indicator=indicator)
                results[raw_value] = features
                await cache_result(
                    indicator.value, provider.name, payload, indicator.category
                )

        for value in unique_values:
            tasks.append(asyncio.create_task(_fetch_one(value)))

        if tasks:
            await asyncio.gather(*tasks)

    logger.debug("%s-обогащение завершено: %s", provider_name, results)
    return results

