import atexit
from typing import Any, Dict, List, Tuple
from contextlib import contextmanager
import sqlite3
from importlib import resources
import shutil
from pathlib import Path


class Database:
    MAX_PREFIX = 24
    PATH = "localis.data"
    FILENAME = "localis.db"
    CONFIG_FILE = Path.cwd() / ".localis.conf"

    def __init__(self, db_path: str = None):
        self.db_path: str = db_path or self.get_db_path()
        self._conn: sqlite3.Connection | None = None
        self._setup_conn()

    def _setup_conn(self) -> None:
        self._conn = sqlite3.connect(self.db_path)
        self._conn.row_factory = sqlite3.Row

        self._conn.execute("PRAGMA journal_mode = OFF")
        self._conn.execute("PRAGMA synchronous = OFF")
        self._conn.execute("PRAGMA temp_store = MEMORY")
        self._conn.execute("PRAGMA cache_size = -100000")
        self._conn.execute("PRAGMA locking_mode = EXCLUSIVE")
        self._conn.execute("PRAGMA mmap_size = 268435456")

    def __enter__(self) -> "Database":
        return self

    def __exit__(self, exc_type, exc_value, traceback) -> None:
        self.close()

    def close(self) -> None:
        if self._conn:
            self._conn.close()
            self._conn = None

    def connect(self) -> None:
        if not self._conn:
            self._setup_conn()

    def commit(self) -> None:
        self._conn.commit()

    def clone(self) -> "Database":
        return Database(self.db_path)

    @contextmanager
    def atomic(self):
        try:
            yield
            self._conn.commit()
        except Exception:
            self._conn.rollback()
            raise

    @classmethod
    def copy_to(cls, dir: str = None, filename: str = FILENAME) -> str:
        """Copy the database to current working directory, returning the new path."""
        path = Path(dir) if dir else Path.cwd()
        target_path = path / filename

        with resources.path(cls.PATH, cls.FILENAME) as db_file:
            shutil.copy(db_file, target_path)

        return str(target_path)

    def execute(self, query: str, params: Tuple | Dict = ()) -> sqlite3.Cursor:
        """Execute a single query"""
        cursor = self._conn.cursor()
        cursor.execute(query, params)
        return cursor

    def execute_many(
        self, query: str, params_list: List[Tuple | Dict]
    ) -> sqlite3.Cursor:
        """Execute query with multiple parameter sets"""
        cursor = self._conn.cursor()
        cursor.executemany(query, params_list)
        return cursor

    def insert_many(self, table: str, data_list: List[Dict[str, Any]]) -> None:
        """Insert multiple rows"""
        if not data_list:
            return

        column_list = list(data_list[0].keys())
        columns = ", ".join(column_list)
        placeholders = ", ".join(["?" for _ in column_list])
        query = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"

        params_list = [tuple(row[col] for col in column_list) for row in data_list]
        self.execute_many(query, params_list)

    def create_table(self, table_name: str, columns: str) -> None:
        """Create table with given columns definition"""
        query = f"CREATE TABLE IF NOT EXISTS {table_name} ({columns})"
        self.execute(query)
        self._conn.commit()

    def create_tables(self, tables: list[object]) -> None:
        for table in tables:
            if hasattr(table, "create_table") and callable(table.create_table):
                table.create_table()

    def drop_tables(self, tables: list[object]) -> None:
        for table in tables:
            if hasattr(table, "drop") and callable(table.drop):
                table.drop()

    def create_fts_table(
        self,
        table_name: str,
        columns: List[str],
    ) -> None:
        """Create FTS5 virtual table"""
        columns_str = ", ".join(columns)

        prefix_list = " ".join([str(i) for i in range(2, self.MAX_PREFIX + 1)])

        fts_options = [
            '''tokenize = "unicode61 remove_diacritics 2"''',
            f'''prefix="{prefix_list}"''',
        ]

        options_str = ", " + ", ".join(fts_options) if fts_options else ""

        query = f"""CREATE VIRTUAL TABLE IF NOT EXISTS "{table_name}" USING fts5({columns_str}{options_str})"""
        self.execute(query)
        self._conn.commit()

    def vacuum(self) -> None:
        """Vacuum database"""
        self.execute("VACUUM")

    def analyze(self) -> sqlite3.Cursor:
        """Analyze database for query optimization"""
        return self.execute("ANALYZE")

    def set_db_path(self, path: str) -> None:
        """Stores the database path in a config file for external storage and reloads the connection with the new path."""
        self.CONFIG_FILE.write_text(path)
        self.close()
        self.db_path = path
        self._setup_conn()

    @classmethod
    def get_db_path(cls) -> str:
        """Fetches the database path from config or defaults to the bundled .db file."""
        if cls.CONFIG_FILE.exists():
            return cls.CONFIG_FILE.read_text().strip()

        from importlib.resources import files

        db_file = files(cls.PATH) / cls.FILENAME
        return str(db_file)

    def revert_to_default(self):
        """Removes config file, external database and reverts to the bundled db file."""
        self.close()
        self.CONFIG_FILE.unlink()
        db_file = Path(self.db_path)
        db_file.unlink()
        self.db_path = self.get_db_path()
        self._setup_conn()


db = Database()


@atexit.register
def close_db() -> None:
    db.close()
