"""LLM Config - 配置加载"""

import json
import logging
from dataclasses import dataclass, field
from typing import Any, TYPE_CHECKING

from agentkit.llm.retry import CircuitBreakerConfig, RetryConfig

if TYPE_CHECKING:
    from agentkit.channels.secrets import SecretsStore

logger = logging.getLogger(__name__)


@dataclass
class CacheConfig:
    """LLM Cache 配置"""

    enabled: bool = False
    backend: str = "auto"  # "auto" | "redis" | "memory"
    redis_url: str = "redis://localhost:6379"
    exact_ttl: int = 3600
    semantic_ttl: int = 86400
    similarity_threshold: float = 0.92
    max_entries: int = 10000
    # Embedding config for semantic cache (Chinese-first: bge-m3 via Xinference)
    embedding_provider: str = "openai"  # "openai" | "xinference" | "local"
    embedding_model: str = "bge-m3"
    embedding_base_url: str | None = None
    embedding_api_key: str | None = None

    @classmethod
    def from_dict(cls, data: dict) -> "CacheConfig":
        if not data:
            return cls()
        emb = data.get("embedding", {})
        return cls(
            enabled=data.get("enabled", False),
            backend=data.get("backend", "auto"),
            redis_url=data.get("redis_url", "redis://localhost:6379"),
            exact_ttl=data.get("exact_ttl", 3600),
            semantic_ttl=data.get("semantic_ttl", 86400),
            similarity_threshold=data.get("similarity_threshold", 0.92),
            max_entries=data.get("max_entries", 10000),
            embedding_provider=emb.get("provider", "openai"),
            embedding_model=emb.get("model", "bge-m3"),
            embedding_base_url=emb.get("base_url"),
            embedding_api_key=emb.get("api_key"),
        )


@dataclass
class ProviderConfig:
    """Provider 配置"""

    api_key: str
    base_url: str
    models: dict[str, dict[str, Any]] = field(default_factory=dict)
    type: str = "openai"  # "openai" | "anthropic" | "gemini"
    max_tokens: int = 4096  # Anthropic: default max_tokens
    timeout: float = 120.0  # Anthropic: request timeout
    max_connections: int = 100  # httpx 连接池最大连接数
    max_keepalive_connections: int = 20  # httpx 连接池最大保活连接数
    keepalive_expiry: float = 30.0  # httpx 保活连接过期时间（秒）
    retry: RetryConfig | None = None
    circuit_breaker: CircuitBreakerConfig | None = None
    # U15 — API Key 加密迁移（plaintext → SecretsStore）。
    # api_key_encrypted: JSON 编码的 SecretEntry（base64 nonce+salt+ciphertext）。
    #   为 None 表示未迁移，仍走 plaintext api_key。
    # api_key_source: 当前 key 来源标记，用于双写/双读迁移窗口。
    #   "plaintext" — 仅 plaintext 列；"secrets_store" — 仅加密列；
    #   "dual" — 双写窗口中（两列都有值，读时优先加密）。
    api_key_encrypted: str | None = None
    api_key_source: str = "plaintext"

    def get_api_key(self) -> str:
        """同步读取 API Key — 返回 plaintext。

        双读窗口的同步入口：无法 await ``SecretsStore.get_secret``，
        因此加密列需通过异步方法 :meth:`aget_api_key` 读取。
        本方法始终返回 plaintext ``api_key``（迁移期保证可用性）。
        """
        if self.api_key_encrypted:
            logger.debug("get_api_key: encrypted key set — use aget_api_key for decryption")
        return self.api_key

    async def aget_api_key(self, secrets_store: "SecretsStore | None" = None) -> str:
        """异步读取 API Key — 双读窗口优先加密列，失败回退 plaintext。

        Args:
            secrets_store: 可选的加密存储。为 None 时直接返回 plaintext。

        Returns:
            解密后的 API Key；加密列解密失败时回退到 plaintext ``api_key``。
        """
        if self.api_key_encrypted and secrets_store is not None:
            try:
                entry = self._decode_secret_entry(self.api_key_encrypted)
                decrypted = await secrets_store.get_secret(entry.key)
                if decrypted is not None:
                    return decrypted
                # store 里没有这个 key（可能已被删除）— 回退 plaintext
                logger.warning(
                    f"aget_api_key: encrypted key for provider type={self.type} "
                    f"not found in secrets_store — fallback to plaintext"
                )
            except Exception as e:
                # 解密失败（master key 不匹配 / 密文损坏）— 回退 plaintext
                logger.warning(
                    f"aget_api_key: decrypt failed for type={self.type}: {e} — fallback to plaintext"
                )
        return self.api_key

    async def migrate_to_secrets(self, secrets_store: "SecretsStore") -> None:
        """把 plaintext api_key 迁移到 SecretsStore（幂等）。

        迁移步骤（双写窗口）：
        1. 若 ``api_key_source == "secrets_store"`` 且 plaintext 已清空 → 已迁移，no-op。
        2. 否则：调用 ``secrets_store.set_secret`` 加密存储 key。
        3. 把返回的 SecretEntry JSON 编码写入 ``api_key_encrypted``。
        4. 标记 ``api_key_source = "secrets_store"``。
        5. 验证：调用 ``get_secret`` 读回对比，成功后清空 plaintext ``api_key=""``。

        幂等性：重复调用不会重复加密（已迁移时直接返回）。
        部分失败恢复：若 set 成功但验证失败，保留 plaintext 不清空，
        ``api_key_encrypted`` 已写入 — 下次重试时由幂等性保证最终一致。

        Args:
            secrets_store: 用于加密存储的 SecretsStore 实例。
        """
        # 幂等：已迁移完成（source 标记 + plaintext 已清空）
        if self.api_key_source == "secrets_store" and not self.api_key and self.api_key_encrypted:
            return

        # 没有 plaintext 可迁移（空 key）— 跳过
        if not self.api_key:
            return

        secret_key = self._secret_key_for_type()
        # 双写：先写加密列
        entry = await secrets_store.set_secret(secret_key, self.api_key)
        self.api_key_encrypted = self._encode_secret_entry(entry, secret_key)

        # 验证：读回对比
        try:
            decrypted = await secrets_store.get_secret(secret_key)
        except Exception as e:
            logger.warning(f"migrate_to_secrets: verify read failed for type={self.type}: {e}")
            # 加密列已写但验证失败 — 保留 plaintext，标记 dual 待重试
            self.api_key_source = "dual"
            return

        if decrypted != self.api_key:
            logger.error(
                f"migrate_to_secrets: verify mismatch for type={self.type} "
                f"— plaintext retained, source=dual"
            )
            self.api_key_source = "dual"
            return

        # 验证通过：清空 plaintext，标记完成
        self.api_key_source = "secrets_store"
        self.api_key = ""

    def _secret_key_for_type(self) -> str:
        """生成 SecretsStore 中的 key（按 provider type 命名空间隔离）。"""
        return f"llm:provider:{self.type}:api_key"

    @staticmethod
    def _encode_secret_entry(entry: Any, key: str) -> str:
        """把 SecretEntry 编码为 JSON 字符串（含 key 字段）。"""
        # entry 是 SecretEntry pydantic 模型，有 model_dump()
        if hasattr(entry, "model_dump"):
            data = entry.model_dump()
        else:
            data = dict(entry)
        data["key"] = key
        return json.dumps(data)

    @staticmethod
    def _decode_secret_entry(encoded: str) -> Any:
        """从 JSON 字符串解码 SecretEntry。返回带 .key 属性的对象。"""
        from agentkit.channels.secrets import SecretEntry

        data = json.loads(encoded)
        return SecretEntry(
            key=data.get("key", ""),
            value=data["value"],
            nonce=data["nonce"],
            salt=data["salt"],
            key_id=data.get("key_id", "default"),
            created_at=data.get("created_at", ""),
            updated_at=data.get("updated_at", ""),
        )


@dataclass
class LLMConfig:
    """LLM 配置"""

    providers: dict[str, ProviderConfig] = field(default_factory=dict)
    model_aliases: dict[str, str] = field(default_factory=dict)
    fallbacks: dict[str, list[str]] = field(default_factory=dict)
    cache: CacheConfig | None = None

    @classmethod
    def from_dict(cls, data: dict) -> "LLMConfig":
        """从字典加载配置"""
        providers = {}
        for name, pconf in data.get("providers", {}).items():
            retry = None
            retry_data = pconf.get("retry")
            if retry_data:
                retry = RetryConfig(
                    max_retries=retry_data.get("max_retries", 3),
                    base_delay=retry_data.get("base_delay", 1.0),
                    max_delay=retry_data.get("max_delay", 30.0),
                    exponential_base=retry_data.get("exponential_base", 2.0),
                )

            circuit_breaker = None
            cb_data = pconf.get("circuit_breaker")
            if cb_data:
                circuit_breaker = CircuitBreakerConfig(
                    failure_threshold=cb_data.get("failure_threshold", 5),
                    recovery_timeout=cb_data.get("recovery_timeout", 60.0),
                    half_open_max=cb_data.get("half_open_max", 1),
                )

            providers[name] = ProviderConfig(
                api_key=pconf.get("api_key", ""),
                base_url=pconf.get("base_url", ""),
                models=pconf.get("models", {}),
                type=pconf.get("type", "openai"),
                max_tokens=pconf.get("max_tokens", 4096),
                timeout=pconf.get("timeout", 120.0),
                max_connections=pconf.get("max_connections", 100),
                max_keepalive_connections=pconf.get("max_keepalive_connections", 20),
                keepalive_expiry=pconf.get("keepalive_expiry", 30.0),
                retry=retry,
                circuit_breaker=circuit_breaker,
                # U15 — 新增加密迁移字段，缺省时保持 plaintext 行为
                api_key_encrypted=pconf.get("api_key_encrypted"),
                api_key_source=pconf.get("api_key_source", "plaintext"),
            )
        cache = None
        cache_data = data.get("cache")
        if cache_data:
            cache = CacheConfig.from_dict(cache_data)

        return cls(
            providers=providers,
            model_aliases=data.get("model_aliases", {}),
            fallbacks=data.get("fallbacks", {}),
            cache=cache,
        )
