"""Field provenance persistence for scout and module observations."""

from __future__ import annotations

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

_PROVENANCE_SCHEMA_VERSION = 1
_VERSION_KEY = "provenance_schema_version"


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _normalize_text(value: Any) -> str:
    return str(value or "").strip()


def _normalize_scalar(value: Any) -> str | None:
    if value is None:
        return None
    if isinstance(value, str):
        normalized = value.strip()
        return normalized or None
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, (int, float)):
        return str(value)
    try:
        serialized = json.dumps(value, sort_keys=True, ensure_ascii=True)
    except Exception:
        serialized = str(value)
    serialized = serialized.strip()
    return serialized or None


def _row_to_dict(row: sqlite3.Row) -> dict[str, Any]:
    return {key: row[key] for key in row.keys()}


class FieldProvenanceStore:
    """SQLite-backed field observation tracking with freshness metadata."""

    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 _meta (
                    key TEXT PRIMARY KEY,
                    value TEXT NOT NULL,
                    updated_at TEXT NOT NULL
                )
                """
            )
            current = self._read_version(conn)
            while current < _PROVENANCE_SCHEMA_VERSION:
                if current == 0:
                    self._migrate_v1(conn)
                    current = 1
                else:
                    raise ValueError(f"Unsupported provenance schema version: {current}")
            self._write_version(conn, current)

    def _read_version(self, conn: sqlite3.Connection) -> int:
        row = conn.execute(
            "SELECT value FROM _meta WHERE key = ?",
            (_VERSION_KEY,),
        ).fetchone()
        if not row:
            return 0
        try:
            return int(row["value"])
        except Exception:
            return 0

    def _write_version(self, conn: sqlite3.Connection, version: int) -> None:
        now = _utc_now_iso()
        conn.execute(
            """
            INSERT INTO _meta(key, value, updated_at)
            VALUES (?, ?, ?)
            ON CONFLICT(key) DO UPDATE SET
                value = excluded.value,
                updated_at = excluded.updated_at
            """,
            (_VERSION_KEY, str(version), now),
        )

    def _migrate_v1(self, conn: sqlite3.Connection) -> None:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS field_provenance (
                provenance_id TEXT PRIMARY KEY,
                entity_id TEXT NOT NULL DEFAULT '',
                source_system TEXT NOT NULL,
                source_object TEXT NOT NULL,
                source_record_id TEXT NOT NULL,
                field_name TEXT NOT NULL,
                field_value TEXT,
                confidence REAL NOT NULL DEFAULT 1.0,
                last_observed_at TEXT NOT NULL,
                first_observed_at TEXT NOT NULL,
                observation_count INTEGER NOT NULL DEFAULT 1,
                updated_by TEXT
            )
            """
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_prov_entity ON field_provenance(entity_id)"
        )
        conn.execute(
            """
            CREATE INDEX IF NOT EXISTS idx_prov_field
            ON field_provenance(source_system, source_record_id, field_name)
            """
        )
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_prov_stale ON field_provenance(last_observed_at)"
        )

    def record_observation(
        self,
        *,
        source_system: str,
        source_object: str,
        source_record_id: str,
        field_name: str,
        field_value: Any,
        entity_id: str | None = None,
        confidence: float = 1.0,
        observed_at: str | None = None,
        updated_by: str | None = None,
    ) -> str:
        normalized_system = _normalize_text(source_system).lower()
        normalized_object = _normalize_text(source_object)
        normalized_record = _normalize_text(source_record_id)
        normalized_field = _normalize_text(field_name).lower()
        normalized_entity = _normalize_text(entity_id)
        normalized_observed_at = _normalize_text(observed_at) or _utc_now_iso()
        normalized_updated_by = _normalize_text(updated_by) or None
        normalized_value = _normalize_scalar(field_value)
        if not normalized_system or not normalized_object or not normalized_record or not normalized_field:
            raise ValueError(
                "source_system, source_object, source_record_id, and field_name are required"
            )

        try:
            confidence_value = float(confidence)
        except Exception:
            confidence_value = 1.0
        confidence_value = min(max(confidence_value, 0.0), 1.0)

        provenance_id = self._build_provenance_id(
            entity_id=normalized_entity,
            source_system=normalized_system,
            source_object=normalized_object,
            source_record_id=normalized_record,
            field_name=normalized_field,
        )

        with self._connect() as conn:
            existing = conn.execute(
                """
                SELECT first_observed_at, observation_count
                FROM field_provenance
                WHERE provenance_id = ?
                LIMIT 1
                """,
                (provenance_id,),
            ).fetchone()
            if existing:
                first_observed_at = str(existing["first_observed_at"] or normalized_observed_at)
                observation_count = int(existing["observation_count"] or 0) + 1
            else:
                first_observed_at = normalized_observed_at
                observation_count = 1

            conn.execute(
                """
                INSERT INTO field_provenance (
                    provenance_id,
                    entity_id,
                    source_system,
                    source_object,
                    source_record_id,
                    field_name,
                    field_value,
                    confidence,
                    last_observed_at,
                    first_observed_at,
                    observation_count,
                    updated_by
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT(provenance_id) DO UPDATE SET
                    entity_id = excluded.entity_id,
                    field_value = excluded.field_value,
                    confidence = excluded.confidence,
                    last_observed_at = excluded.last_observed_at,
                    first_observed_at = excluded.first_observed_at,
                    observation_count = excluded.observation_count,
                    updated_by = excluded.updated_by
                """,
                (
                    provenance_id,
                    normalized_entity,
                    normalized_system,
                    normalized_object,
                    normalized_record,
                    normalized_field,
                    normalized_value,
                    confidence_value,
                    normalized_observed_at,
                    first_observed_at,
                    observation_count,
                    normalized_updated_by,
                ),
            )
        return provenance_id

    def record_observations_batch(self, observations: list[dict[str, Any]]) -> int:
        recorded = 0
        for observation in observations:
            if not isinstance(observation, dict):
                continue
            confidence_raw = observation.get("confidence", 1.0)
            try:
                confidence_value = float(confidence_raw if confidence_raw is not None else 1.0)
            except Exception:
                confidence_value = 1.0
            self.record_observation(
                source_system=str(observation.get("source_system") or ""),
                source_object=str(observation.get("source_object") or ""),
                source_record_id=str(observation.get("source_record_id") or ""),
                field_name=str(observation.get("field_name") or ""),
                field_value=observation.get("field_value"),
                entity_id=str(observation.get("entity_id") or ""),
                confidence=confidence_value,
                observed_at=str(observation.get("observed_at") or ""),
                updated_by=str(observation.get("updated_by") or ""),
            )
            recorded += 1
        return recorded

    def get_authoritative_value(
        self,
        entity_id: str,
        field_name: str,
    ) -> dict[str, Any] | None:
        normalized_entity = _normalize_text(entity_id)
        normalized_field = _normalize_text(field_name).lower()
        if not normalized_entity or not normalized_field:
            return None
        with self._connect() as conn:
            row = conn.execute(
                """
                SELECT *
                FROM field_provenance
                WHERE entity_id = ? AND field_name = ?
                ORDER BY confidence DESC, last_observed_at DESC
                LIMIT 1
                """,
                (normalized_entity, normalized_field),
            ).fetchone()
            if row is None:
                return None
            return _row_to_dict(row)

    def detect_drift(self, entity_id: str, field_name: str) -> list[dict[str, Any]]:
        normalized_entity = _normalize_text(entity_id)
        normalized_field = _normalize_text(field_name).lower()
        if not normalized_entity or not normalized_field:
            return []

        with self._connect() as conn:
            rows = conn.execute(
                """
                SELECT *
                FROM field_provenance
                WHERE entity_id = ? AND field_name = ?
                ORDER BY last_observed_at DESC
                """,
                (normalized_entity, normalized_field),
            ).fetchall()

        values = {
            str(row["field_value"])
            for row in rows
            if row["field_value"] not in (None, "")
        }
        if len(values) <= 1:
            return []
        return [_row_to_dict(row) for row in rows]

    def list_entity_observations(
        self,
        entity_id: str,
        *,
        field_name: str | None = None,
        limit: int = 200,
    ) -> list[dict[str, Any]]:
        normalized_entity = _normalize_text(entity_id)
        normalized_field = _normalize_text(field_name).lower()
        if not normalized_entity:
            return []

        max_limit = max(1, min(int(limit), 1000))
        with self._connect() as conn:
            if normalized_field:
                rows = conn.execute(
                    """
                    SELECT *
                    FROM field_provenance
                    WHERE entity_id = ? AND field_name = ?
                    ORDER BY last_observed_at DESC, source_system ASC
                    LIMIT ?
                    """,
                    (normalized_entity, normalized_field, max_limit),
                ).fetchall()
            else:
                rows = conn.execute(
                    """
                    SELECT *
                    FROM field_provenance
                    WHERE entity_id = ?
                    ORDER BY last_observed_at DESC, field_name ASC, source_system ASC
                    LIMIT ?
                    """,
                    (normalized_entity, max_limit),
                ).fetchall()
            return [_row_to_dict(row) for row in rows]

    def stats(self) -> dict[str, int]:
        with self._connect() as conn:
            total = conn.execute("SELECT COUNT(1) AS count FROM field_provenance").fetchone()
            entities = conn.execute(
                """
                SELECT COUNT(DISTINCT CASE WHEN entity_id <> '' THEN entity_id END) AS count
                FROM field_provenance
                """
            ).fetchone()
            sources = conn.execute(
                "SELECT COUNT(DISTINCT source_system) AS count FROM field_provenance"
            ).fetchone()
            return {
                "observations": int(total["count"]) if total is not None else 0,
                "entities": int(entities["count"]) if entities is not None else 0,
                "source_systems": int(sources["count"]) if sources is not None else 0,
            }

    def source_system_counts(self) -> list[dict[str, Any]]:
        with self._connect() as conn:
            rows = conn.execute(
                """
                SELECT
                    source_system,
                    COUNT(1) AS observations,
                    COUNT(DISTINCT CASE WHEN entity_id <> '' THEN entity_id END) AS entities
                FROM field_provenance
                GROUP BY source_system
                ORDER BY observations DESC, source_system ASC
                """
            ).fetchall()
            return [_row_to_dict(row) for row in rows]

    def stale_fields(self, older_than_days: int = 90) -> list[dict[str, Any]]:
        days = max(int(older_than_days), 0)
        cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
        with self._connect() as conn:
            rows = conn.execute(
                """
                SELECT *
                FROM field_provenance
                WHERE last_observed_at < ?
                ORDER BY last_observed_at ASC
                """,
                (cutoff,),
            ).fetchall()
            return [_row_to_dict(row) for row in rows]

    def purge(self, older_than_days: int = 365) -> int:
        days = max(int(older_than_days), 0)
        cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
        with self._connect() as conn:
            cursor = conn.execute(
                "DELETE FROM field_provenance WHERE last_observed_at < ?",
                (cutoff,),
            )
            return int(cursor.rowcount or 0)

    def purge_source_system(self, source_system: str) -> int:
        normalized_system = _normalize_text(source_system).lower()
        if not normalized_system:
            return 0
        with self._connect() as conn:
            cursor = conn.execute(
                "DELETE FROM field_provenance WHERE source_system = ?",
                (normalized_system,),
            )
            return int(cursor.rowcount or 0)

    @staticmethod
    def _build_provenance_id(
        *,
        entity_id: str,
        source_system: str,
        source_object: str,
        source_record_id: str,
        field_name: str,
    ) -> str:
        base = "|".join(
            [
                entity_id,
                source_system,
                source_object,
                source_record_id,
                field_name,
            ]
        )
        return hashlib.sha256(base.encode("utf-8")).hexdigest()


__all__ = ["FieldProvenanceStore"]
