"""Normalization utilities for match preprocessing."""

from __future__ import annotations

import re
import unicodedata as ud
from pathlib import Path
from typing import Iterable, List

import idna

_DOMAIN_SCHEME_RE = re.compile(r"^[a-z]+://", re.IGNORECASE)
_WWW_PREFIX_RE = re.compile(r"^www\\.", re.IGNORECASE)
_COMPANY_SUFFIX_RE = re.compile(
    r"\b(?:incorporated|inc|llc|l\\.l\\.c|l\\.l\\.c\\.|limited|ltd|co|company|corp|corporation|gmbh|s\\.?a\\.?|ag|bv|pte|plc|pty|sdn bhd|llp|lp|partners|holdings|group)\b\\.?",
    re.IGNORECASE,
)

COMPANY_STOPWORDS = {
    "group",
    "holding",
    "holdings",
    "international",
    "associates",
    "グループ",
    "홀딩스",
    "控股",
    "集团",
    "集團",
}
_COMPANY_MARKER_SUBSTRINGS = [
    "株式会社",
    "有限会社",
    "有限責任会社",
    "有限責任公司",
    "股份有限公司",
    "有限公司",
    "有限责任公司",
    "公司",
    "控股",
    "集团",
    "集團",
    "주식회사",
    "유한회사",
    "ООО",
    "АО",
    "ЗАО",
    "ПАО",
    "ОАО",
]
_SUFFIX_TOKENS = {
    "inc",
    "incorporated",
    "llc",
    "l.l.c",
    "l.l.c.",
    "limited",
    "ltd",
    "co",
    "company",
    "corp",
    "corporation",
    "gmbh",
    "sa",
    "s.a",
    "ag",
    "bv",
    "pte",
    "plc",
    "pty",
    "sdn",
    "bhd",
    "llp",
    "lp",
    "spa",
    "srl",
    "kk",
    "holdings",
    "holding",
    "ooo",
    "zao",
    "ao",
    "pao",
    "оао",
    "ооо",
    "ао",
    "компания",
    "kabushikigaisha",
    "kabushiki",
    "有限責任公司",
    "股份有限公司",
    "有限公司",
    "集团",
    "集團",
    "控股",
    "株式会社",
    "有限会社",
    "주식회사",
    "유한회사",
}
_SUFFIX_PHRASES = [
    ("limited", "liability", "company"),
    ("public", "limited", "company"),
    ("societe", "anonyme"),
    ("societe", "a", "responsabilite", "limitee"),
]
_SUFFIX_TOKENS = {ud.normalize("NFKC", token).casefold() for token in _SUFFIX_TOKENS}
_COMPANY_MARKER_SUBSTRINGS = [
    ud.normalize("NFKC", marker).casefold() for marker in _COMPANY_MARKER_SUBSTRINGS
]
_COMPANY_STOPWORDS_CF = {
    ud.normalize("NFKC", word).casefold() for word in COMPANY_STOPWORDS
}
_SUFFIX_PHRASES = [
    tuple(ud.normalize("NFKC", part).casefold() for part in phrase)
    for phrase in _SUFFIX_PHRASES
]
DEFAULT_CORP_MARKER_PATTERN = (
    r"\b(?:inc|llc|corp|co\\.?|ltd|gmbh|ag|s\\.a\\.|sa|pty|plc|spa|srl|kk)\\b"
)
_NON_CAPTURING_GROUP_RE = re.compile(r"(?<!\\)\((?!\?)")
_FKA_PATTERN = re.compile(
    r"\s*(?:[-\u2013\u2014]?\s*)?\((?:F[./\s]*K[./\s]*A|formerly\s+known\s+as)[^)]*\)",
    re.IGNORECASE,
)
_FKA_CAPTURE_PATTERN = re.compile(
    r"\((?:F[./\s]*K[./\s]*A|formerly\s+known\s+as)\s+(?P<historical>[^)]+)\)",
    re.IGNORECASE,
)


def _ensure_non_capturing(pattern: str) -> str:
    """Convert plain capturing groups to non-capturing to avoid pandas warnings."""
    return _NON_CAPTURING_GROUP_RE.sub("(?:", pattern)


__all__ = [
    "to_domain",
    "normalize_company_name",
    "strip",
    "strip_formerly_known_as",
    "split_formerly_known_as",
    "apply_transforms",
    "apply_transforms_list",
    "COMPANY_STOPWORDS",
    "load_corp_markers",
]


def to_domain(value: str | None) -> str | None:
    """Normalize a value to its registrable domain (eTLD+1).

    Args:
        value: Raw value that may be a URL, email, or bare domain.

    Returns:
        The normalized domain in ASCII (lowercase) or ``None`` if it cannot be derived.
    """
    if not value:
        return None

    candidate = str(value).strip().lower()
    if not candidate:
        return None

    # Strip scheme (http://, https://, etc.)
    candidate = _DOMAIN_SCHEME_RE.sub("", candidate)

    # Remove credentials or fragments
    candidate = candidate.split("@")[-1]
    candidate = candidate.split("#", 1)[0]
    candidate = candidate.split("?", 1)[0]

    # Keep host portion only
    candidate = candidate.split("/", 1)[0]

    # Remove leading www.
    candidate = _WWW_PREFIX_RE.sub("", candidate)

    if not candidate:
        return None

    try:
        ascii_domain = idna.encode(candidate, uts46=True).decode("ascii")
    except Exception:
        ascii_domain = candidate

    parts = ascii_domain.split(".")
    if len(parts) >= 2:
        return ".".join(parts[-2:])
    return ascii_domain or None


def _strip_accents_if_latin(token: str) -> str:
    if not token:
        return token
    has_latin = any("LATIN" in ud.name(ch, "") for ch in token if ch.isalpha())
    if not has_latin:
        return token
    return "".join(ch for ch in ud.normalize("NFKD", token) if ud.category(ch) != "Mn")


def _remove_company_markers(text: str) -> str:
    updated = text
    for marker in _COMPANY_MARKER_SUBSTRINGS:
        while updated.startswith(marker):
            updated = updated[len(marker) :]
        while updated.endswith(marker):
            updated = updated[: -len(marker)]
    return updated.strip()


def _to_lower(value: str | None) -> str:
    if value is None:
        return ""
    return str(value).lower()


def _normalize_company_name(value: str | None) -> str:
    if value is None:
        return ""

    text = str(value).strip()
    if not text:
        return ""

    text = ud.normalize("NFKC", text)
    lowered = text.casefold()

    lowered = _remove_company_markers(lowered)

    lowered = re.sub(r"\s*\([^)]*(?:fka|aka)[^)]*\)", "", lowered, flags=re.IGNORECASE)
    lowered = re.sub(r"\s+(?:fka|aka)\s+.*$", "", lowered, flags=re.IGNORECASE)

    lowered = re.sub(r"(?<=\w)'s\b", "s", lowered, flags=re.UNICODE)

    lowered = re.sub(r"[^\w\s]", " ", lowered, flags=re.UNICODE)
    lowered = lowered.replace("_", " ")
    lowered = re.sub(r"\s+", " ", lowered, flags=re.UNICODE).strip()

    if not lowered:
        return ""

    tokens = lowered.split(" ")

    while tokens and tokens[0] in _SUFFIX_TOKENS:
        tokens.pop(0)

    trimmed = tokens[:]
    changed = True
    while changed and trimmed:
        changed = False
        while trimmed and trimmed[-1] in _SUFFIX_TOKENS:
            trimmed.pop()
            changed = True
        for phrase in _SUFFIX_PHRASES:
            if len(trimmed) >= len(phrase) and tuple(trimmed[-len(phrase) :]) == phrase:
                del trimmed[-len(phrase) :]
                changed = True

    cleaned_tokens = []
    for token in trimmed:
        if not token or token in _SUFFIX_TOKENS or token in _COMPANY_STOPWORDS_CF:
            continue
        if token.isdigit():
            continue
        cleaned_token = _strip_accents_if_latin(token)
        if cleaned_token:
            cleaned_tokens.append(cleaned_token)

    result = " ".join(cleaned_tokens).strip()
    return result


def normalize_company_name(value: str | None) -> str:
    return _normalize_company_name(value)

    return _normalize_company_name(value)


def strip_formerly_known_as(value: str | None) -> str | None:
    """Remove trailing `(FKA ...)` or `(formerly known as ...)` clauses from display strings."""
    if not isinstance(value, str):
        return value
    cleaned = _FKA_PATTERN.sub("", value).strip()
    if not cleaned:
        return value.strip() if isinstance(value, str) else value
    return re.sub(r"\s+", " ", cleaned)


def split_formerly_known_as(value: str | None) -> tuple[str, list[str]]:
    """Return `(display, [historical, ...])` extracted from an FKA-style label."""
    if not isinstance(value, str):
        return value or "", []

    def _explode(candidate: str) -> list[str]:
        spans = []
        normalized = re.sub(r"\s+", " ", candidate.strip())
        if not normalized:
            return spans
        lowered = normalized.lower()
        if (
            ";" in normalized
            or "/" in normalized
            or "|" in normalized
            or " or " in lowered
        ):
            parts = re.split(
                r"\s*(?:;|/|\||\bor\b)\s*", normalized, flags=re.IGNORECASE
            )
        else:
            parts = [normalized]
        for part in parts:
            cleaned = re.sub(r"\s+", " ", part.strip(" \"'"))
            if cleaned:
                spans.append(cleaned)
        return spans

    historicals: list[str] = []
    for match in _FKA_CAPTURE_PATTERN.finditer(value):
        candidate = match.group("historical")
        if not isinstance(candidate, str):
            continue
        for item in _explode(candidate):
            historicals.append(item)

    seen = set()
    deduped: list[str] = []
    for item in historicals:
        lowered = item.lower()
        if lowered in seen:
            continue
        seen.add(lowered)
        deduped.append(item)

    display = strip_formerly_known_as(value) or value.strip()
    cleaned_display = re.sub(r"\s+", " ", display).strip() if display else ""
    return cleaned_display, deduped


def strip(x: str | None) -> str | None:
    """Return the value stripped of surrounding whitespace when possible."""
    return x.strip() if isinstance(x, str) else x


def _strip(value: str | None) -> str:
    if value is None:
        return ""
    stripped = strip(value)
    if stripped is None:
        return ""
    return str(stripped)


TRANSFORM_REGISTRY = {
    "to_domain": to_domain,
    "lower": _to_lower,
    "strip": strip,
    "normalize_company_name": _normalize_company_name,
    "strip_formerly_known_as": strip_formerly_known_as,
}


def apply_transforms(value: str | None, transforms: Iterable[str] | None) -> str:
    """Apply a sequence of named transforms to a single value."""
    result: str | None = value
    if not transforms:
        return _strip(result)

    for name in transforms:
        func = TRANSFORM_REGISTRY.get(name)
        if not func:
            continue
        try:
            result = func(result)  # type: ignore[call-arg]
        except Exception:
            result = ""
        if result in (None, ""):
            return ""
    return _strip(result)


def apply_transforms_list(
    values: Iterable[str | None], transforms: Iterable[str] | None
) -> List[str]:
    """Apply transforms to an iterable of values, returning normalized strings."""
    if not transforms:
        return [apply_transforms(v, ("strip",)) for v in values]
    return [apply_transforms(v, transforms) for v in values]


def load_corp_markers(path: str | Path | None = None) -> str:
    """Load a regex pattern for corporate markers from disk when available."""
    candidate_paths: list[Path] = []
    if path is not None:
        candidate_paths.append(Path(path))
    module_dir = Path(__file__).resolve().parent
    candidate_paths.extend(
        [
            module_dir / "corp_markers.txt",
            module_dir / "corp_markers.regex",
            module_dir.parent / "config" / "corp_markers.txt",
            module_dir.parents[1] / "config" / "corp_markers.txt",
        ]
    )

    for candidate in candidate_paths:
        try:
            if candidate.exists():
                content = candidate.read_text(encoding="utf-8").strip()
                if content:
                    return _ensure_non_capturing(content)
        except OSError:
            continue

    return _ensure_non_capturing(DEFAULT_CORP_MARKER_PATTERN)
