"""Provider fallback selection logic.

Selects providers based on required capabilities and budget availability,
skipping providers that lack tool calling, streaming, or other features.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import ClassVar

from cleveragents.providers.cost_tracker import BudgetStatus, CostTracker
from cleveragents.providers.registry import (
    ProviderCapabilities,
    ProviderInfo,
    ProviderRegistry,
    ProviderType,
)

logger = logging.getLogger(__name__)


@dataclass(frozen=True, slots=True)
class FallbackResult:
    """Result of a fallback provider selection.

    Attributes:
        provider_type: Selected provider type, or None if no suitable
            provider found.
        provider_info: Provider info for the selected provider.
        skipped: List of (provider_type, reason) pairs for skipped providers.
    """

    provider_type: ProviderType | None
    provider_info: ProviderInfo | None
    skipped: list[tuple[str, str]]


class FallbackSelector:
    """Select providers using fallback logic with capability filtering.

    Iterates through configured providers (in user-defined or default
    priority order) and selects the first that meets all required
    capabilities and is within budget.

    Example::

        selector = FallbackSelector(
            registry=registry,
            cost_tracker=cost_tracker,
            fallback_providers=["openai", "anthropic", "groq"],
        )
        result = selector.select(
            require_tool_calls=True,
            require_streaming=True,
        )
        if result.provider_type:
            # Use result.provider_type
            ...
    """

    DEFAULT_FALLBACK_ORDER: ClassVar[list[str]] = [
        "openai",
        "anthropic",
        "google",
        "azure",
        "openrouter",
        "groq",
        "together",
        "cohere",
    ]

    def __init__(
        self,
        *,
        registry: ProviderRegistry,
        cost_tracker: CostTracker | None = None,
        fallback_providers: list[str] | None = None,
    ) -> None:
        """Initialize the fallback selector.

        Args:
            registry: Provider registry for capability lookups.
            cost_tracker: Optional cost tracker for budget checks.
            fallback_providers: Ordered list of provider names to try.
                Uses DEFAULT_FALLBACK_ORDER if not specified.

        Raises:
            TypeError: If registry is None.
            ValueError: If fallback_providers contains empty strings.
        """
        if registry is None:
            raise TypeError("registry must not be None")
        if fallback_providers is not None:
            for idx, name in enumerate(fallback_providers):
                if not name or not name.strip():
                    raise ValueError(
                        f"fallback_providers[{idx}] must be a non-empty string"
                    )
        self._registry = registry
        self._cost_tracker = cost_tracker
        self._fallback_order = (
            [p.strip().lower() for p in fallback_providers]
            if fallback_providers
            else list(self.DEFAULT_FALLBACK_ORDER)
        )

    def select(
        self,
        *,
        require_tool_calls: bool = False,
        require_streaming: bool = False,
        require_vision: bool = False,
        require_json_mode: bool = False,
    ) -> FallbackResult:
        """Select the best available provider matching requirements.

        Iterates through the fallback order, skipping providers that:
        - Are not configured (no API key)
        - Lack required capabilities
        - Would exceed budget limits

        Args:
            require_tool_calls: Require tool/function calling support.
            require_streaming: Require streaming response support.
            require_vision: Require image/vision input support.
            require_json_mode: Require structured JSON output.

        Returns:
            FallbackResult with the selected provider or None.
        """
        skipped: list[tuple[str, str]] = []

        for provider_name in self._fallback_order:
            info = self._registry.get_provider_info(provider_name)
            if info is None:
                skipped.append((provider_name, "unknown provider type"))
                continue

            if not info.is_configured:
                skipped.append((provider_name, "not configured (no API key)"))
                continue

            caps = info.capabilities
            skip_reason = self._check_capabilities(
                caps,
                require_tool_calls=require_tool_calls,
                require_streaming=require_streaming,
                require_vision=require_vision,
                require_json_mode=require_json_mode,
            )
            if skip_reason:
                skipped.append((provider_name, skip_reason))
                continue

            if self._cost_tracker is not None:
                daily_check = self._cost_tracker.check_daily_budget()
                if daily_check.status == BudgetStatus.EXCEEDED:
                    skipped.append((provider_name, "daily budget exceeded"))
                    continue

            logger.info(
                "Fallback selected provider: %s (skipped %d)",
                provider_name,
                len(skipped),
            )
            return FallbackResult(
                provider_type=info.provider_type,
                provider_info=info,
                skipped=skipped,
            )

        logger.warning(
            "No suitable fallback provider found (tried %d providers)",
            len(self._fallback_order),
        )
        return FallbackResult(
            provider_type=None,
            provider_info=None,
            skipped=skipped,
        )

    def _check_capabilities(
        self,
        caps: ProviderCapabilities,
        *,
        require_tool_calls: bool,
        require_streaming: bool,
        require_vision: bool,
        require_json_mode: bool,
    ) -> str | None:
        """Check if capabilities meet requirements.

        Returns:
            Reason string if capabilities are insufficient, None if OK.
        """
        if require_tool_calls and not caps.supports_tool_calls:
            return "missing capability: tool_calls"
        if require_streaming and not caps.supports_streaming:
            return "missing capability: streaming"
        if require_vision and not caps.supports_vision:
            return "missing capability: vision"
        if require_json_mode and not caps.supports_json_mode:
            return "missing capability: json_mode"
        return None
