"""Hermes 自然語言閘道 — ADR-094

Layer 1 意圖路由（關鍵字正則）→ Ollama 本地模型（111）→ Telegram 格式化輸出。

2026-04-24 Claude Sonnet 4.6 (WS4 Hermes NL)
2026-04-24 Claude Sonnet 4.6 (WS4 Hermes NL T1+T2+T3): hermes_dispatch_log DB 寫入 /
  Redis per-chat_id 速率限制 / Multi-turn session (Redis Hash TTL=300s)
2026-04-25 Claude Sonnet 4.6: 改用 Ollama 本地模型（111），按 agent 類型選模型，零費用
  debugger/vuln → deepseek-r1:14b（推理）; code agents → qwen2.5-coder:7b; 其他 → qwen2.5:7b-instruct
"""
from __future__ import annotations

import asyncio
import re
import time

import httpx
import structlog
from sqlalchemy import text

from src.core.redis_client import get_redis
from src.db.base import get_db_context
from src.hermes.agent_loader import get_agent_system_prompt
from src.hermes.display_names import DEFAULT_AGENT, format_response_header
from src.hermes.safety_hooks import is_dangerous_input, is_mutate_intent
from src.services.ollama_endpoint_resolver import resolve_ollama_order

logger = structlog.get_logger(__name__)

# ─────────────────────────────────────────────────────────────────────────────
# Layer 1 意圖路由（關鍵字正則，<10ms）
# ─────────────────────────────────────────────────────────────────────────────
_ROUTING_RULES: list[tuple[re.Pattern, str]] = [
    (re.compile(r"(資料庫|postgres|sql|index|query|migration|schema)", re.IGNORECASE), "db-expert"),
    (re.compile(r"(漏洞|CVE|injection|XSS|CSRF|security|安全)", re.IGNORECASE), "vuln-verifier"),
    (re.compile(r"(bug|crash|error|exception|fail|失敗|崩潰|為什麼壞|不通)", re.IGNORECASE), "debugger"),
    (re.compile(r"(重構|refactor|clean|重寫)", re.IGNORECASE), "refactor-specialist"),
    (re.compile(r"(升級|upgrade|migration|migrate|版本)", re.IGNORECASE), "migration-engineer"),
    (re.compile(r"(設計|UI|frontend|頁面|按鈕|樣式)", re.IGNORECASE), "frontend-designer"),
    (re.compile(r"(工具|tool|hook|MCP|plugin)", re.IGNORECASE), "tool-expert"),
    (re.compile(r"(文件|document|官方|API spec|how to)", re.IGNORECASE), "web-researcher"),
    (re.compile(r"(導覽|介紹|架構|codebase|overview)", re.IGNORECASE), "onboarder"),
    (re.compile(r"(拆解|任務|plan|規劃)", re.IGNORECASE), "planner"),
    (re.compile(r"(審查|review|code review|找問題)", re.IGNORECASE), "critic"),
    (re.compile(r"(實作|implement|develop|功能|feature)", re.IGNORECASE), "fullstack-engineer"),
]

# ─────────────────────────────────────────────────────────────────────────────
# T2：速率限制常數（ADR-094）
# ─────────────────────────────────────────────────────────────────────────────
_RATE_LIMIT_MAX = 20
_RATE_LIMIT_WINDOW_SEC = 60

# ─────────────────────────────────────────────────────────────────────────────
# Ollama 模型路由（按 agent 專業選最適模型，111 主機）
# ─────────────────────────────────────────────────────────────────────────────
_MODEL_BY_AGENT: dict[str, str] = {
    # 推理型（找根因 / 安全分析）→ deepseek-r1:14b（CoT 推理）
    "debugger":           "deepseek-r1:14b",
    "vuln-verifier":      "deepseek-r1:14b",
    # 程式碼 + 通用（review / 實作 / 重構 / DB / 前端 / 工具 / 規劃 / 文件）→ qwen3:8b
    # 2026-04-25 ogt + Claude Sonnet 4.6: qwen2.5-coder:7b + qwen2.5:7b-instruct → qwen3:8b
    #   qwen3:8b Hybrid Thinking 同時勝任程式碼與指令；gemma4 尚未在 Ollama 釋出
    "critic":             "qwen3:8b",
    "db-expert":          "qwen3:8b",
    "fullstack-engineer": "qwen3:8b",
    "refactor-specialist":"qwen3:8b",
    "migration-engineer": "qwen3:8b",
    "frontend-designer":  "qwen3:8b",
    "tool-expert":        "qwen3:8b",
    "planner":            "qwen3:8b",
    "onboarder":          "qwen3:8b",
    "web-researcher":     "qwen3:8b",
}
_DEFAULT_MODEL = "deepseek-r1:14b"
_OLLAMA_TIMEOUT = 90.0  # deepseek-r1:14b 推理較慢，給 90s


def _pick_model(agent_name: str) -> str:
    return _MODEL_BY_AGENT.get(agent_name, _DEFAULT_MODEL)


def _strip_think_tags(text: str) -> str:
    """移除 deepseek-r1 的 <think>...</think> 內部推理塊，只留最終回答。"""
    return re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()


def _route_intent_layer1(msg: str) -> str:
    """Layer 1: 關鍵字正則路由，回傳 agent 名稱"""
    for pattern, agent in _ROUTING_RULES:
        if pattern.search(msg):
            return agent
    return DEFAULT_AGENT


# ─────────────────────────────────────────────────────────────────────────────
# T1：hermes_dispatch_log DB 寫入（ADR-094，非阻擋）
# ─────────────────────────────────────────────────────────────────────────────

async def _write_dispatch_log(
    *,
    chat_id: str,
    user_id: int,
    username: str,
    agent_name: str,
    input_preview: str,
    latency_ms: int,
    success: bool,
    error_type: str | None,
) -> None:
    """寫入派發審計日誌；失敗只 warning，不影響主流程。"""
    try:
        async with get_db_context() as db:
            await db.execute(
                text("""
                    INSERT INTO hermes_dispatch_log
                      (chat_id, user_id, username, agent_name, input_preview,
                       latency_ms, success, error_type)
                    VALUES
                      (:chat_id, :user_id, :username, :agent_name, :input_preview,
                       :latency_ms, :success, :error_type)
                """),
                {
                    "chat_id": chat_id,
                    "user_id": user_id,
                    "username": username,
                    "agent_name": agent_name,
                    "input_preview": input_preview,
                    "latency_ms": latency_ms,
                    "success": success,
                    "error_type": error_type,
                },
            )
            await db.commit()
    except Exception as exc:
        logger.warning("hermes_dispatch_log_write_failed", error=str(exc))


# ─────────────────────────────────────────────────────────────────────────────
# T2：per-chat_id 速率限制（ADR-094，fail-open）
# ─────────────────────────────────────────────────────────────────────────────

async def _check_rate_limit(chat_id: str, project_id: str = "awoooi") -> bool:
    """True = 允許；False = 超過限制（20 req/min per chat_id）。Redis 不可用時放行。"""
    try:
        redis = get_redis()
        key = f"{project_id}:hermes:rl:{chat_id}"
        count = await redis.incr(key)
        if count == 1:
            await redis.expire(key, _RATE_LIMIT_WINDOW_SEC)
        return count <= _RATE_LIMIT_MAX
    except Exception:
        return True  # Redis 不可用 → fail open


# ─────────────────────────────────────────────────────────────────────────────
# T3：Multi-turn session（Redis Hash TTL=300s，ADR-094）
# ─────────────────────────────────────────────────────────────────────────────

async def _load_session_context(chat_id: str, user_id: int, project_id: str = "awoooi") -> str:
    """載入最近 3 輪對話歷史（最多 600 字），組成 context prefix。Redis 不可用時回空字串。"""
    try:
        redis = get_redis()
        key = f"{project_id}:hermes:session:{chat_id}:{user_id}"
        data = await redis.hgetall(key)
        if not data:
            # Phase A: fallback 到舊 key（滾動部署相容）
            data = await redis.hgetall(f"hermes:session:{chat_id}:{user_id}")
        if not data:
            return ""
        turns = sorted(
            [(k, v) for k, v in data.items() if (k if isinstance(k, str) else k.decode()).startswith("turn_")],
            key=lambda x: x[0],
        )[-3:]
        parts = [v.decode() if isinstance(v, bytes) else v for _, v in turns]
        return "【近期對話記錄】\n" + "\n".join(parts) + "\n\n"
    except Exception:
        return ""


async def _save_session_turn(
    chat_id: str, user_id: int, user_msg: str, assistant_reply: str, project_id: str = "awoooi"
) -> None:
    """將本輪對話存入 Redis Hash，並重置 TTL=300s。Redis 不可用時靜默忽略。"""
    try:
        redis = get_redis()
        key = f"{project_id}:hermes:session:{chat_id}:{user_id}"
        legacy_key = f"hermes:session:{chat_id}:{user_id}"  # Phase A dual-write
        turn_key = f"turn_{int(time.time())}"
        value = f"用戶：{user_msg[:100]}\nHermes：{assistant_reply[:200]}"
        await redis.hset(key, turn_key, value)
        await redis.expire(key, 300)
        await redis.hset(legacy_key, turn_key, value)
        await redis.expire(legacy_key, 300)
    except Exception:
        pass


# ─────────────────────────────────────────────────────────────────────────────
# 主入口
# ─────────────────────────────────────────────────────────────────────────────

async def process_nl_message(
    user_message: str,
    *,
    chat_id: str,
    user_id: int,
    username: str = "",
    project_id: str = "awoooi",
) -> str:
    """
    處理 NL 訊息，回傳 Telegram 格式的回覆文字。

    流程：
    1. 安全守門（DENY + MUTATE）
    2. T2 速率限制（20 req/min per chat_id）
    3. Layer 1 關鍵字路由 → agent_name
    4. 讀取 agent system prompt（.claude/agents/*.md）
    5. T3 載入 session context（最近 3 輪）
    6. 呼叫 Claude Agent SDK query()
    7. T3 儲存本輪對話
    8. 格式化為 Telegram MarkdownV2 訊息
    9. T1 非阻擋寫入 hermes_dispatch_log
    """
    # 安全守門
    if is_dangerous_input(user_message):
        logger.warning(
            "hermes_nl_dangerous_input",
            user_id=user_id,
            chat_id=chat_id,
            preview=user_message[:80],
        )
        return "⛔ 偵測到危險指令，拒絕處理。"

    if is_mutate_intent(user_message):
        return (
            "⚠️ 此操作涉及變更，需透過正式審批流程執行。\n"
            "請在 Telegram 告警卡片上操作，或聯繫值班 SRE。"
        )

    # T2：速率限制
    if not await _check_rate_limit(chat_id, project_id):
        return "⚠️ 請求太頻繁，請稍後再試（每分鐘上限 20 次）。"

    # Layer 1 意圖路由
    agent_name = _route_intent_layer1(user_message)

    # 確認 agent 存在，否則 fallback
    system_prompt = get_agent_system_prompt(agent_name)
    if system_prompt is None:
        logger.warning(
            "hermes_nl_agent_not_found",
            agent=agent_name,
            fallback=DEFAULT_AGENT,
        )
        agent_name = DEFAULT_AGENT
        system_prompt = get_agent_system_prompt(agent_name) or ""

    # T3：載入 session context（最近 3 輪）
    session_ctx = await _load_session_context(chat_id, user_id, project_id)
    prompt_with_ctx = f"{session_ctx}{user_message}" if session_ctx else user_message

    t0 = time.monotonic()

    # 呼叫 Ollama 模型（GCP-A → GCP-B → 111，零費用，按 agent 選模型）
    model = _pick_model(agent_name)
    success = False
    error_type: str | None = None
    result_text = ""
    async with httpx.AsyncClient(timeout=_OLLAMA_TIMEOUT) as _hc:
        for endpoint in resolve_ollama_order("hermes"):
            if not endpoint.url:
                continue
            try:
                resp = await _hc.post(
                    f"{endpoint.url}/api/chat",
                    json={
                        "model": model,
                        # Keep Hermes responses in message.content across Ollama 0.24+.
                        "think": False,
                        "messages": [
                            {"role": "system", "content": system_prompt},
                            {"role": "user",   "content": prompt_with_ctx},
                        ],
                        "stream": False,
                        "options": {"num_predict": 1500, "temperature": 0.3},
                    },
                )
                resp.raise_for_status()
                result_text = resp.json().get("message", {}).get("content", "")
                result_text = _strip_think_tags(result_text)
                if not result_text:
                    result_text = "_Agent 回應為空，請稍後再試。_"
                success = True
                break
            except Exception as exc:
                error_type = type(exc).__name__
                logger.error(
                    "hermes_nl_ollama_error",
                    error=str(exc),
                    agent=agent_name,
                    model=model,
                    provider=endpoint.provider_name,
                    exc_type=error_type,
                )
    if not success:
        result_text = f"_Hermes 暫時無法連線（{error_type}），請稍後再試。_"

    latency_ms = int((time.monotonic() - t0) * 1000)
    logger.info(
        "hermes_nl_dispatch",
        agent=agent_name,
        model=model,
        user_id=user_id,
        chat_id=chat_id,
        username=username,
        latency_ms=latency_ms,
        success=success,
    )

    # T3：儲存本輪對話（只在成功時存）
    if success:
        await _save_session_turn(chat_id, user_id, user_message, result_text, project_id)

    # T1：非阻擋寫入 hermes_dispatch_log（失敗不影響回覆）
    asyncio.create_task(
        _write_dispatch_log(
            chat_id=chat_id,
            user_id=user_id,
            username=username,
            agent_name=agent_name,
            input_preview=user_message[:200],
            latency_ms=latency_ms,
            success=success,
            error_type=error_type,
        )
    )

    header = format_response_header(agent_name)
    # Telegram 訊息上限 4096 字元，超過截斷
    body = result_text[:3800]
    return f"{header}{body}"
