#!/bin/sh
set -eu

BASE=bd5a09054e5cf89efd4db82aadb051f20b06ebf7
FREEBSD_SRC=/work/build/freebsd-src
OUTPUT=
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../../.." && pwd)
INPUT=$SCRIPT_DIR/P15-032-input.json

usage()
{
	printf '%s\n' "usage: $0 --output ABSOLUTE_PATH [--base SHA] [--freebsd-src PATH]" >&2
	exit 20
}

while [ "$#" -gt 0 ]; do
	case "$1" in
	--output)
		[ "$#" -ge 2 ] || usage
		OUTPUT=$2
		shift 2
		;;
	--base)
		[ "$#" -ge 2 ] || usage
		BASE=$2
		shift 2
		;;
	--freebsd-src)
		[ "$#" -ge 2 ] || usage
		FREEBSD_SRC=$2
		shift 2
		;;
	*)
		usage
		;;
	esac
done

[ -n "$OUTPUT" ] || usage
case "$OUTPUT" in
/*) ;;
*) printf '%s\n' "P15-032: --output must be absolute" >&2; exit 20 ;;
esac

if [ -d "$OUTPUT" ] && [ -n "$(find "$OUTPUT" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
	printf '%s\n' "P15-032: --output must be empty" >&2
	exit 20
fi
mkdir -p "$OUTPUT"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/P15-032.XXXXXX")
cleanup()
{
	rm -rf "$TMP"
}
trap cleanup EXIT HUP INT TERM

printf 'argv=%s\n' "$0 --output $OUTPUT --base $BASE --freebsd-src $FREEBSD_SRC" >"$OUTPUT/runner.argv"
printf 'whole_gate_deadline_seconds=120\n' >"$OUTPUT/deadlines.txt"
printf 'command_deadline_seconds=10\n' >>"$OUTPUT/deadlines.txt"
printf 'compile_deadline_seconds=20\n' >>"$OUTPUT/deadlines.txt"
printf 'model_deadline_seconds=10\n' >>"$OUTPUT/deadlines.txt"

set +e
/usr/bin/timeout 120s python3 - "$ROOT" "$FREEBSD_SRC" "$INPUT" "$OUTPUT" "$TMP" "$BASE" >"$OUTPUT/runner.stdout" 2>"$OUTPUT/runner.stderr" <<'PY'
import datetime
import hashlib
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys

root = Path(sys.argv[1]).resolve()
freebsd = Path(sys.argv[2]).resolve()
input_path = Path(sys.argv[3]).resolve()
output = Path(sys.argv[4]).resolve()
tmp = Path(sys.argv[5]).resolve()
base = sys.argv[6]
cfg = json.loads(input_path.read_text(encoding="utf-8"))
command_timeout = cfg["timeouts_seconds"]["command"]
commands = []

def run(argv, cwd, timeout=command_timeout, check=True):
    started = datetime.datetime.now(datetime.timezone.utc)
    cp = subprocess.run(argv, cwd=cwd, stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE, timeout=timeout, check=False)
    commands.append({
        "argv": argv,
        "cwd": str(cwd),
        "timeout_seconds": timeout,
        "started_utc": started.isoformat(),
        "exit": cp.returncode,
        "stdout": cp.stdout.decode("utf-8", "replace"),
        "stderr": cp.stderr.decode("utf-8", "replace"),
    })
    if check and cp.returncode != 0:
        raise RuntimeError(f"command failed ({cp.returncode}): {shlex.join(argv)}")
    return cp

def sha256(data):
    return hashlib.sha256(data).hexdigest()

def bounded_read(path):
    data = path.read_bytes()
    if len(data) > 4 * 1024 * 1024:
        raise RuntimeError(f"refusing oversized contract file: {path}")
    return data

def base_blob(path):
    return run(["git", "show", f"{base}:{path}"], root).stdout

def line_of(text, marker):
    return text[:text.index(marker)].count("\n") + 1

started = datetime.datetime.now(datetime.timezone.utc)
if base != cfg["frozen_base"]:
    raise RuntimeError("requested base does not match frozen base")
head = run(["git", "rev-parse", "HEAD"], root).stdout.decode().strip()
remote = run(["git", "rev-parse", "xdm/main"], root).stdout.decode().strip()
if head != base or remote != base:
    raise RuntimeError(f"base mismatch: HEAD={head} xdm/main={remote} expected={base}")

allowed = {
    "repo-pre-15/tests/pre15/gates/P15-032.sh",
    "repo-pre-15/tests/pre15/gates/P15-032-input.json",
    "repo-pre-15/docs/pre15-stage0/P15-032.md",
}
status_raw = run(["git", "status", "--porcelain=v1", "-z"], root).stdout
entries = [entry for entry in status_raw.decode().split("\0") if entry]
paths = set()
for entry in entries:
    path = entry[3:]
    if " -> " in path:
        path = path.split(" -> ", 1)[1]
    paths.add(path)
if not paths.issubset(allowed):
    raise RuntimeError(f"out-of-scope worktree paths: {sorted(paths - allowed)}")

outer_head = run(["git", "rev-parse", "HEAD"], freebsd).stdout.decode().strip()
if outer_head != cfg["freebsd_source"]["outer_git_head"]:
    raise RuntimeError(f"FreeBSD outer source identity mismatch: {outer_head}")

contracts = []
source_hashes = {}
for contract in cfg["contracts"]:
    kind = contract["root"]
    rel = contract["path"]
    if kind == "freebsd":
        data = bounded_read(freebsd / rel)
    elif kind == "repo_base":
        data = base_blob(rel)
    elif kind == "linux_reference":
        data = base_blob(rel)
    else:
        raise RuntimeError(f"unknown contract root: {kind}")
    actual = sha256(data)
    source_hashes[f"{kind}:{rel}"] = actual
    if actual != contract["sha256"]:
        raise RuntimeError(f"hash mismatch for {kind}:{rel}: {actual}")
    text = data.decode("utf-8", "replace")
    matches = []
    for marker in contract.get("must_contain", []):
        if marker not in text:
            raise RuntimeError(f"missing contract marker in {rel}: {marker!r}")
        matches.append({"marker": marker, "line": line_of(text, marker)})
    forbidden = []
    for marker in contract.get("must_not_contain", []):
        present = marker in text
        forbidden.append({"marker": marker, "present": present})
        if present:
            raise RuntimeError(f"forbidden bridge marker in {rel}: {marker!r}")
    contracts.append({"root": kind, "path": rel, "sha256": actual,
                      "matches": matches, "negative_matches": forbidden})

events = cfg["required_events"]
event_index = {name: index for index, name in enumerate(events)}
required_order = [
    ("check_self_identity", "first_io"),
    ("check_namespace_ancestors", "first_io"),
    ("trace_storage_ancestors", "first_io"),
    ("crhold_mount_cred", "first_io"),
    ("vop_set_text", "first_io"),
    ("register_upper_mount", "first_io"),
    ("io_complete", "drain_inflight"),
    ("drain_inflight", "unregister_upper_mount"),
    ("unregister_upper_mount", "vn_close"),
    ("vn_close", "crfree_mount_cred"),
    ("crfree_mount_cred", "release_last_reference"),
]
for before, after in required_order:
    if event_index[before] >= event_index[after]:
        raise RuntimeError(f"invalid lifecycle order: {before} !< {after}")

nodes = set()
adj = {}
for before, after in cfg["lock_edges"]:
    nodes.update((before, after))
    adj.setdefault(before, []).append(after)
visiting = set()
visited = set()
def visit(node):
    if node in visiting:
        raise RuntimeError(f"lock graph cycle at {node}")
    if node in visited:
        return
    visiting.add(node)
    for child in adj.get(node, []):
        visit(child)
    visiting.remove(node)
    visited.add(node)
for node in sorted(nodes):
    visit(node)

dot = ["digraph P15_032 {", "  rankdir=LR;"]
for before, after in cfg["lock_edges"]:
    dot.append(f'  "{before}" -> "{after}";')
dot.append("}")
(output / "lockgraph.dot").write_text("\n".join(dot) + "\n", encoding="utf-8")

invariants = cfg["required_invariants"]
closeable = [name for name in invariants if name != "storage_ancestor_check_before_io"]
controls = cfg["false_go_controls"]
if set(controls) != set(closeable):
    raise RuntimeError("false-GO controls do not exactly cover closeable invariants")

enum_lines = [f"    INV_{name.upper()} = 1ULL << {index}," for index, name in enumerate(closeable)]
required_mask = " |\n        ".join(f"INV_{name.upper()}" for name in closeable)
control_rows = ",\n".join(
    f'    {{"{name}", INV_{name.upper()}}}' for name in controls
)
model = f'''#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

enum invariant {{
{os.linesep.join(enum_lines)}
}};

struct scenario {{
    uint64_t present;
    int storage_ancestry_identity_api;
}};

struct control {{
    const char *name;
    uint64_t bit;
}};

static const uint64_t required =
        {required_mask};

static int
visible_only_oracle(const struct scenario *scenario)
{{
    return ((scenario->present & required) == required ? 0 : EINVAL);
}}

static int
rigorous_oracle(const struct scenario *scenario)
{{
    int error;

    error = visible_only_oracle(scenario);
    if (error != 0)
        return (error);
    if (!scenario->storage_ancestry_identity_api)
        return (EOPNOTSUPP);
    return (0);
}}

int
main(void)
{{
    static const struct control controls[] = {{
{control_rows}
    }};
    struct scenario candidate = {{ required, 0 }};
    struct scenario mutation;
    size_t index;

    if (visible_only_oracle(&candidate) != 0)
        return (1);
    if (rigorous_oracle(&candidate) != EOPNOTSUPP)
        return (2);
    puts("hidden-md-edge: visible-only=GO rigorous=STOP:EOPNOTSUPP");
    for (index = 0; index < sizeof(controls) / sizeof(controls[0]); index++) {{
        mutation = candidate;
        mutation.present &= ~controls[index].bit;
        if (visible_only_oracle(&mutation) == 0 || rigorous_oracle(&mutation) == 0)
            return (3);
        printf("false-go-control:%s=REJECTED\\n", controls[index].name);
    }}
    puts("lifecycle-model=PASS");
    return (0);
}}
'''
model_path = tmp / "P15-032-model.c"
binary_path = tmp / "P15-032-model"
model_path.write_text(model, encoding="utf-8")
compile_cp = run(["/usr/bin/timeout", f"{cfg['timeouts_seconds']['compile']}s",
                  "/usr/bin/cc", "-std=c11", "-Wall", "-Wextra", "-Werror",
                  "-O2", "-o", str(binary_path), str(model_path)], tmp,
                 timeout=cfg["timeouts_seconds"]["compile"] + 2)
model_cp = run(["/usr/bin/timeout", f"{cfg['timeouts_seconds']['model']}s",
                str(binary_path)], tmp,
               timeout=cfg["timeouts_seconds"]["model"] + 2)
(output / "owned-temp-model.c").write_bytes(model_path.read_bytes())
(output / "owned-temp-model.stdout").write_bytes(model_cp.stdout)
(output / "owned-temp-model.stderr").write_bytes(model_cp.stderr)

false_go = {
    "controls": [{"removed_invariant": name, "outcome": "REJECTED"} for name in controls],
    "adversarial_hidden_edge": {
        "topology": "regular vnode -> filesystem -> md GEOM provider -> private backing vnode",
        "visible_only_oracle": "GO",
        "rigorous_oracle": "STOP:EOPNOTSUPP",
    },
}
(output / "false-go-controls.json").write_text(json.dumps(false_go, indent=2) + "\n")
(output / "contracts.json").write_text(json.dumps(contracts, indent=2) + "\n")
(output / "source-hashes.json").write_text(json.dumps(source_hashes, indent=2, sort_keys=True) + "\n")
(output / "commands.json").write_text(json.dumps(commands, indent=2) + "\n")

state = {
    "events": events,
    "required_order": required_order,
    "local_lifecycle": "PASS",
    "storage_ancestor_identity": "UNAVAILABLE",
    "typed_errno_if_detectable": cfg["typed_errno"]["cycle"],
    "typed_errno_for_unsupported_graph": cfg["typed_errno"]["unsupported_dependency_graph"],
}
(output / "state-model.json").write_text(json.dumps(state, indent=2) + "\n")

result = {
    "unit": cfg["unit"],
    "batch": cfg["batch"],
    "classification": "GATE_STOP",
    "decision": "STOP",
    "exit": 10,
    "base": base,
    "head": head,
    "xdm_main": remote,
    "freebsd_git_head": outer_head,
    "freebsd_identity": cfg["freebsd_source"]["identity"],
    "started_utc": started.isoformat(),
    "finished_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "closeable_invariants": closeable,
    "blocking_condition": cfg["blocking_condition"],
    "source_changes_permitted": False,
    "tests_run": ["contract extraction", "owned-temp C lifecycle model", "adversarial false-GO controls"],
    "tests_omitted": ["D", "H", "K", "Q", "TC006", "TC179", "TC184", "full feature", "smoke"],
}
(output / "result.json").write_text(json.dumps(result, indent=2) + "\n")

print("P15-032 STOP: no public identity-preserving VFS/GEOM-to-backing-vnode ancestry API")
sys.exit(10)
PY
RC=$?
set -e

printf 'exit=%s\n' "$RC" >"$OUTPUT/runner.exit"
case "$RC" in
10)
	printf '%s\n' GATE_STOP >"$OUTPUT/classification"
	;;
124)
	printf '%s\n' RUNNER_TIMEOUT >"$OUTPUT/classification"
	;;
0)
	printf '%s\n' GATE_GO >"$OUTPUT/classification"
	;;
*)
	printf '%s\n' RUNNER_FAILURE >"$OUTPUT/classification"
	;;
esac
(cd "$OUTPUT" && find . -maxdepth 1 -type f ! -name manifest.sha256 -print | LC_ALL=C sort | xargs sha256sum) >"$OUTPUT/manifest.sha256"
exit "$RC"
