import os.path
import sqlite3

from .. import DEFAULT_TABLE, RECURRENT_TABLE
from .base import Pocket, RecurrentEntrySchema, StandardEntrySchema
from .utils import DatabaseInterface


class SqliteInterface(DatabaseInterface):
    """Database interface implementation using SQLite."""

    # Valid table names for security
    _VALID_TABLES = {DEFAULT_TABLE, RECURRENT_TABLE}
    # Valid column names for each table
    _VALID_COLUMNS = {
        DEFAULT_TABLE: set(StandardEntrySchema().fields.keys()),
        RECURRENT_TABLE: set(RecurrentEntrySchema().fields.keys()),
    }

    def __init__(self, *args, **kwargs):
        """Initialize SQLite database connection.

        :param args: positional arguments for sqlite3.connect
        :param kwargs: keyword arguments for sqlite3.connect
        """
        self._conn = sqlite3.connect(*args, **kwargs)
        self._conn.row_factory = sqlite3.Row
        self._create_tables()

    def _validate_table_name(self, table_name):
        """Validate table name to prevent SQL injection.

        :param table_name: name of the table
        :raise ValueError: if table name is invalid
        """
        if table_name not in self._VALID_TABLES:
            raise ValueError(f"Invalid table name: {table_name}")

    def _validate_columns(self, table_name, columns):
        """Validate column names to prevent SQL injection.

        :param table_name: name of the table
        :param columns: iterable of column names
        :raise ValueError: if any column name is invalid
        """
        valid_columns = self._VALID_COLUMNS.get(table_name, set())
        for col in columns:
            if col not in valid_columns:
                raise ValueError(f"Invalid column name for {table_name}: {col}")

    def _create_tables(self):
        """Create tables if they don't exist."""
        cursor = self._conn.cursor()

        cursor.execute("""
            CREATE TABLE IF NOT EXISTS standard (
                eid INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                date TEXT NOT NULL,
                category TEXT,
                value REAL NOT NULL
            )
        """)

        cursor.execute("""
            CREATE TABLE IF NOT EXISTS recurrent (
                eid INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                start TEXT NOT NULL,
                end TEXT,
                frequency TEXT NOT NULL,
                category TEXT,
                value REAL NOT NULL
            )
        """)

        self._conn.commit()

    def retrieve(self, table_name, filters=None):
        self._validate_table_name(table_name)
        cursor = self._conn.cursor()

        if not filters:
            cursor.execute(f"SELECT * FROM {table_name}")

        else:
            self._validate_columns(table_name, filters.keys())
            where_sql, params = self.create_query_condition(**filters)
            query = f"SELECT * FROM {table_name} WHERE {where_sql}"
            cursor.execute(query, params)

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

    def retrieve_by_id(self, table_name, element_id):
        self._validate_table_name(table_name)
        cursor = self._conn.cursor()
        cursor.execute(f"SELECT * FROM {table_name} WHERE eid = ?", (element_id,))
        row = cursor.fetchone()

        if row is None:
            return None

        # Convert to dict and exclude eid field to match TinyDB behavior
        result = dict(row)
        result.pop("eid", None)
        return result

    def create(self, table_name, data):
        self._validate_table_name(table_name)
        self._validate_columns(table_name, data.keys())
        cursor = self._conn.cursor()

        # Build INSERT statement
        columns = ", ".join(data.keys())
        placeholders = ", ".join(["?" for _ in data])
        values = tuple(data.values())

        cursor.execute(
            f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})", values
        )
        self._conn.commit()

        return cursor.lastrowid

    def update_by_id(self, table_name, element_id, data):
        self._validate_table_name(table_name)

        # Handle empty data dictionary (all None values filtered out)
        if not data:
            return element_id

        self._validate_columns(table_name, data.keys())
        cursor = self._conn.cursor()

        # Build UPDATE statement
        set_clause = ", ".join([f"{k} = ?" for k in data.keys()])
        values = tuple(data.values()) + (element_id,)

        cursor.execute(f"UPDATE {table_name} SET {set_clause} WHERE eid = ?", values)
        self._conn.commit()

        return element_id

    def delete_by_id(self, table_name, element_id):
        self._validate_table_name(table_name)
        cursor = self._conn.cursor()
        cursor.execute(f"DELETE FROM {table_name} WHERE eid = ?", (element_id,))
        self._conn.commit()

        return element_id

    @staticmethod
    def create_query_condition(**filters):
        """Construct query condition with SQL optimization support.

        :return: where-clause string and parameter tuple
        """
        where_parts = []
        params = []

        for field, pattern in filters.items():
            if pattern is None and field in ["category", "end"]:
                # Filter for None values
                where_parts.append(f"{field} IS NULL")
            elif field == "value":
                # Exact match for value
                where_parts.append(f"{field} = ?")
                params.append(float(pattern))
            else:
                # Pattern matching for string fields using LIKE
                where_parts.append(f"LOWER({field}) LIKE ?")
                params.append(f"%{pattern.lower()}%")

        where_clause = " AND ".join(where_parts)
        return where_clause, tuple(params)

    def close(self):
        """Close the SQLite database connection."""
        self._conn.close()


class SqlitePocket(Pocket):
    def __init__(self, name=None, data_dir=None, **kwargs):
        """Create a pocket with an SQLite database backend, identified by 'name'.

        If 'data_dir' is given, the database is stored in a file with the
        .sqlite extension. Otherwise the data is stored in memory.

        Keyword args are passed to the sqlite3.connect constructor. See the
        respective docs for detailed information.
        """
        if data_dir is None:
            db_path = ":memory:"
        else:
            db_path = os.path.join(data_dir, f"{name}.sqlite")

        db_interface = SqliteInterface(db_path, **kwargs)
        super().__init__(db_interface, name=name)
