"""
Webhook API - 外部告警接收 (OpenClaw Integration)
==================================================
Phase 5: OpenClaw 實體化升級
CAI-201: AWOOOI 核心大腦 Webhook 入口
戰略 B: 告警風暴收斂與成本控制

Phase 6.1: Event Bus (Redis Streams)
- POST /api/v1/webhooks/signals - 輕量級訊號接收 (直接進 Redis Stream)

Endpoints:
- POST /api/v1/webhooks/alerts - 接收外部系統告警 (含 HMAC 驗證)

流程 (Phase 5: OpenClaw + HMAC 安全):
1. HMAC 簽章驗證 (CISO 要求)
2. 接收告警 (K8s, Prometheus, etc.)
3. 生成告警指紋 (namespace:deployment:alert_type Hash)
4. 查詢 DB 是否有同指紋 pending 或 5 分鐘內的記錄
5. [收斂] 如果有：hit_count +1，跳過 LLM，節省成本！
6. [新告警] 如果沒有：觸發 OpenClaw LLM 分析
7. 建立/更新 ApprovalRecord
8. 前端戰情室即時顯示聚合次數
"""

import asyncio
import hashlib
import hmac
import uuid
from typing import Literal

from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status
from pydantic import BaseModel, Field

from src.core.config import settings
from src.core.constants import is_cicd_alertname, is_heartbeat_alertname
from src.core.logging import get_logger
from src.core.metrics import record_alert_chain_success
from src.models.approval import (
    ApprovalRequestCreate,
    BlastRadius,
    DataImpact,
    DryRunCheck,
    RiskLevel,
)
from src.models.webhook import AlertPayload, AlertResponse
from src.services.action_parser import is_safe_kubectl_action
from src.services.alert_analyzer_service import AlertAnalyzer
from src.services.alert_approval_guard import guard_alert_approval_action
from src.services.alert_grouping_service import get_alert_grouping_service
from src.services.alert_rule_engine import get_incident_type, match_rule
from src.services.alertmanager_llm_guard import (
    ALERTMANAGER_LLM_INFLIGHT_LOCK_TTL_SECONDS,
    try_acquire_alertmanager_llm_lock,
)
from src.services.approval_db import get_approval_service
from src.services.auto_approve import get_auto_approve_policy
from src.services.auto_repair_service import AutoRepairService
from src.services.channel_hub import (
    record_alertmanager_event,
    record_grouped_alert_event,
)
from src.services.converged_alert_recurrence_notifier import (
    notify_converged_alert_recurrence,
)

# Phase 15.2: Trace Context (moved to SignalProducerService)
# get_trace_context 已移至 Service 層

# R4 #129 (2026-04-01 ogt): AlertPayload/AlertResponse 移至 models 層，AlertAnalyzer 移至 services 層
# ogt 更新 v1.1 2026-04-01 台北時間: generate_alert_fingerprint 移至 alert_analyzer_service (ADR-024)
# [首席架構師] 移除 generate_alert_fingerprint 直接 import，改用 AlertAnalyzer.generate_fingerprint v1.2 2026-04-01 Asia/Taipei

# Phase 17 P0: Service 層 (消除 Router 直接存取 Redis)
# C2 修正 (首席架構師審查 2026-04-10): create_incident_for_approval + extract_affected_services 已移入 Service 層
from src.services.incident_service import (
    classify_alert_early,
    create_incident_for_approval,
    get_incident_service,
)

# Phase 5: OpenClaw AI Engine
from src.services.openclaw import get_openclaw
from src.services.playbook_match_resolver import resolve_playbook_id_for_alert
from src.services.repair_candidate_service import get_repair_candidate_service
from src.services.security_interceptor import check_webhook_nonce  # P0-06: nonce dedup via Service 層
from src.services.signal_producer import SignalData, get_signal_producer

# Phase 5: Telegram Gateway (行動戰情室)
from src.services.telegram_gateway import TelegramGatewayError, get_telegram_gateway

# Phase 18.1.7: K8s 資源名稱正規化 已移至 alert_analyzer_service (R4 #129)
from src.utils.timezone import now_taipei

router = APIRouter(prefix="/webhooks", tags=["Webhooks"])
logger = get_logger("awoooi.webhooks")


# =============================================================================
# Incident-Approval 同步 (feedback_incident_approval_sync.md 鐵律)
# =============================================================================

# C2 修正 (首席架構師審查 2026-04-10 Claude Sonnet 4.6 Asia/Taipei):
# RISK_TO_SEVERITY / INCIDENT_TTL_SECONDS / _extract_affected_services / create_incident_for_approval
# 已移入 src/services/incident_service.py（業務邏輯不屬 Router 層）


# =============================================================================
# 2026-04-05 ogt: 自動修復背景任務 (ADR-058 閉環)
# =============================================================================


def _should_bypass_alertmanager_llm(
    rule_response: dict | None,
    alert_category: str,
) -> bool:
    """主機/備份類告警命中 YAML NO_ACTION 時，直接走人工排查卡片。"""
    return (
        rule_response is not None
        and rule_response.get("suggested_action") == "NO_ACTION"
        and not str(rule_response.get("kubectl_command", "")).strip()
        and rule_response.get("rule_id", "") not in ("generic_fallback", "")
        and alert_category in {"host_resource", "backup_failure"}
    )


def _should_use_alertmanager_rule_first(
    rule_response: dict | None,
    alert_category: str,
) -> bool:
    """主機/備份類告警命中權威 YAML 規則時，避免再被 LLM 污染成 K8s 動作。"""

    if not rule_response or alert_category not in {"host_resource", "backup_failure"}:
        return False
    if rule_response.get("rule_id", "") in ("generic_fallback", ""):
        return False

    suggested_action = str(rule_response.get("suggested_action", "")).upper()
    command = str(rule_response.get("kubectl_command", "")).strip().lower()
    return (
        suggested_action == "NO_ACTION"
        or suggested_action.startswith("SSH_")
        or command.startswith("ssh ")
    )


async def _analyze_alertmanager_with_timeout(
    openclaw,
    alert_context: dict,
    *,
    alert_id: str,
    alertname: str,
) -> tuple:
    """Run Alertmanager AI analysis without letting it block the workflow forever."""

    try:
        return await asyncio.wait_for(
            openclaw.analyze_alert(alert_context),
            timeout=ALERTMANAGER_BACKGROUND_AI_TIMEOUT_SECONDS,
        )
    except TimeoutError:
        logger.warning(
            "alertmanager_openclaw_timeout_fallback",
            alert_id=alert_id,
            alertname=alertname,
            timeout_sec=ALERTMANAGER_BACKGROUND_AI_TIMEOUT_SECONDS,
        )
        return None, "fallback_timeout", "", None, "", 0, 0.0
    except Exception as exc:
        logger.warning(
            "alertmanager_openclaw_failed_fallback",
            alert_id=alert_id,
            alertname=alertname,
            error=str(exc),
        )
        return None, "fallback_error", "", None, "", 0, 0.0


async def _escalate_auto_repair_unavailable(
    *,
    incident_id: str,
    approval_id: str,
    alert_type: str,
    target_resource: str,
    namespace: str,
    failure_reason: str,
    attempted_actions: str,
) -> None:
    """Delegate automation-blocker escalation to the service layer."""

    from src.services.emergency_escalation_service import (
        escalate_auto_repair_unavailable,
    )

    await escalate_auto_repair_unavailable(
        incident_id=incident_id,
        approval_id=approval_id,
        alert_type=alert_type,
        target_resource=target_resource,
        namespace=namespace,
        failure_reason=failure_reason,
        attempted_actions=attempted_actions,
    )


def _auto_repair_action_label(result, fallback_target: str) -> str:
    """Build a verifier label that includes the actual playbook steps."""
    playbook_id = getattr(result, "playbook_id", None) or "unknown"
    steps = getattr(result, "executed_steps", None) or []
    step_text = " | ".join(str(step) for step in steps).strip()
    if not step_text:
        step_text = fallback_target
    step_text = " ".join(step_text.split())
    if len(step_text) > 240:
        step_text = f"{step_text[:237]}..."
    return f"auto_repair_playbook:{playbook_id} {step_text}".strip()


async def _try_auto_repair_background(
    incident_id: str,
    approval_id: str,
    alert_type: str,
    target_resource: str,
    namespace: str,
) -> None:
    """
    背景評估並執行自動修復

    流程:
    1. 重新載入 Incident
    2. evaluate_auto_repair() — 只保留 P0/P1 嚴重度阻擋 (統帥指令: 直接全部自動修復)
    3. 可修復 → execute_auto_repair() 執行
    4. 不可修復 → 靜默，等人工批准
    所有步驟都寫入 alert_operation_log
    """
    from src.repositories.alert_operation_log_repository import get_alert_operation_log_repository
    op_log = get_alert_operation_log_repository()

    try:
        incident_service = get_incident_service()
        incident = await incident_service.get_from_working_memory(incident_id)
        if not incident:
            logger.warning("auto_repair_incident_not_found", incident_id=incident_id)
            return

        repair_service = AutoRepairService()
        decision = await repair_service.evaluate_auto_repair(incident)

        logger.info(
            "auto_repair_decision",
            incident_id=incident_id,
            approval_id=approval_id,
            can_auto_repair=decision.can_auto_repair,
            reason=decision.reason,
            blocked_by=decision.blocked_by,
        )

        if not decision.can_auto_repair:
            # 記錄評估被阻擋
            await op_log.append(
                "AUTO_REPAIR_TRIGGERED",
                incident_id=incident_id,
                approval_id=approval_id,
                actor="auto_repair",
                action_detail=f"blocked:{decision.blocked_by}",
                success=False,
                error_message=decision.reason,
                context={
                    "blocked_by": decision.blocked_by,
                    "reason": decision.reason,
                    "playbook_id": decision.playbook.playbook_id if decision.playbook else None,
                },
            )
            await _escalate_auto_repair_unavailable(
                incident_id=incident_id,
                approval_id=approval_id,
                alert_type=alert_type,
                target_resource=target_resource,
                namespace=namespace,
                failure_reason=decision.reason,
                attempted_actions=f"evaluate_auto_repair -> blocked:{decision.blocked_by}",
            )
            return

        # 記錄自動修復觸發 (Sprint 5.1 Q10: 加入 Langfuse trace_id 追蹤)
        # (2026-04-08 Claude Sonnet 4.6 Asia/Taipei，ADR-062)
        _langfuse_trace_id = getattr(incident, "langfuse_trace_id", None)
        await op_log.append(
            "AUTO_REPAIR_TRIGGERED",
            incident_id=incident_id,
            approval_id=approval_id,
            actor="auto_repair",
            action_detail=decision.playbook.name if decision.playbook else "unknown",
            success=True,
            context={
                "playbook_id": decision.playbook.playbook_id,
                "playbook_name": decision.playbook.name,
                "similarity_score": decision.similarity_score,
                "risk_level": decision.risk_level.value if decision.risk_level else None,
                "langfuse_trace_id": _langfuse_trace_id,
                "langfuse_url": (
                    f"{settings.LANGFUSE_URL}/trace/{_langfuse_trace_id}"
                    if _langfuse_trace_id else None
                ),
            },
        )

        _pre_execution_snapshot = None
        try:
            from src.core.feature_flags import aiops_flags

            if aiops_flags.is_sub_flag_enabled("AIOPS_P1_PRE_DECISION_INVESTIGATOR"):
                from src.services.evidence_snapshot import get_latest_snapshot
                from src.services.post_execution_verifier import get_post_execution_verifier

                _pre_execution_snapshot = await get_latest_snapshot(incident_id)
                if _pre_execution_snapshot is None:
                    from src.services.pre_decision_investigator import (
                        get_pre_decision_investigator,
                    )

                    _pre_execution_snapshot = await asyncio.wait_for(
                        get_pre_decision_investigator().investigate(incident),
                        timeout=60.0,
                    )
                if _pre_execution_snapshot is not None:
                    await asyncio.wait_for(
                        get_post_execution_verifier().capture_pre_execution_state(
                            incident,
                            _pre_execution_snapshot,
                        ),
                        timeout=30.0,
                    )
        except asyncio.TimeoutError:
            logger.warning(
                "auto_repair_pre_state_capture_timeout",
                incident_id=incident_id,
                approval_id=approval_id,
            )
        except Exception as _pre_state_err:
            logger.warning(
                "auto_repair_pre_state_capture_failed",
                incident_id=incident_id,
                approval_id=approval_id,
                error=str(_pre_state_err),
            )

        # 執行自動修復
        logger.info(
            "auto_repair_executing",
            incident_id=incident_id,
            playbook_id=decision.playbook.playbook_id if decision.playbook else None,
        )
        result = await repair_service.execute_auto_repair(
            incident=incident,
            playbook=decision.playbook,
            is_cold_start=decision.is_cold_start,
            similarity_score=decision.similarity_score,
            run_post_verification=False,
        )

        logger.info(
            "auto_repair_result",
            incident_id=incident_id,
            success=result.success if result else False,
        )

        # 記錄執行結果
        if result:
            try:
                await get_approval_service().update_execution_status(
                    approval_id=approval_id,
                    success=result.success,
                    error_message=result.error,
                )
            except Exception as _approval_status_err:
                logger.warning(
                    "auto_repair_approval_status_update_failed",
                    approval_id=approval_id,
                    incident_id=incident_id,
                    error=str(_approval_status_err),
                )

            await op_log.append(
                "EXECUTION_COMPLETED",
                incident_id=incident_id,
                approval_id=approval_id,
                actor="auto_repair",
                action_detail=f"playbook:{result.playbook_id}",
                success=result.success,
                error_message=result.error,
                context={
                    "playbook_id": result.playbook_id,
                    "steps_count": len(result.executed_steps),
                    "execution_time_ms": result.execution_time_ms,
                    "alert_type": alert_type,
                    "target_resource": target_resource,
                    "namespace": namespace,
                },
            )
            if not result.success:
                await _escalate_auto_repair_unavailable(
                    incident_id=incident_id,
                    approval_id=approval_id,
                    alert_type=alert_type,
                    target_resource=target_resource,
                    namespace=namespace,
                    failure_reason=result.error or "auto repair execution failed",
                    attempted_actions=f"execute_auto_repair playbook:{result.playbook_id}",
                )

        # ADR-073 Phase 2-3: 自動修復結果 → 寫入 Incident outcome (2026-04-12 ogt)
        # 讓 KMConversionService 可依 outcome 判斷是否為 EXECUTION_SUCCESS
        if result:
            _effectiveness = 5 if result.success else 2
            _feedback = (
                f"auto_repair:{result.playbook_id}:success"
                if result.success
                else f"auto_repair:{result.playbook_id}:failed:{result.error}"
            )
            try:
                await incident_service.update_outcome(
                    incident_id=incident_id,
                    effectiveness_score=_effectiveness,
                    human_feedback=_feedback,
                    should_remember=result.success,
                )
            except Exception as _outcome_err:
                logger.warning("auto_repair_outcome_write_failed", error=str(_outcome_err))

        # 2026-04-25 P1.3+P1.4 by Claude Engineer-B2 — 飛輪閉環最後一哩
        # auto_repair 路徑補 PostExecutionVerifier 呼叫 + learning 接線
        # 人工審核路徑已在 approval_execution._run_post_execution_verify 接線，
        # 此處補齊 auto_repair 路徑的對稱接線（ADR-081 Phase 1 + ADR-083 Phase 3）
        _post_verify_result: str | None = None
        if result:
            from src.core.feature_flags import aiops_flags
            if aiops_flags.is_sub_flag_enabled("AIOPS_P1_POST_EXECUTION_VERIFIER"):
                try:
                    from src.services.post_execution_verifier import get_post_execution_verifier
                    # 2026-04-26 critic-B2 hotfix by Claude Opus 4.7
                    # get_latest_snapshot 是 module-level async function，不是 EvidenceSnapshot classmethod
                    # 原 EvidenceSnapshot.get_latest_snapshot(...) 會 raise AttributeError
                    from src.services.evidence_snapshot import get_latest_snapshot
                    from src.services.learning_service import get_learning_service

                    _snapshot = _pre_execution_snapshot or await get_latest_snapshot(incident_id)
                    _action_label = _auto_repair_action_label(
                        result,
                        fallback_target=f"{target_resource}:{namespace}",
                    )
                    _verifier = get_post_execution_verifier()
                    _verify_result = await asyncio.wait_for(
                        _verifier.verify(
                            incident=incident,
                            snapshot=_snapshot,
                            action_taken=_action_label,
                        ),
                        timeout=60.0,
                    )
                    _post_verify_result = _verify_result
                    logger.info(
                        "auto_repair_post_verify_complete",
                        incident_id=incident_id,
                        approval_id=approval_id,
                        verification_result=_verify_result,
                        playbook_id=result.playbook_id,
                    )
                    await get_learning_service().record_verification_result(
                        incident_id=incident_id,
                        action_taken=_action_label,
                        verification_result=_verify_result,
                        matched_playbook_id=result.playbook_id,
                    )
                except asyncio.TimeoutError:
                    logger.warning(
                        "auto_repair_post_verify_timeout",
                        incident_id=incident_id,
                        approval_id=approval_id,
                        timeout_sec=60.0,
                    )
                except Exception as _verify_err:
                    logger.warning(
                        "auto_repair_post_verify_failed",
                        incident_id=incident_id,
                        approval_id=approval_id,
                        error=str(_verify_err),
                    )

        # ADR-073 修補: 自動修復成功 → 解決 Incident → 觸發 KM 轉換
        # 之前 auto_repair 成功後從未呼叫 resolve_incident，KM 永遠不生成
        if result and result.success:
            if _post_verify_result in ("failed", "degraded", "timeout"):
                logger.warning(
                    "incident_resolve_after_auto_repair_skipped_verification",
                    incident_id=incident_id,
                    approval_id=approval_id,
                    verification_result=_post_verify_result,
                    playbook_id=result.playbook_id,
                )
                await _escalate_auto_repair_unavailable(
                    incident_id=incident_id,
                    approval_id=approval_id,
                    alert_type=alert_type,
                    target_resource=target_resource,
                    namespace=namespace,
                    failure_reason=(
                        f"auto repair execution succeeded but post verification "
                        f"returned {_post_verify_result}; incident remains open"
                    ),
                    attempted_actions=(
                        f"execute_auto_repair playbook:{result.playbook_id} "
                        f"-> verifier:{_post_verify_result} -> emergency_intervention"
                    ),
                )
                return
            try:
                _inc_svc_resolve = get_incident_service()
                await _inc_svc_resolve.resolve_incident(incident_id)
                logger.info(
                    "incident_resolved_after_auto_repair",
                    incident_id=incident_id,
                    approval_id=approval_id,
                )
            except Exception as _resolve_err:
                logger.warning("incident_resolve_after_auto_repair_failed", error=str(_resolve_err))

        # 2026-04-10 Claude Sonnet 4.6 Asia/Taipei: 自動修復後更新 Telegram 卡片
        # 透過 TelegramGateway Service 層移除按鈕並回覆結果 (積木化鐵律)
        if result:
            try:
                _pb_name = decision.playbook.name if decision.playbook else "unknown"
                await get_telegram_gateway().mark_auto_repaired(
                    approval_id=approval_id,
                    playbook_name=_pb_name,
                    execution_time_ms=result.execution_time_ms,
                    success=result.success,
                )
                await op_log.append(
                    "TELEGRAM_RESULT_SENT",
                    incident_id=incident_id,
                    approval_id=approval_id,
                    actor="system",
                    action_detail="auto_repair_card_updated",
                    success=result.success,
                    context={"target_resource": target_resource, "namespace": namespace},
                )
            except Exception as tg_err:
                logger.warning("auto_repair_telegram_notify_failed", error=str(tg_err))

    except Exception as e:
        logger.exception(
            "auto_repair_background_failed",
            incident_id=incident_id,
            error=str(e),
        )


# =============================================================================
# Phase 5: Telegram 背景推送任務 (非阻塞)
# =============================================================================

async def _push_to_telegram_background(
    approval_id: str,
    risk_level: str,
    resource_name: str,
    root_cause: str,
    suggested_action: str,
    estimated_downtime: str,
    hit_count: int = 1,
    # v6.0 AI 仲裁欄位
    primary_responsibility: str = "COLLAB",
    confidence: float = 0.0,
    namespace: str = "default",
    # v7.0 SignOz 整合
    signoz_rps: float = 0.0,
    signoz_rps_trend: str = "stable",
    signoz_error_rate: float = 0.0,
    # 2026-03-29 ogt: AI Token/Cost 追蹤
    ai_tokens: int = 0,
    ai_cost: float = 0.0,
    signoz_p99_latency: float = 0.0,
    signoz_latency_trend: str = "stable",
    signoz_trace_url: str = "",
    auto_tuning_command: str = "",
    # 2026-04-02 ogt: 修復 ai_provider 未傳遞 → Telegram 顯示「AI 仲裁判定」而非具體模型名稱
    ai_provider: str = "",
    # 2026-04-08 ogt: 補傳 incident_id 以啟用詳情/重診/歷史按鈕
    incident_id: str = "",
    # ADR-073 Fix: 傳入 notification_type 以正確路由 TYPE-4D Config Drift 卡片
    notification_type: str = "",
    diff_summary: str = "",
    # 2026-04-12 ogt: ADR-075 斷點 E 修復 — alert_category 傳入以啟用 TYPE-8M 路由
    alert_category: str = "",
    # 2026-04-20 ogt: ADR-092 tg_sent Redis 標記（防收斂靜默）
    fingerprint: str = "",
    # P2.4 中間態清理 2026-04-24 ogt + Claude Sonnet 4.6
    placeholder_message_id: int | None = None,
    # 2026-06-11 Codex: 修復候選阻擋時，把下一步與草案欄位直接帶到 Telegram 卡片。
    repair_candidate_blocker_summary: str = "",
    repair_candidate_next_step: str = "",
    repair_candidate_required_fields: list[str] | None = None,
    repair_candidate_work_item_href: str = "",
    repair_candidate_work_item_id: str = "",
) -> None:
    """
    背景任務: 推送待簽核卡片到 Telegram (v7.0 含 SignOz 整合)

    使用 BackgroundTasks 執行，絕不阻塞 Webhook 回應。
    任何 Telegram API 錯誤都會被捕捉並記錄，不影響主流程。
    TYPE-4D Config Drift → send_drift_card(); 其他 → send_approval_card()
    """
    try:
        gateway = get_telegram_gateway()

        # 檢查是否有設定 Bot Token
        if not settings.OPENCLAW_TG_BOT_TOKEN:
            logger.debug(
                "telegram_push_skipped",
                reason="Bot token not configured",
                approval_id=approval_id,
            )
            return

        # TYPE-4D: Config Drift 使用專屬卡片 (send_drift_card)
        # ADR-071-F: [查看Diff][採納變更][回滾][忽略] 四鍵格式
        if notification_type == "TYPE-4D":
            await gateway.send_drift_card(
                incident_id=incident_id,
                approval_id=approval_id,
                resource_name=resource_name[:50],
                diff_summary=diff_summary or root_cause,
                detected_at="",
            )
            logger.info(
                "telegram_push_success_type4d",
                approval_id=approval_id,
                incident_id=incident_id,
            )
            if fingerprint:
                await get_approval_service().mark_telegram_confirmed(fingerprint)
            return

        # 2026-04-12 ogt: ADR-075 斷點 E 修復 — TYPE-8M Meta-System 使用專屬卡片
        # alertchain_health / flywheel_health → ⚙️ META SYSTEM 卡片，不發群組
        if notification_type == "TYPE-8M" or alert_category in ("alertchain_health", "flywheel_health"):
            await gateway.send_meta_alert(
                incident_id=incident_id,
                approval_id=approval_id,
                alertname=resource_name,
                alert_category=alert_category,
                diagnosis=root_cause[:100],
                severity_level=risk_level,
                system_impact=root_cause[:150],
            )
            logger.info(
                "telegram_push_success_type8m",
                approval_id=approval_id,
                incident_id=incident_id,
                alert_category=alert_category,
            )
            if fingerprint:
                await get_approval_service().mark_telegram_confirmed(fingerprint)
            return

        # 如果是收斂告警，在訊息中加入聚合次數
        root_cause_with_count = root_cause
        if hit_count > 1:
            root_cause_with_count = f"[x{hit_count}] {root_cause}"

        await gateway.send_approval_card(
            approval_id=approval_id,
            risk_level=risk_level,
            resource_name=resource_name[:50],
            root_cause=root_cause_with_count[:100],
            suggested_action=suggested_action[:50],
            estimated_downtime=estimated_downtime,
            # v6.0 AI 仲裁
            primary_responsibility=primary_responsibility,
            confidence=confidence,
            namespace=namespace,
            # v7.0 SignOz 整合
            signoz_rps=signoz_rps,
            signoz_rps_trend=signoz_rps_trend,
            signoz_error_rate=signoz_error_rate,
            signoz_p99_latency=signoz_p99_latency,
            signoz_latency_trend=signoz_latency_trend,
            signoz_trace_url=signoz_trace_url,
            auto_tuning_command=auto_tuning_command,
            # 2026-03-29 ogt: AI Token/Cost 追蹤
            ai_tokens=ai_tokens,
            ai_cost=ai_cost,
            ai_provider=ai_provider,
            incident_id=incident_id,
            # ADR-075 斷點 B 修復: 傳入分類以啟用動態按鈕
            alert_category=alert_category,
            notification_type=notification_type,
            repair_candidate_blocker_summary=repair_candidate_blocker_summary,
            repair_candidate_next_step=repair_candidate_next_step,
            repair_candidate_required_fields=repair_candidate_required_fields,
            repair_candidate_work_item_href=repair_candidate_work_item_href,
            repair_candidate_work_item_id=repair_candidate_work_item_id,
        )

        logger.info(
            "telegram_push_success",
            approval_id=approval_id,
            risk_level=risk_level,
            hit_count=hit_count,
            primary_responsibility=primary_responsibility,
            confidence=confidence,
            signoz_integrated=signoz_rps > 0 or signoz_error_rate > 0,
            ai_tokens=ai_tokens,
            ai_cost=f"${ai_cost:.6f}",
        )
        if fingerprint:
            await get_approval_service().mark_telegram_confirmed(fingerprint)

        # P2.4 中間態清理 2026-04-24 ogt + Claude Sonnet 4.6
        if placeholder_message_id:
            await gateway.delete_message(placeholder_message_id)

        # 2026-04-08 Claude Code: 記錄 Telegram 推送事件
        try:
            from src.repositories.alert_operation_log_repository import get_alert_operation_log_repository
            await get_alert_operation_log_repository().append(
                "TELEGRAM_SENT",
                approval_id=approval_id,
                actor="system",
                action_detail="approval_card",
                success=True,
                context={
                    "risk_level": risk_level,
                    "resource_name": resource_name,
                    "hit_count": hit_count,
                    "namespace": namespace,
                },
            )
        except Exception as _log_e:
            logger.warning("alert_op_log_telegram_sent_failed", error=str(_log_e))

    except TelegramGatewayError as e:
        logger.warning(
            "telegram_push_failed",
            approval_id=approval_id,
            error=str(e),
            error_type="TelegramGatewayError",
        )
    except Exception as e:
        logger.error(
            "telegram_push_unexpected_error",
            approval_id=approval_id,
            error=str(e),
            error_type=type(e).__name__,
        )


# =============================================================================
# Phase 5: HMAC Signature Verification (CISO 要求)
# =============================================================================

class HMACVerificationError(Exception):
    """HMAC 簽章驗證失敗"""
    pass


async def verify_webhook_signature(
    request: Request,
    x_signature_256: str | None = Header(None, alias="X-Signature-256"),
    x_webhook_timestamp: str | None = Header(None, alias="X-Webhook-Timestamp"),
    x_webhook_nonce: str | None = Header(None, alias="X-Webhook-Nonce"),
) -> bool:
    """
    驗證 Webhook 請求的 HMAC-SHA256 簽章

    CISO 安全要求:
    - 所有外部 Webhook 必須攜帶 X-Signature-256 Header
    - 簽章格式: sha256=<hex_digest>
    - 使用 WEBHOOK_HMAC_SECRET 進行驗證

    ADR-116 Replay 防護（向後相容）:
    - X-Webhook-Timestamp: Unix epoch 秒，若提供則驗證 ±300 秒範圍
    - X-Webhook-Nonce: 隨機字串，若提供則用 Redis NX 去重（TTL=600s）
    - 兩個 Header 均可選；過渡期不提供時僅記錄 warning

    安全鐵律 (Fail-Closed):
    - 生產環境: HMAC Secret 未設定 → 直接拒絕 (不可跳過)
    - 開發環境: 可跳過驗證 (僅供本地測試)

    Args:
        request: FastAPI Request 物件
        x_signature_256: X-Signature-256 Header 值
        x_webhook_timestamp: X-Webhook-Timestamp Header 值（Unix epoch 秒，可選）
        x_webhook_nonce: X-Webhook-Nonce Header 值（隨機字串，可選）

    Returns:
        bool: 驗證是否通過

    Raises:
        HMACVerificationError: 簽章驗證失敗
    """
    import time as _time

    # ==========================================================================
    # Fail-Closed 安全策略 (CISO 要求)
    # ==========================================================================
    if not settings.WEBHOOK_HMAC_SECRET:
        # 生產環境: 強制拒絕 (Fail-Closed)
        if settings.ENVIRONMENT == "prod":
            logger.critical(
                "hmac_secret_missing_in_production",
                environment=settings.ENVIRONMENT,
                message="CRITICAL: HMAC Secret not configured in production!",
            )
            raise HMACVerificationError(
                "Critical: WEBHOOK_HMAC_SECRET missing in production environment"
            )

        # 開發環境: 允許跳過 (僅供本地測試)
        logger.warning(
            "hmac_verification_skipped_dev_only",
            environment=settings.ENVIRONMENT,
            reason="WEBHOOK_HMAC_SECRET not configured (dev mode only)",
        )
        return True

    # 必須提供簽章
    if not x_signature_256:
        logger.warning("hmac_signature_missing")
        raise HMACVerificationError("Missing X-Signature-256 header")

    # 解析簽章格式
    if not x_signature_256.startswith("sha256="):
        raise HMACVerificationError("Invalid signature format (expected sha256=...)")

    provided_signature = x_signature_256[7:]  # 移除 "sha256=" 前綴

    # 讀取 Request Body
    body = await request.body()

    # 計算預期簽章
    expected_signature = hmac.new(
        settings.WEBHOOK_HMAC_SECRET.encode(),
        body,
        hashlib.sha256,
    ).hexdigest()

    # 常數時間比較 (防止計時攻擊)
    if not hmac.compare_digest(provided_signature, expected_signature):
        logger.warning(
            "hmac_verification_failed",
            provided=provided_signature[:16] + "...",
            expected=expected_signature[:16] + "...",
        )
        raise HMACVerificationError("Invalid signature")

    logger.info("hmac_verification_success")

    # ==========================================================================
    # ADR-116: Replay 防護（向後相容，HMAC 驗證成功後才執行）
    # ==========================================================================

    # --- Timestamp 驗證（±300 秒） ---
    if x_webhook_timestamp is not None:
        try:
            req_ts = int(x_webhook_timestamp)
            now_ts = int(_time.time())
            skew = abs(now_ts - req_ts)
            if skew > 300:
                logger.warning(
                    "webhook_timestamp_out_of_window",
                    request_ts=req_ts,
                    server_ts=now_ts,
                    skew_seconds=skew,
                )
                raise HMACVerificationError(
                    f"Timestamp out of acceptable window (skew={skew}s > 300s)"
                )
        except ValueError:
            logger.warning(
                "webhook_timestamp_invalid_format",
                raw_value=x_webhook_timestamp,
            )
            raise HMACVerificationError("X-Webhook-Timestamp must be a Unix epoch integer")
    else:
        # 過渡期：沒有提供 Timestamp 則記錄 warning 但允許通過
        logger.warning(
            "webhook_replay_protection_missing",
            header="X-Webhook-Timestamp",
            note="transition period: request allowed but sender should add replay headers",
        )

    # --- Nonce 去重（透過 security_interceptor.check_webhook_nonce，fail open） ---
    if x_webhook_nonce is not None:
        valid = await check_webhook_nonce(x_webhook_nonce)
        if not valid:
            raise HMACVerificationError("Nonce replay detected")
    else:
        # 過渡期：沒有提供 Nonce 則記錄 warning 但允許通過
        logger.warning(
            "webhook_replay_protection_missing",
            header="X-Webhook-Nonce",
            note="transition period: request allowed but sender should add replay headers",
        )

    return True


# generate_alert_fingerprint 已封裝為 AlertAnalyzer.generate_fingerprint (首席架構師 v1.2 2026-04-01 Asia/Taipei)

# 戰略 B: 滑動時間窗 (ADR-073: 5 分鐘改 30 分鐘，防同一問題反覆重建 Incident，2026-04-12 ogt)
DEBOUNCE_WINDOW_MINUTES = 30
ALERTMANAGER_BACKGROUND_AI_TIMEOUT_SECONDS = 90.0


# =============================================================================
# Request Models
# =============================================================================

# AlertPayload 和 AlertResponse 已移至 src/models/webhook.py (R4 #129, 2026-04-01 ogt)
# 由 import 區塊頂部的 from src.models.webhook import ... 引入


# =============================================================================
# Phase 6.1: Signal Producer (Redis Streams)
# =============================================================================

# Redis Stream 常量
# 2026-03-27 ogt: 統一 Stream Key 格式 (P0 修復)
SIGNAL_STREAM_KEY = "awoooi:signals"
SIGNAL_STREAM_MAXLEN = 10000  # 防止 Stream 無限增長


class SignalPayload(BaseModel):
    """
    Phase 6.1: 輕量級訊號 Payload

    設計原則:
    - 只做資料轉換，不做複雜運算
    - 直接寫入 Redis Stream，解耦處理邏輯
    - 支援多來源: Prometheus, Grafana, K8s Events, 自訂

    與 AlertPayload 的區別:
    - SignalPayload: 輕量級，直接進 Stream
    - AlertPayload: 同步處理，含 LLM 分析
    """

    source: str = Field(
        ...,
        description="訊號來源 (prometheus, grafana, k8s-events, signoz)",
    )

    alert_name: str = Field(
        ...,
        description="告警名稱 (例如: HighCPUUsage, PodCrashLooping)",
    )

    severity: Literal["info", "warning", "critical"] = Field(
        "warning",
        description="嚴重度",
    )

    namespace: str = Field(
        "default",
        description="K8s Namespace",
    )

    target: str = Field(
        ...,
        description="受影響目標 (Pod, Node, Service 名稱)",
    )

    message: str = Field(
        "",
        description="訊號描述",
    )

    labels: dict | None = Field(
        None,
        description="標籤 (例如: {app: harbor, team: devops})",
    )

    annotations: dict | None = Field(
        None,
        description="附加資訊 (例如: {runbook_url: ..., dashboard_url: ...})",
    )


class SignalResponse(BaseModel):
    """
    Signal 接收回應
    """

    success: bool = Field(..., description="是否成功寫入 Stream")
    message_id: str | None = Field(None, description="Redis Stream Message ID")
    stream: str = Field(SIGNAL_STREAM_KEY, description="寫入的 Stream 名稱")


async def produce_signal_to_stream(signal: SignalPayload) -> str:
    """
    將 Signal 寫入 Redis Stream

    Phase 17 P0: Router 層違規修復 - 改用 Service 層

    Returns:
        str: Redis Stream Message ID
    """
    # Phase 17 P0: 透過 Service 寫入 Stream
    producer = get_signal_producer()

    signal_data = SignalData(
        source=signal.source,
        alert_name=signal.alert_name,
        severity=signal.severity,
        namespace=signal.namespace,
        target=signal.target,
        message=signal.message,
        labels=signal.labels,
        annotations=signal.annotations,
    )

    return await producer.produce(signal_data)


@router.post(
    "/signals",
    response_model=SignalResponse,
    summary="Phase 6.1: 輕量級訊號接收 (Event Bus)",
    description="接收訊號並直接寫入 Redis Stream，完全解耦接收與處理。",
)
async def receive_signal(
    request: Request,
    signal: SignalPayload,
    x_signature_256: str | None = Header(None, alias="X-Signature-256"),
) -> SignalResponse:
    """
    Phase 6.1: Event Bus Producer

    職責:
    1. HMAC 驗證 (可選，依環境)
    2. 將 Signal 轉換為字典
    3. XADD 寫入 stream:awoooi_signals
    4. 立即返回，不做任何複雜運算

    處理邏輯由 SignalWorker (Consumer) 負責。
    """
    # HMAC 驗證 (與 /alerts 相同邏輯)
    try:
        await verify_webhook_signature(request, x_signature_256)
    except HMACVerificationError as e:
        logger.warning("signal_hmac_rejected", error=str(e))
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=f"HMAC verification failed: {str(e)}",
        ) from e

    try:
        # 寫入 Redis Stream
        message_id = await produce_signal_to_stream(signal)

        return SignalResponse(
            success=True,
            message_id=message_id,
            stream=SIGNAL_STREAM_KEY,
        )

    except Exception as e:
        logger.exception("signal_produce_error", error=str(e))
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to produce signal: {str(e)}",
        ) from e


# AlertAnalyzer 已移至 src/services/alert_analyzer_service.py (R4 #129, 2026-04-01 ogt)
# 由 import 區塊頂部的 from src.services.alert_analyzer_service import ... 引入

# =============================================================================
# Endpoints
# =============================================================================

@router.post(
    "/alerts",
    response_model=AlertResponse,
    summary="接收外部告警 (戰略 B: 告警風暴收斂)",
    description="接收告警並自動收斂重複告警。相同指紋的告警會聚合，避免重複呼叫 LLM 造成成本爆炸。",
)
async def receive_alert(
    request: Request,
    alert: AlertPayload,
    background_tasks: BackgroundTasks,
    x_signature_256: str | None = Header(None, alias="X-Signature-256"),
) -> AlertResponse:
    """
    接收外部告警並觸發 OpenClaw AI 大腦分析

    戰略 B 流程 (告警風暴收斂):
    0. HMAC 簽章驗證 (CISO 要求)
    1. 生成告警指紋 (namespace:deployment:alert_type Hash)
    2. 查詢 DB 是否有同指紋的 pending 或 5 分鐘內記錄
    3. [收斂] 如果有：hit_count +1，跳過 LLM！
    4. [新告警] 如果沒有：觸發 LLM 分析
    5. 建立/更新 ApprovalRecord
    """
    # ==========================================================================
    # Phase 5 Step 0: HMAC 簽章驗證 (CISO 要求)
    # ==========================================================================
    try:
        await verify_webhook_signature(request, x_signature_256)
    except HMACVerificationError as e:
        logger.warning("webhook_hmac_rejected", error=str(e))
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=f"HMAC verification failed: {str(e)}",
        ) from e

    alert_id = f"alert-{now_taipei().strftime('%Y%m%d%H%M%S')}"

    # ==========================================================================
    # 戰略 B Step 1: 生成告警指紋
    # ==========================================================================
    fingerprint = AlertAnalyzer.generate_fingerprint(alert)

    logger.info(
        "webhook_alert_received",
        alert_id=alert_id,
        alert_type=alert.alert_type,
        severity=alert.severity,
        source=alert.source,
        target=alert.target_resource,
        fingerprint=fingerprint,
    )

    try:
        service = get_approval_service()

        # ==========================================================================
        # 戰略 B Step 2: 查詢是否有同指紋的現有記錄
        # ==========================================================================
        existing_approval = await service.find_by_fingerprint(
            fingerprint=fingerprint,
            debounce_minutes=DEBOUNCE_WINDOW_MINUTES,
        )

        if existing_approval:
            # ==========================================================================
            # 戰略 B Step 3: [收斂] 同指紋告警 - 跳過 LLM，只更新計數！
            # ==========================================================================
            logger.info(
                "alert_converged_skip_llm",
                alert_id=alert_id,
                fingerprint=fingerprint,
                existing_approval_id=str(existing_approval.id),
                old_hit_count=existing_approval.hit_count,
                message="🛡️ 告警收斂生效！跳過 LLM 分析，節省成本！",
            )

            # 增加 hit_count
            updated_approval = await service.increment_hit_count(existing_approval.id)

            if updated_approval:
                # =================================================================
                # 2026-03-27 ogt: 收斂告警不重複發送 Telegram，只更新 hit_count
                # 避免 Telegram 洗版，用戶可在 UI 查看聚合次數
                # =================================================================
                logger.info(
                    "alert_converged_telegram_recurrence_scheduled",
                    approval_id=str(updated_approval.id),
                    hit_count=updated_approval.hit_count,
                    reason="Converged alert - scheduling throttled recurrence notice",
                )
                background_tasks.add_task(
                    notify_converged_alert_recurrence,
                    source=alert.source,
                    fingerprint=fingerprint,
                    alertname=alert.alert_type,
                    severity=alert.severity,
                    namespace=alert.namespace,
                    target_resource=alert.target_resource,
                    hit_count=updated_approval.hit_count,
                    incident_id=getattr(updated_approval, "incident_id", None),
                    approval_id=str(updated_approval.id),
                    alert_category=alert.alert_type,
                    notification_type="generic",
                )

                return AlertResponse(
                    success=True,
                    message=f"🛡️ 告警收斂 (x{updated_approval.hit_count}) - 已排程節流再通知",
                    alert_id=alert_id,
                    approval_created=False,  # 未建立新卡片
                    approval_id=str(updated_approval.id),
                    risk_level=updated_approval.risk_level.value,
                    suggested_action=updated_approval.action,
                    # 戰略 B
                    hit_count=updated_approval.hit_count,
                    converged=True,  # 標記為收斂告警
                )

        # ==========================================================================
        # 戰略 B Step 4: [新告警] 無同指紋記錄 - 進入 LLM 分析流程
        # ==========================================================================
        logger.info(
            "alert_new_fingerprint_proceed_llm",
            alert_id=alert_id,
            fingerprint=fingerprint,
            message="新指紋告警，啟動 LLM 分析",
        )

        # 準備告警上下文給 LLM
        alert_context = {
            "alert_type": alert.alert_type,
            "severity": alert.severity,
            "source": alert.source,
            "target_resource": alert.target_resource,
            "namespace": alert.namespace,
            "message": alert.message,
            "metrics": alert.metrics or {},
            "labels": alert.labels or {},
        }

        # P2.4 中間態推播 2026-04-24 ogt + Claude Sonnet 4.6
        # LLM 分析前先送佔位卡，讓使用者知道系統已收到告警並正在處理
        _placeholder_msg_id: int | None = None
        try:
            _tg_gw = get_telegram_gateway()
            _placeholder_msg_id = await asyncio.wait_for(
                _tg_gw.send_analyzing_placeholder(
                    alert_type=alert.alert_type or "unknown",
                    resource_name=alert.target_resource or "",
                    severity=alert.severity or "medium",
                ),
                timeout=3.0,  # 最多等 3s，不阻塞主流程
            )
        except Exception:
            pass  # 佔位卡失敗不影響主流程

        # 呼叫 OpenClaw LLM 分析 (v7.0 含 SignOz 整合)
        # 2026-03-29 ogt: 加入 Token/Cost 追蹤
        openclaw = get_openclaw()
        analysis_result, ai_provider, raw_response, signoz_metrics, signoz_trace_url, ai_tokens, ai_cost = await _analyze_alertmanager_with_timeout(
            openclaw,
            alert_context,
            alert_id=alert_id,
            alertname=alert.alert_type,
        )

        if analysis_result:
            # LLM 分析成功
            logger.info(
                "llm_analysis_success",
                alert_id=alert_id,
                provider=ai_provider,
                action_title=analysis_result.action_title,
                risk_level=analysis_result.risk_level.value,
                confidence=analysis_result.confidence,
            )

            risk_mapping = {
                "low": RiskLevel.LOW,
                "medium": RiskLevel.MEDIUM,
                "critical": RiskLevel.CRITICAL,
            }
            risk_level = risk_mapping.get(analysis_result.risk_level.value, RiskLevel.MEDIUM)

            # 2026-04-21 ogt: NO_ACTION/INVESTIGATE/OBSERVE 強制 LOW risk，避免卡 PENDING
            # 根因：_validate_deployment_inventory 降級後若原 risk 為 HIGH/CRITICAL，
            # 會等 Telegram 批准，但無任何可執行動作 → 永久積壓 PENDING
            _non_destructive_actions = {"NO_ACTION", "INVESTIGATE", "OBSERVE"}
            _sa_val = (
                analysis_result.suggested_action.value
                if hasattr(analysis_result.suggested_action, "value")
                else str(analysis_result.suggested_action)
            )
            if _sa_val in _non_destructive_actions:
                risk_level = RiskLevel.LOW

            impact_mapping = {
                "NONE": DataImpact.NONE,
                "READ_ONLY": DataImpact.READ_ONLY,
                "WRITE": DataImpact.WRITE,
                "DESTRUCTIVE": DataImpact.DESTRUCTIVE,
            }
            blast = analysis_result.blast_radius
            data_impact = impact_mapping.get(blast.data_impact.value, DataImpact.NONE)

            # 2026-04-27 Claude Sonnet 4.6: shadow-run Step1 — 補 metadata kwarg，讓 extra_metadata 可觀測
            _cmd_cs1 = (analysis_result.kubectl_command or "").strip()
            _alertname_cs1 = str((alert.labels or {}).get("alertname") or alert.alert_type or "")
            _guarded_action_cs1 = await guard_alert_approval_action(
                action=(_cmd_cs1 or f"{analysis_result.action_title} | NO_ACTION"),
                alert_namespace=alert.namespace,
                alertname=_alertname_cs1,
                alert_category=get_incident_type(_alertname_cs1),
            )
            _matched_playbook_id_cs1 = await resolve_playbook_id_for_alert(
                alertname=_alertname_cs1,
                affected_services=analysis_result.affected_services
                or ([alert.target_resource] if alert.target_resource else []),
                severity=risk_level.value,
            )
            if _guarded_action_cs1.blocked:
                risk_level = RiskLevel.LOW
                _cmd_cs1 = ""

            _approval_metadata_cs1 = {
                "source": ai_provider,
                "confidence_score": analysis_result.confidence,
                "is_rule_based": False,
                "playbook_id": _matched_playbook_id_cs1,
                **_guarded_action_cs1.metadata,
            }
            approval_create = ApprovalRequestCreate(
                action=_guarded_action_cs1.action,
                description=f"[AI: {ai_provider}] {analysis_result.action_title} | {analysis_result.description}",
                risk_level=risk_level,
                blast_radius=BlastRadius(
                    affected_pods=blast.affected_pods,
                    estimated_downtime=blast.estimated_downtime,
                    related_services=list(set(blast.related_services + analysis_result.affected_services)),
                    data_impact=data_impact,
                ),
                dry_run_checks=[
                    DryRunCheck(name="AI 信心度", passed=analysis_result.confidence >= 0.7, message=f"{analysis_result.confidence:.0%}"),
                    DryRunCheck(name="權限驗證", passed=True, message="cluster-admin"),
                    DryRunCheck(name="語法驗證", passed=True, message="kubectl valid"),
                    DryRunCheck(name="偏差分析", passed=True, message=analysis_result.deviation_analysis[:50] if analysis_result.deviation_analysis else "N/A"),
                ],
                requested_by=f"OpenClaw ({ai_provider})",
                metadata=_approval_metadata_cs1,
                matched_playbook_id=_matched_playbook_id_cs1,
            )
            suggested_action = analysis_result.kubectl_command
        else:
            # LLM 失敗，降級使用靜態分析
            logger.warning(
                "llm_analysis_failed_fallback_static",
                alert_id=alert_id,
                provider=ai_provider,
            )
            approval_create = AlertAnalyzer.analyze(alert)
            suggested_action = approval_create.action
            ai_provider = "static_analyzer"

        # ==========================================================================
        # Step 5: 建立帶指紋的 ApprovalRecord
        # ==========================================================================
        approval = await service.create_approval_with_fingerprint(
            request=approval_create,
            fingerprint=fingerprint,
        )

        # 2026-04-27 Claude Sonnet 4.6: shadow-run Step2 — 只記 log，不改執行決策
        try:
            _shadow_proposal = {
                "risk_level": risk_level.value,
                "confidence": getattr(approval_create, "metadata", {}).get("confidence_score", 0.0) if approval_create.metadata else 0.0,
                "action": approval_create.action,
                "kubectl_command": approval_create.action,
                "is_rule_based": False,
                "source": ai_provider,
            }
            _shadow_result = get_auto_approve_policy().evaluate(_shadow_proposal)
            logger.info(
                "shadow_auto_approve_result",
                approval_id=str(approval.id),
                should_auto=_shadow_result.should_auto_approve,
                reason=_shadow_result.reason.value,
                source=ai_provider,
            )
        except Exception as _shadow_err:
            logger.warning("shadow_auto_approve_failed", error=str(_shadow_err))

        # 2026-04-27 ogt + Claude Sonnet 4.6: CS1 LLM 高信心度自動執行
        # 設計：confidence ≥ 0.85 + 非 CRITICAL + 非破壞性 + 有 kubectl 指令 → 直接執行
        # 安全防線：CRITICAL / destructive patterns / NO_ACTION/INVESTIGATE/OBSERVE / 空 kubectl → 降級 PENDING
        if analysis_result:
            _cs1_kubectl = _cmd_cs1
            _cs1_can_auto = (
                bool(_cs1_kubectl)
                and analysis_result.confidence >= 0.85
                and risk_level != RiskLevel.CRITICAL
                and _sa_val not in _non_destructive_actions
                and is_safe_kubectl_action(_cs1_kubectl)
            )
            if _cs1_can_auto:
                try:
                    from src.models.approval import ApprovalRequest, ApprovalStatus
                    from src.services.approval_execution import ApprovalExecutionService

                    _cs1_auto_approval = ApprovalRequest(
                        incident_id=str(approval.incident_id) if approval.incident_id else None,
                        action=approval_create.action,
                        description=approval_create.description,
                        requested_by="auto_approve_llm_high_confidence",
                        required_signatures=0,
                        status=ApprovalStatus.APPROVED,
                        risk_level=risk_level.value,
                        matched_playbook_id=_matched_playbook_id_cs1,
                        metadata={
                            **_approval_metadata_cs1,
                            "is_high_confidence": True,
                            "policy_reason": _shadow_result.reason.value
                            if hasattr(_shadow_result, "reason")
                            else "cs1_auto_confident_execution",
                        },
                    )
                    _cs1_auto_approval.id = approval.id

                    _cs1_executor = ApprovalExecutionService()
                    _cs1_exec_success = await _cs1_executor.execute_approved_action(_cs1_auto_approval)

                    try:
                        await service.update_execution_status(approval.id, _cs1_exec_success)
                    except Exception as _cs1_upd_err:
                        logger.warning(
                            "cs1_auto_execute_status_update_failed",
                            approval_id=str(approval.id),
                            error=str(_cs1_upd_err),
                        )

                        logger.info(
                            "llm_high_confidence_auto_executed",
                            approval_id=str(approval.id),
                            confidence=analysis_result.confidence,
                            exec_success=_cs1_exec_success,
                            action=_cs1_kubectl[:80],
                            is_high_confidence=True,
                            policy_reason=(
                                _shadow_result.reason.value
                                if hasattr(_shadow_result, "reason")
                                else "cs1_auto_confident_execution"
                            ),
                        )
                except Exception as _cs1_auto_err:
                    logger.warning(
                        "llm_high_confidence_auto_execute_failed",
                        approval_id=str(approval.id),
                        error=str(_cs1_auto_err),
                    )
                    # 降級：維持 PENDING，流程繼續到 Telegram 推送

        logger.info(
            "approval_auto_created_with_fingerprint",
            alert_id=alert_id,
            approval_id=str(approval.id),
            fingerprint=fingerprint,
            status=approval.status.value,
            ai_provider=ai_provider,
        )

        # ==========================================================================
        # Step 6: 推送到 Telegram 行動戰情室 (BackgroundTasks - 非阻塞)
        # ==========================================================================
        # 提取 AI 仲裁欄位 (v6.0)
        primary_resp = getattr(analysis_result, "primary_responsibility", "COLLAB")
        ai_confidence = getattr(analysis_result, "confidence", 0.0)

        # 提取 SignOz 數據 (v7.0)
        signoz_rps = 0.0
        signoz_rps_trend = "stable"
        signoz_error_rate = 0.0
        signoz_p99_latency = 0.0
        signoz_latency_trend = "stable"
        auto_tuning_cmd = ""

        if signoz_metrics:
            signoz_rps = signoz_metrics.rps
            signoz_rps_trend = signoz_metrics.rps_trend
            signoz_error_rate = signoz_metrics.error_rate
            signoz_p99_latency = signoz_metrics.p99_latency_ms
            signoz_latency_trend = signoz_metrics.latency_trend

        # 提取調優指令
        if analysis_result and hasattr(analysis_result, "optimization_suggestions"):
            suggestions = getattr(analysis_result, "optimization_suggestions", [])
            if suggestions and len(suggestions) > 0:
                first_suggestion = suggestions[0]
                if hasattr(first_suggestion, "kubectl_or_config"):
                    auto_tuning_cmd = first_suggestion.kubectl_or_config
                elif isinstance(first_suggestion, dict):
                    auto_tuning_cmd = first_suggestion.get("kubectl_or_config", "")

        background_tasks.add_task(
            _push_to_telegram_background,
            approval_id=str(approval.id),
            risk_level=approval_create.risk_level.value,
            resource_name=alert.target_resource,
            root_cause=analysis_result.description if analysis_result else alert.message,
            suggested_action=suggested_action,
            estimated_downtime=approval_create.blast_radius.estimated_downtime,
            hit_count=1,
            # v6.0 AI 仲裁
            primary_responsibility=primary_resp,
            confidence=ai_confidence,
            namespace=alert.namespace,
            # v7.0 SignOz 整合
            signoz_rps=signoz_rps,
            signoz_rps_trend=signoz_rps_trend,
            signoz_error_rate=signoz_error_rate,
            signoz_p99_latency=signoz_p99_latency,
            signoz_latency_trend=signoz_latency_trend,
            signoz_trace_url=signoz_trace_url,
            auto_tuning_command=auto_tuning_cmd,
            # 2026-03-29 ogt: AI Token/Cost 追蹤
            ai_tokens=ai_tokens,
            ai_cost=ai_cost,
            ai_provider=ai_provider,
            # 2026-04-08 ogt: 補傳 incident_id 以啟用詳情/重診/歷史按鈕
            incident_id="",  # /alerts 路徑尚無 incident，detail/reanalyze/history 按鈕不顯示
            # /alerts 路徑沒有 notification_type（非 Alertmanager 路徑），不需 TYPE-4D routing
            # ADR-075: alert_type → category 對應，啟用動態按鈕
            alert_category={
                "k8s_node_failure": "kubernetes",
                "k8s_pod_crash": "kubernetes",
                "db_connection_timeout": "database",
                "high_cpu": "host_resource",
                "high_memory": "host_resource",
                "disk_full": "storage",
                "ssl_expiry": "ssl_cert",
            }.get(alert.alert_type, "general"),
            fingerprint=fingerprint,
            # P2.4 中間態清理 2026-04-24 ogt + Claude Sonnet 4.6
            placeholder_message_id=_placeholder_msg_id,
        )

        return AlertResponse(
            success=True,
            message=f"告警已接收，OpenClaw ({ai_provider}) 已建立待簽核卡片 (Telegram 背景推送中)",
            alert_id=alert_id,
            approval_created=True,
            approval_id=str(approval.id),
            risk_level=approval_create.risk_level.value,
            suggested_action=suggested_action,
            # 戰略 B
            hit_count=1,  # 新建立的告警，計數為 1
            converged=False,  # 非收斂告警
        )

    except Exception as e:
        logger.error(
            "webhook_alert_processing_failed",
            alert_id=alert_id,
            error=str(e),
        )
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"告警處理失敗: {str(e)}",
        ) from e


# =============================================================================
# Phase 10: Alertmanager 原生格式支援 (內網免 HMAC)
# =============================================================================

class AlertmanagerAlert(BaseModel):
    """Alertmanager 單一告警"""
    status: str  # firing, resolved
    labels: dict = {}
    annotations: dict = {}
    startsAt: str | None = None
    endsAt: str | None = None
    generatorURL: str | None = None
    fingerprint: str | None = None


class AlertmanagerPayload(BaseModel):
    """Alertmanager Webhook Payload"""
    version: str | None = "4"
    groupKey: str | None = None
    status: str  # firing, resolved
    receiver: str | None = None
    groupLabels: dict | None = {}
    commonLabels: dict | None = {}
    commonAnnotations: dict | None = {}
    externalURL: str | None = None
    alerts: list[AlertmanagerAlert]


_CICD_JOB_STATUSES = frozenset({"running", "success", "failed", "pending"})


def _cicd_job_status_from_alert(alert: AlertmanagerAlert) -> str:
    """將 CI/CD Alertmanager label 轉成 TelegramGateway 支援的狀態。

    2026-05-12 Codex: Gitea workflow 先送進 AWOOI API，不能只靠
    severity=info 推 success，否則 failed/pending 事件進 AwoooP 後語義會失真。
    """
    labels = alert.labels or {}
    for key in ("status", "job_status", "ci_status"):
        value = str(labels.get(key) or "").strip().lower()
        if value in _CICD_JOB_STATUSES:
            return value

    severity = str(labels.get("severity") or "").strip().lower()
    if severity == "info":
        return "success"
    if severity in {"critical", "error"}:
        return "failed"
    return "running"


def _cicd_duration_seconds_from_alert(alert: AlertmanagerAlert) -> int:
    labels = alert.labels or {}
    raw = labels.get("duration_seconds") or labels.get("duration") or 0
    try:
        value = int(str(raw).strip())
    except (TypeError, ValueError):
        return 0
    return max(value, 0)


def is_internal_ip(client_ip: str) -> bool:
    """檢查是否為內網 IP"""
    import ipaddress
    try:
        ip = ipaddress.ip_address(client_ip)
        # 私有網段: 10.x.x.x, 172.16-31.x.x, 192.168.x.x, localhost
        return ip.is_private or ip.is_loopback
    except ValueError:
        return False


async def _process_new_alert_background(
    alert_context: dict,
    alert_id: str,
    fingerprint: str,
    target_resource: str,
    namespace: str,
    alert_type: str,
    message: str,
    alertname: str,
    severity: str,
    alert_labels: dict,
    notification_type: str,
    alert_category: str,
    can_auto_repair: bool,
) -> None:
    """
    背景任務: LLM 分析 + Approval/Incident 建立 + Telegram 推送

    ADR-089 (2026-04-17 ogt + Claude Sonnet 4.6):
    Alertmanager 收到告警後立即回傳 HTTP 202，
    所有 AI 辯證放入背景執行，避免 Alertmanager 等待 >90s 觸發 Fallback 風暴。
    """
    try:
        service = get_approval_service()
        openclaw = get_openclaw()
        traced_alert_labels = {
            **(alert_labels or {}),
            "fingerprint": fingerprint,
            "alert_id": alert_id,
        }

        rule_response = match_rule(alert_context)
        should_bypass_llm = _should_use_alertmanager_rule_first(rule_response, alert_category)

        if should_bypass_llm:
            logger.info(
                "alertmanager_rule_bypass_llm",
                alert_id=alert_id,
                alertname=alertname,
                rule_id=rule_response.get("rule_id", ""),
                alert_category=alert_category,
                reason="host/backup YAML 權威規則命中，跳過 LLM 避免產生錯誤 K8s 動作",
            )
            risk_mapping = {
                "low": RiskLevel.LOW,
                "medium": RiskLevel.MEDIUM,
                "critical": RiskLevel.CRITICAL,
            }
            rule_risk = risk_mapping.get(
                str(rule_response.get("risk_level", "low")).lower(),
                RiskLevel.LOW,
            )
            blast = rule_response.get("blast_radius", {}) or {}
            impact_mapping = {
                "NONE": DataImpact.NONE,
                "READ_ONLY": DataImpact.READ_ONLY,
                "WRITE": DataImpact.WRITE,
                "DESTRUCTIVE": DataImpact.DESTRUCTIVE,
            }
            data_impact = impact_mapping.get(
                str(blast.get("data_impact", "NONE")).upper(),
                DataImpact.NONE,
            )
            rule_kubectl = str(rule_response.get("kubectl_command", "")).strip()
            rule_description = str(rule_response.get("description", message))
            rule_action = (
                rule_kubectl
                if rule_kubectl else
                f"NO_ACTION - {rule_description[:120]}"
            )
            _matched_playbook_id_cs2 = await resolve_playbook_id_for_alert(
                rule_id=str(rule_response.get("rule_id", "")),
                alertname=alertname,
                affected_services=[target_resource] if target_resource else [],
                severity=rule_risk.value,
            )
            _guarded_action_cs2 = await guard_alert_approval_action(
                action=rule_action,
                alert_namespace=namespace,
                alertname=alertname,
                alert_category=alert_category,
            )
            if _guarded_action_cs2.blocked:
                rule_action = _guarded_action_cs2.action
                rule_kubectl = ""
                rule_risk = RiskLevel.LOW

            # 2026-04-27 Claude Sonnet 4.6: shadow-run Step1 — 補 metadata kwarg，讓 extra_metadata 可觀測
            _approval_metadata_cs2 = {
                "source": "rule_engine",
                "confidence_score": float(rule_response.get("confidence", 0.0) or 0.0),
                "is_rule_based": True,
                "rule_id": str(rule_response.get("rule_id", "")) or None,
                "playbook_id": _matched_playbook_id_cs2,
                **_guarded_action_cs2.metadata,
            }
            approval_create = ApprovalRequestCreate(
                action=rule_action,
                description=f"[Rule: {rule_response.get('rule_id', 'unknown')}] {rule_description}",
                risk_level=rule_risk,
                blast_radius=BlastRadius(
                    affected_pods=int(blast.get("affected_pods", 1) or 1),
                    estimated_downtime=str(blast.get("estimated_downtime", "N/A")),
                    related_services=list(
                        set(
                            (blast.get("related_services") or [])
                            + [str(rule_response.get("target_resource", target_resource))]
                        )
                    ),
                    data_impact=data_impact,
                ),
                dry_run_checks=[
                    DryRunCheck(
                        name="規則命中",
                        passed=True,
                        message=str(rule_response.get("rule_id", "unknown")),
                    ),
                    DryRunCheck(
                        name="來源",
                        passed=True,
                        message="alertmanager",
                    ),
                ],
                requested_by="OpenClaw (rule-engine)",
                metadata=_approval_metadata_cs2,
                matched_playbook_id=_matched_playbook_id_cs2,
            )

            approval = await service.create_approval_with_fingerprint(
                request=approval_create,
                fingerprint=fingerprint,
            )

            # 2026-04-27 Claude Sonnet 4.6: shadow-run Step2 — 只記 log，不改執行決策
            try:
                _shadow_proposal_cs2 = {
                    "risk_level": rule_risk.value,
                    "confidence": _approval_metadata_cs2["confidence_score"],
                    "action": rule_action,
                    "kubectl_command": rule_kubectl,
                    "is_rule_based": True,
                    "source": "rule_engine",
                }
                _shadow_result_cs2 = get_auto_approve_policy().evaluate(_shadow_proposal_cs2)
                logger.info(
                    "shadow_auto_approve_result",
                    approval_id=str(approval.id),
                    should_auto=_shadow_result_cs2.should_auto_approve,
                    reason=_shadow_result_cs2.reason.value,
                    source="rule_engine",
                )
            except Exception as _shadow_err_cs2:
                logger.warning("shadow_auto_approve_failed", error=str(_shadow_err_cs2))

            # 2026-04-27 ogt + Claude Sonnet 4.6: CS2 規則引擎自動執行
            # 設計：is_rule_based=True 確定性高，滿足條件直接執行，不等人工審核
            # 安全防線：CRITICAL / destructive patterns / NO_ACTION / 空 kubectl → 全部降級 PENDING
            _cs2_auto_approval = None
            _cs2_executor = None
            _cs2_exec_success: bool | None = None
            _cs2_exec_error: str | None = None
            try:
                from src.models.approval import ApprovalRequest, ApprovalStatus
                from src.services.approval_execution import ApprovalExecutionService

                _can_auto = (
                    bool(rule_kubectl)
                    and rule_risk != RiskLevel.CRITICAL
                    and is_safe_kubectl_action(rule_kubectl)
                    and "NO_ACTION" not in rule_action
                )
                if _can_auto:
                    _auto_approval = ApprovalRequest(
                        incident_id=None,  # 尚未建立，稍後 update_incident_id 補上
                        action=rule_action,
                        description=approval_create.description,
                        requested_by="auto_approve_rule_engine",
                        required_signatures=0,
                        status=ApprovalStatus.APPROVED,
                        risk_level=rule_risk.value,
                        matched_playbook_id=_matched_playbook_id_cs2,
                    )
                    # 使用 DB 中剛建立的 approval.id 讓 executor 可回寫
                    _auto_approval.id = approval.id
                    _cs2_auto_approval = _auto_approval

                    _cs2_executor = ApprovalExecutionService()
                    _cs2_exec_success = await _cs2_executor.execute_approved_action(_auto_approval)

                    # 更新 DB approval 執行狀態
                    try:
                        await service.update_execution_status(approval.id, _cs2_exec_success)
                    except Exception as _upd_err:
                        logger.warning(
                            "cs2_auto_execute_status_update_failed",
                            approval_id=str(approval.id),
                            error=str(_upd_err),
                        )

                    logger.info(
                        "rule_engine_auto_executed",
                        approval_id=str(approval.id),
                        rule_id=rule_response.get("rule_id", "unknown"),
                        kubectl=rule_kubectl,
                        exec_success=_cs2_exec_success,
                    )
            except Exception as _auto_err:
                _cs2_exec_success = False if _cs2_auto_approval is not None else None
                _cs2_exec_error = str(_auto_err)
                logger.warning(
                    "cs2_auto_execute_failed_degraded_to_pending",
                    approval_id=str(approval.id),
                    error=str(_auto_err),
                )

            incident_id = await create_incident_for_approval(
                approval_id=str(approval.id),
                risk_level=rule_risk.value,
                target_resource=target_resource,
                namespace=namespace,
                alert_type=alert_type,
                message=message,
                source="alertmanager",
                alertname=alertname,
                alert_labels=traced_alert_labels,
                notification_type=notification_type,
                alert_category=alert_category,
            )

            try:
                await service.update_incident_id(approval.id, incident_id)
                approval.incident_id = incident_id
            except Exception as _meta_err:
                logger.warning(
                    "rule_bypass_approval_incident_id_update_failed",
                    approval_id=str(approval.id),
                    incident_id=incident_id,
                    error=str(_meta_err),
                )

            await record_alertmanager_event(
                project_id="awoooi",
                alert_id=alert_id,
                alertname=alertname,
                severity=severity,
                namespace=namespace,
                target_resource=target_resource,
                fingerprint=fingerprint,
                stage="incident_linked",
                notification_type=notification_type,
                alert_category=alert_category,
                incident_id=incident_id,
                approval_id=str(approval.id),
                repeat_count=1,
                labels=traced_alert_labels,
                annotations=alert_context.get("annotations", {}),
            )

            if _cs2_auto_approval is not None and _cs2_exec_success is not None:
                try:
                    _cs2_auto_approval.incident_id = incident_id
                    _cs2_executor = _cs2_executor or ApprovalExecutionService()
                    await _cs2_executor.finalize_auto_approved_execution(
                        _cs2_auto_approval,
                        success=_cs2_exec_success,
                        error_message=_cs2_exec_error,
                    )
                except Exception as _cs2_finalize_err:
                    logger.warning(
                        "cs2_auto_execute_finalize_failed",
                        approval_id=str(approval.id),
                        incident_id=incident_id,
                        error=str(_cs2_finalize_err),
                    )

            _is_heartbeat = is_heartbeat_alertname(alertname)
            if can_auto_repair and not _is_heartbeat:
                await _try_auto_repair_background(
                    incident_id=incident_id,
                    approval_id=str(approval.id),
                    alert_type=alert_type,
                    target_resource=target_resource,
                    namespace=namespace,
                )
            elif not can_auto_repair and not _is_heartbeat:
                from src.repositories.alert_operation_log_repository import get_alert_operation_log_repository
                _op_log_rule = get_alert_operation_log_repository()
                await _op_log_rule.append(
                    "GUARDRAIL_BLOCKED",
                    incident_id=incident_id,
                    approval_id=str(approval.id),
                    actor="prometheus-rule",
                    action_detail=f"Prometheus rule 設定 auto_repair=false，轉入緊急介入: {alertname}",
                    success=False,
                    context={"alertname": alertname, "auto_repair_flag": False},
                )
                await _escalate_auto_repair_unavailable(
                    incident_id=incident_id,
                    approval_id=str(approval.id),
                    alert_type=alert_type,
                    target_resource=target_resource,
                    namespace=namespace,
                    failure_reason="Prometheus rule auto_repair=false，未進入自動修復評估",
                    attempted_actions="rule_engine -> rule_first -> emergency_intervention",
                )

            await _push_to_telegram_background(
                approval_id=str(approval.id),
                risk_level=rule_risk.value,
                resource_name=target_resource,
                root_cause=rule_description,
                suggested_action=rule_action,
                estimated_downtime=str(blast.get("estimated_downtime", "N/A")),
                hit_count=1,
                primary_responsibility=str(
                    rule_response.get("primary_responsibility", "INFRA")
                ),
                confidence=float(rule_response.get("confidence", 0.0) or 0.0),
                namespace=namespace,
                incident_id=incident_id,
                notification_type=notification_type,
                alert_category=alert_category,
                fingerprint=fingerprint,
            )

            record_alert_chain_success("alertmanager")
            return

        analysis_result, ai_provider, raw_response, signoz_metrics, signoz_trace_url, ai_tokens, ai_cost = await _analyze_alertmanager_with_timeout(
            openclaw,
            alert_context,
            alert_id=alert_id,
            alertname=alertname,
        )

        if analysis_result:
            risk_mapping = {
                "low": RiskLevel.LOW,
                "medium": RiskLevel.MEDIUM,
                "critical": RiskLevel.CRITICAL,
            }
            risk_level = risk_mapping.get(analysis_result.risk_level.value, RiskLevel.MEDIUM)

            # 2026-04-21 ogt: NO_ACTION/INVESTIGATE/OBSERVE 強制 LOW risk，避免卡 PENDING
            _non_destructive_actions = {"NO_ACTION", "INVESTIGATE", "OBSERVE"}
            _sa_val = (
                analysis_result.suggested_action.value
                if hasattr(analysis_result.suggested_action, "value")
                else str(analysis_result.suggested_action)
            )
            if _sa_val in _non_destructive_actions:
                risk_level = RiskLevel.LOW

            blast = analysis_result.blast_radius
            impact_mapping = {
                "NONE": DataImpact.NONE,
                "READ_ONLY": DataImpact.READ_ONLY,
                "WRITE": DataImpact.WRITE,
                "DESTRUCTIVE": DataImpact.DESTRUCTIVE,
            }
            data_impact = impact_mapping.get(blast.data_impact.value, DataImpact.NONE) if blast else DataImpact.NONE

            # 2026-04-27 Claude Sonnet 4.6: shadow-run Step1 — 補 metadata kwarg，讓 extra_metadata 可觀測
            _cmd_cs3 = (analysis_result.kubectl_command or "").strip()
            _guarded_action_cs3 = await guard_alert_approval_action(
                action=(_cmd_cs3 or f"{analysis_result.action_title} | NO_ACTION"),
                alert_namespace=namespace,
                alertname=alertname,
                alert_category=alert_category,
            )
            _matched_playbook_id_cs3 = await resolve_playbook_id_for_alert(
                rule_id=str(rule_response.get("rule_id", "")),
                alertname=alertname,
                affected_services=analysis_result.affected_services
                or ([target_resource] if target_resource else []),
                severity=risk_level.value,
            )
            if _guarded_action_cs3.blocked:
                risk_level = RiskLevel.LOW
                _cmd_cs3 = ""

            _approval_metadata_cs3 = {
                "source": ai_provider,
                "confidence_score": analysis_result.confidence,
                "is_rule_based": False,
                "rule_id": str(rule_response.get("rule_id", "")) or None,
                "playbook_id": _matched_playbook_id_cs3,
                **_guarded_action_cs3.metadata,
            }
            approval_create = ApprovalRequestCreate(
                action=_guarded_action_cs3.action,
                description=f"[AI: {ai_provider}] {analysis_result.action_title} | {analysis_result.description}",
                risk_level=risk_level,
                blast_radius=BlastRadius(
                    affected_pods=blast.affected_pods if blast else 1,
                    estimated_downtime=blast.estimated_downtime if blast else "~30s",
                    related_services=list(set((blast.related_services if blast else []) + analysis_result.affected_services)),
                    data_impact=data_impact,
                ),
                dry_run_checks=[
                    DryRunCheck(name="AI 信心度", passed=analysis_result.confidence >= 0.7, message=f"{analysis_result.confidence:.0%}"),
                    DryRunCheck(name="來源", passed=True, message="alertmanager"),
                ],
                requested_by=f"OpenClaw ({ai_provider})",
                metadata=_approval_metadata_cs3,
                matched_playbook_id=_matched_playbook_id_cs3,
            )

            approval = await service.create_approval_with_fingerprint(
                request=approval_create,
                fingerprint=fingerprint,
            )

            # 2026-04-27 Claude Sonnet 4.6: shadow-run Step2 — 只記 log，不改執行決策
            try:
                _shadow_proposal_cs3 = {
                    "risk_level": risk_level.value,
                    "confidence": analysis_result.confidence,
                    "action": approval_create.action,
                    "kubectl_command": _cmd_cs3,
                    "is_rule_based": False,
                    "source": ai_provider,
                }
                _shadow_result_cs3 = get_auto_approve_policy().evaluate(_shadow_proposal_cs3)
                logger.info(
                    "shadow_auto_approve_result",
                    approval_id=str(approval.id),
                    should_auto=_shadow_result_cs3.should_auto_approve,
                    reason=_shadow_result_cs3.reason.value,
                    source=ai_provider,
                )
            except Exception as _shadow_err_cs3:
                logger.warning("shadow_auto_approve_failed", error=str(_shadow_err_cs3))

            # 2026-04-27 Claude Sonnet 4.6: CS3 LLM 高信心自動執行（修法3擴展）
            _cs3_kubectl = _cmd_cs3
            _cs3_can_auto = (
                bool(_cs3_kubectl)
                and analysis_result.confidence >= 0.85
                and risk_level != RiskLevel.CRITICAL
                and "NO_ACTION" not in (analysis_result.action_title or "")
                and is_safe_kubectl_action(_cs3_kubectl)
            )
            _cs3_auto_approval = None
            _cs3_executor = None
            _cs3_exec_success: bool | None = None
            _cs3_exec_error: str | None = None
            if _cs3_can_auto:
                try:
                    from src.models.approval import ApprovalRequest, ApprovalStatus
                    from src.services.approval_execution import ApprovalExecutionService

                    _cs3_auto_approval = ApprovalRequest(
                        action=approval_create.action,
                        description=approval_create.description,
                        requested_by="auto_approve_llm_cs3",
                        required_signatures=0,
                        status=ApprovalStatus.APPROVED,
                        risk_level=risk_level.value,
                        matched_playbook_id=_matched_playbook_id_cs3,
                        metadata={
                            **_approval_metadata_cs3,
                            "is_high_confidence": True,
                            "policy_reason": _shadow_result_cs3.reason.value
                            if hasattr(_shadow_result_cs3, "reason")
                            else "cs3_auto_confident_execution",
                        },
                    )
                    _cs3_auto_approval.id = approval.id
                    _cs3_executor = ApprovalExecutionService()
                    _cs3_exec_success = await _cs3_executor.execute_approved_action(_cs3_auto_approval)
                    try:
                        await service.update_execution_status(approval.id, _cs3_exec_success)
                    except Exception as _cs3_upd_err:
                        logger.warning(
                            "cs3_auto_execute_status_update_failed",
                            approval_id=str(approval.id),
                            error=str(_cs3_upd_err),
                        )
                    logger.info(
                        "cs3_llm_auto_executed",
                        approval_id=str(approval.id),
                        kubectl=_cs3_kubectl,
                        confidence=analysis_result.confidence,
                        success=_cs3_exec_success,
                        provider=ai_provider,
                        is_high_confidence=True,
                        policy_reason=(
                            _shadow_result_cs3.reason.value
                            if hasattr(_shadow_result_cs3, "reason")
                            else "cs3_auto_confident_execution"
                        ),
                    )
                except Exception as _cs3_exec_err:
                    _cs3_exec_success = False if _cs3_auto_approval is not None else None
                    _cs3_exec_error = str(_cs3_exec_err)
                    logger.warning("cs3_llm_auto_execute_failed", error=str(_cs3_exec_err))

            incident_id = await create_incident_for_approval(
                approval_id=str(approval.id),
                risk_level=risk_level.value,
                target_resource=target_resource,
                namespace=namespace,
                alert_type=alert_type,
                message=message,
                source="alertmanager",
                alertname=alertname,
                alert_labels=traced_alert_labels,
                notification_type=notification_type,
                alert_category=alert_category,
            )

            try:
                await service.update_incident_id(approval.id, incident_id)
                approval.incident_id = incident_id
            except Exception as _meta_err:
                logger.warning(
                    "approval_incident_id_update_failed",
                    approval_id=str(approval.id),
                    incident_id=incident_id,
                    error=str(_meta_err),
                )

            await record_alertmanager_event(
                project_id="awoooi",
                alert_id=alert_id,
                alertname=alertname,
                severity=severity,
                namespace=namespace,
                target_resource=target_resource,
                fingerprint=fingerprint,
                stage="incident_linked",
                notification_type=notification_type,
                alert_category=alert_category,
                incident_id=incident_id,
                approval_id=str(approval.id),
                repeat_count=1,
                labels=traced_alert_labels,
                annotations=alert_context.get("annotations", {}),
            )

            if _cs3_auto_approval is not None and _cs3_exec_success is not None:
                try:
                    _cs3_auto_approval.incident_id = incident_id
                    _cs3_executor = _cs3_executor or ApprovalExecutionService()
                    await _cs3_executor.finalize_auto_approved_execution(
                        _cs3_auto_approval,
                        success=_cs3_exec_success,
                        error_message=_cs3_exec_error,
                    )
                except Exception as _cs3_finalize_err:
                    logger.warning(
                        "cs3_auto_execute_finalize_failed",
                        approval_id=str(approval.id),
                        incident_id=incident_id,
                        error=str(_cs3_finalize_err),
                    )

            root_cause = analysis_result.description or message
            estimated_downtime = blast.estimated_downtime if blast else "~30s"
            primary_responsibility = analysis_result.primary_responsibility or "COLLAB"
            confidence = analysis_result.confidence

            _is_heartbeat = is_heartbeat_alertname(alertname)
            if _is_heartbeat:
                logger.info(
                    "auto_repair_skipped_heartbeat",
                    incident_id=incident_id,
                    alertname=alertname,
                )

            if can_auto_repair and not _is_heartbeat:
                await _try_auto_repair_background(
                    incident_id=incident_id,
                    approval_id=str(approval.id),
                    alert_type=alert_type,
                    target_resource=target_resource,
                    namespace=namespace,
                )
            else:
                from src.repositories.alert_operation_log_repository import get_alert_operation_log_repository
                _op_log_rule = get_alert_operation_log_repository()
                await _op_log_rule.append(
                    "GUARDRAIL_BLOCKED",
                    incident_id=incident_id,
                    approval_id=str(approval.id),
                    actor="prometheus-rule",
                    action_detail=f"Prometheus rule 設定 auto_repair=false，強制人工審核: {alertname}",
                    success=False,
                    context={"alertname": alertname, "auto_repair_flag": False},
                )
                if not _is_heartbeat:
                    await _escalate_auto_repair_unavailable(
                        incident_id=incident_id,
                        approval_id=str(approval.id),
                        alert_type=alert_type,
                        target_resource=target_resource,
                        namespace=namespace,
                        failure_reason="Prometheus rule auto_repair=false，未進入自動修復評估",
                        attempted_actions="llm_analysis -> guardrail:auto_repair_false -> emergency_intervention",
                    )

            await _push_to_telegram_background(
                approval_id=str(approval.id),
                risk_level=risk_level.value,
                resource_name=target_resource,
                root_cause=root_cause,
                suggested_action=approval_create.action,
                estimated_downtime=estimated_downtime,
                hit_count=1,
                primary_responsibility=primary_responsibility,
                confidence=confidence,
                namespace=namespace,
                signoz_rps=signoz_metrics.rps if signoz_metrics else 0,
                signoz_rps_trend=signoz_metrics.rps_trend if signoz_metrics else "stable",
                signoz_error_rate=signoz_metrics.error_rate if signoz_metrics else 0,
                signoz_p99_latency=signoz_metrics.p99_latency_ms if signoz_metrics else 0,
                signoz_latency_trend=signoz_metrics.latency_trend if signoz_metrics else "stable",
                signoz_trace_url=signoz_trace_url or "",
                ai_tokens=ai_tokens,
                ai_cost=ai_cost,
                ai_provider=ai_provider,
                incident_id=incident_id,
                notification_type=notification_type,
                alert_category=alert_category,
                fingerprint=fingerprint,
            )

            record_alert_chain_success("alertmanager")

        else:
            # LLM 失敗時，不再把 NO_ACTION 當成終點。
            # 先用預配置 approval id 建立 incident，讓後續 MCP evidence、
            # PlayBook trust、approval 與 Telegram 都指向同一條真相鏈。
            preallocated_approval_id = str(uuid.uuid4())
            _matched_playbook_id_cs4 = await resolve_playbook_id_for_alert(
                rule_id=str(rule_response.get("rule_id", "")),
                alertname=alertname,
                affected_services=[target_resource] if target_resource else [],
                severity="medium",
            )
            fallback_incident_id = await create_incident_for_approval(
                approval_id=preallocated_approval_id,
                risk_level="medium",
                target_resource=target_resource,
                namespace=namespace,
                alert_type=alert_type,
                message=message,
                source="alertmanager",
                alertname=alertname,
                alert_labels=traced_alert_labels,
                notification_type=notification_type,
                alert_category=alert_category,
            )

            fallback_action_text = (
                "NO_ACTION - REPAIR_CANDIDATE_MISSING: "
                "LLM 分析失敗，MCP evidence / PlayBook trust 尚未產生可安全執行的修復指令"
            )
            repair_candidate_result = await get_repair_candidate_service().build_from_incident_id(
                incident_id=fallback_incident_id,
                alertname=alertname,
                target_resource=target_resource,
                namespace=namespace,
                message=message,
                fallback_action=fallback_action_text,
                matched_playbook_id=_matched_playbook_id_cs4,
                rule_id=str(rule_response.get("rule_id", "")),
                severity="medium",
            )

            _approval_metadata_cs4 = {
                "source": "llm_fallback_mcp_playbook_candidate",
                "confidence_score": None,
                "is_rule_based": False,
                "playbook_id": _matched_playbook_id_cs4,
                "preallocated_approval_id": preallocated_approval_id,
            }
            _approval_metadata_cs4.update(repair_candidate_result.metadata)
            _approval_metadata_cs4["preallocated_approval_id"] = preallocated_approval_id

            candidate_confidence = 0.0
            if repair_candidate_result.candidate_found and repair_candidate_result.approval_request:
                evidence = repair_candidate_result.evidence
                playbook = repair_candidate_result.playbook
                evidence_ratio = 0.0
                if evidence and evidence.sensors_attempted:
                    evidence_ratio = evidence.sensors_succeeded / max(evidence.sensors_attempted, 1)
                trust_score = float(playbook.trust_score) if playbook else 0.0
                candidate_confidence = min(0.82, 0.45 + evidence_ratio * 0.2 + trust_score * 0.2)
                fallback_create = repair_candidate_result.approval_request.model_copy(
                    update={
                        "incident_id": fallback_incident_id,
                        "metadata": _approval_metadata_cs4,
                    }
                )
                telegram_root_cause = (
                    "LLM fallback 後已由 MCP evidence + PlayBook trust 產生修復候選；"
                    "等待人工批准後進入 execution / verifier / KM 回寫。"
                )
                primary_responsibility = "OPENCLAW_PLAYBOOK"
            else:
                blockers = repair_candidate_result.blockers or ["repair_candidate_missing"]
                blocker_text = str(
                    repair_candidate_result.metadata.get("repair_candidate_blocker_summary")
                    or ", ".join(blockers)
                )
                next_step = str(
                    repair_candidate_result.metadata.get("repair_candidate_next_step")
                    or "建立人工處置包並補 PlayBook 草案欄位；完成 owner review 後再重跑候選生成。"
                )
                fallback_create = ApprovalRequestCreate(
                    action=f"NO_ACTION - REPAIR_CANDIDATE_MISSING: {blocker_text}",
                    description=(
                        f"[LLM Failed] {message}\n"
                        f"修復候選阻擋：{blocker_text}\n"
                        f"下一步：{next_step}"
                    ),
                    risk_level=RiskLevel.LOW,
                    blast_radius=BlastRadius(
                        affected_pods=1,
                        estimated_downtime="unknown",
                        related_services=[target_resource] if target_resource else [],
                        data_impact=DataImpact.NONE,
                    ),
                    dry_run_checks=[
                        DryRunCheck(
                            name="MCP/PlayBook candidate gate",
                            passed=False,
                            message=blocker_text[:240],
                        ),
                        DryRunCheck(
                            name="Repair PlayBook draft package",
                            passed=False,
                            message=next_step[:240],
                        )
                    ],
                    requested_by="OpenClaw (fallback candidate gate)",
                    incident_id=fallback_incident_id,
                    metadata=_approval_metadata_cs4,
                    matched_playbook_id=_matched_playbook_id_cs4,
                )
                telegram_root_cause = (
                    f"LLM fallback 後未產生修復候選；阻擋：{blocker_text}；下一步：{next_step}"
                )
                primary_responsibility = "HUMAN"

            approval = await service.create_approval_with_fingerprint(
                request=fallback_create,
                fingerprint=fingerprint,
            )

            # 2026-04-27 Claude Sonnet 4.6: shadow-run Step2 — 只記 log，不改執行決策
            try:
                _shadow_proposal_cs4 = {
                    "risk_level": fallback_create.risk_level.value,
                    "confidence": candidate_confidence,
                    "action": fallback_create.action,
                    "kubectl_command": fallback_create.action if fallback_create.action.startswith("kubectl") else "",
                    "is_rule_based": False,
                    "source": _approval_metadata_cs4.get("source", "fallback"),
                }
                _shadow_result_cs4 = get_auto_approve_policy().evaluate(_shadow_proposal_cs4)
                logger.info(
                    "shadow_auto_approve_result",
                    approval_id=str(approval.id),
                    should_auto=_shadow_result_cs4.should_auto_approve,
                    reason=_shadow_result_cs4.reason.value,
                    source="fallback_candidate",
                )
            except Exception as _shadow_err_cs4:
                logger.warning("shadow_auto_approve_failed", error=str(_shadow_err_cs4))

            try:
                await service.update_incident_id(approval.id, fallback_incident_id)
                approval.incident_id = fallback_incident_id
            except Exception as _meta_err:
                logger.warning(
                    "fallback_approval_incident_id_update_failed",
                    approval_id=str(approval.id),
                    incident_id=fallback_incident_id,
                    error=str(_meta_err),
                )

            await record_alertmanager_event(
                project_id="awoooi",
                alert_id=alert_id,
                alertname=alertname,
                severity=severity,
                namespace=namespace,
                target_resource=target_resource,
                fingerprint=fingerprint,
                stage="incident_linked",
                notification_type=notification_type,
                alert_category=alert_category,
                incident_id=fallback_incident_id,
                approval_id=str(approval.id),
                repeat_count=1,
                labels=traced_alert_labels,
                annotations=alert_context.get("annotations", {}),
            )

            _is_heartbeat = is_heartbeat_alertname(alertname)
            if not _is_heartbeat:
                from src.repositories.alert_operation_log_repository import get_alert_operation_log_repository
                _op_log_fallback = get_alert_operation_log_repository()
                if repair_candidate_result.candidate_found:
                    await _op_log_fallback.append(
                        "REPAIR_CANDIDATE_READY",
                        incident_id=fallback_incident_id,
                        approval_id=str(approval.id),
                        actor="openclaw-repair-candidate",
                        action_detail=f"MCP evidence + PlayBook trust 產生候選，等待批准: {fallback_create.action[:220]}",
                        success=True,
                        context={
                            "alertname": alertname,
                            "auto_repair_flag": bool(can_auto_repair),
                            "playbook_id": fallback_create.matched_playbook_id,
                            "candidate_status": "ready_for_approval",
                        },
                    )
                else:
                    await _op_log_fallback.append(
                        "REPAIR_CANDIDATE_BLOCKED",
                        incident_id=fallback_incident_id,
                        approval_id=str(approval.id),
                        actor="openclaw-repair-candidate",
                        action_detail=f"fallback 未產生候選: {fallback_create.action[:220]}",
                        success=False,
                        context={
                            "alertname": alertname,
                            "auto_repair_flag": bool(can_auto_repair),
                            "blockers": repair_candidate_result.blockers,
                        },
                    )
                    await _escalate_auto_repair_unavailable(
                        incident_id=fallback_incident_id,
                        approval_id=str(approval.id),
                        alert_type=alert_type,
                        target_resource=target_resource,
                        namespace=namespace,
                        failure_reason=telegram_root_cause,
                        attempted_actions=(
                            "llm_fallback -> mcp_evidence -> playbook_trust -> "
                            f"candidate_blocked:{','.join(repair_candidate_result.blockers or ['unknown'])}"
                        ),
                    )

            await _push_to_telegram_background(
                approval_id=str(approval.id),
                risk_level=fallback_create.risk_level.value,
                resource_name=target_resource,
                root_cause=telegram_root_cause,
                suggested_action=fallback_create.action,
                estimated_downtime="unknown",
                hit_count=1,
                primary_responsibility=primary_responsibility,
                confidence=candidate_confidence,
                namespace=namespace,
                incident_id=fallback_incident_id,
                notification_type=notification_type,
                alert_category=alert_category,
                fingerprint=fingerprint,
                repair_candidate_blocker_summary=str(
                    _approval_metadata_cs4.get("repair_candidate_blocker_summary") or ""
                ),
                repair_candidate_next_step=str(
                    _approval_metadata_cs4.get("repair_candidate_next_step") or ""
                ),
                repair_candidate_required_fields=(
                    _approval_metadata_cs4.get("repair_candidate_draft_package", {}).get(
                        "required_fields", []
                    )
                    if isinstance(_approval_metadata_cs4.get("repair_candidate_draft_package"), dict)
                    else []
                ),
                repair_candidate_work_item_href=str(
                    (
                        _approval_metadata_cs4.get("repair_candidate_draft_package", {})
                        .get("awooop_work_item", {})
                        .get("work_item_url", "")
                    )
                    if isinstance(_approval_metadata_cs4.get("repair_candidate_draft_package"), dict)
                    else ""
                ),
                repair_candidate_work_item_id=str(
                    (
                        _approval_metadata_cs4.get("repair_candidate_draft_package", {})
                        .get("awooop_work_item", {})
                        .get("work_item_id", "")
                    )
                    if isinstance(_approval_metadata_cs4.get("repair_candidate_draft_package"), dict)
                    else ""
                ),
            )

    except Exception as e:
        logger.error(
            "process_new_alert_background_error",
            alert_id=alert_id,
            alertname=alertname,
            error=str(e),
        )


@router.post(
    "/alertmanager",
    response_model=AlertResponse,
    summary="Phase 10: Alertmanager 原生格式 (內網免 HMAC)",
    description="接收 Alertmanager Webhook，內網來源免 HMAC 驗證，觸發 LLM 分析 + Telegram 通知。",
)
async def alertmanager_webhook(
    request: Request,
    payload: AlertmanagerPayload,
    background_tasks: BackgroundTasks,
) -> AlertResponse:
    """
    接收 Alertmanager 格式告警並觸發完整 AWOOOI 流程

    原始架構流程 (phase5_telemetry_architecture.md):
    Alertmanager → /alertmanager → Alert Normalizer → Fingerprint → LLM → Telegram

    安全策略:
    - 內網 IP (192.168.x.x, 10.x.x.x, 172.x.x.x): 免 HMAC
    - 外網 IP: 拒絕
    """
    # 取得客戶端 IP
    client_ip = request.client.host if request.client else "unknown"
    forwarded_for = request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
    actual_ip = forwarded_for or client_ip

    # 內網檢查
    if not is_internal_ip(actual_ip):
        logger.warning(
            "alertmanager_external_rejected",
            client_ip=actual_ip,
            reason="External IP must use /alerts with HMAC",
        )
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="External sources must use /alerts endpoint with HMAC signature",
        )

    logger.info(
        "alertmanager_webhook_received",
        client_ip=actual_ip,
        status=payload.status,
        alert_count=len(payload.alerts),
    )

    # 只處理第一個 firing 告警 (避免告警風暴)
    firing_alerts = [a for a in payload.alerts if a.status == "firing"]
    if not firing_alerts:
        return AlertResponse(
            success=True,
            message="No firing alerts to process",
            alert_id="",
            approval_created=False,
        )

    alert = firing_alerts[0]
    alert_id = f"alert-{now_taipei().strftime('%Y%m%d%H%M%S')}"

    # ==========================================================================
    # Sprint 5.1 L4-2: ALERT_RECEIVED 溯源記錄 + auto_repair flag 讀取
    # (2026-04-08 Claude Sonnet 4.6 Asia/Taipei，ADR-062 Q9)
    # ==========================================================================
    _alert_labels = alert.labels or {}
    _alert_annotations = alert.annotations or {}
    _alertname_for_log = _alert_labels.get("alertname", "UnknownAlert")
    # Q9: auto_repair flag — Rule=false 強制 HITL（不觸發自動修復背景任務）
    _can_auto_repair_by_rule = _alert_labels.get("auto_repair", "true").lower() == "true"
    try:
        from src.repositories.alert_operation_log_repository import get_alert_operation_log_repository
        _op_log = get_alert_operation_log_repository()
        await _op_log.append(
            "ALERT_RECEIVED",
            actor="alertmanager",
            action_detail=f"收到告警: {_alertname_for_log}",
            context={
                "source": "alertmanager",
                "alert_id": alert_id,
                "alertname": _alertname_for_log,
                "labels": _alert_labels,
                "annotations": _alert_annotations,
                "auto_repair_flag": _can_auto_repair_by_rule,
            },
        )
    except Exception as _log_err:
        logger.warning("alert_received_log_failed", error=str(_log_err))

    # ==========================================================================
    # Alert Normalizer: 轉換 Alertmanager 格式 → AWOOOI AlertPayload
    # ==========================================================================
    alertname = alert.labels.get("alertname", "UnknownAlert")

    # ==========================================================================
    # 2026-03-30 P1: CI/CD 告警偵測 - 配置化 (constants.py)
    # 跳過 AI 仲裁，使用簡潔格式
    # ==========================================================================
    if is_cicd_alertname(alertname):
        # CI/CD 告警 - 使用簡潔格式，不走 AI 仲裁
        logger.info(
            "alertmanager_cicd_detected",
            alert_id=alert_id,
            alertname=alertname,
            message="Bypassing AI arbitration for CI/CD alert",
        )

        try:
            telegram = get_telegram_gateway()
            # 解析 CI/CD 狀態
            stage = alert.labels.get("stage", "")
            job_status = _cicd_job_status_from_alert(alert)
            commit_sha = alert.labels.get("commit", "")
            triggered_by = alert.labels.get("triggered_by", "CI")
            workflow_url = alert.annotations.get("workflow_url", "")
            summary = alert.annotations.get("summary", alertname)
            detail_message = alert.annotations.get("description", "")

            await telegram.send_cicd_progress(
                job_name=summary,
                status=job_status,
                stage=stage,
                commit_sha=commit_sha,
                triggered_by=triggered_by,
                duration_seconds=_cicd_duration_seconds_from_alert(alert),
                message=detail_message,
                workflow_url=workflow_url,
            )

            record_alert_chain_success("alertmanager")
            return AlertResponse(
                success=True,
                message=f"CI/CD alert processed (simple format): {alertname}",
                alert_id=alert_id,
                approval_created=False,
            )
        except Exception as e:
            logger.error("cicd_telegram_failed", error=str(e), alertname=alertname)
            # CI/CD 通知失敗不阻擋流程
            return AlertResponse(
                success=True,
                message=f"CI/CD alert logged (telegram failed): {alertname}",
                alert_id=alert_id,
                approval_created=False,
            )

    # I1 整合 ADR-064 Rule Engine 2026-04-11: YAML 規則動態推斷，ALERTNAME_TO_TYPE 為 fallback
    # 2026-04-12 ogt: AlertPayload.alert_type 只接受 8 個 Literal，超範圍的改為 "custom"
    _ALERT_TYPE_ALLOWED = {
        "k8s_node_failure", "k8s_pod_crash", "db_connection_timeout", "service_404",
        "high_cpu", "high_memory", "disk_full", "ssl_expiry", "custom",
    }
    alert_type = get_incident_type(alertname)
    if alert_type not in _ALERT_TYPE_ALLOWED:
        alert_type = "custom"

    # ADR-073 Phase 2-2: 早期分診 — 在 LLM 前決定 alert_category + notification_type
    # 2026-04-12 ogt: 防止 HostBackupFailed 等被誤路由到 K8s executor
    # 2026-04-25 ogt + Claude Sonnet 4.6: 計算告警 age 供備份告警升級判斷
    # HostBackupFailed + age > 24h → TYPE-3（P0 修復），而非 TYPE-1（純資訊）
    _alert_age_hours: float = 0.0
    if alert.startsAt:
        try:
            from datetime import datetime, timezone
            _starts_at = datetime.fromisoformat(alert.startsAt.replace("Z", "+00:00"))
            _alert_age_hours = (datetime.now(timezone.utc) - _starts_at).total_seconds() / 3600
        except (ValueError, TypeError):
            pass  # 解析失敗視為 age=0，不影響主流程

    alert_category, notification_type = classify_alert_early(
        alertname=alertname,
        severity=alert.labels.get("severity", "warning"),
        labels=alert.labels,
        age_hours=_alert_age_hours,
    )

    severity_map = {"critical": "critical", "warning": "warning", "info": "info"}
    severity = severity_map.get(
        alert.labels.get("severity", "warning").lower(),
        "warning"
    )

    # target_resource 提取優先順序 (2026-04-11 Claude Sonnet 4.6 全自動化修正)
    # component (Docker 服務名) > pod (K8s) > name/container (Cadvisor 容器名) > job > instance IP > alertname
    # 關鍵：Docker 告警 (DockerContainerUnhealthy/DockerContainerExited) 的容器名在 name label
    # 過去 fallback 直接用 alertname，導致 target_resource="DockerContainerUnhealthy" 污染整個修復流程
    _instance = alert.labels.get("instance", "")
    _instance_clean = _instance.split(":")[0] if _instance and ":" in _instance else _instance
    target_resource = (
        alert.labels.get("component")
        or alert.labels.get("pod")
        or alert.labels.get("name")          # Cadvisor/cAdvisor 容器名
        or alert.labels.get("container")     # K8s container name
        or alert.labels.get("job")           # Prometheus job name（次優）
        or (_instance_clean if _instance_clean and not is_internal_ip(_instance_clean) else None)
        or alertname
    )
    namespace = alert.labels.get("namespace", "default")
    message = alert.annotations.get("summary") or alert.annotations.get("description") or alertname

    # 建立正規化的 AlertPayload
    normalized_alert = AlertPayload(
        alert_type=alert_type,
        severity=severity,
        source="alertmanager",
        target_resource=target_resource,
        namespace=namespace,
        message=message,
        metrics={},
        labels=alert.labels,
    )

    # ==========================================================================
    # 告警指紋 + 收斂
    # ==========================================================================
    fingerprint = AlertAnalyzer.generate_fingerprint(normalized_alert)

    logger.info(
        "alertmanager_normalized",
        alert_id=alert_id,
        alert_type=alert_type,
        severity=severity,
        target=target_resource,
        fingerprint=fingerprint,
    )
    background_tasks.add_task(
        record_alertmanager_event,
        project_id="awoooi",
        alert_id=alert_id,
        alertname=alertname,
        severity=severity,
        namespace=namespace,
        target_resource=target_resource,
        fingerprint=fingerprint,
        stage="received",
        notification_type=notification_type,
        alert_category=alert_category,
        source_url=alert.generatorURL,
        labels=dict(alert.labels) if alert.labels else {},
        annotations=dict(alert.annotations) if alert.annotations else {},
    )

    # ==========================================================================
    # ADR-076: 告警聚合引擎 — 5 分鐘滑動視窗，防止告警風暴
    # 2026-04-14 Claude Haiku 4.5 Asia/Taipei
    # 位置：指紋生成後、LLM 分析前（短路子告警）
    # ==========================================================================
    try:
        grouping_result = await get_alert_grouping_service().evaluate(
            alertname=alertname,
            namespace=namespace,
            fingerprint=fingerprint,
        )
    except Exception as e:
        grouping_result = None
        logger.warning(
            "alertmanager_grouping_failed_fail_open",
            alert_id=alert_id,
            fingerprint=fingerprint,
            error=str(e),
        )

    if grouping_result and grouping_result.is_grouped:
        logger.info(
            "alertmanager_grouped_skip",
            alert_id=alert_id,
            group_key=grouping_result.group_key,
            count=grouping_result.count,
            parent_fingerprint=grouping_result.parent_fingerprint,
            reason="Alert storm suppressed — child alert within 5-min window",
        )
        background_tasks.add_task(
            record_grouped_alert_event,
            project_id="awoooi",
            alert_id=alert_id,
            alertname=alertname,
            severity=severity,
            namespace=namespace,
            target_resource=target_resource,
            group_key=grouping_result.group_key,
            count=grouping_result.count,
            parent_fingerprint=grouping_result.parent_fingerprint,
            fingerprint=fingerprint,
        )
        return AlertResponse(
            success=True,
            message=(
                f"🛡️ 告警聚合 (x{grouping_result.count}) — "
                f"同分組 5 分鐘內第 {grouping_result.count} 個告警，已合併為父告警"
            ),
            alert_id=alert_id,
            approval_created=False,
            converged=True,
        )

    try:
        service = get_approval_service()

        # 查詢是否有同指紋的現有記錄
        existing_approval = await service.find_by_fingerprint(
            fingerprint=fingerprint,
            debounce_minutes=DEBOUNCE_WINDOW_MINUTES,
        )

        if existing_approval:
            # 收斂告警 - 跳過 LLM
            logger.info(
                "alertmanager_converged",
                alert_id=alert_id,
                fingerprint=fingerprint,
                existing_id=str(existing_approval.id),
            )

            updated_approval = await service.increment_hit_count(existing_approval.id)
            if updated_approval:
                # 2026-03-27 ogt: 收斂告警不重複發送 Telegram，只更新 hit_count
                # 用戶可在 UI 查看聚合次數，避免 Telegram 洗版
                logger.info(
                    "alertmanager_converged_telegram_recurrence_scheduled",
                    approval_id=str(updated_approval.id),
                    hit_count=updated_approval.hit_count,
                    reason="Converged alert - scheduling throttled recurrence notice",
                )
                background_tasks.add_task(
                    record_alertmanager_event,
                    project_id="awoooi",
                    alert_id=alert_id,
                    alertname=alertname,
                    severity=severity,
                    namespace=namespace,
                    target_resource=target_resource,
                    fingerprint=fingerprint,
                    stage="converged",
                    notification_type=notification_type,
                    alert_category=alert_category,
                    incident_id=getattr(updated_approval, "incident_id", None),
                    approval_id=str(updated_approval.id),
                    repeat_count=updated_approval.hit_count,
                    is_duplicate=True,
                    source_url=alert.generatorURL,
                    labels=dict(alert.labels) if alert.labels else {},
                    annotations=dict(alert.annotations) if alert.annotations else {},
                )
                background_tasks.add_task(
                    notify_converged_alert_recurrence,
                    source="alertmanager",
                    fingerprint=fingerprint,
                    alertname=alertname,
                    severity=severity,
                    namespace=namespace,
                    target_resource=target_resource,
                    hit_count=updated_approval.hit_count,
                    incident_id=getattr(updated_approval, "incident_id", None),
                    approval_id=str(updated_approval.id),
                    alert_category=alert_category,
                    notification_type=notification_type,
                )

                return AlertResponse(
                    success=True,
                    message=f"🛡️ 告警收斂 (x{updated_approval.hit_count}) - 已排程節流再通知",
                    alert_id=alert_id,
                    approval_created=False,
                    approval_id=str(updated_approval.id),
                    risk_level=updated_approval.risk_level.value,
                    suggested_action=updated_approval.action,
                    hit_count=updated_approval.hit_count,
                    converged=True,
                )

        # ==========================================================================
        # ADR-073 修補: TYPE-1 短路 — 純資訊告警不進 LLM，直接發純文字通知
        # 之前分類了但沒有守衛，Heartbeat/Backup/Info 仍然浪費 LLM token
        # ==========================================================================
        if notification_type == "TYPE-1":
            _info_incident_id = await create_incident_for_approval(
                approval_id=str(uuid.uuid4()),  # 純資訊無 approval，UUID 佔位避免 ValueError
                risk_level="low",
                target_resource=target_resource,
                namespace=namespace,
                alert_type=alert_type,
                message=message,
                source="alertmanager",
                alertname=alertname,
                alert_labels={**alert.labels, "fingerprint": fingerprint, "alert_id": alert_id},
                notification_type="TYPE-1",
                alert_category=alert_category,
            )
            background_tasks.add_task(
                record_alertmanager_event,
                project_id="awoooi",
                alert_id=alert_id,
                alertname=alertname,
                severity=severity,
                namespace=namespace,
                target_resource=target_resource,
                fingerprint=fingerprint,
                stage="incident_linked",
                notification_type="TYPE-1",
                alert_category=alert_category,
                incident_id=_info_incident_id,
                source_url=alert.generatorURL,
                labels={**alert.labels, "fingerprint": fingerprint, "alert_id": alert_id},
                annotations=dict(alert.annotations) if alert.annotations else {},
            )
            # 2026-04-15 ogt: TYPE-1 純資訊告警建立後立即關閉
            # 設計原則: backup/heartbeat/info 告警無需追蹤狀態，通知即完成
            # 防止 incidents 表無限累積 INVESTIGATING 記錄（ADR-073 漏洞修補）
            background_tasks.add_task(
                get_incident_service().resolve_incident,
                _info_incident_id,
                "info_notification",
            )
            background_tasks.add_task(
                get_telegram_gateway().send_info_notification,
                incident_id=_info_incident_id,
                title=alertname,
                message=message,
                alertname=alertname,
                severity=severity,
            )
            record_alert_chain_success("alertmanager")
            return AlertResponse(
                success=True,
                message="✅ TYPE-1 純資訊告警已通知 (no LLM)",
                alert_id=alert_id,
                approval_created=False,
            )

        if not await try_acquire_alertmanager_llm_lock(fingerprint, alert_id):
            logger.info(
                "alertmanager_llm_inflight_suppressed",
                alert_id=alert_id,
                fingerprint=fingerprint,
                ttl_seconds=ALERTMANAGER_LLM_INFLIGHT_LOCK_TTL_SECONDS,
            )
            background_tasks.add_task(
                record_alertmanager_event,
                project_id="awoooi",
                alert_id=alert_id,
                alertname=alertname,
                severity=severity,
                namespace=namespace,
                target_resource=target_resource,
                fingerprint=fingerprint,
                stage="llm_inflight_suppressed",
                notification_type=notification_type,
                alert_category=alert_category,
                is_duplicate=True,
                source_url=alert.generatorURL,
                labels=dict(alert.labels) if alert.labels else {},
                annotations=dict(alert.annotations) if alert.annotations else {},
            )
            background_tasks.add_task(
                notify_converged_alert_recurrence,
                source="alertmanager",
                fingerprint=fingerprint,
                alertname=alertname,
                severity=severity,
                namespace=namespace,
                target_resource=target_resource,
                hit_count=2,
                incident_id=None,
                approval_id=None,
                alert_category=alert_category,
                notification_type=notification_type,
                recurrence_stage="llm_inflight",
            )
            return AlertResponse(
                success=True,
                message="🛡️ 告警已由同指紋背景 AI 分析處理中，已排程節流再通知",
                alert_id=alert_id,
                approval_created=False,
                converged=True,
            )

        # ==========================================================================
        # ADR-089 (2026-04-17 ogt + Claude Sonnet 4.6): 新告警 — 背景 LLM 分析
        # 立即回傳 202，AI 辯證在背景非同步執行
        # 修復根因: 同步等待 90s LLM → Alertmanager 超時 → Fallback 風暴
        # ==========================================================================
        alert_context = {
            "alertname": alertname,             # 主要識別符 — LLM 必讀
            "alert_category": alert_category,   # kubernetes/database/storage/host_resource/ssl_cert
            "alert_type": alert_type,
            "severity": severity,
            "source": "alertmanager",
            "target_resource": target_resource,
            "namespace": namespace,
            "fingerprint": fingerprint,
            "message": message,
            "annotations": dict(alert.annotations) if alert.annotations else {},
            "metrics": {},
            "labels": alert.labels,
        }

        background_tasks.add_task(
            _process_new_alert_background,
            alert_context=alert_context,
            alert_id=alert_id,
            fingerprint=fingerprint,
            target_resource=target_resource,
            namespace=namespace,
            alert_type=alert_type,
            message=message,
            alertname=alertname,
            severity=severity,
            alert_labels=alert.labels,
            notification_type=notification_type,
            alert_category=alert_category,
            can_auto_repair=_can_auto_repair_by_rule,
        )

        record_alert_chain_success("alertmanager")
        return AlertResponse(
            success=True,
            message="✅ 告警已排入背景分析 (202 Accepted)",
            alert_id=alert_id,
            approval_created=False,
        )

    except Exception as e:
        logger.error(
            "alertmanager_degraded_accepted_no_retry",
            alert_id=alert_id,
            fingerprint=fingerprint,
            error=str(e),
        )
        return AlertResponse(
            success=False,
            message="⚠️ 告警已接收但處理降級，避免 Alertmanager retry storm；已交由背景治理/人工介入追蹤",
            alert_id=alert_id,
            approval_created=False,
        )


@router.get(
    "/health",
    summary="Webhook 健康檢查",
)
async def webhook_health() -> dict:
    """Webhook 服務健康檢查"""
    return {
        "status": "healthy",
        "service": "AWOOOI Webhook Gateway",
        "supported_alert_types": [
            "k8s_node_failure",
            "k8s_pod_crash",
            "db_connection_timeout",
            "service_404",
            "high_cpu",
            "high_memory",
            "disk_full",
            "ssl_expiry",
            "custom",
        ],
    }
