"""
GovernanceDispatcher — 治理事件 → 修復派遣
============================================
Poll 模式：每 30s 掃 ai_governance_events 中 resolved=False 且
無活躍 dispatch 的事件，呼叫 DecisionFusionAdapter 三維融合後
寫入 governance_remediation_dispatch 表。

職責：
1. Poll unresolved 治理事件（不直接修改 ai_governance_events 表）
2. 呼叫 DecisionFusionAdapter.fuse_decision → FusedDecision
3. 依 decision_path 決定是否寫入 dispatch
4. 不執行 remediation（實際執行由 approval_execution / auto_repair 消費 dispatch 表）

Tier 3 鐵線（絕不觸碰）：
- decision_manager.py / learning_service.py / trust_engine.py
- 本模組透過 DecisionFusionAdapter（wrapper）間接使用這些能力

2026-05-03 ogt + Claude Sonnet 4.6（亞太）: GovernanceDispatcher Wave 2E 實作
2026-05-04 ogt + Claude Sonnet 4.6（亞太）: skip 路徑無限迴圈修復
  - skip 決策後設 Redis 90min 冷卻，避免重複 LLM 呼叫
  - 超過 2 小時的 stale skip 事件標記 resolved=True（新事件若問題持續會重新產生）
"""

from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from typing import Any

import structlog
from sqlalchemy import exists, select, update

from src.db.base import get_db_context
from src.db.models import AiGovernanceEvent, GovernanceRemediationDispatch
from src.repositories.governance_remediation_dispatch_repo import (
    ACTIVE_STATUSES,
    DispatchAlreadyActive,
    create_dispatch,
    get_active_for_event,
    list_by_event,
    transition_status,
)
from src.services.decision_fusion_adapter import FusedDecision, get_decision_fusion_adapter

logger = structlog.get_logger(__name__)

# =============================================================================
# 常數
# TODO: 移到 settings（ADR-P2E-FUTURE），目前暫時 hardcode
# =============================================================================

# Poll 間隔（秒）
# TODO: 移到 settings，允許運維不重啟調整 poll 間隔
_DISPATCHER_INTERVAL_SEC: int = 30

# Skip 冷卻時間（秒）：skip 決策後 90 分鐘內不重新評估同一事件
# 原因：skip = 信心度不足，短期內 playbook trust / MCP 指標不會驟變
_SKIP_COOLDOWN_SEC: int = 5400  # 90 分鐘

# Stale 事件閾值（秒）：超過此時間的 skip 事件直接標 resolved
# 原因：持久問題會由 governance_agent 重新產生新事件；舊事件繼續留著只是積壓
_STALE_EVENT_SEC: int = 7200  # 2 小時

# 每輪最多處理幾個事件（避免單輪阻塞過長）
_MAX_EVENTS_PER_CYCLE: int = 10

# 允許建立 dispatch 的 event_type（對齊 governance_event_type enum）
_DISPATCHABLE_EVENT_TYPES: frozenset[str] = frozenset({
    "trust_drift",
    "knowledge_degradation",
    "llm_hallucination",
    "execution_blast_radius",
    "governance_slo_data_gap",
})


# =============================================================================
# Redis 冷卻 helpers（防止 skip 事件無限重評迴圈）
# =============================================================================

async def _is_skip_cooldown(event_id: str) -> bool:
    """確認事件是否在 skip 冷卻期內（90 分鐘）。"""
    try:
        from src.core.redis_client import get_redis
        redis = get_redis()
        return bool(await redis.exists(f"governance:skip:{event_id}"))
    except Exception:
        return False


async def _set_skip_cooldown(event_id: str) -> None:
    """設置 skip 冷卻期（90 分鐘），防止重複 LLM 呼叫。"""
    try:
        from src.core.redis_client import get_redis
        redis = get_redis()
        await redis.setex(f"governance:skip:{event_id}", _SKIP_COOLDOWN_SEC, "1")
    except Exception as exc:
        logger.warning("governance_skip_cooldown_set_failed", event_id=event_id, error=str(exc))


async def _mark_event_resolved(event_id: str, reason: str) -> None:
    """將 stale skip 事件標記為 resolved（持久問題會由 governance_agent 重新產生新事件）。

    對齊模型設計：resolved=True 由「下次計算時補填」，
    dispatcher skip = 系統判斷當前無法自動修復，等同一次計算完成。
    """
    try:
        from src.utils.timezone import now_taipei
        async with get_db_context() as db:
            await db.execute(
                update(AiGovernanceEvent)
                .where(AiGovernanceEvent.id == event_id)
                .where(AiGovernanceEvent.resolved.is_(False))
                .values(resolved=True, resolved_at=now_taipei())
            )
            logger.info(
                "governance_event_stale_resolved",
                event_id=event_id,
                reason=reason,
            )
    except Exception as exc:
        logger.warning("governance_event_resolve_failed", event_id=event_id, error=str(exc))


# =============================================================================
# 核心函數
# =============================================================================

async def dispatch_governance_event(event: AiGovernanceEvent) -> str | None:
    """處理單一治理事件：決策融合 → 寫 dispatch 記錄。

    Args:
        event: AiGovernanceEvent ORM 物件（唯讀，不修改）

    Returns:
        建立的 dispatch_id（str），或 None（skip / 已有活躍 dispatch）
    """
    event_id = event.id
    event_type = event.event_type

    # Step 0: 檢查是否已有活躍 dispatch（冪等保護）。
    # 先查 DB 再看 skip cooldown，避免曾經被 skip 的 knowledge_degradation
    # 無法補建 Hermes KB healthcheck intake work item。
    existing = await get_active_for_event(event_id)
    if existing is not None:
        logger.debug(
            "governance_dispatch_skipped_already_active",
            event_id=event_id,
            event_type=event_type,
            existing_dispatch_id=existing.id,
            existing_status=existing.dispatch_status,
        )
        return None

    # knowledge_degradation 的 run_kb_growth_healthcheck 是治理工作項 intake，
    # 不是自動修復執行；先落 pending dispatch，讓既有 unresolved 事件也能在 AwoooP 追蹤。
    if _is_kb_growth_healthcheck_event(event):
        if await _has_kb_growth_review_draft(event_id):
            logger.debug(
                "governance_kb_healthcheck_dispatch_skipped_waiting_review",
                event_id=event_id,
                event_type=event_type,
            )
            return None
        return await _record_kb_growth_healthcheck_dispatch(event)

    # Step 1: Redis skip 冷卻檢查（防止一般 skip 事件每 30s 重新做 LLM 呼叫）。
    if await _is_skip_cooldown(event_id):
        logger.debug(
            "governance_dispatch_skip_cooldown",
            event_id=event_id,
            event_type=event_type,
        )
        return None

    # Step 2: 決策融合（三維：LLM × Playbook × MCP）
    adapter = get_decision_fusion_adapter()
    try:
        decision: FusedDecision = await adapter.fuse_decision(event)
    except Exception as exc:
        logger.warning(
            "governance_fusion_failed",
            event_id=event_id,
            event_type=event_type,
            error=str(exc),
        )
        # LLM 失敗 fallback：skip + log（不寫 dispatch）
        logger.info(
            "governance_dispatch_fallback_skip",
            event_id=event_id,
            reason="fusion_exception",
        )
        return None

    # Step 3: 依 decision_path 決定要不要寫 dispatch
    if decision.decision_path == "skip":
        # 2026-05-04 ogt: 修復無限迴圈根因
        # skip 決策後設 90min Redis 冷卻，避免每 30s 重新呼叫 LLM
        # 超過 2h 的 stale 事件直接標 resolved（持久問題由 governance_agent 重新產生新事件）
        await _set_skip_cooldown(event_id)

        triggered_at_aware = event.triggered_at
        if triggered_at_aware is not None and triggered_at_aware.tzinfo is None:
            triggered_at_aware = triggered_at_aware.replace(tzinfo=timezone.utc)
        event_age_sec = (
            (datetime.now(timezone.utc) - triggered_at_aware).total_seconds()
            if triggered_at_aware is not None else 0
        )

        logger.info(
            "governance_dispatch_path_skip",
            event_id=event_id,
            event_type=event_type,
            confidence=round(decision.confidence, 4),
            event_age_sec=int(event_age_sec),
            stale=event_age_sec > _STALE_EVENT_SEC,
        )

        await _record_skipped_dispatch(event, decision)

        if event_age_sec > _STALE_EVENT_SEC:
            await _mark_event_resolved(event_id, reason=f"skip_stale_{int(event_age_sec)}s")

        return None

    # Step 4: 決定 executor_type 與 dispatch_status
    # auto_dispatch → dispatched（下游 auto_repair 消費）
    # pending_approval → pending（等人工審核）
    if decision.decision_path == "auto_dispatch":
        executor_type = "playbook_executor"
    else:  # pending_approval
        executor_type = "manual"

    # Step 5: 建構 decision_context JSONB（完整三維快照）
    decision_context = _build_decision_context(event, decision)

    # Step 6: 寫入 governance_remediation_dispatch（用 repo 函數）
    try:
        dispatch_row = await create_dispatch(
            event_id=event_id,
            event_type=event_type,
            executor_type=executor_type,
            playbook_id=decision.matched_playbook_id,
            decision_context=decision_context,
            created_by="governance_dispatcher",
        )
    except DispatchAlreadyActive:
        # 並行 race condition：另一個 worker 先建立了 dispatch
        logger.info(
            "governance_dispatch_race_condition",
            event_id=event_id,
            event_type=event_type,
        )
        return None
    except Exception as exc:
        logger.warning(
            "governance_dispatch_create_failed",
            event_id=event_id,
            event_type=event_type,
            error=str(exc),
        )
        return None

    logger.info(
        "governance_dispatched",
        dispatch_id=dispatch_row.id,
        event_id=event_id,
        event_type=event_type,
        decision_path=decision.decision_path,
        confidence=round(decision.confidence, 4),
        executor_type=executor_type,
        playbook_id=decision.matched_playbook_id,
    )

    return dispatch_row.id


async def _record_kb_growth_healthcheck_dispatch(event: AiGovernanceEvent) -> str | None:
    """將 knowledge_degradation intake 成 Hermes KB healthcheck work item."""
    try:
        from src.services.governance_agent import (
            _build_knowledge_degradation_dispatch_context,
        )

        dispatch_row = await create_dispatch(
            event_id=event.id,
            event_type=event.event_type,
            executor_type="hermes_kb_growth_healthcheck",
            decision_context=_build_knowledge_degradation_dispatch_context(
                event.id,
                event.details if isinstance(event.details, dict) else {},
            ),
            max_attempts=1,
            created_by="governance_dispatcher_intake",
        )
    except DispatchAlreadyActive:
        logger.info(
            "governance_kb_healthcheck_dispatch_race_condition",
            event_id=event.id,
            event_type=event.event_type,
        )
        return None
    except Exception as exc:
        logger.warning(
            "governance_kb_healthcheck_dispatch_failed",
            event_id=event.id,
            event_type=event.event_type,
            error=str(exc),
        )
        return None

    logger.info(
        "governance_kb_healthcheck_dispatched",
        dispatch_id=dispatch_row.id,
        event_id=event.id,
        event_type=event.event_type,
    )
    return dispatch_row.id


async def _record_skipped_dispatch(event: AiGovernanceEvent, decision: FusedDecision) -> None:
    """留下 skipped 派遣 trail，讓治理事件的 AI 判斷不再只存在 log 裡。

    skip 不代表「事情解決」，而是 DecisionFusion 判斷目前不能自動派遣。
    這筆 terminal dispatch row 讓 AwoooP / Work Items 可顯示：
    AI 已判斷、為何沒有自動處理、下一步需要誰接手。
    """
    try:
        row = await create_dispatch(
            event_id=event.id,
            event_type=event.event_type,
            executor_type="manual",
            playbook_id=decision.matched_playbook_id,
            decision_context=_build_decision_context(event, decision),
            created_by="governance_dispatcher_skip",
        )
        await transition_status(row.id, "pending", "skipped")
    except DispatchAlreadyActive:
        logger.info(
            "governance_skip_dispatch_race_condition",
            event_id=event.id,
            event_type=event.event_type,
        )
    except Exception as exc:
        logger.warning(
            "governance_skip_dispatch_record_failed",
            event_id=event.id,
            event_type=event.event_type,
            error=str(exc),
        )


async def _poll_unresolved_events() -> list[AiGovernanceEvent]:
    """查詢 unresolved 且 event_type 在 dispatchable 範圍內的治理事件。

    Returns:
        最多 _MAX_EVENTS_PER_CYCLE 筆 AiGovernanceEvent ORM 物件列表
    """
    async with get_db_context() as db:
        active_dispatch_exists = exists().where(
            GovernanceRemediationDispatch.governance_event_id == AiGovernanceEvent.id,
            GovernanceRemediationDispatch.dispatch_status.in_(list(ACTIVE_STATUSES)),
        )
        result = await db.execute(
            select(AiGovernanceEvent)
            .where(AiGovernanceEvent.resolved.is_(False))
            .where(AiGovernanceEvent.event_type.in_(list(_DISPATCHABLE_EVENT_TYPES)))
            .where(~active_dispatch_exists)
            .order_by(AiGovernanceEvent.triggered_at.asc())
            .limit(_MAX_EVENTS_PER_CYCLE)
        )
        rows = result.scalars().all()

    return list(rows)


def _build_decision_context(
    event: AiGovernanceEvent,
    decision: FusedDecision,
) -> dict[str, Any]:
    """建構 decision_context JSONB（完整三維輸入快照）。

    規格對齊 DecisionContextV1（models/governance_dispatch.py），
    但直接建 dict 不依賴 Pydantic model（避免引入額外依賴）。

    Fields:
        version: schema 版本（v1）
        trigger_source: 觸發來源
        suggested_action: AI 建議的修復動作摘要
        fusion_scores: 三維分數詳情
        llm_reasoning: LLM 原始輸出摘要
        mcp_snapshot: MCP 情報快照
        decision_path: 決策分支
        confidence: 最終融合信心度
    """
    details = event.details if isinstance(event.details, dict) else {}
    next_action = _extract_event_next_action(details, decision.recommended_action)
    workflow = _build_event_workflow(event, details, decision, next_action)
    ownership = details.get("ownership") if isinstance(details.get("ownership"), dict) else {}

    return {
        "version": "v1",
        "trigger_source": "governance_dispatcher",
        "triggered_metric": event.event_type,
        "metric_value": decision.confidence,
        "threshold": 0.85,  # TODO: 移到 settings
        "suggested_action": decision.recommended_action,
        "next_action": next_action,
        "workflow": workflow,
        "ownership": ownership,
        "fusion_scores": {
            "llm_score": round(decision.llm_score, 4),
            "playbook_score": round(decision.playbook_score, 4),
            "mcp_score": round(decision.mcp_score, 4),
            "confidence": round(decision.confidence, 4),
            "weights": {"llm": 0.4, "playbook": 0.3, "mcp": 0.3},  # TODO: 移到 settings
        },
        "llm_reasoning": decision.llm_reasoning,
        "mcp_snapshot": decision.mcp_snapshot,
        "decision_path": decision.decision_path,
        "matched_playbook_id": decision.matched_playbook_id,
        "playbook_trust": decision.playbook_trust,
        "affected_resources": [event.event_type],
        "extra": {
            "event_id": event.id,
            "event_details_keys": list(details.keys()),
            "ownership": ownership,
        },
    }


def _is_kb_growth_healthcheck_event(event: AiGovernanceEvent) -> bool:
    if event.event_type != "knowledge_degradation":
        return False
    details = event.details if isinstance(event.details, dict) else {}
    remediation = details.get("remediation")
    if isinstance(remediation, dict) and remediation.get("next_action") == "run_kb_growth_healthcheck":
        return True
    return details.get("next_action") == "run_kb_growth_healthcheck"


async def _has_kb_growth_review_draft(event_id: str) -> bool:
    """同一治理事件已有 Hermes review draft 時，不再重複建立 pending dispatch。"""
    try:
        rows = await list_by_event(event_id)
    except Exception as exc:
        logger.warning(
            "governance_kb_healthcheck_history_lookup_failed",
            event_id=event_id,
            error=str(exc),
        )
        return False

    for row in rows:
        if row.executor_type != "hermes_kb_growth_healthcheck":
            continue
        if row.dispatch_status != "succeeded":
            continue
        context = row.decision_context if isinstance(row.decision_context, dict) else {}
        workflow = context.get("workflow") if isinstance(context.get("workflow"), dict) else {}
        worker_result = (
            context.get("worker_result")
            if isinstance(context.get("worker_result"), dict)
            else {}
        )
        if workflow.get("current_stage") == "waiting_owner_review":
            return True
        if worker_result.get("status") == "draft_created":
            return True
    return False


def _extract_event_next_action(details: dict[str, Any], fallback: str) -> str:
    remediation = details.get("remediation")
    if isinstance(remediation, dict):
        next_action = remediation.get("next_action")
        if isinstance(next_action, str) and next_action:
            return next_action[:160]
    next_action = details.get("next_action")
    if isinstance(next_action, str) and next_action:
        return next_action[:160]
    return fallback[:160]


def _build_event_workflow(
    event: AiGovernanceEvent,
    details: dict[str, Any],
    decision: FusedDecision,
    next_action: str,
) -> dict[str, Any]:
    """把治理事件轉成 AwoooP 可讀 workflow metadata。

    這不是自動修復決策；最終是否執行仍由 DecisionFusion 與 dispatch state 決定。
    """
    base_steps = ["detected", "ai_analyzed"]
    if next_action == "run_kb_growth_healthcheck":
        steps = [
            *base_steps,
            "queued_kb_healthcheck",
            "draft_km_updates",
            "waiting_owner_review",
            "km_writeback_after_approval",
            "stale_ratio_recheck",
        ]
        stage_by_status = {
            "pending": "queued_kb_healthcheck",
            "dispatched": "queued_kb_healthcheck",
            "executing": "draft_km_updates",
            "succeeded": "stale_ratio_recheck",
            "failed": "needs_manual_km_triage",
            "skipped": "waiting_owner_review",
            "cancelled": "cancelled",
        }
        current_stage = (
            "waiting_owner_review"
            if decision.decision_path == "skip"
            else "queued_kb_healthcheck"
        )
        work_kind = "kb_growth_healthcheck"
    else:
        steps = [*base_steps, "dispatch_decision", "operator_review"]
        stage_by_status = {
            "pending": "queued_for_review",
            "dispatched": "dispatched",
            "executing": "executing",
            "succeeded": "completed",
            "failed": "failed",
            "skipped": "skipped",
            "cancelled": "cancelled",
        }
        current_stage = stage_by_status.get("pending", "queued_for_review")
        work_kind = "governance_remediation"

    return {
        "work_item_id": f"governance:{event.event_type}:{event.id}",
        "work_kind": work_kind,
        "current_stage": current_stage,
        "steps": steps,
        "stage_by_dispatch_status": stage_by_status,
        "next_action": next_action,
        "needs_human_review": decision.decision_path != "auto_dispatch",
        "writes_km_without_approval": False,
        "impact": details.get("impact") if isinstance(details.get("impact"), dict) else {},
    }


# =============================================================================
# 排程迴圈（仿 run_governance_loop 模式）
# =============================================================================

async def run_governance_dispatcher_loop(
    interval_seconds: int = _DISPATCHER_INTERVAL_SEC,
) -> None:
    """每 30s 掃 unresolved 事件 → dispatch。

    仿照 governance_agent.run_governance_loop 模式：
    - while True → try/except → sleep
    - 任一事件失敗不阻塞其他事件（獨立 try/except）
    - CancelledError 向上傳播（允許 graceful shutdown）

    2026-05-03 ogt + Claude Sonnet 4.6（亞太）: Wave 2E 實作
    """
    logger.info(
        "governance_dispatcher_loop_started",
        interval_seconds=interval_seconds,
        max_events_per_cycle=_MAX_EVENTS_PER_CYCLE,
    )

    while True:
        try:
            events = await _poll_unresolved_events()

            if events:
                logger.info(
                    "governance_dispatcher_cycle_start",
                    event_count=len(events),
                )
                dispatched = 0
                skipped = 0
                for event in events:
                    try:
                        result = await dispatch_governance_event(event)
                        if result is not None:
                            dispatched += 1
                        else:
                            skipped += 1
                    except asyncio.CancelledError:
                        raise
                    except Exception as exc:
                        logger.warning(
                            "governance_dispatcher_event_error",
                            event_id=event.id,
                            event_type=event.event_type,
                            error=str(exc),
                        )
                        skipped += 1

                logger.info(
                    "governance_dispatcher_cycle_done",
                    total=len(events),
                    dispatched=dispatched,
                    skipped=skipped,
                )
            else:
                logger.debug("governance_dispatcher_no_events")

        except asyncio.CancelledError:
            logger.info("governance_dispatcher_loop_cancelled")
            raise
        except Exception as exc:
            logger.warning("governance_dispatcher_loop_error", error=str(exc))

        await asyncio.sleep(interval_seconds)
