#!/usr/bin/env python3
"""
IwoooS Public / Admin / API runtime config 變更證據驗收只讀帳本產生器。

本工具只整理 repo 內 public route、admin/auth boundary、API/CORS、frontend env、
i18n redaction 與 webhook/callback 的 source refs，建立未來 runtime config 變更
證據如何收件、補件、拒收或交給 reviewer 的 metadata-only ledger。它不讀 live
secret、不改 CORS、不改 route、不觸發 webhook、不部署、不開 runtime gate。
"""

from __future__ import annotations

import argparse
import json
import subprocess
import sys
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",
    "source_refs",
    "control_tier",
    "proposed_runtime_config_change_ref",
    "affected_route_refs",
    "public_url_or_domain_ref",
    "admin_auth_boundary_ref",
    "api_contract_readback_ref",
    "cors_origin_diff_ref",
    "frontend_env_diff_ref",
    "i18n_redaction_review_ref",
    "webhook_callback_owner_ref",
    "desktop_mobile_smoke_ref",
    "api_health_readback_ref",
    "sensitive_string_scan_ref",
    "console_error_scan_ref",
    "blast_radius",
    "maintenance_window",
    "rollback_owner",
    "rollback_plan_ref",
    "postcheck_evidence_ref",
    "redacted_evidence_refs",
    "reviewer_outcome",
    "not_approval",
]

REQUIRED_EVIDENCE_FIELDS = [
    "proposed_runtime_config_change_ref",
    "affected_route_refs",
    "public_url_or_domain_ref",
    "admin_auth_boundary_ref",
    "api_contract_readback_ref",
    "cors_origin_diff_ref",
    "frontend_env_diff_ref",
    "i18n_redaction_review_ref",
    "webhook_callback_owner_ref",
    "desktop_mobile_smoke_ref",
    "api_health_readback_ref",
    "sensitive_string_scan_ref",
    "console_error_scan_ref",
    "blast_radius",
    "maintenance_window",
    "rollback_owner",
    "rollback_plan_ref",
    "postcheck_evidence_ref",
    "redacted_evidence_refs",
    "reviewer_outcome",
    "not_approval",
]

REVIEWER_CHECKS = [
    {
        "check_id": "change_ref_present",
        "instruction": "必須有 proposed runtime config change ref，不能只寫口頭同意。",
    },
    {
        "check_id": "affected_route_refs_present",
        "instruction": "必須列出 public / admin / API / callback / webhook / frontend route 影響範圍。",
    },
    {
        "check_id": "public_url_not_internal_ip",
        "instruction": "NEXT_PUBLIC 與公開 URL 只能使用 public domain，不得暴露內網 IP。",
    },
    {
        "check_id": "admin_auth_boundary_called_out",
        "instruction": "涉及後台、operator console 或審批路由時必須標出 auth boundary 與 owner。",
    },
    {
        "check_id": "api_contract_readback_present",
        "instruction": "API 變更需有 readback ref，且 public payload 不得暴露 raw owner namespace、repo slug 或內部狀態碼。",
    },
    {
        "check_id": "cors_origin_diff_ref_only",
        "instruction": "CORS 只能收 origin diff / owner ref，不得直接改白名單或使用萬用來源。",
    },
    {
        "check_id": "frontend_env_diff_ref_present",
        "instruction": "涉及 frontend env / build-time public config 時必須附 diff ref 與 bundle sensitive scan ref。",
    },
    {
        "check_id": "i18n_redaction_review_present",
        "instruction": "前台文案需確認全繁中、無內部對話、無抱怨語句、無 raw identity。",
    },
    {
        "check_id": "webhook_callback_owner_present",
        "instruction": "callback / webhook / Sentry tunnel / Telegram route 需有 owner 與回復方式。",
    },
    {
        "check_id": "desktop_mobile_smoke_present",
        "instruction": "涉及前台或後台 route 時必須有 desktop / mobile smoke ref 與 horizontal overflow 結果。",
    },
    {
        "check_id": "api_health_readback_present",
        "instruction": "API / backend runtime config 需有 health 或 contract readback ref。",
    },
    {
        "check_id": "sensitive_string_scan_present",
        "instruction": "必須附 sensitive string scan ref，至少檢查 raw namespace、internal state code、internal transcript、secret value。",
    },
    {
        "check_id": "console_error_scan_present",
        "instruction": "前端 smoke 需標出 console/page error 結果或說明不適用。",
    },
    {
        "check_id": "no_secret_value_or_cookie",
        "instruction": "不得保存 cookie、token、DSN value、secret value、hash、partial token 或 raw payload。",
    },
    {
        "check_id": "security_header_or_cookie_impact_called_out",
        "instruction": "若影響 headers、cookie、CSRF、rate limit 或 middleware，必須標出安全影響。",
    },
    {
        "check_id": "blast_radius_present",
        "instruction": "必須列出產品、route、API、admin/auth、public domain、callback、webhook 與使用者影響。",
    },
    {
        "check_id": "maintenance_window_present",
        "instruction": "任何 future runtime config 變更都必須另有維護窗口或明確 not-applicable 理由。",
    },
    {
        "check_id": "rollback_owner_present",
        "instruction": "必須有 rollback owner 與回復方式；不能只寫『可回復』。",
    },
    {
        "check_id": "postcheck_evidence_present",
        "instruction": "需有 post-check evidence ref，例如 API readback、browser smoke、bundle scan 或 alert silence review。",
    },
    {
        "check_id": "no_runtime_action_claim",
        "instruction": "不能把本帳本、UI 可見、CD success、AwoooP approval 或 smoke pass 當資安批准。",
    },
    {
        "check_id": "cross_project_sync_noted",
        "instruction": "若影響 AwoooP、IwoooS、agent-bounty、StockPlatform、公開網站或監控，需有跨專案同步 ref。",
    },
]

OUTCOME_LANES = [
    {
        "lane_id": "waiting_change_evidence",
        "meaning": "尚未收到 runtime config 變更證據；所有 accepted / runtime count 維持 0。",
    },
    {
        "lane_id": "quarantine_sensitive_payload",
        "meaning": "收到 cookie、token、secret value、raw internal payload 或未脫敏截圖時只能隔離。",
    },
    {
        "lane_id": "reject_unredacted_or_runtime_claim",
        "meaning": "出現 raw identity、internal transcript、internal state code 或把 evidence 誤當批准時直接拒收。",
    },
    {
        "lane_id": "request_supplement",
        "meaning": "缺 route scope、auth boundary、CORS diff、desktop/mobile smoke、rollback 或 post-check 時要求補件。",
    },
    {
        "lane_id": "ready_for_reviewer_acceptance",
        "meaning": "metadata 合格後只能進 reviewer acceptance，不得自動改 route / CORS / env。",
    },
    {
        "lane_id": "ready_for_runtime_approval_package",
        "meaning": "reviewer 接受後也只能形成 runtime approval package，不自動打開 gate。",
    },
    {
        "lane_id": "waiting_maintenance_window",
        "meaning": "若未來要改 public/admin/API runtime config，仍需獨立維護窗口。",
    },
    {
        "lane_id": "waiting_runtime_gate",
        "meaning": "change evidence accepted 後 runtime gate 仍等待獨立人工批准。",
    },
]

BLOCKED_ACTIONS = [
    "change_public_route",
    "change_admin_route",
    "change_api_route",
    "change_cors_origin",
    "modify_next_public_env",
    "expose_internal_ip",
    "expose_repo_slug",
    "expose_owner_namespace",
    "expose_secret_value",
    "bypass_auth",
    "change_callback_url",
    "change_webhook_secret",
    "modify_middleware_auth",
    "disable_csrf",
    "disable_rate_limit",
    "change_cookie_policy",
    "change_security_headers",
    "publish_internal_transcript",
    "publish_internal_status_code",
    "deploy_frontend",
    "deploy_api",
    "rewrite_nginx_route",
    "change_public_url",
    "change_openapi_contract",
    "mutate_database",
    "run_migration",
    "send_webhook",
    "active_scan",
    "enable_action_button",
    "production_deploy",
    "force_push",
    "switch_github_primary",
]

EXECUTION_BOUNDARIES = {
    "runtime_execution_authorized": False,
    "runtime_config_change_authorized": False,
    "public_route_change_authorized": False,
    "admin_route_change_authorized": False,
    "api_route_change_authorized": False,
    "cors_change_authorized": False,
    "frontend_env_change_authorized": False,
    "middleware_auth_change_authorized": False,
    "callback_url_change_authorized": False,
    "webhook_receiver_change_authorized": False,
    "webhook_secret_change_authorized": False,
    "security_header_change_authorized": False,
    "cookie_policy_change_authorized": False,
    "csrf_disable_authorized": False,
    "rate_limit_disable_authorized": False,
    "api_contract_change_authorized": False,
    "i18n_public_text_internal_identity_allowed": False,
    "internal_ip_exposure_allowed": False,
    "repo_namespace_exposure_allowed": False,
    "owner_namespace_exposure_allowed": False,
    "internal_status_code_exposure_allowed": False,
    "internal_transcript_exposure_allowed": False,
    "secret_value_collection_allowed": False,
    "secret_hash_collection_allowed": False,
    "partial_token_collection_allowed": False,
    "raw_payload_storage_allowed": False,
    "desktop_mobile_smoke_authorized": False,
    "route_smoke_authorized": False,
    "production_deploy_authorized": False,
    "database_migration_authorized": False,
    "force_push_authorized": False,
    "github_primary_switch_authorized": False,
    "action_buttons_allowed": False,
    "not_authorization": True,
}

CANDIDATE_DEFINITIONS = [
    {
        "candidate_id": "public_product_route_and_i18n_redaction",
        "title": "Public product route / i18n redaction boundary",
        "control_tier": "C0",
        "risk": "HIGH",
        "source_refs": [
            "apps/web/src/app/[locale]",
            "apps/web/messages/zh-TW.json",
            "apps/web/messages/en.json",
        ],
        "affected_scope": "公開產品頁、IwoooS / AwoooP / Tenants / Code Review 前台文案、raw identity 與內部協作文字防外洩",
    },
    {
        "candidate_id": "admin_auth_and_operator_console_boundary",
        "title": "Admin / operator console auth boundary",
        "control_tier": "C0",
        "risk": "HIGH",
        "source_refs": [
            "apps/web/src/app/[locale]/awooop",
            "apps/api/src/core/awooop_operator_auth.py",
            "apps/api/src/core/csrf.py",
        ],
        "affected_scope": "AwoooP operator console、approvals、work-items、runs、admin auth / CSRF / owner guard",
    },
    {
        "candidate_id": "api_cors_and_public_url_runtime_config",
        "title": "API / CORS / public URL runtime config",
        "control_tier": "C0",
        "risk": "HIGH",
        "source_refs": [
            "apps/api/src/core/config.py",
            "apps/api/src/config.py",
            "apps/web/src/lib/config.ts",
            "apps/web/src/lib/api-client.ts",
        ],
        "affected_scope": "API base URL、CORS origins、NEXT_PUBLIC build-time config、public domain / internal IP boundary",
    },
    {
        "candidate_id": "frontend_env_and_sentry_tunnel_runtime_config",
        "title": "Frontend env / Sentry tunnel / browser runtime config",
        "control_tier": "C0",
        "risk": "HIGH",
        "source_refs": [
            "apps/web/src/middleware.ts",
            "apps/web/src/app/api/sentry-tunnel/route.ts",
            "apps/web/src/app/api/health/route.ts",
        ],
        "affected_scope": "Next.js middleware、Sentry tunnel、browser-facing env、health route 與 console error boundary",
    },
    {
        "candidate_id": "webhook_callback_and_notification_runtime_config",
        "title": "Webhook / callback / notification runtime route",
        "control_tier": "C0",
        "risk": "HIGH",
        "source_refs": [
            "apps/api/src/models/webhook.py",
            "apps/api/src/routers/proposals.py",
            "apps/api/src/core/deep_linking.py",
        ],
        "affected_scope": "webhook callback、proposal route、deep link、notification route 與 external send boundary",
    },
    {
        "candidate_id": "cross_product_runtime_route_scope",
        "title": "Cross-product runtime route scope",
        "control_tier": "C1",
        "risk": "MEDIUM",
        "source_refs": [
            "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md",
            "docs/security/VIBEWORK-IWOOOS-ONBOARDING-HANDOFF.md",
            "docs/security/AGENT-BOUNTY-IWOOOS-ONBOARDING-HANDOFF.md",
            "docs/security/AGENT-BOUNTY-OWNER-REQUEST-DRAFT.md",
        ],
        "affected_scope": "VibeWork、agent-bounty-protocol、StockPlatform、官方形象網站、藥局網站與其他產品 runtime route scope",
    },
]


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 source_ref_exists(root: Path, ref: str) -> bool:
    return (root / ref).exists()


def build_candidate(root: Path, definition: dict[str, Any]) -> dict[str, Any]:
    source_refs = list(definition["source_refs"])
    return {
        "change_evidence_candidate_id": f"public_runtime_config_change_evidence:{definition['candidate_id']}",
        "status": "waiting_change_evidence",
        "title": definition["title"],
        "control_tier": definition["control_tier"],
        "risk": definition["risk"],
        "source_refs": source_refs,
        "source_ref_existing_count": sum(1 for ref in source_refs if source_ref_exists(root, ref)),
        "write_capable": True,
        "requires_runtime_approval_package": True,
        "affected_scope": definition["affected_scope"],
        "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_runtime_config_change_ref": None,
        "affected_route_refs": [],
        "public_url_or_domain_ref": None,
        "admin_auth_boundary_ref": None,
        "api_contract_readback_ref": None,
        "cors_origin_diff_ref": None,
        "frontend_env_diff_ref": None,
        "i18n_redaction_review_ref": None,
        "webhook_callback_owner_ref": None,
        "desktop_mobile_smoke_ref": None,
        "api_health_readback_ref": None,
        "sensitive_string_scan_ref": None,
        "console_error_scan_ref": None,
        "blast_radius": "pending_change_evidence",
        "maintenance_window": "pending_change_evidence",
        "rollback_owner": "pending_change_evidence",
        "rollback_plan_ref": None,
        "postcheck_evidence_ref": None,
        "redacted_evidence_refs": [],
        "reviewer_outcome": "waiting_change_evidence",
        "not_approval": True,
        "change_evidence_received": False,
        "change_evidence_accepted": False,
        "change_evidence_rejected": False,
        "change_evidence_quarantined": False,
        "route_scope_accepted": False,
        "admin_auth_boundary_accepted": False,
        "api_contract_readback_accepted": False,
        "cors_origin_diff_accepted": False,
        "frontend_env_diff_accepted": False,
        "i18n_redaction_review_accepted": False,
        "webhook_callback_owner_accepted": False,
        "desktop_mobile_smoke_accepted": False,
        "sensitive_string_scan_accepted": False,
        "postcheck_evidence_accepted": False,
        "runtime_approval_package_ready": False,
        "runtime_gate": False,
        **{
            key: value
            for key, value in EXECUTION_BOUNDARIES.items()
            if key != "not_authorization"
        },
    }


def build_report(root: Path, generated_at: str | None) -> dict[str, Any]:
    report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
    candidates = [build_candidate(root, item) for item in CANDIDATE_DEFINITIONS]
    c0_candidates = [item for item in candidates if item["control_tier"] == "C0"]
    c1_candidates = [item for item in candidates if item["control_tier"] == "C1"]
    source_refs = sorted({ref for item in candidates for ref in item["source_refs"]})

    return {
        "schema_version": "public_runtime_config_change_evidence_acceptance_v1",
        "generated_at": report_time,
        "git_commit": git_short_sha(root),
        "status": "change_evidence_acceptance_ledger_ready_no_runtime_action",
        "mode": "metadata_only_no_secret_no_route_change_no_deploy",
        "source_paths": [
            "docs/HARD_RULES.md",
            "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md",
            *source_refs,
        ],
        "summary": {
            "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": sum(1 for item in candidates if item["write_capable"]),
            "source_ref_count": len(source_refs),
            "existing_source_ref_count": sum(1 for ref in source_refs if source_ref_exists(root, ref)),
            "required_evidence_field_count": len(REQUIRED_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,
            "route_scope_accepted_count": 0,
            "admin_auth_boundary_accepted_count": 0,
            "api_contract_readback_accepted_count": 0,
            "cors_origin_diff_accepted_count": 0,
            "frontend_env_diff_accepted_count": 0,
            "i18n_redaction_review_accepted_count": 0,
            "webhook_callback_owner_accepted_count": 0,
            "desktop_mobile_smoke_accepted_count": 0,
            "sensitive_string_scan_accepted_count": 0,
            "postcheck_evidence_accepted_count": 0,
            "runtime_approval_package_ready_count": 0,
            "runtime_gate_count": 0,
            "action_button_count": 0,
            "public_admin_api_runtime_config_coverage_percent_before_acceptance": 62,
            "public_admin_api_runtime_config_coverage_percent_after_acceptance": 64,
        },
        "execution_boundaries": EXECUTION_BOUNDARIES,
        "change_evidence_fields": CHANGE_EVIDENCE_FIELDS,
        "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS,
        "reviewer_checks": REVIEWER_CHECKS,
        "outcome_lanes": OUTCOME_LANES,
        "blocked_actions": BLOCKED_ACTIONS,
        "change_evidence_candidates": candidates,
        "operator_interpretation": [
            "此帳本只描述 public/admin/API/frontend runtime config 變更證據如何收件與拒收，不是 route、CORS、env、auth 或 webhook 變更批准。",
            "前台、public API、HTML、bundle 與 messages 不得顯示 raw owner namespace、repo slug、內部狀態碼、內部對話或 secret value。",
            "desktop/mobile smoke、API health、CD success、UI 可見與 AwoooP approval 都不能被解讀成 runtime gate 已開。",
            "未來若要修改 public/admin/API route、CORS、NEXT_PUBLIC env、middleware auth、callback 或 webhook，仍需獨立 owner response、維護窗口、rollback owner 與 runtime approval package。",
        ],
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Public / Admin / API runtime config 變更證據驗收只讀帳本")
    parser.add_argument("--root", default=".", help="repo root")
    parser.add_argument("--output", help="寫出 JSON 報告")
    parser.add_argument("--generated-at", help="固定報告時間，供 committed snapshot 使用")
    args = parser.parse_args()

    root = Path(args.root).resolve()
    report = build_report(root, 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(
        "PUBLIC_RUNTIME_CONFIG_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__":
    raise SystemExit(main())
