#!/bin/sh

pre15_write_argv_json()
{
	pre15_argv_path=$1
	shift
	python3 - "$pre15_argv_path" "$@" <<'PY'
import json
from pathlib import Path
import sys

path = Path(sys.argv[1])
with path.open("x", encoding="ascii") as output:
    json.dump(sys.argv[2:], output, ensure_ascii=True, separators=(",", ":"))
    output.write("\n")
PY
}

pre15_write_manifest()
{
	python3 - "$PRE15_MANIFEST_PATH" <<'PY'
from __future__ import annotations

import hashlib
import json
import os
from pathlib import Path
import sys


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


def relative(path: Path, root: Path) -> str:
    return path.relative_to(root).as_posix()


manifest_path = Path(sys.argv[1])
run_dir = manifest_path.parent
argv_path = Path(os.environ["PRE15_ARGV_PATH"])
fixture_path = Path(os.environ["PRE15_FIXTURE_HASHES"])
module_path = Path(os.environ["PRE15_MODULE_HASH"])
stdout_path = Path(os.environ["PRE15_STDOUT_PATH"])
stderr_path = Path(os.environ["PRE15_STDERR_PATH"])
ownership_path = Path(os.environ["PRE15_OWNERSHIP_FILE"])
cleanup_log = Path(os.environ["PRE15_CLEANUP_LOG"])
identity_path = Path(os.environ["PRE15_IDENTITY_PATH"])

fixtures = []
fixture_details = []
if fixture_path.exists():
    for raw_line in fixture_path.read_text(encoding="utf-8").splitlines():
        if not raw_line:
            continue
        checksum, label, path = raw_line.split("\t", 2)
        fixtures.append(checksum)
        fixture_details.append(
            {"sha256": checksum, "label": label, "path": path}
        )

module_sha256 = None
module_details = None
if module_path.exists() and module_path.stat().st_size:
    checksum, path = module_path.read_text(encoding="utf-8").rstrip("\n").split(
        "\t", 1
    )
    module_sha256 = checksum
    module_details = {"sha256": checksum, "path": path}

exit_code_text = os.environ["PRE15_EXIT_CODE"]
exit_code = None if exit_code_text == "null" else int(exit_code_text)
identity = json.loads(identity_path.read_text(encoding="ascii"))
manifest = {
    "schema_version": 1,
    "run_id": os.environ["PRE15_RUN_ID"],
    "mode": os.environ["PRE15_MODE"],
    "case_id": os.environ["PRE15_CASE_ID"],
    "dut_head": os.environ["PRE15_DUT_HEAD"],
    "dut_tree": os.environ["PRE15_DUT_TREE"],
    "worktree_diff_sha256": os.environ["PRE15_WORKTREE_DIFF_SHA256"],
    "freebsd_source": os.environ["PRE15_FREEBSD_SOURCE"],
    "linux_source": os.environ["PRE15_LINUX_SOURCE"],
    "module_sha256": module_sha256,
    "fixture_sha256": fixtures,
    "command_argv": json.loads(argv_path.read_text(encoding="ascii")),
    "start_utc": os.environ["PRE15_START_UTC"],
    "end_utc": os.environ["PRE15_END_UTC"],
    "deadline_seconds": int(os.environ["PRE15_DEADLINE"]),
    "exit_code": exit_code,
    "timed_out": os.environ["PRE15_TIMED_OUT"] == "1",
    "target_marker": os.environ["PRE15_TARGET_STATE"],
    "status": os.environ["PRE15_STATUS"],
    "reason": os.environ["PRE15_REASON"],
    "failure_origin": os.environ["PRE15_FAILURE_ORIGIN"],
    "cleanup": os.environ["PRE15_CLEANUP_STATUS"],
    "stdout": relative(stdout_path, run_dir),
    "stderr": relative(stderr_path, run_dir),
    "raw_sha256": {
        "stdout": digest(stdout_path),
        "stderr": digest(stderr_path),
        "ownership": digest(ownership_path),
        "cleanup": digest(cleanup_log),
    },
    "ownership_manifest": relative(ownership_path, run_dir),
    "cleanup_log": relative(cleanup_log, run_dir),
    "identity": identity,
    "fixtures": fixture_details,
    "module": module_details,
}
with manifest_path.open("x", encoding="ascii") as output:
    json.dump(manifest, output, ensure_ascii=True, indent=2, sort_keys=True)
    output.write("\n")
PY
}

pre15_validate_manifest()
{
	pre15_manifest_to_validate=$1
	pre15_schema_to_validate=$2
	python3 - "$pre15_manifest_to_validate" "$pre15_schema_to_validate" <<'PY'
from __future__ import annotations

from datetime import datetime
import hashlib
import json
from pathlib import Path
import re
import sys


manifest_path = Path(sys.argv[1])
schema_path = Path(sys.argv[2])
manifest = json.loads(manifest_path.read_text(encoding="ascii"))
schema = json.loads(schema_path.read_text(encoding="ascii"))

missing = sorted(set(schema["required"]) - set(manifest))
if missing:
    raise SystemExit(f"manifest missing required keys: {', '.join(missing)}")
if manifest["status"] not in schema["properties"]["status"]["enum"]:
    raise SystemExit(f"invalid status: {manifest['status']}")
if manifest["cleanup"] not in schema["properties"]["cleanup"]["enum"]:
    raise SystemExit(f"invalid cleanup: {manifest['cleanup']}")
if manifest["target_marker"] not in schema["properties"]["target_marker"]["enum"]:
    raise SystemExit(f"invalid target marker: {manifest['target_marker']}")
if not re.fullmatch(r"[0-9a-f]{40}", manifest["dut_head"]):
    raise SystemExit("dut_head is not a 40-hex commit")
for key in ("dut_tree", "worktree_diff_sha256"):
    if not re.fullmatch(r"[0-9a-f]{40,64}", manifest[key]):
        raise SystemExit(f"{key} is not a lowercase hex identity")
if manifest["module_sha256"] is not None and not re.fullmatch(
    r"[0-9a-f]{64}", manifest["module_sha256"]
):
    raise SystemExit("module_sha256 is neither null nor SHA256")
if not isinstance(manifest["command_argv"], list) or not manifest["command_argv"]:
    raise SystemExit("command_argv must be a nonempty array")
if not all(isinstance(item, str) for item in manifest["command_argv"]):
    raise SystemExit("command_argv contains a non-string item")
if not isinstance(manifest["fixture_sha256"], list) or not all(
    re.fullmatch(r"[0-9a-f]{64}", item) for item in manifest["fixture_sha256"]
):
    raise SystemExit("fixture_sha256 contains an invalid digest")
if not isinstance(manifest["deadline_seconds"], int) or manifest["deadline_seconds"] <= 0:
    raise SystemExit("deadline_seconds must be positive")
if manifest["exit_code"] is not None and not isinstance(manifest["exit_code"], int):
    raise SystemExit("exit_code must be an integer or null")
for key in ("start_utc", "end_utc"):
    datetime.fromisoformat(manifest[key].replace("Z", "+00:00"))
if manifest["status"] == "PASS":
    if manifest["exit_code"] != 0 or manifest["target_marker"] != "reached":
        raise SystemExit("PASS requires exit_code=0 and a reached target")
    if manifest["cleanup"] != "PASS":
        raise SystemExit("PASS requires successful cleanup")
if manifest["status"] == "DUT_FAIL" and manifest["target_marker"] != "reached":
    raise SystemExit("DUT_FAIL requires a reached target")
if manifest["cleanup"] == "FAIL" and manifest["status"] != "RUNNER_FAIL":
    raise SystemExit("cleanup failure must classify as RUNNER_FAIL")

run_dir = manifest_path.parent
for key in ("stdout", "stderr", "ownership_manifest", "cleanup_log"):
    relative = Path(manifest[key])
    if relative.is_absolute() or ".." in relative.parts:
        raise SystemExit(f"{key} escapes the run directory")
    if not (run_dir / relative).is_file():
        raise SystemExit(f"{key} does not exist: {relative}")
hashed_paths = {
    "stdout": manifest["stdout"],
    "stderr": manifest["stderr"],
    "ownership": manifest["ownership_manifest"],
    "cleanup": manifest["cleanup_log"],
}
for key, relative_path in hashed_paths.items():
    actual = hashlib.sha256((run_dir / relative_path).read_bytes()).hexdigest()
    if actual != manifest["raw_sha256"][key]:
        raise SystemExit(f"{key} hash mismatch")
PY
}
