"""SQLite database for symbol indexing with FTS5 full-text search.

This module provides persistent storage and fast search for code symbols
across large codebases.
"""

from __future__ import annotations

import sqlite3
import hashlib
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Optional, Any
import threading

# Thread-local storage for connections
_local = threading.local()


@dataclass
class IndexStats:
    """Statistics about the index."""

    total_files: int = 0
    total_symbols: int = 0
    total_references: int = 0
    last_indexed: Optional[datetime] = None
    index_size_bytes: int = 0


class SymbolDatabase:
    """SQLite database for symbol storage and search."""

    def __init__(self, db_path: str | Path | None = None):
        """Initialize the database.

        Args:
            db_path: Path to the database file. Defaults to ~/.ieum/index.db
        """
        if db_path is None:
            db_path = Path.home() / ".ieum" / "index.db"

        self.db_path = Path(db_path)
        self.db_path.parent.mkdir(parents=True, exist_ok=True)

        self._init_schema()

    def _get_connection(self) -> sqlite3.Connection:
        """Get thread-local database connection."""
        if not hasattr(_local, "conn") or _local.conn is None:
            _local.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
            _local.conn.row_factory = sqlite3.Row
            _local.conn.execute("PRAGMA journal_mode=WAL")
            _local.conn.execute("PRAGMA synchronous=NORMAL")
        return _local.conn

    def _init_schema(self) -> None:
        """Initialize database schema."""
        conn = self._get_connection()
        conn.executescript("""
            -- Projects table
            CREATE TABLE IF NOT EXISTS projects (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                path TEXT UNIQUE NOT NULL,
                name TEXT NOT NULL,
                last_indexed TEXT,
                file_count INTEGER DEFAULT 0,
                symbol_count INTEGER DEFAULT 0
            );

            -- Files table
            CREATE TABLE IF NOT EXISTS files (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
                path TEXT NOT NULL,
                relative_path TEXT NOT NULL,
                hash TEXT NOT NULL,
                language TEXT,
                last_modified TEXT,
                symbol_count INTEGER DEFAULT 0,
                UNIQUE(project_id, path)
            );

            -- Symbols table
            CREATE TABLE IF NOT EXISTS symbols (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
                name TEXT NOT NULL,
                qualified_name TEXT,
                type TEXT NOT NULL,
                line INTEGER NOT NULL,
                column INTEGER DEFAULT 0,
                end_line INTEGER,
                end_column INTEGER,
                parent TEXT,
                signature TEXT,
                docstring TEXT,
                visibility TEXT DEFAULT 'default',
                is_static INTEGER DEFAULT 0,
                return_type TEXT,
                metadata TEXT
            );

            -- References table
            CREATE TABLE IF NOT EXISTS refs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                symbol_id INTEGER REFERENCES symbols(id) ON DELETE CASCADE,
                file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
                name TEXT NOT NULL,
                line INTEGER NOT NULL,
                column INTEGER DEFAULT 0,
                context TEXT,
                ref_type TEXT DEFAULT 'usage'
            );

            -- Full-text search for symbols
            CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(
                name,
                qualified_name,
                docstring,
                signature,
                content='symbols',
                content_rowid='id',
                tokenize='porter unicode61'
            );

            -- Triggers to keep FTS in sync
            CREATE TRIGGER IF NOT EXISTS symbols_ai AFTER INSERT ON symbols BEGIN
                INSERT INTO symbols_fts(rowid, name, qualified_name, docstring, signature)
                VALUES (new.id, new.name, new.qualified_name, new.docstring, new.signature);
            END;

            CREATE TRIGGER IF NOT EXISTS symbols_ad AFTER DELETE ON symbols BEGIN
                INSERT INTO symbols_fts(symbols_fts, rowid, name, qualified_name, docstring, signature)
                VALUES ('delete', old.id, old.name, old.qualified_name, old.docstring, old.signature);
            END;

            CREATE TRIGGER IF NOT EXISTS symbols_au AFTER UPDATE ON symbols BEGIN
                INSERT INTO symbols_fts(symbols_fts, rowid, name, qualified_name, docstring, signature)
                VALUES ('delete', old.id, old.name, old.qualified_name, old.docstring, old.signature);
                INSERT INTO symbols_fts(rowid, name, qualified_name, docstring, signature)
                VALUES (new.id, new.name, new.qualified_name, new.docstring, new.signature);
            END;

            -- Indexes
            CREATE INDEX IF NOT EXISTS idx_files_project ON files(project_id);
            CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
            CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id);
            CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
            CREATE INDEX IF NOT EXISTS idx_symbols_type ON symbols(type);
            CREATE INDEX IF NOT EXISTS idx_symbols_parent ON symbols(parent);
            CREATE INDEX IF NOT EXISTS idx_refs_symbol ON refs(symbol_id);
            CREATE INDEX IF NOT EXISTS idx_refs_file ON refs(file_id);
            CREATE INDEX IF NOT EXISTS idx_refs_name ON refs(name);
        """)
        conn.commit()

    def close(self) -> None:
        """Close database connection."""
        if hasattr(_local, "conn") and _local.conn is not None:
            _local.conn.close()
            _local.conn = None

    # Project operations
    def get_or_create_project(self, path: str) -> int:
        """Get or create a project entry.

        Args:
            path: Project root path.

        Returns:
            Project ID.
        """
        conn = self._get_connection()
        path = str(Path(path).resolve())
        name = Path(path).name

        cursor = conn.execute(
            "SELECT id FROM projects WHERE path = ?",
            (path,)
        )
        row = cursor.fetchone()

        if row:
            return row["id"]

        cursor = conn.execute(
            "INSERT INTO projects (path, name) VALUES (?, ?)",
            (path, name)
        )
        conn.commit()
        return cursor.lastrowid

    def update_project_stats(self, project_id: int) -> None:
        """Update project statistics."""
        conn = self._get_connection()

        # Count files
        cursor = conn.execute(
            "SELECT COUNT(*) as cnt FROM files WHERE project_id = ?",
            (project_id,)
        )
        file_count = cursor.fetchone()["cnt"]

        # Count symbols
        cursor = conn.execute("""
            SELECT COUNT(*) as cnt FROM symbols s
            JOIN files f ON s.file_id = f.id
            WHERE f.project_id = ?
        """, (project_id,))
        symbol_count = cursor.fetchone()["cnt"]

        conn.execute("""
            UPDATE projects
            SET file_count = ?, symbol_count = ?, last_indexed = ?
            WHERE id = ?
        """, (file_count, symbol_count, datetime.now().isoformat(), project_id))
        conn.commit()

    # File operations
    def get_file_hash(self, file_path: str) -> Optional[str]:
        """Get stored hash for a file."""
        conn = self._get_connection()
        cursor = conn.execute(
            "SELECT hash FROM files WHERE path = ?",
            (str(file_path),)
        )
        row = cursor.fetchone()
        return row["hash"] if row else None

    def add_file(
        self,
        project_id: int,
        file_path: str,
        file_hash: str,
        language: str,
    ) -> int:
        """Add or update a file entry.

        Args:
            project_id: Project ID.
            file_path: Absolute file path.
            file_hash: Content hash.
            language: Programming language.

        Returns:
            File ID.
        """
        conn = self._get_connection()
        file_path = str(Path(file_path).resolve())

        # Get project path for relative path calculation
        cursor = conn.execute("SELECT path FROM projects WHERE id = ?", (project_id,))
        project_path = cursor.fetchone()["path"]
        relative_path = str(Path(file_path).relative_to(project_path))

        # Check if file exists
        cursor = conn.execute(
            "SELECT id FROM files WHERE project_id = ? AND path = ?",
            (project_id, file_path)
        )
        row = cursor.fetchone()

        if row:
            file_id = row["id"]
            # Delete existing symbols and refs
            conn.execute("DELETE FROM symbols WHERE file_id = ?", (file_id,))
            conn.execute("DELETE FROM refs WHERE file_id = ?", (file_id,))
            # Update file entry
            conn.execute("""
                UPDATE files SET hash = ?, language = ?, last_modified = ?
                WHERE id = ?
            """, (file_hash, language, datetime.now().isoformat(), file_id))
        else:
            cursor = conn.execute("""
                INSERT INTO files (project_id, path, relative_path, hash, language, last_modified)
                VALUES (?, ?, ?, ?, ?, ?)
            """, (project_id, file_path, relative_path, file_hash, language, datetime.now().isoformat()))
            file_id = cursor.lastrowid

        conn.commit()
        return file_id

    def remove_file(self, file_path: str) -> None:
        """Remove a file and its symbols."""
        conn = self._get_connection()
        conn.execute("DELETE FROM files WHERE path = ?", (str(file_path),))
        conn.commit()

    # Symbol operations
    def add_symbol(self, file_id: int, symbol: dict) -> int:
        """Add a symbol entry.

        Args:
            file_id: File ID.
            symbol: Symbol data dictionary.

        Returns:
            Symbol ID.
        """
        conn = self._get_connection()

        cursor = conn.execute("""
            INSERT INTO symbols (
                file_id, name, qualified_name, type, line, column,
                end_line, end_column, parent, signature, docstring,
                visibility, is_static, return_type, metadata
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            file_id,
            symbol.get("name"),
            symbol.get("qualified_name") or symbol.get("name"),
            symbol.get("type"),
            symbol.get("line"),
            symbol.get("column", 0),
            symbol.get("end_line"),
            symbol.get("end_column"),
            symbol.get("parent"),
            symbol.get("signature"),
            symbol.get("docstring"),
            symbol.get("visibility", "default"),
            1 if symbol.get("is_static") else 0,
            symbol.get("return_type"),
            str(symbol.get("metadata", {})),
        ))

        conn.commit()
        return cursor.lastrowid

    def add_reference(self, file_id: int, reference: dict, symbol_id: Optional[int] = None) -> int:
        """Add a reference entry.

        Args:
            file_id: File ID.
            reference: Reference data dictionary.
            symbol_id: Optional symbol ID this references.

        Returns:
            Reference ID.
        """
        conn = self._get_connection()

        cursor = conn.execute("""
            INSERT INTO refs (symbol_id, file_id, name, line, column, context, ref_type)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        """, (
            symbol_id,
            file_id,
            reference.get("symbol_name") or reference.get("name"),
            reference.get("line"),
            reference.get("column", 0),
            reference.get("context"),
            reference.get("reference_type", "usage"),
        ))

        conn.commit()
        return cursor.lastrowid

    def update_file_symbol_count(self, file_id: int) -> None:
        """Update symbol count for a file."""
        conn = self._get_connection()
        cursor = conn.execute(
            "SELECT COUNT(*) as cnt FROM symbols WHERE file_id = ?",
            (file_id,)
        )
        count = cursor.fetchone()["cnt"]
        conn.execute(
            "UPDATE files SET symbol_count = ? WHERE id = ?",
            (count, file_id)
        )
        conn.commit()

    # Search operations
    def search_symbols(
        self,
        query: str,
        project_id: Optional[int] = None,
        symbol_type: Optional[str] = None,
        limit: int = 50,
    ) -> list[dict]:
        """Search symbols using full-text search.

        Args:
            query: Search query.
            project_id: Optional project filter.
            symbol_type: Optional type filter.
            limit: Maximum results.

        Returns:
            List of matching symbols.
        """
        conn = self._get_connection()

        # Build query
        sql = """
            SELECT s.*, f.path as file_path, f.relative_path
            FROM symbols s
            JOIN symbols_fts fts ON s.id = fts.rowid
            JOIN files f ON s.file_id = f.id
            WHERE symbols_fts MATCH ?
        """
        params = [query]

        if project_id:
            sql += " AND f.project_id = ?"
            params.append(project_id)

        if symbol_type:
            sql += " AND s.type = ?"
            params.append(symbol_type)

        sql += " ORDER BY rank LIMIT ?"
        params.append(limit)

        cursor = conn.execute(sql, params)
        return [dict(row) for row in cursor.fetchall()]

    def search_by_name(
        self,
        name: str,
        project_id: Optional[int] = None,
        exact: bool = False,
    ) -> list[dict]:
        """Search symbols by name.

        Args:
            name: Symbol name.
            project_id: Optional project filter.
            exact: If True, match exact name only.

        Returns:
            List of matching symbols.
        """
        conn = self._get_connection()

        if exact:
            sql = """
                SELECT s.*, f.path as file_path, f.relative_path
                FROM symbols s
                JOIN files f ON s.file_id = f.id
                WHERE s.name = ?
            """
            params = [name]
        else:
            sql = """
                SELECT s.*, f.path as file_path, f.relative_path
                FROM symbols s
                JOIN files f ON s.file_id = f.id
                WHERE s.name LIKE ?
            """
            params = [f"%{name}%"]

        if project_id:
            sql += " AND f.project_id = ?"
            params.append(project_id)

        cursor = conn.execute(sql, params)
        return [dict(row) for row in cursor.fetchall()]

    def get_symbol_references(
        self,
        symbol_name: str,
        project_id: Optional[int] = None,
    ) -> list[dict]:
        """Get all references to a symbol.

        Args:
            symbol_name: Symbol name.
            project_id: Optional project filter.

        Returns:
            List of references.
        """
        conn = self._get_connection()

        sql = """
            SELECT r.*, f.path as file_path, f.relative_path
            FROM refs r
            JOIN files f ON r.file_id = f.id
            WHERE r.name = ?
        """
        params = [symbol_name]

        if project_id:
            sql += " AND f.project_id = ?"
            params.append(project_id)

        cursor = conn.execute(sql, params)
        return [dict(row) for row in cursor.fetchall()]

    def get_file_symbols(self, file_path: str) -> list[dict]:
        """Get all symbols in a file.

        Args:
            file_path: File path.

        Returns:
            List of symbols.
        """
        conn = self._get_connection()

        cursor = conn.execute("""
            SELECT s.*
            FROM symbols s
            JOIN files f ON s.file_id = f.id
            WHERE f.path = ?
            ORDER BY s.line
        """, (str(file_path),))

        return [dict(row) for row in cursor.fetchall()]

    def get_stats(self, project_id: Optional[int] = None) -> IndexStats:
        """Get index statistics.

        Args:
            project_id: Optional project filter.

        Returns:
            IndexStats object.
        """
        conn = self._get_connection()

        if project_id:
            cursor = conn.execute(
                "SELECT * FROM projects WHERE id = ?",
                (project_id,)
            )
            row = cursor.fetchone()
            if row:
                return IndexStats(
                    total_files=row["file_count"],
                    total_symbols=row["symbol_count"],
                    last_indexed=datetime.fromisoformat(row["last_indexed"]) if row["last_indexed"] else None,
                )
        else:
            cursor = conn.execute("SELECT COUNT(*) as cnt FROM files")
            files = cursor.fetchone()["cnt"]

            cursor = conn.execute("SELECT COUNT(*) as cnt FROM symbols")
            symbols = cursor.fetchone()["cnt"]

            cursor = conn.execute("SELECT COUNT(*) as cnt FROM refs")
            refs = cursor.fetchone()["cnt"]

            return IndexStats(
                total_files=files,
                total_symbols=symbols,
                total_references=refs,
                index_size_bytes=self.db_path.stat().st_size if self.db_path.exists() else 0,
            )

        return IndexStats()


def compute_file_hash(file_path: str | Path) -> str:
    """Compute hash of file contents.

    Args:
        file_path: Path to file.

    Returns:
        MD5 hash string.
    """
    hasher = hashlib.md5()
    with open(file_path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            hasher.update(chunk)
    return hasher.hexdigest()
