import functools
from typing import Any, Callable, Dict

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

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:
        logger.debug(
            "Flask context unavailable (CLI/background threads)", exc_info=True
        )

    # 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.
    """
    # Track whether we are borrowing a session we don't own (caller-provided
    # or Flask g.db_session).  Borrowed sessions must NOT be closed by
    # SettingsManager — their owner is responsible for cleanup.
    borrowed_session = db_session is not None

    # Reuse the Flask request session if it belongs to the requested user.
    # This MUST happen before get_db_session() because that function is
    # wrapped in @thread_specific_cache — the cache bypasses the function
    # body on subsequent calls, so any g.db_session check inside it only
    # fires once per thread.
    if db_session is None:
        try:
            if (
                has_request_context()
                and hasattr(g, "db_session")
                and g.db_session
            ):
                g_user = getattr(g, "current_user", None)
                if not isinstance(g_user, str):
                    g_user = getattr(g_user, "username", None)
                if username is None or g_user == username:
                    db_session = g.db_session
                    borrowed_session = True
        except Exception:
            logger.debug("Could not reuse Flask request session", exc_info=True)

    if db_session is None and username is None and has_request_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"
        else:
            # get_db_session() may return g.db_session via its internal
            # check (line ~68).  If it did, we must mark it as borrowed
            # to prevent both SettingsManager.close() and Flask teardown
            # from closing the same session.
            if not borrowed_session and db_session is not None:
                try:
                    if (
                        has_request_context()
                        and hasattr(g, "db_session")
                        and db_session is g.db_session
                    ):
                        borrowed_session = True
                        logger.warning(
                            "get_db_session() returned g.db_session after "
                            "direct g.db_session check failed — marking as "
                            "borrowed to prevent double-close"
                        )
                except Exception:
                    logger.debug(
                        "Could not verify session identity after "
                        "get_db_session()",
                        exc_info=True,
                    )

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

    logger.debug(
        "get_settings_manager: session_source={}, owned={}",
        "borrowed" if borrowed_session else ("new" if db_session else "None"),
        not borrowed_session,
    )

    # Always use regular SettingsManager (now with built-in simple caching)
    return SettingsManager(db_session, owns_session=not borrowed_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.

    """
    # 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:
            logger.debug(
                "fall back to default if session unavailable", exc_info=True
            )

    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
