"""Helper utilities to enrich promotion candidates before scoring."""

from __future__ import annotations

import logging
import threading
import time
from typing import Any, Dict, Optional, TYPE_CHECKING, Tuple

from sqlalchemy import text
from sqlalchemy.orm import Session

from ...utils_normalize import to_domain

if TYPE_CHECKING:  # pragma: no cover
    from .promotion_service import PromotionCandidateInput

log = logging.getLogger(__name__)

_TABLE_CACHE: Dict[str, bool] = {}
_LOOKUP_CACHE_TTL = 86_400  # 24 hours
_LOOKUP_CACHE: Dict[Tuple[str, Tuple[Tuple[str, Any], ...]], Tuple[float, Any]] = {}
_CACHE_LOCK = threading.Lock()
_LOOKUP_SEMAPHORE = threading.BoundedSemaphore(8)


def hydrate_candidate_facts(
    session: Session,
    *,
    candidate: "PromotionCandidateInput",
    target_snapshot: Optional[Dict[str, Any]],
) -> Dict[str, Any]:
    """
    Fill in missing authority and website facts for a promotion candidate.

    Hydration uses a per-process cache with a 24 hour TTL to avoid repeated lookups when
    large batches run, and records metadata so the caller can track hydration misses.
    """
    payload: Dict[str, Any] = dict(candidate.engine_payload or {})
    nested = (
        payload.get("candidate") if isinstance(payload.get("candidate"), dict) else {}
    )
    hydrate_meta: Dict[str, Any] = {
        "site_source": None,
        "country_source": None,
        "miss_reasons": [],
        "sec_match_type": None,
    }

    target_domain = None
    target_ticker = None
    target_country = None
    if target_snapshot:
        target_domain = target_snapshot.get("domain_claimed")
        target_ticker = target_snapshot.get("target_ticker") or target_snapshot.get(
            "ticker"
        )
        target_country = target_snapshot.get("target_country") or target_snapshot.get(
            "country_code"
        )

    site = _extract_site(payload) or _extract_site(nested)
    if not site and target_domain:
        site = target_domain
        hydrate_meta["site_source"] = "target_snapshot"

    if target_ticker and not payload.get("ticker"):
        payload["ticker"] = target_ticker
    if target_country and not payload.get("hq_country"):
        payload["hq_country"] = target_country
        hydrate_meta["country_source"] = "target_snapshot"
    # Fallback: use candidate.countries if present when hq_country is missing
    if not payload.get("hq_country") and isinstance(nested, dict):
        countries = nested.get("countries")
        if isinstance(countries, list) and countries:
            first = countries[0]
            if isinstance(first, dict):
                candidate_country = first.get("label") or first.get("id")
            else:
                candidate_country = str(first)
            if candidate_country:
                payload["hq_country"] = candidate_country
                hydrate_meta["country_source"] = "candidate_countries"

    # 2. SEC / EDGAR lookup to hydrate ticker, CIK, and canonical website.
    sec_record = None
    cik_hint = payload.get("cik") or (
        nested.get("cik") if isinstance(nested, dict) else None
    )
    ticker_hint = payload.get("ticker") or (
        nested.get("ticker") if isinstance(nested, dict) else None
    )
    company_name = target_snapshot.get("company_name") if target_snapshot else None
    if _table_exists(session, "sec_companies"):
        sec_record = _lookup_sec_company(
            session,
            cik=cik_hint,
            ticker=ticker_hint,
            qid=candidate.graph_entity_id,
            company_name=company_name,
        )
    if sec_record:
        match_type = sec_record.pop("_match_type", None)
        hydrate_meta["sec_match_type"] = match_type
        payload.setdefault("ticker", sec_record.get("ticker"))
        payload.setdefault("cik", sec_record.get("cik"))
        if sec_record.get("country") and not payload.get("hq_country"):
            payload["hq_country"] = sec_record.get("country")
            hydrate_meta["country_source"] = hydrate_meta.get("country_source") or "sec"
        if not site and sec_record.get("website"):
            site = sec_record.get("website")
            hydrate_meta["site_source"] = hydrate_meta.get("site_source") or "sec"
        if match_type == "qid":
            payload.setdefault("qid_to_cik_match", True)
        if sec_record.get("ticker"):
            payload.setdefault("sec_ticker", sec_record.get("ticker"))
        if sec_record.get("cik"):
            payload.setdefault("sec_cik", sec_record.get("cik"))
        if match_type:
            payload.setdefault("sec_match_type", match_type)

    # 3. Wikidata P856 fallback if still missing a site.
    if (
        not site
        and candidate.graph_source == "wikidata"
        and _table_exists(session, "wikidata_entities")
    ):
        wikidata_site = _lookup_wikidata_site(session, candidate.graph_entity_id)
        if wikidata_site:
            site = wikidata_site
            hydrate_meta["site_source"] = hydrate_meta.get("site_source") or "wikidata"

    # 3b. Populate country from Wikidata if missing.
    if (
        not payload.get("hq_country")
        and candidate.graph_source == "wikidata"
        and _table_exists(session, "wikidata_entities")
    ):
        wikidata_country = _lookup_wikidata_country(session, candidate.graph_entity_id)
        if wikidata_country:
            payload["hq_country"] = wikidata_country
            hydrate_meta["country_source"] = (
                hydrate_meta.get("country_source") or "wikidata"
            )

    # 4. Optional DBpedia fallback if everything else failed.
    if not site:
        dbpedia_site = _lookup_dbpedia_site(session, candidate.graph_entity_id)
        if dbpedia_site:
            site = dbpedia_site
            hydrate_meta["site_source"] = hydrate_meta.get("site_source") or "dbpedia"

    # 5. Store hydrated values back onto the payload (both top-level and nested candidate).
    if site:
        canonical = to_domain(site) or site
        payload["official_website"] = canonical
        if isinstance(nested, dict):
            nested.setdefault("official_website", canonical)
            nested.setdefault("domain", canonical)
            domains = nested.get("domains")
            if isinstance(domains, list):
                if canonical not in domains:
                    domains.append(canonical)
            else:
                nested["domains"] = [canonical]
            payload["candidate"] = nested

    if isinstance(nested, dict):
        ticker_value = payload.get("ticker")
        if isinstance(ticker_value, str) and ticker_value.strip():
            ticker_norm = ticker_value.strip().upper()
            tickers = nested.get("tickers")
            if isinstance(tickers, list):
                existing = {str(t).strip().upper() for t in tickers}
                if ticker_norm not in existing:
                    tickers.append(ticker_norm)
            else:
                nested["tickers"] = [ticker_norm]
        if payload.get("cik") and not nested.get("cik"):
            nested["cik"] = payload["cik"]
        country_value = payload.get("hq_country")
        if isinstance(country_value, str) and country_value.strip():
            country_norm = country_value.strip().upper()
            countries = nested.get("countries")
            if isinstance(countries, list):
                existing_codes = {
                    str(item.get("id") or item.get("label") or "").strip().upper()
                    for item in countries
                    if isinstance(item, dict)
                }
                if country_norm and country_norm not in existing_codes:
                    countries.append({"id": country_norm, "label": country_norm})
            else:
                nested["countries"] = [{"id": country_norm, "label": country_norm}]
        payload["candidate"] = nested

    if not site:
        hydrate_meta["miss_reasons"].append("missing_domain")
        if not target_domain:
            hydrate_meta["miss_reasons"].append("no_target_domain")
        if not sec_record:
            hydrate_meta["miss_reasons"].append("sec_lookup_miss")
        if candidate.graph_source != "wikidata":
            hydrate_meta["miss_reasons"].append("non_wikidata_source")
    if not payload.get("hq_country"):
        hydrate_meta["miss_reasons"].append("missing_country")

    payload["_hydrate_meta"] = hydrate_meta
    return payload


def _extract_site(payload: Dict[str, Any]) -> Optional[str]:
    for key in ("official_website", "website", "homepage", "url"):
        value = payload.get(key)
        if isinstance(value, str) and value.strip():
            return value.strip()
    return None


def _lookup_sec_company(
    session: Session,
    *,
    cik: Optional[str],
    ticker: Optional[str],
    qid: Optional[str],
    company_name: Optional[str],
) -> Optional[Dict[str, Any]]:
    """Attempt to fetch SEC metadata prioritising CIK, then ticker, QID, and finally company name."""
    queries = []
    if cik:
        queries.append(
            (
                "SELECT cik, ticker, website, country FROM sec_companies WHERE cik = :cik LIMIT 1",
                {"cik": cik},
                "cik",
            )
        )
    if ticker:
        queries.append(
            (
                "SELECT cik, ticker, website, country FROM sec_companies WHERE UPPER(ticker) = :ticker LIMIT 1",
                {"ticker": ticker.upper()},
                "ticker",
            )
        )
    if qid:
        queries.append(
            (
                "SELECT cik, ticker, website, country FROM sec_companies WHERE wikidata_qid = :qid LIMIT 1",
                {"qid": qid},
                "qid",
            )
        )
    if company_name:
        queries.append(
            (
                "SELECT cik, ticker, website, country FROM sec_companies WHERE UPPER(name) = :name LIMIT 1",
                {"name": company_name.upper()},
                "name",
            )
        )

    for sql, params, match_type in queries:
        cache_key = ("sec", match_type, tuple(sorted(params.items())))

        def loader(s=sql, p=params):
            row = _safe_first(session, s, p)
            return dict(row._mapping) if row else None

        record = _cached_lookup(cache_key, loader)
        if record:
            record["_match_type"] = match_type
            return record
    return None


def _lookup_wikidata_site(session: Session, qid: Optional[str]) -> Optional[str]:
    if not qid:
        return None
    cache_key = ("wikidata_site", (("qid", qid),))

    def loader():
        row = _safe_first(
            session,
            "SELECT official_website FROM wikidata_entities WHERE qid = :qid LIMIT 1",
            {"qid": qid},
        )
        if row and row._mapping.get("official_website"):
            return row._mapping["official_website"]
        return None

    return _cached_lookup(cache_key, loader)


def _lookup_wikidata_country(session: Session, qid: Optional[str]) -> Optional[str]:
    if not qid:
        return None
    cache_key = ("wikidata_country", (("qid", qid),))

    def loader():
        row = _safe_first(
            session,
            """
            SELECT country_code, hq_country_code
            FROM wikidata_entities
            WHERE qid = :qid
            LIMIT 1
            """,
            {"qid": qid},
        )
        if not row:
            return None
        mapping = row._mapping
        for key in ("hq_country_code", "country_code"):
            value = mapping.get(key)
            if isinstance(value, str) and value.strip():
                return value.strip().upper()
        return None

    return _cached_lookup(cache_key, loader)


def _lookup_dbpedia_site(session: Session, qid: Optional[str]) -> Optional[str]:
    if not qid or not _table_exists(session, "dbpedia_entities"):
        return None
    cache_key = ("dbpedia_site", (("qid", qid),))

    def loader():
        row = _safe_first(
            session,
            "SELECT homepage FROM dbpedia_entities WHERE qid = :qid LIMIT 1",
            {"qid": qid},
        )
        if row:
            homepage = row._mapping.get("homepage")
            if isinstance(homepage, str) and homepage.strip():
                return homepage.strip()
        return None

    return _cached_lookup(cache_key, loader)


def _safe_first(session: Session, sql: str, params: Dict[str, Any]) -> Optional[Any]:
    try:
        with _LOOKUP_SEMAPHORE:
            result = session.execute(text(sql), params)
            return result.first()
    except Exception as exc:  # pragma: no cover - diagnostics only
        log.debug("Hydrator query failed for %s with %s", sql, exc)
        session.rollback()
        return None


def _table_exists(session: Session, table_name: str) -> bool:
    cached = _TABLE_CACHE.get(table_name)
    if cached is not None:
        return cached

    try:
        result = session.execute(
            text(
                """
                SELECT EXISTS (
                  SELECT FROM information_schema.tables
                  WHERE table_name = :table_name
                )
                """
            ),
            {"table_name": table_name},
        )
        exists = bool(result.scalar() or False)
        _TABLE_CACHE[table_name] = exists
        return exists
    except Exception as exc:  # pragma: no cover - diagnostics only
        log.debug("Unable to determine presence of %s: %s", table_name, exc)
        session.rollback()
        _TABLE_CACHE[table_name] = False
        return False


def _cached_lookup(key: Tuple[str, Tuple[Tuple[str, Any], ...]], loader) -> Any:
    now = time.time()
    with _CACHE_LOCK:
        cached = _LOOKUP_CACHE.get(key)
        if cached:
            ts, value = cached
            if now - ts < _LOOKUP_CACHE_TTL:
                return value
            _LOOKUP_CACHE.pop(key, None)

    value = loader()

    with _CACHE_LOCK:
        _LOOKUP_CACHE[key] = (now, value)
        if len(_LOOKUP_CACHE) > 2048:
            stale_keys = [
                cache_key
                for cache_key, (ts, _) in _LOOKUP_CACHE.items()
                if now - ts >= _LOOKUP_CACHE_TTL
            ]
            for cache_key in stale_keys:
                _LOOKUP_CACHE.pop(cache_key, None)
    return value
