"""
extractor.analytics.canonicalizer — Surface form normalization and entity merging.

All logic is deterministic. rapidfuzz is used for fuzzy string matching (optional at
import time; imported lazily inside methods). sentence-transformers is fully optional.
"""

from __future__ import annotations

import re
import unicodedata

# ---------------------------------------------------------------------------
# Org suffix / person title stripping
# ---------------------------------------------------------------------------

_ORG_SUFFIXES = re.compile(
    r"\b(Inc|Ltd|Corp|LLC|PLC|GmbH|Holdings|Group|Partners|Associates|"
    r"Ventures|International|Global)\.?\s*$",
    re.IGNORECASE,
)

_PERSON_TITLES = re.compile(
    r"^\s*(Dr|Mr|Mrs|Ms|Prof|Sir|Dame|Gen|Col|Sen|Rep|Gov)\.?\s+",
    re.IGNORECASE,
)

# Ticker → canonical name
_TICKER_MAP: dict[str, str] = {
    "AAPL": "Apple",
    "MSFT": "Microsoft",
    "GOOGL": "Alphabet",
    "META": "Meta Platforms",
    "AMZN": "Amazon",
    "TSLA": "Tesla",
    "NVDA": "Nvidia",
    "JPM": "JPMorgan Chase",
    "BAC": "Bank of America",
}


def normalize_surface(text: str, entity_type: str) -> str:
    """
    Normalize a surface form for canonical comparison.

    Steps:
    1. NFC unicode normalization
    2. Collapse whitespace
    3. Expand ticker symbols (org/company types)
    4. Strip person titles (person type)
    5. Strip org suffixes (org/company types)
    """
    # NFC normalization
    text = unicodedata.normalize("NFC", text)
    # Collapse whitespace
    text = re.sub(r"\s+", " ", text).strip()

    et = entity_type.lower()

    # Ticker expansion — check exact uppercase match
    upper = text.upper().strip()
    if upper in _TICKER_MAP:
        return _TICKER_MAP[upper]

    if et == "person":
        text = _PERSON_TITLES.sub("", text).strip()
    elif et in ("org", "organization", "company"):
        text = _ORG_SUFFIXES.sub("", text).strip()

    return text


def make_slug(text: str) -> str:
    """Lowercase, remove non-word chars except hyphens, collapse spaces to hyphens."""
    text = text.lower()
    # Replace any non-alphanumeric, non-hyphen, non-space char with space
    text = re.sub(r"[^\w\s-]", " ", text)
    # Collapse whitespace and hyphens to single hyphen
    text = re.sub(r"[\s_]+", "-", text.strip())
    text = re.sub(r"-+", "-", text)
    return text.strip("-")


def derive_canonical_id(entity_type: str, canonical_text: str) -> str:
    """Return f'{entity_type.lower()}:{slug}' for the normalized canonical_text."""
    return f"{entity_type.lower()}:{make_slug(normalize_surface(canonical_text, entity_type))}"


# ---------------------------------------------------------------------------
# Canonicalizer class
# ---------------------------------------------------------------------------


class Canonicalizer:
    """
    Determines whether two entity surface forms refer to the same entity.

    Uses rapidfuzz for fuzzy string similarity. sentence-transformers is
    optional and loaded lazily only when use_embeddings=True.
    """

    AUTO_MERGE_THRESHOLD: int = 92
    REVIEW_THRESHOLD: int = 78

    def __init__(self, use_embeddings: bool = False) -> None:
        self._use_embeddings = use_embeddings
        self._embedder = None  # lazy singleton

    # ------------------------------------------------------------------
    # Similarity helpers
    # ------------------------------------------------------------------

    def similarity_score(self, a: str, b: str) -> float:
        """
        Fuzzy similarity in [0, 100].

        Returns max(token_set_ratio, token_sort_ratio, partial_ratio).
        Returns 0.0 if rapidfuzz is not installed.
        """
        try:
            from rapidfuzz import fuzz  # type: ignore[import-untyped]
        except ImportError:
            return 0.0

        scores = [
            fuzz.token_set_ratio(a, b),
            fuzz.token_sort_ratio(a, b),
            fuzz.partial_ratio(a, b),
        ]
        return float(max(scores))

    def embedding_similarity(self, a: str, b: str) -> float:
        """
        Cosine similarity in [0, 1] using SentenceTransformer all-MiniLM-L6-v2.

        Returns 0.0 if sentence-transformers is not installed.
        Loads the model once as a singleton.
        """
        if not self._use_embeddings:
            return 0.0
        try:
            import numpy as np  # type: ignore[import-untyped]
            from sentence_transformers import SentenceTransformer  # type: ignore[import-untyped]
        except ImportError:
            return 0.0

        if self._embedder is None:
            self._embedder = SentenceTransformer("all-MiniLM-L6-v2")

        vecs = self._embedder.encode([a, b], normalize_embeddings=True)
        return float(np.dot(vecs[0], vecs[1]))

    # ------------------------------------------------------------------
    # Merge decision
    # ------------------------------------------------------------------

    def should_merge(
        self,
        a: str,
        b: str,
        entity_type: str,
    ) -> tuple[bool, str, float]:
        """
        Decide whether surface forms *a* and *b* refer to the same entity.

        Returns:
            (merge: bool, decision: str, confidence: float)

        Decision values:
            "exact"      — identical after normalization
            "rule"       — one is a prefix/suffix of the other (≥4 chars)
            "similarity" — rapidfuzz score ≥ AUTO_MERGE_THRESHOLD
            "embedding"  — embedding cosine ≥ 0.92 (when enabled)
            "new"        — no match
        """
        na = normalize_surface(a, entity_type)
        nb = normalize_surface(b, entity_type)

        # Exact match
        if na.lower() == nb.lower():
            return (True, "exact", 100.0)

        # Rule: one contains the other (substring, min 4 chars)
        la, lb = na.lower(), nb.lower()
        min_len = min(len(la), len(lb))
        if min_len >= 4 and (la.startswith(lb) or lb.startswith(la)):
            return (True, "rule", 95.0)

        # Fuzzy similarity
        score = self.similarity_score(na, nb)
        if score >= self.AUTO_MERGE_THRESHOLD:
            return (True, "similarity", score)

        # Embedding fallback
        if self._use_embeddings:
            emb_sim = self.embedding_similarity(na, nb)
            emb_score = emb_sim * 100.0
            if emb_score >= 92.0:
                return (True, "embedding", emb_score)

        return (False, "new", score)
