"""Schema migration for claude-engram SQLite database.

Handles versioned migrations from v1 (original schema) through v2
(categories, TTL, project tags, content dedup) to v3 (counters table),
v4 (summary column), v5 (token_count column), v6 (temporal resolution),
v7 (bi-temporal valid_from/valid_to), v8 (memory_relations graph),
v9 (access_count + consolidation_meta), and v10 (FTS5 full-text search).
"""

from __future__ import annotations

import hashlib
import sqlite3


def get_schema_version(conn: sqlite3.Connection) -> int:
    """Return the current schema version from PRAGMA user_version.

    Args:
        conn: An open SQLite connection.

    Returns:
        The integer schema version (0 if never set).
    """
    row = conn.execute("PRAGMA user_version").fetchone()
    return row[0] if row else 0


def migrate_schema(conn: sqlite3.Connection) -> None:
    """Migrate the database schema to the latest version.

    Currently supports:
        v0/v1 -> v2: Adds proj_tag, ttl_tier, expires_at, content_hash columns
                      and corresponding indexes.
        v2 -> v3:    Creates counters table for key-value counters.

    This function is idempotent -- calling it on an already-migrated database
    is a no-op.

    Args:
        conn: An open SQLite connection with the memories table.
    """
    version = get_schema_version(conn)

    if version < 2:
        _migrate_v1_to_v2(conn)

    if version < 3:
        _migrate_v2_to_v3(conn)

    if version < 4:
        _migrate_v3_to_v4(conn)

    if version < 5:
        _migrate_v4_to_v5(conn)

    if version < 6:
        _migrate_v5_to_v6(conn)

    if version < 7:
        _migrate_v6_to_v7(conn)

    if version < 8:
        _migrate_v7_to_v8(conn)

    if version < 9:
        _migrate_v8_to_v9(conn)

    if version < 10:
        _migrate_v9_to_v10(conn)


def _migrate_v1_to_v2(conn: sqlite3.Connection) -> None:
    """Add Phase 3 columns and indexes to the memories table.

    Adds:
        - proj_tag TEXT: Project isolation tag (e.g. "proj:org/repo")
        - ttl_tier TEXT: TTL tier name (ephemeral/volatile/session/permanent)
        - expires_at TEXT: ISO 8601 expiration timestamp (NULL = permanent)
        - content_hash TEXT: SHA-256 hash for deduplication

    Indexes:
        - idx_memories_proj_tag: Fast project-scoped queries
        - idx_memories_expires_at: Fast TTL expiration sweeps
        - idx_memories_content_hash: Fast dedup lookups
    """
    # SQLite ALTER TABLE only supports one ADD COLUMN per statement.
    # Use executescript for DDL (not transactional in SQLite anyway).
    conn.executescript("""
        ALTER TABLE memories ADD COLUMN proj_tag TEXT DEFAULT NULL;
        ALTER TABLE memories ADD COLUMN ttl_tier TEXT DEFAULT 'permanent';
        ALTER TABLE memories ADD COLUMN expires_at TEXT DEFAULT NULL;
        ALTER TABLE memories ADD COLUMN content_hash TEXT DEFAULT NULL;

        CREATE INDEX IF NOT EXISTS idx_memories_proj_tag ON memories(proj_tag);
        CREATE INDEX IF NOT EXISTS idx_memories_expires_at ON memories(expires_at);
        CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash);

        PRAGMA user_version = 2;
    """)

    # H7: Backfill content_hash for existing rows that have NULL
    rows = conn.execute("SELECT id, content FROM memories WHERE content_hash IS NULL").fetchall()
    for row in rows:
        content = row[1] if isinstance(row, tuple) else row["content"]
        row_id = row[0] if isinstance(row, tuple) else row["id"]
        h = hashlib.sha256(content.encode("utf-8")).hexdigest()
        conn.execute("UPDATE memories SET content_hash = ? WHERE id = ?", (h, row_id))

    # Normalize NULL proj_tag to empty string for UNIQUE constraint correctness
    conn.execute("UPDATE memories SET proj_tag = '' WHERE proj_tag IS NULL")

    # Remove duplicates BEFORE creating the UNIQUE index.
    # Keep the row with the lowest id (earliest insertion) for each group.
    conn.execute(
        """DELETE FROM memories
           WHERE id NOT IN (
               SELECT MIN(id)
               FROM memories
               GROUP BY content_hash, category, proj_tag
           )"""
    )

    # Clean orphaned vectors: if memory_vectors table exists, remove vectors
    # whose memory_id no longer exists (deleted during dedup above).
    has_vectors = conn.execute(
        "SELECT name FROM sqlite_master WHERE type='table' AND name='memory_vectors'"
    ).fetchone()
    if has_vectors:
        conn.execute("DELETE FROM memory_vectors WHERE id NOT IN (SELECT id FROM memories)")

    # C5: Add UNIQUE dedup index (after backfill + dedup so it cannot fail)
    conn.execute(
        """CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_dedup
           ON memories(content_hash, category, proj_tag)"""
    )
    conn.commit()


def _migrate_v2_to_v3(conn: sqlite3.Connection) -> None:
    """Add counters table for lightweight key-value counters.

    Creates:
        - counters table: key-value counters scoped by project tag.
          No embeddings, no TTL — just atomic counters for structured data
          (MCP usage metrics, tool call counts, etc.).
        - idx_counters_proj_tag: Fast project-scoped queries.
        - UNIQUE(key, proj_tag): Enables INSERT ... ON CONFLICT DO UPDATE.
    """
    conn.executescript("""
        CREATE TABLE IF NOT EXISTS counters (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            key TEXT NOT NULL,
            value INTEGER NOT NULL DEFAULT 0,
            proj_tag TEXT NOT NULL DEFAULT '',
            updated_at TEXT NOT NULL,
            UNIQUE(key, proj_tag)
        );
        CREATE INDEX IF NOT EXISTS idx_counters_proj_tag ON counters(proj_tag);

        PRAGMA user_version = 3;
    """)
    conn.commit()


def _migrate_v3_to_v4(conn: sqlite3.Connection) -> None:
    """Add summary column for progressive disclosure (3-tier retrieval).

    Adds:
        - summary TEXT: Compact representation of memory content for Tier 1 retrieval.
          Generated lazily on first search or via explicit summarization.

    The summary column enables progressive disclosure:
        - Tier 1: summary only (~50-100 tokens) - for initial search results
        - Tier 2: full content (~500-1000 tokens) - after user selects
        - Tier 3: full content + related memories - for deep analysis
    """
    conn.executescript("""
        ALTER TABLE memories ADD COLUMN summary TEXT DEFAULT NULL;

        PRAGMA user_version = 4;
    """)
    conn.commit()


def _migrate_v4_to_v5(conn: sqlite3.Connection) -> None:
    """Add token_count column for token budget tracking.

    Adds:
        - token_count INTEGER: Estimated token count of memory content.
          Computed on store using chars/4 heuristic. Enables token-aware
          retrieval budgets without re-counting at query time.
    """
    conn.executescript("""
        ALTER TABLE memories ADD COLUMN token_count INTEGER DEFAULT NULL;

        PRAGMA user_version = 5;
    """)

    # Backfill token_count for existing rows using chars/4 heuristic
    rows = conn.execute("SELECT id, content FROM memories WHERE token_count IS NULL").fetchall()
    for row in rows:
        row_id = row[0] if isinstance(row, tuple) else row["id"]
        content = row[1] if isinstance(row, tuple) else row["content"]
        token_count = max(1, len(content) // 4)
        conn.execute("UPDATE memories SET token_count = ? WHERE id = ?", (token_count, row_id))

    conn.commit()


def _migrate_v5_to_v6(conn: sqlite3.Connection) -> None:
    """Add temporal resolution columns for memory supersession (G3).

    Adds:
        - superseded_by INTEGER: ID of the memory that replaces this one.
          NULL means the memory is current (not replaced).
        - is_active INTEGER: 1 = current, 0 = superseded. Enables fast
          filtering without JOIN. Defaults to 1 (active).

    Index:
        - idx_memories_is_active: Fast filtering of active-only queries.
    """
    conn.executescript("""
        ALTER TABLE memories ADD COLUMN superseded_by INTEGER DEFAULT NULL;
        ALTER TABLE memories ADD COLUMN is_active INTEGER DEFAULT 1;

        CREATE INDEX IF NOT EXISTS idx_memories_is_active ON memories(is_active);

        PRAGMA user_version = 6;
    """)
    conn.commit()


def _migrate_v6_to_v7(conn: sqlite3.Connection) -> None:
    """Add bi-temporal columns for memory validity tracking.

    Adds:
        - valid_from TEXT: When the memory became factually valid (ISO 8601).
          NULL means valid since creation.
        - valid_to TEXT: When the memory stopped being valid (ISO 8601).
          NULL means still valid.

    Index:
        - idx_memories_temporal: Composite index on (valid_from, valid_to)
          for efficient temporal range queries.

    Backfill:
        - Active memories: valid_from = created_at (valid since creation).
        - Superseded memories (is_active=0): valid_to = updated_at
          (best approximation of when they were replaced).
    """
    conn.executescript("""
        ALTER TABLE memories ADD COLUMN valid_from TEXT DEFAULT NULL;
        ALTER TABLE memories ADD COLUMN valid_to TEXT DEFAULT NULL;

        CREATE INDEX IF NOT EXISTS idx_memories_temporal
            ON memories(valid_from, valid_to);

        PRAGMA user_version = 7;
    """)

    # Backfill valid_from for all existing memories
    conn.execute("UPDATE memories SET valid_from = created_at WHERE valid_from IS NULL")

    # Backfill valid_to for already-superseded memories
    conn.execute(
        "UPDATE memories SET valid_to = updated_at WHERE is_active = 0 AND valid_to IS NULL"
    )

    conn.commit()


def _migrate_v7_to_v8(conn: sqlite3.Connection) -> None:
    """Add memory_relations table for graph edges between memories.

    Creates:
        - memory_relations table: Adjacency list for typed edges between memories.
          Supports relation types: updates, supersedes, supports, contradicts,
          resulted_in, related_to.
        - UNIQUE(source_id, target_id, relation_type): Idempotent edge creation
          via INSERT OR IGNORE.
        - Indexes on (source_id, relation_type) and (target_id, relation_type)
          for fast neighbor lookups in either direction.

    Backfill:
        - Creates 'supersedes' edges for all memories where superseded_by IS NOT NULL,
          preserving the temporal relationship from the v6 supersession model.
    """
    conn.executescript("""
        CREATE TABLE IF NOT EXISTS memory_relations (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            source_id INTEGER NOT NULL,
            target_id INTEGER NOT NULL,
            relation_type TEXT NOT NULL,
            metadata TEXT DEFAULT '{}',
            created_at TEXT NOT NULL,
            valid_from TEXT DEFAULT NULL,
            valid_to TEXT DEFAULT NULL,
            invalidated_at TEXT DEFAULT NULL,
            UNIQUE(source_id, target_id, relation_type)
        );

        CREATE INDEX IF NOT EXISTS idx_relations_source
            ON memory_relations(source_id, relation_type);
        CREATE INDEX IF NOT EXISTS idx_relations_target
            ON memory_relations(target_id, relation_type);

        PRAGMA user_version = 8;
    """)

    # Backfill: Create 'supersedes' edges from existing superseded_by column.
    # new_memory supersedes old_memory → source=new, target=old, type=supersedes
    rows = conn.execute(
        "SELECT id, superseded_by, created_at FROM memories WHERE superseded_by IS NOT NULL"
    ).fetchall()
    now = None
    for row in rows:
        old_id = row[0] if isinstance(row, tuple) else row["id"]
        new_id = row[1] if isinstance(row, tuple) else row["superseded_by"]
        created = row[2] if isinstance(row, tuple) else row["created_at"]
        if now is None:
            from datetime import UTC, datetime

            now = datetime.now(tz=UTC).isoformat()
        conn.execute(
            "INSERT OR IGNORE INTO memory_relations "
            "(source_id, target_id, relation_type, created_at, valid_from) "
            "VALUES (?, ?, 'supersedes', ?, ?)",
            (new_id, old_id, now, created),
        )

    conn.commit()


def _migrate_v8_to_v9(conn: sqlite3.Connection) -> None:
    """Add access_count column and consolidation_meta table.

    Adds:
        - access_count INTEGER: Number of times a memory was returned in search
          results. Used for access frequency scoring in adaptive ranking.

    Creates:
        - consolidation_meta table: Tracks last consolidation timestamp for
          automatic consolidation scheduling in the daemon.

    Index:
        - idx_memories_access_count: For sorting by popularity.
    """
    conn.executescript("""
        ALTER TABLE memories ADD COLUMN access_count INTEGER DEFAULT 0;

        CREATE INDEX IF NOT EXISTS idx_memories_access_count
            ON memories(access_count);

        CREATE TABLE IF NOT EXISTS consolidation_meta (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        );

        PRAGMA user_version = 9;
    """)
    conn.commit()


def _migrate_v9_to_v10(conn: sqlite3.Connection) -> None:
    """Add FTS5 full-text search index for hybrid BM25 + vector search.

    Creates:
        - memory_fts virtual table: FTS5 external-content table indexing content
          and category from memories table. Uses porter stemming, Unicode61, and
          diacritics removal for robust text matching.
        - Three AFTER triggers: Keep FTS5 index synchronized with INSERT/UPDATE/DELETE
          on memories table. UPDATE trigger only fires when content or category changes
          to avoid overhead from access_count increments.

    Backfill:
        - Populates FTS5 index from all existing memories via 'rebuild' command.
        - Optimizes FTS5 index after backfill to merge b-tree segments.
    """
    conn.executescript("""
        CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
            content,
            category,
            tokenize='porter unicode61 remove_diacritics 2',
            content='memories',
            content_rowid='id'
        );

        CREATE TRIGGER IF NOT EXISTS memories_ai_fts AFTER INSERT ON memories BEGIN
            INSERT INTO memory_fts(rowid, content, category)
            VALUES (new.id, new.content, new.category);
        END;

        CREATE TRIGGER IF NOT EXISTS memories_ad_fts AFTER DELETE ON memories BEGIN
            INSERT INTO memory_fts(memory_fts, rowid, content, category)
            VALUES('delete', old.id, old.content, old.category);
        END;

        CREATE TRIGGER IF NOT EXISTS memories_au_fts AFTER UPDATE ON memories
        WHEN old.content != new.content OR old.category != new.category BEGIN
            INSERT INTO memory_fts(memory_fts, rowid, content, category)
            VALUES('delete', old.id, old.content, old.category);
            INSERT INTO memory_fts(rowid, content, category)
            VALUES (new.id, new.content, new.category);
        END;

        PRAGMA user_version = 10;
    """)

    # Backfill: populate FTS5 index from existing memories
    conn.execute("INSERT INTO memory_fts(memory_fts) VALUES('rebuild')")

    # Optimize FTS5 index after backfill (merges b-tree segments, reduces query time)
    conn.execute("INSERT INTO memory_fts(memory_fts) VALUES('optimize')")

    conn.commit()
