"""LLM Provider Infrastructure.

This module provides the provider abstraction for LLM calls:
- LLMProvider: Protocol for provider adapters
- get_provider(): Get the appropriate provider
- LLMGenerateResult, LLMEmbedResult: Pydantic response models

ALL PYDANTIC — LLMGenerateResult and LLMEmbedResult are Pydantic models
imported from core.types.llm (the single source of truth).

Architecture:
- OpenRouter is the primary provider (200+ models, vision, audio, etc.)
- Ollama is the fallback for local/offline usage
- All other providers (OpenAI, Anthropic, Google) go through OpenRouter

Usage:
    from stdlib.llm.providers import get_provider, LLMGenerateResult

    provider = get_provider()  # Returns OpenRouter or Ollama
    result = await provider.generate(
        prompt="Hello",
        model="openai/gpt-4o",  # OpenRouter model format
    )
"""

from __future__ import annotations

import contextlib
import logging
import os
from typing import Protocol, runtime_checkable

from core.common import JsonObject
from core.types.llm import (
    LLMEmbedResult,
    LLMGenerateResult,
    LLMToolCall,
)

logger = logging.getLogger(__name__)


# =============================================================================
# Response DTOs — imported from core.types.llm (Pydantic models, not dataclasses)
# =============================================================================
# LLMGenerateResult and LLMEmbedResult are now Pydantic BaseModel instances.
# They are the canonical return types from all providers.
# Import them from core.types.llm for the single source of truth.


# =============================================================================
# Provider Protocol
# =============================================================================


class LLMProviderError(RuntimeError):
    """Normalized provider error."""

    def __init__(
        self,
        message: str,
        provider: str,
        model: str | None = None,
        is_transient: bool = False,
    ):
        self.message = message
        self.provider = provider
        self.model = model
        self.is_transient = is_transient
        super().__init__(f"{provider}: {message}")


@runtime_checkable
class LLMProvider(Protocol):
    """Protocol for LLM provider adapters.

    Note: The provider.generate() signature still accepts dicts for messages/tools
    because it sits at the wire-format boundary. The invoke.py layer converts
    Pydantic models to dicts before calling providers.
    """

    async def generate(
        self,
        *,
        prompt: str,
        model: str,
        system: str | None = None,
        temperature: float = 0.7,
        max_tokens: int = 4096,
        response_format: str = "text",
        tools: list[JsonObject] | None = None,
        tool_choice: str | JsonObject | None = None,
        messages: list[JsonObject] | None = None,
        images: list[str] | None = None,
    ) -> LLMGenerateResult:
        """Generate text using the LLM.

        Returns a Pydantic LLMGenerateResult model.
        """
        ...

    async def embed(
        self,
        texts: list[str],
        *,
        model: str,
    ) -> LLMEmbedResult:
        """Generate embeddings for texts."""
        ...

    async def aclose(self) -> None:
        """Close provider connections."""
        ...


# =============================================================================
# Provider Registry
# =============================================================================

_PROVIDERS: dict[str, LLMProvider] = {}
_INITIALIZED = False


def _init_providers() -> None:
    """Initialize providers from environment.

    Priority for ``openai/*`` models:
    1. Direct OpenAI API (if OPENAI_API_KEY set) — avoids OpenRouter billing
    2. OpenRouter (if OPENROUTER_API_KEY set)
    3. Ollama (local fallback)
    """
    global _INITIALIZED, _PROVIDERS

    if _INITIALIZED:
        return

    openrouter_key = os.getenv("OPENROUTER_API_KEY")
    openai_key = os.getenv("OPENAI_API_KEY")
    anthropic_key = os.getenv("ANTHROPIC_API_KEY")
    ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434")

    # Direct OpenAI (bypasses OpenRouter when available)
    if openai_key:
        from stdlib.llm.providers.openrouter import OpenRouterAdapter

        class _DirectOpenAI(OpenRouterAdapter):
            """Thin wrapper that routes directly to OpenAI API."""

            BASE_URL = "https://api.openai.com/v1"

            def _normalize_model(self, model: str) -> str:
                # Strip provider prefix for direct API: "openai/gpt-4o" → "gpt-4o"
                if model.startswith("openai/"):
                    return model[len("openai/"):]
                return model

        _PROVIDERS["openai_direct"] = _DirectOpenAI(api_key=openai_key)
        logger.debug("Initialized direct OpenAI adapter")

    # OpenRouter (200+ models, fallback for non-direct providers)
    if openrouter_key:
        from stdlib.llm.providers.openrouter import OpenRouterAdapter

        _PROVIDERS["openrouter"] = OpenRouterAdapter(api_key=openrouter_key)
        logger.debug("Initialized OpenRouter adapter")

    # Ollama (local/offline fallback)
    from stdlib.llm.providers.ollama import OllamaAdapter

    _PROVIDERS["ollama"] = OllamaAdapter(host=ollama_host)
    logger.debug("Initialized Ollama adapter (host=%s)", ollama_host)

    _INITIALIZED = True


def get_provider(model: str | None = None) -> LLMProvider:
    """Get the appropriate provider for a model.

    Routing priority:
    1. Local models (no slash) → Ollama
    2. ``openai/*`` models → Direct OpenAI (if available), else OpenRouter
    3. ``anthropic/*`` models → Direct Anthropic (if available), else OpenRouter
    4. Everything else → OpenRouter → Ollama fallback

    Args:
        model: Model name (e.g. ``"openai/gpt-4o"``, ``"anthropic/claude-sonnet-4"``).

    Returns
    -------
        Provider adapter instance
    """
    _init_providers()

    if model:
        model_lower = model.lower()

        # Local models (Ollama) — no slash, not a known cloud prefix
        is_local = "/" not in model and not model_lower.startswith(
            ("gpt-", "claude", "gemini", "openai/", "anthropic/", "google/")
        )
        if is_local and "ollama" in _PROVIDERS:
            return _PROVIDERS["ollama"]

        # Direct OpenAI — bypass OpenRouter for openai/* models
        if model_lower.startswith("openai/") and "openai_direct" in _PROVIDERS:
            return _PROVIDERS["openai_direct"]

    # OpenRouter (handles all other provider/model combos)
    if "openrouter" in _PROVIDERS:
        return _PROVIDERS["openrouter"]

    # Fallback: Ollama
    if "ollama" in _PROVIDERS:
        return _PROVIDERS["ollama"]

    raise LLMProviderError(
        message="No LLM providers configured. Set OPENAI_API_KEY, OPENROUTER_API_KEY, or run Ollama.",
        provider="none",
        model=model,
        is_transient=False,
    )


def list_available_providers() -> list[str]:
    """List configured provider names."""
    _init_providers()
    return list(_PROVIDERS.keys())


async def close_all_providers() -> None:
    """Close all provider connections."""
    for provider in _PROVIDERS.values():
        with contextlib.suppress(Exception):
            await provider.aclose()


__all__ = [
    # DTOs (Pydantic models from core.types.llm)
    "LLMEmbedResult",
    "LLMGenerateResult",
    "LLMToolCall",
    # Protocol
    "LLMProvider",
    "LLMProviderError",
    # Registry
    "close_all_providers",
    "get_provider",
    "list_available_providers",
]
