#!/usr/bin/env python3
"""
IwoooS public gateway 變更前置 Gate 只讀清冊。

本工具只讀取已提交的 Nginx drift snapshot 與 DNS / TLS snapshot，整理
public gateway reload / route change 前必備的 owner、diff、nginx -t、
route smoke、rollback 與維護窗口欄位。它不 SSH、不讀 live 主機、不執行
nginx -t、不 reload、不做 DNS / TLS probe，也不收 secret value。
"""

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))

REQUIRED_PREFLIGHT_GATES = [
    {
        "gate_id": "PG1",
        "label": "source-of-truth hash",
        "required_evidence": "repo_source_raw_and_normalized_sha256",
        "owner_acceptance_required": False,
        "repo_only_ready": True,
    },
    {
        "gate_id": "PG2",
        "label": "affected route list",
        "required_evidence": "domain_route_upstream_tls_admin_websocket_acme_inventory",
        "owner_acceptance_required": False,
        "repo_only_ready": True,
    },
    {
        "gate_id": "PG3",
        "label": "owner response",
        "required_evidence": "owner role/team, decision, reason, affected scope, redacted refs, followup owner",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG4",
        "label": "owner-provided live config",
        "required_evidence": "redacted live conf export or owner statement",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG5",
        "label": "rendered diff",
        "required_evidence": "source-to-live rendered diff with redaction",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG6",
        "label": "nginx -t evidence",
        "required_evidence": "syntax test output captured by approved maintainer",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG7",
        "label": "public route smoke",
        "required_evidence": "public route smoke plan and result for affected domains",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG8",
        "label": "admin route smoke",
        "required_evidence": "admin/auth route smoke when admin path is affected",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG9",
        "label": "WebSocket / API smoke",
        "required_evidence": "WebSocket or API smoke when upstream or upgrade header is affected",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG10",
        "label": "ACME / TLS owner check",
        "required_evidence": "certificate path, SAN/wildcard/shared-cert statement, ACME impact",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG11",
        "label": "maintenance window",
        "required_evidence": "approved window and notification scope",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
    {
        "gate_id": "PG12",
        "label": "rollback owner and ref",
        "required_evidence": "rollback owner, rollback file/ref, post-rollback smoke",
        "owner_acceptance_required": True,
        "repo_only_ready": False,
    },
]

EXECUTION_BOUNDARIES = {
    "runtime_execution_authorized": False,
    "host_live_conf_read_authorized": False,
    "ssh_read_authorized": False,
    "ssh_write_authorized": False,
    "host_write_authorized": False,
    "nginx_test_authorized": False,
    "nginx_test_executed": False,
    "nginx_reload_authorized": False,
    "nginx_reload_executed": False,
    "public_gateway_reload_authorized": False,
    "public_route_change_authorized": False,
    "admin_route_change_authorized": False,
    "websocket_route_change_authorized": False,
    "acme_challenge_change_authorized": False,
    "dns_query_executed": False,
    "live_tls_probe_executed": False,
    "certbot_renew_authorized": False,
    "certbot_renew_executed": False,
    "route_smoke_authorized": False,
    "route_smoke_executed": False,
    "rollback_executed": False,
    "secret_value_collection_allowed": False,
    "action_buttons_allowed": False,
}


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 unique_upstreams(domains: list[dict[str, Any]]) -> list[str]:
    return sorted({upstream for item in domains for upstream in item.get("upstreams", [])})


def build_config_rows(nginx_report: dict[str, Any]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for config in nginx_report.get("configs", []):
        parsed = config.get("repo_source", {}).get("parsed", {})
        rows.append(
            {
                "config_id": config["config_id"],
                "host": config["host"],
                "role": config["role"],
                "control_tier": config["control_tier"],
                "owner_gate": config["owner_gate"],
                "repo_source_path": config["repo_source_path"],
                "live_path": config["live_path"],
                "server_block_count": parsed.get("server_block_count", 0),
                "server_name_count": len(parsed.get("server_names", [])),
                "upstream_count": len(parsed.get("proxy_passes", [])),
                "tls_certificate_path_count": len(parsed.get("ssl_certificates", [])),
                "admin_route_count": len(parsed.get("admin_routes", [])),
                "acme_route_count": len(parsed.get("acme_routes", [])),
                "websocket_route_count": len(parsed.get("websocket_routes", [])),
                "repo_source_hash_ready": True,
                "owner_response_received": False,
                "owner_response_accepted": False,
                "live_conf_evidence_received": False,
                "rendered_diff_ready": False,
                "nginx_test_evidence_received": False,
                "route_smoke_evidence_received": False,
                "maintenance_window_accepted": False,
                "rollback_owner_accepted": False,
                "runtime_gate_open": False,
            }
        )
    return rows


def build_route_impacts(domains: list[dict[str, Any]]) -> list[dict[str, Any]]:
    impacts: list[dict[str, Any]] = []
    for item in domains:
        impacts.append(
            {
                "domain": item["domain"],
                "hosts": item.get("hosts", []),
                "config_ids": item.get("config_ids", []),
                "control_tier": item.get("control_tier", "C3"),
                "upstream_count": len(item.get("upstreams", [])),
                "has_tls_certificate_path": item.get("tls_certificate_path_present", False),
                "certificate_owner_confirmation_required": item.get(
                    "certificate_owner_confirmation_required",
                    False,
                ),
                "acme_challenge_present": item.get("acme_challenge_present", False),
                "admin_route_count": item.get("admin_route_count", 0),
                "websocket_route_count": item.get("websocket_route_count", 0),
                "public_route_smoke_required": True,
                "admin_route_smoke_required": item.get("admin_route_count", 0) > 0,
                "websocket_or_api_smoke_required": item.get("websocket_route_count", 0) > 0
                or len(item.get("upstreams", [])) > 0,
                "tls_owner_check_required": item.get("certificate_owner_confirmation_required", False),
                "owner_response_accepted": False,
                "route_smoke_accepted": False,
            }
        )
    return impacts


def build_report(
    root: Path,
    nginx_report: dict[str, Any],
    domain_tls_report: dict[str, Any],
    generated_at: str | None,
) -> dict[str, Any]:
    report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
    managed_domains = domain_tls_report.get("managed_domains", [])
    config_rows = build_config_rows(nginx_report)
    route_impacts = build_route_impacts(managed_domains)
    upstreams = unique_upstreams(managed_domains)
    repo_only_ready_count = sum(1 for gate in REQUIRED_PREFLIGHT_GATES if gate["repo_only_ready"])
    owner_required_count = sum(1 for gate in REQUIRED_PREFLIGHT_GATES if gate["owner_acceptance_required"])

    summary = {
        "source_config_count": len(config_rows),
        "c0_source_config_count": sum(1 for item in config_rows if item["control_tier"] == "C0"),
        "managed_domain_count": len(managed_domains),
        "route_impact_count": len(route_impacts),
        "unique_upstream_count": len(upstreams),
        "tls_certificate_path_count": domain_tls_report.get("summary", {}).get(
            "unique_certificate_path_count",
            0,
        ),
        "certificate_owner_confirmation_required_count": domain_tls_report.get("summary", {}).get(
            "certificate_owner_confirmation_required_count",
            0,
        ),
        "admin_route_domain_count": domain_tls_report.get("summary", {}).get("admin_route_domain_count", 0),
        "websocket_route_domain_count": domain_tls_report.get("summary", {}).get(
            "websocket_route_domain_count",
            0,
        ),
        "acme_challenge_domain_count": domain_tls_report.get("summary", {}).get(
            "acme_challenge_domain_count",
            0,
        ),
        "preflight_gate_count": len(REQUIRED_PREFLIGHT_GATES),
        "repo_only_preflight_ready_count": repo_only_ready_count,
        "owner_acceptance_required_gate_count": owner_required_count,
        "preflight_gate_accepted_count": 0,
        "owner_response_received_count": 0,
        "owner_response_accepted_count": 0,
        "owner_provided_live_conf_received_count": 0,
        "rendered_diff_ready_count": 0,
        "nginx_test_evidence_count": 0,
        "route_smoke_evidence_count": 0,
        "maintenance_window_accepted_count": 0,
        "rollback_owner_accepted_count": 0,
        "runtime_gate_count": 0,
        "action_button_count": 0,
        "coverage_percent_before_preflight": 78,
        "coverage_percent_after_preflight": 84,
    }

    return {
        "schema_version": "public_gateway_preflight_inventory_v1",
        "generated_at": report_time,
        "status": "repo_only_preflight_contract_ready",
        "source_scope": "committed_nginx_and_domain_tls_snapshots_only",
        "git_commit": git_short_sha(root),
        "source_reports": [
            "docs/security/nginx-config-drift-repo.snapshot.json",
            "docs/security/domain-tls-certbot-inventory.snapshot.json",
        ],
        "summary": summary,
        "execution_boundaries": EXECUTION_BOUNDARIES,
        "required_preflight_gates": REQUIRED_PREFLIGHT_GATES,
        "config_preflight_rows": config_rows,
        "route_impacts": route_impacts,
        "unique_upstreams": upstreams,
        "required_owner_fields": [
            "owner_role_or_team",
            "decision",
            "decision_reason",
            "affected_scope",
            "redacted_evidence_refs",
            "followup_owner",
            "rollback_owner",
            "maintenance_window",
            "validation_plan",
            "nginx_test_evidence_ref",
            "route_smoke_evidence_ref",
        ],
        "next_collection_order": [
            "public_gateway_owner_response",
            "owner_provided_live_conf_export",
            "source_to_live_rendered_diff",
            "nginx_t_evidence",
            "affected_public_route_smoke",
            "admin_route_smoke_if_affected",
            "websocket_or_api_smoke_if_affected",
            "tls_certificate_owner_confirmation",
            "maintenance_window",
            "rollback_owner_and_ref",
        ],
        "operator_interpretation": [
            "這是 Nginx public gateway 變更前置控管清冊，不是 reload 授權。",
            "repo-only ready 只代表 source hash 與 affected route list 已能重跑產生。",
            "owner response、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback owner 全部仍為 0。",
            "若未來發現 live drift，只能建立 evidence 與 owner decision，不得自動覆寫 live。",
        ],
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="IwoooS public gateway 變更前置 Gate 只讀清冊")
    parser.add_argument("--root", default=".", help="repo root")
    parser.add_argument(
        "--nginx-report",
        default="docs/security/nginx-config-drift-repo.snapshot.json",
        help="Nginx repo-only drift detector snapshot",
    )
    parser.add_argument(
        "--domain-tls-report",
        default="docs/security/domain-tls-certbot-inventory.snapshot.json",
        help="DNS / TLS / certbot repo-only inventory snapshot",
    )
    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,
        load_json(root / args.nginx_report),
        load_json(root / args.domain_tls_report),
        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_GATEWAY_PREFLIGHT_INVENTORY_OK "
        f"configs={summary['source_config_count']} "
        f"routes={summary['route_impact_count']} "
        f"gates={summary['preflight_gate_count']} "
        f"runtime_gate={summary['runtime_gate_count']}",
        file=sys.stderr,
    )
    return 0


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