#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
services/llm_caller_registry.py
Operation Ollama-First v5.0 / Phase 16 — caller 名稱集中註冊（critic-A11 L4 修補）

問題：caller 命名分散在 ai_call_logger 註解 / migrations/024 SQL 註解 /
各 service hardcode，三層不一致時報表會看到鬼影（gemini / Gemini / gemini-flash）。

修補：本檔為 single source of truth，ai_call_logger 啟動時驗證；
service 寫死 caller 字串若不在 registry → log warning（不 raise，保留擴展彈性）。

依 ADR-028 caller 白名單（30+ 個 caller）。
"""

from __future__ import annotations
import logging

logger = logging.getLogger(__name__)


# ─────────────────────────────────────────────────────────────────────────────
# Caller 白名單（按服務分組，與 ai_call_logger 註解 + migration 024 對齊）
# ─────────────────────────────────────────────────────────────────────────────
CALLER_REGISTRY: frozenset = frozenset({
    # Hermes 競價分析（hermes_analyst_service）
    'hermes_analyst',           # _call_hermes_batch
    'hermes_intent',            # intent_classify (L1 NLP)
    'hermes_ea_prefetch',       # EA HITL pre-fetch (ADR-021)

    # KM Embedding（openclaw_learning_service）
    'km_embedding_worker',      # 60s retry queue worker
    'km_embedding_realtime',    # _build_semantic_rag_context

    # AiderHeal（aider_heal_executor）
    'aider_heal',               # SSH CLI 跑 Aider（暫不接 logger）

    # MCP Collector（mcp_collector_service）
    'mcp_l1_grounding',         # Gemini 2.0 Flash Grounding
    'mcp_l2_grounding',         # Gemini 1.5 Flash Grounding
    'mcp_l3_ollama',            # Ollama 知識庫兜底
    'mcp_collector',            # Phase 10.5 omnisearch 入口

    # OpenClaw 戰略（openclaw_strategist_service）
    'openclaw_daily',           # 每日報告
    'openclaw_daily_gemini_fallback',
    'openclaw_daily_nim',
    'openclaw_weekly',          # 週一 06:00
    'openclaw_weekly_gemini_fallback',
    'openclaw_weekly_nim',
    'openclaw_monthly',         # 每月 1 日
    'openclaw_monthly_gemini_fallback',
    'openclaw_monthly_nim',
    'openclaw_meta',            # Meta 自審 12:00
    'openclaw_meta_gemini_fallback',
    'openclaw_meta_nim',
    'openclaw_qa',              # Telegram Q&A
    'openclaw_qa_gemini_fallback',  # Telegram Q&A Gemini fallback
    'openclaw_qa_nim',          # Telegram Q&A NIM fallback
    'openclaw_daily_insight',   # Phase 3 A8 拆分後的 200 字 Ollama-first 洞察
    'openclaw_daily_insight_gemini_fallback',
    'openclaw_daily_insight_nim',
    'openclaw_strategist',      # Phase 10.5 mcp_router caller

    # NemoTron 派遣（nemoton_dispatcher_service）
    'nemotron_dispatch',        # NIM 8B 主路徑 / qwen3 主路徑

    # Code Review（code_review_pipeline_service）
    'code_review_hermes',       # Step 2 Hermes 掃描
    'code_review_openclaw',     # Step 3 OpenClaw 評估（Ollama-first；Claude optional fallback）
    'code_review_elephant',     # Step 4 ElephantAlpha 49B
    'code_review_openclaw_gemini',  # Ollama/Claude 失敗後的 Gemini fallback

    # ElephantAlpha（elephant_alpha_*）
    'ea_engine',                # _execute_autonomous_decision (Gemini orchestrate)

    # PPT 簡報（routes/openclaw_bot_routes）
    'ppt_gemini',               # Gemini Flash 主分析
    'ppt_ollama',               # Ollama 失敗 fallback
    'ppt_nim',                  # NIM deepseek-v3.2 主分析
    'ppt_vision',               # Phase 14 PPT 視覺檢查（qwen2-vl）

    # Sales / Trend（routes/ai_routes + routes/trend_routes）
    'sales_copy',               # 文案生成
    'trend_match',              # 商品比對
    'trend_qa',                 # Web Search Q&A
    'product_insights',         # 商品洞察
    'trend_keywords',           # 趨勢關鍵字

    # Telegram Bot
    'tg_bot_copy',              # /copy 文案
    'tg_bot_copy_v2',           # second copy entrance
    'openclaw_bot_main',        # OpenClaw Bot 主鏈 Ollama
    'openclaw_bot_gemini',      # Bot Gemini fallback
    'openclaw_bot_image',       # Bot 圖片商品辨識 Ollama-first
    'openclaw_bot_image_gemini',  # Bot 圖片商品辨識 Gemini fallback
    'openclaw_bot_nim',         # Bot NIM fallback

    # 其他
    'bot_api_copy',             # bot_api_routes
    'trend_crawler',            # trend_crawler_service
    'ai_provider_generic',      # ai_provider 抽象層
})


def is_known_caller(caller: str) -> bool:
    """檢查 caller 是否在白名單"""
    return caller in CALLER_REGISTRY


def assert_known_caller(caller: str, strict: bool = False) -> None:
    """ai_call_logger 啟動時或寫入時驗證。

    Args:
        caller: 待驗證的 caller 名
        strict: True → 不在 registry 時 raise；False（預設）→ 只 log warning

    依 ADR-028：新增 caller 必須先入 ADR + registry，再上 commit。
    """
    if not is_known_caller(caller):
        msg = f"unknown caller: {caller!r} not in CALLER_REGISTRY (ADR-028)"
        if strict:
            raise ValueError(msg)
        logger.warning(f"[CallerRegistry] {msg} — see services/llm_caller_registry.py")


def list_callers_by_service() -> dict:
    """除錯/文件用：分組列出所有合法 caller"""
    return {
        'hermes':             [c for c in CALLER_REGISTRY if c.startswith('hermes_')],
        'openclaw':           [c for c in CALLER_REGISTRY if c.startswith('openclaw_') and not c.startswith('openclaw_bot_')],
        'openclaw_bot':       [c for c in CALLER_REGISTRY if c.startswith('openclaw_bot_')],
        'mcp':                [c for c in CALLER_REGISTRY if c.startswith('mcp_')],
        'code_review':        [c for c in CALLER_REGISTRY if c.startswith('code_review_')],
        'ppt':                [c for c in CALLER_REGISTRY if c.startswith('ppt_')],
        'tg_bot':             [c for c in CALLER_REGISTRY if c.startswith('tg_bot_')],
        'km_embedding':       [c for c in CALLER_REGISTRY if c.startswith('km_embedding_')],
        'sales_trend':        ['sales_copy', 'trend_match', 'trend_qa',
                                'product_insights', 'trend_keywords'],
        'misc':               ['ea_engine', 'aider_heal', 'nemotron_dispatch',
                                'bot_api_copy', 'trend_crawler', 'ai_provider_generic'],
    }


__all__ = [
    'CALLER_REGISTRY',
    'is_known_caller',
    'assert_known_caller',
    'list_callers_by_service',
]
