#!/usr/bin/env python3
from __future__ import annotations

import hashlib
import json
import subprocess
import sys
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "script/project-manifest.json"
REVIEW = ROOT / "work/story/run-002/semantic-review.json"
TIMEOUT_SECONDS = 7200
POLL_SECONDS = 5


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def resolve(manifest: dict, key: str) -> Path:
    value = (manifest.get("active_paths") or {}).get(key)
    if not isinstance(value, str) or not value:
        raise RuntimeError(f"missing active_paths.{key}")
    path = Path(value)
    if not path.is_absolute():
        path = ROOT / path
    path.resolve().relative_to(ROOT.resolve())
    return path


def emit_failure(message: str, **extra: object) -> int:
    result = {"status": "failed", "verified": False, "error": message}
    result.update(extra)
    print(json.dumps(result, ensure_ascii=False), flush=True)
    return 1


def run(command: list[str]) -> dict:
    completed = subprocess.run(command, cwd=ROOT, text=True, capture_output=True)
    return {
        "command": " ".join(command),
        "exit_code": completed.returncode,
        "stdout": completed.stdout.strip(),
        "stderr": completed.stderr.strip(),
    }


def main() -> int:
    initial = json.loads(MANIFEST.read_text(encoding="utf-8"))
    candidate = resolve(initial, "candidate")
    expected_hash = (initial.get("story_run") or {}).get("candidate_sha256")
    expected_run = (initial.get("story_run") or {}).get("run_id")
    if expected_run != "run-002" or not isinstance(expected_hash, str):
        return emit_failure("active run or candidate hash is not locked")

    started = time.monotonic()
    while not REVIEW.is_file():
        if time.monotonic() - started >= TIMEOUT_SECONDS:
            return emit_failure("semantic review wait timed out")
        time.sleep(POLL_SECONDS)

    try:
        review = json.loads(REVIEW.read_text(encoding="utf-8"))
    except Exception as exc:
        return emit_failure(f"semantic review is unreadable: {type(exc).__name__}: {exc}")
    if review.get("candidate_sha256") != expected_hash:
        return emit_failure("semantic review is stale", expected_sha256=expected_hash, observed_sha256=review.get("candidate_sha256"))
    if sha256(candidate) != expected_hash:
        return emit_failure("candidate changed while semantic review was running")

    current = json.loads(MANIFEST.read_text(encoding="utf-8"))
    if (current.get("story_run") or {}).get("run_id") != expected_run:
        return emit_failure("active story run changed while waiting")

    record = run([sys.executable, "script/record-semantic-review.py", "--review-json", str(REVIEW)])
    if record["exit_code"] != 0:
        return emit_failure("semantic review recording failed", result=record)

    semantic_receipt = resolve(current, "semantic_receipt")
    semantic = json.loads(semantic_receipt.read_text(encoding="utf-8"))
    if semantic.get("status") != "completed" or semantic.get("verified") is not True or semantic.get("candidate_sha256") != expected_hash:
        return emit_failure("semantic gate is not current PASS", result=record)

    voice = run([sys.executable, "script/run-voice-test.py"])
    if voice["exit_code"] != 0:
        return emit_failure("voice test failed; no retry performed", semantic_result=record, voice_result=voice)

    print(json.dumps({
        "status": "completed",
        "verified": True,
        "candidate_sha256": expected_hash,
        "semantic_result": record,
        "voice_test_result": voice,
    }, ensure_ascii=False), flush=True)
    return 0


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