import logging
import os

from langchain_anthropic import ChatAnthropic
from langchain_community.llms import VLLM
from langchain_ollama import ChatOllama
from langchain_openai import ChatOpenAI

from ..utilities.db_utils import get_db_setting
from ..utilities.search_utilities import remove_think_tags

# Setup logging
logger = logging.getLogger(__name__)

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


def get_llm(model_name=None, temperature=None, provider=None, openai_endpoint_url=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)

    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

    # 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}"
        )
    print(
        f"Getting LLM with model: {model_name}, temperature: {temperature}, provider: {provider}"
    )

    # Common parameters for all models
    common_params = {
        "temperature": temperature,
    }
    if get_db_setting("llm.supports_max_tokens", True):
        common_params["max_tokens"] = get_db_setting("llm.max_tokens", 30000)

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

    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)

    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"
            )

        llm = ChatOpenAI(
            model=model_name,
            api_key=api_key,
            openai_api_base=openai_endpoint_url,
            **common_params,
        )
        return wrap_llm_without_think_tags(llm)

    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)
        except Exception as e:
            logger.error(f"Error loading VLLM model: {e}")
            logger.warning("Falling back.")
            return get_fallback_model(temperature)

    elif provider == "ollama":
        try:
            # Use the configurable Ollama base URL
            base_url = get_db_setting("llm.ollama.url", "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 as model_check_error:
                logger.error(
                    f"Error checking for model '{model_name}' in Ollama: {str(model_check_error)}"
                )
                # Continue anyway, let ChatOllama handle potential errors

            logger.info(
                f"Creating ChatOllama with model={model_name}, base_url={base_url}"
            )
            try:
                llm = ChatOllama(model=model_name, base_url=base_url, **common_params)
                # 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)
            except Exception as chat_error:
                logger.error(f"Error creating or testing ChatOllama: {str(chat_error)}")
                return get_fallback_model(temperature)
        except Exception as e:
            logger.error(f"Error in Ollama provider section: {str(e)}")
            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=get_db_setting("llm.max_tokens", 30000),
        )
        return wrap_llm_without_think_tags(llm)

    elif provider == "llamacpp":
        # Import LlamaCpp
        from langchain_community.llms import LlamaCpp

        # 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=get_db_setting("llm.max_tokens", 30000),
            n_gpu_layers=n_gpu_layers,
            n_batch=n_batch,
            f16_kv=f16_kv,
            verbose=True,
        )
        return wrap_llm_without_think_tags(llm)

    else:
        return wrap_llm_without_think_tags(get_fallback_model(temperature))


def get_fallback_model(temperature=None):
    """Create a dummy model for when no providers are available"""
    from langchain_community.llms.fake import FakeListLLM

    return FakeListLLM(
        responses=[
            "No language models are available. Please install Ollama or set up API keys."
        ]
    )


def wrap_llm_without_think_tags(llm):
    """Create a wrapper class that processes LLM outputs with remove_think_tags"""

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

        def invoke(self, *args, **kwargs):
            response = self.base_llm.invoke(*args, **kwargs)

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


def get_available_provider_types():
    """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 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

        base_url = get_db_setting("llm.ollama.url", "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 as e:
            logger.error(f"Unexpected error when checking Ollama: {str(e)}")
            return False
    except Exception as outer_e:
        logger.error(f"Error in is_ollama_available: {str(outer_e)}")
        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


def get_available_providers():
    """Get dictionary of available providers"""
    return get_available_provider_types()


AVAILABLE_PROVIDERS = get_available_providers()
selected_provider = get_db_setting("llm.provider", "ollama").lower()

# Log which providers are available
logger.info(f"Available providers: {list(AVAILABLE_PROVIDERS.keys())}")

# Check if selected provider is available
if selected_provider not in AVAILABLE_PROVIDERS and selected_provider != "none":
    logger.warning(f"Selected provider {selected_provider} is not available.")
