"""Domain family registry for company-to-domain resolution.

This module provides a lightweight registry that manages domain-to-family
mappings. It accepts a data source adapter for flexibility in how the
underlying data is loaded (YAML, database, in-memory, etc.).
"""

from __future__ import annotations

import logging
import re
import unicodedata
from enum import Enum
from typing import Dict, Optional, Set, Tuple, TYPE_CHECKING

from ..normalization import normalize_domain
from ..heuristics import FREEMAIL_DOMAINS

if TYPE_CHECKING:
    from ..types import IRegistryDataSource

logger = logging.getLogger(__name__)

# Confidence levels for name matching
CONFIDENCE_EXACT_MATCH = 1.0
CONFIDENCE_NORMALIZED_MATCH = 0.95

# Corporate suffixes to strip during normalization
CORP_SUFFIXES = frozenset({
    "inc", "llc", "ltd", "limited", "corp", "corporation",
    "co", "company", "plc", "gmbh", "ag", "sa", "nv", "bv",
    "pty", "pte", "srl", "spa", "sarl", "sas", "ab", "oy",
})

# Regional tokens to strip
REGIONAL_TOKENS = frozenset({
    "usa", "us", "uk", "emea", "apac", "latam", "americas",
    "europe", "asia", "global", "international", "intl",
})

# Fix possessives: "McDonald's" -> "McDonalds"
_POSSESSIVE_FIX_RE = re.compile(r"\b([a-z0-9]+)(?:'|\u2019)s\b")


class DomainRelation(str, Enum):
    """Relationship between two domains."""
    EXACT = "EXACT"      # Same domain
    FAMILY = "FAMILY"    # Same corporate family
    NONE = "NONE"        # No relationship


class DomainFamilyRegistry:
    """Lightweight domain family registry with pluggable data source.

    This registry manages domain-to-family mappings and provides lookup
    functionality. The actual data loading is delegated to a data source
    adapter implementing IRegistryDataSource.

    For simple use cases, the registry can be populated directly via
    the add_* methods without a data source.

    Args:
        data_source: Optional data source implementing IRegistryDataSource.
                    If provided, the registry will delegate lookups to it.
    """

    def __init__(
        self,
        data_source: Optional["IRegistryDataSource"] = None,
    ) -> None:
        self._data_source = data_source

        # In-memory mappings (used when no data source, or for overrides)
        self.alias_to_family: Dict[str, str] = {}
        self.raw_name_to_family: Dict[str, str] = {}
        self.name_to_family: Dict[str, str] = {}
        self.display_names: Dict[str, Set[str]] = {}
        self.family_display_name: Dict[str, Optional[str]] = {}

    def get_family(self, domain: str) -> Optional[str]:
        """Get the family ID for a domain.

        Args:
            domain: Domain to look up (will be normalized)

        Returns:
            Family ID if found, None otherwise
        """
        normalized = normalize_domain(domain)
        if not normalized:
            return None

        # Check data source first if available
        if self._data_source:
            result = self._data_source.get_family(normalized)
            if result:
                return result

        # Fall back to in-memory mappings
        return self.alias_to_family.get(normalized)

    def get_display_name(self, family_id: str) -> Optional[str]:
        """Get the display name for a family.

        Args:
            family_id: Family ID to look up

        Returns:
            Display name if found, None otherwise
        """
        # Check data source first if available
        if self._data_source:
            result = self._data_source.get_display_name(family_id)
            if result:
                return result

        # Fall back to in-memory mappings
        if family_id in self.family_display_name:
            return self.family_display_name.get(family_id)

        if self.display_names.get(family_id):
            try:
                return sorted(self.display_names[family_id], key=str.lower)[0]
            except Exception:
                return next(iter(self.display_names[family_id]), None)

        return None

    def lookup_by_name(self, name: str) -> Tuple[Optional[str], float]:
        """Look up family by company name.

        Args:
            name: Company name to look up

        Returns:
            Tuple of (family_id, confidence) where confidence is 0.0-1.0
        """
        if not name:
            return (None, 0.0)

        # Check data source first if available
        if self._data_source:
            family_id, confidence = self._data_source.lookup_by_name(name)
            if family_id:
                return (family_id, confidence)

        # Try exact match
        raw = name.strip().lower()
        if raw in self.raw_name_to_family:
            return (self.raw_name_to_family[raw], CONFIDENCE_EXACT_MATCH)

        # Try normalized match
        norm = self._normalize_name(name)
        if norm and norm in self.name_to_family:
            return (self.name_to_family[norm], CONFIDENCE_NORMALIZED_MATCH)

        return (None, 0.0)

    def relation(
        self, d1: Optional[str], d2: Optional[str]
    ) -> Tuple[DomainRelation, Optional[str]]:
        """Determine the relationship between two domains.

        Args:
            d1: First domain
            d2: Second domain

        Returns:
            Tuple of (relation, family_id) where family_id is set for
            EXACT and FAMILY relations
        """
        n1 = normalize_domain(d1) if d1 else None
        n2 = normalize_domain(d2) if d2 else None

        if not n1 or not n2:
            return DomainRelation.NONE, None

        # Skip personal email domains
        if n1 in FREEMAIL_DOMAINS or n2 in FREEMAIL_DOMAINS:
            return DomainRelation.NONE, None

        # Exact match
        if n1 == n2:
            return DomainRelation.EXACT, self.get_family(n1)

        # Check if same family
        f1 = self.get_family(n1)
        f2 = self.get_family(n2)
        if f1 and f1 == f2:
            return DomainRelation.FAMILY, f1

        return DomainRelation.NONE, None

    def add_domain_mapping(self, domain: str, family_id: str) -> None:
        """Add a domain -> family mapping.

        Args:
            domain: Domain to map (will be normalized)
            family_id: Family ID to associate
        """
        normalized = normalize_domain(domain)
        if normalized:
            self.alias_to_family[normalized] = family_id.lower()

    def add_name_mapping(
        self,
        name: str,
        family_id: str,
        *,
        is_display: bool = False,
    ) -> None:
        """Add a company name -> family mapping.

        Args:
            name: Company name
            family_id: Family ID to associate
            is_display: If True, set as display name for the family
        """
        family_id = family_id.lower()

        # Raw match (exact lowercase)
        raw = name.strip().lower()
        if raw:
            self.raw_name_to_family[raw] = family_id

        # Normalized match
        norm = self._normalize_name(name)
        if norm:
            self.name_to_family[norm] = family_id

        # Track display names
        if is_display:
            self.display_names.setdefault(family_id, set()).add(name.strip())
            if family_id not in self.family_display_name:
                self.family_display_name[family_id] = name.strip()

    def _normalize_name(self, s: Optional[str]) -> str:
        """Normalize company name for matching.

        Removes accents, corporate suffixes, and regional tokens.
        """
        if not s:
            return ""

        # Remove accents
        s = unicodedata.normalize("NFKD", s)
        s = "".join(ch for ch in s if not unicodedata.combining(ch))
        s = s.lower()

        # Fix possessives
        s = _POSSESSIVE_FIX_RE.sub(r"\1s", s)

        # Keep alphanumeric, spaces, and &
        s = re.sub(r"[^\w\s&]", " ", s)

        # Tokenize
        tokens = [t for t in re.split(r"\s+", s) if t]

        # Remove corporate suffixes and regional tokens
        tokens = [
            t for t in tokens
            if t not in REGIONAL_TOKENS and t not in CORP_SUFFIXES
        ]

        return " ".join(tokens)
