"""
Governance API Models — /governance 頁面 Pydantic Schemas
=========================================================
PR 1 後端 3 endpoint 的 request/response schema.

Endpoints:
  GET /api/v1/ai/governance/events   — ai_governance_events 查詢
  GET /api/v1/ai/governance/queue    — governance_remediation_dispatch 隊列（Track D 依賴表）
  GET /api/v1/ai/governance/summary  — 30d SLO 違反時序統計

設計原則:
- Pydantic V2，對齊 models/ 目錄
- Severity 映射邏輯集中於此，Router / Service 直接用
- 禁止硬編碼 IP 或內網位址

2026-05-02 ogt + Claude Sonnet 4.6 Asia/Taipei
"""

from __future__ import annotations

from datetime import datetime
from typing import Literal

from pydantic import BaseModel, Field

# =============================================================================
# Severity 映射
# =============================================================================

# critical: slo_violation / conservative_mode / governance_slo_data_gap
# warning:  trust_drift / kb_stale / knowledge_degradation / execution_blast_radius
# info:     其他（含 replay_degraded / self_demotion / llm_hallucination 等）

_CRITICAL_TYPES: frozenset[str] = frozenset({
    "slo_violation",
    "conservative_mode",
    "governance_slo_data_gap",
})

_WARNING_TYPES: frozenset[str] = frozenset({
    "trust_drift",
    "kb_stale",
    "knowledge_degradation",
    "execution_blast_radius",
})


def map_severity(event_type: str) -> Literal["critical", "warning", "info"]:
    """將 event_type 映射為 severity 等級."""
    if event_type in _CRITICAL_TYPES:
        return "critical"
    if event_type in _WARNING_TYPES:
        return "warning"
    return "info"


# =============================================================================
# Endpoint 1: events
# =============================================================================

class GovernanceEvent(BaseModel):
    id: str
    event_type: str
    severity: Literal["critical", "warning", "info"]
    triggered_at: datetime
    resolved: bool
    resolved_at: datetime | None = None
    impact: str = Field(description="≤80 字摘要，從 details 抽取")
    details: dict
    remediation: str | None = None
    dispatch_ids: list[str] = Field(default_factory=list)


class GovernanceEventsResponse(BaseModel):
    items: list[GovernanceEvent]
    total: int
    page: int
    size: int


# =============================================================================
# Endpoint 2: queue
# =============================================================================

class DispatchItem(BaseModel):
    id: str
    governance_event_id: str
    event_type: str
    dispatch_status: str
    executor_type: str | None = None
    proposed_action: str = Field(description="≤120 字動作摘要")
    playbook_id: str | None = None
    playbook_trust: float | None = Field(default=None, ge=0.0, le=1.0)
    created_at: datetime
    dispatched_at: datetime | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    operator_note: str | None = None
    decision_path: str | None = None
    workflow_stage: str | None = None
    workflow_steps: list[str] = Field(default_factory=list)
    next_action: str | None = None
    lead_agent: str | None = None
    support_agents: list[str] = Field(default_factory=list)
    human_owner: str | None = None
    kb_draft_entry_id: str | None = None
    worker_status: str | None = None
    dry_run_plan_fingerprint: str | None = None
    archived_count: int | None = None
    stale_ratio_snapshot: dict | None = None


class GovernanceQueueResponse(BaseModel):
    items: list[DispatchItem]
    total: int
    page: int
    size: int
    table_pending: bool = Field(
        default=False,
        description="dispatch 表尚未建立時為 True",
    )


# =============================================================================
# Endpoint 2B: KM review draft dedupe
# =============================================================================

class KnowledgeReviewDraftDedupeGroup(BaseModel):
    governance_event_id: str
    canonical_entry_id: str
    canonical_title: str
    canonical_updated_at: datetime | None = None
    preferred_source: Literal["dispatch_context", "latest_review_draft"]
    duplicate_entry_ids: list[str] = Field(default_factory=list)
    duplicate_count: int
    total_entries: int
    suggested_action: str
    owner_action: str
    writes_on_read: bool = False
    can_archive_without_owner_approval: bool = False
    archive_history: list[DispatchItem] = Field(default_factory=list)


class KnowledgeReviewDraftDedupeResponse(BaseModel):
    schema_version: str = "km_review_draft_dedupe_v1"
    total_review_drafts: int
    event_group_total: int
    duplicate_draft_total: int
    groups: list[KnowledgeReviewDraftDedupeGroup]
    generated_at: datetime


class KnowledgeReviewDraftArchiveRequest(BaseModel):
    canonical_entry_id: str = Field(min_length=1, max_length=120)
    duplicate_entry_ids: list[str] = Field(min_length=1, max_length=100)
    owner: str = Field(default="operator_console", min_length=1, max_length=100)
    owner_approved: bool = False
    dry_run: bool = False
    dry_run_plan_fingerprint: str | None = Field(
        default=None,
        max_length=80,
        description="Dry-run response fingerprint that must be echoed before a write.",
    )


class KnowledgeReviewDraftStaleRatioSnapshot(BaseModel):
    stale_count: int
    total_count: int
    stale_ratio: float
    threshold: float
    stale_days: int


class KnowledgeReviewDraftArchiveResponse(BaseModel):
    schema_version: str = "km_review_draft_archive_v1"
    governance_event_id: str
    canonical_entry_id: str
    requested_duplicate_entry_ids: list[str]
    archived_entry_ids: list[str] = Field(default_factory=list)
    skipped_entry_ids: list[str] = Field(default_factory=list)
    would_archive_entry_ids: list[str] = Field(default_factory=list)
    status: Literal["dry_run", "archived", "noop_already_archived"]
    owner: str
    owner_approved: bool
    dry_run: bool
    writes_km: bool
    writes_governance_audit: bool
    audit_dispatch_id: str | None = None
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
    stale_ratio_recheck_status: Literal[
        "dry_run",
        "completed",
        "already_active",
        "not_requested",
    ] = "not_requested"
    stale_ratio_recheck_dispatch_id: str | None = None
    dry_run_plan_fingerprint: str | None = None
    next_action: str = "stale_ratio_recheck"
    generated_at: datetime


# =============================================================================
# Endpoint 2C: KM stale candidates
# =============================================================================

class KnowledgeStaleCandidate(BaseModel):
    entry_id: str
    project_id: str
    title: str
    entry_type: str
    category: str | None = None
    status: str
    source: str | None = None
    updated_at: datetime | None = None
    stale_days: int
    view_count: int
    priority_score: int
    priority_tier: Literal["P0", "P1", "P2"]
    recommended_action: Literal[
        "refresh_with_evidence",
        "owner_review",
        "archive_or_supersede",
    ]
    reasons: list[str] = Field(default_factory=list)
    correlation_sources: list[str] = Field(default_factory=list)
    related_incident_id: str | None = None
    related_playbook_id: str | None = None
    related_approval_id: str | None = None
    tags: list[str] = Field(default_factory=list)
    owner_review_dispatch_id: str | None = None
    owner_review_status: str | None = None
    owner_review_stage: str | None = None
    owner_review_next_action: str | None = None


class KnowledgeStaleCandidatesResponse(BaseModel):
    schema_version: str = "km_stale_candidates_v1"
    project_id: str
    total_stale: int
    returned: int
    threshold_days: int
    writes_on_read: bool = False
    manual_review_required: bool = True
    items: list[KnowledgeStaleCandidate]
    generated_at: datetime


class KnowledgeStaleOwnerReviewRequest(BaseModel):
    owner: str = Field(default="operator_console", min_length=1, max_length=100)
    owner_note: str | None = Field(default=None, max_length=240)
    dry_run: bool = False


class KnowledgeStaleOwnerReviewResponse(BaseModel):
    schema_version: str = "km_stale_owner_review_v1"
    entry_id: str
    project_id: str
    status: Literal["dry_run", "queued", "already_queued"]
    governance_event_id: str | None = None
    dispatch_id: str | None = None
    workflow_stage: str
    recommended_action: Literal[
        "refresh_with_evidence",
        "owner_review",
        "archive_or_supersede",
    ]
    owner: str
    owner_note: str | None = None
    writes_km: bool = False
    writes_governance_audit: bool
    next_action: str = "owner_review_stale_km_candidate"
    generated_at: datetime


class KnowledgeStaleOwnerReviewBatchQueueRequest(BaseModel):
    project_id: str = Field(default="awoooi", min_length=1, max_length=64)
    priority_tiers: list[Literal["P0", "P1", "P2"]] = Field(
        default_factory=lambda: ["P0", "P1"],
        min_length=1,
        max_length=3,
    )
    limit: int = Field(default=10, ge=1, le=50)
    owner: str = Field(default="operator_console", min_length=1, max_length=100)
    owner_note: str | None = Field(default=None, max_length=240)
    dry_run: bool = False
    dry_run_plan_fingerprint: str | None = Field(
        default=None,
        max_length=80,
        description="Dry-run response fingerprint that must be echoed before queueing a batch.",
    )


class KnowledgeStaleOwnerReviewBatchItem(BaseModel):
    entry_id: str
    title: str
    priority_tier: Literal["P0", "P1", "P2"]
    recommended_action: Literal[
        "refresh_with_evidence",
        "owner_review",
        "archive_or_supersede",
    ]
    status: Literal["would_queue", "queued", "already_queued", "skipped"]
    reason: str | None = None
    governance_event_id: str | None = None
    dispatch_id: str | None = None
    workflow_stage: str


class KnowledgeStaleOwnerReviewBatchQueueResponse(BaseModel):
    schema_version: str = "km_stale_owner_review_batch_v1"
    project_id: str
    status: Literal["dry_run", "queued", "noop_already_queued"]
    owner: str
    owner_note: str | None = None
    dry_run: bool
    priority_tiers: list[str]
    requested_limit: int
    candidate_count: int
    queued_count: int
    already_queued_count: int
    skipped_count: int
    batch_governance_event_id: str | None = None
    batch_dispatch_id: str | None = None
    workflow_stage: str
    writes_km: bool = False
    writes_governance_audit: bool
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
    dry_run_plan_fingerprint: str | None = None
    items: list[KnowledgeStaleOwnerReviewBatchItem] = Field(default_factory=list)
    next_action: str = "owner_review_stale_km_batch"
    generated_at: datetime


class KnowledgeStaleOwnerReviewInboxItem(BaseModel):
    dispatch_id: str
    governance_event_id: str
    entry_id: str
    project_id: str
    title: str
    dispatch_status: str
    workflow_stage: str
    next_action: str | None = None
    owner: str | None = None
    owner_note: str | None = None
    batch_governance_event_id: str | None = None
    batch_dispatch_id: str | None = None
    priority_tier: Literal["P0", "P1", "P2"]
    priority_score: int
    recommended_action: Literal[
        "refresh_with_evidence",
        "owner_review",
        "archive_or_supersede",
    ]
    stale_days: int
    view_count: int
    correlation_sources: list[str] = Field(default_factory=list)
    reasons: list[str] = Field(default_factory=list)
    related_incident_id: str | None = None
    related_playbook_id: str | None = None
    related_approval_id: str | None = None
    dry_run_plan_fingerprint: str | None = None
    queued_at: datetime | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None


class KnowledgeStaleOwnerReviewInboxResponse(BaseModel):
    schema_version: str = "km_stale_owner_review_inbox_v1"
    project_id: str
    dispatch_status: str
    total: int
    returned: int
    writes_on_read: bool = False
    manual_review_required: bool = True
    items: list[KnowledgeStaleOwnerReviewInboxItem] = Field(default_factory=list)
    generated_at: datetime


class KnowledgeStaleOwnerReviewBurnDownItem(BaseModel):
    completion_dispatch_id: str
    governance_event_id: str
    source_dispatch_id: str | None = None
    recheck_dispatch_id: str | None = None
    entry_id: str | None = None
    project_id: str
    dispatch_status: str
    workflow_stage: str
    review_outcome: Literal[
        "refresh_with_evidence",
        "archive",
        "supersede",
    ] | None = None
    owner: str | None = None
    completed_at: datetime | None = None
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
    stale_count_delta: int | None = None
    stale_ratio_delta: float | None = None
    above_threshold: bool | None = None


class KnowledgeStaleOwnerReviewBurnDownResponse(BaseModel):
    schema_version: str = "km_stale_owner_review_burndown_v1"
    project_id: str
    burn_down_status: Literal["above_threshold", "at_or_below_threshold", "no_data"]
    current_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
    entries_to_threshold: int
    pending_owner_reviews: int
    completed_owner_reviews: int
    completion_audit_total: int
    stale_ratio_recheck_total: int
    latest_stale_count_delta: int | None = None
    latest_stale_ratio_delta: float | None = None
    writes_on_read: bool = False
    manual_review_required: bool = True
    returned: int
    items: list[KnowledgeStaleOwnerReviewBurnDownItem] = Field(default_factory=list)
    generated_at: datetime


class KnowledgeStaleOwnerReviewCompletionQueueItem(BaseModel):
    dispatch_id: str
    governance_event_id: str
    entry_id: str
    project_id: str
    title: str
    dispatch_status: str
    workflow_stage: str
    readiness: Literal["ready", "blocked", "completed", "failed"]
    recommended_completion_outcome: Literal[
        "refresh_with_evidence",
        "archive",
        "supersede",
    ]
    next_action: str
    blockers: list[str] = Field(default_factory=list)
    required_owner_fields: list[str] = Field(default_factory=list)
    can_preview: bool
    can_confirm_after_preview: bool
    writes_km_on_confirm: bool
    owner: str | None = None
    owner_note: str | None = None
    batch_governance_event_id: str | None = None
    batch_dispatch_id: str | None = None
    priority_tier: Literal["P0", "P1", "P2"]
    priority_score: int
    recommended_action: Literal[
        "refresh_with_evidence",
        "owner_review",
        "archive_or_supersede",
    ]
    stale_days: int
    view_count: int
    correlation_sources: list[str] = Field(default_factory=list)
    reasons: list[str] = Field(default_factory=list)
    related_incident_id: str | None = None
    related_playbook_id: str | None = None
    related_approval_id: str | None = None
    dry_run_plan_fingerprint: str | None = None
    queued_at: datetime | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None


class KnowledgeStaleOwnerReviewCompletionQueueResponse(BaseModel):
    schema_version: str = "km_stale_owner_review_completion_queue_v1"
    project_id: str
    status_bucket: Literal["all", "ready", "blocked", "completed", "failed", "pending"]
    priority_tiers: list[str] = Field(default_factory=list)
    recommended_completion_outcome: Literal[
        "all",
        "refresh_with_evidence",
        "archive",
        "supersede",
    ] = "all"
    batch_governance_event_id: str | None = None
    can_preview: bool | None = None
    total: int
    returned: int
    pending_count: int
    ready_count: int
    blocked_count: int
    completed_count: int
    failed_count: int
    writes_on_read: bool = False
    manual_review_required: bool = True
    batch_writes_allowed: bool = False
    items: list[KnowledgeStaleOwnerReviewCompletionQueueItem] = Field(default_factory=list)
    generated_at: datetime


class KnowledgeStaleOwnerReviewCompletionBatchPreviewRequest(BaseModel):
    project_id: str = Field(default="awoooi", min_length=1, max_length=64)
    status_bucket: Literal["all", "ready", "blocked", "completed", "failed", "pending"] = "ready"
    priority_tiers: list[Literal["P0", "P1", "P2"]] = Field(
        default_factory=lambda: ["P0", "P1", "P2"],
        min_length=1,
        max_length=3,
    )
    recommended_completion_outcome: Literal[
        "all",
        "refresh_with_evidence",
        "archive",
        "supersede",
    ] = "all"
    batch_governance_event_id: str | None = Field(default=None, max_length=120)
    limit: int = Field(default=10, ge=1, le=30)
    owner: str = Field(default="operator_console", min_length=1, max_length=100)
    owner_note: str | None = Field(default=None, max_length=240)


class KnowledgeStaleOwnerReviewCompletionBatchPreviewResponse(BaseModel):
    schema_version: str = "km_stale_owner_review_completion_batch_preview_v1"
    project_id: str
    status: Literal["dry_run"] = "dry_run"
    owner: str
    owner_note: str | None = None
    status_bucket: Literal["all", "ready", "blocked", "completed", "failed", "pending"]
    priority_tiers: list[str]
    recommended_completion_outcome: Literal[
        "all",
        "refresh_with_evidence",
        "archive",
        "supersede",
    ]
    batch_governance_event_id: str | None = None
    requested_limit: int
    candidate_count: int
    previewable_count: int
    blocked_count: int
    completed_count: int
    failed_count: int
    writes_km: bool = False
    writes_governance_audit: bool = False
    batch_writes_allowed: bool = False
    manual_review_required: bool = True
    dry_run_plan_fingerprint: str
    next_action: str = "preview_each_ready_item_then_confirm_single_item"
    items: list[KnowledgeStaleOwnerReviewCompletionQueueItem] = Field(default_factory=list)
    generated_at: datetime


class KnowledgeStaleOwnerReviewCompleteRequest(BaseModel):
    dispatch_id: str | None = Field(
        default=None,
        max_length=120,
        description="Owner-review dispatch id. Optional when the backend can resolve the active item by entry id.",
    )
    owner: str = Field(default="operator_console", min_length=1, max_length=100)
    owner_approved: bool = False
    dry_run: bool = False
    review_outcome: Literal[
        "refresh_with_evidence",
        "archive",
        "supersede",
    ]
    owner_note: str | None = Field(default=None, max_length=500)
    updated_title: str | None = Field(default=None, min_length=1, max_length=255)
    updated_content: str | None = Field(default=None, min_length=1)
    superseded_by_entry_id: str | None = Field(default=None, max_length=120)
    dry_run_plan_fingerprint: str | None = Field(
        default=None,
        max_length=80,
        description="Dry-run response fingerprint that must be echoed before a write.",
    )


class KnowledgeStaleOwnerReviewCompleteResponse(BaseModel):
    schema_version: str = "km_stale_owner_review_complete_v1"
    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
    audit_dispatch_id: str | None = None
    stale_ratio_recheck_dispatch_id: str | None = None
    workflow_stage: str
    owner: str
    owner_approved: bool
    dry_run: bool
    writes_km: bool
    writes_governance_audit: bool
    stale_ratio_snapshot: KnowledgeReviewDraftStaleRatioSnapshot | None = None
    dry_run_plan_fingerprint: str | None = None
    next_action: str = "stale_ratio_recheck"
    generated_at: datetime


# =============================================================================
# Endpoint 3: summary
# =============================================================================

class DailyCount(BaseModel):
    date: str = Field(description="YYYY-MM-DD")
    total: int
    by_type: dict[str, int] = Field(description="{event_type: count}")


class GovernanceSummaryResponse(BaseModel):
    compliance_rate: float = Field(description="0.0-1.0，1 - unresolved/total")
    total_events: int
    unresolved_count: int
    daily_counts: list[DailyCount]
