"""
AwoooP Operator Console — Runs List & Approval API
====================================================
  GET  /runs/list     — 列出 runs（可 filter）
  GET  /approvals     — 列出待審核 runs（state=waiting_approval）
  POST /approvals/{run_id}/decide — 核准或拒絕 run
ADR-106（AwoooP Agent Platform），ADR-114（Run State Machine），ADR-116（Gate 5 Approval）
2026-05-05 ogt + Claude Sonnet 4.6
"""

from __future__ import annotations

from datetime import datetime
from decimal import Decimal
from typing import Any, Literal
from uuid import UUID

from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field

from src.core.awooop_operator_auth import (
    AwoooPOperatorPrincipal,
    verify_awooop_operator,
)
from src.services.platform_operator_service import (
    decide_approval as decide_approval_svc,
)
from src.services.platform_operator_service import (
    get_ai_route_status as get_ai_route_status_svc,
)
from src.services.platform_operator_service import (
    get_awooop_status_chain as get_awooop_status_chain_svc,
)
from src.services.platform_operator_service import (
    get_run_detail as get_run_detail_svc,
)
from src.services.platform_operator_service import (
    list_cicd_events as list_cicd_events_svc,
)
from src.services.platform_operator_service import (
    list_approvals as list_approvals_svc,
)
from src.services.platform_operator_service import (
    list_callback_replies as list_callback_replies_svc,
)
from src.services.platform_operator_service import (
    list_runs as list_runs_svc,
)

router = APIRouter()

_DEFAULT_PER_PAGE = 50
_MAX_PER_PAGE = 200


class RunItem(BaseModel):
    run_id: UUID
    project_id: str
    agent_id: str
    state: str
    is_shadow: bool
    cost_usd: Decimal
    step_count: int
    created_at: datetime
    timeout_at: datetime | None
    remediation_summary: dict[str, Any] | None = None
    callback_reply_summary: dict[str, Any] | None = None


class ListRunsResponse(BaseModel):
    runs: list[RunItem]
    total: int
    page: int
    per_page: int


class OperatorSummaryCacheInfo(BaseModel):
    schema_version: str = "operator_summary_cache_v1"
    status: str
    source: str
    ttl_seconds: int
    age_seconds: float = 0.0
    stored_at: datetime
    expires_at: datetime


class CallbackReplyItem(BaseModel):
    message_id: UUID
    run_id: UUID
    project_id: str
    status: str
    needs_human: bool
    action: str | None = None
    incident_id: str | None = None
    event_at: datetime | None = None
    channel_type: str
    message_type: str
    send_status: str
    send_error: str | None = None
    provider_message_id: str | None = None
    triggered_by_state: str | None = None
    content_preview: str | None = None
    run_state: str | None = None
    agent_id: str | None = None
    run_created_at: datetime | None = None
    callback_reply: dict[str, Any]
    awooop_status_chain: dict[str, Any] | None = None
    persisted_awooop_status_chain: dict[str, Any] | None = None
    km_stale_completion_summary: dict[str, Any] | None = None
    persisted_km_stale_completion_summary: dict[str, Any] | None = None
    evidence_capture_status: dict[str, Any] | None = None
    run_detail_href: str | None = None


class OutboundReplyMarkupGapPrefix(BaseModel):
    prefix: str
    total: int
    recent_24h_total: int = 0
    first_sent_at: datetime | None = None
    last_sent_at: datetime | None = None


class CallbackReplyAuditSummary(BaseModel):
    schema_version: str
    project_id: str
    outbound_total: int
    outbound_source_envelope_total: int
    outbound_source_refs_total: int
    outbound_trace_ref_total: int = 0
    outbound_incident_ref_total: int
    outbound_reply_markup_total: int = 0
    outbound_reply_markup_missing_incident_ref_total: int = 0
    outbound_reply_markup_missing_incident_ref_recent_1h_total: int = 0
    outbound_reply_markup_missing_incident_ref_recent_24h_total: int = 0
    outbound_reply_markup_missing_incident_ref_latest_sent_at: datetime | None = None
    outbound_reply_markup_missing_trace_ref_total: int = 0
    outbound_reply_markup_missing_trace_ref_recent_1h_total: int = 0
    outbound_reply_markup_missing_trace_ref_recent_24h_total: int = 0
    outbound_reply_markup_missing_trace_ref_latest_sent_at: datetime | None = None
    outbound_reply_markup_trace_ref_gap_status: str = "clean"
    outbound_reply_markup_trace_ref_gap_next_action: str = "none"
    outbound_reply_markup_trace_ref_after_gap_total: int = 0
    outbound_reply_markup_trace_ref_after_gap_first_sent_at: datetime | None = None
    outbound_reply_markup_trace_ref_after_gap_latest_sent_at: datetime | None = None
    outbound_reply_markup_trace_ref_gap_recovery_status: str = "not_needed"
    outbound_reply_markup_missing_incident_ref_top_prefixes: list[
        OutboundReplyMarkupGapPrefix
    ] = Field(default_factory=list)
    outbound_reply_markup_missing_trace_ref_top_prefixes: list[
        OutboundReplyMarkupGapPrefix
    ] = Field(default_factory=list)
    outbound_failed_total: int
    callback_total: int
    callback_sent_total: int
    callback_fallback_total: int
    callback_rescue_total: int
    callback_failed_total: int
    callback_detail_total: int
    callback_history_total: int
    callback_snapshot_captured_total: int
    callback_snapshot_partial_total: int
    callback_snapshot_missing_total: int
    callback_incident_total: int
    inbound_callback_total: int = 0
    inbound_callback_recent_24h_total: int = 0
    inbound_callback_latest_at: datetime | None = None
    inbound_callback_mirror_status: str = "no_callback_observed"
    inbound_callback_next_action: str = "press_any_telegram_callback_after_rollout"
    snapshot_status: str
    next_action: str
    latest_outbound_at: datetime | None = None
    latest_callback_at: datetime | None = None


class ListCallbackRepliesResponse(BaseModel):
    items: list[CallbackReplyItem]
    total: int
    page: int
    per_page: int
    summary: CallbackReplyAuditSummary | None = None
    cache: OperatorSummaryCacheInfo | None = None


class CicdEventItem(BaseModel):
    id: str
    project_id: str
    alertname: str
    stage: str | None = None
    status: str | None = None
    severity: str | None = None
    commit_sha: str | None = None
    triggered_by: str | None = None
    duration_seconds: int = 0
    summary: str | None = None
    description: str | None = None
    workflow_url: str | None = None
    alert_id: str | None = None
    source: str | None = None
    action_detail: str | None = None
    needs_attention: bool = False
    created_at: datetime


class ListCicdEventsResponse(BaseModel):
    items: list[CicdEventItem]
    total: int
    limit: int


class AiRouteStatusResponse(BaseModel):
    schema_version: str
    workload_type: str
    policy_order: list[dict[str, Any]]
    selected_provider: str | None = None
    selected_url: str | None = None
    selected_model: str | None = None
    fallback_chain: list[dict[str, Any]]
    route_reason: str
    route_source: str
    route_error: str | None = None
    health: dict[str, dict[str, Any]]
    lane_mode: str | None = None
    active_lane: dict[str, Any] | None = None
    skipped_lanes: list[dict[str, Any]] = Field(default_factory=list)
    operator_action: dict[str, Any] | None = None
    repair_evidence: dict[str, Any] | None = None
    checked_at: datetime


class ApprovalItem(BaseModel):
    run_id: UUID
    project_id: str
    agent_id: str
    trigger_type: str | None = None
    trigger_ref: str | None = None
    is_shadow: bool | None = None
    created_at: datetime
    timeout_at: datetime | None
    remediation_summary: dict[str, Any] | None = None
    awooop_status_chain: dict[str, Any] | None = None


class ListApprovalsResponse(BaseModel):
    items: list[ApprovalItem]
    total: int


class DecideApprovalRequest(BaseModel):
    project_id: str = Field(..., description="租戶 ID")
    decision: Literal["approve", "reject"] = Field(..., description="核准或拒絕")
    approver_id: str | None = Field(
        default=None,
        description="Deprecated. Ignored; approver comes from trusted operator headers.",
    )
    reason: str | None = Field(None, description="決策原因（可選）")


class DecideApprovalResponse(BaseModel):
    run_id: str
    decision: str
    new_state: str
    approval_token_jti: str | None


@router.get(
    "/runs/list",
    response_model=ListRunsResponse,
    summary="列出 Runs",
    description=(
        "返回 awooop_run_state 記錄，支援 project_id / state / remediation_status / "
        "callback_reply_status / incident_id filter 與分頁。\n\n"
        "- 按 created_at DESC 排序\n"
        "- 注意：此路徑為 /runs/list 以避免與 runs.py 的 /runs/{run_id} 衝突"
    ),
)
async def list_runs(
    project_id: str | None = Query(None, description="租戶 ID（可選）"),
    state: str | None = Query(None, description="Run 狀態 filter（可選）"),
    remediation_status: str | None = Query(
        None,
        description="AI 證據狀態 filter（no_evidence/mcp_observed/read_only_dry_run/write_observed/blocked/observed）",
    ),
    callback_reply_status: str | None = Query(
        None,
        description="Telegram callback reply 狀態 filter（no_callback/sent/fallback_sent/rescue_sent/failed/observed）",
    ),
    incident_id: str | None = Query(None, description="關聯 Incident ID filter（可選）"),
    page: int = Query(1, ge=1, description="頁碼，從 1 開始"),
    per_page: int = Query(_DEFAULT_PER_PAGE, ge=1, le=_MAX_PER_PAGE, description="每頁筆數"),
) -> dict[str, Any]:
    return await list_runs_svc(
        project_id=project_id,
        state=state,
        remediation_status=remediation_status,
        callback_reply_status=callback_reply_status,
        incident_id=incident_id,
        page=page,
        per_page=per_page,
    )


@router.get(
    "/runs/callback-replies",
    response_model=ListCallbackRepliesResponse,
    summary="列出 Telegram Callback Reply Evidence",
    description=(
        "從 AwoooP outbound mirror 查詢 Telegram 詳情 / 歷史 callback reply 的"
        "送達、fallback、救援與失敗證據；只讀，不修改 incident、run 或 Telegram 狀態。"
    ),
)
async def list_callback_replies(
    project_id: str | None = Query(None, description="租戶 ID（可選）"),
    callback_reply_status: str | None = Query(
        None,
        description="Telegram callback reply 狀態 filter（sent/fallback_sent/rescue_sent/failed/observed/no_callback）",
    ),
    action: str | None = Query(None, description="Callback action filter（例如 detail/history）"),
    incident_id: str | None = Query(None, description="關聯 Incident ID filter（可選）"),
    page: int = Query(1, ge=1, description="頁碼，從 1 開始"),
    per_page: int = Query(20, ge=1, le=_MAX_PER_PAGE, description="每頁筆數"),
    refresh: bool = Query(False, description="略過短 TTL 快取並重新聚合"),
) -> dict[str, Any]:
    return await list_callback_replies_svc(
        project_id=project_id,
        callback_reply_status=callback_reply_status,
        action=action,
        incident_id=incident_id,
        page=page,
        per_page=per_page,
        refresh=refresh,
    )


@router.get(
    "/cicd/events",
    response_model=ListCicdEventsResponse,
    summary="列出 CI/CD evidence events",
    description=(
        "從 alert_operation_log 讀取 CI/CD notification evidence，供 AwoooP "
        "Deployments / Run Console 顯示 rollout-risk、success、failed 等階段狀態。"
    ),
)
async def list_cicd_events(
    project_id: str | None = Query(None, description="租戶 ID（目前支援 awoooi）"),
    stage: str | None = Query(None, description="CI/CD stage filter（可選）"),
    status: str | None = Query(None, description="CI/CD status filter（running/success/failed/pending）"),
    limit: int = Query(12, ge=1, le=50, description="最多返回筆數"),
) -> dict[str, Any]:
    return await list_cicd_events_svc(
        project_id=project_id,
        stage=stage,
        status_filter=status,
        limit=limit,
    )


@router.get(
    "/ai-route-status",
    response_model=AiRouteStatusResponse,
    summary="查詢 AI Provider 路由狀態",
    description=(
        "回傳目前 Ollama/Gemini 路由策略、即時 primary、fallback chain 與健康狀態；"
        "只讀，不觸發推理或自動修復。"
    ),
)
async def get_ai_route_status(
    workload_type: str | None = Query(
        "deep_rca",
        description="工作負載類型，例如 deep_rca/hermes/interactive/embedding/rag/code_review/image_analysis",
    ),
) -> dict[str, Any]:
    return await get_ai_route_status_svc(workload_type=workload_type)


@router.get(
    "/runs/{run_id}/detail",
    summary="查詢 Run 詳細時間線",
    description=(
        "返回單一 Run 的主狀態、Step Journal、MCP Gateway audit、"
        "入站 Channel Event 與出站訊息，供 Operator Console 顯示完整處置脈絡。"
    ),
)
async def get_run_detail(
    run_id: str,
    project_id: str | None = Query(None, description="租戶 ID（可選）"),
) -> dict[str, Any]:
    return await get_run_detail_svc(run_id=run_id, project_id=project_id)


@router.get(
    "/status-chain",
    summary="查詢 AwoooP 狀態鏈",
    description=(
        "依 incident_id 查詢 truth-chain + ADR-100 history 合併後的只讀狀態鏈，"
        "供 Work Items、Approvals、Monitoring 等操作頁面共用。"
    ),
)
async def get_awooop_status_chain(
    project_id: str | None = Query(None, description="租戶 ID（可選）"),
    incident_id: list[str] | None = Query(
        None,
        description="Incident ID，可重複傳入以合併同一工作項的多個事件",
    ),
) -> dict[str, Any]:
    return await get_awooop_status_chain_svc(
        project_id=project_id,
        incident_ids=incident_id or [],
    )


@router.get(
    "/approvals",
    response_model=ListApprovalsResponse,
    summary="列出待審核 Runs",
    description=(
        "返回 state=waiting_approval 的 runs，即需要人工審核的任務清單。\n\n"
        "ADR-116 Gate 5：人工審核關卡"
    ),
)
async def list_approvals(
    project_id: str | None = Query(None, description="租戶 ID（可選）"),
    run_id: str | None = Query(None, description="Run ID（可選，M8 詳情頁查單筆）"),
    remediation_status: str | None = Query(
        None,
        description="AI 證據狀態 filter（no_evidence/mcp_observed/read_only_dry_run/write_observed/blocked/observed）",
    ),
) -> dict[str, Any]:
    return await list_approvals_svc(
        project_id=project_id,
        run_id=run_id,
        remediation_status=remediation_status,
    )


@router.post(
    "/approvals/{run_id}/decide",
    response_model=DecideApprovalResponse,
    summary="核准或拒絕 Run",
    description=(
        "對 waiting_approval 狀態的 run 做出審核決定。\n\n"
        "- approve：發行 approval token → record_approval → run 轉為 running\n"
        "- reject：直接 transition → cancelled\n\n"
        "ADR-116 Gate 5：Operator Console 人工審核"
    ),
)
async def decide_approval(
    run_id: str,
    body: DecideApprovalRequest,
    operator: AwoooPOperatorPrincipal = Depends(verify_awooop_operator),
) -> dict[str, Any]:
    return await decide_approval_svc(
        run_id=run_id,
        project_id=body.project_id,
        decision=body.decision,
        approver_id=operator.operator_id,
        reason=body.reason,
    )
