"""SQLite database management with FTS5 and sqlite-vec."""

import json
import sqlite3
from pathlib import Path

import sqlite_vec

SCHEMA_VERSION = 1


def get_connection(db_path: Path) -> sqlite3.Connection:
    """Open a SQLite connection with sqlite-vec loaded."""
    conn = sqlite3.connect(str(db_path))
    conn.enable_load_extension(True)
    sqlite_vec.load(conn)
    conn.enable_load_extension(False)
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA foreign_keys=ON")
    conn.row_factory = sqlite3.Row
    return conn


def init_schema(conn: sqlite3.Connection, embedding_dim: int):
    """Create all tables, FTS, vector index, and triggers."""
    conn.executescript(f"""
        CREATE TABLE IF NOT EXISTS documents (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            source_path TEXT,
            content_hash TEXT NOT NULL,
            doc_type TEXT NOT NULL CHECK(doc_type IN ('pdf','markdown','code','note')),
            language TEXT,
            created_at TEXT DEFAULT (datetime('now')),
            metadata TEXT DEFAULT '{{}}'
        );

        CREATE TABLE IF NOT EXISTS chunks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
            chunk_index INTEGER NOT NULL,
            text TEXT NOT NULL,
            token_count INTEGER,
            metadata TEXT DEFAULT '{{}}',
            created_at TEXT DEFAULT (datetime('now'))
        );

        CREATE TABLE IF NOT EXISTS tags (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT UNIQUE NOT NULL
        );

        CREATE TABLE IF NOT EXISTS document_tags (
            document_id INTEGER REFERENCES documents(id) ON DELETE CASCADE,
            tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
            PRIMARY KEY (document_id, tag_id)
        );

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

        CREATE INDEX IF NOT EXISTS idx_chunks_document_id ON chunks(document_id);
        CREATE INDEX IF NOT EXISTS idx_documents_content_hash ON documents(content_hash);
    """)

    # FTS5 virtual table (content-sync with chunks)
    conn.execute("""
        CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
            text,
            content='chunks',
            content_rowid='id',
            tokenize='porter unicode61'
        )
    """)

    # FTS sync triggers
    conn.executescript("""
        CREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN
            INSERT INTO chunks_fts(rowid, text) VALUES (new.id, new.text);
        END;

        CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN
            INSERT INTO chunks_fts(chunks_fts, rowid, text) VALUES('delete', old.id, old.text);
        END;

        CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN
            INSERT INTO chunks_fts(chunks_fts, rowid, text) VALUES('delete', old.id, old.text);
            INSERT INTO chunks_fts(rowid, text) VALUES (new.id, new.text);
        END;
    """)

    # Vector table
    conn.execute(f"""
        CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
            chunk_id INTEGER PRIMARY KEY,
            embedding FLOAT[{embedding_dim}]
        )
    """)

    conn.commit()


def get_db_config(conn: sqlite3.Connection, key: str, default: str | None = None) -> str | None:
    """Get a value from the config table."""
    row = conn.execute("SELECT value FROM config WHERE key = ?", (key,)).fetchone()
    return row["value"] if row else default


def set_db_config(conn: sqlite3.Connection, key: str, value: str):
    """Set a value in the config table."""
    conn.execute(
        "INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?",
        (key, value, value),
    )
    conn.commit()


def check_schema_version(conn: sqlite3.Connection) -> int | None:
    """Check the current schema version. Returns None if not initialised."""
    try:
        return int(get_db_config(conn, "schema_version", "0"))
    except Exception:
        return None


def run_migrations(conn: sqlite3.Connection):
    """Run pending schema migrations."""
    current = check_schema_version(conn) or 0

    # Migration registry: version -> callable
    migrations: dict[int, callable] = {
        # Future migrations go here:
        # 2: _migrate_v2,
    }

    for version in sorted(migrations.keys()):
        if current < version:
            migrations[version](conn)
            set_db_config(conn, "schema_version", str(version))

    if current < SCHEMA_VERSION:
        set_db_config(conn, "schema_version", str(SCHEMA_VERSION))


def recreate_vec_table(conn: sqlite3.Connection, embedding_dim: int):
    """Drop and recreate the vector table with a new dimension."""
    conn.execute("DROP TABLE IF EXISTS chunks_vec")
    conn.execute(f"""
        CREATE VIRTUAL TABLE chunks_vec USING vec0(
            chunk_id INTEGER PRIMARY KEY,
            embedding FLOAT[{embedding_dim}]
        )
    """)
    conn.commit()


def insert_document(conn: sqlite3.Connection, title: str, source_path: str | None,
                    content_hash: str, doc_type: str, language: str | None = None,
                    metadata: dict | None = None) -> int:
    """Insert a document and return its ID."""
    cur = conn.execute(
        "INSERT INTO documents (title, source_path, content_hash, doc_type, language, metadata) "
        "VALUES (?, ?, ?, ?, ?, ?)",
        (title, source_path, content_hash, doc_type, language, json.dumps(metadata or {})),
    )
    return cur.lastrowid


def insert_chunk(conn: sqlite3.Connection, document_id: int, chunk_index: int,
                 text: str, token_count: int | None = None,
                 metadata: dict | None = None) -> int:
    """Insert a chunk and return its ID."""
    cur = conn.execute(
        "INSERT INTO chunks (document_id, chunk_index, text, token_count, metadata) "
        "VALUES (?, ?, ?, ?, ?)",
        (document_id, chunk_index, text, token_count, json.dumps(metadata or {})),
    )
    return cur.lastrowid


def insert_embedding(conn: sqlite3.Connection, chunk_id: int, embedding: list[float]):
    """Insert a chunk embedding into the vector table."""
    import struct
    blob = struct.pack(f"{len(embedding)}f", *embedding)
    conn.execute(
        "INSERT INTO chunks_vec (chunk_id, embedding) VALUES (?, ?)",
        (chunk_id, blob),
    )


def hash_exists(conn: sqlite3.Connection, content_hash: str) -> bool:
    """Check if a document with this content hash already exists."""
    row = conn.execute(
        "SELECT 1 FROM documents WHERE content_hash = ? LIMIT 1", (content_hash,)
    ).fetchone()
    return row is not None


def get_or_create_tag(conn: sqlite3.Connection, name: str) -> int:
    """Get or create a tag, return its ID. Tags are stored lowercase."""
    name = name.strip().lower()
    row = conn.execute("SELECT id FROM tags WHERE name = ?", (name,)).fetchone()
    if row:
        return row["id"]
    cur = conn.execute("INSERT INTO tags (name) VALUES (?)", (name,))
    return cur.lastrowid


def tag_document(conn: sqlite3.Connection, document_id: int, tag_names: list[str]):
    """Associate tags with a document."""
    for name in tag_names:
        tag_id = get_or_create_tag(conn, name)
        conn.execute(
            "INSERT OR IGNORE INTO document_tags (document_id, tag_id) VALUES (?, ?)",
            (document_id, tag_id),
        )


def untag_document(conn: sqlite3.Connection, document_id: int, tag_names: list[str]):
    """Remove tag associations from a document."""
    for name in tag_names:
        name = name.strip().lower()
        conn.execute(
            "DELETE FROM document_tags WHERE document_id = ? AND tag_id = "
            "(SELECT id FROM tags WHERE name = ?)",
            (document_id, name),
        )
