import os
from functools import cache

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

from ..utilities.db_utils import get_db_setting
from ..utilities.search_utilities import remove_think_tags
from ..utilities.url_utils import normalize_url
from ..llm import get_llm_from_registry, is_llm_registered

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


def is_openai_available():
    """Check if OpenAI is available"""
    try:
        api_key = get_db_setting("llm.openai.api_key")
        return bool(api_key)
    except Exception:
        return False


def is_anthropic_available():
    """Check if Anthropic is available"""
    try:
        api_key = get_db_setting("llm.anthropic.api_key")
        return bool(api_key)
    except Exception:
        return False


def is_openai_endpoint_available():
    """Check if OpenAI endpoint is available"""
    try:
        api_key = get_db_setting("llm.openai_endpoint.api_key")
        return bool(api_key)
    except Exception:
        return False


def is_ollama_available():
    """Check if Ollama is running"""
    try:
        import requests

        raw_base_url = get_db_setting(
            "llm.ollama.url", "http://localhost:11434"
        )
        base_url = (
            normalize_url(raw_base_url)
            if raw_base_url
            else "http://localhost:11434"
        )
        logger.info(f"Checking Ollama availability at {base_url}/api/tags")

        try:
            response = requests.get(f"{base_url}/api/tags", timeout=3.0)
            if response.status_code == 200:
                logger.info(
                    f"Ollama is available. Status code: {response.status_code}"
                )
                # Log first 100 chars of response to debug
                logger.info(f"Response preview: {str(response.text)[:100]}")
                return True
            else:
                logger.warning(
                    f"Ollama API returned status code: {response.status_code}"
                )
                return False
        except requests.exceptions.RequestException as req_error:
            logger.error(
                f"Request error when checking Ollama: {str(req_error)}"
            )
            return False
        except Exception:
            logger.exception("Unexpected error when checking Ollama")
            return False
    except Exception:
        logger.exception("Error in is_ollama_available")
        return False


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

        return True
    except ImportError:
        return False


def is_lmstudio_available():
    """Check if LM Studio is available"""
    try:
        import requests

        lmstudio_url = get_db_setting(
            "llm.lmstudio.url", "http://localhost:1234"
        )
        # LM Studio typically uses OpenAI-compatible endpoints
        response = requests.get(f"{lmstudio_url}/v1/models", timeout=1.0)
        return response.status_code == 200
    except Exception:
        return False


def is_llamacpp_available():
    """Check if LlamaCpp is available and configured"""
    try:
        from langchain_community.llms import LlamaCpp  # noqa: F401

        model_path = get_db_setting("llm.llamacpp_model_path")
        return bool(model_path) and os.path.exists(model_path)
    except Exception:
        return False


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

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

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

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

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

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

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

    # Check for VLLM capability
    try:
        import torch  # noqa: F401
        import transformers  # noqa: F401

        providers["vllm"] = "VLLM (local models)"
    except ImportError:
        pass

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

    return providers


def get_selected_llm_provider():
    return get_db_setting("llm.provider", "ollama").lower()


def get_llm(
    model_name=None,
    temperature=None,
    provider=None,
    openai_endpoint_url=None,
    research_id=None,
    research_context=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_db_setting("llm.model", "gemma:latest")
    if temperature is None:
        temperature = get_db_setting("llm.temperature", 0.7)
    if provider is None:
        provider = get_db_setting("llm.provider", "ollama")

    # 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 already a BaseChatModel instance
        if isinstance(custom_llm, BaseChatModel):
            # It's already an LLM instance, use it directly
            llm_instance = custom_llm
        elif callable(custom_llm):
            # It's a factory function, call it with parameters
            try:
                llm_instance = custom_llm(
                    model_name=model_name,
                    temperature=temperature,
                )
            except Exception as e:
                logger.exception(f"Error creating custom LLM instance: {e}")
                raise
        else:
            raise ValueError(
                f"Registered LLM {provider} is neither a BaseChatModel nor a callable factory"
            )

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

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

        if provider == "openai" and get_db_setting("llm.openai.api_key"):
            provider_has_config = True
        elif provider == "anthropic" and get_db_setting(
            "llm.anthropic.api_key"
        ):
            provider_has_config = True
        elif provider == "openai_endpoint" and get_db_setting(
            "llm.openai_endpoint.api_key"
        ):
            provider_has_config = True
        elif provider == "ollama" and is_ollama_available():
            provider_has_config = True
        elif provider in ["vllm", "lmstudio", "llamacpp"]:
            # These are local providers, check their availability
            if provider == "vllm" and is_vllm_available():
                provider_has_config = True
            elif provider == "lmstudio" and is_lmstudio_available():
                provider_has_config = True
            elif provider == "llamacpp" and is_llamacpp_available():
                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,
            )

    # 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,
    }

    # Get context window size from settings (use different defaults for local vs cloud providers)
    def get_context_window_size(provider_type):
        if provider_type in ["ollama", "llamacpp", "lmstudio"]:
            # Local providers: use smaller default to prevent memory issues
            return get_db_setting("llm.local_context_window_size", 4096)
        else:
            # Cloud providers: check if unrestricted mode is enabled
            use_unrestricted = get_db_setting(
                "llm.context_window_unrestricted", True
            )
            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
                return get_db_setting("llm.context_window_size", 128000)

    context_window_size = get_context_window_size(provider)

    if get_db_setting("llm.supports_max_tokens", True):
        # Use 80% of context window to leave room for prompts
        if context_window_size is not None:
            max_tokens = min(
                int(get_db_setting("llm.max_tokens", 100000)),
                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_db_setting("llm.max_tokens", 100000))
            common_params["max_tokens"] = max_tokens

    # Handle different providers
    if provider == "anthropic":
        api_key = get_db_setting("llm.anthropic.api_key")
        if not api_key:
            logger.warning(
                "ANTHROPIC_API_KEY not found. 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,
        )

    elif provider == "openai":
        api_key = get_db_setting("llm.openai.api_key")
        if not api_key:
            logger.warning(
                "OPENAI_API_KEY not found. Falling back to default model."
            )
            return get_fallback_model(temperature)

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

    elif provider == "openai_endpoint":
        api_key = get_db_setting("llm.openai_endpoint.api_key")
        if not api_key:
            logger.warning(
                "OPENAI_ENDPOINT_API_KEY not found. 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_db_setting(
                "llm.openai_endpoint.url", "https://openrouter.ai/api/v1"
            )
        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,
        )

    elif provider == "vllm":
        try:
            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_db_setting(
                "llm.ollama.url", "http://localhost:11434"
            )
            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():
                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
            import requests

            try:
                logger.info(
                    f"Checking if model '{model_name}' exists in Ollama"
                )
                response = requests.get(f"{base_url}/api/tags", timeout=3.0)
                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
                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}"
                            )

                # Test invoke to validate model works
                logger.info("Testing Ollama model with simple invocation")
                test_result = llm.invoke("Hello")
                logger.info(
                    f"Ollama test successful. Response type: {type(test_result)}"
                )
                return wrap_llm_without_think_tags(
                    llm,
                    research_id=research_id,
                    provider=provider,
                    research_context=research_context,
                )
            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_db_setting(
            "llm.lmstudio.url", "http://localhost:1234"
        )

        llm = ChatOpenAI(
            model=model_name,
            api_key="lm-studio",  # LM Studio doesn't require a real API key
            base_url=f"{lmstudio_url}/v1",  # Use the configured URL with /v1 endpoint
            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,
        )

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

        # Get LlamaCpp connection mode from settings
        connection_mode = get_db_setting(
            "llm.llamacpp_connection_mode", "local"
        )

        if connection_mode == "http":
            # Use HTTP client mode
            from langchain_community.llms.llamacpp_client import LlamaCppClient

            server_url = get_db_setting(
                "llm.llamacpp_server_url", "http://localhost:8000"
            )

            llm = LlamaCppClient(
                server_url=server_url,
                temperature=temperature,
                max_tokens=get_db_setting("llm.max_tokens", 8192),
            )
        else:
            # Use direct model loading (existing code)
            # Get LlamaCpp model path from settings
            model_path = get_db_setting("llm.llamacpp_model_path")
            if not model_path:
                logger.error("llamacpp_model_path not set in settings")
                raise ValueError("llamacpp_model_path not set in settings")

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

            # 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,
        )

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


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
):
    """Create a wrapper class that processes LLM outputs with remove_think_tags and token counting"""

    # 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):
            # Log detailed request information for Ollama models
            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')}"
                )
                logger.debug(
                    f"LLM Request - Args count: {len(args)}, Kwargs: {list(kwargs.keys())}"
                )

                # Log the prompt if it's in args
                if args and len(args) > 0:
                    prompt_text = (
                        str(args[0])[:200] + "..."
                        if len(str(args[0])) > 200
                        else str(args[0])
                    )
                    logger.debug(f"LLM Request - Prompt preview: {prompt_text}")

                # Check if there's any client configuration
                if hasattr(self.base_llm, "_client"):
                    client = self.base_llm._client
                    if hasattr(client, "_client") and hasattr(
                        client._client, "base_url"
                    ):
                        logger.debug(
                            f"LLM Request - Client base URL: {client._client.base_url}"
                        )

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