"""Поиск IOC в тексте с поддержкой Unicode-доменов."""

from __future__ import annotations

import logging
import re
import unicodedata
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable

import idna
import regex

from .domain import (
    DOMAINIndicator,
    OBFUSCATED_DOT_PATTERN,
    build_patterns as build_domain_patterns,
)
from .hash import HashIndicator, build_patterns as build_hash_patterns
from .ip import IPAddressIndicator, build_patterns as build_ip_patterns
from .patterns import PatternDefinition
from .url import URLIndicator, build_patterns as build_url_patterns

IOCValue = DOMAINIndicator | IPAddressIndicator | URLIndicator | HashIndicator

_DOT_NORMALIZER = regex.compile(
    OBFUSCATED_DOT_PATTERN,
    flags=regex.IGNORECASE | regex.VERBOSE,
)
_DOT_STRIPPER = re.compile(r"\s*\.\s*")

DEFAULT_TLD_PATH = Path(__file__).resolve().parents[2] / "lists" / "tlds.txt"


@dataclass(frozen=True)
class ParseResult:
    """Результат одиночного совпадения."""

    indicator: IOCValue
    pattern: str
    start: int
    end: int
    raw_value: str


@dataclass(slots=True)
class TLDRepository:
    """Хранилище доменных зон для нормализатора."""

    unicode_tlds: frozenset[str]
    alabel_tlds: frozenset[str]

    @classmethod
    def from_iterable(cls, tlds: Iterable[str]) -> TLDRepository:
        unicode_set: set[str] = set()
        alabel_set: set[str] = set()
        for raw in tlds:
            cleaned = raw.strip().strip(".")
            if not cleaned or cleaned.startswith("#"):
                continue
            lowered = cleaned.lower()
            unicode_set.add(lowered)
            try:
                encoded = idna.encode(lowered, uts46=True, std3_rules=True).decode(
                    "ascii"
                )
            except idna.IDNAError:
                continue
            alabel_set.add(encoded.lower())
        return cls(frozenset(unicode_set), frozenset(alabel_set))

    @classmethod
    def from_file(cls, path: str | Path) -> TLDRepository:
        file_path = Path(path)
        if not file_path.exists():
            logging.getLogger(__name__).warning(
                "Файл со списком TLD %s не найден", file_path
            )
            return cls(frozenset(), frozenset())
        content = file_path.read_text(encoding="utf-8").splitlines()
        return cls.from_iterable(content)

    def contains(self, unicode_tld: str, alabel_tld: str) -> bool:
        return unicode_tld in self.unicode_tlds or alabel_tld in self.alabel_tlds


class UnicodeDomainNormalizer:
    """Нормализует домены и фильтрует их по разрешённым зонам."""

    def __init__(self, repository: TLDRepository) -> None:
        self._repository = repository
        self._logger = logging.getLogger(f"{__name__}.UnicodeDomainNormalizer")

    def normalize(self, candidate: str) -> str:
        cleaned = _DOT_STRIPPER.sub(".", _DOT_NORMALIZER.sub(".", candidate))
        cleaned = cleaned.strip(". ")
        if not cleaned:
            return ""
        normalized = unicodedata.normalize("NFC", cleaned)
        try:
            alabel = idna.encode(normalized, uts46=True, std3_rules=True).decode(
                "ascii"
            )
            ulabel = idna.decode(alabel)
        except idna.IDNAError as error:
            self._logger.debug("Игнорируем домен %s: %s", cleaned, error)
            return ""
        last_unicode = ulabel.rsplit(".", 1)[-1].lower()
        last_alabel = alabel.rsplit(".", 1)[-1].lower()
        if not self._repository.contains(last_unicode, last_alabel):
            self._logger.debug(
                "Пропускаем домен %s — зона %s не разрешена", ulabel, last_unicode
            )
            return ""
        self._logger.debug(
            "Нормализованный домен %s преобразован в punycode %s",
            ulabel,
            alabel,
        )
        return alabel.lower()


def load_default_repository() -> TLDRepository:
    """Загрузить список TLD по умолчанию."""

    return TLDRepository.from_file(DEFAULT_TLD_PATH)


class ConsumedRanges:
    """Коллекция занятых диапазонов текста."""

    def __init__(self) -> None:
        self._ranges: list[tuple[int, int]] = []

    def is_blocked(self, start: int, end: int) -> bool:
        """Проверить, что диапазон пересекается с уже занятыми."""

        for left, right in self._ranges:
            if end <= left:
                break
            if start < right and end > left:
                return True
        return False

    def add(self, start: int, end: int) -> None:
        """Добавить новый занятый диапазон с объединением пересечений."""

        new_start = start
        new_end = end
        merged: list[tuple[int, int]] = []
        inserted = False
        for left, right in self._ranges:
            if right < new_start:
                merged.append((left, right))
                continue
            if new_end < left:
                if not inserted:
                    merged.append((new_start, new_end))
                    inserted = True
                merged.append((left, right))
                continue
            new_start = min(new_start, left)
            new_end = max(new_end, right)
        if not inserted:
            merged.append((new_start, new_end))
        self._ranges = merged


class TextIOCParser:
    """Поиск индикаторов компрометации в тексте."""

    def __init__(
        self,
        patterns: Iterable[PatternDefinition] | None = None,
        *,
        domain_normalizer: UnicodeDomainNormalizer | None = None,
    ) -> None:
        self._domain_normalizer = domain_normalizer or UnicodeDomainNormalizer(
            load_default_repository()
        )
        if patterns is None:
            self._patterns = tuple(self._build_default_patterns())
        else:
            self._patterns = tuple(patterns)
        self._logger = logging.getLogger(__name__)

    def parse(self, text: str) -> list[ParseResult]:
        """Вернуть список уникальных IOC, найденных в ``text``."""

        self._logger.debug("Запускаем разбор текста длиной %d", len(text))
        results: dict[IOCValue, ParseResult] = {}
        consumed = ConsumedRanges()

        for definition in self._patterns:
            for match in definition.regex.finditer(text):
                start, end = match.start(), match.end()
                raw_value = match.group(0)
                if consumed.is_blocked(start, end):
                    self._logger.debug(
                        "Пропускаем совпадение %s (позиция %d:%d) — диапазон уже занят",
                        raw_value,
                        start,
                        end,
                    )
                    continue
                rearmed = definition.rearm(raw_value)
                normalized = definition.normalize(rearmed)
                if not normalized:
                    continue
                if definition.validator and not definition.validator(normalized):
                    self._logger.debug(
                        "Фильтруем совпадение %s по правилу %s",
                        normalized,
                        definition.name,
                    )
                    continue
                indicator = definition.create_indicator(normalized)
                consumed.add(start, end)
                current = results.get(indicator)
                if current is None or start < current.start:
                    results[indicator] = ParseResult(
                        indicator=indicator,
                        pattern=definition.name,
                        start=start,
                        end=end,
                        raw_value=raw_value,
                    )
        ordered = sorted(
            results.values(), key=lambda item: (item.start, item.indicator.value)
        )
        self._logger.info("Найдено %d IOC", len(ordered))
        return ordered

    def _build_default_patterns(self) -> tuple[PatternDefinition, ...]:
        """Сформировать набор правил по умолчанию."""

        return (
            *build_url_patterns(),
            *build_domain_patterns(self._domain_normalizer.normalize),
            *build_ip_patterns(),
            *build_hash_patterns(),
        )
