"""Company-to-domain resolution with multi-stage lookup.

This module provides the CompanyDomainResolver class which resolves
company names to domains using a cascade of strategies:
1. Curated overrides (user corrections)
2. Registry lookup (domain family registry)
3. Hardcoded dictionary
4. Alias mappings (brand -> canonical)
5. Ticker-derived mappings
6. Wikidata lookup
7. Heuristic candidates with DNS verification
8. Optional LLM fallback
"""

from __future__ import annotations

import csv
import logging
import os
import time
import unicodedata
from collections import defaultdict
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
from urllib.parse import urlparse

from ..types import (
    ResolutionResult,
    ResolverMetrics,
    CircuitBreaker,
    IFoundryGraphClient,
    ILLMProvider,
    IRegistryDataSource,
)
from ..psl import registrable_root
from ..heuristics import KNOWN_COMPANY_DOMAINS
from .validators import generate_domain_candidates, probe_domain_dns
from .llm_predictor import predict_domain_with_llm

if TYPE_CHECKING:
    pass

logger = logging.getLogger(__name__)

# Default paths for override/alias CSVs (can be overridden via constructor)
DEFAULT_OVERRIDE_PATH = Path(__file__).resolve().parents[4] / "data" / "company_domain_overrides.csv"
DEFAULT_ALIAS_PATH = Path(__file__).resolve().parents[4] / "data" / "fortune_1000_aliases.csv"


def _slug(name: str) -> str:
    """Slugify a company name for lookup."""
    s = (name or "").strip()
    s = unicodedata.normalize("NFKC", s)
    s = s.lower()
    s = s.replace("&", "and")
    # Keep unicode letters/digits, drop punctuation
    return "".join(ch for ch in s if ch.isalnum())


def _normalize_key(name: str) -> str:
    """Normalize a company name for cache key."""
    if not name:
        return ""
    slugged = _slug(name)
    if slugged:
        return slugged
    return unicodedata.normalize("NFKC", name).strip().lower()


def _normalize_for_registry_lookup(name: str) -> str:
    """Normalize company name for registry lookup - strip plural/agent suffixes."""
    s = (name or "").strip().lower()
    if not s:
        return ""

    if s.endswith("ers") and len(s) > 6:
        s = s[:-3]
    elif s.endswith("er") and len(s) > 4:
        s = s[:-2]
    elif s.endswith("s") and len(s) > 4 and not s.endswith(("ss", "us", "is")):
        s = s[:-1]

    if s.endswith("i") and len(s) > 4:
        s = s[:-1]

    if len(s) > 3 and s[-1:] and s[-1] == s[-2]:
        s = s[:-1]

    return s


def _canonicalize_domain(domain: Optional[str]) -> Optional[str]:
    raw = (domain or "").strip().lower()
    if not raw:
        return None

    candidate = raw if "://" in raw else f"http://{raw}"
    try:
        parsed = urlparse(candidate)
    except Exception:
        parsed = None

    host = ((parsed.hostname if parsed else None) or "").strip().lower()
    if host:
        root = registrable_root(host)
        if root:
            return root
        return host

    root = registrable_root(raw)
    if root:
        return root

    if "." in raw and "/" not in raw:
        return raw
    return None


def _domain_name_slug(domain: str) -> str:
    canonical = _canonicalize_domain(domain) or ""
    if not canonical:
        return ""
    return _slug(canonical.split(".", 1)[0])


def _score_domain_for_name(domain: str, signals: List[str]) -> int:
    label = _domain_name_slug(domain)
    if not label:
        return 0

    score = 0
    for signal in signals:
        if not signal:
            continue
        if label == signal:
            score = max(score, 100)
        elif label.startswith(signal) or signal.startswith(label):
            score = max(score, 75)
        elif len(signal) >= 4 and (signal in label or label in signal):
            score = max(score, 40)
    return score


def _detect_org_type(lowered_name: str) -> Optional[str]:
    """Detect organization type from lowercased name."""
    if any(
        term in lowered_name
        for term in ("university", "college", "school", "academy", "institute")
    ):
        return "edu"
    if any(
        term in lowered_name for term in ("hospital", "health", "clinic", "medical")
    ):
        return "health"
    if any(
        term in lowered_name
        for term in (
            "department",
            "city of",
            "county",
            "state of",
            "ministry",
            "united states",
        )
    ):
        return "gov"
    return None


class CompanyDomainResolver:
    """Multi-stage company-to-domain resolver with dependency injection.

    This resolver supports injecting external services (registry, FoundryGraph,
    LLM, Wikidata) to avoid hard dependencies on infrastructure.

    Args:
        registry: Optional registry data source implementing IRegistryDataSource
        foundrygraph: Optional FoundryGraph client implementing IFoundryGraphClient
        llm_provider: Optional LLM provider implementing ILLMProvider
        wikidata_lookup: Optional callable for Wikidata domain lookup
        override_path: Path to override CSV file
        alias_path: Path to alias CSV file
    """

    def __init__(
        self,
        *,
        registry: Optional[IRegistryDataSource] = None,
        foundrygraph: Optional[IFoundryGraphClient] = None,
        llm_provider: Optional[ILLMProvider] = None,
        wikidata_lookup: Optional[Callable[[str], Optional[str]]] = None,
        override_path: Optional[Path] = None,
        alias_path: Optional[Path] = None,
    ) -> None:
        # Injected dependencies
        self._registry = registry
        self._foundrygraph = foundrygraph
        self._llm_provider = llm_provider
        self._wikidata_lookup = wikidata_lookup

        # Cache configuration from environment
        self._name_cache: Dict[str, Tuple[ResolutionResult, float]] = {}
        self._domain_cache: Dict[str, Tuple[Dict[str, Any], float]] = {}
        self._name_ttl = int(os.getenv("FM_COMPANY_TO_DOMAIN_NAME_CACHE_TTL", "0"))
        self._domain_ttl = int(os.getenv("FM_COMPANY_TO_DOMAIN_DOMAIN_CACHE_TTL", "0"))
        self._max_dns_per_name = int(os.getenv("FM_COMPANY_TO_DOMAIN_DNS_PER_NAME", "1"))
        self._dns_timeout = float(os.getenv("FM_COMPANY_TO_DOMAIN_DNS_TIMEOUT", "0.3"))
        self._dns_workers = int(os.getenv("FM_COMPANY_TO_DOMAIN_DNS_WORKERS", "8"))
        self._breaker_threshold = int(
            os.getenv("FM_COMPANY_TO_DOMAIN_DNS_BREAKER_THRESHOLD", "25")
        )
        self._breaker_window = int(
            os.getenv("FM_COMPANY_TO_DOMAIN_DNS_BREAKER_WINDOW", "120")
        )

        # Load static data
        self._override_path = override_path or DEFAULT_OVERRIDE_PATH
        self._alias_path = alias_path or DEFAULT_ALIAS_PATH
        self._overrides = self._load_overrides()
        self._aliases = self._load_aliases()
        self._ticker_map = self._load_ticker_map()

    def _load_overrides(self) -> Dict[str, Dict[str, Any]]:
        """Load curated overrides from CSV file."""
        overrides: Dict[str, Dict[str, Any]] = {}
        if not self._override_path.exists():
            return overrides
        try:
            with self._override_path.open("r", encoding="utf-8") as handle:
                reader = csv.DictReader(handle)
                for row in reader:
                    raw_name = (row.get("name") or "").strip()
                    domain = (row.get("domain") or "").strip().lower()
                    if not raw_name or not domain:
                        continue
                    normalized_domain = registrable_root(domain) or domain
                    try:
                        confidence = float(row.get("confidence") or 0.95)
                    except (TypeError, ValueError):
                        confidence = 0.95
                    source = (row.get("source") or "override").strip() or "override"
                    aliases = [raw_name]
                    extra = row.get("aliases")
                    if extra:
                        aliases.extend(a.strip() for a in extra.split("|") if a.strip())
                    for alias in aliases:
                        key = _normalize_key(alias)
                        if not key:
                            continue
                        overrides[key] = {
                            "domain": normalized_domain,
                            "confidence": confidence,
                            "source": source,
                            "raw_name": raw_name,
                        }
        except Exception as exc:
            logger.debug(
                "Failed to load domain overrides from %s: %s", self._override_path, exc
            )
        return overrides

    def _load_aliases(self) -> Dict[str, Dict[str, Any]]:
        """Load alias mappings from CSV file."""
        alias_map: Dict[str, Dict[str, Any]] = {}
        if not self._alias_path.exists():
            return alias_map
        try:
            with self._alias_path.open("r", encoding="utf-8") as handle:
                reader = csv.DictReader(handle)
                for row in reader:
                    brand = (row.get("brand_name") or "").strip()
                    legal = (row.get("legal_name") or "").strip()
                    domain_raw = (row.get("domain") or "").strip().lower()
                    ticker = (row.get("ticker") or "").strip().upper()
                    if not brand or not domain_raw:
                        continue
                    normalized_brand = _normalize_key(brand)
                    if not normalized_brand:
                        continue
                    domain = registrable_root(domain_raw) or domain_raw
                    alias_map[normalized_brand] = {
                        "domain": domain,
                        "confidence": float(row.get("confidence") or 0.9),
                        "source": row.get("source") or "fortune_alias",
                        "brand": brand,
                        "legal": legal,
                        "ticker": ticker,
                    }
        except Exception as exc:
            logger.debug("Failed to load alias mappings from %s: %s", self._alias_path, exc)
        return alias_map

    def _load_ticker_map(self) -> Dict[str, Dict[str, Any]]:
        """Load ticker-to-domain mappings from FoundryGraph if available."""
        mapping: Dict[str, Dict[str, Any]] = {}
        if self._foundrygraph is None:
            return mapping
        try:
            # Try to query ticker_domains table via FoundryGraph
            # This requires the FoundryGraph client to expose a way to run queries
            # For now, we skip this if the client doesn't support it
            logger.debug("Ticker map loading not yet implemented for injected FoundryGraph")
        except Exception as exc:
            logger.debug("Unable to load ticker_domains from FoundryGraph: %s", exc)
        return mapping

    def resolve_many(
        self,
        names: List[str],
        mode: str = "balanced",
        *,
        use_registry: bool = True,
        use_llm: bool = False,
        allow_dns: Optional[bool] = None,
        use_wikidata: Optional[bool] = None,
        use_heuristics: Optional[bool] = None,
    ) -> Tuple[List[ResolutionResult], ResolverMetrics]:
        """Resolve multiple company names to domains.

        Args:
            names: List of company names
            mode: Resolution mode ('fast', 'balanced', 'strict')
            use_registry: Whether to use domain family registry
            use_llm: Whether to use LLM fallback
            allow_dns: Whether to allow DNS verification (default: True except in 'fast' mode)
            use_wikidata: Whether to use Wikidata lookup
            use_heuristics: Whether to use heuristic candidates

        Returns:
            Tuple of (results list, metrics)
        """
        mode = (mode or "balanced").lower()
        if mode not in {"fast", "balanced", "strict"}:
            mode = "balanced"
        if allow_dns is None:
            allow_dns = mode != "fast"
        firmographics_enabled = (
            os.getenv("FM_COMPANY_TO_DOMAIN_FIRMOGRAPHICS", "true").lower() == "true"
        )

        start = time.time()
        canonical_names: List[str] = [_normalize_key(n) for n in names]
        mapping: Dict[str, List[int]] = defaultdict(list)
        for idx, c in enumerate(canonical_names):
            mapping[c].append(idx)

        metrics = ResolverMetrics(mode=mode, total_unique=len(mapping))
        breaker = CircuitBreaker(
            threshold=self._breaker_threshold, window_seconds=self._breaker_window
        )

        results_by_key: Dict[str, ResolutionResult] = {}

        for key, indices in mapping.items():
            raw_name = names[indices[0]] if indices else ""
            result = self._resolve_single(
                raw_name,
                canonical_key=key,
                mode=mode,
                metrics=metrics,
                breaker=breaker,
                use_registry=use_registry,
                use_llm=use_llm,
                allow_dns=allow_dns,
                use_wikidata=use_wikidata,
                use_heuristics=use_heuristics,
                firmographics_enabled=firmographics_enabled,
            )
            results_by_key[key] = result

        final: List[ResolutionResult] = []
        for key in canonical_names:
            final.append(
                results_by_key.get(
                    key,
                    ResolutionResult({"value": {"domain": "", "confidence": "low"}}),
                )
            )

        metrics.elapsed_ms = (time.time() - start) * 1000.0
        return final, metrics

    def _resolve_single(
        self,
        name: str,
        *,
        canonical_key: str,
        mode: str,
        metrics: ResolverMetrics,
        breaker: CircuitBreaker,
        use_registry: bool,
        use_llm: bool,
        allow_dns: bool,
        use_wikidata: Optional[bool],
        use_heuristics: Optional[bool],
        firmographics_enabled: bool,
    ) -> ResolutionResult:
        """Resolve a single company name to domain."""
        now = time.time()
        cached = self._name_cache.get(canonical_key)
        if cached and now - cached[1] <= self._name_ttl:
            metrics.cache_hits += 1
            return cached[0]

        clean_name = (name or "").strip()
        if not clean_name:
            res = ResolutionResult(
                {"value": {"domain": "", "confidence": "low", "reason": "empty_name"}}
            )
            self._name_cache[canonical_key] = (res, now)
            return res

        slugged = _slug(clean_name)

        # Stage 1: Curated overrides (highest priority - user corrections)
        override = self._overrides.get(canonical_key) or self._overrides.get(slugged)
        if override:
            return self._build_override_result(
                override, canonical_key, now, metrics, firmographics_enabled
            )

        # Stage 2: Registry lookup (automated data)
        if use_registry and self._registry is not None:
            result = self._try_registry_lookup(
                clean_name, canonical_key, now, metrics, firmographics_enabled
            )
            if result:
                return result

        # Stage 2b: Hardcoded dictionary
        if slugged in KNOWN_COMPANY_DOMAINS:
            return self._build_hardcoded_result(
                slugged, canonical_key, now, metrics, firmographics_enabled
            )

        # Stage 2c: Alias mappings (brand -> canonical domain)
        alias_entry = self._aliases.get(canonical_key)
        if alias_entry:
            return self._build_alias_result(
                alias_entry, canonical_key, now, metrics, firmographics_enabled
            )

        # Stage 2d: Ticker derived mapping
        ticker_entry = self._ticker_map.get(canonical_key)
        if ticker_entry:
            return self._build_ticker_result(
                ticker_entry, canonical_key, now, metrics, firmographics_enabled
            )

        # Stage 3: Wikidata lookup
        if use_wikidata is not False:
            result = self._try_wikidata_lookup(
                clean_name, canonical_key, now, metrics, firmographics_enabled
            )
            if result:
                return result

        # Stage 4: Heuristic candidates
        if use_heuristics is False:
            # Return empty result immediately for v1 mode
            metrics.unresolved += 1
            value = {
                "domain": "",
                "confidence": "low",
                "source": "none",
                "reason": "not_in_registry",
                "chips": ["v1_guardrails"],
            }
            result = ResolutionResult({"value": value}, cacheable=True)
            self._name_cache[canonical_key] = (result, now)
            return result

        return self._resolve_with_heuristics(
            clean_name,
            canonical_key=canonical_key,
            mode=mode,
            metrics=metrics,
            breaker=breaker,
            allow_dns=allow_dns,
            use_llm=use_llm,
            firmographics_enabled=firmographics_enabled,
        )

    def _build_override_result(
        self,
        override: Dict[str, Any],
        canonical_key: str,
        now: float,
        metrics: ResolverMetrics,
        firmographics_enabled: bool,
    ) -> ResolutionResult:
        """Build result from override entry."""
        domain = override["domain"]
        confidence_score = override.get("confidence", 0.95)
        if confidence_score >= 0.85:
            confidence_label = "high"
        elif confidence_score >= 0.6:
            confidence_label = "medium"
        else:
            confidence_label = "low"
        metrics.override_hits += 1
        source_type = override.get("source", "override")
        value: Dict[str, Any] = {
            "domain": domain,
            "confidence": confidence_label,
            "confidence_score": confidence_score,
            "source": source_type,
            "chips": ["override", source_type] if source_type != "override" else ["override"],
        }
        if override.get("raw_name"):
            metadata = value.setdefault("metadata", {})
            metadata["override_name"] = override["raw_name"]
        result = ResolutionResult({"value": value})
        self._name_cache[canonical_key] = (result, now)
        return result

    def _try_registry_lookup(
        self,
        clean_name: str,
        canonical_key: str,
        now: float,
        metrics: ResolverMetrics,
        firmographics_enabled: bool,
    ) -> Optional[ResolutionResult]:
        """Try to resolve via registry."""
        registry_enabled = (
            os.getenv("FM_COMPANY_TO_DOMAIN_REGISTRY", "true").lower() == "true"
        )
        if not registry_enabled or self._registry is None:
            return None

        try:
            family_id, conf = self._registry.lookup_by_name(clean_name)
            if family_id and conf >= 0.85:
                normalized = _normalize_for_registry_lookup(clean_name)
                if (not family_id or conf < 0.85) and normalized and normalized != clean_name.lower():
                    family_id_norm, conf_norm = self._registry.lookup_by_name(normalized)
                    if family_id_norm and conf_norm >= 0.85:
                        family_id, conf = family_id_norm, conf_norm
                domain = self._get_primary_domain_from_family(
                    family_id, company_name=clean_name
                )
                if domain:
                    metrics.registry_hits += 1
                    value = {
                        "domain": domain,
                        "confidence": "high",
                        "source": "registry",
                        "family_id": family_id,
                        "chips": ["registry"],
                    }
                    result = ResolutionResult({"value": value})
                    self._name_cache[canonical_key] = (result, now)
                    return result
        except Exception as exc:
            logger.debug(f"Registry lookup failed for {clean_name}: {exc}")
        return None

    def _get_primary_domain_from_family(
        self, family_id: str, *, company_name: Optional[str] = None
    ) -> Optional[str]:
        """Get primary domain for a family ID from registry."""
        if self._registry is None:
            return None

        domains = set()

        try:
            parent = _canonicalize_domain(
                getattr(self._registry, "family_parent_domain", {}).get(family_id)
            )
            if parent:
                domains.add(parent)
        except Exception:
            parent = None

        try:
            family_domains = getattr(self._registry, "family_to_domains", {}).get(
                family_id, set()
            )
            for domain in family_domains or set():
                canonical = _canonicalize_domain(domain)
                if canonical:
                    domains.add(canonical)
        except Exception:
            pass

        if domains:
            signals: List[str] = []
            display_name = None
            try:
                display_name = self._registry.get_display_name(family_id)
            except Exception:
                display_name = None

            for candidate in (company_name, display_name, family_id):
                normalized = _normalize_key(candidate) if candidate else ""
                if normalized and normalized not in signals:
                    signals.append(normalized)

            return sorted(
                domains,
                key=lambda domain: (
                    -_score_domain_for_name(domain, signals),
                    len(domain),
                    domain,
                ),
            )[0]

        try:
            domain = self._registry.get_family(family_id)
            return _canonicalize_domain(domain) or domain
        except Exception:
            return None

    def _build_hardcoded_result(
        self,
        slugged: str,
        canonical_key: str,
        now: float,
        metrics: ResolverMetrics,
        firmographics_enabled: bool,
    ) -> ResolutionResult:
        """Build result from hardcoded dictionary."""
        dom = KNOWN_COMPANY_DOMAINS[slugged]
        root_domain = registrable_root(dom)
        metrics.hardcoded_hits += 1
        value = {
            "domain": root_domain,
            "confidence": "high",
            "source": "hardcoded",
            "chips": ["hardcoded"],
        }
        result = ResolutionResult({"value": value})
        self._name_cache[canonical_key] = (result, now)
        return result

    def _build_alias_result(
        self,
        alias_entry: Dict[str, Any],
        canonical_key: str,
        now: float,
        metrics: ResolverMetrics,
        firmographics_enabled: bool,
    ) -> ResolutionResult:
        """Build result from alias entry."""
        domain = alias_entry["domain"]
        confidence_score = alias_entry.get("confidence", 0.9)
        if confidence_score >= 0.85:
            confidence_label = "high"
        elif confidence_score >= 0.6:
            confidence_label = "medium"
        else:
            confidence_label = "low"
        metrics.alias_hits += 1
        value: Dict[str, Any] = {
            "domain": domain,
            "confidence": confidence_label,
            "confidence_score": confidence_score,
            "source": alias_entry.get("source", "alias"),
            "chips": ["alias"],
        }
        metadata = value.setdefault("metadata", {})
        if alias_entry.get("brand"):
            metadata["brand_name"] = alias_entry["brand"]
        if alias_entry.get("legal"):
            metadata["legal_name"] = alias_entry["legal"]
        if alias_entry.get("ticker"):
            metadata["ticker"] = alias_entry["ticker"]
        result = ResolutionResult({"value": value})
        self._name_cache[canonical_key] = (result, now)
        return result

    def _build_ticker_result(
        self,
        ticker_entry: Dict[str, Any],
        canonical_key: str,
        now: float,
        metrics: ResolverMetrics,
        firmographics_enabled: bool,
    ) -> ResolutionResult:
        """Build result from ticker entry."""
        domain = ticker_entry["domain"]
        confidence_score = ticker_entry.get("confidence", 0.9)
        if confidence_score >= 0.85:
            confidence_label = "high"
        elif confidence_score >= 0.6:
            confidence_label = "medium"
        else:
            confidence_label = "low"
        metrics.ticker_hits += 1
        value: Dict[str, Any] = {
            "domain": domain,
            "confidence": confidence_label,
            "confidence_score": confidence_score,
            "source": ticker_entry.get("source", "ticker_manual"),
            "chips": ["ticker"],
        }
        result = ResolutionResult({"value": value})
        self._name_cache[canonical_key] = (result, now)
        return result

    def _try_wikidata_lookup(
        self,
        clean_name: str,
        canonical_key: str,
        now: float,
        metrics: ResolverMetrics,
        firmographics_enabled: bool,
    ) -> Optional[ResolutionResult]:
        """Try to resolve via Wikidata."""
        wikidata_enabled = (
            os.getenv("FM_COMPANY_TO_DOMAIN_WIKIDATA", "true").lower() == "true"
        )
        if not wikidata_enabled:
            return None

        # Use injected lookup only (core is saas-free)
        lookup_fn = self._wikidata_lookup
        if lookup_fn is None:
            return None

        try:
            wikidata_domain = _canonicalize_domain(lookup_fn(clean_name))
            if wikidata_domain:
                metrics.wikidata_hits += 1
                value = {
                    "domain": wikidata_domain,
                    "confidence": "high",
                    "source": "wikidata",
                    "chips": ["wikidata"],
                }
                result = ResolutionResult({"value": value})
                self._name_cache[canonical_key] = (result, now)
                return result
        except Exception as exc:
            logger.debug(f"Wikidata lookup failed for {clean_name}: {exc}")
        return None

    def _resolve_with_heuristics(
        self,
        clean_name: str,
        *,
        canonical_key: str,
        mode: str,
        metrics: ResolverMetrics,
        breaker: CircuitBreaker,
        allow_dns: bool,
        use_llm: bool,
        firmographics_enabled: bool,
    ) -> ResolutionResult:
        """Resolve using heuristic candidates and DNS verification."""
        now = time.time()
        lowered = clean_name.lower()
        org_hint = _detect_org_type(lowered)

        candidates = generate_domain_candidates(
            clean_name, max_candidates=8, org_hint=org_hint
        )
        candidate_roots: List[str] = []
        for cand in candidates:
            root = registrable_root(cand)
            if root and root not in candidate_roots:
                candidate_roots.append(root)

        # When no candidates produced, bail early
        if not candidate_roots:
            metrics.unresolved += 1
            value = {
                "domain": "",
                "confidence": "low",
                "source": "none",
                "reason": "no_candidates",
                "chips": ["needs_dns"],
                "needs_dns": True,
            }
            result = ResolutionResult({"value": value}, cacheable=False)
            self._name_cache[canonical_key] = (result, now)
            return result

        # Evaluate candidates with DNS if allowed
        verified_domain: Optional[str] = None
        verified_status: Optional[Dict[str, Any]] = None
        dns_checked = 0
        needs_dns = False

        for root_domain in candidate_roots:
            # Quick cached hit
            cached_domain_status = self._domain_cache.get(root_domain)
            if (
                cached_domain_status
                and now - cached_domain_status[1] <= self._domain_ttl
            ):
                status = cached_domain_status[0]
            else:
                status = None

            if status and status.get("ok"):
                verified_domain = root_domain
                verified_status = status
                break

            if (
                not allow_dns
                or breaker.tripped
                or dns_checked >= self._max_dns_per_name
            ):
                needs_dns = True
                continue

            dns_checked += 1
            info = probe_domain_dns(root_domain, timeout=self._dns_timeout)
            self._domain_cache[root_domain] = (info, time.time())
            metrics.dns_attempts += 1
            if info.get("status") == "timeout":
                if breaker.record_timeout():
                    metrics.breaker_tripped = True
            if info.get("status") == "timeout":
                metrics.dns_timeouts += 1
            elif not info.get("ok"):
                metrics.dns_failures += 1

            if info.get("ok"):
                verified_domain = root_domain
                verified_status = info
                break

            if breaker.tripped:
                metrics.breaker_tripped = True

        if verified_domain:
            metrics.dns_verified += 1
            chips = ["dns_verified"]
            if org_hint:
                chips.append(org_hint)
            value = {
                "domain": verified_domain,
                "confidence": "high",
                "source": "dns_verified",
                "chips": chips,
            }
            if verified_status:
                value["verification"] = {
                    "status": verified_status.get("status"),
                    "latency_ms": round(verified_status.get("latency_ms", 0.0), 2),
                    "record": verified_status.get("record"),
                }
            result = ResolutionResult({"value": value})
            self._name_cache[canonical_key] = (result, time.time())
            return result

        # No verified domain found - return heuristic guess
        guess_domain = candidate_roots[0]
        chips = ["heuristic"]
        if org_hint:
            chips.append(org_hint)
        if needs_dns or allow_dns:
            chips.append("needs_dns")
        metrics.heuristic_hits += 1
        metrics.needs_dns += 1
        value = {
            "domain": guess_domain,
            "confidence": "medium" if org_hint else "low",
            "source": "heuristic",
            "chips": chips,
            "needs_dns": True,
            "reason": "dns_unverified",
        }

        # Optional LLM fallback
        llm_enabled = (
            use_llm or os.getenv("FM_COMPANY_TO_DOMAIN_LLM", "false").lower() == "true"
        )
        if llm_enabled and mode != "fast" and not breaker.tripped:
            try:
                llm_domain = predict_domain_with_llm(
                    clean_name, llm_provider=self._llm_provider
                )
                if llm_domain:
                    value["llm_suggestion"] = llm_domain
            except Exception as exc:
                logger.debug(f"LLM prediction failed for {clean_name}: {exc}")

        result = ResolutionResult({"value": value}, cacheable=False)
        self._name_cache[canonical_key] = (result, time.time())
        return result


# =============================================================================
# Module-level convenience functions (use global resolver for backwards compat)
# =============================================================================

_GLOBAL_RESOLVER: Optional[CompanyDomainResolver] = None


def _get_global_resolver() -> CompanyDomainResolver:
    """Get or create global resolver instance."""
    global _GLOBAL_RESOLVER
    if _GLOBAL_RESOLVER is None:
        _GLOBAL_RESOLVER = CompanyDomainResolver()
    return _GLOBAL_RESOLVER


def resolve_company_domains(
    names: List[str],
    mode: str = "balanced",
    *,
    use_registry: bool = True,
    use_llm: bool = False,
    allow_dns: Optional[bool] = None,
    use_wikidata: Optional[bool] = None,
    use_heuristics: Optional[bool] = None,
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
    """Resolve multiple company names to domains.

    Module-level convenience function using global resolver.

    Args:
        names: List of company names
        mode: Resolution mode ('fast', 'balanced', 'strict')
        use_registry: Whether to use domain family registry
        use_llm: Whether to use LLM fallback
        allow_dns: Whether to allow DNS verification
        use_wikidata: Whether to use Wikidata lookup
        use_heuristics: Whether to use heuristic candidates

    Returns:
        Tuple of (results list, metrics dict)
    """
    resolver = _get_global_resolver()
    results, metrics = resolver.resolve_many(
        names,
        mode=mode,
        use_registry=use_registry,
        use_llm=use_llm,
        allow_dns=allow_dns,
        use_wikidata=use_wikidata,
        use_heuristics=use_heuristics,
    )
    return [r.value for r in results], metrics.__dict__


def resolve_company_domain(
    name: str, mode: str = "balanced", **kwargs: Any
) -> Dict[str, Any]:
    """Resolve a single company name to domain.

    Module-level convenience function using global resolver.

    Args:
        name: Company name
        mode: Resolution mode
        **kwargs: Additional arguments passed to resolve_company_domains

    Returns:
        Resolution result dict
    """
    values, metrics = resolve_company_domains([name], mode=mode, **kwargs)
    return values[0] if values else {"value": {"domain": "", "confidence": "low"}}


def get_override_for_name(name: str) -> Optional[Dict[str, Any]]:
    """Get override entry for a company name if it exists.

    Args:
        name: Company name

    Returns:
        Override dict or None
    """
    if not name:
        return None
    resolver = _get_global_resolver()
    canonical = _normalize_key(name)
    slugged = _slug(name)
    override = resolver._overrides.get(canonical) or resolver._overrides.get(slugged)
    if override:
        return dict(override)
    return None
