"""
Governance KM Stale Review Service
==================================

Owner-review intake for stale KM priority candidates.

這層只把 stale KM 候選排入治理工作項與 audit trail，不改寫 KM 內容。
真正 refresh / archive / supersede 仍需 owner 後續審核。
"""

from __future__ import annotations

import hashlib
import json
from datetime import timedelta
from typing import Any, Literal

import structlog
from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession

from src.db.base import get_db_context
from src.db.models import (
    AiGovernanceEvent,
    GovernanceRemediationDispatch,
    KnowledgeEntryRecord,
    generate_uuid,
    taipei_now,
)
from src.models.governance import (
    KnowledgeReviewDraftStaleRatioSnapshot,
    KnowledgeStaleCandidate,
    KnowledgeStaleOwnerReviewBatchItem,
    KnowledgeStaleOwnerReviewBatchQueueRequest,
    KnowledgeStaleOwnerReviewBatchQueueResponse,
    KnowledgeStaleOwnerReviewBurnDownItem,
    KnowledgeStaleOwnerReviewBurnDownResponse,
    KnowledgeStaleOwnerReviewCompleteRequest,
    KnowledgeStaleOwnerReviewCompleteResponse,
    KnowledgeStaleOwnerReviewCompletionBatchPreviewRequest,
    KnowledgeStaleOwnerReviewCompletionBatchPreviewResponse,
    KnowledgeStaleOwnerReviewCompletionQueueItem,
    KnowledgeStaleOwnerReviewCompletionQueueResponse,
    KnowledgeStaleOwnerReviewInboxItem,
    KnowledgeStaleOwnerReviewInboxResponse,
    KnowledgeStaleOwnerReviewRequest,
    KnowledgeStaleOwnerReviewResponse,
)
from src.models.knowledge import EntryStatus
from src.services.governance_agent import KM_STALE_DAYS, KM_STALE_RATIO
from src.services.governance_query_service import (
    _build_km_stale_candidate,
    query_km_stale_candidates,
)
from src.utils.timezone import now_taipei

logger = structlog.get_logger(__name__)

_EXECUTOR_TYPE = "hermes_km_stale_owner_review"
_BATCH_EXECUTOR_TYPE = "hermes_km_stale_owner_review_batch"
_COMPLETE_EXECUTOR_TYPE = "hermes_km_stale_owner_review_complete"
_RECHECK_EXECUTOR_TYPE = "hermes_km_stale_ratio_recheck"
_ACTIVE_DISPATCH_STATUSES = frozenset({"pending", "dispatched", "executing"})
_TERMINAL_FAILED_DISPATCH_STATUSES = frozenset({"failed", "cancelled"})
_OWNER_REVIEW_DISPATCH_STATUSES = frozenset({
    "pending",
    "dispatched",
    "executing",
    "succeeded",
    "failed",
    "skipped",
    "cancelled",
})


class KmStaleOwnerReviewError(Exception):
    """KM stale owner-review request failed validation."""

    def __init__(self, status_code: int, detail: str) -> None:
        super().__init__(detail)
        self.status_code = status_code
        self.detail = detail


async def query_km_stale_owner_review_inbox(
    *,
    project_id: str = "awoooi",
    dispatch_status: str = "pending",
    limit: int = 20,
) -> KnowledgeStaleOwnerReviewInboxResponse:
    """Read owner-review dispatches with KM priority context for the operator console."""
    if dispatch_status != "all" and dispatch_status not in _OWNER_REVIEW_DISPATCH_STATUSES:
        raise KmStaleOwnerReviewError(422, "unsupported stale KM owner-review dispatch_status")

    generated_at = now_taipei()
    items = await _load_owner_review_inbox_items(
        project_id=project_id,
        dispatch_status=dispatch_status,
        generated_at=generated_at,
    )
    items.sort(
        key=lambda item: (
            _priority_rank(item.priority_tier),
            item.priority_score,
            item.stale_days,
            item.queued_at.isoformat() if item.queued_at else "",
        ),
        reverse=True,
    )
    limited = items[:limit]
    return KnowledgeStaleOwnerReviewInboxResponse(
        project_id=project_id,
        dispatch_status=dispatch_status,
        total=len(items),
        returned=len(limited),
        items=limited,
        generated_at=generated_at,
    )


async def query_km_stale_owner_review_completion_queue(
    *,
    project_id: str = "awoooi",
    status_bucket: Literal["all", "ready", "blocked", "completed", "failed", "pending"] = "all",
    priority_tiers: list[str] | None = None,
    recommended_completion_outcome: str = "all",
    batch_governance_event_id: str | None = None,
    can_preview: bool | None = None,
    limit: int = 20,
) -> KnowledgeStaleOwnerReviewCompletionQueueResponse:
    """Read owner-review completion readiness without writing KM content."""
    normalized_priority_tiers = _normalize_priority_tiers(priority_tiers)
    normalized_outcome = _normalize_completion_outcome(recommended_completion_outcome)
    generated_at = now_taipei()
    inbox_items = await _load_owner_review_inbox_items(
        project_id=project_id,
        dispatch_status="all",
        generated_at=generated_at,
    )
    queue_items = [
        _build_completion_queue_item(item)
        for item in inbox_items
    ]
    queue_items.sort(
        key=lambda item: (
            item.readiness == "ready",
            _priority_rank(item.priority_tier),
            item.priority_score,
            item.stale_days,
            item.queued_at.isoformat() if item.queued_at else "",
        ),
        reverse=True,
    )

    filtered_items = [
        item for item in queue_items
        if _completion_queue_bucket_matches(item, status_bucket=status_bucket)
    ]
    filtered_items = _filter_completion_queue_items(
        filtered_items,
        priority_tiers=normalized_priority_tiers,
        recommended_completion_outcome=normalized_outcome,
        batch_governance_event_id=batch_governance_event_id,
        can_preview=can_preview,
    )
    limited = filtered_items[:limit]
    return KnowledgeStaleOwnerReviewCompletionQueueResponse(
        project_id=project_id,
        status_bucket=status_bucket,
        priority_tiers=normalized_priority_tiers,
        recommended_completion_outcome=normalized_outcome,
        batch_governance_event_id=batch_governance_event_id,
        can_preview=can_preview,
        total=len(filtered_items),
        returned=len(limited),
        pending_count=sum(
            1 for item in queue_items
            if item.dispatch_status in _ACTIVE_DISPATCH_STATUSES
        ),
        ready_count=sum(1 for item in queue_items if item.readiness == "ready"),
        blocked_count=sum(1 for item in queue_items if item.readiness == "blocked"),
        completed_count=sum(1 for item in queue_items if item.readiness == "completed"),
        failed_count=sum(1 for item in queue_items if item.readiness == "failed"),
        items=limited,
        generated_at=generated_at,
    )


async def preview_km_stale_owner_review_completion_batch(
    *,
    request: KnowledgeStaleOwnerReviewCompletionBatchPreviewRequest,
) -> KnowledgeStaleOwnerReviewCompletionBatchPreviewResponse:
    """Preview a bounded completion batch without writing KM or governance audit rows."""
    generated_at = now_taipei()
    queue = await query_km_stale_owner_review_completion_queue(
        project_id=request.project_id,
        status_bucket=request.status_bucket,
        priority_tiers=list(request.priority_tiers),
        recommended_completion_outcome=request.recommended_completion_outcome,
        batch_governance_event_id=request.batch_governance_event_id,
        can_preview=None,
        limit=request.limit,
    )
    items = queue.items
    previewable_items = [item for item in items if item.can_preview]
    fingerprint = _build_completion_batch_preview_fingerprint(
        request=request,
        items=items,
    )
    return KnowledgeStaleOwnerReviewCompletionBatchPreviewResponse(
        project_id=request.project_id,
        owner=request.owner,
        owner_note=request.owner_note,
        status_bucket=request.status_bucket,
        priority_tiers=list(dict.fromkeys(request.priority_tiers)),
        recommended_completion_outcome=request.recommended_completion_outcome,
        batch_governance_event_id=request.batch_governance_event_id,
        requested_limit=request.limit,
        candidate_count=len(items),
        previewable_count=len(previewable_items),
        blocked_count=sum(1 for item in items if item.readiness == "blocked"),
        completed_count=sum(1 for item in items if item.readiness == "completed"),
        failed_count=sum(1 for item in items if item.readiness == "failed"),
        dry_run_plan_fingerprint=fingerprint,
        items=items,
        generated_at=generated_at,
    )


async def _load_owner_review_inbox_items(
    *,
    project_id: str,
    dispatch_status: str,
    generated_at: Any,
) -> list[KnowledgeStaleOwnerReviewInboxItem]:
    status_filter = (
        "TRUE"
        if dispatch_status == "all"
        else "d.dispatch_status::text = :dispatch_status"
    )
    sql = text(f"""
        SELECT
            d.id,
            d.governance_event_id,
            d.dispatch_status,
            d.decision_context,
            d.dispatched_at,
            d.started_at,
            d.completed_at,
            COALESCE(
                d.decision_context -> 'workflow' ->> 'entry_id',
                d.decision_context ->> 'entry_id'
            ) AS entry_id,
            COALESCE(
                d.decision_context -> 'workflow' ->> 'project_id',
                d.decision_context ->> 'project_id',
                d.decision_context -> 'candidate' ->> 'project_id'
            ) AS project_id
        FROM governance_remediation_dispatch d
        WHERE d.executor_type = :executor_type
          AND {status_filter}
          AND COALESCE(
                d.decision_context -> 'workflow' ->> 'project_id',
                d.decision_context ->> 'project_id',
                d.decision_context -> 'candidate' ->> 'project_id'
              ) = :project_id
        ORDER BY d.dispatched_at DESC
    """)
    params: dict[str, Any] = {
        "executor_type": _EXECUTOR_TYPE,
        "project_id": project_id,
    }
    if dispatch_status != "all":
        params["dispatch_status"] = dispatch_status

    async with get_db_context() as db:
        result = await db.execute(sql, params)
        rows = result.fetchall()
        entry_ids = [
            str(row.entry_id)
            for row in rows
            if isinstance(row.entry_id, str) and row.entry_id
        ]
        records_by_id: dict[str, KnowledgeEntryRecord] = {}
        if entry_ids:
            record_result = await db.execute(
                select(KnowledgeEntryRecord).where(KnowledgeEntryRecord.id.in_(entry_ids))
            )
            records_by_id = {
                str(record.id): record
                for record in record_result.scalars().all()
            }

    items: list[KnowledgeStaleOwnerReviewInboxItem] = []
    for row in rows:
        entry_id = str(row.entry_id or "")
        record = records_by_id.get(entry_id)
        if record is None:
            continue
        candidate = _build_km_stale_candidate(
            record,
            now=generated_at,
            threshold_days=KM_STALE_DAYS,
        )
        decision_context = row.decision_context if isinstance(row.decision_context, dict) else {}
        items.append(
            _build_owner_review_inbox_item(
                row=row,
                candidate=candidate,
                decision_context=decision_context,
            )
        )
    return items


async def query_km_stale_owner_review_burndown(
    *,
    project_id: str = "awoooi",
    limit: int = 20,
) -> KnowledgeStaleOwnerReviewBurnDownResponse:
    """Read stale KM owner-review completion progress and current burn-down status."""
    generated_at = now_taipei()
    async with get_db_context() as db:
        current_snapshot = await _compute_km_stale_ratio_snapshot(db)
        counts = await _load_owner_review_burndown_counts(db, project_id=project_id)
        completion_rows = await _load_owner_review_completion_rows(
            db,
            project_id=project_id,
            limit=limit,
        )

    items = _build_owner_review_burndown_items(completion_rows, project_id=project_id)
    latest = items[0] if items else None
    return KnowledgeStaleOwnerReviewBurnDownResponse(
        project_id=project_id,
        burn_down_status=_stale_burndown_status(current_snapshot),
        current_snapshot=current_snapshot,
        entries_to_threshold=_entries_to_stale_threshold(current_snapshot),
        pending_owner_reviews=counts["pending_owner_reviews"],
        completed_owner_reviews=counts["completed_owner_reviews"],
        completion_audit_total=counts["completion_audit_total"],
        stale_ratio_recheck_total=counts["stale_ratio_recheck_total"],
        latest_stale_count_delta=latest.stale_count_delta if latest else None,
        latest_stale_ratio_delta=latest.stale_ratio_delta if latest else None,
        returned=len(items),
        items=items,
        generated_at=generated_at,
    )


async def queue_km_stale_owner_review(
    *,
    entry_id: str,
    request: KnowledgeStaleOwnerReviewRequest,
) -> KnowledgeStaleOwnerReviewResponse:
    """Queue a stale KM candidate for owner review without modifying KM content."""
    record = await _load_stale_candidate_record(entry_id)
    candidate = _build_km_stale_candidate(
        record,
        now=now_taipei(),
        threshold_days=KM_STALE_DAYS,
    )

    existing = await _load_active_owner_review_dispatch(entry_id)
    if existing is not None:
        return _build_response(
            entry_id=entry_id,
            project_id=candidate.project_id,
            status="already_queued",
            governance_event_id=str(existing.governance_event_id),
            dispatch_id=str(existing.id),
            recommended_action=candidate.recommended_action,
            owner=request.owner,
            owner_note=request.owner_note,
            writes_governance_audit=False,
        )

    if request.dry_run:
        return _build_response(
            entry_id=entry_id,
            project_id=candidate.project_id,
            status="dry_run",
            recommended_action=candidate.recommended_action,
            owner=request.owner,
            owner_note=request.owner_note,
            writes_governance_audit=False,
        )

    now = taipei_now()
    event_id = generate_uuid()
    dispatch_id = generate_uuid()
    event_details = _build_stale_owner_review_event_details(
        entry_id=entry_id,
        candidate=candidate.model_dump(mode="json"),
        owner=request.owner,
        owner_note=request.owner_note,
    )
    decision_context = _build_stale_owner_review_decision_context(
        governance_event_id=event_id,
        entry_id=entry_id,
        candidate=candidate.model_dump(mode="json"),
        owner=request.owner,
        owner_note=request.owner_note,
    )

    async with get_db_context() as db:
        event = AiGovernanceEvent(
            id=event_id,
            event_type="knowledge_degradation",
            triggered_at=now,
            details=event_details,
            resolved=False,
        )
        dispatch = GovernanceRemediationDispatch(
            id=dispatch_id,
            governance_event_id=event_id,
            event_type="knowledge_degradation",
            dispatch_status="pending",
            playbook_id=None,
            incident_id=None,
            approval_id=None,
            decision_context=decision_context,
            executor_type=_EXECUTOR_TYPE,
            attempt_count=0,
            max_attempts=1,
            dispatched_at=now,
            created_by=request.owner[:100],
        )
        db.add(event)
        db.add(dispatch)
        await db.flush()

    logger.info(
        "km_stale_owner_review_queued",
        entry_id=entry_id,
        project_id=candidate.project_id,
        governance_event_id=event_id,
        dispatch_id=dispatch_id,
        recommended_action=candidate.recommended_action,
    )
    return _build_response(
        entry_id=entry_id,
        project_id=candidate.project_id,
        status="queued",
        governance_event_id=event_id,
        dispatch_id=dispatch_id,
        recommended_action=candidate.recommended_action,
        owner=request.owner,
        owner_note=request.owner_note,
        writes_governance_audit=True,
    )


async def batch_queue_km_stale_owner_reviews(
    *,
    request: KnowledgeStaleOwnerReviewBatchQueueRequest,
) -> KnowledgeStaleOwnerReviewBatchQueueResponse:
    """Queue a bounded P0/P1 stale-KM batch for owner review without KM writes."""
    selected = await _select_batch_stale_candidates(request)
    items = _build_batch_plan_items(selected, dry_run=request.dry_run)
    snapshot = await _load_current_km_stale_ratio_snapshot()
    fingerprint = _build_batch_queue_plan_fingerprint(
        request=request,
        items=items,
        snapshot=snapshot,
    )
    queueable = [item for item in items if item.status == "would_queue"]

    if request.dry_run:
        return _build_batch_queue_response(
            request=request,
            status="dry_run",
            items=items,
            stale_ratio_snapshot=snapshot,
            dry_run_plan_fingerprint=fingerprint,
            writes_governance_audit=False,
        )

    if not request.dry_run_plan_fingerprint:
        raise KmStaleOwnerReviewError(
            403,
            "dry_run_plan_fingerprint from a dry-run preview is required before queueing a stale KM batch",
        )
    if request.dry_run_plan_fingerprint != fingerprint:
        raise KmStaleOwnerReviewError(
            409,
            "dry_run_plan_fingerprint does not match the latest stale KM batch queue plan",
        )
    if not queueable:
        return _build_batch_queue_response(
            request=request,
            status="noop_already_queued",
            items=items,
            stale_ratio_snapshot=snapshot,
            dry_run_plan_fingerprint=fingerprint,
            writes_governance_audit=False,
        )

    write_result = await _write_batch_owner_review_dispatches(
        request=request,
        candidates=selected,
        plan_items=items,
        stale_ratio_snapshot=snapshot,
        plan_fingerprint=fingerprint,
    )
    return _build_batch_queue_response(
        request=request,
        status="queued",
        items=write_result["items"],
        stale_ratio_snapshot=snapshot,
        dry_run_plan_fingerprint=fingerprint,
        writes_governance_audit=True,
        batch_governance_event_id=write_result["batch_governance_event_id"],
        batch_dispatch_id=write_result["batch_dispatch_id"],
    )


async def _select_batch_stale_candidates(
    request: KnowledgeStaleOwnerReviewBatchQueueRequest,
) -> list[KnowledgeStaleCandidate]:
    """Load a bounded priority batch from the read model instead of duplicating scoring logic."""
    fetch_limit = min(100, max(request.limit * 4, request.limit))
    response = await query_km_stale_candidates(
        project_id=request.project_id,
        limit=fetch_limit,
    )
    wanted_tiers = set(request.priority_tiers)
    selected: list[KnowledgeStaleCandidate] = []
    for candidate in response.items:
        if candidate.priority_tier not in wanted_tiers:
            continue
        selected.append(candidate)
        if len(selected) >= request.limit:
            break
    return selected


def _build_batch_plan_items(
    candidates: list[KnowledgeStaleCandidate],
    *,
    dry_run: bool,
) -> list[KnowledgeStaleOwnerReviewBatchItem]:
    items: list[KnowledgeStaleOwnerReviewBatchItem] = []
    for candidate in candidates:
        owner_status = str(candidate.owner_review_status or "")
        if owner_status in _ACTIVE_DISPATCH_STATUSES:
            status: Literal["would_queue", "queued", "already_queued", "skipped"] = "already_queued"
            reason = "active_owner_review_exists"
            workflow_stage = candidate.owner_review_stage or "waiting_owner_review"
        elif owner_status == "succeeded":
            status = "skipped"
            reason = "already_reviewed_or_completed"
            workflow_stage = candidate.owner_review_stage or "km_candidate_reviewed"
        else:
            status = "would_queue" if dry_run else "would_queue"
            reason = None
            workflow_stage = "waiting_owner_review"
        items.append(
            KnowledgeStaleOwnerReviewBatchItem(
                entry_id=candidate.entry_id,
                title=candidate.title,
                priority_tier=candidate.priority_tier,
                recommended_action=candidate.recommended_action,
                status=status,
                reason=reason,
                dispatch_id=candidate.owner_review_dispatch_id,
                workflow_stage=workflow_stage,
            )
        )
    return items


def _build_batch_queue_plan_fingerprint(
    *,
    request: KnowledgeStaleOwnerReviewBatchQueueRequest,
    items: list[KnowledgeStaleOwnerReviewBatchItem],
    snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
) -> str:
    payload = {
        "schema_version": "km_stale_owner_review_batch_plan_v1",
        "project_id": request.project_id,
        "priority_tiers": list(dict.fromkeys(request.priority_tiers)),
        "limit": request.limit,
        "owner": request.owner,
        "owner_note_sha256": (
            hashlib.sha256(request.owner_note.encode("utf-8")).hexdigest()
            if request.owner_note
            else None
        ),
        "stale_ratio_snapshot": snapshot.model_dump(),
        "items": [
            {
                "entry_id": item.entry_id,
                "priority_tier": item.priority_tier,
                "recommended_action": item.recommended_action,
                "status": item.status,
                "dispatch_id": item.dispatch_id,
            }
            for item in items
        ],
    }
    encoded = json.dumps(
        payload,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )
    return "sha256:" + hashlib.sha256(encoded.encode("utf-8")).hexdigest()


async def _write_batch_owner_review_dispatches(
    *,
    request: KnowledgeStaleOwnerReviewBatchQueueRequest,
    candidates: list[KnowledgeStaleCandidate],
    plan_items: list[KnowledgeStaleOwnerReviewBatchItem],
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
    plan_fingerprint: str,
) -> dict[str, Any]:
    now = taipei_now()
    batch_event_id = generate_uuid()
    batch_dispatch_id = generate_uuid()
    candidate_by_id = {candidate.entry_id: candidate for candidate in candidates}
    queued_ids = {item.entry_id for item in plan_items if item.status == "would_queue"}
    queued_item_ids: dict[str, tuple[str, str]] = {}

    async with get_db_context() as db:
        batch_event = AiGovernanceEvent(
            id=batch_event_id,
            event_type="knowledge_degradation",
            triggered_at=now,
            details=_build_batch_owner_review_event_details(
                request=request,
                items=plan_items,
                stale_ratio_snapshot=stale_ratio_snapshot,
            ),
            resolved=True,
            resolved_at=now,
        )
        batch_dispatch = GovernanceRemediationDispatch(
            id=batch_dispatch_id,
            governance_event_id=batch_event_id,
            event_type="knowledge_degradation",
            dispatch_status="succeeded",
            decision_context=_build_batch_owner_review_decision_context(
                batch_governance_event_id=batch_event_id,
                batch_dispatch_id=batch_dispatch_id,
                request=request,
                items=plan_items,
                stale_ratio_snapshot=stale_ratio_snapshot,
                plan_fingerprint=plan_fingerprint,
            ),
            executor_type=_BATCH_EXECUTOR_TYPE,
            attempt_count=0,
            max_attempts=1,
            dispatched_at=now,
            started_at=now,
            completed_at=now,
            created_by=request.owner[:100],
        )
        db.add(batch_event)
        db.add(batch_dispatch)

        for item in plan_items:
            if item.entry_id not in queued_ids:
                continue
            candidate = candidate_by_id[item.entry_id]
            event_id = generate_uuid()
            dispatch_id = generate_uuid()
            candidate_payload = candidate.model_dump(mode="json")
            event_details = _build_stale_owner_review_event_details(
                entry_id=item.entry_id,
                candidate=candidate_payload,
                owner=request.owner,
                owner_note=request.owner_note,
            )
            event_details["batch"] = {
                "batch_governance_event_id": batch_event_id,
                "batch_dispatch_id": batch_dispatch_id,
                "dry_run_plan_fingerprint": plan_fingerprint,
            }
            decision_context = _build_stale_owner_review_decision_context(
                governance_event_id=event_id,
                entry_id=item.entry_id,
                candidate=candidate_payload,
                owner=request.owner,
                owner_note=request.owner_note,
            )
            decision_context = _attach_batch_to_owner_review_context(
                decision_context,
                batch_governance_event_id=batch_event_id,
                batch_dispatch_id=batch_dispatch_id,
                plan_fingerprint=plan_fingerprint,
            )
            db.add(
                AiGovernanceEvent(
                    id=event_id,
                    event_type="knowledge_degradation",
                    triggered_at=now,
                    details=event_details,
                    resolved=False,
                )
            )
            db.add(
                GovernanceRemediationDispatch(
                    id=dispatch_id,
                    governance_event_id=event_id,
                    event_type="knowledge_degradation",
                    dispatch_status="pending",
                    playbook_id=None,
                    incident_id=None,
                    approval_id=None,
                    decision_context=decision_context,
                    executor_type=_EXECUTOR_TYPE,
                    attempt_count=0,
                    max_attempts=1,
                    dispatched_at=now,
                    created_by=request.owner[:100],
                )
            )
            queued_item_ids[item.entry_id] = (event_id, dispatch_id)
        await db.flush()

    updated_items: list[KnowledgeStaleOwnerReviewBatchItem] = []
    for item in plan_items:
        ids = queued_item_ids.get(item.entry_id)
        if ids is None:
            updated_items.append(item)
            continue
        event_id, dispatch_id = ids
        updated_items.append(
            item.model_copy(update={
                "status": "queued",
                "governance_event_id": event_id,
                "dispatch_id": dispatch_id,
                "workflow_stage": "waiting_owner_review",
            })
        )

    logger.info(
        "km_stale_owner_review_batch_queued",
        project_id=request.project_id,
        batch_governance_event_id=batch_event_id,
        batch_dispatch_id=batch_dispatch_id,
        queued_count=len(queued_item_ids),
        candidate_count=len(plan_items),
    )
    return {
        "batch_governance_event_id": batch_event_id,
        "batch_dispatch_id": batch_dispatch_id,
        "items": updated_items,
    }


def _attach_batch_to_owner_review_context(
    context: dict[str, Any],
    *,
    batch_governance_event_id: str,
    batch_dispatch_id: str,
    plan_fingerprint: str,
) -> dict[str, Any]:
    merged = dict(context)
    workflow = dict(merged.get("workflow") if isinstance(merged.get("workflow"), dict) else {})
    workflow.update({
        "batch_governance_event_id": batch_governance_event_id,
        "batch_dispatch_id": batch_dispatch_id,
        "batch_plan_fingerprint": plan_fingerprint,
    })
    merged["workflow"] = workflow
    merged["batch"] = {
        "batch_governance_event_id": batch_governance_event_id,
        "batch_dispatch_id": batch_dispatch_id,
        "dry_run_plan_fingerprint": plan_fingerprint,
    }
    return merged


def _build_batch_owner_review_event_details(
    *,
    request: KnowledgeStaleOwnerReviewBatchQueueRequest,
    items: list[KnowledgeStaleOwnerReviewBatchItem],
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
) -> dict[str, Any]:
    return {
        "schema_version": "km_stale_owner_review_batch_event_v1",
        "trigger_source": "stale_km_priority_batch_queue",
        "next_action": "owner_review_stale_km_batch",
        "impact": {
            "status": "batch_owner_review_queued",
            "project_id": request.project_id,
            "priority_tiers": list(dict.fromkeys(request.priority_tiers)),
            "requested_limit": request.limit,
            "candidate_count": len(items),
            "queued_count": _count_batch_items(items, "would_queue"),
            "already_queued_count": _count_batch_items(items, "already_queued"),
            "skipped_count": _count_batch_items(items, "skipped"),
            "stale_ratio": stale_ratio_snapshot.stale_ratio,
            "threshold": stale_ratio_snapshot.threshold,
        },
        "remediation": {
            "next_action": "owner_review_stale_km_batch",
            "items": [
                "review_p0_p1_stale_km_candidates",
                "complete_each_candidate_after_owner_approval",
                "run_stale_ratio_recheck_after_writeback",
            ],
        },
        "ownership": _stale_owner_review_ownership(),
        "owner": request.owner,
        "owner_note": request.owner_note,
        "stale_ratio_snapshot": stale_ratio_snapshot.model_dump(),
        "items": [item.model_dump() for item in items],
    }


def _build_batch_owner_review_decision_context(
    *,
    batch_governance_event_id: str,
    batch_dispatch_id: str,
    request: KnowledgeStaleOwnerReviewBatchQueueRequest,
    items: list[KnowledgeStaleOwnerReviewBatchItem],
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
    plan_fingerprint: str,
) -> dict[str, Any]:
    queued_count = _count_batch_items(items, "would_queue")
    return {
        "schema_version": "km_stale_owner_review_batch_dispatch_v1",
        "version": "v1",
        "trigger_source": "stale_km_priority_batch_queue",
        "triggered_metric": "knowledge_degradation",
        "metric_value": stale_ratio_snapshot.stale_ratio,
        "threshold": stale_ratio_snapshot.threshold,
        "suggested_action": "owner_review_stale_km_batch",
        "next_action": "owner_review_stale_km_batch",
        "decision_path": "batch_owner_review_queued",
        "ownership": _stale_owner_review_ownership(),
        "workflow": {
            "work_item_id": (
                "governance:knowledge_degradation:"
                f"{batch_governance_event_id}:km_stale_owner_review_batch"
            ),
            "work_kind": "km_stale_owner_review_batch",
            "current_stage": "batch_owner_review_queued",
            "project_id": request.project_id,
            "priority_tiers": list(dict.fromkeys(request.priority_tiers)),
            "requested_limit": request.limit,
            "batch_governance_event_id": batch_governance_event_id,
            "batch_dispatch_id": batch_dispatch_id,
            "steps": [
                "detected",
                "prioritized_stale_candidate",
                "batch_owner_review_queued",
                "waiting_owner_review",
                "owner_updates_or_archives_km",
                "stale_ratio_recheck",
            ],
            "stage_by_dispatch_status": {
                "pending": "batch_owner_review_queued",
                "dispatched": "batch_owner_review_queued",
                "executing": "batch_owner_review_queued",
                "succeeded": "batch_owner_review_queued",
                "failed": "needs_manual_km_triage",
                "skipped": "needs_manual_km_triage",
                "cancelled": "cancelled",
            },
            "next_action": "owner_review_stale_km_batch",
            "needs_human_review": True,
            "writes_km_without_approval": False,
            "writes_km": False,
            "writes_governance_audit": True,
            "dry_run_plan_fingerprint": plan_fingerprint,
            "stale_ratio_snapshot": stale_ratio_snapshot.model_dump(),
        },
        "worker_result": {
            "status": "batch_owner_review_queued",
            "candidate_count": len(items),
            "queued_count": queued_count,
            "already_queued_count": _count_batch_items(items, "already_queued"),
            "skipped_count": _count_batch_items(items, "skipped"),
            "writes_km": False,
        },
        "owner": request.owner,
        "owner_note": request.owner_note,
        "items": [item.model_dump() for item in items],
    }


def _build_batch_queue_response(
    *,
    request: KnowledgeStaleOwnerReviewBatchQueueRequest,
    status: Literal["dry_run", "queued", "noop_already_queued"],
    items: list[KnowledgeStaleOwnerReviewBatchItem],
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
    dry_run_plan_fingerprint: str,
    writes_governance_audit: bool,
    batch_governance_event_id: str | None = None,
    batch_dispatch_id: str | None = None,
) -> KnowledgeStaleOwnerReviewBatchQueueResponse:
    queued_status = "would_queue" if status == "dry_run" else "queued"
    workflow_stage = {
        "dry_run": "batch_owner_review_previewed",
        "queued": "batch_owner_review_queued",
        "noop_already_queued": "batch_noop_already_queued",
    }[status]
    return KnowledgeStaleOwnerReviewBatchQueueResponse(
        project_id=request.project_id,
        status=status,
        owner=request.owner,
        owner_note=request.owner_note,
        dry_run=request.dry_run,
        priority_tiers=list(dict.fromkeys(request.priority_tiers)),
        requested_limit=request.limit,
        candidate_count=len(items),
        queued_count=_count_batch_items(items, queued_status),
        already_queued_count=_count_batch_items(items, "already_queued"),
        skipped_count=_count_batch_items(items, "skipped"),
        batch_governance_event_id=batch_governance_event_id,
        batch_dispatch_id=batch_dispatch_id,
        workflow_stage=workflow_stage,
        writes_km=False,
        writes_governance_audit=writes_governance_audit,
        stale_ratio_snapshot=stale_ratio_snapshot,
        dry_run_plan_fingerprint=dry_run_plan_fingerprint,
        items=items,
        generated_at=now_taipei(),
    )


def _count_batch_items(
    items: list[KnowledgeStaleOwnerReviewBatchItem],
    status: str,
) -> int:
    return sum(1 for item in items if item.status == status)


def _build_owner_review_inbox_item(
    *,
    row: Any,
    candidate: KnowledgeStaleCandidate,
    decision_context: dict[str, Any],
) -> KnowledgeStaleOwnerReviewInboxItem:
    dispatch_status = str(row.dispatch_status)
    workflow = decision_context.get("workflow") if isinstance(decision_context.get("workflow"), dict) else {}
    batch = decision_context.get("batch") if isinstance(decision_context.get("batch"), dict) else {}
    return KnowledgeStaleOwnerReviewInboxItem(
        dispatch_id=str(row.id),
        governance_event_id=str(row.governance_event_id),
        entry_id=candidate.entry_id,
        project_id=candidate.project_id,
        title=candidate.title,
        dispatch_status=dispatch_status,
        workflow_stage=_extract_owner_review_workflow_stage(decision_context, dispatch_status),
        next_action=_extract_owner_review_next_action(decision_context),
        owner=_extract_owner_review_owner(decision_context),
        owner_note=_extract_owner_review_owner_note(decision_context),
        batch_governance_event_id=_first_non_empty_string(
            workflow.get("batch_governance_event_id"),
            batch.get("batch_governance_event_id"),
        ),
        batch_dispatch_id=_first_non_empty_string(
            workflow.get("batch_dispatch_id"),
            batch.get("batch_dispatch_id"),
        ),
        priority_tier=candidate.priority_tier,
        priority_score=candidate.priority_score,
        recommended_action=candidate.recommended_action,
        stale_days=candidate.stale_days,
        view_count=candidate.view_count,
        correlation_sources=candidate.correlation_sources,
        reasons=candidate.reasons,
        related_incident_id=candidate.related_incident_id,
        related_playbook_id=candidate.related_playbook_id,
        related_approval_id=candidate.related_approval_id,
        dry_run_plan_fingerprint=_extract_plan_fingerprint(decision_context),
        queued_at=row.dispatched_at,
        started_at=row.started_at,
        completed_at=row.completed_at,
    )


def _build_completion_queue_item(
    item: KnowledgeStaleOwnerReviewInboxItem,
) -> KnowledgeStaleOwnerReviewCompletionQueueItem:
    outcome = _completion_outcome_for_owner_review_item(item)
    blockers = _completion_queue_blockers(item, outcome=outcome)
    readiness = _completion_queue_readiness(item, blockers=blockers)
    can_preview = readiness == "ready"
    return KnowledgeStaleOwnerReviewCompletionQueueItem(
        dispatch_id=item.dispatch_id,
        governance_event_id=item.governance_event_id,
        entry_id=item.entry_id,
        project_id=item.project_id,
        title=item.title,
        dispatch_status=item.dispatch_status,
        workflow_stage=item.workflow_stage,
        readiness=readiness,
        recommended_completion_outcome=outcome,
        next_action=_completion_queue_next_action(readiness),
        blockers=blockers,
        required_owner_fields=_completion_required_owner_fields(outcome),
        can_preview=can_preview,
        can_confirm_after_preview=can_preview,
        writes_km_on_confirm=can_preview,
        owner=item.owner,
        owner_note=item.owner_note,
        batch_governance_event_id=item.batch_governance_event_id,
        batch_dispatch_id=item.batch_dispatch_id,
        priority_tier=item.priority_tier,
        priority_score=item.priority_score,
        recommended_action=item.recommended_action,
        stale_days=item.stale_days,
        view_count=item.view_count,
        correlation_sources=item.correlation_sources,
        reasons=item.reasons,
        related_incident_id=item.related_incident_id,
        related_playbook_id=item.related_playbook_id,
        related_approval_id=item.related_approval_id,
        dry_run_plan_fingerprint=item.dry_run_plan_fingerprint,
        queued_at=item.queued_at,
        started_at=item.started_at,
        completed_at=item.completed_at,
    )


def _completion_queue_bucket_matches(
    item: KnowledgeStaleOwnerReviewCompletionQueueItem,
    *,
    status_bucket: str,
) -> bool:
    if status_bucket == "all":
        return True
    if status_bucket == "pending":
        return item.dispatch_status in _ACTIVE_DISPATCH_STATUSES
    return item.readiness == status_bucket


def _normalize_priority_tiers(priority_tiers: list[str] | None) -> list[str]:
    if not priority_tiers:
        return []
    allowed = {"P0", "P1", "P2"}
    normalized = list(dict.fromkeys(str(tier).upper() for tier in priority_tiers))
    unsupported = [tier for tier in normalized if tier not in allowed]
    if unsupported:
        raise KmStaleOwnerReviewError(
            422,
            "unsupported stale KM owner-review completion priority_tier",
        )
    return normalized


def _normalize_completion_outcome(outcome: str) -> Literal[
    "all",
    "refresh_with_evidence",
    "archive",
    "supersede",
]:
    if outcome == "all":
        return "all"
    if outcome == "refresh_with_evidence":
        return "refresh_with_evidence"
    if outcome == "archive":
        return "archive"
    if outcome == "supersede":
        return "supersede"
    raise KmStaleOwnerReviewError(
        422,
        "unsupported stale KM owner-review completion outcome",
    )


def _filter_completion_queue_items(
    items: list[KnowledgeStaleOwnerReviewCompletionQueueItem],
    *,
    priority_tiers: list[str],
    recommended_completion_outcome: str,
    batch_governance_event_id: str | None,
    can_preview: bool | None,
) -> list[KnowledgeStaleOwnerReviewCompletionQueueItem]:
    filtered = items
    if priority_tiers:
        wanted = set(priority_tiers)
        filtered = [item for item in filtered if item.priority_tier in wanted]
    if recommended_completion_outcome != "all":
        filtered = [
            item for item in filtered
            if item.recommended_completion_outcome == recommended_completion_outcome
        ]
    if batch_governance_event_id:
        filtered = [
            item for item in filtered
            if item.batch_governance_event_id == batch_governance_event_id
        ]
    if can_preview is not None:
        filtered = [item for item in filtered if item.can_preview is can_preview]
    return filtered


def _build_completion_batch_preview_fingerprint(
    *,
    request: KnowledgeStaleOwnerReviewCompletionBatchPreviewRequest,
    items: list[KnowledgeStaleOwnerReviewCompletionQueueItem],
) -> str:
    payload = {
        "schema_version": "km_stale_owner_review_completion_batch_preview_v1",
        "project_id": request.project_id,
        "status_bucket": request.status_bucket,
        "priority_tiers": list(dict.fromkeys(request.priority_tiers)),
        "recommended_completion_outcome": request.recommended_completion_outcome,
        "batch_governance_event_id": request.batch_governance_event_id,
        "limit": request.limit,
        "owner": request.owner,
        "owner_note_sha256": (
            hashlib.sha256(request.owner_note.encode("utf-8")).hexdigest()
            if request.owner_note
            else None
        ),
        "items": [
            {
                "dispatch_id": item.dispatch_id,
                "entry_id": item.entry_id,
                "readiness": item.readiness,
                "recommended_completion_outcome": item.recommended_completion_outcome,
                "can_preview": item.can_preview,
                "workflow_stage": item.workflow_stage,
            }
            for item in items
        ],
    }
    encoded = json.dumps(
        payload,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )
    return "sha256:" + hashlib.sha256(encoded.encode("utf-8")).hexdigest()


def _completion_outcome_for_owner_review_item(
    item: KnowledgeStaleOwnerReviewInboxItem,
) -> Literal["refresh_with_evidence", "archive", "supersede"]:
    if item.recommended_action == "archive_or_supersede":
        return "archive"
    return "refresh_with_evidence"


def _completion_required_owner_fields(
    outcome: Literal["refresh_with_evidence", "archive", "supersede"],
) -> list[str]:
    if outcome == "refresh_with_evidence":
        return ["owner_note_or_updated_content"]
    if outcome == "supersede":
        return ["superseded_by_entry_id", "owner_note"]
    return ["owner_note"]


def _completion_queue_blockers(
    item: KnowledgeStaleOwnerReviewInboxItem,
    *,
    outcome: Literal["refresh_with_evidence", "archive", "supersede"],
) -> list[str]:
    blockers: list[str] = []
    if not item.dispatch_id:
        blockers.append("missing_dispatch_id")
    if not item.entry_id:
        blockers.append("missing_entry_id")
    if item.dispatch_status == "skipped":
        blockers.append("owner_review_skipped")
    if item.dispatch_status not in (
        _ACTIVE_DISPATCH_STATUSES
        | _TERMINAL_FAILED_DISPATCH_STATUSES
        | frozenset({"succeeded", "skipped"})
    ):
        blockers.append("unsupported_dispatch_status")
    if outcome == "supersede":
        blockers.append("superseded_by_entry_id_required")
    return blockers


def _completion_queue_readiness(
    item: KnowledgeStaleOwnerReviewInboxItem,
    *,
    blockers: list[str],
) -> Literal["ready", "blocked", "completed", "failed"]:
    if item.dispatch_status == "succeeded":
        return "completed"
    if item.dispatch_status in _TERMINAL_FAILED_DISPATCH_STATUSES:
        return "failed"
    if item.dispatch_status in _ACTIVE_DISPATCH_STATUSES and not blockers:
        return "ready"
    return "blocked"


def _completion_queue_next_action(
    readiness: Literal["ready", "blocked", "completed", "failed"],
) -> str:
    return {
        "ready": "preview_stale_km_review_completion",
        "blocked": "inspect_owner_review_blocker",
        "completed": "view_stale_km_completion_audit",
        "failed": "manual_km_owner_review_triage",
    }[readiness]


async def _load_owner_review_burndown_counts(
    db: AsyncSession,
    *,
    project_id: str,
) -> dict[str, int]:
    sql = text("""
        SELECT
            COUNT(*) FILTER (
                WHERE d.executor_type = :owner_executor
                  AND d.dispatch_status::text IN ('pending', 'dispatched', 'executing')
            ) AS pending_owner_reviews,
            COUNT(*) FILTER (
                WHERE d.executor_type = :owner_executor
                  AND d.dispatch_status::text = 'succeeded'
            ) AS completed_owner_reviews,
            COUNT(*) FILTER (
                WHERE d.executor_type = :complete_executor
                  AND d.dispatch_status::text = 'succeeded'
            ) AS completion_audit_total,
            COUNT(*) FILTER (
                WHERE d.executor_type = :complete_executor
                  AND d.dispatch_status::text = 'succeeded'
                  AND d.decision_context -> 'stale_ratio_recheck' ->> 'dispatch_id' IS NOT NULL
            ) AS stale_ratio_recheck_total
        FROM governance_remediation_dispatch d
        WHERE d.event_type = 'knowledge_degradation'
          AND d.executor_type IN (:owner_executor, :complete_executor)
          AND COALESCE(
                d.decision_context -> 'workflow' ->> 'project_id',
                d.decision_context ->> 'project_id',
                d.decision_context -> 'candidate' ->> 'project_id'
              ) = :project_id
    """)
    result = await db.execute(
        sql,
        {
            "owner_executor": _EXECUTOR_TYPE,
            "complete_executor": _COMPLETE_EXECUTOR_TYPE,
            "project_id": project_id,
        },
    )
    row = result.first()
    return {
        "pending_owner_reviews": int(row.pending_owner_reviews or 0) if row else 0,
        "completed_owner_reviews": int(row.completed_owner_reviews or 0) if row else 0,
        "completion_audit_total": int(row.completion_audit_total or 0) if row else 0,
        "stale_ratio_recheck_total": int(row.stale_ratio_recheck_total or 0) if row else 0,
    }


async def _load_owner_review_completion_rows(
    db: AsyncSession,
    *,
    project_id: str,
    limit: int,
) -> list[Any]:
    sql = text("""
        SELECT
            d.id,
            d.governance_event_id,
            d.dispatch_status,
            d.decision_context,
            d.dispatched_at,
            d.started_at,
            d.completed_at
        FROM governance_remediation_dispatch d
        WHERE d.executor_type = :complete_executor
          AND d.dispatch_status::text = 'succeeded'
          AND COALESCE(
                d.decision_context -> 'workflow' ->> 'project_id',
                d.decision_context ->> 'project_id'
              ) = :project_id
        ORDER BY d.completed_at DESC NULLS LAST, d.dispatched_at DESC
        LIMIT :limit
    """)
    result = await db.execute(
        sql,
        {
            "complete_executor": _COMPLETE_EXECUTOR_TYPE,
            "project_id": project_id,
            "limit": limit,
        },
    )
    return list(result.fetchall())


def _build_owner_review_burndown_items(
    rows: list[Any],
    *,
    project_id: str,
) -> list[KnowledgeStaleOwnerReviewBurnDownItem]:
    chronological: list[KnowledgeStaleOwnerReviewBurnDownItem] = []
    previous_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
    for row in reversed(rows):
        context = row.decision_context if isinstance(row.decision_context, dict) else {}
        snapshot = _snapshot_from_context(context)
        stale_count_delta: int | None = None
        stale_ratio_delta: float | None = None
        if snapshot is not None and previous_snapshot is not None:
            stale_count_delta = snapshot.stale_count - previous_snapshot.stale_count
            stale_ratio_delta = round(snapshot.stale_ratio - previous_snapshot.stale_ratio, 3)
        if snapshot is not None:
            previous_snapshot = snapshot

        chronological.append(
            _build_owner_review_burndown_item(
                row=row,
                context=context,
                project_id=project_id,
                stale_count_delta=stale_count_delta,
                stale_ratio_delta=stale_ratio_delta,
            )
        )
    return list(reversed(chronological))


def _build_owner_review_burndown_item(
    *,
    row: Any,
    context: dict[str, Any],
    project_id: str,
    stale_count_delta: int | None,
    stale_ratio_delta: float | None,
) -> KnowledgeStaleOwnerReviewBurnDownItem:
    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 {}
    snapshot = _snapshot_from_context(context)
    dispatch_status = str(row.dispatch_status)
    return KnowledgeStaleOwnerReviewBurnDownItem(
        completion_dispatch_id=str(row.id),
        governance_event_id=str(row.governance_event_id),
        source_dispatch_id=_first_non_empty_string(
            workflow.get("source_dispatch_id"),
            context.get("source_dispatch_id"),
        ),
        recheck_dispatch_id=_extract_recheck_dispatch_id(context),
        entry_id=_first_non_empty_string(
            workflow.get("entry_id"),
            context.get("entry_id"),
            worker_result.get("entry_id"),
        ),
        project_id=_first_non_empty_string(
            workflow.get("project_id"),
            context.get("project_id"),
        ) or project_id,
        dispatch_status=dispatch_status,
        workflow_stage=_extract_complete_workflow_stage(context, dispatch_status),
        review_outcome=_normalize_optional_review_outcome(
            workflow.get("review_outcome") or context.get("review_outcome") or worker_result.get("review_outcome")
        ),
        owner=_first_non_empty_string(context.get("owner")),
        completed_at=row.completed_at,
        stale_ratio_snapshot=snapshot,
        stale_count_delta=stale_count_delta,
        stale_ratio_delta=stale_ratio_delta,
        above_threshold=(
            snapshot.stale_ratio > snapshot.threshold
            if snapshot is not None
            else None
        ),
    )


def _stale_burndown_status(
    snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None,
) -> Literal["above_threshold", "at_or_below_threshold", "no_data"]:
    if snapshot is None or snapshot.total_count <= 0:
        return "no_data"
    if snapshot.stale_ratio > snapshot.threshold:
        return "above_threshold"
    return "at_or_below_threshold"


def _entries_to_stale_threshold(
    snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None,
) -> int:
    if snapshot is None or snapshot.total_count <= 0:
        return 0
    allowed_stale = int(snapshot.total_count * snapshot.threshold)
    return max(0, snapshot.stale_count - allowed_stale)


def _priority_rank(priority_tier: str) -> int:
    return {"P0": 3, "P1": 2, "P2": 1}.get(priority_tier, 0)


def _extract_owner_review_workflow_stage(
    context: dict[str, Any],
    dispatch_status: str,
) -> str:
    workflow = context.get("workflow")
    if isinstance(workflow, dict):
        stages = workflow.get("stage_by_dispatch_status")
        if isinstance(stages, dict):
            value = stages.get(dispatch_status)
            if isinstance(value, str) and value:
                return value
        value = workflow.get("current_stage")
        if isinstance(value, str) and value:
            return value
    return {
        "pending": "waiting_owner_review",
        "dispatched": "waiting_owner_review",
        "executing": "owner_review_in_progress",
        "succeeded": "km_candidate_reviewed",
        "failed": "needs_manual_km_triage",
        "skipped": "waiting_owner_review",
        "cancelled": "cancelled",
    }.get(dispatch_status, "unknown")


def _extract_owner_review_next_action(context: dict[str, Any]) -> str | None:
    workflow = context.get("workflow")
    if isinstance(workflow, dict):
        value = workflow.get("next_action")
        if isinstance(value, str) and value:
            return value
    value = context.get("next_action")
    return value if isinstance(value, str) and value else None


def _extract_owner_review_owner(context: dict[str, Any]) -> str | None:
    value = context.get("owner")
    return value if isinstance(value, str) and value else None


def _extract_owner_review_owner_note(context: dict[str, Any]) -> str | None:
    value = context.get("owner_note")
    return value if isinstance(value, str) and value else None


def _first_non_empty_string(*values: Any) -> str | None:
    for value in values:
        if isinstance(value, str) and value:
            return value
    return None


async def complete_km_stale_owner_review(
    *,
    entry_id: str,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
) -> KnowledgeStaleOwnerReviewCompleteResponse:
    """Complete stale KM owner review after explicit owner approval."""
    completed = await _load_completed_owner_review_dispatch(entry_id)
    if completed is not None and not request.dry_run:
        decision_context = completed.decision_context if isinstance(completed.decision_context, dict) else {}
        workflow = decision_context.get("workflow") if isinstance(decision_context, dict) else {}
        worker_result = decision_context.get("worker_result") if isinstance(decision_context, dict) else {}
        return _build_complete_response(
            entry_id=entry_id,
            project_id=str(workflow.get("project_id") or decision_context.get("project_id") or "awoooi"),
            status="already_completed",
            review_outcome=_normalize_review_outcome(worker_result.get("review_outcome")),
            governance_event_id=str(completed.governance_event_id),
            dispatch_id=str(workflow.get("source_dispatch_id") or request.dispatch_id or ""),
            audit_dispatch_id=str(completed.id),
            stale_ratio_recheck_dispatch_id=_extract_recheck_dispatch_id(decision_context),
            workflow_stage=_extract_complete_workflow_stage(decision_context, "succeeded"),
            owner=request.owner,
            owner_approved=request.owner_approved,
            dry_run=request.dry_run,
            writes_km=False,
            writes_governance_audit=False,
            stale_ratio_snapshot=_snapshot_from_context(decision_context),
            dry_run_plan_fingerprint=_extract_plan_fingerprint(decision_context),
        )

    record = await _load_stale_or_reviewed_record(entry_id)
    dispatch = await _load_owner_review_dispatch_for_completion(
        entry_id=entry_id,
        dispatch_id=request.dispatch_id,
    )
    _validate_completion_request(record, request)
    superseded_by = await _load_superseded_by_record(request.superseded_by_entry_id)
    fingerprint = _build_completion_plan_fingerprint(
        record=record,
        dispatch=dispatch,
        request=request,
        superseded_by=superseded_by,
    )

    if request.dry_run:
        return _build_complete_response(
            entry_id=entry_id,
            project_id=str(record.project_id),
            status="dry_run",
            review_outcome=request.review_outcome,
            governance_event_id=str(dispatch.governance_event_id),
            dispatch_id=str(dispatch.id),
            workflow_stage=_completion_stage_for_outcome(request.review_outcome),
            owner=request.owner,
            owner_approved=request.owner_approved,
            dry_run=True,
            writes_km=False,
            writes_governance_audit=False,
            stale_ratio_snapshot=await _load_current_km_stale_ratio_snapshot(),
            dry_run_plan_fingerprint=fingerprint,
        )

    if not request.owner_approved:
        raise KmStaleOwnerReviewError(
            403,
            "owner_approved=true is required before completing stale KM owner review",
        )
    if not request.dry_run_plan_fingerprint:
        raise KmStaleOwnerReviewError(
            403,
            "dry_run_plan_fingerprint from a dry-run preview is required before writing KM",
        )
    if request.dry_run_plan_fingerprint != fingerprint:
        raise KmStaleOwnerReviewError(
            409,
            "dry_run_plan_fingerprint does not match the latest stale KM review plan",
        )

    result = await _complete_owner_review_and_write_audit(
        entry_id=entry_id,
        request=request,
        resolved_dispatch_id=str(dispatch.id),
        plan_fingerprint=fingerprint,
    )
    return _build_complete_response(
        entry_id=entry_id,
        project_id=result["project_id"],
        status="completed",
        review_outcome=request.review_outcome,
        governance_event_id=result["governance_event_id"],
        dispatch_id=result["dispatch_id"],
        audit_dispatch_id=result["audit_dispatch_id"],
        stale_ratio_recheck_dispatch_id=result["recheck_dispatch_id"],
        workflow_stage=_completion_stage_for_outcome(request.review_outcome),
        owner=request.owner,
        owner_approved=True,
        dry_run=False,
        writes_km=True,
        writes_governance_audit=True,
        stale_ratio_snapshot=result["stale_ratio_snapshot"],
        dry_run_plan_fingerprint=fingerprint,
    )


async def _load_stale_candidate_record(entry_id: str) -> KnowledgeEntryRecord:
    cutoff = now_taipei() - timedelta(days=KM_STALE_DAYS)
    async with get_db_context() as db:
        result = await db.execute(
            select(KnowledgeEntryRecord).where(KnowledgeEntryRecord.id == entry_id)
        )
        record = result.scalar_one_or_none()

    if record is None:
        raise KmStaleOwnerReviewError(404, "KM entry not found")
    if _enum_value(record.status) == EntryStatus.ARCHIVED.value:
        raise KmStaleOwnerReviewError(409, "archived KM entries cannot be queued for stale review")
    updated_at = record.updated_at
    if updated_at is not None and updated_at.tzinfo is None:
        updated_at = updated_at.replace(tzinfo=cutoff.tzinfo)
    if updated_at is None or updated_at >= cutoff:
        raise KmStaleOwnerReviewError(409, "KM entry is no longer past the stale threshold")
    return record


async def _load_active_owner_review_dispatch(
    entry_id: str,
) -> GovernanceRemediationDispatch | None:
    sql = text("""
        SELECT *
        FROM governance_remediation_dispatch d
        WHERE d.executor_type = :executor_type
          AND d.dispatch_status::text IN ('pending', 'dispatched', 'executing')
          AND (
            d.decision_context -> 'workflow' ->> 'entry_id' = :entry_id
            OR d.decision_context ->> 'entry_id' = :entry_id
          )
        ORDER BY d.dispatched_at DESC
        LIMIT 1
    """)
    async with get_db_context() as db:
        result = await db.execute(
            select(GovernanceRemediationDispatch).from_statement(sql),
            {"executor_type": _EXECUTOR_TYPE, "entry_id": entry_id},
        )
        return result.scalar_one_or_none()


async def _load_owner_review_dispatch_for_completion(
    *,
    entry_id: str,
    dispatch_id: str | None,
) -> GovernanceRemediationDispatch:
    """Load the active owner-review dispatch that guards KM writes."""
    id_filter = "AND d.id = :dispatch_id" if dispatch_id else ""
    sql = text(f"""
        SELECT *
        FROM governance_remediation_dispatch d
        WHERE d.executor_type = :executor_type
          AND d.dispatch_status::text IN ('pending', 'dispatched', 'executing')
          AND (
            d.decision_context -> 'workflow' ->> 'entry_id' = :entry_id
            OR d.decision_context ->> 'entry_id' = :entry_id
          )
          {id_filter}
        ORDER BY d.dispatched_at DESC
        LIMIT 1
    """)
    params = {
        "executor_type": _EXECUTOR_TYPE,
        "entry_id": entry_id,
    }
    if dispatch_id:
        params["dispatch_id"] = dispatch_id

    async with get_db_context() as db:
        result = await db.execute(
            select(GovernanceRemediationDispatch).from_statement(sql),
            params,
        )
        dispatch = result.scalar_one_or_none()

    if dispatch is None:
        raise KmStaleOwnerReviewError(
            409,
            "active stale KM owner-review dispatch not found; queue review first",
        )
    return dispatch


async def _load_completed_owner_review_dispatch(
    entry_id: str,
) -> GovernanceRemediationDispatch | None:
    sql = text("""
        SELECT *
        FROM governance_remediation_dispatch d
        WHERE d.executor_type = :executor_type
          AND d.dispatch_status::text = 'succeeded'
          AND (
            d.decision_context -> 'workflow' ->> 'entry_id' = :entry_id
            OR d.decision_context ->> 'entry_id' = :entry_id
          )
        ORDER BY d.completed_at DESC NULLS LAST, d.dispatched_at DESC
        LIMIT 1
    """)
    async with get_db_context() as db:
        result = await db.execute(
            select(GovernanceRemediationDispatch).from_statement(sql),
            {"executor_type": _COMPLETE_EXECUTOR_TYPE, "entry_id": entry_id},
        )
        return result.scalar_one_or_none()


async def _load_stale_or_reviewed_record(entry_id: str) -> KnowledgeEntryRecord:
    async with get_db_context() as db:
        result = await db.execute(
            select(KnowledgeEntryRecord).where(KnowledgeEntryRecord.id == entry_id)
        )
        record = result.scalar_one_or_none()

    if record is None:
        raise KmStaleOwnerReviewError(404, "KM entry not found")
    if _enum_value(record.status) == EntryStatus.ARCHIVED.value:
        raise KmStaleOwnerReviewError(409, "archived KM entries cannot be completed as stale review")
    return record


async def _load_superseded_by_record(
    superseded_by_entry_id: str | None,
) -> KnowledgeEntryRecord | None:
    if not superseded_by_entry_id:
        return None
    async with get_db_context() as db:
        result = await db.execute(
            select(KnowledgeEntryRecord).where(
                KnowledgeEntryRecord.id == superseded_by_entry_id
            )
        )
        record = result.scalar_one_or_none()
    if record is None:
        raise KmStaleOwnerReviewError(404, "superseded_by_entry_id KM entry not found")
    if _enum_value(record.status) == EntryStatus.ARCHIVED.value:
        raise KmStaleOwnerReviewError(409, "superseded_by_entry_id cannot be archived")
    return record


def _validate_completion_request(
    record: KnowledgeEntryRecord,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
) -> None:
    """Validate owner intent before any KM write is allowed."""
    if request.review_outcome == "supersede":
        if not request.superseded_by_entry_id:
            raise KmStaleOwnerReviewError(
                422,
                "superseded_by_entry_id is required when review_outcome=supersede",
            )
        if request.superseded_by_entry_id == str(record.id):
            raise KmStaleOwnerReviewError(409, "KM entry cannot supersede itself")
    if request.review_outcome == "refresh_with_evidence" and not (
        request.owner_note or request.updated_title or request.updated_content
    ):
        raise KmStaleOwnerReviewError(
            422,
            "owner_note or updated KM content is required for refresh_with_evidence",
        )


def _build_completion_plan_fingerprint(
    *,
    record: KnowledgeEntryRecord,
    dispatch: GovernanceRemediationDispatch,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
    superseded_by: KnowledgeEntryRecord | None,
) -> str:
    """Fingerprint the exact owner-reviewed plan that may write KM."""
    payload = {
        "schema_version": "km_stale_owner_review_completion_plan_v1",
        "entry_id": str(record.id),
        "dispatch_id": str(dispatch.id),
        "governance_event_id": str(dispatch.governance_event_id),
        "review_outcome": request.review_outcome,
        "current_status": _enum_value(record.status),
        "current_updated_at": record.updated_at.isoformat() if record.updated_at else None,
        "updated_title": request.updated_title,
        "updated_content_sha256": (
            hashlib.sha256(request.updated_content.encode("utf-8")).hexdigest()
            if request.updated_content
            else None
        ),
        "owner_note_sha256": (
            hashlib.sha256(request.owner_note.encode("utf-8")).hexdigest()
            if request.owner_note
            else None
        ),
        "superseded_by_entry_id": str(superseded_by.id) if superseded_by else None,
        "superseded_by_updated_at": (
            superseded_by.updated_at.isoformat()
            if superseded_by and superseded_by.updated_at
            else None
        ),
    }
    encoded = json.dumps(
        payload,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )
    return "sha256:" + hashlib.sha256(encoded.encode("utf-8")).hexdigest()


async def _complete_owner_review_and_write_audit(
    *,
    entry_id: str,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
    resolved_dispatch_id: str,
    plan_fingerprint: str,
) -> dict[str, Any]:
    """Apply owner-approved KM outcome and write terminal dispatch trail."""
    now = now_taipei()
    async with get_db_context() as db:
        record_result = await db.execute(
            select(KnowledgeEntryRecord)
            .where(KnowledgeEntryRecord.id == entry_id)
            .with_for_update()
        )
        record = record_result.scalar_one_or_none()
        if record is None:
            raise KmStaleOwnerReviewError(404, "KM entry not found")
        if _enum_value(record.status) == EntryStatus.ARCHIVED.value:
            raise KmStaleOwnerReviewError(409, "KM entry was archived before completion")

        dispatch_result = await db.execute(
            select(GovernanceRemediationDispatch)
            .where(GovernanceRemediationDispatch.id == resolved_dispatch_id)
            .with_for_update()
        )
        dispatch = dispatch_result.scalar_one_or_none()
        if dispatch is None:
            raise KmStaleOwnerReviewError(
                409,
                "active stale KM owner-review dispatch not found during write",
            )
        if str(dispatch.dispatch_status) not in {"pending", "dispatched", "executing"}:
            raise KmStaleOwnerReviewError(
                409,
                "stale KM owner-review dispatch is no longer active",
            )
        if str(dispatch.id) != (request.dispatch_id or str(dispatch.id)):
            raise KmStaleOwnerReviewError(409, "dispatch_id does not match active owner review")
        dispatch_entry_id = _dispatch_entry_id(dispatch)
        if dispatch_entry_id != entry_id:
            raise KmStaleOwnerReviewError(409, "dispatch_id does not match KM entry")

        _apply_owner_review_outcome(record, request, now=now)
        dispatch.dispatch_status = "succeeded"
        dispatch.started_at = dispatch.started_at or taipei_now()
        dispatch.completed_at = taipei_now()
        dispatch.decision_context = _merge_owner_review_completion_into_context(
            dispatch.decision_context if isinstance(dispatch.decision_context, dict) else {},
            request=request,
            plan_fingerprint=plan_fingerprint,
            completed_at=now.isoformat(),
        )

        await db.flush()
        stale_ratio_snapshot = await _compute_km_stale_ratio_snapshot(db)
        recheck = GovernanceRemediationDispatch(
            id=generate_uuid(),
            governance_event_id=str(dispatch.governance_event_id),
            event_type="knowledge_degradation",
            dispatch_status="succeeded",
            decision_context=_build_stale_ratio_recheck_context(
                governance_event_id=str(dispatch.governance_event_id),
                entry_id=entry_id,
                request=request,
                stale_ratio_snapshot=stale_ratio_snapshot,
                plan_fingerprint=plan_fingerprint,
                project_id=str(record.project_id),
            ),
            executor_type=_RECHECK_EXECUTOR_TYPE,
            attempt_count=0,
            max_attempts=1,
            dispatched_at=taipei_now(),
            started_at=taipei_now(),
            completed_at=taipei_now(),
            created_by=request.owner[:100],
        )
        audit = GovernanceRemediationDispatch(
            id=generate_uuid(),
            governance_event_id=str(dispatch.governance_event_id),
            event_type="knowledge_degradation",
            dispatch_status="succeeded",
            decision_context=_build_owner_review_completion_audit_context(
                governance_event_id=str(dispatch.governance_event_id),
                source_dispatch_id=str(dispatch.id),
                entry=record,
                request=request,
                stale_ratio_snapshot=stale_ratio_snapshot,
                recheck_dispatch_id=str(recheck.id),
                plan_fingerprint=plan_fingerprint,
                completed_at=now.isoformat(),
            ),
            executor_type=_COMPLETE_EXECUTOR_TYPE,
            attempt_count=0,
            max_attempts=1,
            dispatched_at=taipei_now(),
            started_at=taipei_now(),
            completed_at=taipei_now(),
            created_by=request.owner[:100],
        )
        db.add(recheck)
        db.add(audit)
        await db.flush()

        return {
            "project_id": str(record.project_id),
            "governance_event_id": str(dispatch.governance_event_id),
            "dispatch_id": str(dispatch.id),
            "audit_dispatch_id": str(audit.id),
            "recheck_dispatch_id": str(recheck.id),
            "stale_ratio_snapshot": stale_ratio_snapshot,
        }


def _dispatch_entry_id(dispatch: GovernanceRemediationDispatch) -> str | None:
    context = dispatch.decision_context if isinstance(dispatch.decision_context, dict) else {}
    workflow = context.get("workflow") if isinstance(context.get("workflow"), dict) else {}
    for source in (workflow, context):
        value = source.get("entry_id") if isinstance(source, dict) else None
        if isinstance(value, str) and value:
            return value
    return None


def _apply_owner_review_outcome(
    record: KnowledgeEntryRecord,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
    *,
    now,
) -> None:
    if request.updated_title:
        record.title = request.updated_title
    if request.updated_content:
        record.content = request.updated_content
    if request.review_outcome in ("archive", "supersede"):
        record.status = EntryStatus.ARCHIVED
    record.tags = _append_owner_review_tags(
        [str(tag) for tag in (record.tags or [])],
        request=request,
        reviewed_at=now.isoformat(),
    )
    record.updated_at = now


def _append_owner_review_tags(
    tags: list[str],
    *,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
    reviewed_at: str,
) -> list[str]:
    additions = [
        "reviewed_by:km_stale_owner_review",
        f"stale_review_outcome:{request.review_outcome}",
        f"stale_review_owner:{request.owner[:80]}",
        f"stale_reviewed_at:{reviewed_at}",
    ]
    if request.superseded_by_entry_id:
        additions.append(f"superseded_by:{request.superseded_by_entry_id}")
    merged = list(tags)
    for tag in additions:
        if tag not in merged:
            merged.append(tag)
    return merged


def _merge_owner_review_completion_into_context(
    context: dict[str, Any],
    *,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
    plan_fingerprint: str,
    completed_at: str,
) -> dict[str, Any]:
    merged = dict(context)
    workflow = dict(merged.get("workflow") if isinstance(merged.get("workflow"), dict) else {})
    workflow.update({
        "current_stage": _completion_stage_for_outcome(request.review_outcome),
        "next_action": "stale_ratio_recheck",
        "review_outcome": request.review_outcome,
        "owner_approved": request.owner_approved,
        "dry_run_plan_fingerprint": plan_fingerprint,
        "completed_at": completed_at,
        "writes_km": True,
        "writes_governance_audit": True,
    })
    workflow["steps"] = list(dict.fromkeys([
        *[str(step) for step in workflow.get("steps", []) if step is not None],
        "owner_updates_or_archives_km",
        _completion_stage_for_outcome(request.review_outcome),
        "stale_ratio_recheck",
    ]))
    workflow["stage_by_dispatch_status"] = {
        **(
            workflow.get("stage_by_dispatch_status")
            if isinstance(workflow.get("stage_by_dispatch_status"), dict)
            else {}
        ),
        "succeeded": _completion_stage_for_outcome(request.review_outcome),
        "failed": "needs_manual_km_triage",
    }
    merged["workflow"] = workflow
    merged["next_action"] = "stale_ratio_recheck"
    merged["decision_path"] = f"owner_approved_{request.review_outcome}"
    merged["worker_result"] = {
        "status": "owner_review_completed",
        "review_outcome": request.review_outcome,
        "writes_km": True,
        "completed_at": completed_at,
    }
    return merged


def _build_owner_review_completion_audit_context(
    *,
    governance_event_id: str,
    source_dispatch_id: str,
    entry: KnowledgeEntryRecord,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
    recheck_dispatch_id: str,
    plan_fingerprint: str,
    completed_at: str,
) -> dict[str, Any]:
    stage = _completion_stage_for_outcome(request.review_outcome)
    return {
        "schema_version": "km_stale_owner_review_complete_audit_v1",
        "version": "v1",
        "project_id": str(entry.project_id),
        "decision_path": f"owner_approved_{request.review_outcome}",
        "next_action": "stale_ratio_recheck",
        "ownership": _stale_owner_review_ownership(),
        "workflow": {
            "work_item_id": (
                "governance:knowledge_degradation:"
                f"{governance_event_id}:km_stale_owner_review_complete:{entry.id}"
            ),
            "work_kind": "km_stale_owner_review_complete",
            "current_stage": stage,
            "entry_id": str(entry.id),
            "project_id": str(entry.project_id),
            "source_dispatch_id": source_dispatch_id,
            "steps": [
                "detected",
                "prioritized_stale_candidate",
                "waiting_owner_review",
                "owner_updates_or_archives_km",
                stage,
                "stale_ratio_recheck",
            ],
            "stage_by_dispatch_status": {
                "succeeded": stage,
                "failed": "needs_manual_km_triage",
                "cancelled": "cancelled",
            },
            "next_action": "stale_ratio_recheck",
            "writes_km_without_approval": False,
            "writes_km": True,
            "writes_governance_audit": True,
            "dry_run_plan_fingerprint": plan_fingerprint,
            "stale_ratio_snapshot": stale_ratio_snapshot.model_dump(),
        },
        "worker_result": {
            "status": "owner_review_completed",
            "entry_id": str(entry.id),
            "review_outcome": request.review_outcome,
            "owner_approved": request.owner_approved,
            "writes_km": True,
            "stale_ratio": stale_ratio_snapshot.stale_ratio,
            "threshold": stale_ratio_snapshot.threshold,
            "above_threshold": stale_ratio_snapshot.stale_ratio > stale_ratio_snapshot.threshold,
        },
        "owner": request.owner,
        "owner_note": request.owner_note,
        "entry_id": str(entry.id),
        "review_outcome": request.review_outcome,
        "source_dispatch_id": source_dispatch_id,
        "superseded_by_entry_id": request.superseded_by_entry_id,
        "updated_title": request.updated_title,
        "updated_content_provided": bool(request.updated_content),
        "dry_run_plan_fingerprint": plan_fingerprint,
        "stale_ratio_recheck": {
            "status": "completed",
            "dispatch_id": recheck_dispatch_id,
            "executor_type": _RECHECK_EXECUTOR_TYPE,
        },
        "completed_at": completed_at,
    }


def _build_stale_ratio_recheck_context(
    *,
    governance_event_id: str,
    entry_id: str,
    request: KnowledgeStaleOwnerReviewCompleteRequest,
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot,
    plan_fingerprint: str,
    project_id: str | None = None,
) -> dict[str, Any]:
    resolved_project_id = project_id or "awoooi"
    return {
        "schema_version": "km_stale_owner_review_recheck_v1",
        "version": "v1",
        "project_id": resolved_project_id,
        "trigger_source": "km_stale_owner_review_complete",
        "triggered_metric": "knowledge_degradation",
        "metric_value": stale_ratio_snapshot.stale_ratio,
        "threshold": stale_ratio_snapshot.threshold,
        "suggested_action": "run_stale_ratio_recheck",
        "next_action": "run_stale_ratio_recheck",
        "decision_path": f"stale_ratio_recheck_after_{request.review_outcome}",
        "ownership": _stale_owner_review_ownership(),
        "workflow": {
            "work_item_id": (
                "governance:knowledge_degradation:"
                f"{governance_event_id}:stale_ratio_recheck:{entry_id}"
            ),
            "work_kind": "km_stale_ratio_recheck",
            "current_stage": "stale_ratio_recheck",
            "entry_id": entry_id,
            "project_id": resolved_project_id,
            "review_outcome": request.review_outcome,
            "steps": [
                "detected",
                "waiting_owner_review",
                "owner_updates_or_archives_km",
                "stale_ratio_recheck",
                "km_governance_close_or_continue",
            ],
            "stage_by_dispatch_status": {
                "pending": "stale_ratio_recheck",
                "dispatched": "stale_ratio_recheck",
                "executing": "stale_ratio_recheck",
                "succeeded": "km_governance_rechecked",
                "failed": "needs_manual_km_triage",
                "skipped": "needs_manual_km_triage",
                "cancelled": "cancelled",
            },
            "next_action": "km_governance_close_or_continue",
            "writes_km_without_approval": False,
            "writes_km": False,
            "dry_run_plan_fingerprint": plan_fingerprint,
            "stale_ratio_snapshot": stale_ratio_snapshot.model_dump(),
        },
        "worker_result": {
            "status": "stale_ratio_rechecked",
            "entry_id": entry_id,
            "review_outcome": request.review_outcome,
            "stale_ratio": stale_ratio_snapshot.stale_ratio,
            "threshold": stale_ratio_snapshot.threshold,
            "above_threshold": stale_ratio_snapshot.stale_ratio > stale_ratio_snapshot.threshold,
        },
    }


async def _load_current_km_stale_ratio_snapshot() -> KnowledgeReviewDraftStaleRatioSnapshot:
    async with get_db_context() as db:
        return await _compute_km_stale_ratio_snapshot(db)


async def _compute_km_stale_ratio_snapshot(
    db: AsyncSession,
) -> KnowledgeReviewDraftStaleRatioSnapshot:
    stale_cutoff = now_taipei() - timedelta(days=KM_STALE_DAYS)
    total_result = await db.execute(
        select(func.count()).select_from(KnowledgeEntryRecord).where(
            KnowledgeEntryRecord.status != EntryStatus.ARCHIVED,
        )
    )
    total = int(total_result.scalar() or 0)
    stale_result = await db.execute(
        select(func.count()).select_from(KnowledgeEntryRecord).where(
            KnowledgeEntryRecord.status != EntryStatus.ARCHIVED,
            KnowledgeEntryRecord.updated_at < stale_cutoff,
        )
    )
    stale = int(stale_result.scalar() or 0)
    ratio = round(stale / total, 3) if total > 0 else 0.0
    return KnowledgeReviewDraftStaleRatioSnapshot(
        stale_count=stale,
        total_count=total,
        stale_ratio=ratio,
        threshold=KM_STALE_RATIO,
        stale_days=KM_STALE_DAYS,
    )


def _completion_stage_for_outcome(
    outcome: Literal["refresh_with_evidence", "archive", "supersede"],
) -> str:
    return {
        "refresh_with_evidence": "km_writeback_after_approval",
        "archive": "km_archive_after_approval",
        "supersede": "km_supersede_after_approval",
    }[outcome]


def _normalize_review_outcome(value: Any) -> Literal[
    "refresh_with_evidence",
    "archive",
    "supersede",
]:
    if value in ("archive", "supersede", "refresh_with_evidence"):
        return value
    return "refresh_with_evidence"


def _normalize_optional_review_outcome(value: Any) -> Literal[
    "refresh_with_evidence",
    "archive",
    "supersede",
] | None:
    if value in ("archive", "supersede", "refresh_with_evidence"):
        return value
    return None


def _extract_recheck_dispatch_id(context: dict[str, Any]) -> str | None:
    recheck = context.get("stale_ratio_recheck")
    if isinstance(recheck, dict):
        value = recheck.get("dispatch_id")
        if isinstance(value, str) and value:
            return value
    return None


def _extract_complete_workflow_stage(context: dict[str, Any], dispatch_status: str) -> str:
    workflow = context.get("workflow")
    if isinstance(workflow, dict):
        stages = workflow.get("stage_by_dispatch_status")
        if isinstance(stages, dict):
            value = stages.get(dispatch_status)
            if isinstance(value, str) and value:
                return value
        value = workflow.get("current_stage")
        if isinstance(value, str) and value:
            return value
    return "km_writeback_after_approval"


def _snapshot_from_context(
    context: dict[str, Any],
) -> KnowledgeReviewDraftStaleRatioSnapshot | None:
    for source in (context, context.get("workflow")):
        if not isinstance(source, dict):
            continue
        raw = source.get("stale_ratio_snapshot")
        if isinstance(raw, dict):
            try:
                return KnowledgeReviewDraftStaleRatioSnapshot(**raw)
            except Exception:
                return None
    return None


def _extract_plan_fingerprint(context: dict[str, Any]) -> str | None:
    for source in (context, context.get("workflow")):
        if not isinstance(source, dict):
            continue
        value = source.get("dry_run_plan_fingerprint")
        if isinstance(value, str) and value:
            return value
    return None


def _build_complete_response(
    *,
    entry_id: str,
    project_id: str,
    status: Literal["dry_run", "completed", "already_completed"],
    review_outcome: Literal["refresh_with_evidence", "archive", "supersede"],
    governance_event_id: str,
    dispatch_id: str,
    workflow_stage: str,
    owner: str,
    owner_approved: bool,
    dry_run: bool,
    writes_km: bool,
    writes_governance_audit: bool,
    audit_dispatch_id: str | None = None,
    stale_ratio_recheck_dispatch_id: str | None = None,
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None,
    dry_run_plan_fingerprint: str | None = None,
) -> KnowledgeStaleOwnerReviewCompleteResponse:
    return KnowledgeStaleOwnerReviewCompleteResponse(
        entry_id=entry_id,
        project_id=project_id,
        status=status,
        review_outcome=review_outcome,
        governance_event_id=governance_event_id,
        dispatch_id=dispatch_id,
        audit_dispatch_id=audit_dispatch_id,
        stale_ratio_recheck_dispatch_id=stale_ratio_recheck_dispatch_id,
        workflow_stage=workflow_stage,
        owner=owner,
        owner_approved=owner_approved,
        dry_run=dry_run,
        writes_km=writes_km,
        writes_governance_audit=writes_governance_audit,
        stale_ratio_snapshot=stale_ratio_snapshot,
        dry_run_plan_fingerprint=dry_run_plan_fingerprint,
        generated_at=now_taipei(),
    )


def _build_stale_owner_review_event_details(
    *,
    entry_id: str,
    candidate: dict[str, Any],
    owner: str,
    owner_note: str | None,
) -> dict[str, Any]:
    return {
        "schema_version": "km_stale_owner_review_event_v1",
        "trigger_source": "stale_km_priority_queue",
        "next_action": "owner_review_stale_km_candidate",
        "impact": {
            "status": "waiting_owner_review",
            "metric": "stale_days",
            "value": candidate.get("stale_days"),
            "entry_id": entry_id,
            "priority_tier": candidate.get("priority_tier"),
            "priority_score": candidate.get("priority_score"),
            "stale_ratio_threshold": KM_STALE_RATIO,
            "stale_days_threshold": KM_STALE_DAYS,
        },
        "remediation": {
            "next_action": "owner_review_stale_km_candidate",
            "items": [
                "review_current_incident_sentry_signoz_playbook_evidence",
                "refresh_archive_or_supersede_after_owner_approval",
                "run_stale_ratio_recheck_after_writeback",
            ],
        },
        "ownership": _stale_owner_review_ownership(),
        "candidate": candidate,
        "owner": owner,
        "owner_note": owner_note,
    }


def _build_stale_owner_review_decision_context(
    *,
    governance_event_id: str,
    entry_id: str,
    candidate: dict[str, Any],
    owner: str,
    owner_note: str | None,
) -> dict[str, Any]:
    recommended_action = str(candidate.get("recommended_action") or "owner_review")
    return {
        "schema_version": "km_stale_owner_review_dispatch_v1",
        "version": "v1",
        "trigger_source": "stale_km_priority_queue",
        "triggered_metric": "knowledge_degradation",
        "metric_value": candidate.get("stale_days"),
        "threshold": KM_STALE_DAYS,
        "suggested_action": recommended_action,
        "next_action": "owner_review_stale_km_candidate",
        "decision_path": "pending_owner_review",
        "ownership": _stale_owner_review_ownership(),
        "entry_id": entry_id,
        "candidate": candidate,
        "workflow": {
            "work_item_id": (
                "governance:knowledge_degradation:"
                f"{governance_event_id}:km_stale_owner_review:{entry_id}"
            ),
            "work_kind": "km_stale_owner_review",
            "current_stage": "waiting_owner_review",
            "entry_id": entry_id,
            "project_id": candidate.get("project_id"),
            "priority_tier": candidate.get("priority_tier"),
            "priority_score": candidate.get("priority_score"),
            "recommended_action": recommended_action,
            "steps": [
                "detected",
                "prioritized_stale_candidate",
                "waiting_owner_review",
                "owner_updates_or_archives_km",
                "stale_ratio_recheck",
            ],
            "stage_by_dispatch_status": {
                "pending": "waiting_owner_review",
                "dispatched": "waiting_owner_review",
                "executing": "owner_review_in_progress",
                "succeeded": "km_candidate_reviewed",
                "failed": "needs_manual_km_triage",
                "skipped": "waiting_owner_review",
                "cancelled": "cancelled",
            },
            "next_action": "owner_review_stale_km_candidate",
            "needs_human_review": True,
            "writes_km_without_approval": False,
            "writes_km": False,
            "stale_ratio_recheck_after_writeback": True,
        },
        "worker_result": {
            "status": "queued_owner_review",
            "entry_id": entry_id,
            "recommended_action": recommended_action,
            "writes_km": False,
        },
        "owner": owner,
        "owner_note": owner_note,
    }


def _stale_owner_review_ownership() -> dict[str, Any]:
    return {
        "lead_agent": "Hermes",
        "support_agents": [
            "OpenClaw：補 Incident / 規則 / PlayBook 脈絡，不直接批量改寫 KM。",
            "ElephantAlpha：read-only 稽核 owner review 草稿與風險。",
        ],
        "human_owner": "KM owner / SRE owner",
    }


def _build_response(
    *,
    entry_id: str,
    project_id: str,
    status: Literal["dry_run", "queued", "already_queued"],
    recommended_action: Literal[
        "refresh_with_evidence",
        "owner_review",
        "archive_or_supersede",
    ],
    owner: str,
    writes_governance_audit: bool,
    owner_note: str | None = None,
    governance_event_id: str | None = None,
    dispatch_id: str | None = None,
) -> KnowledgeStaleOwnerReviewResponse:
    return KnowledgeStaleOwnerReviewResponse(
        entry_id=entry_id,
        project_id=project_id,
        status=status,
        governance_event_id=governance_event_id,
        dispatch_id=dispatch_id,
        workflow_stage="waiting_owner_review",
        recommended_action=recommended_action,
        owner=owner,
        owner_note=owner_note,
        writes_km=False,
        writes_governance_audit=writes_governance_audit,
        generated_at=now_taipei(),
    )


def _enum_value(value: Any) -> str:
    return str(value.value if hasattr(value, "value") else value)
