from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from typing import Any, Dict, List, Tuple, Optional, Literal
import time
import random

from ...db import get_session
from ...auth_core import get_current_account
from .schemas import ExplainResponse
from ...services.explain_utils import classify_match_type
from ...utils.score_utils import norm01
from ...services.domain_family import get_registry, domain_normalize

router = APIRouter(prefix="/api/v2", tags=["match"])


def _summarize_evidence(
    details: List[Dict[str, Any]],
) -> Tuple[int, float, List[str], bool, bool]:
    if not details:
        return 0, 0.0, [], False, False
    total_w = total_ws = 0.0
    algs, exacts, fuzzies = [], 0, 0
    has_exact_anchor = False
    for d in details:
        w = float(d.get("weight") or 0.0)
        s = norm01(d.get("score"))  # Use centralized normalization
        total_w += max(0.0, w)
        total_ws += max(0.0, w) * s
        a = (d.get("algorithm") or "").lower()
        algs.append(a)
        if (("exact" in a) or ("domain" in a)) and s >= 0.95:
            exacts += 1
            has_exact_anchor = True
        else:
            fuzzies += 1
    strength = (total_ws / total_w) if total_w > 0 else 0.0
    flags = []
    if len(set(algs)) > 1:
        flags.append("multi-algo")
    if exacts:
        flags.append(f"{exacts} exact")
    if fuzzies:
        flags.append(f"{fuzzies} fuzzy")
    return (
        len(details),
        round(strength, 4),
        flags,
        (len(set(algs)) > 1),
        has_exact_anchor,
    )


async def _fetch_one(db: AsyncSession, table: str, id: str, account_id: str):
    stmt = text(f"""
      SELECT id, score, tier, field_score_details
      FROM {table}
      WHERE id = :id AND account_id = :account_id
      LIMIT 1
    """)
    row = (
        (await db.execute(stmt, {"id": id, "account_id": account_id}))
        .mappings()
        .first()
    )
    if not row:
        return None
    import json

    raw = row["field_score_details"]
    # Robust FSD parsing (TEXT vs JSONB)
    if isinstance(raw, str):
        try:
            details = json.loads(raw) if raw else []
        except Exception:
            details = []
    elif isinstance(raw, list):
        details = raw
    elif isinstance(raw, dict):
        details = [raw]  # Single object to list
    else:
        details = []

    # Normalize field scores to 0..1 to meet API contract
    for d in details:
        d["score"] = norm01(d.get("score"))  # Use centralized normalization
        if "weight" in d:
            try:
                d["weight"] = float(d.get("weight") or 0.0)
            except:
                d["weight"] = 0.0

    # Fallback: if domain evidence lacks relation/family, compute from lead/account domains
    try:
        reg = get_registry()
        lead_dom = None
        acct_dom = None

        # Try to infer from known fields if present in the row/explanation
        for d in details:
            f = (d.get("field") or d.get("source_field") or "").lower()
            if f in {"email_domain", "lead_domain", "email"} and not lead_dom:
                lead_dom = (
                    d.get("value")
                    or d.get("lhs")
                    or d.get("lead_value")
                    or d.get("source_value")
                )
            if f in {"website_domain", "account_domain", "website"} and not acct_dom:
                acct_dom = (
                    d.get("value")
                    or d.get("rhs")
                    or d.get("account_value")
                    or d.get("reference_value")
                )

        # Secondary fallback: if still no domains found, try to fetch from DB
        if (
            (not lead_dom or not acct_dom)
            and hasattr(row, "lead_id")
            and hasattr(row, "account_id")
        ):
            try:
                # Quick fetch of domain fields from Lead and Account records
                if not lead_dom and row.get("lead_id"):
                    lead_stmt = text("SELECT Email FROM Lead WHERE Id = :id LIMIT 1")
                    lead_row = (
                        await db.execute(lead_stmt, {"id": row["lead_id"]})
                    ).first()
                    if lead_row and lead_row[0]:
                        lead_dom = lead_row[0]

                if not acct_dom and row.get("account_id"):
                    acct_stmt = text(
                        "SELECT Website FROM Account WHERE Id = :id LIMIT 1"
                    )
                    acct_row = (
                        await db.execute(acct_stmt, {"id": row["account_id"]})
                    ).first()
                    if acct_row and acct_row[0]:
                        acct_dom = acct_row[0]
            except Exception:
                pass  # Ignore DB fetch errors

        # Normalize domains
        lead_dom = domain_normalize(lead_dom)
        acct_dom = domain_normalize(acct_dom)

        if lead_dom or acct_dom:
            rel, fam = reg.relation(lead_dom, acct_dom)
            for d in details:
                f = (d.get("field") or d.get("source_field") or "").lower()
                if f in {
                    "domain",
                    "email_domain",
                    "website_domain",
                    "email",
                    "website",
                }:
                    if "relation" not in d or not d["relation"]:
                        d["relation"] = rel.value if hasattr(rel, "value") else str(rel)
                    if fam and ("family_id" not in d or not d["family_id"]):
                        d["family_id"] = fam
    except Exception:
        pass  # Best effort fallback

    score = norm01(row.get("score"))  # Use centralized normalization
    return {
        "id": row["id"],
        "tier": row.get("tier") or "POSSIBLE",
        "match_score": round(score, 4),
        "field_score_details": details,
    }


@router.get(
    "/match/explain/{id}",
    response_model=ExplainResponse,
    response_model_exclude_none=True,
)
async def explain_match(
    id: str,
    kind: Optional[Literal["l2a", "dedupe"]] = None,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(get_current_account),
):
    t0 = time.perf_counter()
    tables = (("l2a", "l2a_suggestions"), ("dedupe", "dedupe_suggestions"))
    if kind:
        tables = tuple(t for t in tables if t[0] == kind)

    for k, table in tables:
        rec = await _fetch_one(db, table, id, str(account_id))
        if rec:
            details = rec["field_score_details"]
            rec["match_type"] = classify_match_type(details)
            cnt, strength, flags, ensemble_used, has_exact_anchor = _summarize_evidence(
                details
            )
            rec.update(
                evidence_count=cnt,
                evidence_strength=strength,
                evidence_flags=flags,
                ensemble_used=ensemble_used,
                has_exact_anchor=has_exact_anchor,
                kind=k,
                version="v1",
            )
            # lightweight log (sampled): account, id, kind, ms
            try:
                import logging

                logger = logging.getLogger("fmatch.api")
                msg = "EXPLAIN account=%s id=%s kind=%s ms=%.1f"
                if random.random() < 0.05:  # 5% sampling for info level
                    logger.info(
                        msg, account_id, id, k, (time.perf_counter() - t0) * 1000.0
                    )
                else:
                    logger.debug(
                        msg, account_id, id, k, (time.perf_counter() - t0) * 1000.0
                    )
            except Exception:
                pass
            return rec
    raise HTTPException(status_code=404, detail="Explanation not found")
