"""SQLite-backed event store and aggregate merge logic."""

from __future__ import annotations

import json
import sqlite3
from contextlib import contextmanager
from typing import Any

from kb_mcp.events.candidates import detect_candidates
from kb_mcp.events.schema import schema_locked_connection
from kb_mcp.events.types import DispatchResult, EventEnvelope, utc_now_iso


class EventStore:
    """Durable event store for hook and middleware events."""

    def append(self, envelope: EventEnvelope) -> DispatchResult:
        """Persist an event and merge it into an aggregate."""
        with self.transaction() as conn:
            _assign_checkpoint_identity(conn, envelope)
            conn.execute(
                """
                INSERT INTO events(
                  event_id, occurred_at, received_at, source_tool, source_client,
                  source_layer, event_name, aggregate_type, management_mode, logical_key,
                  correlation_id, session_id, tool_call_id, error_fingerprint, summary,
                  content_excerpt, cwd, repo, project, transcript_path, raw_payload_json,
                  redacted_payload_json
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    envelope.event_id,
                    envelope.occurred_at,
                    envelope.received_at,
                    envelope.source_tool,
                    envelope.source_client,
                    envelope.source_layer,
                    envelope.event_name,
                    envelope.aggregate_type,
                    envelope.management_mode,
                    envelope.logical_key,
                    envelope.correlation_id,
                    envelope.session_id,
                    envelope.tool_call_id,
                    envelope.error_fingerprint,
                    envelope.summary,
                    envelope.content_excerpt,
                    envelope.cwd,
                    envelope.repo,
                    envelope.project,
                    envelope.transcript_path,
                    json.dumps(envelope.raw_payload, ensure_ascii=False),
                    json.dumps(envelope.redacted_payload, ensure_ascii=False),
                ),
            )
            version, status, queued = _merge_envelope(conn, envelope)
            return DispatchResult(
                event_id=envelope.event_id,
                logical_key=envelope.logical_key,
                aggregate_type=envelope.aggregate_type,
                status=status,
                aggregate_version=version,
                queued_sinks=queued,
            )

    def ready_sinks(self, *, maintenance: bool = False, limit: int = 50) -> list[sqlite3.Row]:
        """Claim due outbox rows."""
        with self.transaction() as conn:
            if maintenance:
                _promote_pending_sessions(conn)
            now = utc_now_iso()
            rows = conn.execute(
                """
                SELECT * FROM outbox
                WHERE status = 'ready' AND due_at <= ?
                ORDER BY id
                LIMIT ?
                """,
                (now, limit),
            ).fetchall()
            ids = [row["id"] for row in rows]
            if ids:
                conn.executemany(
                    "UPDATE outbox SET status='claimed', claimed_at=? WHERE id=?",
                    [(now, row_id) for row_id in ids],
                )
            return rows

    def mark_sink_succeeded(self, row_id: int, logical_key: str, aggregate_version: int, sink_name: str, receipt: str) -> None:
        with self.transaction() as conn:
            conn.execute(
                "UPDATE outbox SET status='applied', last_error=NULL WHERE id=?",
                (row_id,),
            )
            conn.execute(
                """
                INSERT OR IGNORE INTO sink_runs(logical_key, aggregate_version, sink_name, receipt, status, created_at)
                VALUES (?, ?, ?, ?, 'applied', ?)
                """,
                (logical_key, aggregate_version, sink_name, receipt, utc_now_iso()),
            )
            remaining = conn.execute(
                """
                SELECT COUNT(*) AS count
                FROM outbox
                WHERE logical_key=? AND aggregate_version=? AND status!='applied'
                """,
                (logical_key, aggregate_version),
            ).fetchone()
            if int(remaining["count"]) == 0:
                conn.execute(
                    """
                    UPDATE logical_events
                    SET status='applied', updated_at=?
                    WHERE logical_key=? AND aggregate_version=?
                    """,
                    (utc_now_iso(), logical_key, aggregate_version),
                )

    def mark_sink_failed(self, row_id: int, message: str) -> None:
        with self.transaction() as conn:
            conn.execute(
                "UPDATE outbox SET status='dead_letter', last_error=? WHERE id=?",
                (message[:500], row_id),
            )

    def fetch_logical_event(self, logical_key: str) -> sqlite3.Row | None:
        with self.transaction() as conn:
            return conn.execute(
                "SELECT * FROM logical_events WHERE logical_key=?",
                (logical_key,),
            ).fetchone()

    def put_runtime_observation(self, key: str, severity: str, message: str, details: dict[str, Any] | None = None) -> None:
        payload = json.dumps(details or {}, ensure_ascii=False)
        with self.transaction() as conn:
            conn.execute(
                """
                INSERT INTO runtime_observations(observation_key, severity, message, details_json, expires_at, updated_at)
                VALUES (?, ?, ?, ?, NULL, ?)
                ON CONFLICT(observation_key) DO UPDATE SET
                  severity=excluded.severity,
                  message=excluded.message,
                  details_json=excluded.details_json,
                  updated_at=excluded.updated_at
                """,
                (key, severity, message, payload, utc_now_iso()),
            )

    def dead_letter_count(self) -> int:
        with self.transaction() as conn:
            row = conn.execute(
                "SELECT COUNT(*) AS count FROM outbox WHERE status='dead_letter'"
            ).fetchone()
            return int(row["count"])

    def replay_dead_letters(self, *, limit: int = 50) -> int:
        with self.transaction() as conn:
            rows = conn.execute(
                """
                SELECT id
                FROM outbox
                WHERE status='dead_letter'
                ORDER BY id
                LIMIT ?
                """,
                (limit,),
            ).fetchall()
            if not rows:
                return 0
            now = utc_now_iso()
            conn.executemany(
                """
                UPDATE outbox
                SET status='ready', claimed_at=NULL, last_error=NULL, due_at=?
                WHERE id=?
                """,
                [(now, row["id"]) for row in rows],
            )
            return len(rows)

    @contextmanager
    def transaction(self) -> sqlite3.Connection:
        with schema_locked_connection() as conn:
            try:
                conn.execute("BEGIN IMMEDIATE")
                yield conn
                conn.commit()
            except Exception:
                conn.rollback()
                raise


def _merge_envelope(conn: sqlite3.Connection, envelope: EventEnvelope) -> tuple[int, str, list[str]]:
    existing = conn.execute(
        "SELECT * FROM logical_events WHERE logical_key=?",
        (envelope.logical_key,),
    ).fetchone()
    state = dict(envelope.aggregate_state)
    if existing:
        current_state = json.loads(existing["aggregate_state_json"])
        current_state.update({k: v for k, v in state.items() if v is not None})
        state = current_state
        version = int(existing["aggregate_version"]) + 1
        status = existing["status"]
        debug_only_reason = existing["debug_only_reason"]
        summary = envelope.summary or existing["summary"]
        content_excerpt = envelope.content_excerpt or existing["content_excerpt"]
        cwd = envelope.cwd or existing["cwd"]
        repo = envelope.repo or existing["repo"]
        project = envelope.project or existing["project"]
        transcript_path = envelope.transcript_path or existing["transcript_path"]
        final_outcome = existing["final_outcome"]
    else:
        version = 1
        status = "collecting"
        debug_only_reason = None
        summary = envelope.summary
        content_excerpt = envelope.content_excerpt
        cwd = envelope.cwd
        repo = envelope.repo
        project = envelope.project
        transcript_path = envelope.transcript_path
        final_outcome = None

    queued: list[str] = []

    if envelope.aggregate_type == "session":
        if envelope.event_name == "session_ended":
            if envelope.source_layer == "session_launcher" and envelope.correlation_id:
                status = "pending_finalization"
            else:
                status = "collecting"
                debug_only_reason = "missing_correlation_id"
        elif envelope.event_name == "process_exit":
            final_outcome = state.get("exit_code")
        else:
            status = "collecting"
    elif envelope.aggregate_type == "tool":
        if envelope.event_name in {"tool_succeeded", "tool_failed"}:
            status = "ready"
            if envelope.event_name == "tool_failed":
                queued = ["incident_writer"]
            elif _is_anchor_save(envelope):
                queued = ["promotion_planner", "promotion_applier"]
        else:
            status = "collecting"
    elif envelope.aggregate_type == "error":
        status = "ready"
        queued = ["incident_writer"]
    elif envelope.aggregate_type == "compact":
        if envelope.event_name in {"compact_finished", "turn_checkpointed"}:
            status = "ready"
            queued = ["checkpoint_writer"]
            detected = detect_candidates(envelope.summary, envelope.content_excerpt)
            if detected["has_candidates"]:
                queued.append("candidate_writer")
            if _needs_thin_promotion(envelope):
                queued.extend(["promotion_planner", "promotion_applier"])
        else:
            status = "collecting"

    now = utc_now_iso()
    conn.execute(
        """
        INSERT INTO logical_events(
          logical_key, aggregate_type, correlation_id, session_id, management_mode,
          source_tool, source_client, status, aggregate_version, summary, content_excerpt,
          cwd, repo, project, transcript_path, final_outcome, debug_only_reason,
          aggregate_state_json, ready_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(logical_key) DO UPDATE SET
          status=excluded.status,
          aggregate_version=excluded.aggregate_version,
          summary=excluded.summary,
          content_excerpt=excluded.content_excerpt,
          cwd=excluded.cwd,
          repo=excluded.repo,
          project=excluded.project,
          transcript_path=excluded.transcript_path,
          final_outcome=excluded.final_outcome,
          debug_only_reason=excluded.debug_only_reason,
          aggregate_state_json=excluded.aggregate_state_json,
          ready_at=excluded.ready_at,
          updated_at=excluded.updated_at
        """,
        (
            envelope.logical_key,
            envelope.aggregate_type,
            envelope.correlation_id,
            envelope.session_id,
            envelope.management_mode,
            envelope.source_tool,
            envelope.source_client,
            status,
            version,
            summary,
            content_excerpt,
            cwd,
            repo,
            project,
            transcript_path,
            str(final_outcome) if final_outcome is not None else None,
            debug_only_reason,
            json.dumps(state, ensure_ascii=False),
            now if status == "ready" else None,
            now,
        ),
    )

    for sink_name in queued:
        conn.execute(
            """
            INSERT OR IGNORE INTO outbox(logical_key, aggregate_version, sink_name, status, due_at, claimed_at, last_error, created_at)
            VALUES (?, ?, ?, 'ready', ?, NULL, NULL, ?)
            """,
            (envelope.logical_key, version, sink_name, now, now),
        )
    return version, status, queued


def _is_anchor_save(envelope: EventEnvelope) -> bool:
    if envelope.event_name != "tool_succeeded":
        return False
    note_type = envelope.raw_payload.get("saved_note_type")
    return note_type in {"gap", "knowledge", "adr"}


def _needs_thin_promotion(envelope: EventEnvelope) -> bool:
    if envelope.event_name not in {"compact_finished", "turn_checkpointed"}:
        return False
    state = envelope.aggregate_state
    return bool(state.get("final_hint")) or state.get("checkpoint_kind") == "session_end"


def _assign_checkpoint_identity(conn: sqlite3.Connection, envelope: EventEnvelope) -> None:
    if envelope.aggregate_type != "compact":
        return
    partition_key = envelope.aggregate_state.get("checkpoint_partition_key")
    if not partition_key:
        return
    ordinal = envelope.aggregate_state.get("checkpoint_ordinal")
    if ordinal:
        row = conn.execute(
            "SELECT next_ordinal FROM checkpoint_sequences WHERE partition_key=?",
            (partition_key,),
        ).fetchone()
        next_ordinal = max(int(ordinal) + 1, int(row["next_ordinal"]) if row else 1)
        conn.execute(
            """
            INSERT INTO checkpoint_sequences(partition_key, next_ordinal)
            VALUES (?, ?)
            ON CONFLICT(partition_key) DO UPDATE SET next_ordinal=excluded.next_ordinal
            """,
            (partition_key, next_ordinal),
        )
        return
    row = conn.execute(
        "SELECT next_ordinal FROM checkpoint_sequences WHERE partition_key=?",
        (partition_key,),
    ).fetchone()
    next_ordinal = int(row["next_ordinal"]) if row else 1
    conn.execute(
        """
        INSERT INTO checkpoint_sequences(partition_key, next_ordinal)
        VALUES (?, ?)
        ON CONFLICT(partition_key) DO UPDATE SET next_ordinal=excluded.next_ordinal
        """,
        (partition_key, next_ordinal + 1),
    )
    envelope.aggregate_state["checkpoint_ordinal"] = next_ordinal
    envelope.logical_key = f"compact:{partition_key}:{next_ordinal}"


def _promote_pending_sessions(conn: sqlite3.Connection) -> None:
    rows = conn.execute(
        """
        SELECT logical_key, aggregate_version
        FROM logical_events
        WHERE aggregate_type='session' AND status='pending_finalization' AND debug_only_reason IS NULL
        """
    ).fetchall()
    now = utc_now_iso()
    for row in rows:
        conn.execute(
            "UPDATE logical_events SET status='ready', ready_at=?, updated_at=? WHERE logical_key=?",
            (now, now, row["logical_key"]),
        )
        conn.execute(
            """
            INSERT OR IGNORE INTO outbox(logical_key, aggregate_version, sink_name, status, due_at, claimed_at, last_error, created_at)
            VALUES (?, ?, 'session_finalizer', 'ready', ?, NULL, NULL, ?)
            """,
            (row["logical_key"], row["aggregate_version"], now, now),
        )
