#!/usr/bin/env python3
"""Agent Mechanics Lab 01: recover without repeating a committed side effect."""
from __future__ import annotations

import argparse
import json
import os
import shutil
import sys
import tempfile
import time
from pathlib import Path
from typing import Any

OPERATION_ID = "op_publish_report_v1"


class InjectedCrash(RuntimeError):
    """Raised at a controlled persistence boundary."""


def append_event(path: Path, event_type: str, **payload: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    record = {"seq": len(read_events(path)) + 1, "ts": round(time.time(), 6), "type": event_type, **payload}
    with path.open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n")
        handle.flush()
        os.fsync(handle.fileno())


def read_events(path: Path) -> list[dict[str, Any]]:
    if not path.exists():
        return []
    events: list[dict[str, Any]] = []
    for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
        if not line.strip():
            continue
        try:
            events.append(json.loads(line))
        except json.JSONDecodeError as exc:
            raise RuntimeError(f"invalid event log at line {line_number}: {exc}") from exc
    return events


def load_ledger(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {"effects": {}}
    return json.loads(path.read_text(encoding="utf-8"))


def save_ledger(path: Path, ledger: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = path.with_suffix(".tmp")
    with temporary.open("w", encoding="utf-8") as handle:
        json.dump(ledger, handle, ensure_ascii=False, indent=2, sort_keys=True)
        handle.flush()
        os.fsync(handle.fileno())
    os.replace(temporary, path)


def commit_effect(ledger_path: Path) -> bool:
    """Commit once. Return True only when this call created the effect."""
    ledger = load_ledger(ledger_path)
    effects = ledger.setdefault("effects", {})
    if OPERATION_ID in effects:
        return False
    effects[OPERATION_ID] = {
        "kind": "publish_report",
        "target": "team-knowledge-base",
        "committed_at": round(time.time(), 6),
    }
    save_ledger(ledger_path, ledger)
    return True


def event_types(events: list[dict[str, Any]]) -> set[str]:
    return {str(event.get("type")) for event in events}


def report(state_dir: Path) -> dict[str, Any]:
    events = read_events(state_dir / "events.jsonl")
    ledger = load_ledger(state_dir / "effect-ledger.json")
    effect_count = 1 if OPERATION_ID in ledger.get("effects", {}) else 0
    return {
        "state_dir": str(state_dir),
        "event_count": len(events),
        "event_types": [event.get("type") for event in events],
        "effect_count": effect_count,
        "verified": "verified" in event_types(events),
    }


def run_once(state_dir: Path, crash_at: str = "none") -> dict[str, Any]:
    events_path = state_dir / "events.jsonl"
    ledger_path = state_dir / "effect-ledger.json"
    events = read_events(events_path)
    types = event_types(events)

    if "verified" in types:
        return report(state_dir)

    if "planned" not in types:
        append_event(events_path, "planned", operation_id=OPERATION_ID, tool="publish_report")

    if crash_at == "before_effect":
        raise InjectedCrash("before_effect")

    created = commit_effect(ledger_path)

    # The dangerous boundary: the external effect exists, but the event is not durable yet.
    if crash_at == "after_effect" and "effect_committed" not in types:
        raise InjectedCrash("after_effect")

    events = read_events(events_path)
    types = event_types(events)
    if "effect_committed" not in types:
        append_event(
            events_path,
            "effect_committed",
            operation_id=OPERATION_ID,
            created_by_this_run=created,
            recovered_from_ledger=not created,
        )

    events = read_events(events_path)
    types = event_types(events)
    if "result_persisted" not in types:
        append_event(events_path, "result_persisted", operation_id=OPERATION_ID, status="ok")

    if crash_at == "after_result":
        raise InjectedCrash("after_result")

    ledger = load_ledger(ledger_path)
    effect_count = sum(1 for key in ledger.get("effects", {}) if key == OPERATION_ID)
    if effect_count != 1:
        append_event(events_path, "verification_failed", operation_id=OPERATION_ID, effect_count=effect_count)
        raise RuntimeError(f"expected exactly one committed effect, found {effect_count}")

    append_event(events_path, "verified", operation_id=OPERATION_ID, effect_count=effect_count)
    return report(state_dir)


def self_test() -> None:
    with tempfile.TemporaryDirectory(prefix="agent-mechanics-loop-") as temporary:
        state_dir = Path(temporary)
        try:
            run_once(state_dir, "after_effect")
        except InjectedCrash as exc:
            assert str(exc) == "after_effect"
        else:
            raise AssertionError("fault injection did not fire")

        first_recovery = run_once(state_dir)
        second_recovery = run_once(state_dir)
        assert first_recovery["effect_count"] == 1
        assert first_recovery["verified"] is True
        assert second_recovery["effect_count"] == 1
        assert second_recovery["event_count"] == first_recovery["event_count"]
    print("PASS loop-recovery duplicate_effects=0 verified=true")


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--state-dir", type=Path, default=Path(".agent-mechanics-lab/loop-recovery"))
    parser.add_argument("--crash-at", choices=["none", "before_effect", "after_effect", "after_result"], default="none")
    parser.add_argument("--reset", action="store_true", help="remove prior lab state before running")
    parser.add_argument("--report", action="store_true", help="print current state without executing")
    parser.add_argument("--self-test", action="store_true", help="run the deterministic CI check")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    if args.self_test:
        self_test()
        return 0
    if args.reset and args.state_dir.exists():
        shutil.rmtree(args.state_dir)
    if args.report:
        print(json.dumps(report(args.state_dir), ensure_ascii=False, indent=2))
        return 0
    try:
        result = run_once(args.state_dir, args.crash_at)
    except InjectedCrash as exc:
        print(f"INJECTED_CRASH boundary={exc} state_dir={args.state_dir}")
        return 75
    print(json.dumps(result, ensure_ascii=False, indent=2))
    print("PASS duplicate_effects=0")
    return 0


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