"""
P2-408 AI Agent low / medium risk whitelist snapshot.

Loads the latest committed whitelist candidate snapshot that turns P2-407
no-write report analysis into reviewable low / medium risk candidates. This
module intentionally does not run an auto worker, send Telegram, write a
Gateway queue, write delivery receipts, read secrets, call paid APIs, mutate
hosts, run kubectl, or write production state.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from src.services.snapshot_paths import default_evaluations_dir

_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__))
_SNAPSHOT_PATTERN = "ai_agent_low_medium_risk_whitelist_*.json"
_SCHEMA_VERSION = "ai_agent_low_medium_risk_whitelist_v1"
_RUNTIME_AUTHORITY = "low_medium_risk_whitelist_no_live_execution_committed_snapshot"
_EXPECTED_CURRENT_TASK = "P2-408"
_EXPECTED_NEXT_TASK = "P2-409"
_EXPECTED_CANONICAL_ROOM = "AwoooI SRE 戰情室"
_EXPECTED_CANONICAL_ROOM_ENV = "SRE_GROUP_CHAT_ID"
_EXPECTED_SOURCE_SCHEMAS = {
    "ai_agent_report_no_write_analysis_runtime_v1",
    "ai_agent_operation_permission_model_v1",
    "ai_agent_candidate_operation_dry_run_evidence_v1",
    "ai_agent_report_automation_review_v1",
    "dependency_supply_chain_drift_monitor_v1",
}
_TRUE_TRUTH_FLAGS = {
    "p2_407_no_write_analysis_loaded",
    "operation_permission_model_loaded",
    "candidate_dry_run_evidence_loaded",
    "report_policy_review_loaded",
    "dependency_drift_loaded",
    "low_risk_candidates_ready",
    "medium_risk_candidates_ready",
    "dry_run_verifier_required",
    "rollback_proof_required",
    "audit_reason_required",
    "high_risk_redirect_ready",
}
_FALSE_TRUTH_FLAGS = {
    "auto_worker_enabled",
    "low_risk_live_execution_enabled",
    "medium_risk_live_execution_enabled",
    "gateway_queue_write_enabled",
    "telegram_send_enabled",
    "bot_api_call_enabled",
    "receipt_production_write_enabled",
    "production_write_enabled",
    "secret_read_enabled",
    "paid_api_call_enabled",
    "host_write_enabled",
    "kubectl_action_enabled",
    "destructive_operation_enabled",
    "openclaw_replacement_allowed",
}
_ZERO_TRUTH_COUNTS = {
    "auto_worker_run_count_24h",
    "low_risk_execution_count_24h",
    "medium_risk_execution_count_24h",
    "gateway_queue_write_count_24h",
    "telegram_send_count_24h",
    "bot_api_call_count_24h",
    "receipt_production_write_count_24h",
    "production_write_count_24h",
    "secret_read_count_24h",
    "paid_api_call_count_24h",
    "host_write_count_24h",
    "kubectl_action_count_24h",
    "destructive_operation_count_24h",
}
_TRUE_BOUNDARY_FLAGS = {
    "read_only_whitelist_allowed",
    "dry_run_verifier_preview_allowed",
    "rollback_proof_preview_allowed",
    "audit_reason_template_allowed",
}
_FALSE_BOUNDARY_FLAGS = {
    "auto_worker_enabled",
    "low_risk_live_execution_enabled",
    "medium_risk_live_execution_enabled",
    "gateway_queue_write_enabled",
    "telegram_send_enabled",
    "bot_api_call_enabled",
    "receipt_production_write_enabled",
    "production_write_enabled",
    "secret_read_enabled",
    "paid_api_call_enabled",
    "host_write_enabled",
    "kubectl_action_enabled",
    "destructive_operation_enabled",
    "openclaw_replacement_allowed",
}
_ZERO_ROLLUP_FIELDS = {
    "auto_worker_run_count",
    "low_risk_execution_count",
    "medium_risk_execution_count",
    "gateway_queue_write_count",
    "telegram_send_count",
    "bot_api_call_count",
    "receipt_production_write_count",
    "production_write_count",
    "secret_read_count",
    "paid_api_call_count",
    "host_write_count",
    "kubectl_action_count",
    "destructive_operation_count",
}
_FORBIDDEN_PUBLIC_TERMS = {
    "批准！繼續",
    "In app browser",
    "My request for Codex",
    "chain_of_thought",
    "chain-of-thought",
    "private reasoning text",
    "authorization_header",
    "authorization header value",
    "telegram token value",
    "raw prompt",
    "raw_payload",
}


def load_latest_ai_agent_low_medium_risk_whitelist(
    evaluations_dir: Path | None = None,
) -> dict[str, Any]:
    """Load the newest committed P2-408 low / medium risk whitelist snapshot."""
    directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
    candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
    if not candidates:
        raise FileNotFoundError(f"no AI Agent low / medium risk whitelist snapshots found in {directory}")

    latest = candidates[-1]
    with latest.open(encoding="utf-8") as handle:
        payload = json.load(handle)

    if not isinstance(payload, dict):
        raise ValueError(f"{latest}: expected JSON object")

    label = str(latest)
    _require_schema(payload, label)
    _require_sources(payload, label)
    _require_whitelist_truth(payload, label)
    _require_candidates(payload, label)
    _require_verifiers(payload, label)
    _require_rollback_proofs(payload, label)
    _require_audit_templates(payload, label)
    _require_high_risk_redirects(payload, label)
    _require_owner_gates(payload, label)
    _require_boundaries(payload, label)
    _require_rollups(payload, label)
    _require_no_forbidden_public_terms(payload, label)
    return payload


def _require_schema(payload: dict[str, Any], label: str) -> None:
    if payload.get("schema_version") != _SCHEMA_VERSION:
        raise ValueError(f"{label}: expected schema_version={_SCHEMA_VERSION}")
    status = payload.get("program_status") or {}
    expected = {
        "overall_completion_percent": 100,
        "current_priority": "P2",
        "current_task_id": _EXPECTED_CURRENT_TASK,
        "next_task_id": _EXPECTED_NEXT_TASK,
        "read_only_mode": True,
        "runtime_authority": _RUNTIME_AUTHORITY,
    }
    mismatches = _mismatches(status, expected)
    if mismatches:
        raise ValueError(f"{label}: program_status mismatch: {mismatches}")
    if not status.get("status_note"):
        raise ValueError(f"{label}: program_status.status_note is required")


def _require_sources(payload: dict[str, Any], label: str) -> None:
    if not payload.get("source_refs"):
        raise ValueError(f"{label}: source_refs must not be empty")
    sources = payload.get("source_readbacks") or []
    schemas = {item.get("source_schema_version") for item in sources}
    missing = sorted(_EXPECTED_SOURCE_SCHEMAS - schemas)
    if missing:
        raise ValueError(f"{label}: missing source schemas: {missing}")
    for item in sources:
        readback_id = item.get("readback_id") or "<missing>"
        for field in ("source_ref", "endpoint", "owner_agent", "status", "key_readback", "next_action"):
            if not item.get(field):
                raise ValueError(f"{label}: source readback {readback_id} missing {field}")


def _require_whitelist_truth(payload: dict[str, Any], label: str) -> None:
    truth = payload.get("whitelist_truth") or {}
    missing_true = sorted(flag for flag in _TRUE_TRUTH_FLAGS if truth.get(flag) is not True)
    if missing_true:
        raise ValueError(f"{label}: whitelist truth flags must remain true: {missing_true}")
    unsafe_false = sorted(flag for flag in _FALSE_TRUTH_FLAGS if truth.get(flag) is not False)
    if unsafe_false:
        raise ValueError(f"{label}: whitelist truth flags must remain false: {unsafe_false}")
    non_zero = sorted(field for field in _ZERO_TRUTH_COUNTS if truth.get(field) != 0)
    if non_zero:
        raise ValueError(f"{label}: whitelist live counts must remain zero: {non_zero}")
    if not truth.get("truth_note"):
        raise ValueError(f"{label}: whitelist_truth.truth_note is required")


def _require_candidates(payload: dict[str, Any], label: str) -> None:
    candidates = payload.get("whitelist_candidates") or []
    if len(candidates) < 1:
        raise ValueError(f"{label}: whitelist_candidates must not be empty")
    risk_tiers = {item.get("risk_tier") for item in candidates}
    if not {"low", "medium"}.issubset(risk_tiers):
        raise ValueError(f"{label}: whitelist_candidates must include low and medium candidates")
    for item in candidates:
        candidate_id = item.get("candidate_id") or "<missing>"
        if item.get("risk_tier") not in {"low", "medium"}:
            raise ValueError(f"{label}: whitelist candidate {candidate_id} must be low or medium")
        if item.get("owner_approval_required_for_live_execution") is not True:
            raise ValueError(f"{label}: whitelist candidate {candidate_id} must require owner approval before live execution")
        for flag in ("live_execution_allowed", "production_write_allowed"):
            if item.get(flag) is not False:
                raise ValueError(f"{label}: whitelist candidate {candidate_id}.{flag} must remain false")
        if item.get("side_effect_count") != 0:
            raise ValueError(f"{label}: whitelist candidate {candidate_id} side_effect_count must remain zero")
        for field in (
            "allowed_no_write_outputs",
            "required_evidence",
            "dry_run_verifier_id",
            "rollback_proof_id",
            "audit_reason_template_id",
            "blocked_runtime_actions",
            "next_gate",
        ):
            if not item.get(field):
                raise ValueError(f"{label}: whitelist candidate {candidate_id} missing {field}")


def _require_verifiers(payload: dict[str, Any], label: str) -> None:
    verifiers = payload.get("dry_run_verifiers") or []
    verifier_ids = {item.get("verifier_id") for item in verifiers}
    referenced_ids = {item.get("dry_run_verifier_id") for item in payload.get("whitelist_candidates") or []}
    missing = sorted(referenced_ids - verifier_ids)
    if missing:
        raise ValueError(f"{label}: missing dry-run verifiers referenced by candidates: {missing}")
    for item in verifiers:
        verifier_id = item.get("verifier_id") or "<missing>"
        for flag in ("live_readback_allowed", "production_write_allowed"):
            if item.get(flag) is not False:
                raise ValueError(f"{label}: dry-run verifier {verifier_id}.{flag} must remain false")
        if not item.get("required_inputs") or not item.get("pass_condition"):
            raise ValueError(f"{label}: dry-run verifier {verifier_id} missing inputs or pass condition")


def _require_rollback_proofs(payload: dict[str, Any], label: str) -> None:
    proofs = payload.get("rollback_proofs") or []
    proof_ids = {item.get("rollback_proof_id") for item in proofs}
    referenced_ids = {item.get("rollback_proof_id") for item in payload.get("whitelist_candidates") or []}
    missing = sorted(referenced_ids - proof_ids)
    if missing:
        raise ValueError(f"{label}: missing rollback proofs referenced by candidates: {missing}")
    for item in proofs:
        proof_id = item.get("rollback_proof_id") or "<missing>"
        if item.get("rollback_command_allowed") is not False:
            raise ValueError(f"{label}: rollback proof {proof_id}.rollback_command_allowed must remain false")
        if item.get("required_before_live_execution") is not True:
            raise ValueError(f"{label}: rollback proof {proof_id} must be required before live execution")
        if not item.get("rollback_scope"):
            raise ValueError(f"{label}: rollback proof {proof_id} missing rollback_scope")


def _require_audit_templates(payload: dict[str, Any], label: str) -> None:
    templates = payload.get("audit_reason_templates") or []
    template_ids = {item.get("template_id") for item in templates}
    referenced_ids = {item.get("audit_reason_template_id") for item in payload.get("whitelist_candidates") or []}
    missing = sorted(referenced_ids - template_ids)
    if missing:
        raise ValueError(f"{label}: missing audit reason templates referenced by candidates: {missing}")
    for item in templates:
        template_id = item.get("template_id") or "<missing>"
        if item.get("risk_tier") not in {"low", "medium"}:
            raise ValueError(f"{label}: audit template {template_id} must be low or medium")
        if item.get("sensitive_payload_allowed") is not False:
            raise ValueError(f"{label}: audit template {template_id}.sensitive_payload_allowed must remain false")
        if not item.get("required_fields") or not item.get("example_reason"):
            raise ValueError(f"{label}: audit template {template_id} missing required fields or example reason")


def _require_high_risk_redirects(payload: dict[str, Any], label: str) -> None:
    redirects = payload.get("high_risk_redirects") or []
    if len(redirects) < 1:
        raise ValueError(f"{label}: high_risk_redirects must not be empty")
    for item in redirects:
        redirect_id = item.get("redirect_id") or "<missing>"
        if item.get("risk_tier") not in {"high", "critical"}:
            raise ValueError(f"{label}: redirect {redirect_id} must be high or critical")
        if item.get("redirect_to") != "P2-409 Owner Review Queue":
            raise ValueError(f"{label}: redirect {redirect_id} must point to P2-409 Owner Review Queue")
        if not item.get("blocked_runtime_actions") or not item.get("reason"):
            raise ValueError(f"{label}: redirect {redirect_id} missing blocked actions or reason")


def _require_owner_gates(payload: dict[str, Any], label: str) -> None:
    gates = payload.get("owner_review_gates") or []
    if len(gates) < 1:
        raise ValueError(f"{label}: owner_review_gates must not be empty")
    for gate in gates:
        gate_id = gate.get("gate_id") or "<missing>"
        if gate.get("status") not in {"owner_review_required", "blocked_by_runtime_gate", "draft_ready"}:
            raise ValueError(f"{label}: owner gate {gate_id} status is invalid")
        for field in ("required_fields", "acceptance_checks", "blocked_runtime_actions"):
            if not gate.get(field):
                raise ValueError(f"{label}: owner gate {gate_id} missing {field}")


def _require_boundaries(payload: dict[str, Any], label: str) -> None:
    boundaries = payload.get("activation_boundaries") or {}
    missing_true = sorted(flag for flag in _TRUE_BOUNDARY_FLAGS if boundaries.get(flag) is not True)
    if missing_true:
        raise ValueError(f"{label}: activation boundaries must remain true: {missing_true}")
    unsafe_false = sorted(flag for flag in _FALSE_BOUNDARY_FLAGS if boundaries.get(flag) is not False)
    if unsafe_false:
        raise ValueError(f"{label}: activation boundaries must remain false: {unsafe_false}")

    telegram = payload.get("telegram_policy") or {}
    expected_telegram = {
        "canonical_room": _EXPECTED_CANONICAL_ROOM,
        "canonical_room_env": _EXPECTED_CANONICAL_ROOM_ENV,
        "gateway_queue_write_allowed": False,
        "direct_bot_api_allowed": False,
        "telegram_send_allowed": False,
        "receipt_write_allowed": False,
    }
    mismatches = _mismatches(telegram, expected_telegram)
    if mismatches:
        raise ValueError(f"{label}: telegram_policy mismatch: {mismatches}")

    redaction = payload.get("display_redaction_contract") or {}
    if redaction.get("redaction_required") is not True:
        raise ValueError(f"{label}: display redaction must remain required")
    for flag in (
        "unsafe_payload_display_allowed",
        "private_reasoning_display_allowed",
        "secret_value_display_allowed",
        "work_window_transcript_display_allowed",
    ):
        if redaction.get(flag) is not False:
            raise ValueError(f"{label}: display redaction flag {flag} must remain false")


def _require_rollups(payload: dict[str, Any], label: str) -> None:
    rollups = payload.get("rollups") or {}
    candidates = payload.get("whitelist_candidates") or []
    verifiers = payload.get("dry_run_verifiers") or []
    rollback_proofs = payload.get("rollback_proofs") or []
    audit_templates = payload.get("audit_reason_templates") or []
    redirects = payload.get("high_risk_redirects") or []
    gates = payload.get("owner_review_gates") or []
    sources = payload.get("source_readbacks") or []
    blocked_actions = {
        *(
            action
            for candidate in candidates
            for action in (candidate.get("blocked_runtime_actions") or [])
        ),
        *(
            action
            for redirect in redirects
            for action in (redirect.get("blocked_runtime_actions") or [])
        ),
        *(
            action
            for gate in gates
            for action in (gate.get("blocked_runtime_actions") or [])
        ),
    }
    blocked_actions.discard(None)
    expected = {
        "source_readback_count": len(sources),
        "whitelist_candidate_count": len(candidates),
        "low_risk_candidate_count": sum(1 for item in candidates if item.get("risk_tier") == "low"),
        "medium_risk_candidate_count": sum(1 for item in candidates if item.get("risk_tier") == "medium"),
        "candidate_only_count": len(candidates),
        "dry_run_verifier_count": len(verifiers),
        "rollback_proof_count": len(rollback_proofs),
        "audit_reason_template_count": len(audit_templates),
        "high_risk_redirect_count": len(redirects),
        "owner_review_gate_count": len(gates),
        "live_execution_approval_required_count": sum(
            1 for item in candidates if item.get("owner_approval_required_for_live_execution") is True
        ),
        "blocked_runtime_action_count": len(blocked_actions),
    }
    mismatches = {
        key: {"expected": value, "actual": rollups.get(key)}
        for key, value in expected.items()
        if rollups.get(key) != value
    }
    if mismatches:
        raise ValueError(f"{label}: rollup counts must match payload sections: {mismatches}")

    non_zero = sorted(field for field in _ZERO_ROLLUP_FIELDS if rollups.get(field) != 0)
    if non_zero:
        raise ValueError(f"{label}: live rollup counts must remain zero: {non_zero}")


def _require_no_forbidden_public_terms(payload: dict[str, Any], label: str) -> None:
    public_text = json.dumps(payload, ensure_ascii=False)
    lower_public_text = public_text.lower()
    leaked_terms = sorted(
        term
        for term in _FORBIDDEN_PUBLIC_TERMS
        if (term.lower() if term.isascii() else term) in lower_public_text
    )
    if leaked_terms:
        raise ValueError(f"{label}: forbidden public terms present: {leaked_terms}")


def _mismatches(actual: dict[str, Any], expected: dict[str, Any]) -> dict[str, dict[str, Any]]:
    return {
        key: {"expected": expected_value, "actual": actual.get(key)}
        for key, expected_value in expected.items()
        if actual.get(key) != expected_value
    }
