"""Deal Desk trust and document verification metrics."""

from __future__ import annotations

import json
import sqlite3
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Iterator

from .context import DealContextV1
from .history import document_evidence_escalated, summarize_close_document_evidence
from .reconciliation import ReconciliationPacketV1
from .store import DealDeskStore


def _utc_now() -> datetime:
    return datetime.now(timezone.utc)


def _parse_iso(value: Any) -> datetime | None:
    if not isinstance(value, str) or not value.strip():
        return None
    raw = value.strip()
    if raw.endswith("Z"):
        raw = raw[:-1] + "+00:00"
    try:
        parsed = datetime.fromisoformat(raw)
    except Exception:
        return None
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _iso(value: datetime) -> str:
    return value.astimezone(timezone.utc).isoformat()


def _round_float(value: float | None) -> float | None:
    if value is None:
        return None
    return round(float(value), 6)


def _rate(numerator: int, denominator: int) -> float | None:
    if denominator <= 0:
        return None
    return _round_float(float(numerator) / float(denominator))


def _count_values(values: list[str]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for value in values:
        normalized = str(value or "").strip()
        if not normalized:
            normalized = "unknown"
        counts[normalized] = counts.get(normalized, 0) + 1
    return counts


def _merge_count_maps(items: list[dict[str, int]]) -> dict[str, int]:
    merged: dict[str, int] = {}
    for item in items:
        for key, value in item.items():
            merged[key] = merged.get(key, 0) + int(value or 0)
    return merged


def _safe_int(value: Any, default: int = 0) -> int:
    try:
        return int(value)
    except Exception:
        return default


def _data_source_for_counts(*, history_count: int, legacy_count: int) -> str:
    if history_count and legacy_count:
        return "hybrid"
    if history_count:
        return "close_run_ledger"
    return "legacy_inferred_state"


@dataclass(frozen=True)
class _MetricRunSnapshot:
    source: str
    run_id: str
    packet_id: str | None
    entrypoint: str
    observed_at: str
    close_submitted_at: str | None
    overall_disposition: str
    close_workflow_state: str
    document_extraction_status: str
    document_escalated: bool
    document_evidence_row_count: int
    document_confidence_sum: float
    document_method_counts: dict[str, int]


def _day_key(value: str | None) -> str | None:
    parsed = _parse_iso(value)
    if parsed is None:
        return None
    return parsed.date().isoformat()


def _latest_packet_for_context(
    *,
    store: DealDeskStore,
    context: DealContextV1,
) -> ReconciliationPacketV1 | None:
    return store.load_latest_reconciliation_packet(
        opportunity_id=context.opportunity_id,
        crm_org_id=context.crm_org_id,
        org_alias=context.org_alias,
    )


def _history_snapshots(
    *,
    store: DealDeskStore,
    window_start: datetime,
) -> tuple[list[_MetricRunSnapshot], set[str]]:
    snapshots: list[_MetricRunSnapshot] = []
    packet_ids: set[str] = set()
    for run in store.list_close_certification_runs():
        observed_at = _parse_iso(run.triggered_at)
        if observed_at is None or observed_at < window_start:
            continue
        snapshots.append(
            _MetricRunSnapshot(
                source="close_run_ledger",
                run_id=run.run_id,
                packet_id=run.reconciliation_packet_id,
                entrypoint=str(run.entrypoint or "").strip() or "unknown",
                observed_at=run.triggered_at,
                close_submitted_at=run.close_submitted_at,
                overall_disposition=run.overall_disposition,
                close_workflow_state=run.close_workflow_state,
                document_extraction_status=run.document_extraction_status,
                document_escalated=bool(run.document_escalated),
                document_evidence_row_count=int(run.document_evidence_count or 0),
                document_confidence_sum=float(run.document_extraction_confidence_sum or 0.0),
                document_method_counts=dict(run.document_extraction_method_counts or {}),
            )
        )
        if run.reconciliation_packet_id:
            packet_ids.add(run.reconciliation_packet_id)
    return snapshots, packet_ids


def _legacy_snapshots(
    *,
    store: DealDeskStore,
    window_start: datetime,
    excluded_packet_ids: set[str],
) -> list[_MetricRunSnapshot]:
    snapshots: list[_MetricRunSnapshot] = []
    for context in store.list_contexts():
        packet = _latest_packet_for_context(store=store, context=context)
        if packet is None or packet.packet_id in excluded_packet_ids:
            continue
        submitted_at = _parse_iso(context.close_submitted_at)
        packet_generated_at = _parse_iso(packet.generated_at)
        entrypoint: str | None = None
        observed_at: str | None = None
        if submitted_at is not None and submitted_at >= window_start:
            entrypoint = "submitted_for_close"
            observed_at = context.close_submitted_at
        elif (
            context.close_submitted_at in (None, "")
            and packet_generated_at is not None
            and packet_generated_at >= window_start
        ):
            entrypoint = "closed_won_retro"
            observed_at = packet.generated_at
        if entrypoint is None or observed_at is None:
            continue
        evidence_summary = summarize_close_document_evidence(packet.document_evidence)
        snapshots.append(
            _MetricRunSnapshot(
                source="legacy_inferred_state",
                run_id=packet.packet_id,
                packet_id=packet.packet_id,
                entrypoint=entrypoint,
                observed_at=observed_at,
                close_submitted_at=context.close_submitted_at,
                overall_disposition=packet.overall_disposition,
                close_workflow_state=context.close_workflow_state,
                document_extraction_status=packet.document_extraction_status,
                document_escalated=document_evidence_escalated(
                    document_evidence_status=packet.document_evidence_status,
                    document_discrepancy_count=packet.document_discrepancy_count,
                    document_extraction_status=packet.document_extraction_status,
                ),
                document_evidence_row_count=evidence_summary.row_count,
                document_confidence_sum=evidence_summary.confidence_sum,
                document_method_counts=evidence_summary.method_counts,
            )
        )
    return snapshots


def _rollup_snapshots(
    snapshots: list[_MetricRunSnapshot],
) -> dict[str, Any]:
    history_run_count = sum(1 for item in snapshots if item.source == "close_run_ledger")
    legacy_run_count = sum(1 for item in snapshots if item.source == "legacy_inferred_state")
    submitted_runs = [item for item in snapshots if item.entrypoint == "submitted_for_close"]
    retro_runs = [item for item in snapshots if item.entrypoint == "closed_won_retro"]

    certification_pass_count = sum(
        1 for item in submitted_runs if item.overall_disposition == "queued_for_certification"
    )
    straight_through_progressed_count = sum(
        1
        for item in submitted_runs
        if item.overall_disposition == "queued_for_certification"
        and item.close_workflow_state in {"closed_won_progressed", "post_close_executed"}
    )
    post_close_executed_count = sum(
        1 for item in submitted_runs if item.close_workflow_state == "post_close_executed"
    )
    exception_count = sum(
        1 for item in submitted_runs if item.close_workflow_state == "exception_pending"
    )
    document_escalation_count = sum(1 for item in snapshots if item.document_escalated)

    evidence_row_count = sum(int(item.document_evidence_row_count or 0) for item in snapshots)
    confidence_sum = sum(float(item.document_confidence_sum or 0.0) for item in snapshots)
    confidence_avg = _round_float(confidence_sum / float(evidence_row_count)) if evidence_row_count else None

    return {
        "data_source": _data_source_for_counts(
            history_count=history_run_count,
            legacy_count=legacy_run_count,
        ),
        "history_run_count": history_run_count,
        "legacy_inferred_run_count": legacy_run_count,
        "submitted_for_close_count": len(submitted_runs),
        "retro_validation_count": len(retro_runs),
        "run_count": len(snapshots),
        "certification_pass_count": certification_pass_count,
        "certification_pass_rate": _rate(certification_pass_count, len(submitted_runs)),
        "straight_through_progressed_count": straight_through_progressed_count,
        "straight_through_progression_rate": _rate(straight_through_progressed_count, len(submitted_runs)),
        "post_close_executed_count": post_close_executed_count,
        "post_close_execution_rate": _rate(post_close_executed_count, len(submitted_runs)),
        "exception_count": exception_count,
        "exception_rate": _rate(exception_count, len(submitted_runs)),
        "document_escalation_count": document_escalation_count,
        "document_escalation_rate": _rate(document_escalation_count, len(snapshots)),
        "document_extraction_confidence_avg": confidence_avg,
        "document_evidence_row_count": evidence_row_count,
        "disposition_counts": _count_values([item.overall_disposition for item in snapshots]),
        "close_workflow_state_counts": _count_values([item.close_workflow_state for item in submitted_runs]),
        "document_extraction_status_counts": _count_values([item.document_extraction_status for item in snapshots]),
        "document_extraction_method_counts": _merge_count_maps([item.document_method_counts for item in snapshots]),
    }


def _build_history(
    snapshots: list[_MetricRunSnapshot],
) -> list[dict[str, Any]]:
    by_day: dict[str, list[_MetricRunSnapshot]] = {}
    for item in snapshots:
        key = _day_key(item.observed_at)
        if not key:
            continue
        by_day.setdefault(key, []).append(item)
    history: list[dict[str, Any]] = []
    for key in sorted(by_day):
        rolled = _rollup_snapshots(by_day[key])
        rolled["day_key"] = key
        history.append(rolled)
    return history


def _snapshot_summary(payload: dict[str, Any]) -> dict[str, Any]:
    return {
        "data_source": str(payload.get("data_source") or "").strip() or "legacy_inferred_state",
        "history_run_count": _safe_int(payload.get("history_run_count"), default=0),
        "legacy_inferred_run_count": _safe_int(payload.get("legacy_inferred_run_count"), default=0),
        "submitted_for_close_count": _safe_int(payload.get("submitted_for_close_count"), default=0),
        "retro_validation_count": _safe_int(payload.get("retro_validation_count"), default=0),
        "run_count": _safe_int(payload.get("run_count"), default=0),
        "certification_pass_count": _safe_int(payload.get("certification_pass_count"), default=0),
        "certification_pass_rate": _round_float(payload.get("certification_pass_rate")),
        "straight_through_progressed_count": _safe_int(
            payload.get("straight_through_progressed_count"),
            default=0,
        ),
        "straight_through_progression_rate": _round_float(payload.get("straight_through_progression_rate")),
        "post_close_executed_count": _safe_int(payload.get("post_close_executed_count"), default=0),
        "post_close_execution_rate": _round_float(payload.get("post_close_execution_rate")),
        "exception_count": _safe_int(payload.get("exception_count"), default=0),
        "exception_rate": _round_float(payload.get("exception_rate")),
        "document_escalation_count": _safe_int(payload.get("document_escalation_count"), default=0),
        "document_escalation_rate": _round_float(payload.get("document_escalation_rate")),
        "document_extraction_confidence_avg": _round_float(
            payload.get("document_extraction_confidence_avg")
        ),
        "document_evidence_row_count": _safe_int(payload.get("document_evidence_row_count"), default=0),
        "disposition_counts": dict(payload.get("disposition_counts") or {}),
        "close_workflow_state_counts": dict(payload.get("close_workflow_state_counts") or {}),
        "document_extraction_status_counts": dict(payload.get("document_extraction_status_counts") or {}),
        "document_extraction_method_counts": dict(payload.get("document_extraction_method_counts") or {}),
    }


def _average(values: list[Any]) -> float | None:
    numeric = [float(value) for value in values if isinstance(value, (int, float))]
    if not numeric:
        return None
    return _round_float(sum(numeric) / float(len(numeric)))


def _delta(current: Any, baseline: Any) -> float | None:
    if not isinstance(current, (int, float)) or not isinstance(baseline, (int, float)):
        return None
    return _round_float(float(current) - float(baseline))


def _current_snapshot_from_payload(payload: dict[str, Any]) -> dict[str, Any]:
    latest = payload.get("latest_saved_snapshot") if isinstance(payload.get("latest_saved_snapshot"), dict) else None
    if latest is not None:
        return dict(latest)
    return {
        "snapshot_id": str(payload.get("snapshot_id") or "").strip() or None,
        "day_key": str(payload.get("day_key") or "").strip() or None,
        "lookback_days": _safe_int(payload.get("lookback_days"), default=0),
        "generated_at": str(payload.get("generated_at") or "").strip() or None,
        **_snapshot_summary(payload),
    }


def _baseline_from_snapshots(
    snapshots: list[dict[str, Any]],
    *,
    baseline_days: int,
) -> dict[str, Any]:
    window = [item for item in snapshots[1 : 1 + max(1, int(baseline_days))] if isinstance(item, dict)]
    if not window:
        return {
            "snapshot_count": 0,
            "start_day_key": None,
            "end_day_key": None,
            "run_count_avg": None,
            "submitted_for_close_count_avg": None,
            "straight_through_progression_rate_avg": None,
            "exception_rate_avg": None,
            "document_escalation_rate_avg": None,
            "document_extraction_confidence_avg_avg": None,
            "document_extraction_method_counts": {},
            "document_extraction_status_counts": {},
        }
    return {
        "snapshot_count": len(window),
        "start_day_key": str(window[-1].get("day_key") or "").strip() or None,
        "end_day_key": str(window[0].get("day_key") or "").strip() or None,
        "run_count_avg": _average([item.get("run_count") for item in window]),
        "submitted_for_close_count_avg": _average([item.get("submitted_for_close_count") for item in window]),
        "straight_through_progression_rate_avg": _average(
            [item.get("straight_through_progression_rate") for item in window]
        ),
        "exception_rate_avg": _average([item.get("exception_rate") for item in window]),
        "document_escalation_rate_avg": _average([item.get("document_escalation_rate") for item in window]),
        "document_extraction_confidence_avg_avg": _average(
            [item.get("document_extraction_confidence_avg") for item in window]
        ),
        "document_extraction_method_counts": _merge_count_maps(
            [
                dict(item.get("document_extraction_method_counts") or {})
                for item in window
                if isinstance(item.get("document_extraction_method_counts"), dict)
            ]
        ),
        "document_extraction_status_counts": _merge_count_maps(
            [
                dict(item.get("document_extraction_status_counts") or {})
                for item in window
                if isinstance(item.get("document_extraction_status_counts"), dict)
            ]
        ),
    }


def compute_dealdesk_metrics(
    state_dir: str | Path | None,
    *,
    lookback_days: int = 30,
    now: datetime | str | None = None,
) -> dict[str, Any]:
    store = DealDeskStore(state_dir)
    lookback = max(1, int(lookback_days))
    current_time = _parse_iso(now) if isinstance(now, str) else now
    now_dt = current_time if isinstance(current_time, datetime) else _utc_now()
    window_start = now_dt - timedelta(days=lookback)

    history_runs, packet_ids = _history_snapshots(
        store=store,
        window_start=window_start,
    )
    legacy_runs = _legacy_snapshots(
        store=store,
        window_start=window_start,
        excluded_packet_ids=packet_ids,
    )
    all_runs = history_runs + legacy_runs
    current = _rollup_snapshots(all_runs)
    return {
        "snapshot_id": f"dealdesk:{lookback}:{window_start.date().isoformat()}",
        "generated_at": _iso(now_dt),
        "day_key": now_dt.date().isoformat(),
        "lookback_days": lookback,
        "window_start": _iso(window_start),
        "history_granularity": "day",
        "history": _build_history(all_runs),
        **current,
    }


class DealDeskMetricStore:
    """Persistence for daily Deal Desk metric snapshots."""

    def __init__(self, db_path: Path | str):
        self.db_path = Path(db_path).resolve()
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        self._init_db()

    @contextmanager
    def _connect(self) -> Iterator[sqlite3.Connection]:
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA busy_timeout=5000")
        try:
            yield conn
            conn.commit()
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    def _init_db(self) -> None:
        with self._connect() as conn:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS dealdesk_metric_snapshots (
                    snapshot_id TEXT PRIMARY KEY,
                    day_key TEXT NOT NULL,
                    lookback_days INTEGER NOT NULL,
                    generated_at TEXT NOT NULL,
                    data_source TEXT NOT NULL,
                    run_count INTEGER NOT NULL,
                    payload_json TEXT NOT NULL,
                    UNIQUE(day_key, lookback_days)
                )
                """
            )
            conn.execute(
                """
                CREATE INDEX IF NOT EXISTS idx_dealdesk_metric_snapshots_generated
                ON dealdesk_metric_snapshots(generated_at DESC)
                """
            )

    def save_snapshot(self, payload: dict[str, Any]) -> str:
        day_key = str(payload.get("day_key") or "").strip() or _utc_now().date().isoformat()
        lookback_days = max(1, _safe_int(payload.get("lookback_days"), default=30))
        generated_at = str(payload.get("generated_at") or "").strip() or _iso(_utc_now())
        snapshot_id = str(payload.get("snapshot_id") or "").strip() or f"dealdesk:{lookback_days}:{day_key}"
        data_source = str(payload.get("data_source") or "").strip() or "legacy_inferred_state"
        run_count = _safe_int(payload.get("run_count"), default=0)
        payload_json = json.dumps(payload, sort_keys=True, ensure_ascii=True)
        with self._connect() as conn:
            conn.execute(
                """
                INSERT INTO dealdesk_metric_snapshots (
                    snapshot_id,
                    day_key,
                    lookback_days,
                    generated_at,
                    data_source,
                    run_count,
                    payload_json
                )
                VALUES (?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT(day_key, lookback_days) DO UPDATE SET
                    snapshot_id = excluded.snapshot_id,
                    generated_at = excluded.generated_at,
                    data_source = excluded.data_source,
                    run_count = excluded.run_count,
                    payload_json = excluded.payload_json
                """,
                (
                    snapshot_id,
                    day_key,
                    lookback_days,
                    generated_at,
                    data_source,
                    run_count,
                    payload_json,
                ),
            )
        return snapshot_id

    def list_snapshots(
        self,
        *,
        lookback_days: int | None = None,
        limit: int = 30,
    ) -> list[dict[str, Any]]:
        bounded_limit = max(1, min(int(limit), 365))
        with self._connect() as conn:
            if lookback_days is None:
                rows = conn.execute(
                    """
                    SELECT snapshot_id, day_key, lookback_days, generated_at, data_source, run_count, payload_json
                    FROM dealdesk_metric_snapshots
                    ORDER BY generated_at DESC
                    LIMIT ?
                    """,
                    (bounded_limit,),
                ).fetchall()
            else:
                rows = conn.execute(
                    """
                    SELECT snapshot_id, day_key, lookback_days, generated_at, data_source, run_count, payload_json
                    FROM dealdesk_metric_snapshots
                    WHERE lookback_days = ?
                    ORDER BY generated_at DESC
                    LIMIT ?
                    """,
                    (int(lookback_days), bounded_limit),
                ).fetchall()

        snapshots: list[dict[str, Any]] = []
        for row in rows:
            try:
                payload = json.loads(row["payload_json"] or "{}")
            except Exception:
                payload = {}
            payload_dict = payload if isinstance(payload, dict) else {}
            snapshots.append(
                {
                    "snapshot_id": row["snapshot_id"],
                    "day_key": row["day_key"],
                    "lookback_days": int(row["lookback_days"] or 0),
                    "generated_at": row["generated_at"],
                    **_snapshot_summary(payload_dict),
                }
            )
        return snapshots

    def latest_snapshot(self, *, lookback_days: int | None = None) -> dict[str, Any] | None:
        snapshots = self.list_snapshots(lookback_days=lookback_days, limit=1)
        return snapshots[0] if snapshots else None


def collect_dealdesk_metrics(
    state_dir: Path | str,
    *,
    lookback_days: int = 30,
    now: datetime | str | None = None,
) -> dict[str, Any]:
    resolved_state_dir = Path(state_dir).resolve()
    metrics = compute_dealdesk_metrics(
        resolved_state_dir,
        lookback_days=lookback_days,
        now=now,
    )
    store = DealDeskMetricStore(resolved_state_dir / "db" / "dealdesk_metrics.sqlite")
    snapshot_id = store.save_snapshot(metrics)
    return {
        **metrics,
        "snapshot_id": snapshot_id,
        "latest_saved_snapshot": store.latest_snapshot(lookback_days=lookback_days),
        "saved_snapshots": store.list_snapshots(lookback_days=lookback_days, limit=30),
    }


def build_dealdesk_metrics_drilldown(
    state_dir: Path | str,
    *,
    lookback_days: int = 30,
    baseline_days: int = 7,
    history_limit: int = 14,
    now: datetime | str | None = None,
) -> dict[str, Any]:
    bounded_baseline = max(1, min(int(baseline_days), 30))
    bounded_history = max(1, min(int(history_limit), 90))
    payload = collect_dealdesk_metrics(
        state_dir,
        lookback_days=lookback_days,
        now=now,
    )
    current = _current_snapshot_from_payload(payload)
    saved_snapshots = [
        item
        for item in (payload.get("saved_snapshots") or [])[:bounded_history]
        if isinstance(item, dict)
    ]
    baseline = _baseline_from_snapshots(saved_snapshots, baseline_days=bounded_baseline)
    comparison = {
        "current_day_key": current.get("day_key"),
        "baseline_days": bounded_baseline,
        "baseline_snapshot_count": baseline.get("snapshot_count"),
        "deltas": {
            "run_count": _delta(current.get("run_count"), baseline.get("run_count_avg")),
            "submitted_for_close_count": _delta(
                current.get("submitted_for_close_count"),
                baseline.get("submitted_for_close_count_avg"),
            ),
            "straight_through_progression_rate": _delta(
                current.get("straight_through_progression_rate"),
                baseline.get("straight_through_progression_rate_avg"),
            ),
            "exception_rate": _delta(
                current.get("exception_rate"),
                baseline.get("exception_rate_avg"),
            ),
            "document_escalation_rate": _delta(
                current.get("document_escalation_rate"),
                baseline.get("document_escalation_rate_avg"),
            ),
            "document_extraction_confidence_avg": _delta(
                current.get("document_extraction_confidence_avg"),
                baseline.get("document_extraction_confidence_avg_avg"),
            ),
        },
    }
    return {
        "status": "ok" if _safe_int(current.get("run_count"), 0) > 0 else "empty",
        "generated_at": str(payload.get("generated_at") or "").strip() or None,
        "snapshot_id": current.get("snapshot_id"),
        "lookback_days": _safe_int(payload.get("lookback_days"), default=max(1, int(lookback_days))),
        "baseline_days": bounded_baseline,
        "history_limit": bounded_history,
        "current": current,
        "baseline": baseline,
        "comparison": comparison,
        "saved_snapshots": saved_snapshots,
        "activity_history": list(payload.get("history") or []),
    }


__all__ = [
    "DealDeskMetricStore",
    "build_dealdesk_metrics_drilldown",
    "collect_dealdesk_metrics",
    "compute_dealdesk_metrics",
]
