"""
Hermes KB Growth Worker
=======================

消費 governance_remediation_dispatch 中的 hermes_kb_growth_healthcheck work item，
把 knowledge_degradation 告警推進成可審核的 KM 草稿。

邊界：
- 可以建立 REVIEW 狀態的 auto_runbook 草稿，讓 owner 在前端審核。
- 不可以直接把 KM 標成 APPROVED / PUBLISHED。
- 不修改 immutable ai_governance_events；流程進度寫回 dispatch.decision_context。

2026-05-19 ogt + Codex: T90 Hermes KB growth healthcheck worker。
"""

from __future__ import annotations

import asyncio
from copy import deepcopy
from typing import Any

import structlog

from src.db.base import get_db_context
from src.db.models import GovernanceRemediationDispatch
from src.models.knowledge import (
    EntrySource,
    EntryStatus,
    EntryType,
    KnowledgeEntry,
    KnowledgeEntryCreate,
)
from src.repositories.governance_remediation_dispatch_repo import (
    InvalidStatusTransition,
    list_pending_by_executor,
    transition_status,
    update_decision_context,
)
from src.repositories.knowledge_repository import KnowledgeDBRepository

logger = structlog.get_logger(__name__)

EXECUTOR_TYPE = "hermes_kb_growth_healthcheck"
DEFAULT_INTERVAL_SECONDS = 300
DEFAULT_LIMIT = 20


async def run_hermes_kb_growth_once(limit: int = DEFAULT_LIMIT) -> dict[str, int]:
    """執行一輪 Hermes KB growth healthcheck。

    Returns:
        統計資訊，供 log / smoke test 判讀。
    """
    rows = await list_pending_by_executor(EXECUTOR_TYPE, limit=limit)
    result = {
        "scanned": len(rows),
        "processed": 0,
        "skipped": 0,
        "failed": 0,
    }

    for row in rows:
        try:
            await _process_dispatch(row)
            result["processed"] += 1
        except InvalidStatusTransition as exc:
            result["skipped"] += 1
            logger.info(
                "hermes_kb_growth_dispatch_skipped",
                dispatch_id=row.id,
                event_id=row.governance_event_id,
                reason=str(exc),
            )
        except Exception as exc:
            result["failed"] += 1
            logger.exception(
                "hermes_kb_growth_dispatch_failed",
                dispatch_id=row.id,
                event_id=row.governance_event_id,
                error=str(exc),
            )
            await _mark_failed_if_started(row.id, str(exc))

    if any(result.values()):
        logger.info("hermes_kb_growth_once_completed", **result)
    return result


async def run_hermes_kb_growth_loop(
    interval_seconds: int = DEFAULT_INTERVAL_SECONDS,
    limit: int = DEFAULT_LIMIT,
) -> None:
    """背景 loop：定期消費 Hermes KB growth dispatch。"""
    logger.info(
        "hermes_kb_growth_loop_started",
        interval_seconds=interval_seconds,
        limit=limit,
    )
    while True:
        try:
            await run_hermes_kb_growth_once(limit=limit)
        except asyncio.CancelledError:
            raise
        except Exception as exc:
            logger.exception("hermes_kb_growth_loop_error", error=str(exc))
        await asyncio.sleep(interval_seconds)


async def _process_dispatch(row: GovernanceRemediationDispatch) -> None:
    """處理單筆 pending dispatch，最後停在 waiting_owner_review。"""
    dispatched = await transition_status(row.id, "pending", "dispatched")
    executing = await transition_status(dispatched.id, "dispatched", "executing")

    km_entry = await _create_or_get_km_review_draft(executing)
    updated_context = _build_review_context(
        executing.decision_context or {},
        dispatch_id=executing.id,
        governance_event_id=executing.governance_event_id,
        km_entry_id=km_entry.id,
    )
    await update_decision_context(executing.id, updated_context)
    await transition_status(executing.id, "executing", "succeeded")

    logger.info(
        "hermes_kb_growth_review_draft_ready",
        dispatch_id=executing.id,
        event_id=executing.governance_event_id,
        km_entry_id=km_entry.id,
        workflow_stage="waiting_owner_review",
    )


async def _create_or_get_km_review_draft(
    dispatch: GovernanceRemediationDispatch,
) -> KnowledgeEntry:
    """以 governance event tag 做冪等，建立或取得 REVIEW 狀態 KM 草稿。"""
    dispatch_tag = f"dispatch:{dispatch.id}"
    event_tag = f"governance_event:{dispatch.governance_event_id}"
    payload = _build_km_review_entry_payload(dispatch)

    async with get_db_context() as db:
        repo = KnowledgeDBRepository(db)
        existing, _ = await repo.list_entries(tags=[event_tag], limit=1)
        if existing:
            return existing[0]
        existing, _ = await repo.list_entries(tags=[dispatch_tag], limit=1)
        if existing:
            return existing[0]
        return await repo.create(payload)


def _build_km_review_entry_payload(
    dispatch: GovernanceRemediationDispatch,
) -> KnowledgeEntryCreate:
    """把 governance dispatch 轉成待審核的 KM 草稿 payload。"""
    context = dispatch.decision_context or {}
    workflow = context.get("workflow") if isinstance(context.get("workflow"), dict) else {}
    impact = workflow.get("impact") if isinstance(workflow.get("impact"), dict) else {}
    extra = context.get("extra") if isinstance(context.get("extra"), dict) else {}
    ownership = context.get("ownership") if isinstance(context.get("ownership"), dict) else {}
    if not ownership and isinstance(extra.get("ownership"), dict):
        ownership = extra["ownership"]

    stale_count = _pick_first(impact, extra, key="stale_count")
    total_count = _pick_first(impact, extra, key="total_count")
    stale_ratio = _pick_first(impact, context, key="stale_ratio")
    threshold = _pick_first(impact, context, key="threshold")
    stale_days = _pick_first(impact, extra, key="stale_days")
    lead_agent = ownership.get("lead_agent") or "Hermes"
    human_owner = ownership.get("human_owner") or "KM owner / SRE owner"

    content = "\n".join([
        "# KM 健康檢查草稿",
        "",
        "## 來源",
        f"- governance_event_id: {dispatch.governance_event_id}",
        f"- dispatch_id: {dispatch.id}",
        f"- executor_type: {dispatch.executor_type}",
        "",
        "## 影響摘要",
        f"- stale_count: {_format_unknown(stale_count)}",
        f"- total_count: {_format_unknown(total_count)}",
        f"- stale_ratio: {_format_ratio(stale_ratio)}",
        f"- threshold: {_format_ratio(threshold)}",
        f"- stale_days: {_format_unknown(stale_days)}",
        "",
        "## AI 已完成",
        "- Hermes 已接手 knowledge_degradation dispatch。",
        "- 已產生 KM 更新草稿與 owner review work item。",
        "- 尚未把任何條目標成 approved / published。",
        "",
        "## Owner 審核重點",
        "- 優先反查最近被 Incident、Sentry、SigNoz、PlayBook 引用的 KM。",
        "- 確認草稿內容沒有把過期處置方式寫回正式知識庫。",
        "- 審核通過後再進入 km_writeback_after_approval。",
        "",
        "## 安全邊界",
        "- writes_km_without_approval=false",
        f"- lead_agent={lead_agent}",
        f"- human_owner={human_owner}",
    ])

    return KnowledgeEntryCreate(
        title=f"KM healthcheck review draft - {dispatch.governance_event_id[:8]}",
        content=content,
        entry_type=EntryType.AUTO_RUNBOOK,
        category="AI治理",
        tags=[
            "governance:knowledge_degradation",
            "workflow:kb_growth_healthcheck",
            "stage:waiting_owner_review",
            "agent:Hermes",
            "needs_owner_review",
            f"dispatch:{dispatch.id}",
            f"governance_event:{dispatch.governance_event_id}",
        ],
        source=EntrySource.AI_EXTRACTED,
        status=EntryStatus.REVIEW,
        path_type="hermes_kb_growth_healthcheck",
        created_by="hermes_kb_growth_worker",
    )


def _build_review_context(
    context: dict[str, Any],
    *,
    dispatch_id: str,
    governance_event_id: str,
    km_entry_id: str,
) -> dict[str, Any]:
    """更新 dispatch read model，讓 Work Items/Telegram 可見目前停在 owner review。"""
    updated = deepcopy(context)
    workflow = updated.setdefault("workflow", {})
    if not isinstance(workflow, dict):
        workflow = {}
        updated["workflow"] = workflow

    stages = workflow.setdefault("stage_by_dispatch_status", {})
    if not isinstance(stages, dict):
        stages = {}
        workflow["stage_by_dispatch_status"] = stages
    stages.update({
        "executing": "draft_km_updates",
        "succeeded": "waiting_owner_review",
        "failed": "needs_manual_km_triage",
    })

    workflow["current_stage"] = "waiting_owner_review"
    workflow["next_action"] = "owner_review_km_draft"
    workflow["needs_human_review"] = True
    workflow["writes_km_without_approval"] = False
    workflow["kb_draft_entry_id"] = km_entry_id

    updated["next_action"] = "owner_review_km_draft"
    updated["decision_path"] = "draft_created_waiting_owner_review"
    updated["proposed_action"] = "Hermes 已建立 KM 更新草稿，等待 owner 審核"
    updated["worker_result"] = {
        "worker": "Hermes",
        "executor_type": EXECUTOR_TYPE,
        "dispatch_id": dispatch_id,
        "governance_event_id": governance_event_id,
        "km_draft_entry_id": km_entry_id,
        "stage": "waiting_owner_review",
        "status": "draft_created",
        "writes_km_without_approval": False,
    }
    return updated


async def _mark_failed_if_started(dispatch_id: str, error: str) -> None:
    """若 worker 已取得 dispatch，將它收斂到 failed，保留錯誤。"""
    for from_status in ("executing", "dispatched"):
        try:
            await transition_status(
                dispatch_id,
                from_status,
                "failed",
                last_error=error[:500],
            )
            return
        except InvalidStatusTransition:
            continue
        except Exception as exc:
            logger.warning(
                "hermes_kb_growth_mark_failed_failed",
                dispatch_id=dispatch_id,
                from_status=from_status,
                error=str(exc),
            )
            return


def _pick_first(*sources: dict[str, Any], key: str) -> Any:
    for source in sources:
        if key in source:
            return source[key]
    return None


def _format_unknown(value: Any) -> str:
    return "unknown" if value is None else str(value)


def _format_ratio(value: Any) -> str:
    try:
        return f"{float(value) * 100:.1f}%"
    except (TypeError, ValueError):
        return "unknown"
