import functools
from typing import Any, Callable, Dict

from cachetools import LRUCache
from flask import g, has_app_context, session as flask_session
from loguru import logger
from sqlalchemy.orm import Session

from ..settings.env_registry import use_fallback_llm

from ..config.paths import get_data_directory
from ..database.encrypted_db import db_manager
from .threading_utils import thread_specific_cache

# Database paths using new centralized configuration
DATA_DIR = get_data_directory()
# DB_PATH removed - use per-user encrypted databases instead


@thread_specific_cache(cache=LRUCache(maxsize=10))
def get_db_session(
    _namespace: str = "", username: str | None = None
) -> Session:
    """
    Get database session - uses encrypted per-user database if authenticated.

    Args:
        _namespace: This can be specified to an arbitrary string in order to
                   force the caching mechanism to create separate settings even in
                   the same thread. Usually it does not need to be specified.
        username: Optional username for thread context (e.g., background research threads).
                 If not provided, will try to get from Flask context.

    Returns:
        The database session for the current user/context.
    """
    # CRITICAL: Detect if we're in a background thread and raise an error
    # This helps identify code that's trying to access the database from threads
    import threading

    # Check if we're in a background thread (not in Flask request context)
    # We check for request context specifically because app context might exist
    # during startup but we still shouldn't access the database from background threads
    thread_name = threading.current_thread().name

    # Allow MainThread during startup, but not other threads
    if not has_app_context() and thread_name != "MainThread":
        thread_id = threading.get_ident()
        raise RuntimeError(
            f"Database access attempted from background thread '{thread_name}' (ID: {thread_id}). "
            f"Database access from threads is not allowed due to SQLite thread safety constraints. "
            f"Use settings_snapshot or pass all required data to the thread at creation time."
        )

    # If username is explicitly provided (e.g., from background thread)
    if username:
        user_session = db_manager.get_session(username)
        if user_session:
            return user_session
        raise RuntimeError(f"No database found for user {username}")

    # Otherwise, check Flask request context
    try:
        # Check if we have a database session in Flask's g object
        if hasattr(g, "db_session") and g.db_session:
            return g.db_session

        # Check if we have a username in the Flask session
        username = flask_session.get("username")
        if username:
            user_session = db_manager.get_session(username)
            if user_session:
                return user_session
    except Exception:
        # Error accessing Flask context
        pass

    # No shared database - return None to allow SettingsManager to work without DB
    logger.warning(
        "get_db_session() is deprecated. Use get_user_db_session() from database.session_context"
    )
    return None


def get_settings_manager(
    db_session: Session | None = None, username: str | None = None
):
    """
    Get the settings manager for the current context.

    Args:
        db_session: Optional database session
        username: Optional username for caching (required for SettingsManager)

    Returns:
        The appropriate settings manager instance.
    """
    # If db_session not provided, try to get one
    if db_session is None and username is None and has_app_context():
        username = flask_session.get("username")

    if db_session is None:
        try:
            db_session = get_db_session(username=username)
        except RuntimeError:
            # No authenticated user - settings manager will use defaults
            db_session = None
            username = "anonymous"

    # Import here to avoid circular imports
    from ..settings import SettingsManager

    # Always use regular SettingsManager (now with built-in simple caching)
    return SettingsManager(db_session)


def no_db_settings(func: Callable[..., Any]) -> Callable[..., Any]:
    """
    Decorator that runs the wrapped function with the settings database
    completely disabled. This will prevent the function from accidentally
    reading settings from the DB. Settings can only be read from environment
    variables or the defaults file.

    Args:
        func: The function to wrap.

    Returns:
        The wrapped function.

    """

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Temporarily disable DB access in the settings manager.
        manager = get_settings_manager()
        db_session = manager.db_session
        manager.db_session = None

        try:
            return func(*args, **kwargs)
        finally:
            # Restore the original database session.
            manager.db_session = db_session

    return wrapper


def get_setting_from_db_main_thread(
    key: str, default_value: Any | None = None, username: str | None = None
) -> str | Dict[str, Any] | None:
    """
    Get a setting from the database with fallback to default value

    Args:
        key: The setting key.
        default_value: If the setting is not found, it will return this instead.
        username: Optional username for thread context (e.g., background research threads).

    Returns:
        The setting value.

    """
    # In fallback LLM mode, always return default values without database access
    if use_fallback_llm():
        logger.debug(
            f"Using default value for {key} in fallback LLM environment"
        )
        return default_value

    # CRITICAL: Detect if we're in a background thread and raise an error
    import threading

    # Check if we're in a background thread
    thread_name = threading.current_thread().name

    # Allow MainThread during startup, but not other threads
    if not has_app_context() and thread_name != "MainThread":
        thread_id = threading.get_ident()
        raise RuntimeError(
            f"get_db_setting('{key}') called from background thread '{thread_name}' (ID: {thread_id}). "
            f"Database access from threads is not allowed. Use settings_snapshot or thread-local settings context."
        )

    try:
        # Use the new session context to ensure proper database access
        from ..database.session_context import get_user_db_session

        try:
            with get_user_db_session(username) as db_session:
                if db_session:
                    # Use the unified settings manager
                    settings_manager = get_settings_manager(
                        db_session, username
                    )
                    return settings_manager.get_setting(
                        key, default=default_value
                    )
        except Exception:
            # If we can't get a session, fall back to default
            pass

    except Exception:
        logger.exception(f"Error getting setting {key} from database")

    logger.warning(f"Could not read setting '{key}' from the database.")
    return default_value
