#!/usr/bin/env python3
"""
IwoooS Docker / systemd / host service repo-only 清冊。

本工具只讀取已提交的 repo 檔案，整理 Docker Compose、systemd/repair
白名單、Ansible service role 與 config backup coverage。它不 SSH、不讀
live host、不執行 docker compose、不執行 systemctl、不重啟任何服務。
"""

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": "local_dev_compose",
        "label": "AWOOOI local development compose",
        "source_path": "docker-compose.yml",
        "expected_host_scope": "local_dev_only",
        "config_kind": "docker_compose_source",
        "service_scope": ["web", "api", "postgres", "redis"],
        "control_tier": "C1",
        "current_state": "repo_source_visible",
        "requires_live_evidence": False,
        "requires_owner_response": True,
        "next_owner_action": "確認本檔僅供 local dev，不得作為 production compose；補 dev secret placeholder policy。",
    },
    {
        "surface_id": "monitoring_110_compose",
        "label": "110 monitoring docker compose",
        "source_path": "k8s/monitoring/docker-compose-110.yml",
        "expected_host_scope": "192.168.0.110",
        "config_kind": "docker_compose_source",
        "service_scope": ["cadvisor", "prometheus", "grafana", "blackbox-exporter", "alertmanager", "github-exporter"],
        "control_tier": "C1",
        "current_state": "repo_source_visible_with_live_drift_warning",
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 110 live compose hash、restart window、rollback owner、post-check 指標與 drift disposition。",
    },
    {
        "surface_id": "monitoring_exporters_188_compose",
        "label": "188 database exporters compose",
        "source_path": "ops/monitoring/docker-compose.exporters.yaml",
        "expected_host_scope": "192.168.0.188",
        "config_kind": "docker_compose_source",
        "service_scope": ["postgres-exporter", "redis-exporter"],
        "control_tier": "C1",
        "current_state": "repo_source_visible_needs_live_hash",
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 188 exporter compose live hash、env source policy、restart window 與 rollback owner。",
    },
    {
        "surface_id": "sentry_110_reference_compose",
        "label": "110 Sentry self-hosted reference compose",
        "source_path": "ops/sentry-self-hosted/docker-compose.yml",
        "expected_host_scope": "192.168.0.110",
        "config_kind": "docker_compose_reference",
        "service_scope": ["sentry-placeholder-reference"],
        "control_tier": "C1",
        "current_state": "reference_only_not_runtime_source",
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "確認 110 Sentry 實際 source-of-truth、official self-hosted revision、backup path 與 rollback owner。",
    },
    {
        "surface_id": "langfuse_110_compose",
        "label": "110 Langfuse compose",
        "source_path": "infra/langfuse/docker-compose.yml",
        "expected_host_scope": "192.168.0.110",
        "config_kind": "docker_compose_source",
        "service_scope": ["langfuse", "langfuse-db"],
        "control_tier": "C1",
        "current_state": "repo_source_visible_needs_secret_policy_review",
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 110 live compose hash、secret placeholder disposition、restart window 與 rollback owner。",
    },
    {
        "surface_id": "ansible_docker_compose_service_role",
        "label": "Ansible docker-compose-service role",
        "source_path": "infra/ansible/roles/docker-compose-service/tasks/main.yml",
        "expected_host_scope": "multi_host",
        "config_kind": "ansible_service_executor",
        "service_scope": ["docker compose up -d"],
        "control_tier": "C1",
        "current_state": "executor_role_visible_needs_gate_mapping",
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 role 使用範圍、allowed service_dir、check-mode plan、rollback owner 與人工批准 gate。",
    },
    {
        "surface_id": "repair_bot_110_whitelist",
        "label": "110 repair-bot compose whitelist",
        "source_path": "scripts/repair-bot/repair-bot-110.sh",
        "expected_host_scope": "192.168.0.110",
        "config_kind": "host_repair_whitelist",
        "service_scope": ["sentry", "harbor", "gitea", "gitea-runner", "langfuse", "alertmanager", "signoz"],
        "control_tier": "C1",
        "current_state": "write_capable_whitelist_visible_gate_closed",
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 authorized_keys command binding、disable switch、audit log path、rollback owner 與 post-check 指標。",
    },
    {
        "surface_id": "repair_bot_188_whitelist",
        "label": "188 repair-bot compose/systemd whitelist",
        "source_path": "scripts/repair-bot/repair-bot-188.sh",
        "expected_host_scope": "192.168.0.188",
        "config_kind": "host_repair_whitelist",
        "service_scope": ["openclaw", "minio", "signoz", "redis", "nginx", "ollama"],
        "control_tier": "C1",
        "current_state": "write_capable_whitelist_visible_gate_closed",
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 systemd restart approval gate、sudoers boundary、disable switch、rollback owner 與 route smoke。",
    },
    {
        "surface_id": "config_backup_host_capture",
        "label": "host config backup capture contract",
        "source_path": "scripts/backup/backup-configs.sh",
        "expected_host_scope": "110_188_120_121_cluster",
        "config_kind": "backup_capture_contract",
        "service_scope": ["systemd", "docker", "nginx", "cron", "k8s", "host-configs"],
        "control_tier": "C1",
        "current_state": "capture_script_visible_not_executed_by_this_inventory",
        "requires_live_evidence": True,
        "requires_owner_response": True,
        "next_owner_action": "補 latest backup status、restore drill owner、secret handling proof、retention owner 與 restore validation plan。",
    },
]


FALSE_BOUNDARIES = {
    "runtime_execution_authorized": False,
    "host_write_authorized": False,
    "ssh_read_authorized": False,
    "ssh_write_authorized": False,
    "docker_compose_action_authorized": False,
    "systemctl_action_authorized": False,
    "service_restart_authorized": False,
    "sudo_action_authorized": False,
    "live_host_read_authorized": False,
    "secret_value_collection_allowed": False,
    "active_scan_authorized": False,
    "repair_bot_execution_authorized": False,
    "ansible_apply_authorized": 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 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,
        "restart_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_hosts = sorted({surface["expected_host_scope"] for surface in surfaces})
    write_capable = [
        surface
        for surface in surfaces
        if surface["config_kind"] in {"host_repair_whitelist", "ansible_service_executor"}
    ]
    live_evidence = [surface for surface in surfaces if surface["requires_live_evidence"]]

    return {
        "schema_version": "host_service_config_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_host_scope_count": len(expected_hosts),
            "docker_compose_source_count": sum(
                1 for surface in surfaces if surface["config_kind"] in {"docker_compose_source", "docker_compose_reference"}
            ),
            "host_repair_whitelist_count": sum(1 for surface in surfaces if surface["config_kind"] == "host_repair_whitelist"),
            "systemd_restart_surface_count": 1,
            "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": len(live_evidence),
            "owner_response_received_count": 0,
            "owner_response_accepted_count": 0,
            "live_evidence_received_count": 0,
            "restart_window_accepted_count": 0,
            "rollback_owner_accepted_count": 0,
            "runtime_gate_count": 0,
            "action_button_count": 0,
            "coverage_percent_after_inventory": 50,
            "coverage_percent_before_inventory": 42,
        },
        "execution_boundaries": FALSE_BOUNDARIES,
        "expected_host_scopes": expected_hosts,
        "config_surfaces": surfaces,
        "write_capable_surfaces": [
            {
                "surface_id": surface["surface_id"],
                "label": surface["label"],
                "config_kind": surface["config_kind"],
                "expected_host_scope": surface["expected_host_scope"],
                "service_scope": surface["service_scope"],
                "required_gate": "owner_response_plus_maintenance_window_plus_rollback_owner",
            }
            for surface in write_capable
        ],
        "next_collection_order": [
            "repair_bot_110_whitelist",
            "repair_bot_188_whitelist",
            "monitoring_110_compose",
            "monitoring_exporters_188_compose",
            "langfuse_110_compose",
            "config_backup_host_capture",
            "ansible_docker_compose_service_role",
            "sentry_110_reference_compose",
            "local_dev_compose",
        ],
        "operator_interpretation": [
            "這是 repo-only 主機服務配置清冊，不是 live host 盤點。",
            "write-capable 白名單與 Ansible role 可見，不代表 repair-bot、docker compose、systemctl 或 sudo 已授權。",
            "所有 live hash、restart window、rollback owner、post-check 指標都仍需 owner response。",
            "本清冊讓 Docker/systemd 類別從 inventory_needed 進到 repo_only_inventory_ready，但 runtime gate 仍為 0。",
        ],
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="IwoooS host service repo-only config inventory")
    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(
        "HOST_SERVICE_CONFIG_INVENTORY_OK "
        f"surfaces={summary['surface_count']} "
        f"hosts={summary['expected_host_scope_count']} "
        f"write_capable={summary['write_capable_surface_count']} "
        f"runtime_gate={summary['runtime_gate_count']}",
        file=sys.stderr,
    )
    return 0


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