"""
cortex/storage/sqlite_adapter.py — Full SQLite Implementation of StorageAdapter (M1).

The reference (and currently only) implementation of the StorageAdapter ABC.
This is where all Cortex data lives — claims, entities, episodes, cornerstones,
feedback, brain graphs, embeddings, and retrieval logs.

Key design decisions:
    - aiosqlite for all async I/O (non-blocking in asyncio event loops)
    - WAL mode for concurrent read performance (one writer, many readers)
    - busy_timeout prevents SQLITE_BUSY errors under concurrent access
    - FTS5 virtual tables for BM25 full-text search (fts_claims, fts_episodes)
    - sqlite-vec extension for ANN vector search; graceful fallback to
      brute-force cosine similarity if extension is unavailable
    - All IDs are hex UUIDs: lower(hex(randomblob(16))) — generated in SQL
    - All timestamps are ISO 8601 UTC strings (TEXT columns, not INTEGER)
    - Parameterized queries ONLY — no string interpolation of user data (SQL injection safe)
    - Schema is created in initialize() and is idempotent (CREATE TABLE IF NOT EXISTS)

Schema organization (v3):
    - session_records — wake/sleep lifecycle
    - episode_events — immutable conversation log (L3)
    - semantic_claims — subject-predicate-object triples (L4)
    - claim_provenance — evidence chain (claim → episode)
    - entities — resolved real-world entities
    - entity_aliases — alternative names mapping to entities
    - cornerstone_memories — identity-protecting sealed memories
    - memory_edges — inter-memory graph links
    - memory_feedback — retrieval quality feedback
    - procedure_memories — how-to knowledge (L5)
    - brain_graph_index / brain_graph_nodes / brain_graph_edges — knowledge graphs
    - embedding_index — vector embeddings (binary BLOB)
    - retrieval_log — retrieval audit trail
    - cortex_config — runtime configuration key-value store
    - fts_claims / fts_episodes — FTS5 virtual tables

Dependencies: aiosqlite, sqlite-vec (optional), cortex.types, cortex.storage.adapter.
Used by: Every module via the StorageAdapter interface.
"""
from __future__ import annotations

import asyncio
import os
from pathlib import Path
from typing import Optional

import aiosqlite

from cortex.storage.adapter import StorageAdapter

# Import helpers at module level for backward compatibility
from cortex.storage._helpers import (  # noqa: F401
    _cosine_similarity,
    _now,
    _pack_floats,
    _row_to_brain_edge,
    _row_to_brain_graph,
    _row_to_brain_node,
    _row_to_claim,
    _row_to_cornerstone,
    _row_to_edge,
    _row_to_entity,
    _row_to_entity_alias,
    _row_to_episode,
    _row_to_evolution_proposal,
    _row_to_feedback,
    _row_to_provenance,
    _row_to_session,
    _sha256,
    _unpack_floats,
)
from cortex.storage.queries import (
    ActivationQueryMixin,
    BrainGraphQueryMixin,
    ClaimQueryMixin,
    CornerstoneQueryMixin,
    EntityQueryMixin,
    EventQueryMixin,
    GraphQueryMixin,
    PeerQueryMixin,
    SessionQueryMixin,
    StatsQueryMixin,
    WebhookQueryMixin,
)

# ---------------------------------------------------------------------------
# sqlite-vec availability probe
# ---------------------------------------------------------------------------

_SQLITE_VEC_AVAILABLE = False
try:
    import sqlite_vec  # type: ignore
    _SQLITE_VEC_AVAILABLE = True
except ImportError:
    pass


# ---------------------------------------------------------------------------
# SQLiteAdapter
# ---------------------------------------------------------------------------

class SQLiteAdapter(
    ActivationQueryMixin,
    EntityQueryMixin,
    CornerstoneQueryMixin,
    ClaimQueryMixin,
    SessionQueryMixin,
    BrainGraphQueryMixin,
    EventQueryMixin,
    StatsQueryMixin,
    PeerQueryMixin,
    GraphQueryMixin,
    WebhookQueryMixin,
    StorageAdapter,
):
    """
    Async SQLite implementation using aiosqlite.

    Usage::

        adapter = SQLiteAdapter(db_path="cortex.db",
                                busy_timeout_ms=5000,
                                enable_wal=True)
        await adapter.initialize()
        # ... use adapter ...
        await adapter.close()

    Or as an async context manager::

        async with SQLiteAdapter("cortex.db") as adapter:
            ...
    """

    _MIGRATIONS_DIR = Path(__file__).parent / "migrations"

    #: Default file permission mode for on-disk DB files (owner read/write only).
    DEFAULT_DB_FILE_MODE = 0o600

    def __init__(
        self,
        db_path: str | Path = "cortex.db",
        *,
        busy_timeout_ms: int = 5000,
        enable_wal: bool = True,
        auto_migrate: bool = True,
        db_file_mode: int = DEFAULT_DB_FILE_MODE,
        max_readers: int = 1,
        embedding_dim: int = 1536,
    ) -> None:
        """Create a SQLiteAdapter.

        Connection pooling note (L1 audit):
            SQLiteAdapter uses a single persistent aiosqlite connection for all
            operations.  SQLite's WAL (Write-Ahead Logging) mode inherently
            supports concurrent *readers* at the OS level, so multiple processes
            or threads can read the same DB file simultaneously.  Within a single
            asyncio process however, all operations share this one connection and
            are serialised by the event loop — full connection pooling is not
            necessary for the typical single-process Cortex deployment.

            The ``max_readers`` parameter is reserved for a future multi-reader
            pool (e.g. one write connection + N read-only connections under WAL).
            Currently it is validated but not acted upon.  If your deployment
            requires higher read concurrency, run multiple Cortex processes each
            with their own adapter instance pointed at the same WAL-mode DB file.
        """
        if max_readers < 1:
            raise ValueError(f"max_readers must be >= 1, got {max_readers}")
        self._db_path = str(db_path)
        self._busy_timeout_ms = busy_timeout_ms
        self._enable_wal = enable_wal
        self._auto_migrate = auto_migrate
        self._db_file_mode = db_file_mode
        self._max_readers = max_readers  # reserved for future pool expansion
        self._embedding_dim = embedding_dim
        self._conn: Optional[aiosqlite.Connection] = None
        self._vec_loaded = False
        self._write_lock = asyncio.Lock()

    # ------------------------------------------------------------------ #
    # Context manager
    # ------------------------------------------------------------------ #

    async def __aenter__(self) -> "SQLiteAdapter":
        await self.initialize()
        return self

    async def __aexit__(self, *_) -> None:
        await self.close()

    # ------------------------------------------------------------------ #
    # Internal connection helper
    # ------------------------------------------------------------------ #

    async def _get_conn(self) -> aiosqlite.Connection:
        if self._conn is None:
            raise RuntimeError("SQLiteAdapter not initialized. Call await initialize() first.")
        return self._conn

    async def _setup_conn(self, conn: aiosqlite.Connection) -> None:
        """Configure PRAGMAs on a fresh connection."""
        await conn.execute(f"PRAGMA busy_timeout = {self._busy_timeout_ms}")
        await conn.execute("PRAGMA foreign_keys = ON")
        if self._enable_wal:
            await conn.execute("PRAGMA journal_mode = WAL")
        conn.row_factory = aiosqlite.Row

    # ------------------------------------------------------------------ #
    # Lifecycle
    # ------------------------------------------------------------------ #

    async def initialize(self) -> None:
        """Open connection, configure PRAGMAs, run schema migrations.

        For on-disk databases the file is created with a restrictive umask
        (0o177) so that the OS does not grant group/other permissions before
        we can explicitly set them. After the connection is established the
        file mode is set to ``self._db_file_mode`` (default: 0o600 —
        owner read/write only).  ``:memory:`` databases are not affected.
        """
        import logging
        _log = logging.getLogger(__name__)

        _is_memory = self._db_path == ":memory:"

        # Ensure parent directory exists and is writable (skip for in-memory DBs)
        if not _is_memory:
            db_dir = os.path.dirname(os.path.abspath(self._db_path))
            os.makedirs(db_dir, exist_ok=True)

            # Check write permissions early — WAL mode and normal writes will
            # fail with opaque SQLite errors on read-only filesystems (#396).
            db_file = os.path.abspath(self._db_path)
            if os.path.exists(db_file) and not os.access(db_file, os.W_OK):
                raise PermissionError(
                    f"Cortex database file is not writable: {db_file}. "
                    "SQLite requires read-write access (WAL mode). "
                    "Check file permissions or move the database to a writable location."
                )
            if not os.access(db_dir, os.W_OK):
                raise PermissionError(
                    f"Cortex database directory is not writable: {db_dir}. "
                    "SQLite needs write access to create the database and WAL files. "
                    "Check directory permissions."
                )

        if _is_memory:
            # In-memory database: skip umask manipulation and permission fixup
            self._conn = await aiosqlite.connect(self._db_path)
        else:
            # Apply restrictive umask before SQLite creates the file so that
            # the initial open(2) call never grants group/other bits, even
            # momentarily.  We restore the previous umask immediately after.
            old_umask = os.umask(0o177)
            try:
                self._conn = await aiosqlite.connect(self._db_path)
            finally:
                os.umask(old_umask)

            # Explicitly chmod to the configured mode.  This covers two cases:
            #   1. The file was just created → sets the desired permissions.
            #   2. The file already existed with looser permissions → tightens them.
            try:
                os.chmod(self._db_path, self._db_file_mode)
            except OSError as exc:
                _log.warning(
                    "SQLiteAdapter: could not set DB file permissions on %r: %s",
                    self._db_path,
                    exc,
                )

        await self._setup_conn(self._conn)

        # Run integrity check on startup
        try:
            async with self._conn.execute("PRAGMA quick_check") as cur:
                result = await cur.fetchone()
            if result and result[0] != "ok":
                _log.critical(
                    "SQLite integrity check FAILED on %r: %s. "
                    "Database may be corrupted — proceeding anyway but data loss is possible.",
                    self._db_path,
                    result[0],
                )
            else:
                _log.debug("SQLite integrity check passed for %r.", self._db_path)
        except Exception as exc:
            _log.warning(
                "SQLite integrity check could not run on %r: %s",
                self._db_path,
                exc,
            )

        # Load sqlite-vec extension if available, then ensure vec_index virtual table exists.
        if _SQLITE_VEC_AVAILABLE:
            try:
                await self._conn.enable_load_extension(True)
                await self._conn.load_extension(sqlite_vec.loadable_path())
                await self._conn.enable_load_extension(False)
                # Create the KNN virtual table using the configured embedding dimension.
                # This is idempotent (IF NOT EXISTS) so it's safe to run on every startup.
                await self._conn.execute(
                    f"CREATE VIRTUAL TABLE IF NOT EXISTS vec_index USING vec0("
                    f"item_type TEXT, item_id TEXT, embedding float[{self._embedding_dim}])"
                )
                await self._conn.commit()
                self._vec_loaded = True

                # Backfill: sync any embedding_index rows missing from vec_index.
                # This handles data written before vec_index INSERT was added.
                try:
                    async with self._conn.execute(
                        "SELECT COUNT(*) FROM vec_index"
                    ) as cur:
                        vec_count = (await cur.fetchone())[0]
                    async with self._conn.execute(
                        "SELECT COUNT(*) FROM embedding_index"
                    ) as cur:
                        ei_count = (await cur.fetchone())[0]
                    if ei_count > 0 and vec_count == 0:
                        _log.info(
                            "vec_index empty but embedding_index has %d rows — backfilling",
                            ei_count,
                        )
                        async with self._conn.execute(
                            "SELECT item_type, item_id, embedding FROM embedding_index"
                        ) as cur:
                            rows = await cur.fetchall()
                        for row in rows:
                            await self._conn.execute(
                                "INSERT INTO vec_index (item_type, item_id, embedding) VALUES (?, ?, ?)",
                                (row[0], row[1], row[2]),
                            )
                        await self._conn.commit()
                        _log.info("vec_index backfill complete: %d rows synced", len(rows))
                except Exception as exc:
                    _log.warning("vec_index backfill failed (brute-force fallback will be used): %s", exc)

            except Exception as exc:
                _log.warning("sqlite-vec extension load or vec_index creation failed: %s", exc)
                self._vec_loaded = False

        if self._auto_migrate:
            applied = await self._run_migration()
            if applied:
                _log.info("SQLiteAdapter: applied %d migration(s).", applied)
        else:
            _log.debug("SQLiteAdapter: auto_migrate=False, skipping migrations.")

    async def _run_migration(self) -> int:
        """Apply all pending migrations via MigrationRunner.

        Returns the number of migrations applied (0 if already up-to-date).
        Backwards-compatible: falls back to the legacy v3.sql executescript
        path when the cortex_config table does not yet exist, ensuring
        fresh databases still bootstrap correctly.
        """
        from cortex.storage.migrations import MigrationRunner

        conn = await self._get_conn()
        runner = MigrationRunner(conn)
        return await runner.apply_all()

    async def close(self) -> None:
        """Close the underlying aiosqlite connection; safe to call multiple times."""
        if self._conn:
            await self._conn.close()
            self._conn = None
