"""Promotion pipeline service scaffolding."""

from __future__ import annotations

import json
import logging
from collections import Counter, defaultdict
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Callable, Dict, Iterable, List, Optional
from uuid import UUID, uuid4

import sqlalchemy as sa
from sqlalchemy import Table, MetaData
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.sql import func
from sqlalchemy.orm import Session

from backend_admin.app.config import (
    LLM_MIN_CONFIDENCE,
    LLM_MODEL,
    LLM_REVIEW_ENABLED,
    LLM_DRY_RUN,
    LLM_APPROVE_ENABLED,
    AUTOPROMOTE_MAX,
    AUTOPROMOTE_RANK_MAX,
    PROMOTION_ENRICHMENT_ENABLED,
    PROMOTION_ENRICHMENT_RELAX_LANG_FOR,
)

from .promotion_llm_assistant import PromotionLLMAssistant, create_llm_client
from .validators import DomainValidator
from .enrichment import EnrichmentOrchestrator
from .telemetry import emit as telemetry_emit

log = logging.getLogger(__name__)

# Use configurable LLM backend (defaults to Gemini 2.5, can switch to Ollama via env)
_llm_client = create_llm_client() if LLM_REVIEW_ENABLED else None
_llm_assistant = PromotionLLMAssistant(_llm_client) if _llm_client else None
_domain_validator = DomainValidator()


@dataclass
class PromotionCandidateInput:
    target_id: UUID
    graph_entity_id: str
    graph_source: str
    engine_payload: Dict[str, Any]


@dataclass
class PromotionEvidencePayload:
    signals: Dict[str, Any] = field(default_factory=dict)
    components: Dict[str, float] = field(default_factory=dict)
    total_score: float = 0.0
    threshold_used: float = 0.0
    freshness_days: Optional[int] = None
    conflicts: Dict[str, Any] = field(default_factory=dict)
    enrichment: Dict[str, Any] = field(default_factory=dict)
    enrichment_boost: float = 0.0
    score_with_enrichment: Optional[float] = None


@dataclass
class PromotionCandidateBundle:
    candidate: PromotionCandidateInput
    evidence: PromotionEvidencePayload
    context: Dict[str, Any] = field(default_factory=dict)


@dataclass
class PromotionDecisionOutcome:
    decision: str
    reason: str
    reason_chips: List[str]
    conflict_type: Optional[str] = None
    conflicts: Dict[str, Any] = field(default_factory=dict)
    queue_suggestion: Dict[str, Any] = field(default_factory=dict)
    priority: int = 50


@dataclass
class PromotionBatchResult:
    auto_promoted: int = 0
    needs_review: int = 0
    rejected: int = 0

    def as_stats(self) -> Dict[str, int]:
        return {
            "auto_promoted": self.auto_promoted,
            "needs_review": self.needs_review,
            "rejected": self.rejected,
        }


class PromotionService:
    """Coordinates promotion candidate persistence, decisions, and auditing."""

    def __init__(self, session: Session, *, actor: str = "system") -> None:
        self.session = session
        self.actor = actor
        self.autopromote_max = max(0, AUTOPROMOTE_MAX)
        self.autopromote_rank_max = max(0, AUTOPROMOTE_RANK_MAX)
        self._metrics_ctx: Optional[Dict[str, Any]] = None
        self._enrichment_enabled = PROMOTION_ENRICHMENT_ENABLED
        self._enrichment_relax_lang_for = set(PROMOTION_ENRICHMENT_RELAX_LANG_FOR)
        self._enrichment = (
            EnrichmentOrchestrator() if self._enrichment_enabled else None
        )
        self._enrichment_runs = 0

        # Reflect tables once for Core insert() (no ORM models needed)
        self._md = MetaData()
        bind = self.session.bind
        self._cand_table = Table("promotion_candidates", self._md, autoload_with=bind)
        self._queue_table = Table(
            "promotion_review_queue", self._md, autoload_with=bind
        )

    def register_run(
        self, list_source: str, list_year: Optional[int], idempotency_key: str
    ) -> UUID:
        run_id = uuid4()
        stmt = sa.text(
            """
            INSERT INTO promotion_runs (run_id, idempotency_key, list_source, list_year)
            VALUES (:run_id, :idempotency_key, :list_source, :list_year)
            ON CONFLICT (idempotency_key)
            DO UPDATE SET
              list_source = EXCLUDED.list_source,
              list_year = EXCLUDED.list_year,
              started_at = promotion_runs.started_at
            RETURNING run_id
            """
        )
        result = self.session.execute(
            stmt,
            {
                "run_id": str(run_id),
                "idempotency_key": idempotency_key,
                "list_source": list_source,
                "list_year": list_year,
            },
        )
        persisted_run_id = result.scalar_one()
        self.session.commit()
        return UUID(str(persisted_run_id))

    def mark_run_finished(self, run_id: UUID, stats: Dict[str, Any]) -> None:
        stmt = sa.text(
            """
            UPDATE promotion_runs
            SET finished_at = :finished_at,
                stats_json = :stats_json
            WHERE run_id = :run_id
            """
        ).bindparams(sa.bindparam("stats_json", type_=sa.JSON))
        self.session.execute(
            stmt,
            {
                "finished_at": datetime.now(tz=timezone.utc),
                "stats_json": stats,
                "run_id": str(run_id),
            },
        )
        self.session.commit()

    def _apply_enrichment(
        self,
        bundle: PromotionCandidateBundle,
        target_snapshot: Optional[Dict[str, Any]],
    ) -> None:
        """
        Run enrichment pipeline for a candidate when the rollout flag is enabled.

        Stores enrichment payload on the candidate engine_payload, updates evidence signals,
        and records telemetry/metrics for monitoring. Execution is idempotent per bundle.
        """
        if not self._enrichment or bundle.context.get("_enrichment_applied"):
            return

        payload = bundle.candidate.engine_payload
        if not isinstance(payload, dict):
            payload = {}
            bundle.candidate.engine_payload = payload

        domains = _extract_candidate_domains(payload)
        candidate_context = bundle.context.get("candidate")
        candidate_domain = None
        if candidate_context is not None:
            candidate_domain = getattr(candidate_context, "domain", None) or getattr(
                candidate_context, "homepage", None
            )
            if candidate_domain:
                candidate_domain = str(candidate_domain).strip()
                if candidate_domain and candidate_domain not in domains:
                    domains.insert(0, candidate_domain)

        if not domains:
            bundle.context.setdefault("enrichment", {})
            bundle.context["enrichment"]["status"] = "skipped_no_domain"
            bundle.context["_enrichment_applied"] = True
            return

        candidate_domain = domains[0]
        target_name = None
        candidate_name = None

        if target_snapshot:
            target_name = target_snapshot.get("company_name")

        candidate_name = payload.get("company_name") or payload.get("candidate_name")
        if candidate_context is not None and not candidate_name:
            candidate_name = getattr(candidate_context, "name", None)

        if not target_name:
            target_name = candidate_name or "Unknown Company"

        # Guard to avoid duplicate work
        bundle.context["_enrichment_applied"] = True
        try:
            enrichment_result = self._enrichment.enrich(
                target_company_name=target_name,
                candidate_domain=candidate_domain,
                candidate_company_name=candidate_name,
            )
        except Exception as exc:  # pragma: no cover - defensive logging
            log.warning(
                "Enrichment failed for %s (%s): %s",
                bundle.candidate.graph_entity_id,
                candidate_domain,
                exc,
            )
            self._metrics_incr("promotion.enrichment.error")
            telemetry_emit(
                "promotion.enrichment.error",
                graph_entity_id=bundle.candidate.graph_entity_id,
                target_id=str(bundle.candidate.target_id),
                domain=candidate_domain,
                error=str(exc)[:200],
            )
            bundle.context.setdefault("enrichment", {})
            bundle.context["enrichment"]["status"] = "error"
            bundle.context["enrichment"]["error"] = str(exc)
            return

        raw_boost = enrichment_result.confidence_boost or 0.0
        adjusted_boost = raw_boost
        target_country = None
        if target_snapshot:
            for key in ("target_country", "country_code"):
                value = target_snapshot.get(key)
                if isinstance(value, str) and value.strip():
                    target_country = value.strip().upper()
                    break
        if (
            enrichment_result.content_is_english is False
            and target_country
            and target_country in self._enrichment_relax_lang_for
        ):
            adjusted_boost = min(0.3, raw_boost + 0.02)
            enrichment_result.confidence_boost = adjusted_boost

        enrichment_payload: Dict[str, Any] = self._enrichment.to_dict(enrichment_result)
        enrichment_payload["enriched_at"] = datetime.now(tz=timezone.utc).isoformat()
        if adjusted_boost != raw_boost:
            enrichment_payload["raw_confidence_boost"] = raw_boost
        enrichment_payload["applied_boost"] = enrichment_result.confidence_boost
        enrichment_payload["target_country"] = target_country

        payload["enrichment"] = enrichment_payload
        bundle.evidence.enrichment = enrichment_payload
        bundle.evidence.enrichment_boost = enrichment_result.confidence_boost
        combined_score = (
            bundle.evidence.total_score + enrichment_result.confidence_boost
        )
        bundle.evidence.score_with_enrichment = round(
            max(0.0, min(combined_score, 1.0)), 4
        )

        signals = bundle.evidence.signals or {}
        signals["enrichment_boost"] = enrichment_result.confidence_boost
        signals["enrichment_score_with_boost"] = bundle.evidence.score_with_enrichment
        signals["enrichment_website_live"] = enrichment_result.website_live
        signals["enrichment_https"] = enrichment_result.website_https
        signals["enrichment_tls_valid"] = enrichment_result.website_tls_valid
        signals["enrichment_descriptions_match"] = enrichment_result.descriptions_match
        signals["enrichment_match_score"] = round(
            enrichment_result.description_match_score or 0.0, 3
        )
        signals["enrichment_non_english"] = (
            enrichment_result.content_is_english is False
        )
        signals["enrichment_language"] = enrichment_result.content_language
        signals["enrichment_source_url"] = enrichment_result.content_source_url
        signals["enrichment_response_time_ms"] = (
            enrichment_result.website_response_time_ms
        )
        signals["enrichment_parked"] = bool(enrichment_result.is_parked_domain)
        signals["enrichment_final_host"] = enrichment_result.website_final_host
        signals["enrichment_final_etld1"] = enrichment_result.website_final_etld1
        bundle.evidence.signals = signals

        if enrichment_result.is_parked_domain:
            bundle.evidence.conflicts.setdefault("enrichment_parked", True)

        timings_ms = enrichment_result.timings_ms or {}
        total_ms = timings_ms.get("total")
        bundle.context.setdefault("timings", {})
        if total_ms:
            bundle.context["timings"]["enrichment"] = total_ms / 1000.0
            self._metrics_add_time("promotion.timing.enrichment", total_ms / 1000.0)

        bundle.context["enrichment"] = {
            "status": "completed",
            "boost": enrichment_result.confidence_boost,
            "match_score": enrichment_result.description_match_score,
            "language": enrichment_result.content_language,
            "parked": bool(enrichment_result.is_parked_domain),
            "website_live": enrichment_result.website_live,
        }

        self._metrics_incr("promotion.enrichment.completed")
        self._enrichment_runs += 1
        telemetry_emit(
            "promotion.enrichment.result",
            graph_entity_id=bundle.candidate.graph_entity_id,
            target_id=str(bundle.candidate.target_id),
            domain=candidate_domain,
            boost=round(enrichment_result.confidence_boost, 3),
            match_score=round(enrichment_result.description_match_score or 0.0, 3),
            website_live=enrichment_result.website_live,
            https=enrichment_result.website_https,
            tls_valid=enrichment_result.website_tls_valid,
            parked=bool(enrichment_result.is_parked_domain),
            language=enrichment_result.content_language,
            is_english=enrichment_result.content_is_english,
            response_ms=enrichment_result.website_response_time_ms,
        )

    def process_batch(
        self,
        run_id: UUID,
        bundles: Iterable[PromotionCandidateBundle],
        decision_handler: Optional[
            Callable[[UUID, PromotionCandidateBundle, PromotionDecisionOutcome], None]
        ] = None,
        *,
        persist: bool = True,
    ) -> PromotionBatchResult:
        batch_result = PromotionBatchResult()
        from .promotion_policy import is_medium_confidence, should_auto_promote

        metrics_ctx = {
            "counters": Counter(),
            "timings": defaultdict(float),
        }
        self._metrics_ctx = metrics_ctx
        autopromote_count = 0

        tier_counts = {"auto": 0, "llm": 0, "human": 0}
        processed = 0
        try:
            for bundle in bundles:
                processed += 1
                signals_snapshot = bundle.evidence.signals or {}
                if should_auto_promote(signals_snapshot):
                    tier_counts["auto"] += 1
                elif LLM_REVIEW_ENABLED and is_medium_confidence(signals_snapshot):
                    tier_counts["llm"] += 1
                else:
                    tier_counts["human"] += 1

                timings = bundle.context.get("timings")
                if isinstance(timings, dict):
                    for name, duration in timings.items():
                        if isinstance(duration, (int, float)):
                            self._metrics_add_time(
                                f"promotion.timing.{name}", float(duration)
                            )

                hydrate_meta = bundle.context.get("hydrate_meta")
                if isinstance(hydrate_meta, dict):
                    site_source = hydrate_meta.get("site_source")
                    if site_source:
                        self._metrics_incr(
                            f"promotion.hydrate.site_source.{site_source}"
                        )
                    for reason in hydrate_meta.get("miss_reasons", []):
                        self._metrics_incr(f"promotion.hydrate.miss_reason.{reason}")

                target_snapshot = bundle.context.get("target_snapshot")
                if target_snapshot is None:
                    target_snapshot = self._load_target_snapshot(
                        bundle.candidate.target_id
                    )
                    if target_snapshot:
                        bundle.context["target_snapshot"] = target_snapshot

                if self._enrichment:
                    self._apply_enrichment(bundle, target_snapshot)

                if persist:
                    cand_id = self._insert_candidate(run_id, bundle.candidate)
                    self._upsert_evidence(cand_id, bundle.evidence)
                else:
                    cand_id = uuid4()
                decision = self._decide(bundle.evidence)
                if (
                    decision.decision == "needs_review"
                    and decision.reason == "shadow_auto_candidate"
                ):
                    log.info(
                        "promotion.shadow_auto_candidate target=%s run=%s",
                        bundle.candidate.target_id,
                        run_id,
                    )
                decision = self._maybe_llm_assist(cand_id, bundle, decision)
                if persist:
                    self._upsert_decision(cand_id, decision)

                capped = False
                if decision.decision == "auto_promoted":
                    decision, cap_reason = self._enforce_autopromote_caps(
                        decision,
                        autopromote_count=autopromote_count,
                        target_snapshot=target_snapshot,
                    )
                    if cap_reason:
                        capped = True
                        self._metrics_incr(f"promotion.autopromote.capped.{cap_reason}")

                if decision.decision == "auto_promoted":
                    if persist:
                        self._auto_promote(cand_id, bundle, decision)
                    batch_result.auto_promoted += 1
                    autopromote_count += 1
                    self._metrics_incr("promotion.auto_promote")
                    if decision.reason.startswith("llm_"):
                        self._metrics_incr("promotion.llm_promote")
                elif decision.decision == "needs_review":
                    if persist:
                        self._enqueue_review(cand_id, bundle.candidate.target_id, decision)
                    batch_result.needs_review += 1
                    if decision.reason.startswith("llm"):
                        self._metrics_incr("promotion.llm_review")
                    elif capped:
                        self._metrics_incr("promotion.human_review_cap")
                    else:
                        self._metrics_incr("promotion.human_review")
                else:
                    if persist:
                        self._log_audit(
                            cand_id,
                            action="reject",
                            before_state={},
                            after_state={"reason": decision.reason},
                        )
                    batch_result.rejected += 1
                    self._metrics_incr("promotion.rejected")

                if decision_handler:
                    try:
                        decision_handler(cand_id, bundle, decision)
                    except Exception as exc:
                        log.warning("Decision handler failed for %s: %s", cand_id, exc)

        finally:
            if persist:
                self.session.commit()
            counters = dict(metrics_ctx["counters"])
            if counters:
                log.info("promotion.metrics counters=%s", counters)
            timings_log = {k: round(v, 6) for k, v in metrics_ctx["timings"].items()}
            if timings_log:
                log.info("promotion.metrics timings=%s", timings_log)
            if self._enrichment and self._enrichment_runs:
                telemetry_emit(
                    "promotion.enrichment.summary",
                    run_id=str(run_id),
                    runs=self._enrichment_runs,
                    metrics=self._enrichment.get_metrics_snapshot(),
                )
                self._enrichment.reset_metrics()
                self._enrichment_runs = 0
            self._metrics_ctx = None

        processed_total = processed or 1
        auto_rate = batch_result.auto_promoted / processed_total
        llm_rate = tier_counts["llm"] / processed_total if processed_total else 0.0
        log.info(
            "promotion.tiers %s autopromote_rate=%.3f llm_candidate_rate=%.3f",
            tier_counts,
            auto_rate,
            llm_rate,
        )
        return batch_result

    def _insert_candidate(self, run_id: UUID, payload: PromotionCandidateInput) -> UUID:
        """Insert candidate using SQLAlchemy Core insert() with JSONB support."""
        cand_id = uuid4()
        stmt = (
            pg_insert(self._cand_table)
            .values(
                cand_id=str(cand_id),
                run_id=str(run_id),
                target_id=str(payload.target_id),
                graph_entity_id=payload.graph_entity_id,
                graph_source=payload.graph_source,
                engine_payload=payload.engine_payload,  # Pass dict; driver handles JSONB
                created_at=func.now(),
            )
            .returning(self._cand_table.c.cand_id)
        )
        persisted_id = self.session.execute(stmt).scalar_one()
        return UUID(str(persisted_id))

    def _upsert_evidence(
        self, cand_id: UUID, payload: PromotionEvidencePayload
    ) -> None:
        stmt = sa.text(
            """
                INSERT INTO promotion_evidence (
                  cand_id, signals, components, total_score, threshold_used, freshness_days, conflicts_json
                ) VALUES (
                  :cand_id, :signals, :components, :total_score, :threshold_used, :freshness_days, :conflicts
                )
                ON CONFLICT (cand_id)
                DO UPDATE SET
                  signals = EXCLUDED.signals,
                  components = EXCLUDED.components,
                  total_score = EXCLUDED.total_score,
                  threshold_used = EXCLUDED.threshold_used,
                  freshness_days = EXCLUDED.freshness_days,
                  conflicts_json = EXCLUDED.conflicts_json
                """
        ).bindparams(
            sa.bindparam("signals", type_=sa.JSON),
            sa.bindparam("components", type_=sa.JSON),
            sa.bindparam("conflicts", type_=sa.JSON),
        )
        self.session.execute(
            stmt,
            {
                "cand_id": str(cand_id),
                "signals": payload.signals,
                "components": payload.components,
                "total_score": float(payload.total_score),
                "threshold_used": float(payload.threshold_used),
                "freshness_days": payload.freshness_days,
                "conflicts": payload.conflicts,
            },
        )

    def _upsert_decision(
        self, cand_id: UUID, outcome: PromotionDecisionOutcome
    ) -> None:
        stmt = sa.text(
            """
                INSERT INTO promotion_decisions (
                  decision_id, cand_id, decision, decision_reason, reason_chips, actor
                ) VALUES (
                  :decision_id, :cand_id, :decision, :decision_reason, :reason_chips, :actor
                )
                ON CONFLICT (cand_id)
                DO UPDATE SET
                  decision = EXCLUDED.decision,
                  decision_reason = EXCLUDED.decision_reason,
                  reason_chips = EXCLUDED.reason_chips,
                  actor = EXCLUDED.actor,
                  decided_at = now()
                """
        ).bindparams(sa.bindparam("reason_chips", type_=sa.JSON))
        decision_id = uuid4()
        self.session.execute(
            stmt,
            {
                "decision_id": str(decision_id),
                "cand_id": str(cand_id),
                "decision": outcome.decision,
                "decision_reason": outcome.reason,
                "reason_chips": outcome.reason_chips,
                "actor": self.actor,
            },
        )

    def _maybe_llm_assist(
        self,
        cand_id: UUID,
        bundle: PromotionCandidateBundle,
        outcome: PromotionDecisionOutcome,
    ) -> PromotionDecisionOutcome:
        if (
            not LLM_REVIEW_ENABLED
            or not _llm_assistant
            or outcome.decision != "needs_review"
        ):
            return outcome
        from .promotion_policy import is_medium_confidence

        signals = bundle.evidence.signals or {}
        if not is_medium_confidence(signals):
            return outcome

        target_snapshot = bundle.context.get("target_snapshot")
        if target_snapshot is None:
            target_snapshot = self._load_target_snapshot(bundle.candidate.target_id)
            if target_snapshot:
                bundle.context["target_snapshot"] = target_snapshot

        if not target_snapshot:
            return outcome

        conflicts = bundle.evidence.conflicts or {}
        signals_json = json.dumps(signals, ensure_ascii=False)
        conflicts_json = json.dumps(conflicts, ensure_ascii=False)
        candidate_domains = _extract_candidate_domains(bundle.candidate.engine_payload)

        list_year = target_snapshot.get("list_year")
        rank = target_snapshot.get("rank")
        list_year_value: Optional[str]
        if isinstance(list_year, int):
            list_year_value = str(list_year)
        else:
            list_year_value = "MISSING"
        rank_value: Optional[str]
        if isinstance(rank, int):
            rank_value = str(rank)
        else:
            rank_value = "MISSING"
        try:
            llm_decision, raw_payload = _llm_assistant.review(
                company_name=target_snapshot["company_name"],
                list_source=target_snapshot["list_source"],
                list_year=list_year_value,
                rank=rank_value,
                qid=bundle.candidate.graph_entity_id,
                ticker=target_snapshot.get("target_ticker")
                or target_snapshot.get("ticker"),
                country=target_snapshot.get("target_country")
                or target_snapshot.get("country_code"),
                candidate_domains=candidate_domains,
                signals_json=signals_json,
                conflicts_json=conflicts_json,
            )
        except Exception as exc:
            log.warning("LLM assist call failed for %s: %s", cand_id, exc)
            # Log the exception for metrics
            self._record_llm_review(
                cand_id=cand_id,
                target_id=bundle.candidate.target_id,
                decision="unsure",
                confidence=0.0,
                suggested_domain=None,
                verifier_pass=False,
                reasoning=f"exception: {str(exc)[:200]}",
                model_digest="",
            )
            return outcome

        model_digest = ""
        # Model digest only available for Ollama backend
        if _llm_client and hasattr(_llm_client, "get_model_info"):
            try:
                model_info = _llm_client.get_model_info()
                model_digest = (model_info or {}).get("digest", "") or ""
            except Exception as exc:
                log.debug("Unable to fetch model digest for %s: %s", LLM_MODEL, exc)

        decision_label = "unsure"
        confidence_val = 0.0
        suggested_domain = None
        reasoning = ""
        if llm_decision:
            decision_label = llm_decision.decision
            confidence_val = float(llm_decision.confidence)
            suggested_domain = llm_decision.suggested_domain
            reasoning = llm_decision.reasoning or ""
        else:
            # Handle parse/validation failures
            if raw_payload.get("_parse_error"):
                reasoning = f"parse_error: {raw_payload.get('_raw', '')[:200]}"
                log.warning("LLM parse error for candidate %s: %s", cand_id, reasoning)
            elif raw_payload.get("_validation_error"):
                reasoning = f"validation_error: {raw_payload.get('_validation_error', '')[:200]}"
                log.warning(
                    "LLM validation error for candidate %s: %s", cand_id, reasoning
                )
            else:
                reasoning = "parse_error: unknown"
                log.warning("LLM unknown parse error for candidate %s", cand_id)

            # Log failure as unsure for metrics tracking
            self._record_llm_review(
                cand_id=cand_id,
                target_id=bundle.candidate.target_id,
                decision="unsure",
                confidence=0.0,
                suggested_domain=None,
                verifier_pass=False,
                reasoning=reasoning,
                model_digest=model_digest,
            )
            return outcome  # Fall back to human review

        verifier_pass_flag = False
        verified_domain: Optional[str] = None
        if suggested_domain:
            verify_result = _domain_validator.verify(suggested_domain)
            status_code = verify_result.get("status")
            if status_code is not None:
                self._metrics_incr(f"promotion.verify.status_code.{status_code}")
            if verify_result.get("blocklisted"):
                self._metrics_incr("promotion.verify.blocklisted")
            verifier_pass_flag = bool(verify_result.get("verified"))
            if verifier_pass_flag:
                verified_domain = verify_result.get("final_etld1") or suggested_domain
        else:
            verifier_pass_flag = bool(signals.get("domain_verified"))

        final_outcome = outcome
        threshold_met = confidence_val >= LLM_MIN_CONFIDENCE
        if llm_decision and threshold_met:
            if decision_label == "approve":
                if verifier_pass_flag:
                    chips = list(outcome.reason_chips)
                    chips.append("llm_assist")
                    if not LLM_DRY_RUN and LLM_APPROVE_ENABLED:
                        if verified_domain:
                            self._maybe_backfill_domain(cand_id, verified_domain)
                        final_outcome = PromotionDecisionOutcome(
                            decision="auto_promoted",
                            reason="llm_assisted_auto_promote",
                            reason_chips=chips,
                            conflict_type=outcome.conflict_type,
                            conflicts=conflicts,
                            queue_suggestion={},
                            priority=min(outcome.priority, 50),
                        )
                    else:
                        queue_note = (
                            "llm_dry_run_passed"
                            if LLM_DRY_RUN
                            else "llm_auto_gate_disabled"
                        )
                        reason_label = (
                            "llm_dry_run_passed"
                            if LLM_DRY_RUN
                            else "llm_auto_gate_disabled"
                        )
                        final_outcome = PromotionDecisionOutcome(
                            decision="needs_review",
                            reason=reason_label,
                            reason_chips=chips,
                            conflict_type=outcome.conflict_type,
                            conflicts=conflicts,
                            queue_suggestion={"note": queue_note},
                            priority=outcome.priority,
                        )
                else:
                    chips = list(outcome.reason_chips)
                    chips.append("llm_assist_needs_domain")
                    final_outcome = PromotionDecisionOutcome(
                        decision="needs_review",
                        reason="llm_domain_failed_verification",
                        reason_chips=chips,
                        conflict_type=outcome.conflict_type,
                        conflicts=conflicts,
                        queue_suggestion={"action": "llm_verify_domain"},
                        priority=max(outcome.priority, 80),
                    )
            elif decision_label == "reject":
                chips = list(outcome.reason_chips)
                chips.append("llm_assist_reject")
                final_outcome = PromotionDecisionOutcome(
                    decision="rejected",
                    reason="llm_assisted_reject",
                    reason_chips=chips,
                    conflict_type=outcome.conflict_type,
                    conflicts=conflicts,
                    queue_suggestion=outcome.queue_suggestion,
                    priority=outcome.priority,
                )

        confidence_clamped = max(0.0, min(confidence_val, 1.0))
        self._record_llm_review(
            cand_id=cand_id,
            target_id=bundle.candidate.target_id,
            decision=decision_label,
            confidence=confidence_clamped,
            suggested_domain=verified_domain or suggested_domain,
            verifier_pass=verifier_pass_flag,
            reasoning=reasoning,
            model_digest=model_digest,
        )

        return final_outcome

    def _enqueue_review(
        self,
        cand_id: UUID,
        target_id: UUID,
        outcome: PromotionDecisionOutcome,
    ) -> None:
        import json as json_module

        suggested_next_json = (
            json_module.dumps(outcome.queue_suggestion)
            if outcome.queue_suggestion
            else "{}"
        )

        stmt = sa.text(
            """
            WITH previous AS (
                SELECT cand_id
                FROM promotion_review_queue
                WHERE target_id = :target_id
            ),
            upsert AS (
                INSERT INTO promotion_review_queue (
                  target_id, cand_id, priority, conflict_type, suggested_next, status, created_at, updated_at
                ) VALUES (
                  :target_id, :cand_id, :priority, :conflict_type, CAST(:suggested_next_json AS jsonb), 'pending', NOW(), NOW()
                )
                ON CONFLICT (target_id)
                DO UPDATE SET
                  cand_id = EXCLUDED.cand_id,
                  priority = GREATEST(promotion_review_queue.priority, EXCLUDED.priority),
                  conflict_type = COALESCE(EXCLUDED.conflict_type, promotion_review_queue.conflict_type),
                  suggested_next = EXCLUDED.suggested_next,
                  status = 'pending',
                  assigned_to = NULL,
                  resolved_at = NULL,
                  updated_at = NOW()
                RETURNING cand_id
            )
            SELECT
              (SELECT cand_id FROM previous) AS old_cand_id,
              (SELECT cand_id FROM upsert)   AS new_cand_id
            """
        )

        result = self.session.execute(
            stmt,
            {
                "target_id": str(target_id),
                "cand_id": str(cand_id),
                "priority": outcome.priority,
                "conflict_type": outcome.conflict_type,
                "suggested_next_json": suggested_next_json,
            },
        ).one()

        old_cand_id = result.old_cand_id
        new_cand_id = result.new_cand_id
        if old_cand_id and old_cand_id != new_cand_id:
            self.session.execute(
                sa.text(
                    """
                    UPDATE promotion_candidates
                    SET superseded_by = :new_cand_id
                    WHERE cand_id = :old_cand_id
                    """
                ),
                {"new_cand_id": str(new_cand_id), "old_cand_id": str(old_cand_id)},
            )

        self._log_audit(
            cand_id,
            action="queue_review",
            before_state={},
            after_state={
                "conflict_type": outcome.conflict_type,
                "priority": outcome.priority,
                "suggested_next": outcome.queue_suggestion,
            },
        )

    def _metrics_incr(self, key: str, amount: int = 1) -> None:
        if not self._metrics_ctx:
            return
        self._metrics_ctx["counters"][key] += amount

    def _metrics_add_time(self, key: str, duration: float) -> None:
        if not self._metrics_ctx:
            return
        self._metrics_ctx["timings"][key] += max(duration, 0.0)

    def _enforce_autopromote_caps(
        self,
        decision: PromotionDecisionOutcome,
        *,
        autopromote_count: int,
        target_snapshot: Optional[Dict[str, Any]],
    ) -> tuple[PromotionDecisionOutcome, Optional[str]]:
        if decision.decision != "auto_promoted":
            return decision, None

        rank_value = None
        if target_snapshot:
            rank_value = target_snapshot.get("rank")

        cap_reason: Optional[str] = None
        if (
            self.autopromote_rank_max
            and isinstance(rank_value, int)
            and rank_value > self.autopromote_rank_max
        ):
            cap_reason = "auto_rank_cap"
        elif self.autopromote_max and autopromote_count >= self.autopromote_max:
            cap_reason = "auto_cap_exceeded"

        if not cap_reason:
            return decision, None

        chips = list(decision.reason_chips)
        chips.append("cap")
        capped_decision = PromotionDecisionOutcome(
            decision="needs_review",
            reason=cap_reason,
            reason_chips=chips,
            conflict_type=decision.conflict_type,
            conflicts=decision.conflicts,
            queue_suggestion={"note": cap_reason},
            priority=max(decision.priority, 60),
        )
        return capped_decision, cap_reason

    def _record_llm_review(
        self,
        *,
        cand_id: UUID,
        target_id: UUID,
        decision: str,
        confidence: float,
        suggested_domain: Optional[str],
        verifier_pass: bool,
        reasoning: str,
        model_digest: str,
    ) -> None:
        stmt = sa.text(
            """
            INSERT INTO promotion_llm_reviews (
              cand_id, target_id, model, model_digest, decision, confidence, suggested_domain, verifier_pass, reasoning
            ) VALUES (
              :cand_id, :target_id, :model, :model_digest, :decision, :confidence, :suggested_domain, :verifier_pass, :reasoning
            )
            """
        )
        self.session.execute(
            stmt,
            {
                "cand_id": str(cand_id),
                "target_id": str(target_id),
                "model": LLM_MODEL,
                "model_digest": model_digest or "",
                "decision": decision,
                "confidence": round(confidence, 2),
                "suggested_domain": suggested_domain,
                "verifier_pass": verifier_pass,
                "reasoning": (reasoning or "")[:500],
            },
        )

    def _maybe_backfill_domain(self, cand_id: UUID, domain: str) -> None:
        stmt = sa.text(
            """
            UPDATE promotion_candidates
            SET engine_payload = jsonb_set(
              engine_payload,
              '{llm_suggested_domain}',
              to_jsonb(CAST(:domain AS text)),
              true
            )
            WHERE cand_id = :cand_id
            """
        )
        self.session.execute(
            stmt,
            {
                "cand_id": str(cand_id),
                "domain": domain,
            },
        )

    def _load_target_snapshot(self, target_id: UUID) -> Optional[Dict[str, Any]]:
        stmt = sa.text(
            """
            SELECT
              company_name,
              list_source,
              list_year,
              rank,
              domain_claimed,
              ticker,
              country_code
            FROM promotion_targets
            WHERE target_id = :target_id
            """
        )
        row = self.session.execute(stmt, {"target_id": str(target_id)}).fetchone()
        if not row:
            return None
        mapping = dict(row._mapping)
        mapping.setdefault("target_ticker", mapping.get("ticker"))
        mapping.setdefault("target_country", mapping.get("country_code"))
        return mapping

    def _auto_promote(
        self,
        cand_id: UUID,
        bundle: PromotionCandidateBundle,
        outcome: PromotionDecisionOutcome,
    ) -> None:
        self._log_audit(
            cand_id,
            action="auto_promote",
            before_state={},
            after_state={
                "candidate": bundle.candidate.engine_payload,
                "signals": bundle.evidence.signals,
                "components": bundle.evidence.components,
                "chips": outcome.reason_chips,
            },
        )

    def _log_audit(
        self,
        cand_id: UUID,
        *,
        action: str,
        before_state: Dict[str, Any],
        after_state: Dict[str, Any],
    ) -> None:
        stmt = sa.text(
            """
                INSERT INTO promotion_audit (
                  audit_id, cand_id, action, actor, before_state, after_state
                ) VALUES (
                  :audit_id, :cand_id, :action, :actor, :before_state, :after_state
                )
                """
        ).bindparams(
            sa.bindparam("before_state", type_=sa.JSON),
            sa.bindparam("after_state", type_=sa.JSON),
        )
        self.session.execute(
            stmt,
            {
                "audit_id": str(uuid4()),
                "cand_id": str(cand_id),
                "action": action,
                "actor": self.actor,
                "before_state": before_state,
                "after_state": after_state,
            },
        )

    def _decide(self, evidence: PromotionEvidencePayload) -> PromotionDecisionOutcome:
        signals = evidence.signals or {}
        domain_verified = bool(signals.get("domain_verified"))
        ticker_match = bool(signals.get("ticker_match"))
        authority_ok = bool(
            signals.get("authority_match") or signals.get("authority_verified")
        )
        country_aligned = signals.get("country_aligned")
        if country_aligned is None:
            country_aligned = True
        hierarchy_conflict = bool(signals.get("hierarchy_conflict"))
        freshness_days = (
            evidence.freshness_days if evidence.freshness_days is not None else 9999
        )

        conflict_count, conflict_labels = self._summarize_conflicts(evidence.conflicts)
        chips = self._chips_from_signals(signals)

        existing_family_conflict = bool(evidence.conflicts.get("existing_family"))
        stale_data = bool(evidence.conflicts.get("stale_data"))

        if (
            authority_ok
            and country_aligned
            and domain_verified
            and freshness_days <= 90
            and not hierarchy_conflict
            and not existing_family_conflict
            and not stale_data
        ):
            cleaned_conflicts = dict(evidence.conflicts) if evidence.conflicts else {}
            cleaned_conflicts.pop("missing_domain", None)
            outcome = PromotionDecisionOutcome(
                decision="auto_promoted",
                reason="authoritative_domain_match",
                reason_chips=chips,
                conflicts=cleaned_conflicts,
                priority=5,
            )
            if LLM_DRY_RUN:
                chips_shadow = list(chips)
                chips_shadow.append("shadow")
                return PromotionDecisionOutcome(
                    decision="needs_review",
                    reason="shadow_auto_candidate",
                    reason_chips=chips_shadow,
                    conflicts=cleaned_conflicts,
                    queue_suggestion={"note": "shadow_auto_candidate"},
                    priority=50,
                )
            return outcome

        if not domain_verified:
            return PromotionDecisionOutcome(
                decision="needs_review",
                reason="missing_domain_verification",
                reason_chips=chips,
                conflict_type="domain_verification",
                conflicts=evidence.conflicts,
                queue_suggestion={"action": "verify_domain"},
                priority=90,
            )

        if not (authority_ok or ticker_match):
            return PromotionDecisionOutcome(
                decision="needs_review",
                reason="missing_authority_signal",
                reason_chips=chips,
                conflict_type="authority_gap",
                conflicts=evidence.conflicts,
                queue_suggestion={"action": "confirm_ticker_or_authority"},
                priority=80,
            )

        if not country_aligned:
            return PromotionDecisionOutcome(
                decision="needs_review",
                reason="country_mismatch",
                reason_chips=chips,
                conflict_type="country_mismatch",
                conflicts=evidence.conflicts,
                queue_suggestion={"action": "validate_country"},
                priority=75,
            )

        if hierarchy_conflict:
            return PromotionDecisionOutcome(
                decision="needs_review",
                reason="hierarchy_conflict",
                reason_chips=chips,
                conflict_type="hierarchy_conflict",
                conflicts=evidence.conflicts,
                queue_suggestion={"action": "confirm_parent_entity"},
                priority=85,
            )

        if conflict_count > 0:
            return PromotionDecisionOutcome(
                decision="needs_review",
                reason="conflict_detected",
                reason_chips=chips,
                conflict_type=conflict_labels[0] if conflict_labels else "conflict",
                conflicts=evidence.conflicts,
                queue_suggestion={"conflicts": conflict_labels},
                priority=70 + min(conflict_count * 5, 20),
            )

        score_gap = (evidence.threshold_used or 0) - (evidence.total_score or 0)
        if evidence.total_score <= 0.1 or score_gap > 0.3:
            return PromotionDecisionOutcome(
                decision="rejected",
                reason="low_score",
                reason_chips=chips,
                conflicts=evidence.conflicts,
                priority=10,
            )

        return PromotionDecisionOutcome(
            decision="needs_review",
            reason="insufficient_signals",
            reason_chips=chips,
            conflicts=evidence.conflicts,
            queue_suggestion={"note": "add_additional_signals"},
            priority=60,
        )

    @staticmethod
    def _summarize_conflicts(conflicts: Optional[Dict[str, Any]]) -> tuple:
        """Count and list conflict keys."""
        if not conflicts:
            return 0, []
        labels = [k for k, v in conflicts.items() if v]
        return len(labels), labels

    @staticmethod
    def _chips_from_signals(signals: Dict[str, Any]) -> List[str]:
        chips: List[str] = []
        mapping = {
            "domain_verified": "check_domain",
            "ticker_match": "check_ticker",
            "authority_match": "check_authority",
            "authority_verified": "check_authority",
            "country_aligned": "check_country",
            "freshness_days": "fresh_data",
            "revenue_sane": "revenue_ok",
            "employees_sane": "employees_ok",
            "hierarchy_conflict": "hierarchy_conflict",
            "enrichment_website_live": "enrich_live",
            "enrichment_https": "enrich_https",
            "enrichment_descriptions_match": "enrich_match",
            "enrichment_parked": "enrich_parked",
            "enrichment_non_english": "enrich_lang_review",
        }
        for key, label in mapping.items():
            if signals.get(key):
                chips.append(label)
        boost = signals.get("enrichment_boost")
        if isinstance(boost, (int, float)) and abs(boost) >= 0.01:
            chips.append(f"boost:{boost:+.2f}")
        match_score = signals.get("enrichment_match_score")
        if isinstance(match_score, (int, float)) and match_score > 0:
            chips.append(f"match:{match_score:.2f}")
        score_with_boost = signals.get("enrichment_score_with_boost")
        if isinstance(score_with_boost, (int, float)) and score_with_boost > 0:
            chips.append(f"score:{score_with_boost:.2f}")
        return chips


def _extract_candidate_domains(payload: Dict[str, Any]) -> List[str]:
    domains: List[str] = []
    for key in ("official_website", "website", "homepage"):
        value = payload.get(key)
        if isinstance(value, str) and value.strip():
            domains.append(value.strip())

    candidate = payload.get("candidate")
    if isinstance(candidate, dict):
        for key in ("official_website", "website"):
            value = candidate.get(key)
            if isinstance(value, str) and value.strip():
                domains.append(value.strip())

    return domains
