"""Client factory functions and failover logic for LLM integration.

Provides ``get_provider``, ``get_client``, ``get_client_auto``,
``get_client_with_failover``, ``FailoverLLMClient``, and module-level
convenience functions (``configure``, ``analyze``, ``explain``).
"""

from __future__ import annotations

import os
from collections.abc import Callable
from typing import Any

from oscura.api.integrations.llm.integration import LLMIntegration
from oscura.api.integrations.llm.types import (
    LLMClient,
    LLMConfig,
    LLMError,
    LLMProvider,
    LLMResponse,
)


def get_provider(name: str, **config_kwargs: Any) -> LLMClient:
    """Get LLM provider by name with unified interface.

    Args:
        name: Provider name ('openai', 'anthropic', 'local')
        **config_kwargs: Configuration parameters for the provider

    Returns:
        LLM client instance

    Raises:
        LLMError: If provider unknown or configuration invalid

    Examples:
        >>> client = get_provider('openai', model='gpt-4', api_key='...')
        >>> response = client.analyze(trace, "What is the frequency?")
        >>>
        >>> client = get_provider('local')
        >>> response = client.analyze(trace, "Analyze this signal")
    """
    try:
        provider_enum = LLMProvider(name.lower())
    except ValueError:
        raise LLMError(f"Unknown provider: {name}. Available: {[p.value for p in LLMProvider]}")

    config = LLMConfig(
        provider=provider_enum,
        model=config_kwargs.get("model", "default"),
        api_key=config_kwargs.get("api_key"),
        base_url=config_kwargs.get("base_url"),
        privacy_mode=config_kwargs.get("privacy_mode", provider_enum == LLMProvider.LOCAL),
        timeout=config_kwargs.get("timeout", 30.0),
        max_retries=config_kwargs.get("max_retries", 3),
        requests_per_minute=config_kwargs.get("requests_per_minute", 60),
    )

    try:
        from oscura.api.integrations.llm.anthropic_provider import AnthropicClient
        from oscura.api.integrations.llm.local_provider import LocalLLMClient
        from oscura.api.integrations.llm.openai_provider import OpenAIClient

        if provider_enum == LLMProvider.OPENAI:
            return OpenAIClient(config)
        elif provider_enum == LLMProvider.ANTHROPIC:
            return AnthropicClient(config)
        elif provider_enum == LLMProvider.LOCAL:
            return LocalLLMClient(config)
        else:
            raise LLMError(
                f"Provider {name} not yet implemented. "
                "Falling back to local provider is recommended."
            )
    except ImportError as e:
        raise LLMError(
            f"Provider {name} unavailable: {e}. "
            "Install the required package or use 'local' provider."
        )


# Global LLM integration instance
_global_llm: LLMIntegration | None = None


def get_llm() -> LLMIntegration:
    """Get global LLM integration instance.

    Returns:
        Global LLM integration instance
    """
    global _global_llm
    if _global_llm is None:
        _global_llm = LLMIntegration()
    return _global_llm


def configure(provider: str, model: str, **kwargs: Any) -> None:
    """Configure global LLM integration.

    Args:
        provider: Provider name
        model: Model identifier
        **kwargs: Additional configuration
    """
    llm = get_llm()
    llm.configure(provider, model, **kwargs)


def analyze(trace: Any, question: str) -> LLMResponse:
    """Analyze trace with LLM.

    Args:
        trace: Trace object
        question: Natural language question

    Returns:
        LLM response
    """
    llm = get_llm()
    return llm.analyze(trace, question)


def explain(measurement: Any) -> str:
    """Explain measurement with LLM.

    Args:
        measurement: Measurement result

    Returns:
        Explanation text
    """
    llm = get_llm()
    return llm.explain(measurement)


# ==============================================================================
# Client factory functions (API-020)
# ==============================================================================


def get_client(provider: str | None = None, **config_kwargs: Any) -> LLMClient:
    """Get LLM client with optional auto-selection.

    Alias for get_provider() with auto-selection support.

    Args:
        provider: Provider name ('openai', 'anthropic', 'local'), or None for auto-select
        **config_kwargs: Configuration parameters for the provider

    Returns:
        LLM client instance

    Examples:
        >>> client = get_client()  # Auto-select based on available API keys
        >>> client = get_client("openai", model="gpt-4")
    """
    if provider is not None:
        return get_provider(provider, **config_kwargs)

    return get_client_auto(**config_kwargs)


def get_client_auto(**config_kwargs: Any) -> LLMClient:
    """Automatically select an available LLM provider.

    Checks for API keys in environment and returns first available provider:
    1. OpenAI (if OPENAI_API_KEY set)
    2. Anthropic (if ANTHROPIC_API_KEY set)
    3. Local (fallback, always available)

    Args:
        **config_kwargs: Configuration parameters for the provider

    Returns:
        LLM client instance for the first available provider

    Examples:
        >>> client = get_client_auto(model="gpt-4")
    """
    if os.environ.get("OPENAI_API_KEY"):
        try:
            return get_provider("openai", **config_kwargs)
        except LLMError:
            pass  # Fall through to next provider

    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            return get_provider("anthropic", **config_kwargs)
        except LLMError:
            pass  # Fall through to next provider

    return get_provider("local", **config_kwargs)


def get_client_with_failover(
    providers: list[str] | None = None, **config_kwargs: Any
) -> FailoverLLMClient:
    """Get LLM client with automatic failover between providers.

    Args:
        providers: List of provider names in preference order.
                   Default: ["openai", "anthropic", "local"]
        **config_kwargs: Configuration parameters for providers

    Returns:
        FailoverLLMClient that tries providers in order

    Examples:
        >>> client = get_client_with_failover(
        ...     providers=["openai", "anthropic"],
        ...     model="gpt-4"
        ... )
    """
    if providers is None:
        providers = ["openai", "anthropic", "local"]

    return FailoverLLMClient(providers, **config_kwargs)


class FailoverLLMClient:
    """LLM client wrapper with automatic failover between providers.

    Attempts each provider in order until one succeeds. Useful for
    handling API outages or rate limiting gracefully.
    """

    def __init__(self, providers: list[str], **config_kwargs: Any) -> None:
        """Initialize failover client.

        Args:
            providers: List of provider names in preference order
            **config_kwargs: Configuration parameters for providers
        """
        self.providers = providers
        self.config_kwargs = config_kwargs
        self._clients: dict[str, LLMClient] = {}
        self._last_successful_provider: str | None = None

    def _get_or_create_client(self, provider: str) -> LLMClient | None:
        """Get or create client for provider.

        Args:
            provider: Provider name

        Returns:
            LLM client or None if unavailable
        """
        if provider not in self._clients:
            try:
                self._clients[provider] = get_provider(provider, **self.config_kwargs)
            except LLMError:
                return None
        return self._clients.get(provider)

    def _try_providers(self, operation: Callable[[LLMClient], Any]) -> Any:
        """Try operation on each provider until one succeeds.

        Args:
            operation: Callable that takes a client and returns result

        Returns:
            Result from first successful provider

        Raises:
            LLMError: If all providers fail
        """
        errors: list[str] = []

        if self._last_successful_provider:
            reordered = [self._last_successful_provider] + [
                p for p in self.providers if p != self._last_successful_provider
            ]
        else:
            reordered = self.providers

        for provider in reordered:
            client = self._get_or_create_client(provider)
            if client is None:
                errors.append(f"{provider}: not available")
                continue

            try:
                result = operation(client)
                self._last_successful_provider = provider
                return result
            except Exception as e:
                errors.append(f"{provider}: {e}")
                continue

        raise LLMError(f"All providers failed: {'; '.join(errors)}")

    def chat_completion(
        self,
        prompt: str,
        model: str | None = None,
        **kwargs: Any,
    ) -> str:
        """Send chat completion with failover.

        Args:
            prompt: User prompt
            model: Model name (optional, uses config default)
            **kwargs: Additional parameters

        Returns:
            Response text from first successful provider
        """

        def operation(client: LLMClient) -> str:
            if hasattr(client, "chat_completion"):
                messages = [{"role": "user", "content": prompt}]
                response = client.chat_completion(messages, **kwargs)  # type: ignore[attr-defined]
                return response.answer  # type: ignore[no-any-return]
            else:
                response = client.query(prompt, {})
                return response.answer

        return self._try_providers(operation)  # type: ignore[no-any-return]

    def analyze_trace(self, trace_data: dict[str, Any]) -> dict[str, Any]:
        """Analyze trace data with failover.

        Args:
            trace_data: Dictionary containing trace information

        Returns:
            Analysis results dictionary
        """

        def operation(client: LLMClient) -> dict[str, Any]:
            class DictTrace:
                def __init__(self, data: dict[str, Any]) -> None:
                    self._data = data
                    for k, v in data.items():
                        setattr(self, k, v)

            trace = DictTrace(trace_data)

            if hasattr(client, "analyze_trace"):
                response = client.analyze_trace(trace, "Analyze this signal")  # type: ignore[attr-defined]
            else:
                response = client.analyze(trace, "Analyze this signal")

            return {
                "answer": response.answer,
                "suggested_commands": response.suggested_commands,
                "metadata": response.metadata,
            }

        return self._try_providers(operation)  # type: ignore[no-any-return]

    def suggest_measurements(self, signal_characteristics: dict[str, Any]) -> list[str]:
        """Suggest measurements based on signal characteristics.

        Args:
            signal_characteristics: Dictionary describing the signal

        Returns:
            List of suggested measurement names
        """

        def operation(client: LLMClient) -> list[str]:
            class CharTrace:
                def __init__(self, chars: dict[str, Any]) -> None:
                    self.metadata = type("Meta", (), chars)()
                    self.data = None

            trace = CharTrace(signal_characteristics)

            if hasattr(client, "suggest_measurements"):
                response = client.suggest_measurements(trace)  # type: ignore[attr-defined]
            else:
                response = client.analyze(trace, "What measurements should I perform?")

            return response.suggested_commands  # type: ignore[no-any-return]

        return self._try_providers(operation)  # type: ignore[no-any-return]

    def query(self, prompt: str, context: dict[str, Any]) -> LLMResponse:
        """Send query with failover.

        Args:
            prompt: User prompt
            context: Analysis context

        Returns:
            LLM response
        """
        return self._try_providers(  # type: ignore[no-any-return]
            lambda c: c.query(prompt, context)
        )

    def analyze(self, trace: Any, question: str) -> LLMResponse:
        """Analyze trace with failover.

        Args:
            trace: Trace object
            question: Natural language question

        Returns:
            Analysis response
        """
        return self._try_providers(  # type: ignore[no-any-return]
            lambda c: c.analyze(trace, question)
        )

    def explain(self, measurement: Any) -> str:
        """Explain measurement with failover.

        Args:
            measurement: Measurement result

        Returns:
            Explanation text
        """
        return self._try_providers(  # type: ignore[no-any-return]
            lambda c: c.explain(measurement)
        )


# ==============================================================================
# Provider discovery (API-020)
# ==============================================================================


def is_provider_available(provider: str) -> bool:
    """Check if a provider is available (API key set, package installed).

    Args:
        provider: Provider name to check

    Returns:
        True if provider can be initialized

    Examples:
        >>> if is_provider_available("openai"):
        ...     client = get_client("openai")
    """
    if provider == "local":
        return True

    if provider == "openai":
        if not os.environ.get("OPENAI_API_KEY"):
            return False
        try:
            import openai  # type: ignore[import-not-found]  # noqa: F401

            return True
        except ImportError:
            return False

    if provider == "anthropic":
        if not os.environ.get("ANTHROPIC_API_KEY"):
            return False
        try:
            import anthropic  # type: ignore[import-not-found]  # noqa: F401

            return True
        except ImportError:
            return False

    return False


def list_available_providers() -> list[str]:
    """List all currently available LLM providers.

    Returns:
        List of provider names that can be used

    Examples:
        >>> providers = list_available_providers()
        >>> print(providers)  # ['openai', 'local'] if OpenAI key is set
    """
    return [provider.value for provider in LLMProvider if is_provider_available(provider.value)]
