"""Persistent queue for inbound CRM webhook events."""

from __future__ import annotations

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


DEFAULT_MAX_ATTEMPTS = 5
DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 120


def event_queue_db_path(state_dir: Path | str) -> Path:
    return Path(state_dir) / "db" / "crm_event_queue.sqlite"


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


def _utc_now_iso() -> str:
    return _utc_now().isoformat()


def _parse_iso(value: str | None) -> datetime | None:
    if not value:
        return None
    try:
        return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
    except Exception:
        return None


def _to_json(payload: dict[str, Any]) -> str:
    return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)


def _from_json(raw: str | None) -> dict[str, Any]:
    if not raw:
        return {}
    try:
        payload = json.loads(raw)
    except Exception:
        return {}
    return payload if isinstance(payload, dict) else {}


class CrmEventQueue:
    """SQLite-backed queue with at-least-once delivery semantics."""

    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, timeout=10)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA busy_timeout=10000")
        try:
            yield conn
            conn.commit()
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    def _init_db(self) -> None:
        with self._connect() as conn:
            conn.executescript(
                """
                CREATE TABLE IF NOT EXISTS crm_event_queue (
                    event_id TEXT PRIMARY KEY,
                    provider TEXT NOT NULL,
                    source_kind TEXT NOT NULL,
                    source_event_id TEXT NOT NULL,
                    event_type TEXT NOT NULL,
                    payload_json TEXT NOT NULL,
                    metadata_json TEXT NOT NULL,
                    status TEXT NOT NULL,
                    attempts INTEGER NOT NULL DEFAULT 0,
                    max_attempts INTEGER NOT NULL DEFAULT 5,
                    leased_until TEXT,
                    processing_outcome TEXT,
                    finding_run_id TEXT,
                    receipt_run_id TEXT,
                    last_error TEXT,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL,
                    processed_at TEXT
                );

                CREATE UNIQUE INDEX IF NOT EXISTS idx_crm_event_queue_dedupe
                    ON crm_event_queue(provider, source_event_id);

                CREATE INDEX IF NOT EXISTS idx_crm_event_queue_status_created
                    ON crm_event_queue(status, created_at ASC, event_id ASC);
                """
            )

    def enqueue_event(
        self,
        *,
        provider: str,
        source_kind: str,
        source_event_id: str,
        event_type: str,
        payload: dict[str, Any],
        metadata: dict[str, Any] | None = None,
        max_attempts: int = DEFAULT_MAX_ATTEMPTS,
    ) -> dict[str, Any]:
        event_id = str(uuid.uuid4())
        now = _utc_now_iso()
        normalized_provider = str(provider or "").strip().lower() or "unknown"
        normalized_source_kind = str(source_kind or "").strip().lower() or "unknown"
        normalized_source_event_id = str(source_event_id or "").strip()
        normalized_event_type = str(event_type or "").strip().lower() or "unknown"
        bounded_attempts = max(1, int(max_attempts))

        if not normalized_source_event_id:
            raise ValueError("source_event_id is required for replay-safe dedupe")

        try:
            with self._connect() as conn:
                conn.execute(
                    """
                    INSERT INTO crm_event_queue (
                        event_id,
                        provider,
                        source_kind,
                        source_event_id,
                        event_type,
                        payload_json,
                        metadata_json,
                        status,
                        attempts,
                        max_attempts,
                        leased_until,
                        processing_outcome,
                        finding_run_id,
                        receipt_run_id,
                        last_error,
                        created_at,
                        updated_at,
                        processed_at
                    ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, NULL, NULL, NULL, NULL, NULL, ?, ?, NULL)
                    """,
                    (
                        event_id,
                        normalized_provider,
                        normalized_source_kind,
                        normalized_source_event_id,
                        normalized_event_type,
                        _to_json(payload),
                        _to_json(metadata or {}),
                        bounded_attempts,
                        now,
                        now,
                    ),
                )
        except sqlite3.IntegrityError:
            existing = self.find_by_source(
                provider=normalized_provider,
                source_event_id=normalized_source_event_id,
            )
            if existing is None:
                raise
            existing["deduped"] = True
            return existing

        row = self.get_event(event_id)
        if row is None:
            raise RuntimeError("queued event could not be loaded")
        row["deduped"] = False
        return row

    def find_by_source(self, *, provider: str, source_event_id: str) -> dict[str, Any] | None:
        with self._connect() as conn:
            row = conn.execute(
                """
                SELECT * FROM crm_event_queue
                WHERE provider = ? AND source_event_id = ?
                LIMIT 1
                """,
                (str(provider or "").strip().lower(), str(source_event_id or "").strip()),
            ).fetchone()
        return self._row_to_event(row)

    def get_event(self, event_id: str) -> dict[str, Any] | None:
        with self._connect() as conn:
            row = conn.execute(
                "SELECT * FROM crm_event_queue WHERE event_id = ? LIMIT 1",
                (str(event_id or "").strip(),),
            ).fetchone()
        return self._row_to_event(row)

    def list_events(
        self,
        *,
        status: str | None = None,
        provider: str | None = None,
        source_kind: str | None = None,
        limit: int = 100,
    ) -> list[dict[str, Any]]:
        bounded_limit = min(max(int(limit), 1), 500)
        clauses: list[str] = []
        params: list[Any] = []
        normalized_status = str(status or "").strip().lower() or None
        normalized_provider = str(provider or "").strip().lower() or None
        normalized_source_kind = str(source_kind or "").strip().lower() or None

        if normalized_status:
            clauses.append("status = ?")
            params.append(normalized_status)
        if normalized_provider:
            clauses.append("provider = ?")
            params.append(normalized_provider)
        if normalized_source_kind:
            clauses.append("source_kind = ?")
            params.append(normalized_source_kind)

        query = "SELECT * FROM crm_event_queue"
        if clauses:
            query += " WHERE " + " AND ".join(clauses)
        query += " ORDER BY updated_at DESC, created_at DESC, event_id DESC LIMIT ?"
        params.append(bounded_limit)

        with self._connect() as conn:
            rows = conn.execute(query, tuple(params)).fetchall()
        return [item for item in (self._row_to_event(row) for row in rows) if item is not None]

    def lease_next(self, *, visibility_timeout_seconds: int = DEFAULT_VISIBILITY_TIMEOUT_SECONDS) -> dict[str, Any] | None:
        with self._connect() as conn:
            now = datetime.now(timezone.utc)
            now_iso = now.isoformat()
            row = conn.execute(
                """
                SELECT event_id
                FROM crm_event_queue
                WHERE attempts < max_attempts
                  AND (
                        status = 'pending'
                        OR (status = 'processing' AND (leased_until IS NULL OR leased_until < ?))
                      )
                ORDER BY created_at ASC, event_id ASC
                LIMIT 1
                """,
                (now_iso,),
            ).fetchone()
            if row is None:
                return None
            return self._lease_event_locked(
                conn,
                event_id=str(row["event_id"]),
                visibility_timeout_seconds=visibility_timeout_seconds,
            )

    def lease_event(
        self,
        event_id: str,
        *,
        visibility_timeout_seconds: int = DEFAULT_VISIBILITY_TIMEOUT_SECONDS,
    ) -> dict[str, Any] | None:
        with self._connect() as conn:
            return self._lease_event_locked(
                conn,
                event_id=str(event_id or "").strip(),
                visibility_timeout_seconds=visibility_timeout_seconds,
            )

    def _lease_event_locked(
        self,
        conn: sqlite3.Connection,
        *,
        event_id: str,
        visibility_timeout_seconds: int,
    ) -> dict[str, Any] | None:
        if not event_id:
            return None
        now = datetime.now(timezone.utc)
        now_iso = now.isoformat()
        lease_until = (now + timedelta(seconds=max(1, int(visibility_timeout_seconds)))).isoformat()
        cursor = conn.execute(
            """
            UPDATE crm_event_queue
            SET
                status = 'processing',
                attempts = attempts + 1,
                leased_until = ?,
                updated_at = ?
            WHERE event_id = ?
              AND attempts < max_attempts
              AND (
                    status = 'pending'
                    OR (status = 'processing' AND (leased_until IS NULL OR leased_until < ?))
                  )
            """,
            (lease_until, now_iso, event_id, now_iso),
        )
        if cursor.rowcount <= 0:
            return None
        row = conn.execute(
            "SELECT * FROM crm_event_queue WHERE event_id = ? LIMIT 1",
            (event_id,),
        ).fetchone()
        return self._row_to_event(row)

    def mark_processed(
        self,
        *,
        event_id: str,
        processing_outcome: str,
        finding_run_id: str | None = None,
        receipt_run_id: str | None = None,
    ) -> None:
        now = _utc_now_iso()
        with self._connect() as conn:
            conn.execute(
                """
                UPDATE crm_event_queue
                SET
                    status = 'processed',
                    leased_until = NULL,
                    processing_outcome = ?,
                    finding_run_id = ?,
                    receipt_run_id = ?,
                    last_error = NULL,
                    processed_at = ?,
                    updated_at = ?
                WHERE event_id = ?
                """,
                (
                    str(processing_outcome or "").strip() or "processed",
                    str(finding_run_id or "").strip() or None,
                    str(receipt_run_id or "").strip() or None,
                    now,
                    now,
                    str(event_id or "").strip(),
                ),
            )

    def mark_failed(
        self,
        *,
        event_id: str,
        error: str,
        retryable: bool = True,
    ) -> str:
        row = self.get_event(event_id)
        if row is None:
            return "missing"
        attempts = int(row.get("attempts") or 0)
        max_attempts = int(row.get("max_attempts") or DEFAULT_MAX_ATTEMPTS)
        should_dead_letter = (not retryable) or attempts >= max_attempts
        next_status = "dead_letter" if should_dead_letter else "pending"
        now = _utc_now_iso()
        with self._connect() as conn:
            conn.execute(
                """
                UPDATE crm_event_queue
                SET
                    status = ?,
                    leased_until = NULL,
                    last_error = ?,
                    updated_at = ?
                WHERE event_id = ?
                """,
                (
                    next_status,
                    str(error or "").strip()[:2000],
                    now,
                    str(event_id or "").strip(),
                ),
            )
        return next_status

    def queue_counts(self) -> dict[str, int]:
        with self._connect() as conn:
            rows = conn.execute(
                """
                SELECT status, COUNT(*) AS count
                FROM crm_event_queue
                GROUP BY status
                """
            ).fetchall()
        counts = {str(row["status"]): int(row["count"]) for row in rows}
        return {
            "pending": counts.get("pending", 0),
            "processing": counts.get("processing", 0),
            "processed": counts.get("processed", 0),
            "dead_letter": counts.get("dead_letter", 0),
        }

    def queue_health(self) -> dict[str, Any]:
        now = _utc_now()
        now_iso = now.isoformat()
        with self._connect() as conn:
            oldest_pending = conn.execute(
                """
                SELECT created_at
                FROM crm_event_queue
                WHERE status = 'pending'
                ORDER BY created_at ASC, event_id ASC
                LIMIT 1
                """
            ).fetchone()
            oldest_dead_letter = conn.execute(
                """
                SELECT updated_at
                FROM crm_event_queue
                WHERE status = 'dead_letter'
                ORDER BY updated_at ASC, event_id ASC
                LIMIT 1
                """
            ).fetchone()
            processing_rows = conn.execute(
                """
                SELECT leased_until
                FROM crm_event_queue
                WHERE status = 'processing'
                """
            ).fetchall()

        active_processing = 0
        expired_leases = 0
        for row in processing_rows:
            leased_until = _parse_iso(row["leased_until"])
            if leased_until is None or leased_until < now:
                expired_leases += 1
            else:
                active_processing += 1

        pending_age_seconds: int | None = None
        dead_letter_age_seconds: int | None = None
        pending_created_at = oldest_pending["created_at"] if oldest_pending is not None else None
        dead_letter_updated_at = oldest_dead_letter["updated_at"] if oldest_dead_letter is not None else None
        pending_dt = _parse_iso(pending_created_at)
        dead_letter_dt = _parse_iso(dead_letter_updated_at)
        if pending_dt is not None:
            pending_age_seconds = max(0, int((now - pending_dt).total_seconds()))
        if dead_letter_dt is not None:
            dead_letter_age_seconds = max(0, int((now - dead_letter_dt).total_seconds()))

        return {
            "counts": self.queue_counts(),
            "processing": {
                "active_leases": active_processing,
                "expired_leases": expired_leases,
            },
            "oldest_pending_created_at": pending_created_at,
            "oldest_pending_age_seconds": pending_age_seconds,
            "oldest_dead_letter_updated_at": dead_letter_updated_at,
            "oldest_dead_letter_age_seconds": dead_letter_age_seconds,
            "updated_at": now_iso,
        }

    def operational_metrics(self, *, window_hours: int = 24) -> dict[str, Any]:
        bounded_window_hours = max(1, int(window_hours))
        now = _utc_now()
        cutoff = (now - timedelta(hours=bounded_window_hours)).isoformat()
        with self._connect() as conn:
            rows = conn.execute(
                """
                SELECT
                    provider,
                    SUM(CASE WHEN status = 'processed' AND processed_at >= ? THEN 1 ELSE 0 END) AS processed_count,
                    SUM(CASE WHEN status = 'dead_letter' AND updated_at >= ? THEN 1 ELSE 0 END) AS dead_letter_count
                FROM crm_event_queue
                GROUP BY provider
                """,
                (cutoff, cutoff),
            ).fetchall()

        provider_breakdown: dict[str, dict[str, int]] = {}
        processed_in_window = 0
        dead_letter_in_window = 0
        for row in rows:
            provider = str(row["provider"] or "").strip() or "unknown"
            processed_count = int(row["processed_count"] or 0)
            dead_letter_count = int(row["dead_letter_count"] or 0)
            provider_breakdown[provider] = {
                "processed_in_window": processed_count,
                "dead_letter_in_window": dead_letter_count,
            }
            processed_in_window += processed_count
            dead_letter_in_window += dead_letter_count

        completed_in_window = processed_in_window + dead_letter_in_window
        throughput_events_per_hour = processed_in_window / bounded_window_hours
        throughput_events_per_day = throughput_events_per_hour * 24.0
        exception_rate = (dead_letter_in_window / completed_in_window) if completed_in_window else None
        return {
            "window_hours": bounded_window_hours,
            "processed_in_window": processed_in_window,
            "dead_letter_in_window": dead_letter_in_window,
            "completed_in_window": completed_in_window,
            "throughput_events_per_hour": throughput_events_per_hour,
            "throughput_events_per_day": throughput_events_per_day,
            "exception_rate": exception_rate,
            "provider_breakdown": provider_breakdown,
            "updated_at": now.isoformat(),
        }

    def replay_event(self, *, event_id: str) -> dict[str, Any] | None:
        current = self.get_event(event_id)
        if current is None or str(current.get("status") or "") != "dead_letter":
            return None
        now = _utc_now_iso()
        with self._connect() as conn:
            cursor = conn.execute(
                """
                UPDATE crm_event_queue
                SET
                    status = 'pending',
                    attempts = 0,
                    leased_until = NULL,
                    processing_outcome = NULL,
                    finding_run_id = NULL,
                    receipt_run_id = NULL,
                    last_error = NULL,
                    processed_at = NULL,
                    updated_at = ?
                WHERE event_id = ?
                  AND status = 'dead_letter'
                """,
                (now, str(event_id or "").strip()),
            )
            if (cursor.rowcount or 0) <= 0:
                return None
        return self.get_event(event_id)

    def _row_to_event(self, row: sqlite3.Row | None) -> dict[str, Any] | None:
        if row is None:
            return None
        payload = {
            "event_id": str(row["event_id"]),
            "provider": str(row["provider"]),
            "source_kind": str(row["source_kind"]),
            "source_event_id": str(row["source_event_id"]),
            "event_type": str(row["event_type"]),
            "payload": _from_json(row["payload_json"]),
            "metadata": _from_json(row["metadata_json"]),
            "status": str(row["status"]),
            "attempts": int(row["attempts"] or 0),
            "max_attempts": int(row["max_attempts"] or DEFAULT_MAX_ATTEMPTS),
            "leased_until": row["leased_until"],
            "processing_outcome": row["processing_outcome"],
            "finding_run_id": row["finding_run_id"],
            "receipt_run_id": row["receipt_run_id"],
            "last_error": row["last_error"],
            "created_at": row["created_at"],
            "updated_at": row["updated_at"],
            "processed_at": row["processed_at"],
        }
        leased_until = _parse_iso(payload.get("leased_until"))
        if leased_until is not None:
            payload["leased_until_epoch"] = int(leased_until.timestamp())
        return payload


__all__ = [
    "CrmEventQueue",
    "DEFAULT_MAX_ATTEMPTS",
    "DEFAULT_VISIBILITY_TIMEOUT_SECONDS",
    "event_queue_db_path",
]
