#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""scripts/run_runtime_event_enactor.py — allowlisted runner for the
task-2553+55 bounded runtime-event enactor.

Reads a +54 runtime-event-loop result (default: the real dogfood artifact
``memory/events/task-2553.runtime-event-loop-result_260518.json``), runs the
``anu_v3.runtime_event_enactor`` bounded enactor, and writes ONLY the §4
expected_files (write surface = allowlist; any other path is refused).

회장 §6 박제 (this runner enforces it structurally):

  * ZERO PR / branch / main write · ZERO merge · ZERO credential / OWNER_PAT
  * ZERO self-dispatch / self-collector · ZERO cron register/remove
  * ZERO dead-man / fixed-time as a progress trigger
  * ZERO modification of any existing +44/+47/+49/+50~54 artifact
  * the ONLY write = the §4 expected_files allowlist (Track A additive
    closeout is a brand-new file).

Layer A / NO-CRON. ZERO subprocess / cokacdir / network / git mutation.
"""
from __future__ import annotations

import argparse
import json
import os
import sys
from typing import Dict

WORKSPACE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if WORKSPACE not in sys.path:
    sys.path.insert(0, WORKSPACE)

from anu_v3.runtime_event_enactor import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    CLOSEOUT_ARTIFACT_TARGET,
    RuntimeEventEnactor,
    enactor_result_envelope,
)

ANU_KEY = "c119085addb0f8b7"
EXECUTOR_SELF_KEY = "a999e2ea4c06d2fb"

DEFAULT_LOOP_RESULT = (
    "memory/events/task-2553.runtime-event-loop-result_260518.json"
)

# §4 expected_files — the ONLY paths this runner may write (Track A additive
# closeout + +55 decision/enactor-result/result). Any other path is refused.
WRITE_ALLOWLIST = frozenset(
    {
        CLOSEOUT_ARTIFACT_TARGET,
        "memory/events/task-2553+55.decision.json",
        "memory/events/task-2553+55.enactor-result.json",
        "memory/events/task-2553+55.result.json",
    }
)


class WriteSurfaceViolation(RuntimeError):
    """Raised if a write is attempted outside the §4 allowlist."""


def _assert_allowlisted(rel: str) -> None:
    if rel not in WRITE_ALLOWLIST:
        raise WriteSurfaceViolation(
            f"write refused — {rel!r} is outside the §4 expected_files "
            "allowlist (회장 §4/§6 — write surface = allowlist only)."
        )


def _write_json(rel: str, payload: Dict[str, object]) -> str:
    _assert_allowlisted(rel)
    abs_path = os.path.join(WORKSPACE, rel)
    os.makedirs(os.path.dirname(abs_path), exist_ok=True)
    with open(abs_path, "w", encoding="utf-8") as fh:
        json.dump(payload, fh, ensure_ascii=False, indent=2, sort_keys=True)
        fh.write("\n")
    return rel


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(
        description="task-2553+55 bounded runtime-event enactor runner"
    )
    ap.add_argument(
        "--loop-result",
        default=DEFAULT_LOOP_RESULT,
        help="path (relative to workspace) to the +54 loop result JSON",
    )
    ap.add_argument(
        "--generated-at-kst",
        default="2026-05-18 22:10 KST",
    )
    ap.add_argument(
        "--progress-trigger-source",
        default="registry_completed_event",
        help="must be registry_completed_event; a dead-man/fixed-time/"
        "fallback source is a hard FAIL",
    )
    ap.add_argument(
        "--no-write",
        action="store_true",
        help="compute every decision but perform ZERO write (dry).",
    )
    args = ap.parse_args(argv)

    loop_path = os.path.join(WORKSPACE, args.loop_result)
    if not os.path.isfile(loop_path):
        print(
            json.dumps(
                {
                    "status": "error",
                    "message": f"loop result not found: {args.loop_result}",
                },
                ensure_ascii=False,
            )
        )
        return 2
    with open(loop_path, "r", encoding="utf-8") as fh:
        loop_result = json.load(fh)

    enactor = RuntimeEventEnactor(
        anu_keys=[ANU_KEY],
        executor_self_key=EXECUTOR_SELF_KEY,
        workspace_root=WORKSPACE,
    )

    written = []

    def _additive_writer(rel: str, payload: Dict[str, object]) -> None:
        written.append(_write_json(rel, payload))

    result = enactor.enact(
        loop_result,
        progress_trigger_source=args.progress_trigger_source,
        allow_actual_dispatch=False,  # Track B verify-only — never set.
        artifact_writer=None if args.no_write else _additive_writer,
        generated_at_kst=args.generated_at_kst,
    )

    envelope = enactor_result_envelope(
        result, generated_at_kst=args.generated_at_kst
    )
    decision = {
        "schema": "task-2553+55.decision.v1",
        "generated_at_kst": args.generated_at_kst,
        "verdict": result.verdict,
        "authority": "none",
        "auto_executed": False,
        "track_A_final_consolidated_closeout": "additive artifact only",
        "track_B_next_dispatch_safety_gate": "verify-only (no promotion)",
        "track_C_hold_packet_routing": "verify-only",
        "loop_result_sha256": result.loop_result_sha256,
        "enacted_count": result.enacted_count,
        "idempotent_skip_count": result.idempotent_skip_count,
        "dispatch_ready_count": result.dispatch_ready_count,
        "blocked_count": result.blocked_count,
        "hold_routed_count": result.hold_routed_count,
        "hold_for_chair": result.hold_for_chair,
    }

    if not args.no_write:
        written.append(
            _write_json(
                "memory/events/task-2553+55.decision.json", decision
            )
        )
        written.append(
            _write_json(
                "memory/events/task-2553+55.enactor-result.json", envelope
            )
        )

    print(
        json.dumps(
            {
                "status": "ok",
                "verdict": result.verdict,
                "enacted_count": result.enacted_count,
                "idempotent_skip_count": result.idempotent_skip_count,
                "dispatch_ready_count": result.dispatch_ready_count,
                "blocked_count": result.blocked_count,
                "hold_routed_count": result.hold_routed_count,
                "hold_for_chair": result.hold_for_chair,
                "written": written,
                "dry": bool(args.no_write),
            },
            ensure_ascii=False,
        )
    )
    return 0


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