"""
Encrypted database management using SQLCipher.
Handles per-user encrypted databases with browser-friendly authentication.
"""

import os
from pathlib import Path
from typing import Any, Dict, Optional

from loguru import logger
from sqlalchemy import create_engine, event, text
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import QueuePool, NullPool, StaticPool

from ..config.paths import get_data_directory, get_user_database_filename
from .sqlcipher_compat import get_sqlcipher_module
from .sqlcipher_utils import (
    set_sqlcipher_key,
    set_sqlcipher_rekey,
    apply_sqlcipher_pragmas,
    apply_performance_pragmas,
    verify_sqlcipher_connection,
)


class DatabaseManager:
    """Manages encrypted SQLCipher databases for each user."""

    def __init__(self):
        self.connections: Dict[str, Engine] = {}
        self.data_dir = get_data_directory() / "encrypted_databases"
        self.data_dir.mkdir(parents=True, exist_ok=True)

        # Check SQLCipher availability
        self.has_encryption = self._check_encryption_available()

        # Determine pool class based on environment
        # Use StaticPool for testing to avoid locking issues
        self._use_static_pool = bool(os.environ.get("TESTING"))
        self._pool_class = StaticPool if self._use_static_pool else QueuePool

    def _get_pool_kwargs(self) -> Dict[str, Any]:
        """Get pool configuration kwargs based on pool type.

        StaticPool doesn't support pool_size or max_overflow,
        so we only include them for QueuePool.
        """
        if self._use_static_pool:
            return {}
        return {
            "pool_size": 10,
            "max_overflow": 30,  # Increase overflow so we don't run out of connections
        }

    def _is_valid_encryption_key(self, password: str) -> bool:
        """
        Check if the provided password is valid (not None or empty).

        Args:
            password: The password to check

        Returns:
            True if the password is valid, False otherwise
        """
        return password is not None and password != ""

    def _check_encryption_available(self) -> bool:
        """Check if SQLCipher is available for encryption."""
        try:
            import os as os_module
            import tempfile

            # Test if SQLCipher actually works, not just if it imports
            with tempfile.NamedTemporaryFile(delete=False) as tmp:
                tmp_path = tmp.name

            try:
                # Try to create a test encrypted database

                # Use raw sqlcipher3 connection to test
                sqlcipher_module = get_sqlcipher_module()
                sqlcipher = sqlcipher_module.dbapi2

                conn = sqlcipher.connect(tmp_path)
                # Use centralized key setting
                set_sqlcipher_key(conn, "testpass")
                conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)")
                conn.execute("INSERT INTO test VALUES (1)")
                result = conn.execute("SELECT * FROM test").fetchone()
                conn.close()

                if result != (1,):
                    raise Exception("SQLCipher encryption test failed")
                logger.info(
                    "SQLCipher available and working - databases will be encrypted"
                )
                return True
            except Exception as e:
                logger.warning(f"SQLCipher module found but not working: {e}")
                raise ImportError("SQLCipher not functional")
            finally:
                # Clean up test file
                try:
                    os_module.unlink(tmp_path)
                except:
                    pass

        except ImportError:
            import os

            # Check if user has explicitly allowed unencrypted databases
            allow_unencrypted = (
                os.environ.get("LDR_ALLOW_UNENCRYPTED", "").lower() == "true"
            )

            if not allow_unencrypted:
                logger.exception(
                    "SECURITY ERROR: SQLCipher is not installed!\n"
                    "Your databases will NOT be encrypted.\n"
                    "To fix this:\n"
                    "1. Install SQLCipher: sudo apt install sqlcipher libsqlcipher-dev\n"
                    "2. Reinstall project: pdm install\n"
                    "Or use Docker with SQLCipher pre-installed.\n\n"
                    "To explicitly allow unencrypted databases (NOT RECOMMENDED):\n"
                    "export LDR_ALLOW_UNENCRYPTED=true"
                )
                raise RuntimeError(
                    "SQLCipher not available. Set LDR_ALLOW_UNENCRYPTED=true to proceed without encryption (NOT RECOMMENDED)"
                )
            else:
                logger.warning(
                    "⚠️  WARNING: Running with UNENCRYPTED databases!\n"
                    "This means:\n"
                    "- Passwords don't protect data access\n"
                    "- API keys are stored in plain text\n"
                    "- Anyone with file access can read all data\n"
                    "Install SQLCipher for secure operation!"
                )
                return False

    def _get_user_db_path(self, username: str) -> Path:
        """Get the path for a user's encrypted database."""
        return self.data_dir / get_user_database_filename(username)

    def _apply_pragmas(self, connection, connection_record):
        """Apply pragmas for optimal performance."""
        # Check if this is SQLCipher or regular SQLite
        is_encrypted = self.has_encryption

        # Use centralized performance pragma application
        from .sqlcipher_utils import apply_performance_pragmas

        apply_performance_pragmas(connection)

        # SQLCipher-specific pragmas
        if is_encrypted:
            from .sqlcipher_utils import get_sqlcipher_settings

            settings = get_sqlcipher_settings()
            pragmas = [
                f"PRAGMA kdf_iter = {settings['kdf_iterations']}",
                f"PRAGMA cipher_page_size = {settings['page_size']}",
            ]
            for pragma in pragmas:
                try:
                    connection.execute(pragma)
                except Exception:
                    pass
        else:
            # Regular SQLite pragma
            try:
                connection.execute(
                    "PRAGMA mmap_size = 268435456"
                )  # 256MB memory mapping
            except Exception:
                pass

    def create_user_database(self, username: str, password: str) -> Engine:
        """Create a new encrypted database for a user."""

        # Validate the encryption key
        if not self._is_valid_encryption_key(password):
            logger.error(
                f"Invalid encryption key for user {username}: password is None or empty"
            )
            raise ValueError(
                "Invalid encryption key: password cannot be None or empty"
            )

        db_path = self._get_user_db_path(username)

        if db_path.exists():
            raise ValueError(f"Database already exists for user {username}")

        # Create connection string - use regular SQLite when SQLCipher not available
        if self.has_encryption:
            # Create directory if it doesn't exist
            db_path.parent.mkdir(parents=True, exist_ok=True)

            # SOLUTION: Create database structure using raw SQLCipher outside SQLAlchemy
            # This bypasses the SQLAlchemy DDL execution that causes MemoryError in Flask
            try:
                sqlcipher3 = get_sqlcipher_module()

                # Create tables directly with SQLCipher, bypassing SQLAlchemy DDL
                # Use IMMEDIATE isolation level for proper write transaction handling
                conn = sqlcipher3.connect(
                    str(db_path),
                    isolation_level="IMMEDIATE",
                    check_same_thread=False,
                )
                # Use centralized SQLCipher setup
                set_sqlcipher_key(conn, password)
                apply_sqlcipher_pragmas(conn, creation_mode=True)

                # Get the CREATE TABLE statements from SQLAlchemy models
                from sqlalchemy.dialects import sqlite
                from sqlalchemy.schema import CreateTable

                from .models import Base

                # Create tables one by one
                sqlite_dialect = sqlite.dialect()
                for table in Base.metadata.sorted_tables:
                    if table.name != "users":
                        # Get the SQL for this table with SQLite dialect
                        create_sql = str(
                            CreateTable(table).compile(dialect=sqlite_dialect)
                        )
                        logger.debug(f"Creating table {table.name}")
                        conn.execute(create_sql)

                conn.commit()
                conn.close()

                logger.info(
                    f"Database structure created successfully for {username}"
                )

            except Exception:
                logger.exception("Error creating database structure")
                raise

            # Small delay to ensure file is fully written
            import time

            time.sleep(0.1)

            # Now create SQLAlchemy engine using custom connection creator
            # This ensures encryption is properly initialized for every connection
            sqlcipher3 = get_sqlcipher_module()

            def create_sqlcipher_connection():
                """Create a properly initialized SQLCipher connection."""
                conn = sqlcipher3.connect(
                    str(db_path),
                    isolation_level="IMMEDIATE",
                    check_same_thread=False,
                )
                cursor = conn.cursor()

                # Use centralized SQLCipher setup
                set_sqlcipher_key(cursor, password)

                # Verify connection works
                if not verify_sqlcipher_connection(cursor):
                    raise ValueError("Failed to verify database key")

                # Apply SQLCipher and performance settings
                apply_sqlcipher_pragmas(cursor, creation_mode=False)
                apply_performance_pragmas(cursor, username)

                cursor.close()
                return conn

            # Create engine with custom creator function and optimized cache
            engine = create_engine(
                "sqlite://",
                creator=create_sqlcipher_connection,
                poolclass=self._pool_class,
                echo=False,
                query_cache_size=1000,  # Increased for complex queries with SQLCipher
                **self._get_pool_kwargs(),
            )
        else:
            logger.warning(
                f"SQLCipher not available - creating UNENCRYPTED database for user {username}"
            )
            # Fall back to regular SQLite with query cache
            engine = create_engine(
                f"sqlite:///{db_path}",
                connect_args={"check_same_thread": False, "timeout": 30},
                poolclass=self._pool_class,
                echo=False,
                query_cache_size=1000,  # Same optimization for unencrypted
                **self._get_pool_kwargs(),
            )

            # For unencrypted databases, just apply pragmas
            event.listen(engine, "connect", self._apply_pragmas)

        # Tables have already been created using raw SQLCipher above
        # No need to create them again with SQLAlchemy

        # Store connection
        self.connections[username] = engine

        # Initialize database tables using centralized initialization
        from .initialize import initialize_database

        try:
            # Create a session for settings initialization
            Session = sessionmaker(bind=engine)
            with Session() as session:
                initialize_database(engine, session)
        except Exception as e:
            logger.warning(f"Could not initialize database fully: {e}")
            # Still continue - basic tables were created above

        logger.info(f"Created encrypted database for user {username}")
        return engine

    def open_user_database(
        self, username: str, password: str
    ) -> Optional[Engine]:
        """Open an existing encrypted database for a user."""

        # Validate the encryption key
        if not self._is_valid_encryption_key(password):
            logger.error(
                f"Invalid encryption key when opening database for user {username}: password is None or empty"
            )
            # TODO: Fix the root cause - research threads are not getting the correct password
            logger.error(
                "TODO: This usually means the research thread is not receiving the user's "
                "password for database encryption. Need to ensure password is passed from "
                "the main thread to research threads."
            )
            raise ValueError(
                "Invalid encryption key: password cannot be None or empty"
            )

        # Check if already open
        if username in self.connections:
            return self.connections[username]

        db_path = self._get_user_db_path(username)

        if not db_path.exists():
            logger.error(f"No database found for user {username}")
            return None

        # Create connection string - use regular SQLite when SQLCipher not available
        if self.has_encryption:
            # Use the same custom connection creator approach as create_user_database
            sqlcipher3 = get_sqlcipher_module()

            def create_sqlcipher_connection():
                """Create a properly initialized SQLCipher connection."""
                conn = sqlcipher3.connect(
                    str(db_path),
                    isolation_level="IMMEDIATE",
                    check_same_thread=False,
                )
                cursor = conn.cursor()

                # Use centralized SQLCipher setup
                set_sqlcipher_key(cursor, password)

                # Verify connection works
                if not verify_sqlcipher_connection(cursor):
                    raise ValueError("Failed to verify database key")

                # Apply SQLCipher and performance settings
                apply_sqlcipher_pragmas(cursor, creation_mode=False)
                apply_performance_pragmas(cursor, username)

                cursor.close()
                return conn

            # Create engine with custom creator function and optimized cache
            engine = create_engine(
                "sqlite://",
                creator=create_sqlcipher_connection,
                poolclass=self._pool_class,
                echo=False,
                query_cache_size=1000,  # Increased for complex queries with SQLCipher
                **self._get_pool_kwargs(),
            )
        else:
            logger.warning(
                f"SQLCipher not available - opening UNENCRYPTED database for user {username}"
            )
            # Fall back to regular SQLite (no password protection!)
            engine = create_engine(
                f"sqlite:///{db_path}",
                connect_args={"check_same_thread": False, "timeout": 30},
                poolclass=self._pool_class,
                echo=False,
                query_cache_size=1000,  # Same optimization for unencrypted
                **self._get_pool_kwargs(),
            )

            # For unencrypted databases, just apply pragmas
            event.listen(engine, "connect", self._apply_pragmas)

        try:
            # Test connection by running a simple query
            with engine.connect() as conn:
                conn.execute(text("SELECT 1"))

            # Store connection
            self.connections[username] = engine

            # Run database initialization (creates missing tables and runs migrations)
            from .initialize import initialize_database

            try:
                initialize_database(engine)
            except Exception as e:
                logger.warning(f"Could not run migrations for {username}: {e}")

            logger.info(f"Opened encrypted database for user {username}")
            return engine

        except Exception as e:
            logger.exception(
                f"Failed to open database for user {username}: {e}"
            )
            return None

    def get_session(self, username: str) -> Optional[Session]:
        """Create a new session for a user's database."""
        if username not in self.connections:
            # Use debug level for this common scenario to reduce log noise
            logger.debug(f"No open database for user {username}")
            return None

        # Always create a fresh session to avoid stale session issues
        engine = self.connections[username]
        SessionLocal = sessionmaker(bind=engine)
        return SessionLocal()

    def close_user_database(self, username: str):
        """Close a user's database connection."""
        if username in self.connections:
            self.connections[username].dispose()
            del self.connections[username]
            logger.info(f"Closed database for user {username}")

    def check_database_integrity(self, username: str) -> bool:
        """Check integrity of a user's encrypted database."""
        if username not in self.connections:
            return False

        try:
            with self.connections[username].connect() as conn:
                # Quick integrity check
                result = conn.execute(text("PRAGMA quick_check"))
                if result.fetchone()[0] != "ok":
                    return False

                # SQLCipher integrity check
                result = conn.execute(text("PRAGMA cipher_integrity_check"))
                # If this returns any rows, there are HMAC failures
                failures = list(result)
                if failures:
                    logger.error(
                        f"Integrity check failed for {username}: {len(failures)} HMAC failures"
                    )
                    return False

                return True

        except Exception:
            logger.exception(f"Integrity check error for user: {username}")
            return False

    def change_password(
        self, username: str, old_password: str, new_password: str
    ) -> bool:
        """Change the encryption password for a user's database."""
        if not self.has_encryption:
            logger.warning(
                "Cannot change password - SQLCipher not available (databases are unencrypted)"
            )
            return False

        db_path = self._get_user_db_path(username)

        if not db_path.exists():
            return False

        try:
            # Close existing connection if any
            self.close_user_database(username)

            # Open with old password
            engine = self.open_user_database(username, old_password)
            if not engine:
                return False

            # Rekey the database (only works with SQLCipher)
            with engine.connect() as conn:
                # Use centralized rekey function
                set_sqlcipher_rekey(conn, new_password)

            logger.info(f"Password changed for user {username}")
            return True

        except Exception:
            logger.exception(f"Failed to change password for user: {username}")
            return False
        finally:
            # Close the connection
            self.close_user_database(username)

    def user_exists(self, username: str) -> bool:
        """Check if a user exists in the auth database."""
        from .auth_db import get_auth_db_session
        from .models.auth import User

        auth_db = get_auth_db_session()
        user = auth_db.query(User).filter_by(username=username).first()
        auth_db.close()

        return user is not None

    def get_memory_usage(self) -> Dict[str, Any]:
        """Get memory usage statistics."""
        return {
            "active_connections": len(self.connections),
            "active_sessions": 0,  # Sessions are created on-demand, not tracked
            "estimated_memory_mb": len(self.connections)
            * 3.5,  # ~3.5MB per connection
        }

    def create_thread_safe_session_for_metrics(
        self, username: str, password: str
    ):
        """
        Create a new database session safe for use in background threads.
        This is specifically for metrics/logging - NOT for settings or user data.

        Args:
            username: The username
            password: The user's password (encryption key)

        Returns:
            A SQLAlchemy session that can be used in the current thread

        IMPORTANT: This should ONLY be used for:
        - Writing token metrics
        - Writing search metrics
        - Writing logs

        DO NOT use this for:
        - Reading/writing settings
        - Modifying user data
        - Any operation that should be synchronized with user requests
        """
        db_path = self._get_user_db_path(username)

        if not db_path.exists():
            raise ValueError(f"No database found for user {username}")

        # Create a thread-local engine
        if self.has_encryption:
            sqlcipher3 = get_sqlcipher_module()

            def create_thread_connection():
                """Create a SQLCipher connection for this thread."""
                try:
                    conn = sqlcipher3.connect(
                        str(db_path), check_same_thread=False
                    )
                    cursor = conn.cursor()

                    # Use centralized SQLCipher setup
                    set_sqlcipher_key(cursor, password)
                    apply_sqlcipher_pragmas(cursor, creation_mode=False)

                    # Verify connection works
                    if not verify_sqlcipher_connection(cursor):
                        raise ValueError("Failed to verify database key")
                except Exception as e:
                    logger.exception(
                        f"Failed to create thread connection for {username}: {e}"
                    )
                    raise

                # Apply performance pragmas for metrics writes
                apply_performance_pragmas(cursor, username)

                cursor.close()
                return conn

            engine = create_engine(
                "sqlite://",
                creator=create_thread_connection,
                poolclass=NullPool,  # Important: no connection pooling for threads
                echo=False,
            )
        else:
            # Unencrypted fallback
            logger.warning("Creating unencrypted thread session for metrics")
            engine = create_engine(
                f"sqlite:///{db_path}",
                poolclass=NullPool,
                echo=False,
            )

        # Create session
        Session = sessionmaker(bind=engine)
        return Session()


# Global instance
db_manager = DatabaseManager()
