#!/usr/bin/env python3
"""
IwoooS SSH / network access repo-only 清冊。

本工具只讀取已提交的 repo 檔案，整理 SSH target、known_hosts policy、
sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 SSH action rule。
它不 SSH、不 keyscan、不讀 live firewall、不套用 NetworkPolicy、不改
NodePort、不啟動 WireGuard、不執行 sudo 或任何主機命令。
"""

from __future__ import annotations

import argparse
import hashlib
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))


SURFACES: list[dict[str, Any]] = [
    {
        "surface_id": "ansible_inventory_ssh_targets",
        "label": "Ansible inventory SSH targets",
        "source_path": "infra/ansible/inventory/hosts.yml",
        "expected_scope": "110_111_112_120_121_188",
        "config_kind": "ssh_target_inventory",
        "control_tier": "C1",
        "current_state": "repo_source_visible_needs_pinned_host_key_policy",
        "access_scope": ["192.168.0.110", "192.168.0.111", "192.168.0.112", "192.168.0.120", "192.168.0.121", "192.168.0.188"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 host owner、pinned known_hosts disposition、ProxyJump policy、key owner 與 rollback owner。",
    },
    {
        "surface_id": "ansible_common_ssh_args",
        "label": "Ansible common SSH host key policy",
        "source_path": "infra/ansible/inventory/group_vars/all.yml",
        "expected_scope": "multi_host",
        "config_kind": "ssh_client_policy",
        "control_tier": "C1",
        "current_state": "accept_new_policy_visible_needs_owner_disposition",
        "access_scope": ["StrictHostKeyChecking=accept-new", "ConnectTimeout=10"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "確認 accept-new 是否只限 bootstrap，補升級 pinned known_hosts 的 owner 與時間窗。",
    },
    {
        "surface_id": "gitea_cd_known_hosts_secret",
        "label": "Gitea CD repair known_hosts secret",
        "source_path": ".gitea/workflows/cd.yaml",
        "expected_scope": "110_120_121_188_known_hosts",
        "config_kind": "known_hosts_secret_workflow",
        "control_tier": "C1",
        "current_state": "repo_guard_visible_live_secret_not_verified",
        "access_scope": ["192.168.0.110", "192.168.0.120", "192.168.0.121", "192.168.0.188"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 owner-provided known_hosts secret metadata、缺 120 時的處置、key rotation owner 與失敗通知 owner。",
    },
    {
        "surface_id": "gitea_cd_deploy_ssh",
        "label": "Gitea CD K8s deploy SSH path",
        "source_path": ".gitea/workflows/cd.yaml",
        "expected_scope": "k8s_ssh_host",
        "config_kind": "ci_deploy_ssh",
        "control_tier": "C1",
        "current_state": "write_capable_deploy_path_visible_gate_closed",
        "access_scope": ["K8S_SSH_HOST", "deploy_key", "kubectl apply", "ArgoCD sync"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 deploy SSH host owner、maintenance window、rollback owner、post-check 指標與 break-glass policy。",
    },
    {
        "surface_id": "gitea_cd_dev_ssh",
        "label": "Gitea CD dev deploy SSH path",
        "source_path": ".gitea/workflows/cd-dev.yaml",
        "expected_scope": "192.168.0.120",
        "config_kind": "ci_deploy_ssh",
        "control_tier": "C1",
        "current_state": "dev_write_capable_deploy_path_visible_gate_closed",
        "access_scope": ["192.168.0.120", "deploy_key", "kubectl apply"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "確認 dev deploy key scope、host key policy、rollback owner 與 dev/prod 邊界。",
    },
    {
        "surface_id": "deploy_alerts_ssh_path",
        "label": "Deploy alerts SSH path",
        "source_path": ".gitea/workflows/deploy-alerts.yaml",
        "expected_scope": "192.168.0.110",
        "config_kind": "ci_deploy_ssh",
        "control_tier": "C1",
        "current_state": "write_capable_alert_deploy_path_visible_gate_closed",
        "access_scope": ["192.168.0.110", "deploy alert scripts"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 alert deploy owner、known_hosts pinning、rollback owner、post-check 與通知路徑。",
    },
    {
        "surface_id": "monitoring_discover_docker_ssh",
        "label": "Monitoring Docker discovery SSH scanner",
        "source_path": "ops/monitoring/discover_docker.py",
        "expected_scope": "110_188_docker_hosts",
        "config_kind": "ssh_discovery_script",
        "control_tier": "C1",
        "current_state": "accept_new_scanner_visible_needs_read_only_gate",
        "access_scope": ["192.168.0.110", "192.168.0.188", "docker ps"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 scanner 執行 owner、read-only window、pinned known_hosts、輸出脫敏與失敗處置。",
    },
    {
        "surface_id": "monitoring_exporter_deploy_ssh",
        "label": "Monitoring exporter deploy SSH script",
        "source_path": "ops/monitoring/deploy-exporters.sh",
        "expected_scope": "192.168.0.188",
        "config_kind": "monitoring_ssh_deploy_script",
        "control_tier": "C1",
        "current_state": "write_capable_script_visible_gate_closed",
        "access_scope": ["192.168.0.188", "scp", "docker compose up -d"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 exporter deploy owner、maintenance window、rollback owner、host key policy 與 post-check 指標。",
    },
    {
        "surface_id": "backup_config_ssh_capture",
        "label": "Backup config SSH capture",
        "source_path": "scripts/backup/backup-configs.sh",
        "expected_scope": "110_188_120_121_cluster",
        "config_kind": "ssh_backup_capture",
        "control_tier": "C1",
        "current_state": "read_capable_capture_visible_not_executed",
        "access_scope": ["/etc/ssh", "/etc/nginx", "systemd", "docker", "k8s"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 backup execution owner、secret redaction proof、retention owner 與 restore validation。",
    },
    {
        "surface_id": "host_ops_sudoers_wrapper",
        "label": "Host ops sudoers wrapper",
        "source_path": "scripts/host-ops/awoooi-wrapper.sudoers",
        "expected_scope": "host_ops_minimal_sudo",
        "config_kind": "sudoers_policy",
        "control_tier": "C1",
        "current_state": "sudoers_source_visible_needs_live_owner_evidence",
        "access_scope": ["awoooi-hosts-add", "docker kill SIGHUP", "promtool", "amtool"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 live sudoers hash、visudo validation、command owner、rollback owner 與 forbidden command proof。",
    },
    {
        "surface_id": "k8s_prod_network_policy",
        "label": "K8s production NetworkPolicy",
        "source_path": "k8s/awoooi-prod/02-network-policy.yaml",
        "expected_scope": "awoooi_prod_namespace",
        "config_kind": "k8s_network_policy",
        "control_tier": "C1",
        "current_state": "repo_policy_visible_needs_live_cluster_diff",
        "access_scope": ["default deny", "ingress", "egress", "SSH egress", "Ollama", "monitoring"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 live NetworkPolicy diff、ingress / egress owner、rollback owner 與 route smoke。",
    },
    {
        "surface_id": "argocd_metrics_network_policy",
        "label": "ArgoCD metrics NetworkPolicy",
        "source_path": "k8s/argocd/argocd-metrics-network-policy.yaml",
        "expected_scope": "argocd_namespace",
        "config_kind": "k8s_network_policy",
        "control_tier": "C1",
        "current_state": "repo_policy_visible_needs_prometheus_owner_confirmation",
        "access_scope": ["192.168.0.188", "argocd metrics", "192.168.0.0/24 UI"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 Prometheus scrape owner、NodePort exposure owner、live policy diff 與 rollback owner。",
    },
    {
        "surface_id": "argocd_metrics_nodeport",
        "label": "ArgoCD metrics NodePort",
        "source_path": "k8s/argocd/argocd-metrics-nodeport.yaml",
        "expected_scope": "argocd_nodeport_30882_30883",
        "config_kind": "k8s_nodeport_service",
        "control_tier": "C1",
        "current_state": "nodeport_source_visible_needs_exposure_review",
        "access_scope": ["nodePort 30882", "nodePort 30883"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 NodePort exposure owner、firewall owner、Prometheus source whitelist 與 rollback owner。",
    },
    {
        "surface_id": "velero_metrics_nodeport",
        "label": "Velero metrics NodePort",
        "source_path": "k8s/velero/velero-metrics-service.yaml",
        "expected_scope": "velero_nodeport_30885",
        "config_kind": "k8s_nodeport_service",
        "control_tier": "C1",
        "current_state": "nodeport_source_visible_needs_exposure_review",
        "access_scope": ["nodePort 30885", "backup metrics"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 Velero metrics exposure owner、firewall owner、Prometheus source whitelist 與 rollback owner。",
    },
    {
        "surface_id": "wireguard_mesh_runbook",
        "label": "GCP Ollama WireGuard mesh runbook",
        "source_path": "docs/runbooks/GCP-OLLAMA-WIREGUARD-MESH.md",
        "expected_scope": "110_111_120_121_gcp_a_gcp_b",
        "config_kind": "wireguard_runbook",
        "control_tier": "C1",
        "current_state": "target_architecture_documented_not_applied",
        "access_scope": ["10.77.114.0/24", "51820/udp", "GCP-A", "GCP-B"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 WireGuard owner、public-key metadata、firewall rule owner、canary plan、rollback owner 與 cutover gate。",
    },
    {
        "surface_id": "alert_rules_ssh_actions",
        "label": "Alert rules SSH action surface",
        "source_path": "apps/api/alert_rules.yaml",
        "expected_scope": "ssh_mcp_action_catalog",
        "config_kind": "alert_ssh_action_rules",
        "control_tier": "C1",
        "current_state": "ssh_action_catalog_visible_gate_closed",
        "access_scope": ["ssh_diagnose", "docker restart", "systemctl restart", "docker compose", "docker prune"],
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 action owner、read/write/admin 分級、approval gate、cooldown、rollback owner 與 post-check 指標。",
    },
]


FALSE_BOUNDARIES = {
    "runtime_execution_authorized": False,
    "host_write_authorized": False,
    "ssh_read_authorized": False,
    "ssh_write_authorized": False,
    "sudo_action_authorized": False,
    "firewall_change_authorized": False,
    "network_policy_apply_authorized": False,
    "nodeport_change_authorized": False,
    "wireguard_change_authorized": False,
    "known_hosts_patch_authorized": False,
    "host_keyscan_authorized": False,
    "live_host_read_authorized": False,
    "secret_value_collection_allowed": False,
    "ssh_key_collection_allowed": False,
    "active_scan_authorized": False,
    "action_buttons_allowed": False,
}


WRITE_CAPABLE_KINDS = {
    "ci_deploy_ssh",
    "monitoring_ssh_deploy_script",
    "sudoers_policy",
    "alert_ssh_action_rules",
}


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 file_metadata(root: Path, source_path: str) -> dict[str, Any]:
    path = root / source_path
    exists = path.exists()
    if not exists:
        return {"source_exists": False, "line_count": 0, "sha256": None}
    content = path.read_bytes()
    return {
        "source_exists": True,
        "line_count": len(content.decode("utf-8", errors="replace").splitlines()),
        "sha256": hashlib.sha256(content).hexdigest(),
    }


def build_surface(root: Path, surface: dict[str, Any]) -> dict[str, Any]:
    metadata = file_metadata(root, surface["source_path"])
    return {
        **surface,
        **metadata,
        "owner_response_received": False,
        "owner_response_accepted": False,
        "live_evidence_received": False,
        "maintenance_window_accepted": False,
        "rollback_owner_accepted": False,
        "runtime_gate_open": False,
        "action_buttons_allowed": False,
    }


def build_report(root: Path, generated_at: str | None) -> dict[str, Any]:
    report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
    surfaces = [build_surface(root, surface) for surface in SURFACES]
    expected_scopes = sorted({surface["expected_scope"] for surface in surfaces})
    write_capable = [surface for surface in surfaces if surface["config_kind"] in WRITE_CAPABLE_KINDS]
    ssh_sources = [surface for surface in surfaces if "ssh" in surface["config_kind"] or surface["config_kind"] in {"known_hosts_secret_workflow", "sudoers_policy"}]
    network_policies = [surface for surface in surfaces if surface["config_kind"] == "k8s_network_policy"]
    nodeports = [surface for surface in surfaces if surface["config_kind"] == "k8s_nodeport_service"]
    wireguard = [surface for surface in surfaces if surface["config_kind"] == "wireguard_runbook"]

    return {
        "schema_version": "ssh_network_access_inventory_v1",
        "generated_at": report_time,
        "git_commit": git_short_sha(root),
        "status": "repo_only_inventory_ready",
        "source_scope": "committed_repo_files_only",
        "summary": {
            "surface_count": len(surfaces),
            "source_exists_count": sum(1 for surface in surfaces if surface["source_exists"]),
            "expected_scope_count": len(expected_scopes),
            "ssh_source_surface_count": len(ssh_sources),
            "network_policy_surface_count": len(network_policies),
            "nodeport_surface_count": len(nodeports),
            "sudoers_surface_count": sum(1 for surface in surfaces if surface["config_kind"] == "sudoers_policy"),
            "wireguard_surface_count": len(wireguard),
            "write_capable_surface_count": len(write_capable),
            "surfaces_requiring_owner_response_count": sum(1 for surface in surfaces if surface["requires_owner_response"]),
            "surfaces_requiring_live_evidence_count": sum(1 for surface in surfaces if surface["requires_live_evidence"]),
            "owner_response_received_count": 0,
            "owner_response_accepted_count": 0,
            "live_evidence_received_count": 0,
            "maintenance_window_accepted_count": 0,
            "rollback_owner_accepted_count": 0,
            "runtime_gate_count": 0,
            "action_button_count": 0,
            "coverage_percent_after_inventory": 54,
            "coverage_percent_before_inventory": 48,
        },
        "execution_boundaries": FALSE_BOUNDARIES,
        "expected_scopes": expected_scopes,
        "access_surfaces": surfaces,
        "write_capable_surfaces": [
            {
                "surface_id": surface["surface_id"],
                "label": surface["label"],
                "config_kind": surface["config_kind"],
                "expected_scope": surface["expected_scope"],
                "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner",
            }
            for surface in write_capable
        ],
        "next_collection_order": [
            "gitea_cd_known_hosts_secret",
            "ansible_inventory_ssh_targets",
            "host_ops_sudoers_wrapper",
            "k8s_prod_network_policy",
            "alert_rules_ssh_actions",
            "argocd_metrics_nodeport",
            "velero_metrics_nodeport",
            "wireguard_mesh_runbook",
            "monitoring_discover_docker_ssh",
            "backup_config_ssh_capture",
        ],
        "operator_interpretation": [
            "這是 repo-only SSH / network access 清冊，不是 live host、firewall 或 cluster truth。",
            "source_exists=true 只代表 repo 檔案存在；不代表 known_hosts、sudoers、NetworkPolicy、NodePort 或 WireGuard 已套用。",
            "write-capable SSH / sudoers / alert action surface 可見代表需被管控，不代表 SSH、sudo、docker、systemctl 或 kubectl 已授權。",
            "所有 live hash、pinned host key、maintenance window、rollback owner、firewall owner 與 post-check 指標都仍需 owner response。",
        ],
    }


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="產生 IwoooS SSH / network access repo-only 清冊")
    parser.add_argument("--root", default=".", help="repo root")
    parser.add_argument("--output", help="輸出 JSON 檔")
    parser.add_argument("--generated-at", help="固定 generated_at")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    root = Path(args.root).resolve()
    report = build_report(root, args.generated_at)
    text = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
    if args.output:
        Path(args.output).write_text(text + "\n", encoding="utf-8")
    else:
        print(text)
    print(
        "SSH_NETWORK_ACCESS_INVENTORY_OK "
        f"surfaces={report['summary']['surface_count']} "
        f"ssh_sources={report['summary']['ssh_source_surface_count']} "
        f"nodeports={report['summary']['nodeport_surface_count']} "
        f"runtime_gate={report['summary']['runtime_gate_count']}",
        file=sys.stderr,
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
