"""Provider 故障切换链（ProviderFailoverChain）.

设计依据 02-architecture-design.md §13.3：
  - 按 priority 顺序尝试 Provider
  - 熔断器拦截已熔断的 Provider
  - 单 Provider 失败自动切换下一个
  - 全部失败抛 AILLMUnavailableError 触发降级
"""

from collections.abc import AsyncGenerator
from typing import Any

import structlog

from ..errors import AILLMUnavailableError
from .base import LLMProvider, LLMResponse, LLMStreamChunk
from .circuit_breaker import CircuitBreaker

logger = structlog.get_logger()


class ProviderFailoverChain:
    """Provider 故障切换链.

    用法：
        chain = ProviderFailoverChain(providers, circuit_breaker)
        response = await chain.chat(messages, model)  # 自动 failover
    """

    def __init__(
        self,
        providers: list[LLMProvider],
        circuit_breaker: CircuitBreaker,
    ) -> None:
        if not providers:
            raise ValueError("providers list must not be empty")
        self._providers = providers
        self._provider_map: dict[str, LLMProvider] = {p.name: p for p in providers}
        self._circuit_breaker = circuit_breaker

    @property
    def providers(self) -> list[LLMProvider]:
        """已注册的 Provider 列表."""
        return list(self._providers)

    @property
    def circuit_breaker(self) -> CircuitBreaker:
        return self._circuit_breaker

    def available_providers(self) -> list[LLMProvider]:
        """返回已配置且未熔断的 Provider 列表."""
        return [
            p
            for p in self._providers
            if p.is_available() and self._circuit_breaker.is_closed(p.name)
        ]

    async def chat(
        self,
        messages: list[dict[str, str]],
        model: str,
        temperature: float = 0.7,
        **kwargs: Any,
    ) -> LLMResponse:
        """非流式对话（自动 failover）.

        Returns:
            LLMResponse（来自第一个成功的 Provider）

        Raises:
            AILLMUnavailableError: 所有 Provider 都失败
        """
        tried: list[str] = []
        last_error: Exception | None = None

        for provider in self._providers:
            if not provider.is_available():
                logger.debug("provider_skip_not_configured", provider=provider.name)
                continue
            if not self._circuit_breaker.is_closed(provider.name):
                logger.info(
                    "provider_skip_circuit_open",
                    provider=provider.name,
                )
                continue

            tried.append(provider.name)
            try:
                response = await provider.chat(messages, model, temperature, **kwargs)
                self._circuit_breaker.record_success(provider.name)
                logger.info(
                    "llm_chat_success",
                    provider=provider.name,
                    model=response.model,
                    total_tokens=response.usage.get("total_tokens", 0),
                )
                return response
            except AILLMUnavailableError as exc:
                last_error = exc
                self._circuit_breaker.record_failure(provider.name)
                logger.warning(
                    "provider_failover",
                    failed_provider=provider.name,
                    error=str(exc),
                )
                continue
            except Exception as exc:  # noqa: BLE001 - 兜底，未知异常也切换
                last_error = exc
                self._circuit_breaker.record_failure(provider.name)
                logger.error(
                    "provider_unexpected_error",
                    provider=provider.name,
                    error=str(exc),
                )
                continue

        logger.error(
            "all_providers_failed",
            tried=tried,
            last_error=str(last_error) if last_error else "no provider available",
        )
        raise AILLMUnavailableError(
            f"all providers failed (tried: {tried or 'none configured'})",
        )

    async def stream_chat(
        self,
        messages: list[dict[str, str]],
        model: str,
        temperature: float = 0.7,
        **kwargs: Any,
    ) -> AsyncGenerator[LLMStreamChunk, None]:
        """流式对话（自动 failover）.

        注意：流式 failover 仅在首 chunk 前生效。一旦开始 yield，
        中途断流不再切换（调用方需处理中断）。

        Yields:
            LLMStreamChunk（来自第一个成功的 Provider）

        Raises:
            AILLMUnavailableError: 所有 Provider 都无法建立流
        """
        tried: list[str] = []
        last_error: Exception | None = None

        for provider in self._providers:
            if not provider.is_available():
                continue
            if not self._circuit_breaker.is_closed(provider.name):
                continue

            tried.append(provider.name)
            try:
                # 尝试建立流（通过迭代验证连通性，首 chunk 后不再 failover）
                stream = provider.stream_chat(messages, model, temperature, **kwargs)
                async for chunk in stream:
                    yield chunk
                # 流正常结束
                self._circuit_breaker.record_success(provider.name)
                logger.info(
                    "llm_stream_success",
                    provider=provider.name,
                    model=model,
                )
                return
            except AILLMUnavailableError as exc:
                last_error = exc
                self._circuit_breaker.record_failure(provider.name)
                logger.warning(
                    "provider_stream_failover",
                    failed_provider=provider.name,
                    error=str(exc),
                )
                continue
            except Exception as exc:  # noqa: BLE001
                last_error = exc
                self._circuit_breaker.record_failure(provider.name)
                logger.error(
                    "provider_stream_unexpected_error",
                    provider=provider.name,
                    error=str(exc),
                )
                continue

        logger.error(
            "all_providers_stream_failed",
            tried=tried,
            last_error=str(last_error) if last_error else "no provider available",
        )
        raise AILLMUnavailableError(
            f"all providers stream failed (tried: {tried or 'none configured'})",
        )

    async def embed(self, text: str, model: str) -> tuple[list[float], str]:
        """文本向量化（自动 failover）.

        Returns:
            (embedding, provider_name)
        """
        for provider in self._providers:
            if not provider.is_available():
                continue
            if not self._circuit_breaker.is_closed(provider.name):
                continue
            try:
                embedding = await provider.embed(text, model)
                self._circuit_breaker.record_success(provider.name)
                return embedding, provider.name
            except NotImplementedError:
                logger.debug("provider_no_embed", provider=provider.name)
                continue
            except AILLMUnavailableError as exc:
                self._circuit_breaker.record_failure(provider.name)
                logger.warning(
                    "provider_embed_failover",
                    failed_provider=provider.name,
                    error=str(exc),
                )
                continue
        raise AILLMUnavailableError("no provider supports embed or all failed")
