#!/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=1b4b8abc249d9bbbb75e19b5259c5f46f91909ef
artifacts=$PRE15_RUN_DIR/artifacts

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

for source in internal.h compress.h decompressor.c decompressor_lz4.c \
    decompressor_lzma.c decompressor_deflate.c decompressor_zstd.c zdata.c zmap.c; do
	pre15_record_fixture "b30-$source" "$PRE15_DUT/src/$source"
done
pre15_record_fixture b30-linux-compress "$PRE15_ROOT/src-linux/compress.h"
pre15_record_fixture b30-linux-decompressor "$PRE15_ROOT/src-linux/decompressor.c"
pre15_record_fixture b30-case \
	"$PRE15_DUT/tests/pre15/cases/B30-codec-structure.sh"

mkdir -p "$artifacts"
pre15_target_reached

if python3 -B - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" \
    "$artifacts/B30-codec-structure.json" <<'PY'
from __future__ import annotations

from collections import Counter
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]
report_path = Path(sys.argv[4])
src = dut / "src"


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 B30 baseline {path}: {completed.stderr}")
    return completed.stdout


def replace_once(source: str, old: str, new: str, label: str) -> str:
    count = source.count(old)
    if count != 1:
        raise SystemExit(f"B30 baseline anchor count for {label}: {count}")
    return source.replace(old, new, 1)


expected_changed = {
    "repo-pre-15/src/compress.h",
    "repo-pre-15/src/decompressor.c",
    "repo-pre-15/src/decompressor_deflate.c",
    "repo-pre-15/src/decompressor_lzma.c",
    "repo-pre-15/src/decompressor_zstd.c",
    "repo-pre-15/src/internal.h",
    "repo-pre-15/src/zdata.c",
    "repo-pre-15/tests/pre15/cases/B30-codec-structure.sh",
}
changed = subprocess.run(
    ["git", "-C", str(root), "diff", "--name-only", baseline, "--", "repo-pre-15"],
    check=True,
    text=True,
    stdout=subprocess.PIPE,
).stdout.splitlines()
changed.extend(
    subprocess.run(
        [
            "git",
            "-C",
            str(root),
            "ls-files",
            "--others",
            "--exclude-standard",
            "--",
            "repo-pre-15",
        ],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
    ).stdout.splitlines()
)
if set(changed) != expected_changed or len(changed) != len(expected_changed):
    raise SystemExit(f"B30 repo-pre-15 write set mismatch: {changed!r}")

current = {
    path: (src / path).read_text(encoding="utf-8")
    for path in (
        "internal.h",
        "compress.h",
        "decompressor.c",
        "decompressor_lz4.c",
        "decompressor_lzma.c",
        "decompressor_deflate.c",
        "decompressor_zstd.c",
        "zdata.c",
        "zmap.c",
    )
}
before = {
    path: committed(f"src/{path}")
    for path in current
}

expected = before["internal.h"]
expected = replace_once(
    expected,
    "struct erofs_sb_lz4_info {\n"
    "\tuint16_t max_distance_pages;\n"
    "\tuint16_t max_pclusterblks;\n"
    "};\n\n",
    "",
    "LZ4 state type",
)
expected = replace_once(
    expected,
    "\tstruct erofs_sb_lz4_info lz4;\n",
    "",
    "LZ4 state member",
)
if current["internal.h"] != expected:
    raise SystemExit("internal.h differs from exact B30 dead-state removal")

expected = before["compress.h"]
expected = replace_once(
    expected,
    "struct z_erofs_decompressor {\n"
    "\tconst char *name;\n"
    "\tbool supports_subextent;\n"
    "\t/* Callbacks return zero or a positive FreeBSD errno. */\n"
    "\tint (*config)(struct erofs_sb_info *, const struct erofs_super_block *,\n"
    "\t    const void *, size_t);\n"
    "\tint (*decompress)(const struct z_erofs_decompress_req *);\n"
    "};",
    "struct z_erofs_decompressor {\n"
    "\t/* Callbacks return zero or a positive FreeBSD errno. */\n"
    "\tint (*config)(struct erofs_sb_info *, const struct erofs_super_block *,\n"
    "\t    const void *, size_t);\n"
    "\tint (*decompress)(const struct z_erofs_decompress_req *);\n"
    "\tbool supports_subextent;\n"
    "\tconst char *name;\n"
    "};",
    "descriptor fields",
)
if current["compress.h"] != expected:
    raise SystemExit("compress.h differs from exact B30 descriptor alignment")

expected = before["decompressor.c"]
for old, new, label in (
    ("\t\tdistance = le16toh(lz4->max_distance);\n", "", "unused cfg distance"),
    ("\t\tmax_pclusterblks = 1;\n\t\tsbi->available_compr_algs", "\t\tsbi->available_compr_algs", "legacy dead local store"),
    (
        "\tsbi->lz4.max_pclusterblks = max_pclusterblks;\n"
        "\tsbi->lz4.max_distance_pages = distance != 0 ?\n"
        "\t    howmany(distance, PAGE_SIZE) + 1 :\n"
        "\t    howmany(UINT16_MAX, PAGE_SIZE) + 1;\n",
        "",
        "runtime LZ4 stores",
    ),
    (
        "static const struct z_erofs_decompressor z_erofs_shifted_decomp = {\n"
        "\t.name = \"shifted\",\n"
        "\t.decompress = z_erofs_transform_plain,\n"
        "};",
        "static const struct z_erofs_decompressor z_erofs_shifted_decomp = {\n"
        "\t.decompress = z_erofs_transform_plain,\n"
        "\t.name = \"shifted\",\n"
        "};",
        "shifted descriptor",
    ),
    (
        "static const struct z_erofs_decompressor z_erofs_interlaced_decomp = {\n"
        "\t.name = \"interlaced\",\n"
        "\t.decompress = z_erofs_transform_plain,\n"
        "};",
        "static const struct z_erofs_decompressor z_erofs_interlaced_decomp = {\n"
        "\t.decompress = z_erofs_transform_plain,\n"
        "\t.name = \"interlaced\",\n"
        "};",
        "interlaced descriptor",
    ),
    (
        "static const struct z_erofs_decompressor z_erofs_lz4_decomp = {\n"
        "\t.name = \"lz4\",\n"
        "\t.supports_subextent = 1,\n"
        "\t.config = z_erofs_load_lz4_config,\n"
        "\t.decompress = z_erofs_lz4_decompress,\n"
        "};",
        "static const struct z_erofs_decompressor z_erofs_lz4_decomp = {\n"
        "\t.config = z_erofs_load_lz4_config,\n"
        "\t.decompress = z_erofs_lz4_decompress,\n"
        "\t.supports_subextent = 1,\n"
        "\t.name = \"lz4\",\n"
        "};",
        "LZ4 descriptor",
    ),
    (
        "static const struct z_erofs_decompressor * const z_erofs_decomp[] = {\n"
        "\t[Z_EROFS_COMPRESSION_LZ4] = &z_erofs_lz4_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_LZMA] = &z_erofs_lzma_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_DEFLATE] = &z_erofs_deflate_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_ZSTD] = &z_erofs_zstd_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_SHIFTED] = &z_erofs_shifted_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_INTERLACED] = &z_erofs_interlaced_decomp,\n"
        "};",
        "static const struct z_erofs_decompressor * const z_erofs_decomp[] = {\n"
        "\t[Z_EROFS_COMPRESSION_SHIFTED] = &z_erofs_shifted_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_INTERLACED] = &z_erofs_interlaced_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_LZ4] = &z_erofs_lz4_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_LZMA] = &z_erofs_lzma_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_DEFLATE] = &z_erofs_deflate_decomp,\n"
        "\t[Z_EROFS_COMPRESSION_ZSTD] = &z_erofs_zstd_decomp,\n"
        "};",
        "decompressor table",
    ),
):
    expected = replace_once(expected, old, new, label)
if current["decompressor.c"] != expected:
    raise SystemExit("decompressor.c differs from exact B30 structural edits")

for path, name, capability in (
    ("decompressor_lzma.c", "lzma", "1"),
    ("decompressor_deflate.c", "deflate", "1"),
    ("decompressor_zstd.c", "zstd", "0"),
):
    expected = before[path]
    expected = replace_once(
        expected,
        f"const struct z_erofs_decompressor z_erofs_{name}_decomp = {{\n"
        f"\t.name = \"{name}\",\n"
        f"\t.supports_subextent = {capability},\n"
        f"\t.config = z_erofs_load_{name}_config,\n"
        f"\t.decompress = z_erofs_{name}_decompress,\n"
        "};",
        f"const struct z_erofs_decompressor z_erofs_{name}_decomp = {{\n"
        f"\t.config = z_erofs_load_{name}_config,\n"
        f"\t.decompress = z_erofs_{name}_decompress,\n"
        f"\t.supports_subextent = {capability},\n"
        f"\t.name = \"{name}\",\n"
        "};",
        f"{name} descriptor",
    )
    if current[path] != expected:
        raise SystemExit(f"{path} differs from exact B30 descriptor reorder")

if current["decompressor_lz4.c"] != before["decompressor_lz4.c"]:
    raise SystemExit("B30 changed the independent FreeBSD LZ4 backend")
expected = before["zdata.c"]
if expected.count("z_erofs_read_extent") != 2:
    raise SystemExit("B30 zdata baseline rename denominator changed")
expected = expected.replace("z_erofs_read_extent", "z_erofs_decode_extent")
if current["zdata.c"] != expected:
    raise SystemExit("zdata.c differs from the exact two-token decode rename")
if current["zmap.c"] != before["zmap.c"]:
    raise SystemExit("B30 changed the zmap record reader")

descriptor_markers = [
    "int (*config)(",
    "int (*decompress)(",
    "bool supports_subextent;",
    "const char *name;",
]
positions = [current["compress.h"].index(marker) for marker in descriptor_markers]
if positions != sorted(positions):
    raise SystemExit("FreeBSD descriptor field order drifted")


def initializer_fields(source: str, object_name: str) -> list[tuple[str, str]]:
    match = re.search(
        rf"(?:static )?const struct z_erofs_decompressor {object_name} = \{{\n"
        rf"(?P<body>.*?)\n\}};",
        source,
        re.DOTALL,
    )
    if match is None:
        raise SystemExit(f"missing descriptor initializer: {object_name}")
    return re.findall(r"^\s*\.([a-z_]+)\s*=\s*([^,]+),$", match.group("body"), re.MULTILINE)


descriptors = {
    "shifted": initializer_fields(current["decompressor.c"], "z_erofs_shifted_decomp"),
    "interlaced": initializer_fields(current["decompressor.c"], "z_erofs_interlaced_decomp"),
    "lz4": initializer_fields(current["decompressor.c"], "z_erofs_lz4_decomp"),
    "lzma": initializer_fields(current["decompressor_lzma.c"], "z_erofs_lzma_decomp"),
    "deflate": initializer_fields(current["decompressor_deflate.c"], "z_erofs_deflate_decomp"),
    "zstd": initializer_fields(current["decompressor_zstd.c"], "z_erofs_zstd_decomp"),
}
expected_descriptors = {
    "shifted": [("decompress", "z_erofs_transform_plain"), ("name", '"shifted"')],
    "interlaced": [("decompress", "z_erofs_transform_plain"), ("name", '"interlaced"')],
    "lz4": [
        ("config", "z_erofs_load_lz4_config"),
        ("decompress", "z_erofs_lz4_decompress"),
        ("supports_subextent", "1"),
        ("name", '"lz4"'),
    ],
    "lzma": [
        ("config", "z_erofs_load_lzma_config"),
        ("decompress", "z_erofs_lzma_decompress"),
        ("supports_subextent", "1"),
        ("name", '"lzma"'),
    ],
    "deflate": [
        ("config", "z_erofs_load_deflate_config"),
        ("decompress", "z_erofs_deflate_decompress"),
        ("supports_subextent", "1"),
        ("name", '"deflate"'),
    ],
    "zstd": [
        ("config", "z_erofs_load_zstd_config"),
        ("decompress", "z_erofs_zstd_decompress"),
        ("supports_subextent", "0"),
        ("name", '"zstd"'),
    ],
}
if descriptors != expected_descriptors:
    raise SystemExit(f"backend descriptor initializer mismatch: {descriptors!r}")

table_match = re.search(
    r"static const struct z_erofs_decompressor \* const z_erofs_decomp\[\] = \{\n"
    r"(?P<body>.*?)\n\};",
    current["decompressor.c"],
    re.DOTALL,
)
if table_match is None:
    raise SystemExit("missing FreeBSD decompressor table")
table_order = re.findall(r"\[(Z_EROFS_COMPRESSION_[A-Z0-9_]+)\]", table_match.group("body"))
expected_order = [
    "Z_EROFS_COMPRESSION_SHIFTED",
    "Z_EROFS_COMPRESSION_INTERLACED",
    "Z_EROFS_COMPRESSION_LZ4",
    "Z_EROFS_COMPRESSION_LZMA",
    "Z_EROFS_COMPRESSION_DEFLATE",
    "Z_EROFS_COMPRESSION_ZSTD",
]
if table_order != expected_order:
    raise SystemExit(f"FreeBSD decompressor table order mismatch: {table_order!r}")
linux_decompressor = (root / "src-linux/decompressor.c").read_text(encoding="utf-8")
linux_table = linux_decompressor[linux_decompressor.index("z_erofs_decomp[] = {") :]
linux_order = re.findall(r"\[(Z_EROFS_COMPRESSION_[A-Z0-9_]+)\]", linux_table)[:6]
if linux_order != expected_order:
    raise SystemExit(f"Linux decompressor table anchor drifted: {linux_order!r}")

callbacks = []
for path in sorted(src.glob("decompressor*.c")):
    for line in path.read_text(encoding="utf-8").splitlines():
        match = re.match(r"\s*\.(config|decompress)\s*=\s*([^,]+),", line)
        if match:
            callbacks.append((path.name, match.group(1), match.group(2)))
expected_callbacks = [
    ("decompressor.c", "decompress", "z_erofs_transform_plain"),
    ("decompressor.c", "decompress", "z_erofs_transform_plain"),
    ("decompressor.c", "config", "z_erofs_load_lz4_config"),
    ("decompressor.c", "decompress", "z_erofs_lz4_decompress"),
    ("decompressor_deflate.c", "config", "z_erofs_load_deflate_config"),
    ("decompressor_deflate.c", "decompress", "z_erofs_deflate_decompress"),
    ("decompressor_lzma.c", "config", "z_erofs_load_lzma_config"),
    ("decompressor_lzma.c", "decompress", "z_erofs_lzma_decompress"),
    ("decompressor_zstd.c", "config", "z_erofs_load_zstd_config"),
    ("decompressor_zstd.c", "decompress", "z_erofs_zstd_decompress"),
]
if Counter(callbacks) != Counter(expected_callbacks) or len(callbacks) != 10:
    raise SystemExit(f"G01 decompressor callback multiset changed: {callbacks!r}")

all_source = "\n".join(current.values())
for removed in ("erofs_sb_lz4_info", "max_distance_pages", "sbi->lz4"):
    if removed in all_source:
        raise SystemExit(f"removed LZ4 runtime state remains: {removed}")
config = current["decompressor.c"][: current["decompressor.c"].index("static int\nz_erofs_transform_plain")]
for marker in (
    "uint32_t max_pclusterblks;",
    "max_pclusterblks = le16toh(lz4->max_pclusterblks);",
    "if (max_pclusterblks == 0)",
    "max_pclusterblks = 1;",
    "max_pclusterblks >\n\t\t    (Z_EROFS_PCLUSTER_MAX_SIZE >> sbi->blkszbits)",
    "return (EOPNOTSUPP);",
    "distance = le16toh(dsb->u1.lz4_max_distance);",
    "if (distance == 0 && !erofs_sb_has_lz4_0padding(sbi))",
):
    if marker not in config:
        raise SystemExit(f"LZ4 config validation marker missing: {marker!r}")

if current["zdata.c"].count("z_erofs_decode_extent") != 2:
    raise SystemExit("zdata decode helper rename denominator changed")
if "z_erofs_read_extent" in current["zdata.c"]:
    raise SystemExit("old zdata decode helper name remains")
if current["zmap.c"].count("z_erofs_read_extent") != 4:
    raise SystemExit("zmap record reader definition/calls changed")
if "z_erofs_decode_extent" in current["zmap.c"]:
    raise SystemExit("zmap record reader was incorrectly renamed")
if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", all_source):
    raise SystemExit("B30 introduced Linux negative errno")

for path, load_name, decode_name, descriptor_name in (
    ("decompressor_lzma.c", "z_erofs_load_lzma_config", "z_erofs_lzma_decompress", "z_erofs_lzma_decomp"),
    ("decompressor_deflate.c", "z_erofs_load_deflate_config", "z_erofs_deflate_decompress", "z_erofs_deflate_decomp"),
    ("decompressor_zstd.c", "z_erofs_load_zstd_config", "z_erofs_zstd_decompress", "z_erofs_zstd_decomp"),
):
    positions = [current[path].index(name) for name in (load_name, decode_name, descriptor_name)]
    if positions != sorted(positions):
        raise SystemExit(f"backend definition order drifted: {path}")

report = {
    "status": "PASS",
    "baseline": baseline,
    "changed_paths": sorted(changed),
    "descriptor_fields": ["config", "decompress", "supports_subextent", "name"],
    "descriptor_table_order": table_order,
    "callback_field_count": len(callbacks),
    "callback_target_multiset": sorted(callbacks),
    "removed_runtime_state": ["erofs_sb_lz4_info", "max_distance_pages", "max_pclusterblks store"],
    "retained_config_validation": ["max_pclusterblks local", "format cap", "legacy distance branch"],
    "rename": {"zdata_decode": 2, "zmap_record_reader": 4},
    "preserved": ["B25 typed errno", "B27 trailing policy", "B28 partial fallback ownership"],
}
with report_path.open("x", encoding="ascii") as stream:
    json.dump(report, stream, ensure_ascii=True, indent=2, sort_keys=True)
    stream.write("\n")
print("B30-descriptors PASS fields=4 backends=6 callbacks=10")
print("B30-write-only PASS dead-state=2 config-local=1 rename=2/4")
PY
then
	:
else
	pre15_dut_fail 'B30 descriptor, dead-state, or rename oracle failed'
fi

sha256sum "$artifacts/B30-codec-structure.json" > "$artifacts/SHA256SUMS"
printf 'B30 PASS codec structure aligned without behavior changes\n'
