import logging
import sqlite3

from mcard.config.settings import settings
from mcard.engine.abstract_sql_engine import AbstractSqlEngine
from mcard.engine.base import DatabaseConnection, resolve_db_path
from mcard.model.card import MCard
from mcard.model.pagination import Page

# DEFAULT_PAGE_SIZE is used in function signatures, so we fetch it once or define a local constant
# However, usually we can access settings.pagination.default_page_size if it's constant.
# For function signatures, we can't use dynamic properties easily if they change.
# But settings are generally static after init.
# To avoid "argument default is mutable" or weirdness, we can grab the value.
DEFAULT_PAGE_SIZE = settings.pagination.default_page_size

logger = logging.getLogger(__name__)


class SQLiteConnection(DatabaseConnection):
    """Database connection for SQLite.

    The schema is loaded exclusively from the canonical SQL files via the
    ``MCardSchema`` singleton.  No hardcoded CREATE TABLE statements should
    appear in this file.
    """

    # =====================================================================
    # Single Source of Truth: Schema files
    # =====================================================================
    # The SQLite engine does NOT define its own schema.  It loads all
    # table / index definitions from the canonical SQL files via the
    # MCardSchema singleton.
    #
    # Canonical source files:
    #   schema/mcard_schema.sql         (REQUIRED -- core tables)
    #   schema/mcard_vector_schema.sql  (optional -- vector/graph tables)
    #
    # If a new table is added to either file, this engine picks it up
    # automatically.  No hardcoded CREATE TABLE statements should ever
    # appear in this file.
    # =====================================================================

    SCHEMA_SOURCE_FILES = (
        "schema/mcard_schema.sql",  # Required -- core tables
        "schema/mcard_vector_schema.sql",  # Optional -- vector/graph tables
    )

    def __init__(self, db_path: str):
        self.db_path = resolve_db_path(db_path)
        self.conn: sqlite3.Connection | None = None

    def connect(self):
        """Open the SQLite connection and initialise schema if the DB is empty.

        All schema definitions are loaded from the canonical SQL files via
        ``MCardSchema.init_all_tables(conn)``.  No CREATE TABLE statements
        are hardcoded in this method.

        Raises:
            FileNotFoundError: if the required core schema SQL file is missing.
        """
        logger.debug(f"Connecting to database at {self.db_path}")
        try:
            self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
            # Set busy_timeout to match TypeScript SqliteNodeEngine parity
            from mcard.config.config_constants import SQLITE_BUSY_TIMEOUT_MS

            self.conn.execute(f"PRAGMA busy_timeout = {SQLITE_BUSY_TIMEOUT_MS}")
            logger.debug(f"Connection established to {self.db_path}")

            # Check if the database is empty and initialize schema if necessary
            cursor = self.conn.cursor()
            cursor.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='card'"
            )

            if not cursor.fetchone():
                logger.debug(f"Creating new database schema at {self.db_path}")

                from mcard.schema import MCardSchema

                logger.debug("SQLite schema loading from canonical source")

                schema = MCardSchema.get_instance()

                # Initialize ALL tables from canonical SQL files
                count = schema.init_all_tables(self.conn)
                logger.debug(
                    f"Database schema created successfully "
                    f"({count} statements executed from canonical SQL files)"
                )

                # Apply triggers from settings (legacy, references documents table)
                triggers = settings.database.triggers
                for trigger in triggers:
                    self.conn.execute(trigger)
                    self.conn.commit()
            else:
                logger.debug(f"Using existing database at {self.db_path}")

        except sqlite3.Error as e:
            logger.error(
                f"Database error connecting to {self.db_path}: {e}", exc_info=True
            )
            raise

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

    def commit(self):
        if self.conn:
            self.conn.commit()

    def rollback(self):
        if self.conn:
            self.conn.rollback()


class SQLiteEngine(AbstractSqlEngine):
    def __init__(self, connection: SQLiteConnection):
        self.connection = connection
        self.connection.connect()

    def __del__(self):
        self.connection.disconnect()

    def __enter__(self):
        return self

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

    # =====================================================================
    # AbstractSqlEngine primitives
    # =====================================================================

    def _query_rows(self, sql: str, params: tuple = ()) -> list[dict]:
        cursor = self.connection.conn.cursor()
        cursor.execute(sql, params)
        columns = [desc[0] for desc in cursor.description] if cursor.description else []
        return [dict(zip(columns, row, strict=False)) for row in cursor.fetchall()]

    def _exec_sql(self, sql: str, params: tuple = ()) -> int:
        cursor = self.connection.conn.cursor()
        cursor.execute(sql, params)
        self.connection.commit()
        return cursor.rowcount

    def _ensure_handle_tables(self) -> None:
        """Ensure the handle_registry and handle_history tables exist.

        Uses the MCardSchema singleton to load table definitions from
        the canonical SQL files.
        """
        from mcard.schema import MCardSchema

        schema = MCardSchema.get_instance()
        cursor = self.connection.conn.cursor()
        cursor.execute(schema.get_table("handle_registry"))
        cursor.execute(schema.get_table("handle_history"))
        cursor.execute(schema.get_index("idx_handle_current_hash"))
        self.connection.commit()
        logger.debug("Handle tables ensured.")

    # =====================================================================
    # Card Operations (engine-specific)
    # =====================================================================

    def add(self, card: MCard) -> str:
        hash_value = str(card.hash)
        try:
            cursor = self.connection.conn.cursor()
            # Ensure content is bytes
            content_bytes = (
                card.content
                if isinstance(card.content, bytes)
                else card.content.encode("utf-8")
            )
            cursor.execute(
                "INSERT INTO card (hash, content, g_time) VALUES (?, ?, ?)",
                (hash_value, content_bytes, str(card.g_time)),
            )
            self.connection.commit()
            logger.debug(f"Added card with hash {hash_value}")
            return hash_value
        except sqlite3.IntegrityError as e:
            raise ValueError(
                f"Card with hash {hash_value} and {str(card.g_time)} already exists, \n Content: {card.content[:20]}"
            ) from e

    def get(self, hash_value: str) -> MCard | None:
        cursor = self.connection.conn.cursor()
        cursor.execute(
            "SELECT content, g_time, hash FROM card WHERE hash = ?", (str(hash_value),)
        )
        row = cursor.fetchone()

        if not row:
            return None

        content, g_time, hash_val = row
        return self._row_to_card(content, hash_val, g_time)

    def delete(self, hash_value: str) -> bool:
        cursor = self.connection.conn.cursor()
        cursor.execute("DELETE FROM card WHERE hash = ?", (str(hash_value),))
        self.connection.commit()
        return cursor.rowcount > 0

    def search_by_string(
        self,
        search_string: str,
        page_number: int = 1,
        page_size: int = DEFAULT_PAGE_SIZE,
    ) -> Page:
        """Search for cards by string in content, hash, or g_time"""
        if page_number < 1:
            raise ValueError("Page number must be >= 1")
        if page_size < 1:
            raise ValueError("Page size must be >= 1")

        offset = (page_number - 1) * page_size
        cursor = self.connection.conn.cursor()

        # Get total count of matching items
        # Use CAST(content AS TEXT) to search within BLOB content as text
        cursor.execute(
            """
            SELECT COUNT(*) FROM card
            WHERE hash LIKE ? OR g_time LIKE ? OR CAST(content AS TEXT) LIKE ?
        """,
            (f"%{search_string}%", f"%{search_string}%", f"%{search_string}%"),
        )
        total_items = cursor.fetchone()[0]

        # Get the actual items for the current page
        cursor.execute(
            """
            SELECT content, g_time, hash FROM card
            WHERE hash LIKE ? OR g_time LIKE ? OR CAST(content AS TEXT) LIKE ?
            ORDER BY g_time DESC LIMIT ? OFFSET ?
        """,
            (
                f"%{search_string}%",
                f"%{search_string}%",
                f"%{search_string}%",
                page_size,
                offset,
            ),
        )

        items = [
            self._row_to_card(content, hash_val, g_time)
            for content, g_time, hash_val in cursor.fetchall()
        ]

        return Page.create(items, total_items, page_number, page_size)

    def search_by_content(
        self,
        search_string: str | bytes,
        page_number: int = 1,
        page_size: int = DEFAULT_PAGE_SIZE,
    ) -> Page:
        """Search for cards by string or binary pattern in content.

        Args:
            search_string: The string or binary pattern to search for
            page_number: The page number (1-based)
            page_size: Number of items per page

        Returns:
            A Page object containing the matching cards
        """
        if page_number < 1:
            raise ValueError("Page number must be >= 1")
        if page_size < 1:
            raise ValueError("Page size must be >= 1")

        if not search_string:
            raise ValueError("Search string cannot be empty")

        offset = (page_number - 1) * page_size
        cursor = self.connection.conn.cursor()

        # Convert search string to bytes if it's a string
        if isinstance(search_string, str):
            search_bytes = search_string.encode("utf-8")
        else:
            search_bytes = search_string

        # For binary search, we need to use the INSTR function to find the pattern
        cursor.execute(
            """
            SELECT COUNT(*) FROM card
            WHERE INSTR(content, ?) > 0
        """,
            (search_bytes,),
        )
        total_items = cursor.fetchone()[0]

        # Get the actual items for the current page
        cursor.execute(
            """
            SELECT content, g_time, hash FROM card
            WHERE INSTR(content, ?) > 0
            ORDER BY g_time DESC
            LIMIT ? OFFSET ?
        """,
            (search_bytes, page_size, offset),
        )

        items = [
            self._row_to_card(content, hash_val, g_time)
            for content, g_time, hash_val in cursor.fetchall()
        ]

        return Page.create(items, total_items, page_number, page_size)

    def count(self) -> int:
        cursor = self.connection.conn.cursor()
        cursor.execute("SELECT COUNT(*) FROM card")
        return cursor.fetchone()[0]

    def get_cards_by_page(
        self, page_number: int = 1, page_size: int = DEFAULT_PAGE_SIZE
    ) -> Page:
        """Get a page of cards from the database.

        Args:
            page_number: The page number (1-based).
            page_size: Number of items per page.

        Returns:
            A Page object containing the requested cards.

        Raises:
            ValueError: If page_number < 1 or page_size < 1
        """
        if page_number < 1:
            raise ValueError("Page number must be >= 1")
        if page_size < 1:
            raise ValueError("Page size must be >= 1")

        cursor = self.connection.conn.cursor()
        cursor.execute("SELECT COUNT(*) FROM card")
        total_items = cursor.fetchone()[0]

        offset = (page_number - 1) * page_size
        cursor.execute(
            "SELECT content, g_time, hash FROM card ORDER BY g_time DESC LIMIT ? OFFSET ?",
            (page_size, offset),
        )

        items = [
            self._row_to_card(content, hash_val, g_time)
            for content, g_time, hash_val in cursor.fetchall()
        ]

        return Page.create(items, total_items, page_number, page_size)
