#!/bin/sh
set -eu

: "${PRE15_DUT:?PRE15_DUT is required}"
: "${PRE15_ROOT:?PRE15_ROOT is required}"
: "${PRE15_RUN_DIR:?PRE15_RUN_DIR is required}"
: "${PRE15_LIB_DIR:?PRE15_LIB_DIR is required}"
. "$PRE15_LIB_DIR/runner.sh"

baseline=fb79d17edba7c47fae6e60b2a92771a8f3a2fcdf
zmap=$PRE15_DUT/src/zmap.c
internal=$PRE15_DUT/src/internal.h
linux_zmap=$PRE15_ROOT/src-linux/zmap.c
oracle=$PRE15_DUT/tests/pre15/fixtures/B07-map-oracle.json
gate_input=$PRE15_DUT/tests/pre15/gates/P15-006-input.json
artifacts=$PRE15_RUN_DIR/artifacts

for tool in git python3 sha256sum; do
	command -v "$tool" >/dev/null 2>&1 || \
		pre15_infra_blocked "missing B24 host tool: $tool"
done

pre15_record_fixture b24-zmap "$zmap"
pre15_record_fixture b24-internal "$internal"
pre15_record_fixture b24-linux-zmap "$linux_zmap"
pre15_record_fixture b24-map-oracle "$oracle"
pre15_record_fixture b24-gate-input "$gate_input"
pre15_record_fixture b24-case \
	"$PRE15_DUT/tests/pre15/cases/B24-zmap-order.sh"

mkdir -p "$artifacts"
pre15_target_reached

if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$oracle" \
    "$gate_input" "$artifacts/B24-zmap-order.json" <<'PY'
from __future__ import annotations

from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys


root = Path(sys.argv[1])
dut = Path(sys.argv[2])
baseline = sys.argv[3]
oracle_path = Path(sys.argv[4])
gate_input_path = Path(sys.argv[5])
report_path = Path(sys.argv[6])


def committed(path: str) -> str:
    completed = subprocess.run(
        ["git", "-C", str(root), "show", f"{baseline}:repo-pre-15/{path}"],
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    if completed.returncode != 0:
        raise SystemExit(f"cannot read B24 baseline {path}: {completed.stderr}")
    return completed.stdout


def function(source: str, name: str) -> str:
    match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
    if match is None:
        raise SystemExit(f"missing function: {name}")
    name_line = source.rfind("\n", 0, match.start()) + 1
    start = source.rfind("\n", 0, name_line - 1) + 1
    brace = source.find("{", match.end())
    if brace < 0:
        raise SystemExit(f"missing function body: {name}")
    depth = 0
    state = "code"
    index = brace
    while index < len(source):
        char = source[index]
        following = source[index + 1] if index + 1 < len(source) else ""
        if state == "code":
            if char == "/" and following == "*":
                state = "block"
                index += 2
                continue
            if char == "/" and following == "/":
                state = "line"
                index += 2
                continue
            if char == '"':
                state = "string"
            elif char == "'":
                state = "character"
            elif char == "{":
                depth += 1
            elif char == "}":
                depth -= 1
                if depth == 0:
                    return source[start : index + 1]
        elif state == "block" and char == "*" and following == "/":
            state = "code"
            index += 2
            continue
        elif state == "line" and char == "\n":
            state = "code"
        elif state in {"string", "character"}:
            if char == "\\":
                index += 2
                continue
            if (state == "string" and char == '"') or (
                state == "character" and char == "'"
            ):
                state = "code"
        index += 1
    raise SystemExit(f"unterminated function: {name}")


function_order = [
    "z_erofs_index_base",
    "z_erofs_index_advance",
    "z_erofs_lcluster_count",
    "z_erofs_lcluster_pos",
    "z_erofs_lcn_advance",
    "z_erofs_compact_pblk",
    "z_erofs_fragment_offset",
    "z_erofs_post_eof_len",
    "z_erofs_physical_end",
    "z_erofs_read_index",
    "z_erofs_load_full_lcluster",
    "decode_compactedbits",
    "get_compacted_la_distance",
    "z_erofs_load_compact_lcluster",
    "z_erofs_load_lcluster_from_disk",
    "z_erofs_extent_lookback",
    "z_erofs_get_extent_compressedlen",
    "z_erofs_get_extent_decompressedlen",
    "z_erofs_extent_add",
    "z_erofs_extent_roundup",
    "z_erofs_extent_table_pos",
    "z_erofs_extent_record_pos",
    "z_erofs_read_extent",
    "z_erofs_extent_lstart",
    "z_erofs_validate_extent_table",
    "z_erofs_map_blocks_fo",
    "z_erofs_map_blocks_ext",
    "z_erofs_fill_inode",
    "z_erofs_map_sanity_check",
    "z_erofs_map_blocks",
]
linux_algorithm_order = [
    "z_erofs_load_full_lcluster",
    "decode_compactedbits",
    "get_compacted_la_distance",
    "z_erofs_load_compact_lcluster",
    "z_erofs_load_lcluster_from_disk",
    "z_erofs_extent_lookback",
    "z_erofs_get_extent_compressedlen",
    "z_erofs_get_extent_decompressedlen",
    "z_erofs_map_blocks_fo",
    "z_erofs_map_blocks_ext",
    "z_erofs_fill_inode",
    "z_erofs_map_sanity_check",
]

current = (dut / "src/zmap.c").read_text(encoding="utf-8")
before = committed("src/zmap.c")
current_internal = (dut / "src/internal.h").read_text(encoding="utf-8")
before_internal = committed("src/internal.h")
linux = (root / "src-linux/zmap.c").read_text(encoding="utf-8")

moved_start = before.index("static int\nz_erofs_extent_add(")
moved_end = before.index("static int\nz_erofs_map_blocks_ext(", moved_start)
moved = before[moved_start:moved_end]
without_moved = before[:moved_start] + before[moved_end:]
insert_at = without_moved.index("static int\nz_erofs_map_blocks_fo(")
expected = without_moved[:insert_at] + moved + without_moved[insert_at:]
if current != expected:
    raise SystemExit("zmap.c differs from the exact B24 validator-group move")

before_functions = {name: function(before, name) for name in function_order}
current_functions = {name: function(current, name) for name in function_order}
for name in function_order:
    if current_functions[name] != before_functions[name]:
        raise SystemExit(f"B24 changed function text instead of moving it: {name}")

def definition_position(source: str, name: str) -> int:
    match = re.search(
        r"^(?:" + re.escape(name) + r"|(?:static\s+)?"
        r"[A-Za-z_][A-Za-z0-9_ *]*\b" + re.escape(name) + r")\s*\(",
        source,
        re.MULTILINE,
    )
    if match is None:
        raise SystemExit(f"missing definition position: {name}")
    return match.start()


positions = [definition_position(current, name) for name in function_order]
if positions != sorted(positions):
    raise SystemExit("B24 zmap definition order does not match the reviewed order")
for source, label in ((current, "FreeBSD"), (linux, "Linux")):
    positions = [definition_position(source, name) for name in linux_algorithm_order]
    if positions != sorted(positions):
        raise SystemExit(f"{label} common zmap algorithm order drifted")

def direct_calls(functions: dict[str, str]) -> Counter[tuple[str, str]]:
    calls: Counter[tuple[str, str]] = Counter()
    for caller, body in functions.items():
        brace = body.find("{")
        for callee in re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", body[brace + 1 :]):
            if callee not in {"if", "for", "while", "switch", "return", "sizeof"}:
                calls[(caller, callee)] += 1
    return calls


before_calls = direct_calls(before_functions)
current_calls = direct_calls(current_functions)
if current_calls != before_calls:
    raise SystemExit("B24 changed the direct-call multiset")

global_definitions = sorted(
    name for name, body in current_functions.items() if not body.startswith("static ")
)
if global_definitions != ["z_erofs_fill_inode", "z_erofs_map_blocks"]:
    raise SystemExit(f"unexpected zmap global definitions: {global_definitions}")
if current_internal != before_internal:
    raise SystemExit("B24 changed internal.h despite no removable zmap global")
inode_source = (dut / "src/inode.c").read_text(encoding="utf-8")
data_source = (dut / "src/data.c").read_text(encoding="utf-8")
if inode_source.count("z_erofs_fill_inode(sbi, vi)") != 1:
    raise SystemExit("eager z_erofs_fill_inode no longer has its inode consumer")
if data_source.count("z_erofs_map_blocks(sbi, vi, &next)") != 1:
    raise SystemExit("compressed map backend no longer has its common adapter consumer")
for prototype in (
    "int z_erofs_fill_inode(struct erofs_sb_info *sbi, struct erofs_inode *vi);",
    "int z_erofs_map_blocks(struct erofs_sb_info *sbi, struct erofs_inode *vi,\n"
    "    struct erofs_map_blocks *map);",
):
    if current_internal.count(prototype) != 1:
        raise SystemExit(f"required FreeBSD zmap prototype drifted: {prototype!r}")
if "int z_erofs_map_blocks_iter(struct inode *inode, struct erofs_map_blocks *map," not in linux:
    raise SystemExit("Linux zmap iterator naming anchor changed")

for marker in ("erofs_read_metadata(", "erofs_put_metabuf(", "goto out;"):
    if current.count(marker) != before.count(marker):
        raise SystemExit(f"B24 changed provider I/O or cleanup marker count: {marker}")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", current):
    raise SystemExit("B24 introduced Linux negative errno")

oracle = json.loads(oracle_path.read_text(encoding="ascii"))
gate_input = json.loads(gate_input_path.read_text(encoding="ascii"))
records = oracle.get("records", [])
if oracle.get("map_case_count") != 80 or len(records) != 80:
    raise SystemExit("frozen G02/B07 map tuple denominator changed")
if gate_input.get("expected_device_case_count") != 13:
    raise SystemExit("frozen G02/B07 device denominator changed")
tuple_bytes = b"".join(bytes.fromhex(record["tuple_hex"]) for record in records)
tuple_digest = hashlib.sha256(tuple_bytes).hexdigest()
if tuple_digest != oracle.get("tuple_bytes_sha256"):
    raise SystemExit("frozen G02/B07 tuple bytes no longer match their digest")
if len({record["id"] for record in records}) != 80:
    raise SystemExit("frozen G02/B07 tuple IDs are not unique")

changed = set(
    subprocess.run(
        ["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
    ).stdout.splitlines()
)
changed.update(
    subprocess.run(
        [
            "git", "-C", str(root), "ls-files", "--others", "--exclude-standard",
            "--", "repo-pre-15",
        ],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
    ).stdout.splitlines()
)
allowed = {
    "repo-pre-15/src/zmap.c",
    "repo-pre-15/tests/pre15/cases/B24-zmap-order.sh",
}
if changed != allowed:
    raise SystemExit(f"B24 write set differs: {sorted(changed ^ allowed)}")

function_digest = hashlib.sha256(
    "\n\n".join(current_functions[name] for name in function_order).encode("utf-8")
).hexdigest()
report = {
    "baseline": baseline,
    "batch": "B24",
    "candidate": "P15-016",
    "direct_call_edges": sum(current_calls.values()),
    "freebsd_backend_name": "z_erofs_map_blocks",
    "freebsd_positive_errno": True,
    "function_count": len(function_order),
    "function_text_sha256": function_digest,
    "global_definitions": global_definitions,
    "linux_iterator_name": "z_erofs_map_blocks_iter",
    "map_cases": len(records),
    "device_cases": gate_input["expected_device_case_count"],
    "provider_io_unchanged": True,
    "metadata_cleanup_unchanged": True,
    "status": "PASS",
    "tuple_bytes_sha256": tuple_digest,
    "write_set": sorted(changed),
}
report_path.write_text(
    json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
    encoding="ascii",
)
print(
    "B24 order/callgraph PASS "
    f"functions={len(function_order)} calls={sum(current_calls.values())} globals=2"
)
print(
    "B24 map tuples PASS "
    f"maps={len(records)} devices={gate_input['expected_device_case_count']} "
    f"sha256={tuple_digest}"
)
PY
then
	:
else
	pre15_dut_fail 'B24 order, callgraph, visibility, or tuple proof failed'
fi

sha256sum "$artifacts/B24-zmap-order.json" >"$artifacts/SHA256SUMS"
printf '%s\n' \
	'B24 host PASS exact validator move and Linux algorithm order' \
	'B24 host PASS frozen G02 map tuples; QEMU and full feature suite NOT_RUN'
