"""Shared thread-local storage for settings context

This module provides a single thread-local storage instance that can be
shared across different modules to maintain settings context in threads.
"""

import threading
from contextlib import contextmanager

from ..settings.manager import get_typed_setting_value
from ..utilities.type_utils import to_bool


class NoSettingsContextError(Exception):
    """Raised when settings context is not available in a thread."""

    pass


# Shared thread-local storage for settings context
_thread_local = threading.local()

# Sentinel distinguishing "key absent from snapshot" from "key present with
# value None". Using None for both collapses legitimately-stored null values
# (e.g. embeddings.openai.dimensions, which defaults to JSON null) into the
# "not found" path, raising NoSettingsContextError in Flask request threads
# that have no thread-local context. See #4208.
_NOT_FOUND = object()


def set_settings_context(settings_context):
    """Set a settings context for the current thread."""
    _thread_local.settings_context = settings_context


def clear_settings_context():
    """Clear the settings context for the current thread.

    Should be called in a finally block after set_settings_context() to prevent
    context from leaking to subsequent tasks when threads are reused in a pool.
    """
    if hasattr(_thread_local, "settings_context"):
        del _thread_local.settings_context


def get_settings_context():
    """Get the settings context for the current thread."""
    if hasattr(_thread_local, "settings_context"):
        return _thread_local.settings_context
    return None


@contextmanager
def settings_context(ctx):
    """Context manager that sets and clears settings context automatically.

    Ensures cleanup even if an exception occurs, preventing context leaks
    when threads are reused in a pool.

    Example:
        with settings_context(my_settings):
            run_research()
    """
    set_settings_context(ctx)
    try:
        yield
    finally:
        clear_settings_context()


def get_setting_from_snapshot(
    key,
    default=None,
    username=None,
    settings_snapshot=None,
):
    """Get setting from context only - no database access from threads.

    Args:
        key: Setting key to retrieve
        default: Default value if setting not found
        username: Username (unused, kept for backward compatibility)
        settings_snapshot: Optional settings snapshot dict

    Returns:
        Setting value or default

    Raises:
        RuntimeError: If no settings context is available
    """
    # First check if we have settings_snapshot passed directly.
    # _NOT_FOUND (not None) is the absence sentinel so a key whose stored
    # value is None is still treated as found.  See #4208.
    value = _NOT_FOUND
    if settings_snapshot and key in settings_snapshot:
        raw = settings_snapshot[key]
        # Handle both full format {"value": x} and simplified format (just x)
        if isinstance(raw, dict) and "value" in raw:
            value = get_typed_setting_value(
                key,
                raw["value"],
                raw.get("ui_element", "text"),
            )
        else:
            value = raw
    # Search for child keys.
    elif settings_snapshot:
        for k, v in settings_snapshot.items():
            if k.startswith(f"{key}."):
                k = k.removeprefix(f"{key}.")
                # Handle both full format {"value": x} and simplified format (just x)
                if isinstance(v, dict) and "value" in v:
                    v = get_typed_setting_value(
                        k, v["value"], v.get("ui_element", "text")
                    )
                # else: v is already the raw value from simplified snapshot
                if value is _NOT_FOUND:
                    value = {k: v}
                else:
                    value[k] = v

    if value is not _NOT_FOUND:
        # Extract value from dict structure if needed
        return value

    # Check if we have a settings context in this thread
    if (
        hasattr(_thread_local, "settings_context")
        and _thread_local.settings_context
    ):
        value = _thread_local.settings_context.get_setting(key, default)
        # Extract value from dict structure if needed (same as above)
        if isinstance(value, dict) and "value" in value:
            return value["value"]
        return value

    # If a default was provided, return it instead of raising an exception
    if default is not None:
        from loguru import logger

        logger.debug(
            f"Setting '{key}' not found in snapshot or context, using default"
        )
        return default

    # Only raise the exception if no default was provided
    raise NoSettingsContextError(
        f"No settings context available in thread for key '{key}'. All settings must be passed via settings_snapshot."
    )


def get_bool_setting_from_snapshot(
    key,
    default=False,
    username=None,
    settings_snapshot=None,
):
    """Get a boolean setting from snapshot, handling string conversion.

    This centralizes the string-to-boolean conversion logic for settings
    retrieved from snapshots. Handles various truthy string representations
    that may come from API requests, config files, or SQLite.

    Args:
        key: Setting key to retrieve
        default: Default boolean value if setting not found
        username: Username (unused, kept for backward compatibility)
        settings_snapshot: Optional settings snapshot dict

    Returns:
        Boolean value of the setting
    """
    value = get_setting_from_snapshot(
        key,
        default,
        username=username,
        settings_snapshot=settings_snapshot,
    )

    return to_bool(value, default)


def _get_optional_setting(
    param_dict,
    param_name,
    setting_key,
    settings_snapshot,
    *,
    cast=None,
    check="not_none",
):
    """Fetch a setting and, when present, write it into ``param_dict``.

    Consolidates the recurring
    ``try: get_setting_from_snapshot(...) ... except NoSettingsContextError: pass``
    pattern used by the LLM provider ``create_llm`` factories.
    ``NoSettingsContextError`` is silently swallowed because these
    parameters are always optional.

    Args:
        param_dict: Target dict (e.g. ``llm_params``) mutated in place.
        param_name: Key to set on ``param_dict`` when a value is present.
        setting_key: Dotted settings path to retrieve.
        settings_snapshot: Snapshot dict passed through to
            :func:`get_setting_from_snapshot`.
        cast: Optional callable applied to the value before assignment
            (e.g. ``int`` for ``max_tokens``).
        check: ``"not_none"`` (default) writes the value when it is not
            ``None``. ``"falsy"`` writes only when the value is truthy.
            The falsy mode is an intentional safeguard for fields where
            a falsy stored value (``organization=""``) must be dropped
            rather than forwarded.

    The ``max_tokens`` blocks that resolve their value through
    ``compute_max_tokens`` / ``get_context_window_for_provider`` (rather
    than a single settings lookup) do not fit this helper and are left
    in place. The ``api_base`` block in ``openai.py`` is also left in
    place because it wraps an SSRF validation side effect.
    """
    try:
        value = get_setting_from_snapshot(
            setting_key,
            default=None,
            settings_snapshot=settings_snapshot,
        )
    except NoSettingsContextError:
        return
    if check == "not_none":
        if value is not None:
            param_dict[param_name] = cast(value) if cast else value
    elif check == "falsy":
        if value:
            param_dict[param_name] = cast(value) if cast else value
    else:
        raise ValueError(f"Unknown check mode: {check!r}")
