"""SQLite-based Event Store implementation.

Provides durable event persistence suitable for single-node deployments.
For distributed systems, consider PostgreSQL or EventStoreDB.
"""

from collections.abc import Iterator
from datetime import datetime, UTC
import json
from pathlib import Path
import sqlite3
import threading
from typing import Any

from mcp_hangar.domain.contracts.event_store import ConcurrencyError, IEventStore
from mcp_hangar.domain.events import DomainEvent
from mcp_hangar.domain.exceptions import CompactionError
from mcp_hangar.logging_config import get_logger

from .event_serializer import EventSerializer

logger = get_logger(__name__)


class SQLiteEventStore(IEventStore):
    """SQLite-based event store with optimistic concurrency.

    Thread-safe implementation suitable for single-node deployments.

    Features:
    - Append-only event storage
    - Optimistic concurrency control via version checks
    - Global ordering across all streams
    - Efficient stream reads with indexing

    Schema:
    - events: Main event table with global ordering
    - streams: Track stream versions for concurrency control
    """

    def __init__(self, db_path: str | Path = ":memory:", *, serializer: EventSerializer | None = None):
        """Initialize SQLite event store.

        Args:
            db_path: Path to SQLite database file.
                Use ":memory:" for in-memory store (testing).
            serializer: Optional EventSerializer instance. Allows injecting an upcaster-aware serializer.
        """
        self._db_path = str(db_path)
        self._serializer = serializer or EventSerializer()
        self._lock = threading.Lock()
        self._is_memory = self._db_path == ":memory:"

        # For in-memory database, keep a persistent connection
        # (each new connection to :memory: creates a NEW database)
        self._persistent_conn: sqlite3.Connection | None = None
        if self._is_memory:
            self._persistent_conn = self._create_connection()

        self._init_schema()

        logger.info(
            "sqlite_event_store_initialized",
            db_path=self._db_path,
            in_memory=self._is_memory,
        )

    def _create_connection(self) -> sqlite3.Connection:
        """Create a new database connection."""
        conn = sqlite3.connect(self._db_path, check_same_thread=False)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA foreign_keys = ON")
        if not self._is_memory:
            conn.execute("PRAGMA journal_mode = WAL")
        return conn

    def _init_schema(self) -> None:
        """Initialize database schema."""
        conn = self._connect()
        try:
            conn.executescript(
                """
                -- Main events table
                CREATE TABLE IF NOT EXISTS events (
                    global_position INTEGER PRIMARY KEY AUTOINCREMENT,
                    stream_id TEXT NOT NULL,
                    stream_version INTEGER NOT NULL,
                    event_type TEXT NOT NULL,
                    data TEXT NOT NULL,
                    metadata TEXT,
                    created_at TEXT NOT NULL,
                    UNIQUE(stream_id, stream_version)
                );

                -- Index for efficient stream reads
                CREATE INDEX IF NOT EXISTS idx_events_stream
                ON events(stream_id, stream_version);

                -- Index for global reads (projections)
                CREATE INDEX IF NOT EXISTS idx_events_global
                ON events(global_position);

                -- Stream version tracking for optimistic concurrency
                CREATE TABLE IF NOT EXISTS streams (
                    stream_id TEXT PRIMARY KEY,
                    version INTEGER NOT NULL DEFAULT -1,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL
                );

                -- Aggregate snapshots for bounded replay
                CREATE TABLE IF NOT EXISTS snapshots (
                    stream_id TEXT PRIMARY KEY,
                    version INTEGER NOT NULL,
                    state_data TEXT NOT NULL,
                    created_at TEXT NOT NULL
                );
            """
            )
        finally:
            if not self._is_memory:
                conn.close()

    def _connect(self) -> sqlite3.Connection:
        """Get database connection.

        For in-memory databases, returns the persistent connection.
        For file-based databases, creates a new connection.
        """
        if self._is_memory and self._persistent_conn:
            return self._persistent_conn
        return self._create_connection()

    def append(
        self,
        stream_id: str,
        events: list[DomainEvent],
        expected_version: int,
    ) -> int:
        """Append events to a stream with optimistic concurrency.

        Args:
            stream_id: Stream identifier (e.g., "provider:math").
            events: Events to append.
            expected_version: Expected current version (-1 for new stream).

        Returns:
            New stream version after append.

        Raises:
            ConcurrencyError: If version mismatch.
        """
        if not events:
            return expected_version

        with self._lock:
            conn = self._connect()
            try:
                cursor = conn.cursor()
                timestamp = datetime.now(UTC).isoformat()

                # Check current version
                cursor.execute(
                    "SELECT version FROM streams WHERE stream_id = ?",
                    (stream_id,),
                )
                row = cursor.fetchone()
                current_version = row["version"] if row else -1

                if current_version != expected_version:
                    raise ConcurrencyError(stream_id, expected_version, current_version)

                # Append events
                new_version = current_version
                for event in events:
                    new_version += 1
                    event_type, data = self._serializer.serialize(event)

                    cursor.execute(
                        """
                        INSERT INTO events
                        (stream_id, stream_version, event_type, data, created_at)
                        VALUES (?, ?, ?, ?, ?)
                        """,
                        (stream_id, new_version, event_type, data, timestamp),
                    )

                # Update or insert stream version
                if current_version == -1:
                    cursor.execute(
                        """
                        INSERT INTO streams (stream_id, version, created_at, updated_at)
                        VALUES (?, ?, ?, ?)
                        """,
                        (stream_id, new_version, timestamp, timestamp),
                    )
                else:
                    cursor.execute(
                        """
                        UPDATE streams SET version = ?, updated_at = ?
                        WHERE stream_id = ?
                        """,
                        (new_version, timestamp, stream_id),
                    )

                conn.commit()

                logger.debug(
                    "events_appended",
                    stream_id=stream_id,
                    events_count=len(events),
                    new_version=new_version,
                )

                return new_version

            except ConcurrencyError:
                conn.rollback()
                raise
            except Exception as e:  # noqa: BLE001 -- infra-boundary: rollback and propagate on any DB error
                conn.rollback()
                logger.error(
                    "event_append_failed",
                    stream_id=stream_id,
                    error=str(e),
                )
                raise
            finally:
                if not self._is_memory:
                    conn.close()

    def read_stream(
        self,
        stream_id: str,
        from_version: int = 0,
    ) -> list[DomainEvent]:
        """Read events from a stream.

        Args:
            stream_id: Stream identifier.
            from_version: Start version (inclusive).

        Returns:
            List of events in order. Empty if stream doesn't exist.
        """
        conn = self._connect()
        try:
            cursor = conn.execute(
                """
                SELECT event_type, data FROM events
                WHERE stream_id = ? AND stream_version >= ?
                ORDER BY stream_version ASC
                """,
                (stream_id, from_version),
            )

            events = []
            for row in cursor.fetchall():
                event = self._serializer.deserialize(row["event_type"], row["data"])
                events.append(event)

            logger.debug(
                "stream_read",
                stream_id=stream_id,
                from_version=from_version,
                events_count=len(events),
            )

            return events
        finally:
            if not self._is_memory:
                conn.close()

    def read_all(
        self,
        from_position: int = 0,
        limit: int = 1000,
    ) -> Iterator[tuple[int, str, DomainEvent]]:
        """Read all events across streams (for projections).

        Args:
            from_position: Start position (exclusive).
            limit: Maximum events to return.

        Yields:
            Tuples of (global_position, stream_id, event).
        """
        conn = self._connect()
        try:
            cursor = conn.execute(
                """
                SELECT global_position, stream_id, event_type, data
                FROM events
                WHERE global_position > ?
                ORDER BY global_position ASC
                LIMIT ?
                """,
                (from_position, limit),
            )

            # Fetch all rows first to allow closing connection
            rows = cursor.fetchall()
        finally:
            if not self._is_memory:
                conn.close()

        for row in rows:
            event = self._serializer.deserialize(row["event_type"], row["data"])
            yield row["global_position"], row["stream_id"], event

    def get_stream_version(self, stream_id: str) -> int:
        """Get current version of a stream.

        Args:
            stream_id: Stream identifier.

        Returns:
            Current version, or -1 if stream doesn't exist.
        """
        conn = self._connect()
        try:
            cursor = conn.execute(
                "SELECT version FROM streams WHERE stream_id = ?",
                (stream_id,),
            )
            row = cursor.fetchone()
            return row["version"] if row else -1
        finally:
            if not self._is_memory:
                conn.close()

    def get_all_stream_ids(self) -> list[str]:
        """Get all stream IDs in the store.

        Returns:
            List of stream identifiers.
        """
        conn = self._connect()
        try:
            cursor = conn.execute("SELECT stream_id FROM streams ORDER BY stream_id")
            return [row["stream_id"] for row in cursor.fetchall()]
        finally:
            if not self._is_memory:
                conn.close()

    def get_event_count(self) -> int:
        """Get total number of events in the store.

        Returns:
            Total event count.
        """
        conn = self._connect()
        try:
            cursor = conn.execute("SELECT COUNT(*) as count FROM events")
            row = cursor.fetchone()
            return row["count"] if row else 0
        finally:
            if not self._is_memory:
                conn.close()

    def get_stream_count(self) -> int:
        """Get total number of streams.

        Returns:
            Total stream count.
        """
        conn = self._connect()
        try:
            cursor = conn.execute("SELECT COUNT(*) as count FROM streams")
            row = cursor.fetchone()
            return row["count"] if row else 0
        finally:
            if not self._is_memory:
                conn.close()

    def list_streams(self, prefix: str = "") -> list[str]:
        """List all stream IDs, optionally filtered by prefix.

        Args:
            prefix: Optional prefix to filter streams.

        Returns:
            List of stream IDs matching the prefix.
        """
        conn = self._connect()
        try:
            if prefix:
                cursor = conn.execute(
                    "SELECT stream_id FROM streams WHERE stream_id LIKE ? ORDER BY stream_id",
                    (f"{prefix}%",),
                )
            else:
                cursor = conn.execute("SELECT stream_id FROM streams ORDER BY stream_id")
            return [row["stream_id"] for row in cursor.fetchall()]
        finally:
            if not self._is_memory:
                conn.close()

    def save_snapshot(
        self,
        stream_id: str,
        version: int,
        state: dict[str, Any],
    ) -> None:
        """Save aggregate snapshot inside lock scope for version consistency.

        Args:
            stream_id: Stream identifier (matches event stream).
            version: Stream version this snapshot represents.
            state: Serialized aggregate state (must be JSON-serializable).
        """
        with self._lock:
            conn = self._connect()
            try:
                timestamp = datetime.now(UTC).isoformat()
                conn.execute(
                    """
                    INSERT OR REPLACE INTO snapshots
                    (stream_id, version, state_data, created_at)
                    VALUES (?, ?, ?, ?)
                    """,
                    (stream_id, version, json.dumps(state), timestamp),
                )
                conn.commit()
                logger.debug(
                    "snapshot_saved",
                    stream_id=stream_id,
                    version=version,
                )
            except Exception as e:  # noqa: BLE001 -- infra-boundary: rollback and propagate on any DB error
                conn.rollback()
                logger.error("snapshot_save_failed", stream_id=stream_id, error=str(e))
                raise
            finally:
                if not self._is_memory:
                    conn.close()

    def load_snapshot(
        self,
        stream_id: str,
    ) -> dict[str, Any] | None:
        """Load latest snapshot for a stream.

        Args:
            stream_id: Stream identifier.

        Returns:
            Dict with "version" and "state" keys, or None if no snapshot exists.
        """
        conn = self._connect()
        try:
            cursor = conn.execute(
                "SELECT version, state_data FROM snapshots WHERE stream_id = ?",
                (stream_id,),
            )
            row = cursor.fetchone()
            if row is None:
                return None
            return {
                "version": row["version"],
                "state": json.loads(row["state_data"]),
            }
        finally:
            if not self._is_memory:
                conn.close()

    def compact_stream(self, stream_id: str) -> int:
        """Delete events that precede the latest snapshot for a stream.

        Args:
            stream_id: Identifier of the stream to compact.

        Returns:
            Number of events deleted.

        Raises:
            CompactionError: When no snapshot exists for the stream.
        """
        snapshot = self.load_snapshot(stream_id)
        if snapshot is None:
            raise CompactionError(stream_id, "no snapshot exists; create a snapshot before compacting")

        snapshot_version: int = snapshot["version"]

        with self._lock:
            conn = self._connect()
            try:
                cursor = conn.execute(
                    "DELETE FROM events WHERE stream_id = ? AND stream_version <= ?",
                    (stream_id, snapshot_version),
                )
                deleted = cursor.rowcount
                conn.commit()
            except Exception as e:  # noqa: BLE001 -- infra-boundary: rollback and propagate on any DB error
                conn.rollback()
                logger.error("compact_stream_failed", stream_id=stream_id, error=str(e))
                raise
            finally:
                if not self._is_memory:
                    conn.close()

        from ...metrics import record_events_compacted

        record_events_compacted(stream_id, deleted)

        logger.info(
            "stream_compacted",
            stream_id=stream_id,
            snapshot_version=snapshot_version,
            events_deleted=deleted,
        )

        return deleted
