#!/bin/sh
set -eu
umask 022

gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
input=$gate_dir/P15-030-input.json
base=
output=
deadline=60

while test "$#" -gt 0; do
	case "$1" in
	--base)
		test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 20; }
		base=$2
		shift 2
		;;
	--output)
		test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 20; }
		output=$2
		shift 2
		;;
	*)
		printf 'unknown argument: %s\n' "$1" >&2
		exit 20
		;;
	esac
done

test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 20; }
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 20; }
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 20; }
for tool in git mktemp python3 sha256sum timeout; do
	command -v "$tool" >/dev/null 2>&1 || {
		printf 'missing required host tool: %s\n' "$tool" >&2
		exit 21
	}
done
case "$output" in
/*) ;;
*) output=$PWD/$output ;;
esac
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 20; }
mkdir -p "$output"
work=$(mktemp -d "${TMPDIR:-/tmp}/P15-030.XXXXXX")

cleanup_trap()
{
	rm -rf -- "$work"
}
trap cleanup_trap EXIT HUP INT TERM

python3 -B - "$output/command-argv.json" "$0" --base "$base" --output "$output" <<'PY'
import json
from pathlib import Path
import sys

Path(sys.argv[1]).write_text(
    json.dumps(sys.argv[2:], ensure_ascii=True, separators=(",", ":")) + "\n",
    encoding="ascii",
)
PY
python3 -B - "$output/ownership.json" "$work" "$output" <<'PY'
import json
from pathlib import Path
import sys

Path(sys.argv[1]).write_text(
    json.dumps(
        {
            "owned_temporary_paths": [sys.argv[2]],
            "persistent_output": sys.argv[3],
            "owned_processes": [],
            "owned_ports": [],
            "owned_overlays": [],
            "qemu": "NOT_RUN",
        },
        ensure_ascii=True,
        indent=2,
        sort_keys=True,
    ) + "\n",
    encoding="ascii",
)
PY

set +e
timeout -k 5 "$deadline" python3 -B - \
    "$root" "$input" "$base" "$output" "$deadline" <<'PY' \
    >"$output/stdout.log" 2>"$output/stderr.log"
from __future__ import annotations

import hashlib
import json
from pathlib import Path
import subprocess
import sys
from typing import Any


ROOT = Path(sys.argv[1])
INPUT = Path(sys.argv[2])
REQUESTED_BASE = sys.argv[3]
OUTPUT = Path(sys.argv[4])
DEADLINE = int(sys.argv[5])
SPEC = json.loads(INPUT.read_text(encoding="utf-8"))


class GateFailure(RuntimeError):
    def __init__(self, status: str, reason: str):
        super().__init__(reason)
        self.status = status
        self.reason = reason


def sha256_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def write_json(path: Path, value: Any) -> None:
    path.write_text(
        json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
        encoding="ascii",
    )


def run_bytes(argv: list[str], allowed: set[int] | None = None) -> bytes:
    try:
        completed = subprocess.run(
            argv,
            cwd=ROOT,
            check=False,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=20,
        )
    except subprocess.TimeoutExpired as error:
        raise GateFailure(
            "INFRA_BLOCKED", f"command timed out: {' '.join(argv)}"
        ) from error
    accepted = {0} if allowed is None else allowed
    if completed.returncode not in accepted:
        detail = completed.stderr.decode("utf-8", errors="replace").strip()
        raise GateFailure(
            "RUNNER_FAIL",
            f"command failed ({completed.returncode}): {' '.join(argv)}: {detail}",
        )
    return completed.stdout


def git_text(*args: str) -> str:
    return run_bytes(["git", "-C", str(ROOT), *args]).decode(
        "utf-8", errors="strict"
    ).strip()


def source_at(commit: str, relative: str) -> bytes:
    return run_bytes(["git", "-C", str(ROOT), "show", f"{commit}:{relative}"])


def parse_ls_tree(raw: bytes) -> dict[str, dict[str, Any]]:
    entries: dict[str, dict[str, Any]] = {}
    for record in raw.split(b"\0"):
        if not record:
            continue
        metadata, path_bytes = record.split(b"\t", 1)
        mode, kind, oid, size = metadata.decode("ascii").split()
        path = path_bytes.decode("utf-8", errors="strict")
        entries[path] = {
            "mode": mode,
            "kind": kind,
            "oid": oid,
            "size": int(size),
        }
    return entries


def grep_paths(commit: str, expressions: list[str], fixed: bool = False) -> list[str]:
    argv = ["git", "-C", str(ROOT), "grep", "-I", "-l"]
    if fixed:
        argv.append("-F")
    else:
        argv.append("-i")
    for expression in expressions:
        argv.extend(["-e", expression])
    argv.extend(
        [
            commit,
            "--",
            "repo-pre-15",
            "planning/pre15",
            ":(exclude)planning/pre15/introduction.md",
            ":(exclude)repo-pre-15/tests/pre15/gates/P15-030.sh",
            ":(exclude)repo-pre-15/tests/pre15/gates/P15-030-input.json",
            ":(exclude)repo-pre-15/docs/pre15-stage0/P15-030.md",
        ]
    )
    raw = run_bytes(argv, {0, 1})
    prefix = f"{commit}:"
    paths = []
    for line in raw.decode("utf-8", errors="strict").splitlines():
        if not line.startswith(prefix):
            raise GateFailure("RUNNER_FAIL", f"unexpected git grep path: {line}")
        paths.append(line[len(prefix):])
    return sorted(paths)


def classify_reference(path: str) -> str:
    if path.startswith("planning/pre15/evidence/"):
        return "historical-evidence"
    if path.startswith("planning/pre15/"):
        return "planning-proposal"
    if path.startswith("repo-pre-15/tests/"):
        return "test-only"
    if path.startswith("repo-pre-15/docs/") or path.startswith("repo-pre-15/current/"):
        return "project-report-or-documentation"
    return "possible-operational-consumer"


def require_text(record: dict[str, Any], field: str) -> str:
    value = record.get(field)
    if not isinstance(value, str) or not value.strip():
        raise GateFailure("RUNNER_FAIL", f"consumer declaration lacks {field}")
    return value.strip()


def validate_consumer_manifest(
    path: str,
    data: bytes,
    entries: dict[str, dict[str, Any]],
    commit: str,
) -> dict[str, Any]:
    try:
        record = json.loads(data.decode("utf-8", errors="strict"))
    except (UnicodeDecodeError, json.JSONDecodeError) as error:
        raise GateFailure("RUNNER_FAIL", f"invalid consumer manifest: {path}") from error
    if not isinstance(record, dict):
        raise GateFailure("RUNNER_FAIL", f"consumer manifest is not an object: {path}")
    contract = SPEC["consumer_contract"]
    if record.get("schema") != contract["schema_marker"]:
        raise GateFailure("RUNNER_FAIL", f"consumer manifest schema mismatch: {path}")
    consumer_class = require_text(record, "consumer_class")
    if consumer_class not in contract["allowed_classes"]:
        raise GateFailure("RUNNER_FAIL", f"consumer class is not operational: {path}")
    if record.get("production_use") is not True:
        raise GateFailure("RUNNER_FAIL", f"consumer is not declared in production use: {path}")
    access_mode = require_text(record, "access_mode")
    if access_mode != contract["required_access_mode"]:
        raise GateFailure("RUNNER_FAIL", f"consumer access mode is not privileged: {path}")
    for field in (
        "consumer_id",
        "owner",
        "version",
        "entrypoint",
        "workflow",
        "deployment_reference",
    ):
        require_text(record, field)
    signals = record.get("signal_ids")
    if (
        not isinstance(signals, list)
        or not signals
        or any(not isinstance(item, str) for item in signals)
        or len(set(signals)) != len(signals)
        or not set(signals).issubset(set(contract["allowed_signal_ids"]))
    ):
        raise GateFailure("RUNNER_FAIL", f"consumer signal set is invalid: {path}")
    implementation_path = require_text(record, "implementation_path")
    if not implementation_path.startswith("repo-pre-15/"):
        raise GateFailure("RUNNER_FAIL", f"consumer implementation is outside DUT: {path}")
    if any(
        implementation_path.startswith(prefix)
        for prefix in contract["prohibited_implementation_prefixes"]
    ):
        raise GateFailure("RUNNER_FAIL", f"consumer implementation is nonqualifying: {path}")
    if implementation_path not in entries:
        raise GateFailure("RUNNER_FAIL", f"consumer implementation is not tracked: {path}")
    implementation = source_at(commit, implementation_path)
    implementation_hash = sha256_bytes(implementation)
    if implementation_hash != require_text(record, "implementation_sha256"):
        raise GateFailure("RUNNER_FAIL", f"consumer implementation hash mismatch: {path}")
    implementation_text = implementation.decode("utf-8", errors="replace").lower()
    if "sysctl" not in implementation_text:
        raise GateFailure("RUNNER_FAIL", f"consumer implementation does not invoke sysctl: {path}")
    missing_signals = [item for item in signals if item.lower() not in implementation_text]
    if missing_signals:
        raise GateFailure(
            "RUNNER_FAIL", f"consumer implementation omits signals {missing_signals}: {path}"
        )
    return {
        "manifest_path": path,
        "manifest_sha256": sha256_bytes(data),
        "consumer_id": record["consumer_id"],
        "consumer_class": consumer_class,
        "implementation_path": implementation_path,
        "implementation_sha256": implementation_hash,
        "signal_ids": signals,
        "access_mode": access_mode,
        "production_use": True,
    }


def main() -> int:
    if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-030":
        raise GateFailure("RUNNER_FAIL", "invalid P15-030 input identity")
    if SPEC.get("gate") != "G08" or SPEC.get("batch") != "B37a":
        raise GateFailure("RUNNER_FAIL", "invalid G08/B37a input identity")
    resolved = git_text("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
    if resolved != SPEC["required_base"]:
        raise GateFailure(
            "INFRA_BLOCKED",
            f"P15-030 must replay {SPEC['required_base']}, got {resolved}",
        )
    for path, expected_oid in SPEC["scope_tree_oids"].items():
        actual_oid = git_text("rev-parse", f"{resolved}:{path}")
        if actual_oid != expected_oid:
            raise GateFailure("INFRA_BLOCKED", f"frozen scope tree changed: {path}")

    source_hashes = {
        path: sha256_bytes(source_at(resolved, path))
        for path in SPEC["source_sha256"]
    }
    if source_hashes != SPEC["source_sha256"]:
        raise GateFailure("INFRA_BLOCKED", "frozen G08 source identity changed")
    write_json(OUTPUT / "source-sha256.json", source_hashes)
    addendum_paths = (
        "repo-pre-15/tests/pre15/gates/P15-030.sh",
        "repo-pre-15/tests/pre15/gates/P15-030-input.json",
        "repo-pre-15/docs/pre15-stage0/P15-030.md",
    )
    addendum_hashes = {
        path: sha256_bytes((ROOT / path).read_bytes()) for path in addendum_paths
    }
    write_json(OUTPUT / "gate-addendum-sha256.json", addendum_hashes)

    entries = parse_ls_tree(
        run_bytes(
            [
                "git",
                "-C",
                str(ROOT),
                "ls-tree",
                "-r",
                "--long",
                "-z",
                resolved,
                "--",
                "repo-pre-15",
                "planning/pre15",
            ]
        )
    )
    prohibited = set(SPEC["prohibited_paths"])
    scoped_entries = {path: value for path, value in entries.items() if path not in prohibited}
    if len(scoped_entries) != SPEC["consumer_inventory"]["expected_scoped_path_count"]:
        raise GateFailure("INFRA_BLOCKED", "frozen consumer scope path count changed")

    reference_paths = grep_paths(
        resolved, SPEC["consumer_inventory"]["reference_terms"]
    )
    maximum_bytes = SPEC["consumer_inventory"]["maximum_candidate_bytes"]
    references = []
    reference_counts = {
        "planning-proposal": 0,
        "historical-evidence": 0,
        "test-only": 0,
        "project-report-or-documentation": 0,
        "possible-operational-consumer": 0,
    }
    for path in reference_paths:
        if path in prohibited:
            raise GateFailure("RUNNER_FAIL", "prohibited path entered consumer scan")
        entry = scoped_entries[path]
        if entry["size"] > maximum_bytes:
            raise GateFailure("INFRA_BLOCKED", f"consumer candidate exceeds bound: {path}")
        data = source_at(resolved, path)
        classification = classify_reference(path)
        reference_counts[classification] += 1
        references.append(
            {
                "path": path,
                "blob_oid": entry["oid"],
                "bytes": entry["size"],
                "sha256": sha256_bytes(data),
                "classification": classification,
                "qualifies": False,
                "reason": {
                    "planning-proposal": "proposal or audit text is not an existing consumer",
                    "historical-evidence": "historical gate output is not an existing consumer",
                    "test-only": "a test or gate is explicitly nonqualifying",
                    "project-report-or-documentation": "documentation is not an implemented consumer",
                    "possible-operational-consumer": "requires a validated consumer declaration",
                }[classification],
            }
        )
    expected_counts = SPEC["consumer_inventory"]["expected_reference_counts"]
    if len(reference_paths) != expected_counts["total"]:
        raise GateFailure("INFRA_BLOCKED", "frozen sysctl/sysfs reference count changed")
    for classification, count in reference_counts.items():
        if count != expected_counts[classification]:
            raise GateFailure(
                "INFRA_BLOCKED", f"frozen reference classification changed: {classification}"
            )

    marker = SPEC["consumer_contract"]["schema_marker"]
    manifest_paths = grep_paths(resolved, [marker], fixed=True)
    consumers = []
    manifest_hashes = {}
    for path in manifest_paths:
        entry = scoped_entries[path]
        if entry["size"] > maximum_bytes:
            raise GateFailure("INFRA_BLOCKED", f"consumer manifest exceeds bound: {path}")
        data = source_at(resolved, path)
        manifest_hashes[path] = sha256_bytes(data)
        consumers.append(validate_consumer_manifest(path, data, scoped_entries, resolved))
    if manifest_hashes != SPEC["approved_consumer_manifest_sha256"]:
        raise GateFailure("INFRA_BLOCKED", "declared consumer inventory changed")

    inventory = {
        "schema": 1,
        "base": resolved,
        "scope": SPEC["consumer_inventory"]["scope"],
        "scoped_path_count": len(scoped_entries),
        "prohibited_path_read": False,
        "reference_terms": SPEC["consumer_inventory"]["reference_terms"],
        "reference_path_count": len(reference_paths),
        "reference_classification_counts": reference_counts,
        "references": references,
        "consumer_manifest_marker": marker,
        "consumer_manifest_count": len(consumers),
        "qualified_consumer_count": len(consumers),
        "qualified_consumers": consumers,
        "nonqualifying_examples": SPEC["consumer_contract"]["nonqualifying_examples"],
    }
    write_json(OUTPUT / "consumer-inventory.json", inventory)

    consumer_present = bool(consumers)
    conditions = [
        {
            "id": "concrete-existing-diagnostic-or-operational-consumer",
            "status": "PASS" if consumer_present else "STOP",
            "observation": (
                f"{len(consumers)} qualified consumer declaration(s)"
                if consumer_present
                else "zero qualified consumers; all 54 broad references are planning, history, tests, or documentation"
            ),
        },
        {
            "id": "bounded-atomic-counters",
            "status": "NOT_RUN",
            "reason": "existing-consumer prerequisite is absent",
        },
        {
            "id": "freebsd15-native-sysctl-ctx-lifecycle",
            "status": "NOT_RUN",
            "reason": "existing-consumer prerequisite is absent; no prototype was built",
        },
        {
            "id": "stable-permissions-abi-and-no-unauthenticated-metadata-leak",
            "status": "NOT_RUN",
            "reason": "existing-consumer prerequisite is absent",
        },
    ]
    if consumer_present:
        raise GateFailure(
            "RUNNER_FAIL",
            "a consumer is now declared; run the FreeBSD 15 native lifecycle prototype before any GO",
        )
    if all(item["status"] == "PASS" for item in conditions):
        raise GateFailure("RUNNER_FAIL", "false-GO guard accepted incomplete conditions")

    result = {
        "schema": 1,
        "gate": "G08",
        "candidate": "P15-030",
        "batch": "B37a",
        "status": "STOP",
        "reason": "no concrete existing diagnostic or operational consumer",
        "requested_base": REQUESTED_BASE,
        "resolved_base": resolved,
        "deadline_seconds": DEADLINE,
        "expected_exit_code": 22,
        "conditions": conditions,
        "false_go_guard": "GO requires all four conditions PASS; evaluated false",
        "consumer_inventory": "consumer-inventory.json",
        "b37a": "STOP-NO-SOURCE",
        "dependency_closure": {
            "candidate": "P15-068",
            "batch": "B37b",
            "status": "STOP",
            "batch_status": "STOP-NO-SOURCE",
            "reason": "P15-068 requires the P15-030 sysctl transport, which is STOP",
            "independent_identity_format_verdict": "NOT_RUN",
            "second_sysctl_lifecycle": "NOT_RUN",
        },
        "prototype": "NOT_RUN",
        "builds": "NOT_RUN",
        "qemu": "NOT_RUN",
        "full_feature_suite": "NOT_RUN",
        "smoke_suite": "NOT_RUN",
        "production_source_changed": False,
        "p15_068_gate_addendum_created": False,
    }
    write_json(OUTPUT / "result.json", result)
    print(json.dumps(result, ensure_ascii=True, sort_keys=True))
    return 22


try:
    raise SystemExit(main())
except GateFailure as error:
    failure = {
        "schema": 1,
        "gate": "G08",
        "candidate": "P15-030",
        "status": error.status,
        "reason": error.reason,
        "requested_base": REQUESTED_BASE,
        "deadline_seconds": DEADLINE,
        "b37a": "NOT_RUN",
        "p15_068": "NOT_RUN",
        "qemu": "NOT_RUN",
        "production_source_changed": False,
    }
    write_json(OUTPUT / "result.json", failure)
    print(json.dumps(failure, ensure_ascii=True, sort_keys=True))
    raise SystemExit({"RUNNER_FAIL": 20, "INFRA_BLOCKED": 21}.get(error.status, 20))
PY
gate_rc=$?
set -e

cleanup_status=PASS
if ! rm -rf -- "$work"; then
	cleanup_status=FAIL
	gate_rc=20
fi
trap - EXIT HUP INT TERM
printf 'owned temporary path removed: %s\ncleanup=%s\n' \
    "$work" "$cleanup_status" >"$output/cleanup.log"

python3 -B - "$output" "$gate_rc" "$cleanup_status" <<'PY'
from __future__ import annotations

import hashlib
import json
from pathlib import Path
import sys


output = Path(sys.argv[1])
exit_code = int(sys.argv[2])
cleanup = sys.argv[3]
if cleanup != "PASS":
    status = "RUNNER_FAIL"
    origin = "runner"
elif exit_code == 22:
    status = "STOP"
    origin = "gate"
elif exit_code in (124, 137):
    status = "INFRA_BLOCKED"
    origin = "infrastructure"
else:
    status = "RUNNER_FAIL" if exit_code == 20 else "INFRA_BLOCKED"
    origin = "runner" if exit_code == 20 else "infrastructure"
(output / "attempt.json").write_text(
    json.dumps(
        {
            "schema": 1,
            "exit_code": exit_code,
            "status": status,
            "failure_origin": origin,
            "cleanup": cleanup,
            "target_marker": "reached" if status == "STOP" else "not_reached",
        },
        ensure_ascii=True,
        indent=2,
        sort_keys=True,
    ) + "\n",
    encoding="ascii",
)
lines = []
for path in sorted(output.iterdir(), key=lambda item: item.name):
    if path.is_file() and path.name != "SHA256SUMS":
        digest = hashlib.sha256(path.read_bytes()).hexdigest()
        lines.append(f"{digest}  {path.name}")
(output / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
PY

printf 'P15-030 gate exit=%s cleanup=%s output=%s\n' \
    "$gate_rc" "$cleanup_status" "$output"
exit "$gate_rc"
