import os
from functools import cache

from langchain_anthropic import ChatAnthropic
from langchain_core.language_models import BaseChatModel, FakeListChatModel
from langchain_ollama import ChatOllama
from langchain_openai import ChatOpenAI
from loguru import logger

from ..llm import get_llm_from_registry, is_llm_registered
from ..utilities.search_utilities import remove_think_tags
from ..utilities.url_utils import normalize_url
from ..security import safe_get

# Import providers module to trigger auto-discovery
try:
    from ..llm.providers import discover_providers  # noqa: F401
    # Auto-discovery happens on module import
except ImportError:
    pass  # Providers module not available yet
from .thread_settings import (
    get_llm_setting_from_snapshot as get_setting_from_snapshot,
    NoSettingsContextError,
)

# Valid provider options
VALID_PROVIDERS = [
    "ollama",
    "openai",
    "anthropic",
    "google",
    "openrouter",
    "vllm",
    "openai_endpoint",
    "lmstudio",
    "llamacpp",
    "none",
]


def is_openai_available(settings_snapshot=None):
    """Check if OpenAI is available by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.openai import OpenAIProvider

        return OpenAIProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        return False


def is_anthropic_available(settings_snapshot=None):
    """Check if Anthropic is available by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.anthropic import AnthropicProvider

        return AnthropicProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        return False


def is_openai_endpoint_available(settings_snapshot=None):
    """Check if OpenAI endpoint is available by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.custom_openai_endpoint import (
            CustomOpenAIEndpointProvider,
        )

        return CustomOpenAIEndpointProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        return False


def is_ollama_available(settings_snapshot=None):
    """Check if Ollama is running by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.ollama import OllamaProvider

        return OllamaProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        return False


def is_vllm_available():
    """Check if VLLM capability is available"""
    try:
        import torch  # noqa: F401
        import transformers  # noqa: F401

        # Only try to import VLLM if the dependencies are available
        # The VLLM class itself might not fail to import, but using it will fail
        # without the proper dependencies
        import vllm  # noqa: F401
        from langchain_community.llms import VLLM  # noqa: F401

        return True
    except ImportError:
        return False


def is_lmstudio_available(settings_snapshot=None):
    """Check if LM Studio is available by delegating to the provider class."""
    try:
        from ..llm.providers.implementations.lmstudio import LMStudioProvider

        return LMStudioProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        return False


def is_llamacpp_available(settings_snapshot=None):
    """Check if LlamaCpp is available and properly configured.

    Checks that the library is installed and a model path is configured.
    For llama.cpp server connections, use 'openai_endpoint' provider instead.
    """
    try:
        # Import check
        from langchain_community.llms import LlamaCpp  # noqa: F401

        # Check if model path is configured and looks valid
        # Note: For llama.cpp server connections, use 'openai_endpoint' provider instead
        model_path_str = get_setting_from_snapshot(
            "llm.llamacpp_model_path",
            default=None,
            settings_snapshot=settings_snapshot,
        )

        # If no path configured, LlamaCpp is not available
        if not model_path_str:
            return False

        # Path is configured, actual validation happens when model loads
        return True

    except ImportError:
        # LlamaCpp library not installed
        return False

    except Exception:
        return False


def is_google_available(settings_snapshot=None):
    """Check if Google/Gemini is available"""
    try:
        from ..llm.providers.google import GoogleProvider

        return GoogleProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        return False


def is_openrouter_available(settings_snapshot=None):
    """Check if OpenRouter is available"""
    try:
        from ..llm.providers.openrouter import OpenRouterProvider

        return OpenRouterProvider.is_available(settings_snapshot)
    except ImportError:
        return False
    except Exception:
        return False


@cache
def get_available_providers(settings_snapshot=None):
    """Return available model providers"""
    providers = {}

    if is_ollama_available(settings_snapshot):
        providers["ollama"] = "Ollama (local models)"

    if is_openai_available(settings_snapshot):
        providers["openai"] = "OpenAI API"

    if is_anthropic_available(settings_snapshot):
        providers["anthropic"] = "Anthropic API"

    if is_google_available(settings_snapshot):
        providers["google"] = "Google Gemini API"

    if is_openrouter_available(settings_snapshot):
        providers["openrouter"] = "OpenRouter API"

    if is_openai_endpoint_available(settings_snapshot):
        providers["openai_endpoint"] = "OpenAI-compatible Endpoint"

    if is_lmstudio_available(settings_snapshot):
        providers["lmstudio"] = "LM Studio (local models)"

    if is_llamacpp_available(settings_snapshot):
        providers["llamacpp"] = "LlamaCpp (local models)"

    # Check for VLLM capability
    if is_vllm_available():
        providers["vllm"] = "VLLM (local models)"

    # Default fallback
    if not providers:
        providers["none"] = "No model providers available"

    return providers


def get_selected_llm_provider(settings_snapshot=None):
    return get_setting_from_snapshot(
        "llm.provider", "ollama", settings_snapshot=settings_snapshot
    ).lower()


def _get_context_window_for_provider(provider_type, settings_snapshot=None):
    """Get context window size from settings based on provider type.

    Local providers (ollama, llamacpp, lmstudio) use a smaller default to prevent
    memory issues. Cloud providers check if unrestricted mode is enabled.

    Returns:
        int or None: The context window size, or None for unrestricted cloud providers.
    """
    if provider_type in ["ollama", "llamacpp", "lmstudio"]:
        # Local providers: use smaller default to prevent memory issues
        window_size = get_setting_from_snapshot(
            "llm.local_context_window_size",
            4096,
            settings_snapshot=settings_snapshot,
        )
        # Ensure it's an integer
        return int(window_size) if window_size is not None else 4096
    else:
        # Cloud providers: check if unrestricted mode is enabled
        use_unrestricted = get_setting_from_snapshot(
            "llm.context_window_unrestricted",
            True,
            settings_snapshot=settings_snapshot,
        )
        if use_unrestricted:
            # Let cloud providers auto-handle context (return None or very large value)
            return None  # Will be handled per provider
        else:
            # Use user-specified limit
            window_size = get_setting_from_snapshot(
                "llm.context_window_size",
                128000,
                settings_snapshot=settings_snapshot,
            )
            return int(window_size) if window_size is not None else 128000


def get_llm(
    model_name=None,
    temperature=None,
    provider=None,
    openai_endpoint_url=None,
    research_id=None,
    research_context=None,
    settings_snapshot=None,
):
    """
    Get LLM instance based on model name and provider.

    Args:
        model_name: Name of the model to use (if None, uses database setting)
        temperature: Model temperature (if None, uses database setting)
        provider: Provider to use (if None, uses database setting)
        openai_endpoint_url: Custom endpoint URL to use (if None, uses database
            setting)
        research_id: Optional research ID for token tracking
        research_context: Optional research context for enhanced token tracking

    Returns:
        A LangChain LLM instance with automatic think-tag removal
    """

    # Use database values for parameters if not provided
    if model_name is None:
        model_name = get_setting_from_snapshot(
            "llm.model", "gemma:latest", settings_snapshot=settings_snapshot
        )
    if temperature is None:
        temperature = get_setting_from_snapshot(
            "llm.temperature", 0.7, settings_snapshot=settings_snapshot
        )
    if provider is None:
        provider = get_setting_from_snapshot(
            "llm.provider", "ollama", settings_snapshot=settings_snapshot
        )

    # Clean model name: remove quotes and extra whitespace
    if model_name:
        model_name = model_name.strip().strip("\"'").strip()

    # Clean provider: remove quotes and extra whitespace
    if provider:
        provider = provider.strip().strip("\"'").strip()

    # Normalize provider: convert to lowercase
    provider = provider.lower() if provider else None

    # Check if this is a registered custom LLM first
    if provider and is_llm_registered(provider):
        logger.info(f"Using registered custom LLM: {provider}")
        custom_llm = get_llm_from_registry(provider)

        # Check if it's a callable (factory function) or a BaseChatModel instance
        if callable(custom_llm) and not isinstance(custom_llm, BaseChatModel):
            # It's a callable (factory function), call it with parameters
            try:
                llm_instance = custom_llm(
                    model_name=model_name,
                    temperature=temperature,
                    settings_snapshot=settings_snapshot,
                )
            except TypeError as e:
                # Re-raise TypeError with better message
                raise TypeError(
                    f"Registered LLM factory '{provider}' has invalid signature. "
                    f"Factory functions must accept 'model_name', 'temperature', and 'settings_snapshot' parameters. "
                    f"Error: {e}"
                )

            # Validate the result is a BaseChatModel
            if not isinstance(llm_instance, BaseChatModel):
                raise ValueError(
                    f"Factory function for {provider} must return a BaseChatModel instance, "
                    f"got {type(llm_instance).__name__}"
                )
        elif isinstance(custom_llm, BaseChatModel):
            # It's already a proper LLM instance, use it directly
            llm_instance = custom_llm
        else:
            raise ValueError(
                f"Registered LLM {provider} must be either a BaseChatModel instance "
                f"or a callable factory function. Got: {type(custom_llm).__name__}"
            )

        return wrap_llm_without_think_tags(
            llm_instance,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    # Check if we're in testing mode and should use fallback (but only when no API keys are configured)
    # Skip fallback check if we're in test mode with mocks
    if os.environ.get("LDR_USE_FALLBACK_LLM", "") and not os.environ.get(
        "LDR_TESTING_WITH_MOCKS", ""
    ):
        # Only use fallback if the provider has no valid configuration
        provider_has_config = False

        if (
            (
                provider == "openai"
                and get_setting_from_snapshot(
                    "llm.openai.api_key",
                    default=None,
                    settings_snapshot=settings_snapshot,
                )
            )
            or (
                provider == "anthropic"
                and get_setting_from_snapshot(
                    "llm.anthropic.api_key",
                    default=None,
                    settings_snapshot=settings_snapshot,
                )
            )
            or (
                provider == "openai_endpoint"
                and get_setting_from_snapshot(
                    "llm.openai_endpoint.api_key",
                    settings_snapshot=settings_snapshot,
                )
            )
            or (
                provider == "ollama"
                and is_ollama_available(settings_snapshot=settings_snapshot)
            )
        ):
            provider_has_config = True
        elif provider in ["vllm", "lmstudio", "llamacpp"]:
            # These are local providers, check their availability
            if (
                (provider == "vllm" and is_vllm_available())
                or (
                    provider == "lmstudio"
                    and is_lmstudio_available(
                        settings_snapshot=settings_snapshot
                    )
                )
                or (
                    provider == "llamacpp"
                    and is_llamacpp_available(
                        settings_snapshot=settings_snapshot
                    )
                )
            ):
                provider_has_config = True

        if not provider_has_config:
            logger.info(
                "LDR_USE_FALLBACK_LLM is set and no valid provider config found, using fallback model"
            )
            return wrap_llm_without_think_tags(
                get_fallback_model(temperature),
                research_id=research_id,
                provider="fallback",
                research_context=research_context,
                settings_snapshot=settings_snapshot,
            )

    # Validate provider
    if provider not in VALID_PROVIDERS:
        logger.error(f"Invalid provider in settings: {provider}")
        raise ValueError(
            f"Invalid provider: {provider}. Must be one of: {VALID_PROVIDERS}"
        )
    logger.info(
        f"Getting LLM with model: {model_name}, temperature: {temperature}, provider: {provider}"
    )

    # Common parameters for all models
    common_params = {
        "temperature": temperature,
    }

    context_window_size = _get_context_window_for_provider(
        provider, settings_snapshot
    )

    # Add context limit to research context for overflow detection
    if research_context and context_window_size:
        research_context["context_limit"] = context_window_size
        logger.info(
            f"Set context_limit={context_window_size} in research_context"
        )
    else:
        logger.debug(
            f"Context limit not set: research_context={bool(research_context)}, context_window_size={context_window_size}"
        )

    if get_setting_from_snapshot(
        "llm.supports_max_tokens", True, settings_snapshot=settings_snapshot
    ):
        # Use 80% of context window to leave room for prompts
        if context_window_size is not None:
            max_tokens = min(
                int(
                    get_setting_from_snapshot(
                        "llm.max_tokens",
                        100000,
                        settings_snapshot=settings_snapshot,
                    )
                ),
                int(context_window_size * 0.8),
            )
            common_params["max_tokens"] = max_tokens
        else:
            # Unrestricted context: use provider's default max_tokens
            max_tokens = int(
                get_setting_from_snapshot(
                    "llm.max_tokens",
                    100000,
                    settings_snapshot=settings_snapshot,
                )
            )
            common_params["max_tokens"] = max_tokens

    # Handle different providers
    if provider == "anthropic":
        api_key = get_setting_from_snapshot(
            "llm.anthropic.api_key", settings_snapshot=settings_snapshot
        )

        if not api_key:
            logger.warning(
                "Anthropic API key not found in settings. Falling back to default model."
            )
            return get_fallback_model(temperature)

        llm = ChatAnthropic(
            model=model_name, anthropic_api_key=api_key, **common_params
        )
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    elif provider == "openai":
        api_key = get_setting_from_snapshot(
            "llm.openai.api_key", settings_snapshot=settings_snapshot
        )

        if not api_key:
            logger.warning(
                "OpenAI API key not found in settings. Falling back to default model."
            )
            return get_fallback_model(temperature)

        # Build OpenAI-specific parameters
        openai_params = {
            "model": model_name,
            "api_key": api_key,
            **common_params,
        }

        # Add optional parameters if they exist in settings
        try:
            api_base = get_setting_from_snapshot(
                "llm.openai.api_base",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if api_base:
                openai_params["openai_api_base"] = api_base
        except NoSettingsContextError:
            pass  # Optional parameter

        try:
            organization = get_setting_from_snapshot(
                "llm.openai.organization",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if organization:
                openai_params["openai_organization"] = organization
        except NoSettingsContextError:
            pass  # Optional parameter

        try:
            streaming = get_setting_from_snapshot(
                "llm.streaming",
                default=None,
                settings_snapshot=settings_snapshot,
            )
        except NoSettingsContextError:
            streaming = None  # Optional parameter
        if streaming is not None:
            openai_params["streaming"] = streaming

        try:
            max_retries = get_setting_from_snapshot(
                "llm.max_retries",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if max_retries is not None:
                openai_params["max_retries"] = max_retries
        except NoSettingsContextError:
            pass  # Optional parameter

        try:
            request_timeout = get_setting_from_snapshot(
                "llm.request_timeout",
                default=None,
                settings_snapshot=settings_snapshot,
            )
            if request_timeout is not None:
                openai_params["request_timeout"] = request_timeout
        except NoSettingsContextError:
            pass  # Optional parameter

        llm = ChatOpenAI(**openai_params)
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    elif provider == "openai_endpoint":
        api_key = get_setting_from_snapshot(
            "llm.openai_endpoint.api_key", settings_snapshot=settings_snapshot
        )

        if not api_key:
            logger.warning(
                "OpenAI endpoint API key not found in settings. Falling back to default model."
            )
            return get_fallback_model(temperature)

        # Get endpoint URL from settings
        if openai_endpoint_url is None:
            openai_endpoint_url = get_setting_from_snapshot(
                "llm.openai_endpoint.url",
                "https://openrouter.ai/api/v1",
                settings_snapshot=settings_snapshot,
            )
        openai_endpoint_url = normalize_url(openai_endpoint_url)

        llm = ChatOpenAI(
            model=model_name,
            api_key=api_key,
            openai_api_base=openai_endpoint_url,
            **common_params,
        )
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    elif provider == "vllm":
        if not is_vllm_available():
            logger.warning(
                "VLLM dependencies are not available. Falling back to default model."
            )
            return get_fallback_model(temperature)

        try:
            from langchain_community.llms import VLLM

            llm = VLLM(
                model=model_name,
                trust_remote_code=True,
                max_new_tokens=128,
                top_k=10,
                top_p=0.95,
                temperature=temperature,
            )
            return wrap_llm_without_think_tags(
                llm,
                research_id=research_id,
                provider=provider,
                research_context=research_context,
            )
        except Exception:
            logger.exception("Error loading VLLM model")
            return get_fallback_model(temperature)

    elif provider == "ollama":
        try:
            # Use the configurable Ollama base URL
            raw_base_url = get_setting_from_snapshot(
                "llm.ollama.url",
                "http://localhost:11434",
                settings_snapshot=settings_snapshot,
            )
            base_url = (
                normalize_url(raw_base_url)
                if raw_base_url
                else "http://localhost:11434"
            )

            # Check if Ollama is available before trying to use it
            if not is_ollama_available(settings_snapshot=settings_snapshot):
                logger.error(
                    f"Ollama not available at {base_url}. Falling back to dummy model."
                )
                return get_fallback_model(temperature)

            # Check if the requested model exists
            try:
                logger.info(
                    f"Checking if model '{model_name}' exists in Ollama"
                )
                response = safe_get(
                    f"{base_url}/api/tags",
                    timeout=3.0,
                    allow_localhost=True,
                    allow_private_ips=True,
                )
                if response.status_code == 200:
                    # Handle both newer and older Ollama API formats
                    data = response.json()
                    models = []
                    if "models" in data:
                        # Newer Ollama API
                        models = data.get("models", [])
                    else:
                        # Older Ollama API format
                        models = data

                    # Get list of model names
                    model_names = [m.get("name", "").lower() for m in models]
                    logger.info(
                        f"Available Ollama models: {', '.join(model_names[:5])}{' and more' if len(model_names) > 5 else ''}"
                    )

                    if model_name.lower() not in model_names:
                        logger.error(
                            f"Model '{model_name}' not found in Ollama. Available models: {', '.join(model_names[:5])}"
                        )
                        return get_fallback_model(temperature)
            except Exception:
                logger.exception(
                    f"Error checking for model '{model_name}' in Ollama"
                )
                # Continue anyway, let ChatOllama handle potential errors

            logger.info(
                f"Creating ChatOllama with model={model_name}, base_url={base_url}"
            )
            try:
                # Add num_ctx parameter for Ollama context window size
                ollama_params = {**common_params}
                if context_window_size is not None:
                    ollama_params["num_ctx"] = context_window_size

                # Thinking/reasoning handling for models like deepseek-r1:
                # The 'reasoning' parameter controls both:
                # 1. Whether the model performs thinking (makes it smarter when True)
                # 2. Whether thinking is separated from the answer (always separated when True)
                #
                # When reasoning=True:
                # - Model performs thinking/reasoning
                # - Thinking goes to additional_kwargs["reasoning_content"] (discarded by LDR)
                # - Only the final answer appears in response.content
                #
                # When reasoning=False:
                # - Model does NOT perform thinking (faster but less smart)
                # - Gives direct answers

                enable_thinking = get_setting_from_snapshot(
                    "llm.ollama.enable_thinking",
                    True,  # Default: enable thinking (smarter responses)
                    settings_snapshot=settings_snapshot,
                )

                if enable_thinking is not None and isinstance(
                    enable_thinking, bool
                ):
                    ollama_params["reasoning"] = enable_thinking
                    logger.debug(
                        f"Ollama thinking enabled: {enable_thinking} "
                        f"(thinking will be {'shown internally but discarded' if enable_thinking else 'disabled'})"
                    )

                llm = ChatOllama(
                    model=model_name, base_url=base_url, **ollama_params
                )

                # Log the actual client configuration after creation
                logger.debug(
                    f"ChatOllama created - base_url attribute: {getattr(llm, 'base_url', 'not found')}"
                )
                if hasattr(llm, "_client"):
                    client = llm._client
                    logger.debug(f"ChatOllama _client type: {type(client)}")
                    if hasattr(client, "_client"):
                        inner_client = client._client
                        logger.debug(
                            f"ChatOllama inner client type: {type(inner_client)}"
                        )
                        if hasattr(inner_client, "base_url"):
                            logger.debug(
                                f"ChatOllama inner client base_url: {inner_client.base_url}"
                            )

                return wrap_llm_without_think_tags(
                    llm,
                    research_id=research_id,
                    provider=provider,
                    research_context=research_context,
                    settings_snapshot=settings_snapshot,
                )
            except Exception:
                logger.exception("Error creating or testing ChatOllama")
                return get_fallback_model(temperature)
        except Exception:
            logger.exception("Error in Ollama provider section")
            return get_fallback_model(temperature)

    elif provider == "lmstudio":
        # LM Studio supports OpenAI API format, so we can use ChatOpenAI directly
        lmstudio_url = get_setting_from_snapshot(
            "llm.lmstudio.url",
            "http://localhost:1234/v1",
            settings_snapshot=settings_snapshot,
        )
        # Use URL as-is (default already includes /v1)
        base_url = normalize_url(lmstudio_url)

        llm = ChatOpenAI(
            model=model_name,
            api_key="lm-studio",  # LM Studio doesn't require a real API key  # pragma: allowlist secret
            base_url=base_url,
            temperature=temperature,
            max_tokens=max_tokens,  # Use calculated max_tokens based on context size
        )
        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    # Update the llamacpp section in get_llm function
    elif provider == "llamacpp":
        # Import LlamaCpp
        from langchain_community.llms import LlamaCpp

        # Note: For llama.cpp server connections, use 'openai_endpoint' provider
        # with the server's /v1 URL (e.g., 'http://localhost:8000/v1')

        # Get LlamaCpp model path from settings
        model_path = get_setting_from_snapshot(
            "llm.llamacpp_model_path", settings_snapshot=settings_snapshot
        )
        if not model_path:
            logger.error("llamacpp_model_path not set in settings")
            raise ValueError(
                "LlamaCpp model path not configured. Either:\n"
                "1. Set 'llm.llamacpp_model_path' to your .gguf file path, or\n"
                "2. For llama.cpp server connections, use 'openai_endpoint' provider "
                "with the server's /v1 endpoint (e.g., 'http://localhost:8000/v1')"
            )

        # Validate model path for security FIRST using centralized validator
        # This MUST happen before any filesystem operations on user input
        from ..security.path_validator import PathValidator
        from .paths import get_models_directory

        try:
            validated_path = PathValidator.validate_model_path(model_path)
        except ValueError as e:
            error_msg = str(e)
            # If the path is not a file, try to provide helpful directory listing
            # Only do this after path has passed security validation (safe_join check)
            if "not a file" in error_msg:
                try:
                    model_root = str(get_models_directory())
                    safe_path = PathValidator.validate_safe_path(
                        model_path, model_root, allow_absolute=False
                    )
                    if safe_path and safe_path.is_dir():
                        gguf_files = list(safe_path.glob("*.gguf"))
                        if gguf_files:
                            files_list = ", ".join(
                                f.name for f in gguf_files[:5]
                            )
                            if len(gguf_files) > 5:
                                files_list += (
                                    f" (and {len(gguf_files) - 5} more)"
                                )
                            suggestion = f"Found .gguf files: {files_list}"
                        else:
                            suggestion = (
                                "No .gguf files found in this directory"
                            )
                        raise ValueError(
                            f"Model path is a directory, not a file: {model_path}\n"
                            f"Please specify the full path to a .gguf model file.\n"
                            f"{suggestion}"
                        ) from e
                except ValueError:
                    pass  # Re-raise original error if secondary validation fails
            logger.exception("Model path validation failed")
            raise

        model_path = str(validated_path)

        # Validate file extension - LlamaCpp requires .gguf or .bin files
        # Safe to use validated_path here since it passed security validation
        if validated_path.suffix.lower() not in (".gguf", ".bin"):
            raise ValueError(
                f"Invalid model file extension: {validated_path.suffix}\n"
                f"LlamaCpp requires .gguf or .bin model files.\n"
                f"File: {validated_path.name}"
            )

        # Get additional LlamaCpp parameters
        n_gpu_layers = get_setting_from_snapshot(
            "llm.llamacpp_n_gpu_layers",
            1,
            settings_snapshot=settings_snapshot,
        )
        n_batch = get_setting_from_snapshot(
            "llm.llamacpp_n_batch", 512, settings_snapshot=settings_snapshot
        )
        f16_kv = get_setting_from_snapshot(
            "llm.llamacpp_f16_kv", True, settings_snapshot=settings_snapshot
        )

        # Create LlamaCpp instance
        llm = LlamaCpp(
            model_path=model_path,
            temperature=temperature,
            max_tokens=max_tokens,  # Use calculated max_tokens
            n_gpu_layers=n_gpu_layers,
            n_batch=n_batch,
            f16_kv=f16_kv,
            n_ctx=context_window_size,  # Set context window size directly (None = use default)
            verbose=True,
        )

        return wrap_llm_without_think_tags(
            llm,
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )

    else:
        return wrap_llm_without_think_tags(
            get_fallback_model(temperature),
            research_id=research_id,
            provider=provider,
            research_context=research_context,
            settings_snapshot=settings_snapshot,
        )


def get_fallback_model(temperature=None):
    """Create a dummy model for when no providers are available"""
    return FakeListChatModel(
        responses=[
            "No language models are available. Please install Ollama or set up API keys."
        ]
    )


def wrap_llm_without_think_tags(
    llm,
    research_id=None,
    provider=None,
    research_context=None,
    settings_snapshot=None,
):
    """Create a wrapper class that processes LLM outputs with remove_think_tags and token counting"""

    # First apply rate limiting if enabled
    from ..web_search_engines.rate_limiting.llm import (
        create_rate_limited_llm_wrapper,
    )

    # Check if LLM rate limiting is enabled (independent of search rate limiting)
    # Use the thread-safe get_db_setting defined in this module
    if get_setting_from_snapshot(
        "rate_limiting.llm_enabled", False, settings_snapshot=settings_snapshot
    ):
        llm = create_rate_limited_llm_wrapper(llm, provider)

    # Set context_limit in research_context for overflow detection.
    # This is needed for providers that go through the registered provider path
    # (which returns before the code in get_llm that sets context_limit).
    if research_context is not None and provider is not None:
        if "context_limit" not in research_context:
            context_limit = _get_context_window_for_provider(
                provider, settings_snapshot
            )
            if context_limit is not None:
                research_context["context_limit"] = context_limit
                logger.info(
                    f"Set context_limit={context_limit} in wrap_llm for provider={provider}"
                )

    # Import token counting functionality if research_id is provided
    callbacks = []
    if research_id is not None:
        from ..metrics import TokenCounter

        token_counter = TokenCounter()
        token_callback = token_counter.create_callback(
            research_id, research_context
        )
        # Set provider and model info on the callback
        if provider:
            token_callback.preset_provider = provider
        # Try to extract model name from the LLM instance
        if hasattr(llm, "model_name"):
            token_callback.preset_model = llm.model_name
        elif hasattr(llm, "model"):
            token_callback.preset_model = llm.model
        callbacks.append(token_callback)

    # Add callbacks to the LLM if it supports them
    if callbacks and hasattr(llm, "callbacks"):
        if llm.callbacks is None:
            llm.callbacks = callbacks
        else:
            llm.callbacks.extend(callbacks)

    class ProcessingLLMWrapper:
        def __init__(self, base_llm):
            self.base_llm = base_llm

        def invoke(self, *args, **kwargs):
            # Removed verbose debug logging to reduce log clutter
            # Uncomment the lines below if you need to debug LLM requests
            # if hasattr(self.base_llm, "base_url"):
            #     logger.debug(f"LLM Request - Base URL: {self.base_llm.base_url}")
            #     logger.debug(f"LLM Request - Model: {getattr(self.base_llm, 'model', 'unknown')}")

            try:
                response = self.base_llm.invoke(*args, **kwargs)
                # logger.debug(f"LLM Response - Success, type: {type(response)}")
            except Exception as e:
                logger.exception("LLM Request - Failed with error")
                # Log any URL information from the error
                error_str = str(e)
                if "http://" in error_str or "https://" in error_str:
                    logger.exception(
                        f"LLM Request - Error contains URL info: {error_str}"
                    )
                raise

            # Process the response content if it has a content attribute
            if hasattr(response, "content"):
                response.content = remove_think_tags(response.content)
            elif isinstance(response, str):
                response = remove_think_tags(response)

            return response

        # Pass through any other attributes to the base LLM
        def __getattr__(self, name):
            return getattr(self.base_llm, name)

    return ProcessingLLMWrapper(llm)
