"""Cross-system canonical field drift check."""

from __future__ import annotations

import os
import sqlite3
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Tuple

from .base import BaseScoutCheck
from ..crm_provider import FilterExpr
from ..models import EvidenceRow, RecommendedAction, ScoutFinding


class CrossSystemFieldDriftCheck(BaseScoutCheck):
    _DEFAULT_CANONICAL_FIELDS = ("owner_id", "lifecycle_stage", "domain", "region", "icp_flag")

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self._recommended_actions: list[RecommendedAction] = []
        self._action_keys: set[tuple[str, str]] = set()

    def build_count_filters(self) -> Tuple[str, List[FilterExpr]]:
        # Custom execute path: drift comes from substrate persistence, not direct CRM filters.
        return "Lead", []

    def build_sample_request(self) -> Tuple[str, List[str], List[FilterExpr], str | None, str, bool]:
        # Custom execute path: drift comes from substrate persistence, not direct CRM queries.
        return "Lead", ["Id"], [], None, "ASC", False

    def evaluate(self, rows: List[Dict[str, Any]]) -> List[EvidenceRow]:
        del rows
        return []

    def execute(self) -> ScoutFinding:  # type: ignore[override]
        start = time.perf_counter()
        self._recommended_actions = []
        self._action_keys = set()

        db_path = self._resolve_db_path()
        if db_path is None or not db_path.exists():
            return self._ok_finding(
                total_records_scanned=0,
                evidence=[],
                warning=(
                    "Field drift scout skipped because substrate database is unavailable. "
                    "Run DNC/sequence/customer/follow-up scouts first to populate provenance."
                ),
                duration_ms=int((time.perf_counter() - start) * 1000),
            )

        try:
            from g_gremlin.autopilot.field_provenance import FieldProvenanceStore

            store = FieldProvenanceStore(db_path)
        except Exception as exc:
            return self._error_finding(str(exc), duration_ms=int((time.perf_counter() - start) * 1000))

        canonical_fields = self._canonical_fields()
        min_sources = max(2, self._to_int(self.params.get("min_source_systems"), default=2))
        max_entities = max(self.top_n, self._to_int(self.params.get("max_entities"), default=max(self.top_n * 10, 200)))

        entity_ids = self._candidate_entity_ids(db_path, canonical_fields, max_entities)
        evidence_all: list[EvidenceRow] = []

        for entity_id in entity_ids:
            for field_name in canonical_fields:
                drift_rows = store.detect_drift(entity_id, field_name)
                if not drift_rows:
                    continue

                source_systems = sorted(
                    {
                        str(row.get("source_system") or "").strip().lower()
                        for row in drift_rows
                        if str(row.get("source_system") or "").strip()
                    }
                )
                if len(source_systems) < min_sources:
                    continue

                value_by_source = self._value_by_source(drift_rows)
                authoritative = self._authoritative_row(drift_rows)
                related_ids = self._unique_non_blank(
                    [str(row.get("source_record_id") or "") for row in drift_rows]
                )
                reason = (
                    f"Canonical field '{field_name}' has conflicting values across "
                    f"{len(source_systems)} source systems."
                )

                evidence_all.append(
                    EvidenceRow(
                        record_id=entity_id,
                        sobject="entity",
                        fields={
                            "entity_id": entity_id,
                            "field_name": field_name,
                            "source_systems": source_systems,
                            "values_by_source": value_by_source,
                            "authoritative_value": authoritative.get("field_value"),
                            "authoritative_source_system": authoritative.get("source_system"),
                            "authoritative_last_observed_at": authoritative.get("last_observed_at"),
                        },
                        reason=reason,
                        reason_code="CROSS_SYSTEM_FIELD_DRIFT",
                        severity=self.definition.severity_default,
                        related_ids=related_ids,
                    )
                )
                self._queue_action(entity_id, field_name, source_systems, value_by_source)

        findings_count = len(evidence_all)
        evidence = evidence_all[: self.top_n]
        status = "findings" if findings_count > 0 else "ok"
        duration_ms = int((time.perf_counter() - start) * 1000)
        return ScoutFinding(
            scout_name=self.definition.name,
            display_name=self.definition.display_name,
            description=self.definition.description,
            category=self.definition.category,
            severity=self.definition.severity_default,
            total_records_scanned=len(entity_ids),
            findings_count=findings_count,
            evidence=evidence,
            recommended_actions=self.get_recommended_actions(),
            soql_queries=self._soql_queries,
            duration_ms=duration_ms,
            parameters_used=self.params,
            status=status,
            warnings=self._warnings,
        )

    def get_recommended_actions(self) -> List[RecommendedAction]:
        return list(self._recommended_actions)

    def _queue_action(
        self,
        entity_id: str,
        field_name: str,
        source_systems: list[str],
        values_by_source: dict[str, Any],
    ) -> None:
        key = (entity_id, field_name)
        if key in self._action_keys:
            return
        self._action_keys.add(key)
        self._recommended_actions.append(
            RecommendedAction(
                action_type="route_exception",
                label="Route cross-system field drift to owner",
                risk_level="low",
                details={
                    "reason_code": "CROSS_SYSTEM_FIELD_DRIFT",
                    "entity_id": entity_id,
                    "field_name": field_name,
                    "source_systems": source_systems,
                    "values_by_source": values_by_source,
                },
            )
        )

    def _candidate_entity_ids(self, db_path: Path, fields: list[str], limit: int) -> list[str]:
        if not fields:
            return []
        placeholders = ", ".join(["?"] * len(fields))
        query = (
            "SELECT DISTINCT entity_id "
            "FROM field_provenance "
            "WHERE entity_id <> '' AND field_name IN ({fields}) "
            "ORDER BY entity_id ASC "
            "LIMIT ?"
        ).format(fields=placeholders)
        try:
            conn = sqlite3.connect(db_path)
            try:
                rows = conn.execute(query, (*fields, int(limit))).fetchall()
            finally:
                conn.close()
        except Exception as exc:
            self._warnings.append(f"Unable to query provenance entities: {exc}")
            return []

        entity_ids: list[str] = []
        for row in rows:
            entity_id = str(row[0] or "").strip()
            if entity_id:
                entity_ids.append(entity_id)
        return entity_ids

    def _canonical_fields(self) -> list[str]:
        configured = self.params.get("canonical_fields") or list(self._DEFAULT_CANONICAL_FIELDS)
        ordered: list[str] = []
        for value in configured:
            field_name = str(value or "").strip().lower()
            if field_name and field_name not in ordered:
                ordered.append(field_name)
        return ordered

    def _resolve_db_path(self) -> Path | None:
        explicit = str(self.params.get("db_path") or "").strip()
        if explicit:
            return Path(explicit).expanduser().resolve()
        if self.state_dir is not None:
            return (self.state_dir / "db" / "platform.sqlite").resolve()
        state_dir_env = os.getenv("G_GREMLIN_STATE_DIR", "").strip()
        if state_dir_env:
            return (Path(state_dir_env).expanduser().resolve() / "db" / "platform.sqlite").resolve()
        return (Path.home() / ".g_gremlin" / "autopilot" / "db" / "platform.sqlite").resolve()

    def _authoritative_row(self, rows: list[dict[str, Any]]) -> dict[str, Any]:
        if not rows:
            return {}

        def _sort_key(row: dict[str, Any]) -> tuple[float, float]:
            try:
                confidence = float(row.get("confidence") or 0.0)
            except Exception:
                confidence = 0.0
            observed_at = self._parse_iso(row.get("last_observed_at"))
            ts = observed_at.timestamp() if observed_at is not None else 0.0
            return confidence, ts

        return max(rows, key=_sort_key)

    def _value_by_source(self, rows: list[dict[str, Any]]) -> dict[str, Any]:
        values: dict[str, Any] = {}
        by_source: dict[str, list[dict[str, Any]]] = {}
        for row in rows:
            source = str(row.get("source_system") or "").strip().lower()
            if not source:
                continue
            by_source.setdefault(source, []).append(row)
        for source, source_rows in by_source.items():
            latest = max(
                source_rows,
                key=lambda item: (
                    self._parse_iso(item.get("last_observed_at")).timestamp()
                    if self._parse_iso(item.get("last_observed_at")) is not None
                    else 0.0
                ),
            )
            values[source] = latest.get("field_value")
        return values

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

    @staticmethod
    def _to_int(value: Any, *, default: int) -> int:
        try:
            return int(value)
        except Exception:
            return default

    @staticmethod
    def _unique_non_blank(values: list[str]) -> list[str]:
        ordered: list[str] = []
        seen: set[str] = set()
        for raw in values:
            value = str(raw or "").strip()
            if not value or value in seen:
                continue
            seen.add(value)
            ordered.append(value)
        return ordered

    def _ok_finding(
        self,
        *,
        total_records_scanned: int,
        evidence: list[EvidenceRow],
        warning: str | None,
        duration_ms: int,
    ) -> ScoutFinding:
        warnings = list(self._warnings)
        if warning:
            warnings.append(warning)
        return ScoutFinding(
            scout_name=self.definition.name,
            display_name=self.definition.display_name,
            description=self.definition.description,
            category=self.definition.category,
            severity=self.definition.severity_default,
            total_records_scanned=total_records_scanned,
            findings_count=len(evidence),
            evidence=evidence,
            recommended_actions=self.get_recommended_actions(),
            soql_queries=self._soql_queries,
            duration_ms=duration_ms,
            parameters_used=self.params,
            status="ok",
            warnings=warnings,
        )


__all__ = ["CrossSystemFieldDriftCheck"]
