#!/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=8e85a8b64dd1a956376ff1cb31d2157bc73df8ad
artifacts=$PRE15_RUN_DIR/artifacts
inode=$PRE15_DUT/src/inode.c
vnops=$PRE15_DUT/src/erofs_vnops.c
super=$PRE15_DUT/src/super.c
internal=$PRE15_DUT/src/internal.h
namei=$PRE15_DUT/src/namei.c

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

pre15_record_fixture b09-inode "$inode"
pre15_record_fixture b09-vnops "$vnops"
pre15_record_fixture b09-super "$super"
pre15_record_fixture b09-internal "$internal"
pre15_record_fixture b09-namei "$namei"
pre15_record_fixture b09-case "$PRE15_DUT/tests/pre15/cases/B09-vnode.sh"

mkdir -p "$artifacts"
pre15_target_reached

if python3 - "$PRE15_ROOT" "$PRE15_DUT" "$baseline" "$artifacts" <<'PY'
from __future__ import annotations

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]
artifacts = 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/src/{path}"],
        check=False,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    if completed.returncode != 0:
        raise SystemExit(f"cannot read B09 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"{label}: expected one transform source, found {count}")
    return source.replace(old, new, 1)


def extract_function(source: str, name: str) -> str:
    match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
    if not match:
        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}")


def initializer(source: str, declaration: str, prefix: str) -> list[tuple[str, str]]:
    match = re.search(
        re.escape(declaration) + r"\s*=\s*\{(.*?)\n\};", source, re.DOTALL
    )
    if not match:
        raise SystemExit(f"initializer absent: {declaration}")
    return re.findall(
        rf"^\s*\.({prefix}_[A-Za-z0-9_]+)\s*=\s*([^,]+),",
        match.group(1),
        re.MULTILINE,
    )


def require_order(source: str, markers: list[str], label: str) -> None:
    position = -1
    for marker in markers:
        position = source.find(marker, position + 1)
        if position < 0:
            raise SystemExit(f"{label} is missing ordered marker: {marker}")


current = {
    name: (src / name).read_text(encoding="utf-8")
    for name in ("inode.c", "erofs_vnops.c", "super.c", "internal.h", "namei.c")
}
base = {name: committed(name) for name in current}

moved_marker = "static u_int\nerofs_vfs_hash(erofs_nid_t nid)"
moved_offset = base["inode.c"].find(moved_marker)
if moved_offset < 0:
    raise SystemExit("B09 baseline vnode adapter block is absent")
moved_block = base["inode.c"][moved_offset:]
expected_inode = base["inode.c"][:moved_offset].rstrip("\n") + "\n"
if current["inode.c"] != expected_inode:
    raise SystemExit("inode.c changed outside the exact vnode adapter movement")
if current["internal.h"] != base["internal.h"]:
    raise SystemExit("internal.h changed despite no B09 interface delta")
if current["namei.c"] != base["namei.c"]:
    raise SystemExit("namecache/lookup code changed outside the B09 write set")

expected_vnops = replace_once(
    base["erofs_vnops.c"],
    "#include <sys/extattr.h>\n",
    "#include <sys/extattr.h>\n#include <sys/fnv_hash.h>\n",
    "vnode hash include",
)
old_open = """\tif (vp->v_type == VREG) {
\t\tif (vnode_create_vobject(vp, vi->size, ap->a_td) != 0)
\t\t\treturn (ENOMEM);
\t}
"""
new_open = """\tif (vp->v_type == VREG)
\t\tvnode_create_vobject(vp, vi->size, ap->a_td);
"""
expected_vnops = replace_once(expected_vnops, old_open, new_open, "pager dead branch")
expected_vnops = replace_once(
    expected_vnops,
    "static int\nerofs_reclaim(struct vop_reclaim_args *ap)",
    moved_block + "\nstatic int\nerofs_reclaim(struct vop_reclaim_args *ap)",
    "vnode adapter placement",
)
if current["erofs_vnops.c"] != expected_vnops:
    raise SystemExit("erofs_vnops.c differs from the two declared B09 transforms")

expected_super = replace_once(
    base["super.c"], "static vfs_vget_t erofs_vgetf;\n", "", "vget declaration"
)
expected_super = replace_once(
    expected_super,
    """static int
erofs_vgetf(struct mount *mp, ino_t ino, int flags, struct vnode **vpp)
{
\treturn (erofs_vget(mp, ino, flags, vpp));
}

""",
    "",
    "vget wrapper",
)
expected_super = replace_once(
    expected_super,
    "\t.vfs_vget = erofs_vgetf,",
    "\t.vfs_vget = erofs_vget,",
    "vfs_vget slot",
)
if current["super.c"] != expected_super:
    raise SystemExit("super.c differs from the exact direct-vget transform")

moved_functions = (
    "erofs_vfs_hash",
    "erofs_vfs_hash_cmp",
    "erofs_fill_vnode",
    "erofs_vget",
)
for name in moved_functions:
    if extract_function(base["inode.c"], name) != extract_function(
        current["erofs_vnops.c"], name
    ):
        raise SystemExit(f"moved function changed semantics: {name}")
    definitions = {
        path: len(re.findall(r"^" + re.escape(name) + r"\s*\(", text, re.MULTILINE))
        for path, text in current.items()
    }
    if definitions["erofs_vnops.c"] != 1 or sum(definitions.values()) != 1:
        raise SystemExit(f"B09 function ownership is not unique: {name} {definitions}")

base_vnode_slots = initializer(
    base["erofs_vnops.c"], "struct vop_vector erofs_vnodeops", "vop"
)
base_fifo_slots = initializer(
    base["erofs_vnops.c"], "struct vop_vector erofs_fifoops", "vop"
)
vnode_slots = initializer(
    current["erofs_vnops.c"], "struct vop_vector erofs_vnodeops", "vop"
)
fifo_slots = initializer(
    current["erofs_vnops.c"], "struct vop_vector erofs_fifoops", "vop"
)
base_vfs_slots = initializer(base["super.c"], "static struct vfsops erofs_vfsops", "vfs")
vfs_slots = initializer(current["super.c"], "static struct vfsops erofs_vfsops", "vfs")
expected_vfs_slots = [
    (slot, "erofs_vget" if slot == "vfs_vget" else target)
    for slot, target in base_vfs_slots
]
if vnode_slots != base_vnode_slots or fifo_slots != base_fifo_slots:
    raise SystemExit("FreeBSD KOBJ VOP slot order or targets changed")
if vfs_slots != expected_vfs_slots:
    raise SystemExit("VFS slots changed beyond direct erofs_vget registration")
if ("vop_lookup", "vfs_cache_lookup") not in vnode_slots or (
    "vop_cachedlookup", "erofs_lookup"
) not in vnode_slots:
    raise SystemExit("FreeBSD namecache slots were not preserved")
if "erofs_vgetf" in current["super.c"]:
    raise SystemExit("trivial erofs_vgetf wrapper remains")

vget = extract_function(current["erofs_vnops.c"], "erofs_vget")
require_order(
    vget,
    [
        "td = curthread;",
        "nid = (uint64_t)ino;",
        "shared = (flags & LK_TYPE_MASK) == LK_SHARED;",
        "hash = erofs_vfs_hash(nid);",
        "error = vfs_hash_get(",
        "if (error != 0 || *vpp != NULL)",
        "sbi = MTOE(mp);",
        "vi = malloc(sizeof(*vi), M_EROFS, M_WAITOK | M_ZERO);",
        "error = getnewvnode(\"erofs\", mp, &erofs_vnodeops, &vp);",
        "vp->v_data = vi;",
        "vi->nid = nid;",
        "lockmgr(vp->v_vnlock, LK_EXCLUSIVE, NULL);",
        "error = insmntque(vp, mp);",
        "error = vfs_hash_insert(",
        "if (error != 0 || *vpp != NULL)",
        "error = erofs_read_inode(sbi, nid, vi);",
        "vgone(vp);",
        "vput(vp);",
        "erofs_fill_vnode(sbi, vp, vi);",
        "vn_set_state(vp, VSTATE_CONSTRUCTED);",
        "if (shared)",
        "VOP_LOCK(vp, LK_DOWNGRADE);",
        "*vpp = vp;",
    ],
    "erofs_vget lifecycle",
)
if vget.count("erofs_vfs_hash_cmp") != 2:
    raise SystemExit("vget no longer uses one comparator at both hash callsites")
if vget.count("free(vi, M_EROFS);") != 2 or vget.count("*vpp = NULL;") != 3:
    raise SystemExit("vget allocation/insmntque/read failure cleanup changed")
if vget.count("vgone(vp);") != 1 or vget.count("vput(vp);") != 1:
    raise SystemExit("vget failed-inode vnode cleanup changed")
if re.search(r"return\s*\(\s*-", vget):
    raise SystemExit("negative errno entered the FreeBSD vget path")

reclaim = extract_function(current["erofs_vnops.c"], "erofs_reclaim")
if reclaim != extract_function(base["erofs_vnops.c"], "erofs_reclaim"):
    raise SystemExit("reclaim/hash removal semantics changed")
require_order(
    reclaim,
    ["vi = VTOE(vp);", "vfs_hash_remove(vp);", "free(vi, M_EROFS);", "vp->v_data = NULL;"],
    "erofs_reclaim lifecycle",
)

open_body = extract_function(current["erofs_vnops.c"], "erofs_open")
if open_body.count("vnode_create_vobject(vp, vi->size, ap->a_td);") != 1:
    raise SystemExit("regular-vnode pager setup is not a single direct call")
if "ENOMEM" in open_body or "vnode_create_vobject" not in open_body:
    raise SystemExit("dead pager errno folding remains")

slot_report = {
    "status": "PASS",
    "vnode": [{"slot": slot, "target": target} for slot, target in vnode_slots],
    "fifo": [{"slot": slot, "target": target} for slot, target in fifo_slots],
    "vfs": [{"slot": slot, "target": target} for slot, target in vfs_slots],
}
(artifacts / "B09-vop-slots.json").write_text(
    json.dumps(slot_report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
    encoding="ascii",
)

callgraph = [
    "VFS root -> erofs_vget",
    "namecache cachedlookup -> erofs_lookup -> erofs_vget",
    "VFS vget slot -> erofs_vget",
    "erofs_vget -> vfs_hash_get[erofs_vfs_hash_cmp]",
    "erofs_vget -> getnewvnode -> LK_EXCLUSIVE -> insmntque",
    "erofs_vget -> vfs_hash_insert[erofs_vfs_hash_cmp]",
    "erofs_vget -> erofs_read_inode -> erofs_fill_vnode",
    "erofs_vget -> VSTATE_CONSTRUCTED -> optional LK_DOWNGRADE",
    "erofs_reclaim -> vfs_hash_remove -> free inode",
]
(artifacts / "B09-vget-callgraph.txt").write_text(
    "\n".join(callgraph) + "\n", encoding="ascii"
)

result = {
    "status": "PASS",
    "final_ids": ["P15-008", "P15-050", "P15-088"],
    "baseline": baseline,
    "moved_functions": list(moved_functions),
    "moved_functions_byte_identical": True,
    "vnode_slots": len(vnode_slots),
    "fifo_slots": len(fifo_slots),
    "vfs_slots": len(vfs_slots),
    "hash_comparator_callsites": 2,
    "exclusive_construct_lock": True,
    "shared_result_downgrade": True,
    "insmntque_cleanup_preserved": True,
    "reclaim_hash_removal_preserved": True,
    "namecache_slots_preserved": True,
    "positive_errno_preserved": True,
    "b11_b15_scope": "NOT_INCLUDED",
    "qemu": "NOT_RUN",
    "full_feature_suite": "NOT_RUN",
}
(artifacts / "B09-result.json").write_text(
    json.dumps(result, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
    encoding="ascii",
)
print(
    f"B09 PASS moved={len(moved_functions)} "
    f"slots={len(vnode_slots)}+{len(fifo_slots)}/{len(vfs_slots)} hash=2"
)
PY
then
	:
else
	pre15_dut_fail 'B09 vnode ownership, VOP slots, or vget graph check failed'
fi

sha256sum "$artifacts/B09-vop-slots.json" \
	"$artifacts/B09-vget-callgraph.txt" "$artifacts/B09-result.json" \
	> "$artifacts/SHA256SUMS"
printf 'B09 PASS vnode adapter ownership and FreeBSD lifecycle preserved\n'
