#!/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"

internal=$PRE15_DUT/src/internal.h
data=$PRE15_DUT/src/data.c
inode=$PRE15_DUT/src/inode.c
super=$PRE15_DUT/src/super.c
zmap=$PRE15_DUT/src/zmap.c
linux_internal=$PRE15_ROOT/src-linux/internal.h
artifacts=$PRE15_RUN_DIR/artifacts

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

pre15_record_fixture b05-freebsd-internal "$internal"
pre15_record_fixture b05-freebsd-data "$data"
pre15_record_fixture b05-freebsd-inode "$inode"
pre15_record_fixture b05-freebsd-super "$super"
pre15_record_fixture b05-freebsd-zmap "$zmap"
pre15_record_fixture b05-linux-internal "$linux_internal"

mkdir -p "$artifacts"
pre15_target_reached

if python3 - "$PRE15_ROOT" "$artifacts/field-check.json" <<'PY'
from __future__ import annotations

import json
from pathlib import Path
import re
import sys


root = Path(sys.argv[1])
output = Path(sys.argv[2])
dut_src = root / "repo-pre-15" / "src"
internal = (dut_src / "internal.h").read_text(encoding="utf-8")
data = (dut_src / "data.c").read_text(encoding="utf-8")
inode = (dut_src / "inode.c").read_text(encoding="utf-8")
super_source = (dut_src / "super.c").read_text(encoding="utf-8")
zmap = (dut_src / "zmap.c").read_text(encoding="utf-8")
linux = (root / "src-linux" / "internal.h").read_text(encoding="utf-8")


def struct_body(source: str, name: str) -> str:
    match = re.search(
        rf"^struct {re.escape(name)} \{{\n(?P<body>.*?)^\}};",
        source,
        re.MULTILINE | re.DOTALL,
    )
    if not match:
        raise SystemExit(f"missing struct {name}")
    return match.group("body")


def fields(source: str, name: str) -> list[str]:
    result = []
    for line in struct_body(source, name).splitlines():
        declaration = line.strip()
        if not declaration or declaration.startswith("/*"):
            continue
        match = re.search(r"([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[[^]]+\])?;$", declaration)
        if not match:
            raise SystemExit(f"unparsed field in struct {name}: {declaration!r}")
        result.append(match.group(1))
    return result


freebsd_device = fields(internal, "erofs_device_info")
linux_device = fields(linux, "erofs_device_info")
expected_device = [
    "devvp",
    "dev",
    "cp",
    "mediasize",
    "sectorsize",
    "blocks",
    "uniaddr",
]
if freebsd_device != expected_device:
    raise SystemExit(f"FreeBSD device field order mismatch: {freebsd_device!r}")
if [field for field in linux_device if field in {"blocks", "uniaddr"}] != [
    "blocks",
    "uniaddr",
]:
    raise SystemExit(f"Linux device common-field order mismatch: {linux_device!r}")

freebsd_map = fields(internal, "erofs_map_dev")
linux_map = fields(linux, "erofs_map_dev")
expected_map = ["m_dif", "m_pa", "m_deviceid", "m_plen"]
if freebsd_map != expected_map:
    raise SystemExit(f"FreeBSD map field order mismatch: {freebsd_map!r}")
if [field for field in linux_map if field in {"m_dif", "m_pa", "m_deviceid"}] != [
    "m_dif",
    "m_pa",
    "m_deviceid",
]:
    raise SystemExit(f"Linux map common-field order mismatch: {linux_map!r}")

inode_fields = fields(internal, "erofs_inode")
for removed in ("ino", "inline_data"):
    if removed in inode_fields:
        raise SystemExit(f"removed inode field remains: {removed}")
if "m_sbi" in freebsd_map:
    raise SystemExit("removed map back-pointer remains")

removed_uses = {
    "map back-pointer": sum(
        source.count("m_sbi") for source in (internal, data, inode, super_source, zmap)
    ),
    "saved ondisk ino": len(re.findall(r"\bvi->ino\b", inode)),
    "inline-data cache": len(re.findall(r"\bvi->inline_data\b", inode)),
}
if any(removed_uses.values()):
    raise SystemExit(f"removed field use remains: {removed_uses!r}")

helper = re.search(
    r"erofs_fill_from_devinfo\(struct erofs_map_dev \*map,\n"
    r"    struct erofs_device_info \*dif, erofs_off_t pa\)",
    data,
)
if not helper:
    raise SystemExit("map helper still accepts the removed super back-pointer")
if len(re.findall(r"erofs_fill_from_devinfo\(", data)) != 4:
    raise SystemExit("unexpected map helper definition/call count")

initializer = re.search(
    r"map = \(struct erofs_map_dev\) \{\n"
    r"\t\t\.m_pa = off,\n"
    r"\t\t\.m_deviceid = device_id,\n"
    r"\t\t\.m_plen = len,\n"
    r"\t\};",
    data,
)
if not initializer:
    raise SystemExit("erofs_map_dev initializer order mismatch")

required_markers = {
    "raw nid identity": "vi->nid = nid;",
    "layout source": "vi->datalayout = erofs_inode_datalayout(ifmt);",
    "device vnode": "struct vnode *devvp;",
    "GEOM consumer": "struct g_consumer *cp;",
    "provider media size": "uint64_t mediasize;",
    "provider sector size": "uint32_t sectorsize;",
    "future volume label": "char volume_name[17];",
    "deferred LZ4 state": "struct erofs_sb_lz4_info lz4;",
}
for label, marker in required_markers.items():
    if marker not in internal and marker not in inode:
        raise SystemExit(f"required ownership/source marker missing: {label}")

result = {
    "freebsd_device_fields": freebsd_device,
    "freebsd_map_fields": freebsd_map,
    "linux_device_common_fields": [
        field for field in linux_device if field in {"blocks", "uniaddr"}
    ],
    "linux_map_common_fields": [
        field for field in linux_map if field in {"m_dif", "m_pa", "m_deviceid"}
    ],
    "removed_field_uses": removed_uses,
    "retained_freebsd_ownership_fields": [
        "devvp",
        "dev",
        "cp",
        "mediasize",
        "sectorsize",
    ],
}
with output.open("x", encoding="ascii") as stream:
    json.dump(result, stream, ensure_ascii=True, indent=2, sort_keys=True)
    stream.write("\n")
print("fields PASS device=7 map=4 removed-uses=0")
PY
then
	:
else
	pre15_dut_fail 'B05 field layout or write-only closure oracle failed'
fi

sha256sum "$artifacts/field-check.json" > "$artifacts/SHA256SUMS"
printf 'B05 PASS private field model and zero-consumer closure\n'
