#!/usr/bin/env python3
"""Summarize credential escrow intake readiness without opening runtime gates.

Read-only by design. This script consumes sanitized artifacts such as the
post-reboot summary, owner packet, placeholder/owner response, offsite report,
and escrow marker status. It never reads secret values, writes credential
markers, sends owner requests, or modifies host/runtime state.
"""

from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[2]
OWNER_PACKET_GENERATOR = (
    ROOT / "scripts" / "reboot-recovery" / "post-reboot-next-gate-owner-packets.py"
)
OWNER_RESPONSE_PREFLIGHT = (
    ROOT / "scripts" / "reboot-recovery" / "post-reboot-owner-response-preflight.py"
)

EXPECTED_OWNER_PACKET_SCHEMA = "awoooi_post_reboot_next_gate_owner_packets_v1"
RESPONSE_SCHEMA = "awoooi_post_reboot_next_gate_owner_response_v1"
ESCROW_GATE_ID = "credential_escrow_evidence"
ESCROW_ITEM_IDS = {
    "restic_repository_password",
    "offsite_provider_credentials",
    "break_glass_admin_credentials",
    "dns_registrar_recovery",
    "oauth_ai_provider_recovery",
}
FORBIDDEN_TRUE_FIELDS = {
    "runtime_action_requested",
    "runtime_action_authorized",
    "host_write_requested",
    "host_write_authorized",
    "secret_value_included",
    "secret_value_collection_allowed",
    "credential_marker_write_requested",
    "credential_marker_write_authorized",
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Read credential escrow intake artifacts into a no-secret scorecard.",
    )
    parser.add_argument("--summary-file", type=Path, help="Post-reboot readiness summary.")
    parser.add_argument("--owner-packet-file", type=Path, help="Post-reboot owner packet JSON.")
    parser.add_argument("--response-file", type=Path, help="Owner response or placeholder JSON.")
    parser.add_argument("--offsite-report-file", type=Path, help="offsite-escrow-evidence-report output.")
    parser.add_argument("--escrow-status-file", type=Path, help="mark-credential-escrow-verified --status output.")
    parser.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
    parser.add_argument("--no-color", action="store_true", help="Accepted for command symmetry; output is plain text.")
    return parser.parse_args()


def read_text(path: Path | None) -> str:
    if not path:
        return ""
    try:
        return path.read_text(encoding="utf-8")
    except FileNotFoundError as exc:
        raise SystemExit(f"artifact_not_found={path}") from exc


def load_json(path: Path, label: str) -> dict[str, Any]:
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError as exc:
        raise SystemExit(f"{label}_not_found={path}") from exc
    except json.JSONDecodeError as exc:
        raise SystemExit(f"{label}_json_invalid={exc}") from exc
    if not isinstance(payload, dict):
        raise SystemExit(f"{label}_json_not_object")
    return payload


def parse_key_values(text: str) -> dict[str, str]:
    values: dict[str, str] = {}
    for raw_line in text.splitlines():
        line = raw_line.strip()
        if not line or "=" not in line:
            continue
        key, value = line.split("=", 1)
        key = key.strip()
        if re.fullmatch(r"[A-Z0-9_]+", key):
            values[key] = value.strip()
    return values


def split_csv(value: str | None) -> list[str]:
    if not value or value == "none":
        return []
    return [item.strip() for item in value.split(",") if item.strip()]


def as_list(value: Any) -> list[Any]:
    if value is None:
        return []
    if isinstance(value, list):
        return value
    return [value]


def as_int(value: Any) -> int | None:
    if value is None:
        return None
    try:
        return int(str(value))
    except (TypeError, ValueError):
        return None


def int_or_unknown(value: int | None) -> int | str:
    return value if value is not None else "unknown"


def bool_as_int(value: Any) -> int:
    return 1 if value is True else 0


def load_owner_packet(args: argparse.Namespace) -> dict[str, Any]:
    if args.owner_packet_file:
        return load_json(args.owner_packet_file, "owner_packet_file")
    if not args.summary_file:
        return {}
    cmd = [
        str(OWNER_PACKET_GENERATOR),
        "--no-color",
        "--summary-file",
        str(args.summary_file),
    ]
    completed = subprocess.run(
        cmd,
        cwd=ROOT,
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )
    if completed.returncode != 0:
        raise SystemExit(
            "owner_packet_generation_failed "
            f"rc={completed.returncode}\n{completed.stdout}"
        )
    try:
        packet = json.loads(completed.stdout)
    except json.JSONDecodeError as exc:
        raise SystemExit(f"owner_packet_json_invalid={exc}") from exc
    if not isinstance(packet, dict):
        raise SystemExit("owner_packet_json_not_object")
    return packet


def owner_packet_gate_ids(packet: dict[str, Any]) -> list[str]:
    if not packet:
        return []
    if packet.get("schema_version") != EXPECTED_OWNER_PACKET_SCHEMA:
        raise SystemExit(f"owner_packet_schema={packet.get('schema_version')!r}")
    return [
        str(item.get("packet_id"))
        for item in as_list(packet.get("owner_packets"))
        if isinstance(item, dict) and item.get("packet_id")
    ]


def credential_required_items(packet: dict[str, Any]) -> set[str]:
    for item in as_list(packet.get("owner_packets")):
        if not isinstance(item, dict) or item.get("packet_id") != ESCROW_GATE_ID:
            continue
        return {
            str(raw_item)
            for raw_item in as_list(item.get("required_items"))
            if str(raw_item) in ESCROW_ITEM_IDS
        }
    return set()


def response_gate_ids(response: dict[str, Any]) -> list[str]:
    if not response:
        return []
    if response.get("schema_version") != RESPONSE_SCHEMA:
        raise SystemExit(f"response_schema={response.get('schema_version')!r}")
    return [
        str(item.get("gate_id"))
        for item in as_list(response.get("responses"))
        if isinstance(item, dict) and item.get("gate_id")
    ]


def count_true_fields(value: Any) -> dict[str, int]:
    counts = {key: 0 for key in FORBIDDEN_TRUE_FIELDS}
    if isinstance(value, dict):
        for key, child in value.items():
            if key in counts and child is not False:
                counts[key] += 1
            child_counts = count_true_fields(child)
            for child_key, child_value in child_counts.items():
                counts[child_key] += child_value
    elif isinstance(value, list):
        for child in value:
            child_counts = count_true_fields(child)
            for child_key, child_value in child_counts.items():
                counts[child_key] += child_value
    return counts


def run_preflight(args: argparse.Namespace) -> dict[str, Any]:
    if not args.response_file:
        return {}
    cmd = [str(OWNER_RESPONSE_PREFLIGHT), "--json", "--no-color"]
    if args.owner_packet_file:
        cmd.extend(["--owner-packet-file", str(args.owner_packet_file)])
    elif args.summary_file:
        cmd.extend(["--summary-file", str(args.summary_file)])
    else:
        return {}
    cmd.extend(["--response-file", str(args.response_file)])
    completed = subprocess.run(
        cmd,
        cwd=ROOT,
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )
    if completed.returncode != 0:
        raise SystemExit(
            "owner_response_preflight_failed "
            f"rc={completed.returncode}\n{completed.stdout}"
        )
    try:
        result = json.loads(completed.stdout)
    except json.JSONDecodeError as exc:
        raise SystemExit(f"owner_response_preflight_json_invalid={exc}") from exc
    if not isinstance(result, dict):
        raise SystemExit("owner_response_preflight_json_not_object")
    return result


def parse_escrow_status(text: str) -> dict[str, int]:
    seen = 0
    missing = 0
    present = 0
    for raw_line in text.splitlines():
        parts = raw_line.strip().split()
        if len(parts) < 2 or parts[0] not in ESCROW_ITEM_IDS:
            continue
        seen += 1
        state = parts[1].lower()
        if state == "missing":
            missing += 1
        elif state in {"present", "verified", "fresh", "ok"}:
            present += 1
    return {"seen": seen, "missing": missing, "present": present}


def first_known(*values: int | None) -> int | None:
    for value in values:
        if value is not None:
            return value
    return None


def evaluate(args: argparse.Namespace) -> dict[str, Any]:
    summary = parse_key_values(read_text(args.summary_file))
    offsite = parse_key_values(read_text(args.offsite_report_file))
    escrow_status = parse_escrow_status(read_text(args.escrow_status_file))
    packet = load_owner_packet(args)
    response = load_json(args.response_file, "response_file") if args.response_file else {}
    preflight = run_preflight(args)

    summary_gates = split_csv(summary.get("NEXT_REQUIRED_GATES"))
    packet_gates = owner_packet_gate_ids(packet)
    response_gates = response_gate_ids(response)
    unexpected_packet_gates = sorted(set(packet_gates) - {ESCROW_GATE_ID})
    unexpected_response_gates = sorted(set(response_gates) - set(packet_gates or summary_gates))

    required_items = credential_required_items(packet)
    missing_required_items = sorted(ESCROW_ITEM_IDS - required_items) if ESCROW_GATE_ID in packet_gates else []

    summary_missing = as_int(summary.get("ESCROW_MISSING_COUNT"))
    offsite_missing = first_known(
        as_int(offsite.get("MISSING_ESCROW_MARKER_COUNT")),
        as_int(offsite.get("ESCROW_MISSING_COUNT")),
    )
    status_missing = escrow_status["missing"] if escrow_status["seen"] else None
    effective_missing = first_known(offsite_missing, summary_missing, status_missing)

    true_counts = count_true_fields(response)
    forbidden_true_total = sum(true_counts.values())
    preflight_status = str(preflight.get("status", "not_run"))
    preflight_blockers = as_list(preflight.get("blockers"))
    active_gate_present = ESCROW_GATE_ID in set(summary_gates or packet_gates)

    if not active_gate_present:
        status = "not_required_current_summary"
        next_step = "rerun_post_reboot_summary_when_next_required_gates_change"
    elif forbidden_true_total:
        status = "blocked_forbidden_runtime_or_marker_request"
        next_step = "strip_runtime_secret_host_write_or_marker_write_fields_before_preflight"
    elif unexpected_packet_gates or unexpected_response_gates:
        status = "blocked_owner_packet_or_response_gate_mismatch"
        next_step = "regenerate_owner_packet_and_response_template_from_same_summary"
    elif preflight_status == "ready_for_independent_reviewer_acceptance":
        status = "ready_for_independent_reviewer_acceptance"
        if effective_missing == 0:
            next_step = "rerun_post_reboot_summary_to_close_dr_gate"
        else:
            next_step = "independent_reviewer_acceptance_then_marker_dry_run"
    else:
        status = "blocked_waiting_non_secret_credential_escrow_evidence"
        next_step = "collect_redacted_non_secret_evidence_refs_then_rerun_preflight"

    result = {
        "schema_version": "awoooi_post_reboot_credential_escrow_intake_scorecard_v1",
        "status": status,
        "next_step": next_step,
        "active_gate_present": active_gate_present,
        "summary_next_required_gates": summary_gates,
        "owner_packet_gate_count": len(packet_gates),
        "owner_packet_gates": packet_gates,
        "unexpected_owner_packet_gate_count": len(unexpected_packet_gates),
        "unexpected_owner_packet_gates": unexpected_packet_gates,
        "response_gate_count": len(response_gates),
        "response_gates": response_gates,
        "unexpected_response_gate_count": len(unexpected_response_gates),
        "unexpected_response_gates": unexpected_response_gates,
        "required_item_count": len(required_items),
        "missing_required_item_count": len(missing_required_items),
        "missing_required_items": missing_required_items,
        "summary_escrow_missing_count": int_or_unknown(summary_missing),
        "offsite_escrow_missing_count": int_or_unknown(offsite_missing),
        "escrow_status_seen_count": escrow_status["seen"],
        "escrow_status_missing_count": int_or_unknown(status_missing),
        "effective_escrow_missing_count": int_or_unknown(effective_missing),
        "script_missing_count": int_or_unknown(as_int(offsite.get("SCRIPT_MISSING_COUNT"))),
        "offsite_configured": int_or_unknown(as_int(offsite.get("OFFSITE_CONFIGURED"))),
        "rclone_configured": int_or_unknown(as_int(offsite.get("RCLONE_CONFIGURED"))),
        "preflight_status": preflight_status,
        "preflight_blocker_count": len(preflight_blockers),
        "owner_response_received_count": preflight.get("owner_response_received_count", 0),
        "owner_response_accepted_count": preflight.get("owner_response_accepted_count", 0),
        "runtime_gate_count": preflight.get("runtime_gate_count", 0),
        "runtime_action_authorized": bool_as_int(preflight.get("runtime_action_authorized")),
        "host_write_authorized": bool_as_int(preflight.get("host_write_authorized")),
        "secret_value_collection_allowed": bool_as_int(preflight.get("secret_value_collection_allowed")),
        "runtime_action_requested_count": true_counts["runtime_action_requested"],
        "host_write_requested_count": true_counts["host_write_requested"],
        "secret_value_included_count": true_counts["secret_value_included"],
        "secret_value_collection_allowed_count": true_counts["secret_value_collection_allowed"],
        "credential_marker_write_requested_count": true_counts["credential_marker_write_requested"],
        "credential_marker_write_authorized_count": true_counts["credential_marker_write_authorized"],
        "forbidden_true_field_count": forbidden_true_total,
    }
    return result


def csv_value(value: Any) -> str:
    if isinstance(value, list):
        return ",".join(str(item) for item in value) if value else "none"
    if isinstance(value, bool):
        return "1" if value else "0"
    return str(value)


def print_key_values(result: dict[str, Any]) -> None:
    print("POST_REBOOT_CREDENTIAL_ESCROW_INTAKE_SCORECARD=1")
    ordered_keys = [
        "status",
        "next_step",
        "active_gate_present",
        "summary_next_required_gates",
        "owner_packet_gate_count",
        "owner_packet_gates",
        "unexpected_owner_packet_gate_count",
        "response_gate_count",
        "response_gates",
        "unexpected_response_gate_count",
        "required_item_count",
        "missing_required_item_count",
        "summary_escrow_missing_count",
        "offsite_escrow_missing_count",
        "escrow_status_seen_count",
        "escrow_status_missing_count",
        "effective_escrow_missing_count",
        "script_missing_count",
        "offsite_configured",
        "rclone_configured",
        "preflight_status",
        "preflight_blocker_count",
        "owner_response_received_count",
        "owner_response_accepted_count",
        "runtime_gate_count",
        "runtime_action_authorized",
        "host_write_authorized",
        "secret_value_collection_allowed",
        "runtime_action_requested_count",
        "host_write_requested_count",
        "secret_value_included_count",
        "credential_marker_write_requested_count",
        "credential_marker_write_authorized_count",
        "forbidden_true_field_count",
    ]
    for key in ordered_keys:
        print(f"{key.upper()}={csv_value(result.get(key))}")


def main() -> int:
    args = parse_args()
    result = evaluate(args)
    if args.json:
        print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    else:
        print_key_values(result)
    return 0


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