"""Factory for creating LLM providers from settings.

Uses LiteLLM UniversalProvider — supports 100+ providers
through a single interface. No provider-specific code.
"""

from __future__ import annotations

import logging
from collections.abc import AsyncIterator

from agntspace.infra.config import Settings
from agntspace.llm.base import LLMError, LLMProvider, LLMResponse, Message, StreamChunk
from agntspace.llm.provider import UniversalProvider

log = logging.getLogger(__name__)

_NO_KEY_MSG = (
    "No API key configured. Please go to Settings > LLM and enter your API key, "
    "or set the ANTHROPIC_API_KEY / OPENAI_API_KEY environment variable."
)


def _resolve_ollama_model(configured: str) -> str:
    """Resolve a user-configured Ollama model name against what's installed.

    Ollama accepts both tagged (llama3:latest) and untagged (llama3) names
    for some models, but many setups reject untagged names. If the configured
    name doesn't match any installed model exactly, try appending ":latest"
    or finding any installed variant with the same prefix.
    """
    try:
        import httpx
        r = httpx.get("http://localhost:11434/api/tags", timeout=2.0)
        if r.status_code != 200:
            return configured
        installed = {m["name"] for m in r.json().get("models", [])}
    except Exception:
        return configured

    if configured in installed:
        return configured
    with_latest = f"{configured}:latest"
    if with_latest in installed:
        log.info("Ollama model %s not found, using %s instead", configured, with_latest)
        return with_latest
    # Fallback: any installed model with the same base name
    base = configured.split(":")[0]
    for name in sorted(installed):
        if name.startswith(f"{base}:"):
            log.info("Ollama model %s not found, using %s instead", configured, name)
            return name
    log.warning("Ollama model %s not installed (available: %s)", configured, sorted(installed))
    return configured


class _StubProvider(LLMProvider):
    """Placeholder provider when no API key is available.

    Lets the server boot so the user can access the UI to enter the key.
    Every LLM call returns a clear error message instead of crashing.
    """

    async def complete(
        self,
        messages: list[Message],
        system: str | None = None,
        max_tokens: int = 4096,
        temperature: float = 0.7,
    ) -> LLMResponse:
        raise LLMError(_NO_KEY_MSG, provider="none", retryable=False)

    async def stream(  # type: ignore[override]
        self,
        messages: list[Message],
        system: str | None = None,
        max_tokens: int = 4096,
        temperature: float = 0.7,
    ) -> AsyncIterator[StreamChunk]:
        yield StreamChunk(delta=_NO_KEY_MSG, done=True)


def get_provider(
    settings: Settings,
    credential_store: object | None = None,
) -> LLMProvider:
    """Create an LLM provider based on current settings.

    Returns a stub provider if no API key is available — the server
    still starts so the user can access the UI to enter the key.

    Model format: "provider/model" for LiteLLM routing.
    Examples:
        anthropic/claude-sonnet-4-6-20260415
        openai/gpt-5.4
        ollama/llama3
        gemini/gemini-2.5-flash
    """
    fallback = settings.llm.fallback_model or None

    if settings.llm.mode == "local":
        # Resolve the configured model against what's actually installed in
        # Ollama. User config says "llama3" but Ollama has "llama3:latest" —
        # without this fixup every LLM call fails with "Model not found".
        local_name = _resolve_ollama_model(settings.llm.local_model)
        model = f"ollama/{local_name}"
        return UniversalProvider(
            model=model,
            api_base="http://localhost:11434",
            fallback_model=fallback,
        )

    # Cloud mode — build model string
    provider = settings.llm.cloud_provider
    model_name = settings.llm.cloud_model

    # cloud_model may be a quality profile name (quality/balanced/economy) instead of a model name.
    # Resolve to a real model in that case.
    _PROFILE_NAMES = {"quality", "balanced", "economy"}
    if model_name in _PROFILE_NAMES:
        _PROVIDER_DEFAULTS: dict[str, str] = {
            "anthropic": "claude-sonnet-4-20250514",
            "openai": "gpt-5.4",
            "google": "gemini-2.0-flash",
        }
        model_name = _PROVIDER_DEFAULTS.get(provider, "claude-sonnet-4-20250514")
        log.info("Resolved quality profile '%s' for %s → %s", settings.llm.cloud_model, provider, model_name)

    model = model_name if "/" in model_name else f"{provider}/{model_name}"

    # Get the appropriate API key: env vars first, then credential store
    api_key = settings.active_api_key
    if (not api_key or api_key == "ollama") and credential_store is not None:
        # UI saves under the provider name (e.g. "anthropic", "openai")
        creds = getattr(credential_store, "get", lambda _: None)(provider)
        if creds:
            api_key = creds.get("api_key", "")

    if not api_key or api_key == "ollama":
        log.warning(
            "API key not set for %s. Server will start but LLM features are unavailable. "
            "Set the key in Settings > LLM or via ANTHROPIC_API_KEY / OPENAI_API_KEY env var.",
            provider,
        )
        return _StubProvider()

    return UniversalProvider(model=model, api_key=api_key, fallback_model=fallback)
