#!/usr/bin/env python3
"""
IwoooS K8s / ArgoCD GitOps 變更證據驗收只讀帳本產生器。

本工具讀取 repo-only K8s / ArgoCD manifest inventory 與 owner response
acceptance snapshot，建立未來 GitOps 變更證據如何收件、補件、拒收或進入
人工 reviewer review 的 metadata-only ledger。它不讀 live cluster、不呼叫
ArgoCD API、不 sync、不執行 kubectl、不套用 manifest、不讀 secret value。
"""

from __future__ import annotations

import argparse
import json
import subprocess
import sys
from collections import Counter
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any


TAIPEI = timezone(timedelta(hours=8))

CHANGE_EVIDENCE_FIELDS = [
    "change_evidence_candidate_id",
    "acceptance_candidate_id",
    "group_id",
    "root",
    "control_tier",
    "proposed_commit_ref",
    "rendered_manifest_diff_ref",
    "argocd_application_readback_ref",
    "argocd_sync_revision_ref",
    "health_before_ref",
    "health_after_ref",
    "rollout_status_ref",
    "service_route_smoke_ref",
    "metrics_alert_ref",
    "secret_metadata_parity_ref",
    "blast_radius",
    "maintenance_window",
    "rollback_revision",
    "postcheck_owner",
    "reviewer_outcome",
    "not_approval",
]

REQUIRED_EVIDENCE_FIELDS = [
    "proposed_commit_ref",
    "rendered_manifest_diff_ref",
    "argocd_application_readback_ref",
    "argocd_sync_revision_ref",
    "health_before_ref",
    "health_after_ref",
    "rollout_status_ref",
    "service_route_smoke_ref",
    "metrics_alert_ref",
    "secret_metadata_parity_ref",
    "blast_radius",
    "maintenance_window",
    "rollback_revision",
    "postcheck_owner",
    "affected_scope",
    "redacted_evidence_refs",
    "reviewer_outcome",
    "not_approval",
]

REVIEWER_CHECKS = [
    {
        "check_id": "change_ref_present",
        "instruction": "必須有 proposed commit / PR / patch ref，不能只寫聊天同意。",
    },
    {
        "check_id": "group_scope_matches_inventory",
        "instruction": "affected scope 必須能對回 manifest inventory group 與 root path。",
    },
    {
        "check_id": "rendered_manifest_diff_ref_only",
        "instruction": "只能收 rendered manifest diff ref，不保存完整 manifest payload。",
    },
    {
        "check_id": "argocd_readback_ref_shape",
        "instruction": "ArgoCD app / sync revision / health readback 只能是 owner-provided redacted ref。",
    },
    {
        "check_id": "secret_value_absent",
        "instruction": "不得出現 Secret value、token、kubeconfig、private key、cookie、DSN 或 credential derivative。",
    },
    {
        "check_id": "network_policy_impact_called_out",
        "instruction": "涉及 NetworkPolicy、Service、Ingress 或 NodePort 時必須標出 route / port 影響。",
    },
    {
        "check_id": "rbac_impact_called_out",
        "instruction": "涉及 RBAC / ServiceAccount 時必須標出權限影響與 rollback revision。",
    },
    {
        "check_id": "workload_rollout_plan_present",
        "instruction": "涉及 Deployment、CronJob、HPA 或 image 時必須有 rollout / rollback / smoke plan。",
    },
    {
        "check_id": "health_before_after_present",
        "instruction": "需有 health before / after evidence ref；沒有 before evidence 只能要求補件。",
    },
    {
        "check_id": "route_smoke_or_not_applicable",
        "instruction": "公開 / admin / API route 受影響時需有 smoke ref；不適用時要說明原因。",
    },
    {
        "check_id": "metric_alert_postcheck_present",
        "instruction": "需有 metric、alert、事件或 log post-check evidence ref。",
    },
    {
        "check_id": "blast_radius_present",
        "instruction": "必須列 namespace、workload、route、port、secret metadata、backup / restore 影響範圍。",
    },
    {
        "check_id": "maintenance_window_present",
        "instruction": "任何 future sync / apply / rollout 都必須另有維護窗口。",
    },
    {
        "check_id": "rollback_revision_present",
        "instruction": "必須有 rollback revision 或明確禁止窗口與 follow-up owner。",
    },
    {
        "check_id": "postcheck_owner_present",
        "instruction": "post-check owner 必須可追溯，不能只寫 team generic approval。",
    },
    {
        "check_id": "no_runtime_action_claim",
        "instruction": "不能把本帳本、owner response、CD success 或 deploy marker 當 runtime approval。",
    },
    {
        "check_id": "counts_transition_safe",
        "instruction": "只有 reviewer record 可更新 received / accepted / rejected；不得同時開 runtime gate。",
    },
    {
        "check_id": "cross_project_sync_noted",
        "instruction": "若影響 AwoooP、agent-bounty、監控、公開 gateway 或外部產品，需有跨專案同步 ref。",
    },
]

OUTCOME_LANES = [
    {
        "lane_id": "waiting_change_evidence",
        "meaning": "尚未收到 GitOps 變更證據；所有 accepted / runtime count 維持 0。",
    },
    {
        "lane_id": "quarantine_sensitive_payload",
        "meaning": "收到 manifest payload、kubeconfig、Secret value 或可還原 credential 時只能隔離。",
    },
    {
        "lane_id": "reject_unredacted_or_runtime_claim",
        "meaning": "出現未脫敏 payload、secret value 或把 evidence 誤當執行批准時直接拒收。",
    },
    {
        "lane_id": "request_supplement",
        "meaning": "缺 before / after、rollback、blast radius、post-check 或 route smoke 時要求補件。",
    },
    {
        "lane_id": "ready_for_reviewer_acceptance",
        "meaning": "metadata 合格後，只能進 reviewer acceptance，不得自動 sync 或 apply。",
    },
    {
        "lane_id": "ready_for_runtime_approval_package",
        "meaning": "reviewer 接受後也只能形成 runtime approval package，不自動打開 gate。",
    },
    {
        "lane_id": "waiting_maintenance_window",
        "meaning": "若未來要 sync / rollout / rollback，仍需獨立維護窗口。",
    },
    {
        "lane_id": "waiting_runtime_gate",
        "meaning": "即使 change evidence accepted，runtime gate 仍等待獨立人工批准。",
    },
]

BLOCKED_ACTIONS = [
    "argocd_api_read_without_approval",
    "live_cluster_read_without_approval",
    "argocd_sync",
    "argocd_rollback",
    "kubectl_apply",
    "kubectl_patch",
    "kubectl_delete",
    "kubectl_rollout_restart",
    "helm_upgrade",
    "kustomize_image_set",
    "secret_value_collection",
    "kubeconfig_collection",
    "live_cluster_write",
    "manual_pod_restart",
    "scale_workload",
    "change_network_policy",
    "change_rbac",
    "change_service_type",
    "change_nodeport",
    "change_ingress_route",
    "change_configmap_runtime",
    "change_secret_metadata_without_owner",
    "restore_backup",
    "velero_restore",
    "prometheus_rule_apply",
    "mark_change_evidence_accepted_without_reviewer_record",
    "open_runtime_gate",
    "add_action_button",
]


def git_short_sha(root: Path) -> str:
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--short", "HEAD"],
            cwd=root,
            check=True,
            capture_output=True,
            text=True,
        )
        return result.stdout.strip()
    except Exception:
        return "unknown"


def load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def tags_by_group(inventory: dict[str, Any]) -> dict[str, list[str]]:
    grouped: dict[str, set[str]] = {}
    for row in inventory.get("manifest_rows", []):
        group_id = row["group_id"]
        grouped.setdefault(group_id, set()).update(row.get("gate_tags", []))
    return {key: sorted(value) for key, value in grouped.items()}


def summary_by_group(inventory: dict[str, Any]) -> dict[str, dict[str, Any]]:
    return {item["group_id"]: item for item in inventory.get("group_summaries", [])}


def change_candidate(
    acceptance: dict[str, Any],
    group_summary: dict[str, Any],
    group_tags: list[str],
) -> dict[str, Any]:
    group_id = acceptance["group_id"]
    is_write_capable = bool(
        {"network_policy", "rbac", "workload_or_schedule", "backup_restore", "apply_capable_script"}
        & set(group_tags)
    )
    return {
        "change_evidence_candidate_id": f"k8s_argocd_change_evidence:{group_id}",
        "status": "waiting_change_evidence",
        "acceptance_candidate_id": acceptance["acceptance_candidate_id"],
        "group_id": group_id,
        "root": acceptance["root"],
        "control_tier": acceptance["control_tier"],
        "file_count": group_summary.get("file_count", 0),
        "yaml_manifest_file_count": group_summary.get("yaml_manifest_file_count", 0),
        "supporting_source_file_count": group_summary.get("supporting_source_file_count", 0),
        "top_level_kind_marker_count": group_summary.get("top_level_kind_marker_count", 0),
        "gate_tags": group_tags,
        "write_capable": is_write_capable,
        "requires_runtime_approval_package": True,
        "change_evidence_fields": CHANGE_EVIDENCE_FIELDS,
        "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS,
        "reviewer_checks": [item["check_id"] for item in REVIEWER_CHECKS],
        "outcome_lanes": [item["lane_id"] for item in OUTCOME_LANES],
        "blocked_actions": BLOCKED_ACTIONS,
        "proposed_commit_ref": None,
        "rendered_manifest_diff_ref": None,
        "argocd_application_readback_ref": None,
        "argocd_sync_revision_ref": None,
        "health_before_ref": None,
        "health_after_ref": None,
        "rollout_status_ref": None,
        "service_route_smoke_ref": None,
        "metrics_alert_ref": None,
        "secret_metadata_parity_ref": None,
        "blast_radius": "pending_change_evidence",
        "maintenance_window": "pending_change_evidence",
        "rollback_revision": "pending_change_evidence",
        "postcheck_owner": "pending_change_evidence",
        "reviewer_outcome": "waiting_change_evidence",
        "not_approval": True,
        "change_evidence_received": False,
        "change_evidence_accepted": False,
        "change_evidence_rejected": False,
        "change_evidence_quarantined": False,
        "supplement_requested": False,
        "proposed_commit_ref_accepted": False,
        "rendered_manifest_diff_accepted": False,
        "argocd_application_readback_accepted": False,
        "argocd_sync_revision_accepted": False,
        "health_before_accepted": False,
        "health_after_accepted": False,
        "rollout_status_accepted": False,
        "service_route_smoke_accepted": False,
        "metrics_alert_accepted": False,
        "secret_metadata_parity_accepted": False,
        "blast_radius_accepted": False,
        "maintenance_window_accepted": False,
        "rollback_revision_accepted": False,
        "postcheck_owner_accepted": False,
        "cross_project_sync_accepted": False,
        "runtime_approval_package_ready": False,
        "argocd_api_read_authorized": False,
        "argocd_api_read_executed": False,
        "argocd_sync_authorized": False,
        "argocd_sync_executed": False,
        "kubectl_action_authorized": False,
        "kubectl_action_executed": False,
        "helm_upgrade_authorized": False,
        "helm_upgrade_executed": False,
        "live_cluster_read_authorized": False,
        "live_cluster_read_executed": False,
        "network_policy_apply_authorized": False,
        "nodeport_change_authorized": False,
        "rbac_change_authorized": False,
        "secret_value_collection_allowed": False,
        "production_write_authorized": False,
        "runtime_gate": False,
        "action_buttons_allowed": False,
    }


def build_report(
    root: Path,
    inventory: dict[str, Any],
    owner_acceptance: dict[str, Any],
    generated_at: str | None,
) -> dict[str, Any]:
    report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
    tag_map = tags_by_group(inventory)
    group_map = summary_by_group(inventory)
    candidates = [
        change_candidate(item, group_map.get(item["group_id"], {}), tag_map.get(item["group_id"], []))
        for item in owner_acceptance.get("acceptance_candidates", [])
    ]
    c0_candidates = [item for item in candidates if item["control_tier"] == "C0"]
    c1_candidates = [item for item in candidates if item["control_tier"] == "C1"]
    tag_counter: Counter[str] = Counter(tag for item in candidates for tag in item["gate_tags"])
    write_capable_candidates = [item for item in candidates if item["write_capable"]]

    inventory_summary = inventory.get("summary", {})
    return {
        "schema_version": "k8s_argocd_change_evidence_acceptance_v1",
        "generated_at": report_time,
        "git_commit": git_short_sha(root),
        "source_inventory_schema_version": inventory.get("schema_version"),
        "source_inventory_status": inventory.get("status"),
        "source_acceptance_schema_version": owner_acceptance.get("schema_version"),
        "source_acceptance_status": owner_acceptance.get("status"),
        "status": "change_evidence_acceptance_ledger_ready_no_runtime_action",
        "summary": {
            "source_scan_group_count": inventory_summary.get("scan_group_count", 0),
            "source_manifest_file_count": inventory_summary.get("file_count", 0),
            "source_yaml_manifest_file_count": inventory_summary.get("yaml_manifest_file_count", 0),
            "source_c0_file_count": inventory_summary.get("c0_file_count", 0),
            "source_acceptance_candidate_count": owner_acceptance.get("summary", {}).get(
                "acceptance_candidate_count", 0
            ),
            "deployment_object_count": inventory_summary.get("deployment_object_count", 0),
            "cronjob_object_count": inventory_summary.get("cronjob_object_count", 0),
            "secret_object_count": inventory_summary.get("secret_object_count", 0),
            "network_policy_object_count": inventory_summary.get("network_policy_object_count", 0),
            "rbac_object_count": inventory_summary.get("rbac_object_count", 0),
            "argocd_application_count": inventory_summary.get("argocd_application_count", 0),
            "prometheus_rule_count": inventory_summary.get("prometheus_rule_count", 0),
            "change_evidence_candidate_count": len(candidates),
            "c0_change_evidence_candidate_count": len(c0_candidates),
            "c1_change_evidence_candidate_count": len(c1_candidates),
            "write_capable_candidate_count": len(write_capable_candidates),
            "gate_tag_count": len(tag_counter),
            "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS),
            "change_evidence_field_count": len(CHANGE_EVIDENCE_FIELDS),
            "reviewer_check_count": len(REVIEWER_CHECKS),
            "outcome_lane_count": len(OUTCOME_LANES),
            "blocked_action_count": len(BLOCKED_ACTIONS),
            "change_evidence_received_count": 0,
            "change_evidence_accepted_count": 0,
            "change_evidence_rejected_count": 0,
            "change_evidence_quarantined_count": 0,
            "supplement_requested_count": 0,
            "rendered_manifest_diff_accepted_count": 0,
            "argocd_application_readback_accepted_count": 0,
            "argocd_sync_revision_accepted_count": 0,
            "health_before_accepted_count": 0,
            "health_after_accepted_count": 0,
            "rollout_status_accepted_count": 0,
            "service_route_smoke_accepted_count": 0,
            "metrics_alert_accepted_count": 0,
            "secret_metadata_parity_accepted_count": 0,
            "blast_radius_accepted_count": 0,
            "maintenance_window_accepted_count": 0,
            "rollback_revision_accepted_count": 0,
            "postcheck_owner_accepted_count": 0,
            "cross_project_sync_accepted_count": 0,
            "runtime_approval_package_ready_count": 0,
            "argocd_api_read_authorized_count": 0,
            "argocd_api_read_executed_count": 0,
            "argocd_sync_authorized_count": 0,
            "argocd_sync_executed_count": 0,
            "kubectl_action_authorized_count": 0,
            "kubectl_action_executed_count": 0,
            "helm_upgrade_authorized_count": 0,
            "helm_upgrade_executed_count": 0,
            "live_cluster_read_authorized_count": 0,
            "live_cluster_read_executed_count": 0,
            "network_policy_apply_authorized_count": 0,
            "nodeport_change_authorized_count": 0,
            "rbac_change_authorized_count": 0,
            "secret_value_collection_allowed_count": 0,
            "production_write_authorized_count": 0,
            "runtime_gate_count": 0,
            "action_button_count": 0,
            "coverage_percent_before_acceptance": 62,
            "coverage_percent_after_acceptance": 64,
        },
        "execution_boundaries": {
            "change_evidence_received": False,
            "change_evidence_accepted": False,
            "rendered_manifest_diff_accepted": False,
            "argocd_application_readback_accepted": False,
            "argocd_sync_revision_accepted": False,
            "health_before_after_accepted": False,
            "rollout_status_accepted": False,
            "service_route_smoke_accepted": False,
            "metrics_alert_accepted": False,
            "secret_metadata_parity_accepted": False,
            "blast_radius_accepted": False,
            "maintenance_window_accepted": False,
            "rollback_revision_accepted": False,
            "postcheck_owner_accepted": False,
            "cross_project_sync_accepted": False,
            "runtime_approval_package_ready": False,
            "argocd_api_read_authorized": False,
            "argocd_api_read_executed": False,
            "argocd_sync_authorized": False,
            "argocd_sync_executed": False,
            "kubectl_action_authorized": False,
            "kubectl_action_executed": False,
            "helm_upgrade_authorized": False,
            "helm_upgrade_executed": False,
            "live_cluster_read_authorized": False,
            "live_cluster_read_executed": False,
            "network_policy_apply_authorized": False,
            "nodeport_change_authorized": False,
            "rbac_change_authorized": False,
            "secret_value_collection_allowed": False,
            "production_write_authorized": False,
            "runtime_execution_authorized": False,
            "action_buttons_allowed": False,
            "not_authorization": True,
        },
        "change_evidence_fields": CHANGE_EVIDENCE_FIELDS,
        "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS,
        "reviewer_checks": REVIEWER_CHECKS,
        "outcome_lanes": OUTCOME_LANES,
        "blocked_actions": BLOCKED_ACTIONS,
        "gate_tag_counts": dict(sorted(tag_counter.items())),
        "change_evidence_candidates": candidates,
        "next_steps": [
            "要求 owner 提供 proposed commit、rendered manifest diff、ArgoCD app / sync revision、health before / after、rollout、route smoke、metrics / alert、rollback 與 post-check evidence ref。",
            "收到資料後先做敏感 payload 隔離與 reviewer acceptance，不得自動 sync、apply、restart、scale 或改 NetworkPolicy / RBAC / NodePort。",
            "若未來要進 runtime approval package，必須另有維護窗口、rollback revision、跨專案同步與 production post-check gate。",
        ],
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="IwoooS K8s / ArgoCD 變更證據驗收只讀帳本產生器")
    parser.add_argument("--root", default=".", help="repo root")
    parser.add_argument(
        "--inventory-report",
        default="docs/security/k8s-argocd-manifest-inventory.snapshot.json",
        help="k8s-argocd-manifest-inventory.py 輸出的 JSON",
    )
    parser.add_argument(
        "--acceptance-report",
        default="docs/security/k8s-argocd-owner-response-acceptance.snapshot.json",
        help="k8s-argocd-owner-response-acceptance.py 輸出的 JSON",
    )
    parser.add_argument("--output", help="寫出 JSON 報告")
    parser.add_argument("--generated-at", help="固定報告時間，供 committed snapshot 使用")
    args = parser.parse_args()

    root = Path(args.root).resolve()
    inventory = load_json(root / args.inventory_report)
    acceptance = load_json(root / args.acceptance_report)
    report = build_report(root, inventory, acceptance, args.generated_at)
    payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)

    if args.output:
        output = Path(args.output)
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(payload + "\n", encoding="utf-8")
    else:
        print(payload)

    summary = report["summary"]
    print(
        "K8S_ARGOCD_CHANGE_EVIDENCE_ACCEPTANCE_OK "
        f"candidates={summary['change_evidence_candidate_count']} "
        f"c0={summary['c0_change_evidence_candidate_count']} "
        f"write_capable={summary['write_capable_candidate_count']} "
        f"checks={summary['reviewer_check_count']} "
        f"lanes={summary['outcome_lane_count']} "
        f"accepted={summary['change_evidence_accepted_count']} "
        f"runtime_gate={summary['runtime_gate_count']}",
        file=sys.stderr,
    )
    return 0


if __name__ == "__main__":
    sys.exit(main())
