"""
llm_smart_router.decorator
--------------------------

Smart Model Decorator 核心實作

提供 @smart_model 裝飾器,為 LLM 呼叫函數加裝自動模型追蹤與替換功能。
"""

import asyncio
import logging
from functools import wraps
from typing import Callable, Optional, Any

from .providers import get_provider
from .cache import get_replacement_cache
from .exceptions import ModelReplacementError, ProviderAPIError

# 通知勾子型別定義
# on_replace(provider, old_model, new_model) -> Any
OnReplaceCallback = Callable[[str, str, str], Any]


# 設定 logger
logger = logging.getLogger("llm_smart_router")


def smart_model(
    provider: str,
    model: str,
    model_param: str = "model",
    max_retries: int = 1,
    raise_on_fail: bool = True,
    dry_run: bool = False,
    on_replace: Optional[OnReplaceCallback] = None,
    cache_ttl: Optional[int] = None
) -> Callable:
    """
    智能模型裝飾器
    
    為 LLM 呼叫函數加裝自動模型追蹤與替換功能。當模型失效時,
    自動查詢供應商 API 並切換到同家族的最新版本。
    
    Args:
        provider: 供應商名稱 (如 "claude", "gemini", "openai", "qwen", "mistral")
        model: 初始模型名稱 (如 "claude-3-5-haiku")
        model_param: 函數中模型參數的名稱 (預設 "model")
        max_retries: 失敗後的重試次數 (預設 1 次)
        raise_on_fail: 無法找到替代模型時是否拋出異常 (預設 True)
        dry_run: 是否啟用 Dry Run 模式,僅模擬不實際呼叫 (預設 False)
        on_replace: 替換通知勾子,當模型替換時觸發 (可選)
        cache_ttl: 替換快取的 TTL 時間 (秒),None 表示使用預設值 (可選)
    
    Returns:
        裝飾後的函數
    
    Raises:
        ModelReplacementError: 當 raise_on_fail=True 且無法找到替代模型時拋出
        ProviderNotSupportedError: 當指定的 provider 不支援時拋出
    
    Examples:
        >>> @smart_model(provider="claude", model="claude-3-5-haiku")
        ... def ask_claude(model: str, question: str):
        ...     return anthropic.messages.create(model=model, ...)
        
        >>> # 使用通知勾子
        >>> def notify_slack(provider, old, new):
        ...     print(f"Model replaced: {old} -> {new}")
        >>> @smart_model(provider="gemini", model="gemini-2.5-flash", on_replace=notify_slack)
        ... async def ask_gemini(model: str, text: str):
        ...     return await genai.generate_content_async(model=model, ...)
    """
    def decorator(func: Callable) -> Callable:
        # 判斷是否為非同步函數
        is_async = asyncio.iscoroutinefunction(func)
        
        if is_async:
            @wraps(func)
            async def async_wrapper(*args, **kwargs) -> Any:
                """非同步函數包裝器"""
                return await _execute_with_retry(
                    func, args, kwargs,
                    provider, model, model_param,
                    max_retries, raise_on_fail, dry_run,
                    on_replace, cache_ttl,
                    is_async=True
                )
            return async_wrapper
        else:
            @wraps(func)
            def sync_wrapper(*args, **kwargs) -> Any:
                """同步函數包裝器"""
                return asyncio.run(_execute_with_retry(
                    func, args, kwargs,
                    provider, model, model_param,
                    max_retries, raise_on_fail, dry_run,
                    on_replace, cache_ttl,
                    is_async=False
                ))
            return sync_wrapper
    
    return decorator


async def _execute_with_retry(
    func: Callable,
    args: tuple,
    kwargs: dict,
    provider: str,
    initial_model: str,
    model_param: str,
    max_retries: int,
    raise_on_fail: bool,
    dry_run: bool,
    on_replace: Optional[OnReplaceCallback],
    cache_ttl: Optional[int],
    is_async: bool
) -> Any:
    """
    執行函數並在模型失效時重試
    
    此函數處理核心的重試邏輯:
    1. Dry Run 模式: 直接返回 Mock 資料
    2. 正常模式: 呼叫函數,若失敗則查詢替代模型並重試
    
    Args:
        func: 被裝飾的函數
        args: 函數的位置參數
        kwargs: 函數的關鍵字參數
        provider: 供應商名稱
        initial_model: 初始模型名稱
        model_param: 模型參數名稱
        max_retries: 最大重試次數
        raise_on_fail: 失敗時是否拋出異常
        dry_run: 是否為 Dry Run 模式
        is_async: 函數是否為非同步
    
    Returns:
        函數執行結果或 Dry Run Mock 資料
    """
    # Dry Run 模式: 返回 Mock 資料
    if dry_run:
        logger.info(f"[DRY RUN] 將使用模型: {initial_model} ({provider})")
        return {
            "dry_run": True,
            "provider": provider,
            "model": initial_model,
            "function": func.__name__
        }
    
    # 正常模式: 執行函數並處理重試
    current_model = initial_model
    provider_adapter = get_provider(provider)
    
    # max_retries 表示「失敗後重試次數」，所以總嘗試次數是 max_retries + 2
    # (1次初始 + max_retries次重試 + 1次替代模型重試)
    for attempt in range(1, max_retries + 3):
        try:
            # 注入模型參數
            kwargs[model_param] = current_model
            
            # 執行原函數
            if is_async:
                result = await func(*args, **kwargs)
            else:
                result = func(*args, **kwargs)
            
            # 成功執行,返回結果
            logger.debug(f"成功使用模型 {current_model} 呼叫 {func.__name__}")
            return result
            
        except Exception as e:
            # 記錄捕獲到的異常詳情
            logger.debug(f"[除錯] 捕獲異常 - 類型: {type(e).__name__}, 訊息: {str(e)}")
            
            # 判斷是否為模型失效錯誤
            is_model_error = _is_model_not_found_error(e, provider)
            logger.debug(f"[除錯] 模型失效判斷結果: {is_model_error} (provider={provider})")
            
            if not is_model_error:
                # 不是模型失效錯誤,直接拋出
                logger.debug("[除錯] 非模型失效錯誤，直接拋出異常")
                raise
            
            # 是模型失效錯誤,嘗試查詢替代模型
            logger.warning(f"模型 {current_model} 失效: {e}")
            
            # 嘗試從持久化快取取得替代模型
            cached_replacement = _get_cached_replacement(provider, current_model, cache_ttl)
            if cached_replacement:
                current_model = cached_replacement
                logger.info(f"[快取命中] 切換至 {current_model}")
                continue
            
            # 快取未命中,查詢供應商 API
            logger.debug(f"[除錯] 快取未命中，開始查詢供應商 {provider} 的替代模型")
            try:
                replacement = await provider_adapter.find_replacement(current_model)
                logger.debug(f"[除錯] find_replacement 返回結果: {replacement}")
                
                if replacement:
                    current_model = replacement
                    logger.info(f"[自動替換] {initial_model} → {current_model}")
                    
                    # 儲存到持久化快取
                    _cache_replacement(provider, initial_model, current_model)
                    
                    # 觸發替換通知回呼
                    await _trigger_on_replace(on_replace, provider, initial_model, current_model)
                    
                    # 繼續下一次重試
                    continue
                else:
                    # 無法找到替代模型
                    logger.error(f"無法為 {current_model} 找到替代模型")
                    # 檢查是否已達最大重試次數
                    if attempt >= max_retries:
                        logger.debug(f"[除錯] 已達最大重試次數 {max_retries}")
                        if raise_on_fail:
                            raise ModelReplacementError(
                                f"供應商 {provider} 無提供 {current_model} 的替代模型"
                            ) from e
                        else:
                            raise
                    else:
                        # 還有重試機會但無替代模型，拋出異常結束
                        raise
            except ProviderAPIError as api_error:
                logger.error(f"查詢供應商 API 失敗: {api_error}")
                # 檢查是否已達最大重試次數
                if attempt >= max_retries:
                    logger.debug(f"[除錯] 已達最大重試次數 {max_retries} (API 失敗)")
                    if raise_on_fail:
                        raise
                    else:
                        raise e from api_error
                else:
                    # 還有重試機會但 API 失敗，拋出異常結束
                    raise
    
    # 理論上不會執行到這裡
    raise RuntimeError("未預期的執行路徑")


def _is_model_not_found_error(exception: Exception, provider: str) -> bool:
    """
    判斷異常是否為模型失效錯誤
    
    根據不同供應商的錯誤訊息特徵判斷。
    
    Args:
        exception: 捕獲到的異常
        provider: 供應商名稱
    
    Returns:
        True 表示為模型失效錯誤,False 表示其他錯誤
    """
    # 各供應商的模型失效錯誤標記
    error_indicators = {
        "claude": ["model_not_found", "invalid_model", "not found", "invalid model"],
        "anthropic": ["model_not_found", "invalid_model", "not found", "invalid model"],
        "gemini": ["not found", "invalid model", "does not exist", "model not found", "models/"],
        "google": ["not found", "invalid model", "does not exist", "model not found", "models/"],
        "openai": ["model_not_found", "does not exist", "invalid model", "model not found"],
        "gpt": ["model_not_found", "does not exist", "invalid model", "model not found"],
        "qwen": ["InvalidParameter.Model", "model not found", "invalid model"],
        "alibaba": ["InvalidParameter.Model", "model not found", "invalid model"],
        "mistral": ["model_not_found", "invalid model", "model not found"],
    }
    
    error_msg = str(exception).lower()
    exception_type = type(exception).__name__
    
    # 記錄判斷依據
    logger.debug(f"[除錯] 錯誤訊息: {error_msg[:200]}...")  # 僅記錄前 200 字元
    logger.debug(f"[除錯] 異常類型: {exception_type}")
    
    indicators = error_indicators.get(provider.lower(), [])
    
    # 檢查錯誤訊息是否包含指標關鍵字
    for indicator in indicators:
        if indicator.lower() in error_msg:
            logger.debug(f"[除錯] 匹配到錯誤指標: '{indicator}'")
            return True
    
    # 額外檢查: 某些 SDK 可能拋出 ValueError 或 AttributeError
    if exception_type in ["ValueError", "AttributeError", "KeyError"]:
        # 檢查訊息中是否提到模型
        if "model" in error_msg:
            logger.debug(f"[除錯] 異常類型 {exception_type} + 訊息包含 'model'，判定為模型錯誤")
            return True
    
    logger.debug("[除錯] 未匹配到任何錯誤指標")
    return False


def _get_cached_replacement(
    provider: str, 
    model: str, 
    cache_ttl: Optional[int] = None
) -> Optional[str]:
    """
    從持久化快取取得替代模型
    
    Args:
        provider: 供應商名稱
        model: 失效的模型名稱
        cache_ttl: 自訂 TTL,若未指定則使用預設值
    
    Returns:
        替代模型名稱,若快取未命中或已過期則返回 None
    """
    replacement_cache = get_replacement_cache()
    return replacement_cache.get(provider, model, ttl=cache_ttl)


def _cache_replacement(
    provider: str, 
    failed_model: str, 
    replacement: str
) -> None:
    """
    儲存替代模型映射至持久化快取
    
    Args:
        provider: 供應商名稱
        failed_model: 失效的模型名稱
        replacement: 替代模型名稱
    """
    replacement_cache = get_replacement_cache()
    replacement_cache.set(provider, failed_model, replacement)


async def _trigger_on_replace(
    on_replace: Optional[OnReplaceCallback],
    provider: str,
    old_model: str,
    new_model: str
) -> None:
    """
    觸發替換通知回呼 (支援同步與非同步)
    
    Args:
        on_replace: 通知勾子函數
        provider: 供應商名稱
        old_model: 原始模型名稱
        new_model: 替代模型名稱
    """
    if on_replace is not None:
        try:
            if asyncio.iscoroutinefunction(on_replace):
                await on_replace(provider, old_model, new_model)
            else:
                on_replace(provider, old_model, new_model)
        except Exception as e:
            # 通知失敗不應影響主流程
            logger.warning(f"[通知] on_replace 回呼執行失敗: {e}")

