"""SQLite database layer with FTS5 full-text search."""

import sqlite3
from pathlib import Path
from typing import Optional


class Database:
    """SQLite database for the footage library."""

    def __init__(self, db_path: str | Path):
        self.db_path = str(db_path)
        self.conn = sqlite3.connect(self.db_path)
        self.conn.row_factory = sqlite3.Row
        self.conn.execute("PRAGMA journal_mode=WAL")
        self.conn.execute("PRAGMA foreign_keys=ON")
        self._init_schema()

    def _init_schema(self):
        self.conn.executescript("""
            CREATE TABLE IF NOT EXISTS clips (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                file_path TEXT UNIQUE NOT NULL,
                file_name TEXT NOT NULL,
                file_size INTEGER,
                duration REAL,
                width INTEGER,
                height INTEGER,
                frame_rate REAL,
                codec TEXT,
                audio_codec TEXT,
                audio_sample_rate INTEGER,
                content_hash TEXT,
                created_at TEXT DEFAULT (datetime('now')),
                indexed_at TEXT DEFAULT (datetime('now')),
                index_status TEXT DEFAULT 'pending',
                error_message TEXT
            );

            CREATE TABLE IF NOT EXISTS transcript_segments (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                clip_id INTEGER NOT NULL REFERENCES clips(id) ON DELETE CASCADE,
                start_time REAL NOT NULL,
                end_time REAL NOT NULL,
                text TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS frame_descriptions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                clip_id INTEGER NOT NULL REFERENCES clips(id) ON DELETE CASCADE,
                timestamp REAL NOT NULL,
                description TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS tags (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                clip_id INTEGER NOT NULL REFERENCES clips(id) ON DELETE CASCADE,
                tag TEXT NOT NULL,
                UNIQUE(clip_id, tag)
            );

            CREATE INDEX IF NOT EXISTS idx_clips_file_path ON clips(file_path);
            CREATE INDEX IF NOT EXISTS idx_clips_content_hash ON clips(content_hash);
            CREATE INDEX IF NOT EXISTS idx_clips_index_status ON clips(index_status);
            CREATE INDEX IF NOT EXISTS idx_transcript_segments_clip_id ON transcript_segments(clip_id);
            CREATE INDEX IF NOT EXISTS idx_frame_descriptions_clip_id ON frame_descriptions(clip_id);
            CREATE INDEX IF NOT EXISTS idx_tags_clip_id ON tags(clip_id);
            CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
        """)

        # FTS5 virtual tables with content-sync triggers
        self.conn.executescript("""
            CREATE VIRTUAL TABLE IF NOT EXISTS transcript_fts USING fts5(
                text,
                content='transcript_segments',
                content_rowid='id'
            );

            CREATE VIRTUAL TABLE IF NOT EXISTS frame_fts USING fts5(
                description,
                content='frame_descriptions',
                content_rowid='id'
            );
        """)

        # Triggers to keep FTS in sync with content tables
        # Transcript triggers
        self.conn.executescript("""
            CREATE TRIGGER IF NOT EXISTS transcript_ai AFTER INSERT ON transcript_segments BEGIN
                INSERT INTO transcript_fts(rowid, text) VALUES (new.id, new.text);
            END;

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

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

        # Frame description triggers
        self.conn.executescript("""
            CREATE TRIGGER IF NOT EXISTS frame_ai AFTER INSERT ON frame_descriptions BEGIN
                INSERT INTO frame_fts(rowid, description) VALUES (new.id, new.description);
            END;

            CREATE TRIGGER IF NOT EXISTS frame_ad AFTER DELETE ON frame_descriptions BEGIN
                INSERT INTO frame_fts(frame_fts, rowid, description) VALUES('delete', old.id, old.description);
            END;

            CREATE TRIGGER IF NOT EXISTS frame_au AFTER UPDATE ON frame_descriptions BEGIN
                INSERT INTO frame_fts(frame_fts, rowid, description) VALUES('delete', old.id, old.description);
                INSERT INTO frame_fts(rowid, description) VALUES (new.id, new.description);
            END;
        """)

        self.conn.commit()

    def close(self):
        if self.conn:
            self.conn.close()
            self.conn = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
        return False

    # ── Clip CRUD ──────────────────────────────────────────────

    def upsert_clip(self, **kwargs) -> int:
        """Insert or update a clip. Returns the clip id.

        Matching priority:
        1. Exact file_path match → update in place
        2. content_hash match (for renamed/moved files) → update path + metadata, preserve clip_id
        3. No match → insert new clip
        """
        file_path = kwargs.get("file_path")
        if not file_path:
            raise ValueError("file_path is required")

        existing = self.get_clip_by_path(file_path)
        if not existing:
            content_hash = kwargs.get("content_hash")
            if content_hash:
                existing = self.get_clip_by_hash(content_hash)

        if existing:
            clip_id = existing["id"]
            # Update all fields including file_path (handles renames)
            updates = {k: v for k, v in kwargs.items() if k != "id"}
            if updates:
                set_clause = ", ".join(f"{k} = ?" for k in updates)
                values = list(updates.values()) + [clip_id]
                self.conn.execute(
                    f"UPDATE clips SET {set_clause}, indexed_at = datetime('now') WHERE id = ?",
                    values,
                )
                self.conn.commit()
            return clip_id

        columns = list(kwargs.keys())
        placeholders = ", ".join("?" for _ in columns)
        col_names = ", ".join(columns)
        values = [kwargs[c] for c in columns]
        cursor = self.conn.execute(
            f"INSERT INTO clips ({col_names}) VALUES ({placeholders})",
            values,
        )
        self.conn.commit()
        return cursor.lastrowid

    def get_clip_by_path(self, file_path: str) -> Optional[dict]:
        row = self.conn.execute(
            "SELECT * FROM clips WHERE file_path = ?", (str(file_path),)
        ).fetchone()
        return dict(row) if row else None

    def get_clip_by_hash(self, content_hash: str) -> Optional[dict]:
        """Find a clip by its content hash."""
        row = self.conn.execute(
            "SELECT * FROM clips WHERE content_hash = ?", (content_hash,)
        ).fetchone()
        return dict(row) if row else None

    def get_clip_by_id(self, clip_id: int) -> Optional[dict]:
        row = self.conn.execute(
            "SELECT * FROM clips WHERE id = ?", (clip_id,)
        ).fetchone()
        return dict(row) if row else None

    def list_clips(self, limit: int = 100, offset: int = 0) -> list[dict]:
        rows = self.conn.execute(
            "SELECT * FROM clips ORDER BY file_path LIMIT ? OFFSET ?",
            (limit, offset),
        ).fetchall()
        return [dict(r) for r in rows]

    def delete_clip(self, clip_id: int):
        """Delete a clip and all associated data (cascades to segments, descriptions, tags)."""
        self.conn.execute("DELETE FROM clips WHERE id = ?", (clip_id,))
        self.conn.commit()

    def find_missing_files(self, directory: str = None) -> list[dict]:
        """Find clips whose files no longer exist on disk (without deleting them).

        Args:
            directory: If provided, only check clips under this directory prefix.

        Returns:
            List of dicts with 'clip_id' and 'file_path' for each missing clip.
        """
        if directory:
            rows = self.conn.execute(
                "SELECT id, file_path FROM clips WHERE file_path LIKE ?",
                (str(directory) + "%",),
            ).fetchall()
        else:
            rows = self.conn.execute("SELECT id, file_path FROM clips").fetchall()

        missing = []
        for row in rows:
            if not Path(row["file_path"]).exists():
                missing.append({"clip_id": row["id"], "file_path": row["file_path"]})

        return missing

    def remove_missing_files(self, directory: str = None) -> list[dict]:
        """Remove clips whose files no longer exist on disk.

        Args:
            directory: If provided, only check clips under this directory prefix.

        Returns:
            List of dicts with 'clip_id' and 'file_path' for each removed clip.
        """
        missing = self.find_missing_files(directory)
        for item in missing:
            self.delete_clip(item["clip_id"])
        return missing

    def update_clip_status(self, clip_id: int, status: str, error_message: str = None):
        self.conn.execute(
            "UPDATE clips SET index_status = ?, error_message = ?, indexed_at = datetime('now') WHERE id = ?",
            (status, error_message, clip_id),
        )
        self.conn.commit()

    def get_clip_count(self) -> dict:
        """Return counts by index_status."""
        rows = self.conn.execute(
            "SELECT index_status, COUNT(*) as count FROM clips GROUP BY index_status"
        ).fetchall()
        result = {"total": 0}
        for row in rows:
            result[row["index_status"]] = row["count"]
            result["total"] += row["count"]
        return result

    # ── Transcript segments ────────────────────────────────────

    def insert_transcript_segments(self, clip_id: int, segments: list[dict]):
        """Delete existing segments and insert new ones (idempotent re-index)."""
        self.conn.execute(
            "DELETE FROM transcript_segments WHERE clip_id = ?", (clip_id,)
        )
        for seg in segments:
            self.conn.execute(
                "INSERT INTO transcript_segments (clip_id, start_time, end_time, text) VALUES (?, ?, ?, ?)",
                (clip_id, seg["start"], seg["end"], seg["text"]),
            )
        self.conn.commit()

    def get_transcript(self, clip_id: int) -> list[dict]:
        rows = self.conn.execute(
            "SELECT start_time, end_time, text FROM transcript_segments WHERE clip_id = ? ORDER BY start_time",
            (clip_id,),
        ).fetchall()
        return [dict(r) for r in rows]

    # ── Frame descriptions ─────────────────────────────────────

    def insert_frame_descriptions(self, clip_id: int, descriptions: list[dict]):
        """Delete existing descriptions and insert new ones (idempotent re-index)."""
        self.conn.execute(
            "DELETE FROM frame_descriptions WHERE clip_id = ?", (clip_id,)
        )
        for desc in descriptions:
            self.conn.execute(
                "INSERT INTO frame_descriptions (clip_id, timestamp, description) VALUES (?, ?, ?)",
                (clip_id, desc["timestamp"], desc["description"]),
            )
        self.conn.commit()

    def get_frame_descriptions(self, clip_id: int) -> list[dict]:
        rows = self.conn.execute(
            "SELECT timestamp, description FROM frame_descriptions WHERE clip_id = ? ORDER BY timestamp",
            (clip_id,),
        ).fetchall()
        return [dict(r) for r in rows]

    # ── Tags ───────────────────────────────────────────────────

    def add_tag(self, clip_id: int, tag: str):
        self.conn.execute(
            "INSERT OR IGNORE INTO tags (clip_id, tag) VALUES (?, ?)",
            (clip_id, tag.lower().strip()),
        )
        self.conn.commit()

    def get_tags(self, clip_id: int) -> list[str]:
        rows = self.conn.execute(
            "SELECT tag FROM tags WHERE clip_id = ? ORDER BY tag", (clip_id,)
        ).fetchall()
        return [r["tag"] for r in rows]

    def list_all_tags(self) -> list[dict]:
        rows = self.conn.execute(
            "SELECT tag, COUNT(*) as count FROM tags GROUP BY tag ORDER BY count DESC"
        ).fetchall()
        return [dict(r) for r in rows]

    # ── FTS search ─────────────────────────────────────────────

    def search_transcripts(self, query: str, limit: int = 20) -> list[dict]:
        """Search transcript text via FTS5. Returns matching segments with clip info."""
        rows = self.conn.execute(
            """
            SELECT ts.clip_id, ts.start_time, ts.end_time, ts.text,
                   c.file_path, c.file_name,
                   rank
            FROM transcript_fts
            JOIN transcript_segments ts ON transcript_fts.rowid = ts.id
            JOIN clips c ON ts.clip_id = c.id
            WHERE transcript_fts MATCH ?
            ORDER BY rank
            LIMIT ?
            """,
            (query, limit),
        ).fetchall()
        return [dict(r) for r in rows]

    def search_frames(self, query: str, limit: int = 20) -> list[dict]:
        """Search frame descriptions via FTS5."""
        rows = self.conn.execute(
            """
            SELECT fd.clip_id, fd.timestamp, fd.description,
                   c.file_path, c.file_name,
                   rank
            FROM frame_fts
            JOIN frame_descriptions fd ON frame_fts.rowid = fd.id
            JOIN clips c ON fd.clip_id = c.id
            WHERE frame_fts MATCH ?
            ORDER BY rank
            LIMIT ?
            """,
            (query, limit),
        ).fetchall()
        return [dict(r) for r in rows]

    # ── Directory browsing ─────────────────────────────────────

    def list_subdirectories(self) -> list[dict]:
        """List unique parent directories and clip counts."""
        rows = self.conn.execute(
            """
            SELECT
                CASE
                    WHEN instr(file_path, '/') > 0
                    THEN substr(file_path, 1, length(file_path) - length(file_name) - 1)
                    ELSE '.'
                END as directory,
                COUNT(*) as clip_count
            FROM clips
            GROUP BY directory
            ORDER BY directory
            """
        ).fetchall()
        return [dict(r) for r in rows]
