#!/usr/bin/env python3
"""Append-only strict lifecycle observer v7; dry-run is the only current mode."""

from __future__ import annotations
import argparse
import hashlib
import json
import os
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parent
MANIFEST = ROOT / "candidate-manifest.json"
FULL = "dac4e81281e8ab7f5c46461e79d405998e0273fe9f98dc09ff67b73528083092"
MAX_SECONDS = 6 * 60 * 60
STATES = (
    "membership_hash_chain",
    "polling_receipts",
    "onboarding",
    "checkin",
    "generation",
    "approval_card",
    "delivery_capability",
    "delivery",
    "cleanup",
)
HANDOFFS = (
    "operator:launch_exact_candidate",
    "customer:onboarding_answers_only",
    "owner:review_exact_card",
    "operator:issue_one_use_capability",
    "customer:capture_surface",
    "operator:cleanup_after_retention",
)


def digest(p: Path) -> str:
    return hashlib.sha256(p.read_bytes()).hexdigest()


def main() -> int:
    p = argparse.ArgumentParser()
    p.add_argument("mode", choices=("dry-run", "observe"))
    p.add_argument("--events", type=Path)
    p.add_argument("--ready", type=Path)
    a = p.parse_args()
    m = json.loads(MANIFEST.read_text())
    if m.get("full_candidate_digest") != FULL:
        raise SystemExit("BLOCKED candidate drift")
    contract = {
        "schema": "task26-strict-lifecycle-observer-v7-successor",
        "status": "PASS_DRY_RUN" if a.mode == "dry-run" else "ARMED",
        "candidate": FULL,
        "subscribe_before_snapshot": True,
        "states": STATES,
        "max_seconds": MAX_SECONDS,
        "handoffs": HANDOFFS,
        "invariants": {
            "one_candidate": True,
            "recovery_forbidden": True,
            "config_repair_forbidden": True,
            "duplicate_forbidden": True,
            "raw_answers_forbidden": True,
            "provider_text_forbidden": True,
        },
        "manifest_sha256": digest(MANIFEST),
    }
    if a.mode == "dry-run":
        print(json.dumps(contract, sort_keys=True))
        return 0
    if not a.events or not a.ready:
        raise SystemExit("BLOCKED observe paths required")
    # Subscription is durably announced before any snapshot or human action.
    a.events.parent.mkdir(parents=True, exist_ok=True)
    fd = os.open(a.events, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC, 0o600)
    first = (
        json.dumps(
            {"event": "SUBSCRIBED", "candidate": FULL, "states": STATES}, sort_keys=True
        )
        + "\n"
    ).encode()
    os.write(fd, first)
    os.fsync(fd)
    os.close(fd)
    a.ready.write_text(json.dumps(contract, sort_keys=True) + "\n")
    os.chmod(a.ready, 0o600)
    deadline = time.monotonic() + MAX_SECONDS
    while time.monotonic() < deadline:
        time.sleep(min(1, deadline - time.monotonic()))
    raise SystemExit("BLOCKED six-hour lifecycle deadline exceeded")


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