#!/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-045-input.json
base=
output=
deadline=90

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-045.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": [],
            "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 re
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=30,
        )
    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, allowed: set[int] | None = None) -> str:
    return run_bytes(["git", "-C", str(ROOT), *args], allowed).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 truthy(value: Any) -> bool:
    if value is True:
        return True
    if isinstance(value, str):
        return value.strip().lower() in {"true", "yes", "required", "enabled", "1"}
    if isinstance(value, int):
        return value == 1
    return False


def flattened(value: Any, prefix: str = "") -> list[tuple[str, Any]]:
    records: list[tuple[str, Any]] = []
    if isinstance(value, dict):
        for key, child in value.items():
            path = f"{prefix}.{key}" if prefix else str(key)
            records.append((path.lower().replace("-", "_"), child))
            records.extend(flattened(child, path))
    elif isinstance(value, list):
        for index, child in enumerate(value):
            records.extend(flattened(child, f"{prefix}[{index}]"))
    return records


def json_demand_witness(value: Any) -> dict[str, Any] | None:
    if not isinstance(value, dict):
        return None
    records = flattened(value)
    kind = any(
        any(token in key for token in ("manifest_type", "manifest_kind", "document_type"))
        and isinstance(item, str)
        and any(token in item.lower() for token in ("support", "deploy"))
        for key, item in records
    )
    version = any(
        "version" in key and isinstance(item, (str, int)) and str(item).strip()
        for key, item in records
    )
    targets = any(
        any(token in key for token in ("deployment_target", "support_target", "product_target"))
        and ((isinstance(item, str) and item.strip()) or (isinstance(item, list) and item))
        for key, item in records
    )
    required = any(
        "zstd" in key
        and any(token in key for token in ("default", "required", "out_of_box", "outofthebox"))
        and truthy(item)
        for key, item in records
    )
    if kind and version and targets and required:
        return {
            "format": "json",
            "manifest_kind": True,
            "version": True,
            "deployment_targets": True,
            "zstd_default_required": True,
        }
    return None


def text_demand_witness(text: str) -> dict[str, Any] | None:
    fields: dict[str, str] = {}
    for line in text.splitlines():
        match = re.fullmatch(r"([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)", line)
        if match is not None:
            fields[match.group(1).lower().replace("-", "_")] = match.group(2).strip()
    kind = fields.get("manifest_type", "").lower()
    version = fields.get("manifest_version", "")
    targets = fields.get("deployment_targets", "") or fields.get("support_targets", "")
    required = fields.get("zstd_default_required", "")
    if any(token in kind for token in ("support", "deploy")) and version and targets and truthy(required):
        return {
            "format": "text-header",
            "manifest_kind": True,
            "version": True,
            "deployment_targets": True,
            "zstd_default_required": True,
        }
    return None


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) -> list[str]:
    raw = run_bytes(
        [
            "git", "-C", str(ROOT), "grep", "-I", "-l", "-i",
            "-e", "zstd", "-e", "zstandard", commit, "--",
            "planning/pre15/evidence",
        ],
        {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 main() -> int:
    if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-045":
        raise GateFailure("RUNNER_FAIL", "invalid P15-045 input identity")
    if SPEC.get("gate") != "G07" or SPEC.get("batch") != "B35":
        raise GateFailure("RUNNER_FAIL", "invalid G07/B35 input identity")
    resolved = git_text("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
    if resolved != SPEC["required_base"]:
        raise GateFailure(
            "INFRA_BLOCKED",
            f"P15-045 must replay {SPEC['required_base']}, got {resolved}",
        )
    evidence_tree = git_text("rev-parse", f"{resolved}:planning/pre15/evidence")
    if evidence_tree != SPEC["evidence_tree_oid"]:
        raise GateFailure("INFRA_BLOCKED", "frozen project evidence tree changed")

    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 G07 source identity changed")
    write_json(OUTPUT / "source-sha256.json", source_hashes)
    addendum_paths = (
        "repo-pre-15/tests/pre15/gates/P15-045.sh",
        "repo-pre-15/tests/pre15/gates/P15-045-input.json",
        "repo-pre-15/docs/pre15-stage0/P15-045.md",
    )
    addendum_hashes = {
        path: sha256_bytes((ROOT / path).read_bytes()) for path in addendum_paths
    }
    write_json(OUTPUT / "gate-addendum-sha256.json", addendum_hashes)

    tree_entries = parse_ls_tree(
        run_bytes(
            [
                "git", "-C", str(ROOT), "ls-tree", "-r", "--long", "-z",
                resolved, "--", "planning/pre15/evidence",
            ]
        )
    )
    if len(tree_entries) != SPEC["evidence_tracked_path_count"]:
        raise GateFailure("INFRA_BLOCKED", "frozen evidence path count changed")
    prohibited = set(SPEC["prohibited_paths"])
    if prohibited.intersection(tree_entries):
        raise GateFailure("RUNNER_FAIL", "prohibited path entered evidence scan")

    zstd_paths = grep_paths(resolved)
    maximum_bytes = SPEC["demand_manifest_contract"]["maximum_text_candidate_bytes"]
    demand_terms = tuple(SPEC["demand_manifest_contract"]["demand_terms"])
    candidates = []
    qualified = []
    oversized = []
    for path in zstd_paths:
        entry = tree_entries[path]
        if entry["size"] > maximum_bytes:
            oversized.append({"path": path, **entry})
            continue
        data = source_at(resolved, path)
        text = data.decode("utf-8", errors="replace")
        lowered = text.lower()
        signals = sorted(term for term in demand_terms if term.lower() in lowered)
        if not signals:
            continue
        witness = None
        if path.endswith(".json"):
            try:
                witness = json_demand_witness(json.loads(text))
            except json.JSONDecodeError:
                witness = None
        if witness is None:
            witness = text_demand_witness(text)
        record = {
            "path": path,
            "blob_oid": entry["oid"],
            "bytes": entry["size"],
            "sha256": sha256_bytes(data),
            "demand_signals": signals,
            "qualification": witness,
            "disposition": (
                "QUALIFIED versioned deployment-demand manifest"
                if witness is not None
                else "test/build/planning evidence; no explicit versioned deployment demand"
            ),
        }
        candidates.append(record)
        if witness is not None:
            qualified.append(record)

    if oversized:
        write_json(OUTPUT / "oversized-candidates.json", oversized)
        raise GateFailure(
            "INFRA_BLOCKED", "a Zstd evidence candidate exceeds the bounded review size"
        )

    approved = SPEC["approved_demand_manifest_sha256"]
    actual_approved = {record["path"]: record["sha256"] for record in qualified}
    if actual_approved != approved:
        raise GateFailure(
            "INFRA_BLOCKED", "qualified demand inventory differs from frozen gate input"
        )

    makefile = source_at(resolved, "repo-pre-15/src/Makefile").decode("utf-8")
    zstd_source = source_at(
        resolved, "repo-pre-15/src/decompressor_zstd.c"
    ).decode("utf-8")
    readme = source_at(resolved, "repo-pre-15/README.md").decode("utf-8")
    manual = source_at(resolved, "repo-pre-15/docs/erofs.5").decode("utf-8")
    policy_checks = {
        "makefile_default_is_opt_in_zero": makefile.count("WITH_ZSTDIO?= 0") == 1,
        "enabled_build_defines_zstdio": "CFLAGS.decompressor_zstd.c+= -DZSTDIO" in makefile,
        "disabled_stub_returns_eopnotsupp": (
            "#else\nstatic int\nz_erofs_zstd_decompress" in zstd_source
            and "return (EOPNOTSUPP);\n}\n#endif" in zstd_source
        ),
        "readme_names_kernel_option": "kernel built with `options ZSTDIO`" in readme,
        "manual_names_kernel_option": '.Cd "options ZSTDIO"' in manual,
    }
    if not all(policy_checks.values()):
        raise GateFailure("INFRA_BLOCKED", "current opt-in ZSTDIO contract changed")

    path_inventory = "\0".join(zstd_paths).encode("utf-8")
    candidate_inventory = "\0".join(
        record["path"] for record in candidates
    ).encode("utf-8")
    demand_scan = {
        "schema": 1,
        "base": resolved,
        "evidence_tree_oid": evidence_tree,
        "tracked_path_count": len(tree_entries),
        "zstd_text_path_count": len(zstd_paths),
        "zstd_text_path_inventory_sha256": sha256_bytes(path_inventory),
        "demand_candidate_count": len(candidates),
        "demand_candidate_path_inventory_sha256": sha256_bytes(candidate_inventory),
        "qualified_manifest_count": len(qualified),
        "qualified_manifests": qualified,
        "candidates": candidates,
        "contract": SPEC["demand_manifest_contract"],
        "prohibited_path_read": False,
    }
    write_json(OUTPUT / "demand-scan.json", demand_scan)

    demand_present = bool(qualified)
    conditions = [
        {
            "id": "versioned-deployment-demand",
            "status": "PASS" if demand_present else "STOP",
            "observation": (
                f"{len(qualified)} qualified manifest(s)"
                if demand_present
                else "zero qualifying manifests in the frozen project evidence tree"
            ),
        },
        {
            "id": "disabled-runtime-exact-EOPNOTSUPP",
            "status": "NOT_RUN",
            "reason": "deployment-demand prerequisite is absent",
        },
        {
            "id": "enabled-real-reproducible-Zstd-EROFS-read",
            "status": "NOT_RUN",
            "reason": "deployment-demand prerequisite is absent",
        },
        {
            "id": "FreeBSD-kernel-ZSTDIO-symbol-capability-model",
            "status": "NOT_RUN",
            "reason": "deployment-demand prerequisite is absent; no dependency KLD is claimed",
        },
        {
            "id": "KLD-size-delta-at-most-64KiB-and-10-percent",
            "status": "NOT_RUN",
            "reason": "deployment-demand prerequisite is absent",
        },
    ]
    all_go = all(item["status"] == "PASS" for item in conditions)
    if all_go:
        raise GateFailure(
            "RUNNER_FAIL", "false-GO guard: downstream runtime probes were not executed"
        )
    result = {
        "schema": 1,
        "gate": "G07",
        "candidate": "P15-045",
        "batch": "B35",
        "status": "STOP",
        "reason": "no concrete versioned support/deployment manifest requires Zstd enabled out of the box",
        "requested_base": REQUESTED_BASE,
        "resolved_base": resolved,
        "deadline_seconds": DEADLINE,
        "expected_exit_code": 22,
        "conditions": conditions,
        "false_go_guard": "GO requires every condition PASS; evaluated false",
        "current_policy_static_checks": policy_checks,
        "with_zstdio_default": 0,
        "b35": "STOP-NO-SOURCE",
        "builds": "NOT_RUN",
        "qemu": "NOT_RUN",
        "full_feature_suite": "NOT_RUN",
        "smoke_suite": "NOT_RUN",
        "generic_dependency_kld_claimed": False,
        "production_source_changed": 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": "G07",
        "candidate": "P15-045",
        "status": error.status,
        "reason": error.reason,
        "requested_base": REQUESTED_BASE,
        "deadline_seconds": DEADLINE,
        "b35": "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"
    origin = "runner"
(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-045 gate exit=%s cleanup=%s output=%s\n' \
    "$gate_rc" "$cleanup_status" "$output"
exit "$gate_rc"
