from __future__ import annotations

from dataclasses import dataclass, field
import logging
from pathlib import Path
from typing import Any, Dict, Iterable

import tomllib


logger = logging.getLogger(__name__)


@dataclass
class DomainAnalysisConfig:
    """Конфигурация доменного пайплайна анализа."""

    files_bucket_block_threshold: int = 8
    block_substrings: list[str] = field(default_factory=lambda: ["cdn"])
    feature_contributions_top_n: int = 5
    model_threshold: float = 0.5


@dataclass
class IPAnalysisConfig:
    """Настройки упрощённой стратегии анализа IP-адресов."""

    passive_dns_safe_threshold: int = 50
    providers: list[str] = field(
        default_factory=lambda: [
            "cloudflare",
            "akamai",
            "fastly",
            "amazon",
            "aws",
            "google",
            "gcp",
            "azure",
            "microsoft",
            "digitalocean",
            "hetzner",
            "ovh",
            "linode",
            "vultr",
        ]
    )
    cdn_usage_types: list[str] = field(
        default_factory=lambda: ["content delivery network"]
    )


@dataclass
class AnalysisConfig:
    """Глобальные настройки пайплайна анализа индикаторов."""

    domain: DomainAnalysisConfig = field(default_factory=DomainAnalysisConfig)
    ip: IPAnalysisConfig = field(default_factory=IPAnalysisConfig)


@dataclass
class Settings:
    worker_count: int = 2
    log_level: str = "DEBUG"
    log_file_enabled: bool = False
    log_file_path: str = "ioc-checker.log"
    enrichment_concurrency: int = 5
    http_timeout: float = 15.0
    http_proxy: str | None = None
    http_proxy_skip_verify: bool = True
    api_host: str = "127.0.0.1"
    api_port: int = 8001
    providers: list[str] = field(
        default_factory=lambda: [
            "abuseipdb",
            "dbip",
            "whois",
            "kaspersky",
            "top1m",
        ]
    )
    database_url: str = "sqlite+aiosqlite:///./cache.db"
    tokens: Dict[str, list[str]] = field(default_factory=dict)
    systemdns_nameservers: list[str] | None = None
    analysis: AnalysisConfig = field(default_factory=AnalysisConfig)

    def _resolve_token_pool(self, token_name: str) -> list[str]:
        """Вернуть пул токенов для провайдера."""

        normalized = token_name.strip().lower()
        if not normalized:
            logger.debug("Пустое имя токена при запросе пула")
            return []
        pool = self.tokens.get(normalized)
        if not pool:
            logger.debug("Токены для %s не настроены", token_name)
            return []
        return pool

    def get_token_for_worker(
        self, token_name: str, worker_index: int | None = None
    ) -> str | None:
        """Выбрать токен для провайдера с учётом индекса воркера."""

        pool = self._resolve_token_pool(token_name)
        if not pool:
            return None
        if worker_index is None:
            chosen_index = 0
        else:
            chosen_index = worker_index % len(pool)
        token = pool[chosen_index]
        logger.debug(
            "Выбран слот %d из %d для токенов %s воркером %s",
            chosen_index,
            len(pool),
            token_name,
            "default" if worker_index is None else worker_index,
        )
        return token

    def has_tokens(self, token_name: str) -> bool:
        """Проверить наличие хотя бы одного токена для провайдера."""

        return bool(self._resolve_token_pool(token_name))


def _normalize_tokens(raw: Dict[str, object]) -> Dict[str, list[str]]:
    tokens: Dict[str, list[str]] = {}
    for name, value in raw.items():
        if not isinstance(name, str):
            logger.debug("Игнорируется ключ токена с нестроковым именем: %r", name)
            continue
        normalized_name = name.strip().lower()
        if not normalized_name:
            logger.debug("Игнорируется токен с пустым именем")
            continue
        if isinstance(value, Iterable) and not isinstance(value, (str, dict)):
            entries: list[str] = []
            for entry in value:
                if not isinstance(entry, str):
                    logger.debug(
                        "Игнорируется небуквенный элемент токена %s: %r",
                        normalized_name,
                        entry,
                    )
                    continue
                cleaned = entry.strip()
                if cleaned:
                    entries.append(cleaned)
            if not entries:
                logger.debug(
                    "Игнорируются токены провайдера %s: список пуст", normalized_name
                )
                continue
            tokens[normalized_name] = entries
            continue
        logger.warning("Игнорируется токен %s: ожидается массив строк", normalized_name)
    return tokens


def _normalize_nameservers(value: object) -> list[str] | None:
    nameservers: list[str] = []
    if value is None:
        return None
    if isinstance(value, str):
        cleaned = value.strip()
        if cleaned:
            nameservers.append(cleaned)
        return nameservers or None
    if isinstance(value, Iterable) and not isinstance(value, dict):
        for entry in value:
            if not isinstance(entry, str):
                logger.warning("Игнорируется нестроковое значение DNS-сервера: %r", entry)
                continue
            cleaned = entry.strip()
            if cleaned:
                nameservers.append(cleaned)
        return nameservers or None
    logger.warning(
        "Неподдерживаемое значение списка DNS-серверов systemdns в конфигурации: %r",
        value,
    )
    return None


def _collect_string_list(value: object, *, field_name: str) -> list[str]:
    """Извлекает список строк из конфигурационного значения."""

    collected: list[str] = []
    if value is None:
        return collected
    if isinstance(value, str):
        candidate = value.strip()
        if candidate:
            collected.append(candidate)
        else:
            logger.warning("Игнорируется пустая строка в параметре %s", field_name)
        return collected
    if isinstance(value, Iterable) and not isinstance(value, dict):
        for entry in value:
            if not isinstance(entry, str):
                logger.warning(
                    "Игнорируется небуквенное значение в %s: %r", field_name, entry
                )
                continue
            cleaned = entry.strip()
            if cleaned:
                collected.append(cleaned)
            else:
                logger.warning("Игнорируется пустая строка в списке %s", field_name)
        return collected
    logger.warning("Игнорируется неподдерживаемый тип %s: %r", field_name, value)
    return collected
def _parse_domain_analysis(section: object) -> DomainAnalysisConfig:
    config = DomainAnalysisConfig()
    if section is None:
        return config
    if not isinstance(section, dict):
        logger.warning(
            "Игнорируется секция анализа доменов: ожидалась таблица, получено %r",
            section,
        )
        return config

    bucket_value = section.get("files_bucket_block_threshold")
    if bucket_value is not None:
        try:
            bucket_int = int(bucket_value)
            if bucket_int < 0:
                raise ValueError
            config.files_bucket_block_threshold = bucket_int
        except (TypeError, ValueError):
            logger.warning(
                "Некорректное значение files_bucket_block_threshold: %r",
                bucket_value,
            )

    substrings_value = section.get("block_substrings")
    collected = _collect_string_list(
        substrings_value, field_name="analysis.domain.block_substrings"
    )
    if collected:
        config.block_substrings = collected

    top_n_value = section.get("feature_contributions_top_n")
    if top_n_value is not None:
        try:
            top_n_int = int(top_n_value)
            if top_n_int < 0:
                raise ValueError
            config.feature_contributions_top_n = top_n_int
        except (TypeError, ValueError):
            logger.warning(
                "Некорректное значение feature_contributions_top_n: %r",
                top_n_value,
            )

    threshold_value = section.get("model_threshold")
    if threshold_value is not None:
        try:
            threshold = float(threshold_value)
            if not 0.0 <= threshold <= 1.0:
                raise ValueError
            config.model_threshold = threshold
        except (TypeError, ValueError):
            logger.warning(
                "Некорректное значение model_threshold для доменов: %r",
                threshold_value,
            )

    return config


def _parse_ip_analysis(section: object) -> IPAnalysisConfig:
    config = IPAnalysisConfig()
    if section is None:
        return config
    if not isinstance(section, dict):
        logger.warning(
            "Игнорируется секция анализа IP: ожидалась таблица, получено %r",
            section,
        )
        return config

    threshold_value = section.get("passive_dns_safe_threshold")
    if threshold_value is not None:
        try:
            numeric = int(threshold_value)
            if numeric < 0:
                raise ValueError
        except (TypeError, ValueError):
            logger.warning(
                "Порог passive_dns_safe_threshold в конфигурации IP имеет некорректное значение: %r",
                threshold_value,
            )
        else:
            config.passive_dns_safe_threshold = numeric

    providers_list = _collect_string_list(
        section.get("providers"), field_name="analysis.ip.providers"
    )
    if providers_list:
        config.providers = [entry.lower() for entry in providers_list]

    usage_list = _collect_string_list(
        section.get("cdn_usage_types"), field_name="analysis.ip.cdn_usage_types"
    )
    if usage_list:
        config.cdn_usage_types = [entry.lower() for entry in usage_list]

    return config


def _parse_analysis(section: object) -> AnalysisConfig:
    if section is None:
        return AnalysisConfig()
    if not isinstance(section, dict):
        logger.warning(
            "Игнорируется секция analysis в конфигурации: ожидалась таблица, получено %r",
            section,
        )
        return AnalysisConfig()

    domain_section = section.get("domain")
    domain_config = _parse_domain_analysis(domain_section)
    ip_section = section.get("ip")
    ip_config = _parse_ip_analysis(ip_section)
    return AnalysisConfig(domain=domain_config, ip=ip_config)


def _load_tokens(base: Path) -> Dict[str, list[str]]:
    """Загрузить токены провайдеров из ``secrets.toml``."""

    secrets_path = base / "secrets.toml"
    if not secrets_path.exists():
        logger.info("Файл %s не найден — токены провайдеров отсутствуют", secrets_path)
        return {}
    try:
        raw_text = secrets_path.read_text()
        data = tomllib.loads(raw_text) if raw_text.strip() else {}
    except OSError as exc:
        logger.warning("Не удалось прочитать %s: %s", secrets_path, exc)
        return {}
    except tomllib.TOMLDecodeError as exc:
        logger.warning("Ошибка парсинга TOML в %s: %s", secrets_path, exc)
        return {}
    if not isinstance(data, dict):
        logger.warning("Игнорируется содержимое %s: ожидается таблица", secrets_path)
        return {}
    tokens_section = data.get("tokens")
    if not isinstance(tokens_section, dict):
        logger.warning(
            "Игнорируется файл %s: раздел tokens отсутствует или имеет некорректный формат",
            secrets_path,
        )
        return {}
    logger.debug("Загружена секция tokens из файла секретов")
    return _normalize_tokens(tokens_section)


def load_settings() -> Settings:
    base = Path(__file__).resolve().parent.parent
    path = base / "config.toml"
    data: Dict[str, Any] = {}
    if path.exists():
        logger.debug("Загружаем конфигурацию из %s", path)
        data = tomllib.loads(path.read_text())
    if not isinstance(data.get("providers"), list):
        data.pop("providers", None)

    log_to_file = data.get("log_file_enabled")
    if isinstance(log_to_file, bool):
        data["log_file_enabled"] = log_to_file
    elif log_to_file is not None:
        logger.warning(
            "Некорректное значение log_file_enabled в конфигурации: %r", log_to_file
        )
        data.pop("log_file_enabled", None)

    log_file_path = data.get("log_file_path")
    if log_file_path is not None:
        if isinstance(log_file_path, str) and log_file_path.strip():
            resolved_log_path = Path(log_file_path.strip())
            if not resolved_log_path.is_absolute():
                resolved_log_path = base / resolved_log_path
            data["log_file_path"] = str(resolved_log_path)
        else:
            logger.warning(
                "Некорректное значение log_file_path в конфигурации: %r", log_file_path
            )
            data.pop("log_file_path", None)

    analysis_section = data.pop("analysis", None)
    analysis_config = _parse_analysis(analysis_section)

    systemdns_section = data.pop("systemdns", None)
    if isinstance(systemdns_section, dict):
        nameservers_value = systemdns_section.get("nameservers")
    else:
        if systemdns_section is not None:
            logger.warning(
                "Некорректная секция systemdns: ожидается таблица, получено %r",
                systemdns_section,
            )
        nameservers_value = data.get("systemdns_nameservers")

    if "systemdns_nameservers" in data and not isinstance(
        data.get("systemdns_nameservers"), list
    ):
        nameservers_value = data.pop("systemdns_nameservers")

    normalized_nameservers = _normalize_nameservers(nameservers_value)
    if normalized_nameservers is not None:
        data["systemdns_nameservers"] = normalized_nameservers

    timeout_value = data.get("http_timeout")
    if timeout_value is not None:
        try:
            timeout = float(timeout_value)
            if timeout <= 0:
                raise ValueError
            data["http_timeout"] = timeout
        except (TypeError, ValueError):
            logger.warning(
                "Недопустимое значение http_timeout в конфигурации: %r; используем значение по умолчанию",
                timeout_value,
            )
            data.pop("http_timeout", None)

    proxy_value = data.get("http_proxy")
    if proxy_value is not None:
        if isinstance(proxy_value, str):
            cleaned_proxy = proxy_value.strip()
            if cleaned_proxy:
                data["http_proxy"] = cleaned_proxy
            else:
                logger.warning(
                    "В конфигурации указан пустой http_proxy; используется значение по умолчанию"
                )
                data.pop("http_proxy", None)
        else:
            logger.warning(
                "Недопустимое значение http_proxy в конфигурации: %r", proxy_value
            )
            data.pop("http_proxy", None)

    skip_verify_value = data.get("http_proxy_skip_verify")
    if skip_verify_value is not None:
        if isinstance(skip_verify_value, bool):
            data["http_proxy_skip_verify"] = skip_verify_value
        elif isinstance(skip_verify_value, str):
            normalized = skip_verify_value.strip().lower()
            if normalized in {"1", "true", "yes", "on"}:
                data["http_proxy_skip_verify"] = True
            elif normalized in {"0", "false", "no", "off"}:
                data["http_proxy_skip_verify"] = False
            else:
                logger.warning(
                    "Недопустимое строковое значение http_proxy_skip_verify: %r",
                    skip_verify_value,
                )
                data.pop("http_proxy_skip_verify", None)
        else:
            logger.warning(
                "Недопустимый тип http_proxy_skip_verify: %r", skip_verify_value
            )
            data.pop("http_proxy_skip_verify", None)

    concurrency_value = data.get("enrichment_concurrency")
    if concurrency_value is not None:
        try:
            concurrency = int(concurrency_value)
            if concurrency < 1:
                raise ValueError
            data["enrichment_concurrency"] = concurrency
        except (TypeError, ValueError):
            logger.warning(
                "Некорректное значение enrichment_concurrency: %r",
                concurrency_value,
            )
            data.pop("enrichment_concurrency", None)

    api_section = data.pop("api", None)
    if isinstance(api_section, dict):
        host_value = api_section.get("host")
        if isinstance(host_value, str):
            cleaned_host = host_value.strip()
            if cleaned_host:
                data["api_host"] = cleaned_host
        elif host_value is not None:
            logger.warning(
                "Некорректное значение api.host в конфигурации: %r; используется значение по умолчанию",
                host_value,
            )
        port_value = api_section.get("port")
        if port_value is not None:
            try:
                port = int(port_value)
                if not 0 < port < 65536:
                    raise ValueError
                data["api_port"] = port
            except (TypeError, ValueError):
                logger.warning(
                    "Некорректное значение api.port в конфигурации: %r; используется значение по умолчанию",
                    port_value,
                )
    elif api_section is not None:
        logger.warning("Игнорируется некорректная конфигурация api: ожидается таблица")

    data["tokens"] = _load_tokens(base)
    settings_obj = Settings(**data, analysis=analysis_config)
    logger.info(
        "Конфигурация загружена: провайдеры=%s воркеры=%s",  # token values intentionally omitted
        settings_obj.providers,
        settings_obj.worker_count,
    )
    logger.debug(
        "Параметры API: host=%s port=%s",
        settings_obj.api_host,
        settings_obj.api_port,
    )
    logger.debug(
        "Токены настроены для провайдеров: %s",
        sorted(settings_obj.tokens.keys()),
    )
    logger.debug("Таймаут HTTP настроен на %.2f секунды", settings_obj.http_timeout)
    if settings_obj.http_proxy:
        logger.debug("HTTP прокси включён: %s", settings_obj.http_proxy)
    else:
        logger.debug("HTTP прокси не задан")
    if settings_obj.http_proxy_skip_verify:
        logger.warning(
            "Проверка TLS-сертификатов отключена глобально; используйте режим только в доверенной среде"
        )
    return settings_obj


settings = load_settings()


def configure_logging() -> None:
    """Configure the global logging setup according to the settings."""

    level_name = settings.log_level.upper()
    level = getattr(logging, level_name, logging.INFO)
    handlers: list[logging.Handler] = [logging.StreamHandler()]

    log_path: Path | None = None
    if settings.log_file_enabled:
        log_path = Path(settings.log_file_path)
        try:
            log_path.parent.mkdir(parents=True, exist_ok=True)
        except OSError as exc:
            logger.warning(
                "Не удалось создать каталог для файла логов %s: %s", log_path, exc
            )
        else:
            try:
                file_handler = logging.FileHandler(log_path, encoding="utf-8")
            except OSError as exc:
                logger.warning(
                    "Не удалось открыть файл логов %s: %s", log_path, exc
                )
            else:
                handlers.append(file_handler)
                logger.debug("Файловый лог активирован: %s", log_path)

    logging.basicConfig(
        level=level,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        handlers=handlers,
        force=True,
    )
    logger.info("Настроено логирование: уровень %s", level_name)
    if log_path:
        if any(isinstance(handler, logging.FileHandler) for handler in handlers):
            logger.info("Логи пишутся в файл: %s", log_path)
        else:
            logger.warning("Файловый лог не активирован из-за ошибок инициализации")
    if settings.http_proxy:
        logger.info(
            "HTTP(S)-прокси для внешних запросов: %s",
            settings.http_proxy,
        )
    else:
        logger.info("HTTP(S)-прокси для внешних запросов не настроен")


configure_logging()
