"""Provider factory for creating LangChain chat models.

Provides a unified interface for creating chat models from different
LLM providers based on user configuration. All providers go through
init_chat_model with OpenAI-compatible providers using base_url.
"""

from __future__ import annotations

import logging
import os
from dataclasses import dataclass
from typing import TYPE_CHECKING

from langchain.chat_models import init_chat_model

from unfamiliar.config import APIKeyError, resolve_api_key
from unfamiliar.llm.exceptions import ProviderError

if TYPE_CHECKING:
    from langchain_core.language_models.chat_models import BaseChatModel

    from unfamiliar.config import UnfamiliarConfig

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class ProviderConfig:
    """Configuration for a single LLM provider.

    Attributes:
        langchain_provider: LangChain provider identifier (e.g., "anthropic", "openai")
        default_model: Default model name to use if none specified
        api_key_env: Environment variable name for the API key
        base_url: Custom API endpoint URL (for OpenAI-compatible providers)
        retry_base_delay: Base delay in seconds for exponential backoff on retries.
            Providers with aggressive rate limits (GLM) need longer delays.
        supports_tool_calling: Whether this provider supports tool-calling-based
            structured output (response_format with JSON schema). When False,
            structured output uses JSON text fallback instead.
    """

    langchain_provider: str
    default_model: str
    api_key_env: str
    base_url: str | None = None
    retry_base_delay: float = 1.0
    supports_tool_calling: bool = True


# Built-in provider configurations keyed by user-facing name
_BUILTIN_PROVIDERS: dict[str, ProviderConfig] = {
    "anthropic": ProviderConfig(
        langchain_provider="anthropic",
        default_model="claude-sonnet-4-5-20250929",
        api_key_env="ANTHROPIC_API_KEY",
    ),
    "glm": ProviderConfig(
        langchain_provider="openai",
        default_model="glm-5",
        api_key_env="ZHIPUAI_API_KEY",
        base_url="https://api.z.ai/api/coding/paas/v4",
        retry_base_delay=2.0,  # GLM uses concurrency-based limits
        supports_tool_calling=False,  # GLM rate-limits structured outputs API
    ),
}

# Shared default parameters for all providers
_DEFAULT_TEMPERATURE = 0.2
_DEFAULT_MAX_RETRIES = 2


def provider_supports_tool_calling(provider_name: str) -> bool:
    """Check whether a provider supports tool-calling-based structured output.

    Args:
        provider_name: Provider name (e.g., "anthropic", "glm"). Case-insensitive.

    Returns:
        True if the provider supports tool calling, False otherwise.
        Returns True for unknown providers (optimistic default).
    """
    provider = provider_name.lower().strip()
    config = _BUILTIN_PROVIDERS.get(provider)
    if config:
        return config.supports_tool_calling
    return True


def get_retry_delay(provider_name: str) -> float:
    """Return the appropriate base delay for retry backoff based on provider.

    Args:
        provider_name: Provider name (e.g., "anthropic", "glm"). Case-insensitive.

    Returns:
        Base delay in seconds for exponential backoff.
        Returns 1.0 for unknown providers.
    """
    provider = provider_name.lower().strip()
    config = _BUILTIN_PROVIDERS.get(provider)
    if config:
        return config.retry_base_delay
    return 1.0  # Default for unknown providers


def get_provider_config(
    provider_name: str,
    config: UnfamiliarConfig,
) -> ProviderConfig:
    """Merge config overrides onto built-in provider defaults.

    Args:
        provider_name: Provider name (e.g., "anthropic", "glm"). Case-insensitive.
        config: User configuration with optional LLM overrides.

    Returns:
        ProviderConfig with merged values.

    Raises:
        ProviderError: If the provider name is not recognized.
    """
    normalized = provider_name.lower().strip()
    builtin = _BUILTIN_PROVIDERS.get(normalized)
    if builtin is None:
        raise ProviderError(
            f"Unknown provider: '{provider_name}'. "
            f"Supported providers: {', '.join(_BUILTIN_PROVIDERS.keys())}"
        )

    # Apply config overrides (non-None values from config.llm replace defaults)
    return ProviderConfig(
        langchain_provider=builtin.langchain_provider,
        default_model=config.llm.model or builtin.default_model,
        api_key_env=config.llm.api_key_env or builtin.api_key_env,
        base_url=(
            config.llm.base_url
            if config.llm.base_url is not None
            else builtin.base_url
        ),
        retry_base_delay=builtin.retry_base_delay,  # Not user-configurable
        supports_tool_calling=builtin.supports_tool_calling,  # Not user-configurable
    )


def _resolve_max_tokens(
    config: UnfamiliarConfig,
    max_tokens_override: int | None,
) -> int | None:
    """Resolve max_tokens from override, config, or env var.

    Priority: max_tokens param > config.llm.max_tokens > UNFAMILIAR_MAX_TOKENS > None.

    Returns None if not set anywhere, meaning the model decides output length.
    """
    if max_tokens_override is not None:
        return max_tokens_override
    if config.llm.max_tokens is not None:
        return config.llm.max_tokens
    env_value = os.environ.get("UNFAMILIAR_MAX_TOKENS")
    if env_value:
        try:
            return int(env_value)
        except ValueError:
            logger.warning(
                "Invalid UNFAMILIAR_MAX_TOKENS value '%s', ignoring",
                env_value,
            )
    return None


def create_chat_model(
    config: UnfamiliarConfig,
    max_tokens: int | None = None,
) -> BaseChatModel:
    """Create a LangChain chat model from user configuration.

    All providers go through init_chat_model. OpenAI-compatible providers
    (like GLM/Zhipu) use model_provider="openai" with a base_url.

    Args:
        config: User configuration containing provider selection and API keys
        max_tokens: Override max output tokens. If None, uses config.llm.max_tokens,
            then UNFAMILIAR_MAX_TOKENS env var. If still None, omits max_tokens
            from the API call (model decides).

    Returns:
        A configured BaseChatModel instance

    Raises:
        ProviderError: If the provider is unknown, API key is missing, or provider
        package is not installed.
    """
    provider_name = config.provider.lower().strip()
    model_override = os.environ.get("UNFAMILIAR_MODEL")
    logger.debug("Creating chat model for provider: %s", provider_name)

    # Get merged provider config (registry defaults + config overrides)
    provider_config = get_provider_config(provider_name, config)

    # Resolve API key using registry's api_key_env
    try:
        api_key = resolve_api_key(
            provider_name,
            config,
            api_key_env_override=provider_config.api_key_env,
        )
    except APIKeyError as exc:
        raise ProviderError(
            f"No API key found for provider '{provider_name}'. "
            f"Set the {provider_config.api_key_env} environment variable."
        ) from exc

    # Determine model name (env override > config > registry default)
    model_name = model_override or provider_config.default_model

    # Resolve max_tokens (may be None → omit from API call)
    effective_max_tokens = _resolve_max_tokens(config, max_tokens)

    # Build kwargs for init_chat_model
    kwargs: dict = {
        "model": model_name,
        "model_provider": provider_config.langchain_provider,
        "api_key": api_key,
        "temperature": _DEFAULT_TEMPERATURE,
        "max_retries": _DEFAULT_MAX_RETRIES,
    }

    # Add base_url only for OpenAI-compatible providers
    if provider_config.base_url is not None:
        kwargs["base_url"] = provider_config.base_url

    # Add max_tokens only if explicitly set (let model decide otherwise)
    if effective_max_tokens is not None:
        kwargs["max_tokens"] = effective_max_tokens

    try:
        model = init_chat_model(**kwargs)
        extras = []
        if effective_max_tokens is not None:
            extras.append(f"max_tokens={effective_max_tokens}")
        if provider_config.base_url is not None:
            extras.append(f"base_url={provider_config.base_url}")
        extra_str = ", ".join(extras) if extras else ""
        logger.info(
            "Created %s chat model: %s (provider=%s, "
            "temperature=%.1f%s)",
            provider_name,
            model_name,
            provider_config.langchain_provider,
            _DEFAULT_TEMPERATURE,
            extra_str,
        )
        return model
    except ImportError as exc:
        raise ProviderError(
            f"Failed to initialize {provider_name} provider. "
            f"Ensure the required package is installed: {exc}"
        ) from exc
    except (ValueError, TypeError) as exc:
        raise ProviderError(
            f"Failed to initialize {provider_name} provider: {exc}"
        ) from exc
