#!/usr/bin/env python3
"""Independently verify B19b fixtures, Bloom semantics, and source scope."""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path
import re
import statistics
import struct
import subprocess


SUPER = 1024
MAGIC = 0xE0F5E1E2


class Reject(RuntimeError):
    def __init__(self, errno_name: str, point: str):
        super().__init__(f"{errno_name} at {point}")
        self.errno_name = errno_name
        self.point = point


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def write_json(path: Path, value: object) -> None:
    path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")


def rotl32(value: int, count: int) -> int:
    return ((value << count) | (value >> (32 - count))) & 0xFFFFFFFF


def xxh32(data: bytes, seed: int) -> int:
    prime1 = 2654435761
    prime2 = 2246822519
    prime3 = 3266489917
    prime4 = 668265263
    prime5 = 374761393
    cursor = 0

    def round32(accumulator: int, lane: int) -> int:
        accumulator = (accumulator + lane * prime2) & 0xFFFFFFFF
        return (rotl32(accumulator, 13) * prime1) & 0xFFFFFFFF

    if len(data) >= 16:
        accumulator1 = (seed + prime1 + prime2) & 0xFFFFFFFF
        accumulator2 = (seed + prime2) & 0xFFFFFFFF
        accumulator3 = seed & 0xFFFFFFFF
        accumulator4 = (seed - prime1) & 0xFFFFFFFF
        limit = len(data) - 16
        while cursor <= limit:
            accumulator1 = round32(accumulator1, int.from_bytes(data[cursor:cursor + 4], "little"))
            accumulator2 = round32(accumulator2, int.from_bytes(data[cursor + 4:cursor + 8], "little"))
            accumulator3 = round32(accumulator3, int.from_bytes(data[cursor + 8:cursor + 12], "little"))
            accumulator4 = round32(accumulator4, int.from_bytes(data[cursor + 12:cursor + 16], "little"))
            cursor += 16
        value = (
            rotl32(accumulator1, 1)
            + rotl32(accumulator2, 7)
            + rotl32(accumulator3, 12)
            + rotl32(accumulator4, 18)
        ) & 0xFFFFFFFF
    else:
        value = (seed + prime5) & 0xFFFFFFFF
    value = (value + len(data)) & 0xFFFFFFFF
    while cursor + 4 <= len(data):
        value = (value + int.from_bytes(data[cursor:cursor + 4], "little") * prime3) & 0xFFFFFFFF
        value = (rotl32(value, 17) * prime4) & 0xFFFFFFFF
        cursor += 4
    while cursor < len(data):
        value = (value + data[cursor] * prime5) & 0xFFFFFFFF
        value = (rotl32(value, 11) * prime1) & 0xFFFFFFFF
        cursor += 1
    value ^= value >> 15
    value = (value * prime2) & 0xFFFFFFFF
    value ^= value >> 13
    value = (value * prime3) & 0xFFFFFFFF
    value ^= value >> 16
    return value & 0xFFFFFFFF


class Reader:
    def __init__(self, path: Path, spec: dict):
        self.data = path.read_bytes()
        self.spec = spec
        self.calls = 0
        self.bytes = 0
        self.blocks_read: set[int] = set()
        header = self.read(SUPER, 144, "super")
        if struct.unpack_from("<I", header)[0] != MAGIC:
            raise Reject("EINTEGRITY", "super.magic")
        self.feature_compat = struct.unpack_from("<I", header, 8)[0]
        self.block_bits = header[12]
        self.block_size = 1 << self.block_bits
        self.root_nid = struct.unpack_from("<H", header, 14)[0]
        self.blocks = struct.unpack_from("<I", header, 36)[0]
        self.limit = self.blocks << self.block_bits
        self.meta_blkaddr = struct.unpack_from("<I", header, 40)[0]
        self.xattr_blkaddr = struct.unpack_from("<I", header, 44)[0]
        self.filter_reserved = header[104]
        if self.limit > len(self.data):
            raise Reject("EINTEGRITY", "super.bounds")

    def read(self, offset: int, length: int, point: str) -> bytes:
        limit = self.limit if hasattr(self, "limit") else len(self.data)
        if offset < 0 or length < 0 or offset > limit or length > limit - offset:
            raise Reject("EINTEGRITY", f"{point}.bounds")
        self.calls += 1
        self.bytes += length
        if length:
            self.blocks_read.update(range(offset // 4096, (offset + length - 1) // 4096 + 1))
        return self.data[offset:offset + length]

    def reset_reads(self) -> None:
        self.calls = 0
        self.bytes = 0
        self.blocks_read.clear()

    def inode(self, nid: int) -> dict[str, int]:
        offset = (self.meta_blkaddr << self.block_bits) + (nid << 5)
        raw = self.read(offset, 64, "inode")
        inode_format, xattr_count = struct.unpack_from("<HH", raw)
        inode_size = 64 if inode_format & 1 else 32
        xattr_size = 0 if xattr_count == 0 else 12 + 4 * (xattr_count - 1)
        size = struct.unpack_from("<Q" if inode_size == 64 else "<I", raw, 8)[0]
        return {
            "offset": offset,
            "inode_size": inode_size,
            "xattr_size": xattr_size,
            "layout": (inode_format >> 1) & 7,
            "size": size,
            "start_block": struct.unpack_from("<I", raw, 16)[0],
        }

    def inode_data(self, inode: dict[str, int], logical: int, length: int) -> bytes:
        if inode["layout"] == 2:
            physical = inode["offset"] + inode["inode_size"] + inode["xattr_size"] + logical
        elif inode["layout"] == 0:
            physical = (inode["start_block"] << self.block_bits) + logical
        else:
            raise Reject("EINTEGRITY", "directory.layout")
        return self.read(physical, length, "directory.data")

    def directory_entries(self, inode: dict[str, int]) -> list[tuple[bytes, int]]:
        entries = []
        logical = 0
        while logical < inode["size"]:
            length = min(self.block_size, inode["size"] - logical)
            data = self.inode_data(inode, logical, length)
            if len(data) < 12:
                raise Reject("EINTEGRITY", "directory.header")
            first_name = struct.unpack_from("<H", data, 8)[0]
            if first_name == 0 or first_name % 12 or first_name > len(data):
                raise Reject("EINTEGRITY", "directory.name-offset")
            count = first_name // 12
            for index in range(count):
                offset = index * 12
                nid = struct.unpack_from("<Q", data, offset)[0]
                start = struct.unpack_from("<H", data, offset + 8)[0]
                end = struct.unpack_from("<H", data, offset + 20)[0] if index + 1 < count else len(data)
                if start > end or end > len(data):
                    raise Reject("EINTEGRITY", "directory.name-bounds")
                entries.append((data[start:end].split(b"\0", 1)[0], nid))
            logical += length
        return entries

    def resolve(self, path: str) -> dict[str, int]:
        inode = self.inode(self.root_nid)
        for component in path.strip("/").encode().split(b"/"):
            matches = [nid for name, nid in self.directory_entries(inode) if name == component]
            if len(matches) != 1:
                raise Reject("ENOATTR", "path.lookup")
            inode = self.inode(matches[0])
        return inode

    def stats(self, scanned: bool) -> dict[str, int | bool]:
        return {
            "bytes": self.bytes,
            "calls": self.calls,
            "provider_blocks": len(self.blocks_read),
            "scanned": scanned,
        }

    def lookup(self, inode: dict[str, int], name: bytes, candidate: bool) -> tuple[str, bytes | None, dict]:
        self.reset_reads()
        body_offset = inode["offset"] + inode["inode_size"]
        usable = self.feature_compat & self.spec["format"]["feature_compat"] and self.filter_reserved == 0
        if candidate and usable:
            if inode["xattr_size"] < 12:
                raise Reject("EINTEGRITY", "ibody.header")
            if inode["xattr_size"] == 12:
                raise Reject("EOPNOTSUPP", "ibody.header-only")
            header = self.read(body_offset, 12, "ibody.header")
            shared_count = header[4]
            if 12 + shared_count * 4 > inode["xattr_size"]:
                raise Reject("EINTEGRITY", "ibody.shared-count")
            name_filter = struct.unpack_from("<I", header)[0]
            bit = xxh32(name, self.spec["format"]["seed"] + 1) & 31
            if name_filter & (1 << bit):
                return "ENOATTR", None, self.stats(False)
        body = self.read(body_offset, inode["xattr_size"], "ibody")
        if len(body) < 12:
            raise Reject("EINTEGRITY", "ibody.header")
        if len(body) == 12:
            raise Reject("EOPNOTSUPP", "ibody.header-only")
        shared_count = body[4]
        header_size = 12 + shared_count * 4
        if header_size > len(body):
            raise Reject("EINTEGRITY", "ibody.shared-count")
        cursor = header_size
        while cursor < len(body):
            if cursor + 4 > len(body):
                raise Reject("EINTEGRITY", "inline.header")
            name_length, name_index, value_length = struct.unpack_from("<BBH", body, cursor)
            total = (4 + name_length + value_length + 3) & ~3
            if total > len(body) - cursor:
                raise Reject("EINTEGRITY", "inline.bounds")
            actual = body[cursor + 4:cursor + 4 + name_length]
            if b"\0" in actual:
                raise Reject("EINTEGRITY", "inline.name-nul")
            if name_index == 1 and actual == name:
                start = cursor + 4 + name_length
                return "PASS", body[start:start + value_length], self.stats(True)
            cursor += total
        for index in range(shared_count):
            shared_id = struct.unpack_from("<I", body, 12 + index * 4)[0]
            offset = (self.xattr_blkaddr << self.block_bits) + shared_id * 4
            header = self.read(offset, 4, "shared.header")
            name_length, name_index, value_length = struct.unpack("<BBH", header)
            total = (4 + name_length + value_length + 3) & ~3
            raw = self.read(offset, total, "shared.entry")
            actual = raw[4:4 + name_length]
            if b"\0" in actual:
                raise Reject("EINTEGRITY", "shared.name-nul")
            if name_index == 1 and actual == name:
                return "PASS", raw[4 + name_length:4 + name_length + value_length], self.stats(True)
        return "ENOATTR", None, self.stats(True)


def lookup(path: Path, name: bytes, candidate: bool, spec: dict) -> dict:
    try:
        reader = Reader(path, spec)
        inode = reader.resolve(spec["fixture"]["target"])
        status, value, reads = reader.lookup(inode, name, candidate)
        return {"errno": 0 if status == "PASS" else spec["errno"][status], "reads": reads, "status": status, "value_hex": None if value is None else value.hex()}
    except Reject as error:
        return {"errno": spec["errno"].get(error.errno_name, -1), "point": error.point, "status": error.errno_name, "value_hex": None}


def function(source: str, name: str) -> str:
    match = re.search(rf"\n{name}\([^;]*?\n\{{", source, re.DOTALL)
    if match is None:
        raise SystemExit(f"missing source function: {name}")
    start = match.start() + 1
    brace = source.index("{", match.start())
    depth = 0
    for index in range(brace, len(source)):
        if source[index] == "{":
            depth += 1
        elif source[index] == "}":
            depth -= 1
            if depth == 0:
                return source[start:index + 1]
    raise SystemExit(f"unterminated source function: {name}")


def audit_source(root: Path, dut: Path, spec: dict) -> dict:
    header = (dut / "src/erofs_fs.h").read_text(encoding="utf-8")
    internal = (dut / "src/internal.h").read_text(encoding="utf-8")
    xattr = (dut / "src/xattr.c").read_text(encoding="utf-8")
    linux_header = (root / "src-linux/erofs_fs.h").read_text(encoding="utf-8")
    linux_xattr = (root / "src-linux/xattr.c").read_text(encoding="utf-8")
    for marker in (
        "#define EROFS_XATTR_FILTER_BITS",
        "#define EROFS_XATTR_FILTER_DEFAULT",
        "#define EROFS_XATTR_FILTER_SEED",
    ):
        if marker not in header or marker not in linux_header:
            raise SystemExit(f"Bloom format marker missing: {marker}")
    if "erofs_sb_has_xattr_filter_v1" not in internal or "xattr_filter_reserved == 0" not in internal:
        raise SystemExit("FreeBSD unknown-filter fallback helper is incomplete")
    hash_body = function(xattr, "erofs_xxh32")
    filter_name = function(xattr, "erofs_xattr_filter_name")
    filter_negative = function(xattr, "erofs_xattr_filter_negative")
    getxattr = function(xattr, "erofs_getxattr")
    if "static uint32_t\nerofs_xxh32" not in xattr:
        raise SystemExit("xxh32 is not file-local and namespaced")
    for marker in ("le32dec(cursor)", "UINT32_C(2654435761)", "hash ^= hash >> 16"):
        if marker not in hash_body:
            raise SystemExit(f"xxh32 source marker missing: {marker}")
    for marker in (
        "EXTATTR_NAMESPACE_USER",
        "EXTATTR_NAMESPACE_SYSTEM",
        "EROFS_XATTR_INDEX_USER",
        "EROFS_XATTR_INDEX_POSIX_ACL_ACCESS",
        "EROFS_XATTR_INDEX_POSIX_ACL_DEFAULT",
        "EROFS_XATTR_INDEX_TRUSTED",
        "EROFS_XATTR_INDEX_SECURITY",
    ):
        if marker not in filter_name:
            raise SystemExit(f"FreeBSD namespace mapping marker missing: {marker}")
    for marker in (
        "erofs_sb_has_xattr_filter_v1(sbi)",
        "header_size > vi->xattr_isize",
        "erofs_xxh32(filter_name, filter_name_len",
        "erofs_put_metabuf(&buf)",
    ):
        if marker not in filter_negative:
            raise SystemExit(f"fast-negative integrity marker missing: {marker}")
    if any(marker in filter_negative.lower() for marker in ("malloc", "mtx_", "cv_", "cache")):
        raise SystemExit("B19b introduced cache/allocation/locking into the fast-negative")
    if getxattr.index("erofs_xattr_filter_negative") > getxattr.index("erofs_xattr_load_body"):
        raise SystemExit("fast-negative runs after the full xattr body read")
    if "if (filter_negative)\n\t\treturn (ENOATTR);" not in getxattr:
        raise SystemExit("getxattr does not restrict the shortcut to proven negatives")
    if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", header + internal + xattr):
        raise SystemExit("Linux negative errno entered the FreeBSD implementation")
    for marker in (
        "xxh32(name, strlen(name)",
        "EROFS_XATTR_FILTER_SEED + index",
        "vi->xattr_name_filter & (1U << hashbit)",
        "!sbi->xattr_filter_reserved",
    ):
        if marker not in linux_xattr:
            raise SystemExit(f"Linux comparison anchor changed: {marker}")
    changed = subprocess.run(
        ["git", "-C", str(root), "diff", "--name-only", spec["gate"]["commit"], "--", "repo-pre-15/src"],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
    ).stdout.splitlines()
    expected = ["repo-pre-15/src/erofs_fs.h", "repo-pre-15/src/internal.h", "repo-pre-15/src/xattr.c"]
    if sorted(changed) != expected:
        raise SystemExit(f"B19b source write set differs: {changed}")
    return {
        "freebsd_errno": "positive",
        "freebsd_namespace": "extattr user/system mapping retained",
        "hash_symbol": "file-local erofs_xxh32",
        "linux_semantics": "xxh32(name suffix, seed + index), inverse 32-bit filter",
        "source_write_set": expected,
        "status": "PASS",
    }


def verify(fixtures: Path, spec: dict, root: Path, dut: Path) -> dict:
    hashes = {path.name: sha256(path) for path in sorted(fixtures.glob("*.erofs"))}
    if hashes != spec["fixture_sha256"]:
        raise SystemExit(f"fixture hashes differ: {hashes}")
    cases = [
        ("hit", "valid.erofs", b"attr00", "PASS", True),
        ("miss", "valid.erofs", spec["fixture"]["miss"].encode(), "ENOATTR", False),
        ("false-positive", "valid.erofs", spec["fixture"]["collision"].encode(), "ENOATTR", True),
        ("unknown-filter", "unknown-filter.erofs", spec["fixture"]["miss"].encode(), "ENOATTR", True),
        ("feature-off", "feature-off.erofs", spec["fixture"]["miss"].encode(), "ENOATTR", True),
        ("corrupt-shared-count", "corrupt-shared-count.erofs", spec["fixture"]["miss"].encode(), "EINTEGRITY", None),
        ("corrupt-shared-id", "corrupt-shared-id.erofs", b"attr00", "EINTEGRITY", None),
    ]
    results = []
    for identifier, image, name, expected_status, expected_scan in cases:
        baseline = lookup(fixtures / image, name, False, spec)
        candidate = lookup(fixtures / image, name, True, spec)
        if baseline["status"] != expected_status or candidate["status"] != expected_status:
            raise SystemExit(f"{identifier} status differs: {baseline}, {candidate}")
        if baseline["errno"] < 0 or candidate["errno"] < 0:
            raise SystemExit(f"{identifier} returned negative errno")
        if expected_scan is not None and candidate["reads"]["scanned"] != expected_scan:
            raise SystemExit(f"{identifier} scan decision differs")
        results.append({"baseline": baseline, "candidate": candidate, "id": identifier, "name": name.decode()})
    baseline_calls = []
    candidate_calls = []
    baseline_blocks = []
    candidate_blocks = []
    for _ in range(spec["thresholds"]["samples"]):
        baseline = lookup(fixtures / "valid.erofs", spec["fixture"]["miss"].encode(), False, spec)["reads"]
        candidate = lookup(fixtures / "valid.erofs", spec["fixture"]["miss"].encode(), True, spec)["reads"]
        baseline_calls.append(baseline["calls"])
        candidate_calls.append(candidate["calls"])
        baseline_blocks.append(baseline["provider_blocks"])
        candidate_blocks.append(candidate["provider_blocks"])
    call_reduction = 100 * (statistics.median(baseline_calls) - statistics.median(candidate_calls)) / statistics.median(baseline_calls)
    block_reduction = 100 * (statistics.median(baseline_blocks) - statistics.median(candidate_blocks)) / statistics.median(baseline_blocks)
    threshold = spec["thresholds"]["minimum_provider_metadata_read_reduction_percent"]
    if call_reduction < threshold or block_reduction < threshold:
        raise SystemExit("B19b source model no longer meets the measured benefit gate")
    return {
        "benchmark": {
            "baseline_calls": baseline_calls,
            "baseline_provider_blocks": baseline_blocks,
            "call_reduction_percent": call_reduction,
            "candidate_calls": candidate_calls,
            "candidate_provider_blocks": candidate_blocks,
            "provider_block_reduction_percent": block_reduction,
            "threshold_percent": threshold,
        },
        "cases": results,
        "fixture_sha256": hashes,
        "source": audit_source(root, dut, spec),
        "status": "PASS",
        "test": spec["test"],
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--dut", type=Path, required=True)
    parser.add_argument("--fixtures", type=Path, required=True)
    parser.add_argument("--report", type=Path, required=True)
    parser.add_argument("--root", type=Path, required=True)
    parser.add_argument("--spec", type=Path, required=True)
    args = parser.parse_args()
    spec = json.loads(args.spec.read_text(encoding="ascii"))
    if spec.get("schema") != 1 or spec.get("batch") != "B19b":
        raise SystemExit("invalid B19b oracle spec")
    report = verify(args.fixtures, spec, args.root, args.dut)
    write_json(args.report, report)
    print(json.dumps({"case_count": len(report["cases"]), "status": "PASS", "test": report["test"]}, sort_keys=True))


if __name__ == "__main__":
    main()
