#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
from pathlib import Path
import re
import subprocess


EINTEGRITY = "EINTEGRITY"
PASS = "PASS"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--root", required=True, type=Path)
    parser.add_argument("--dut", required=True, type=Path)
    parser.add_argument("--baseline", required=True)
    parser.add_argument("--spec", required=True, type=Path)
    parser.add_argument("--report", required=True, type=Path)
    return parser.parse_args()


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


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 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}")


def function_position(source: str, name: str) -> int:
    match = re.search(r"^" + re.escape(name) + r"\s*\(", source, re.MULTILINE)
    if not match:
        raise SystemExit(f"missing function position: {name}")
    return match.start()


def source_checks(root: Path, dut: Path, baseline: str) -> dict[str, object]:
    src = dut / "src"
    current = {
        name: (src / name).read_text(encoding="utf-8")
        for name in ("dir.c", "namei.c", "internal.h", "erofs_vnops.c")
    }
    base = {
        name: committed(root, baseline, f"repo-pre-15/src/{name}")
        for name in current
    }
    if current["dir.c"] == base["dir.c"] or current["namei.c"] == base["namei.c"]:
        raise SystemExit("B10 did not change both declared directory sources")
    if current["internal.h"] != base["internal.h"]:
        raise SystemExit("B10 added directory-wide state outside its write set")
    if current["erofs_vnops.c"] != base["erofs_vnops.c"]:
        raise SystemExit("B10 changed the FreeBSD VOP adapter")

    completed = subprocess.run(
        [
            "git",
            "-C",
            str(root),
            "diff",
            "--name-only",
            baseline,
            "--",
            "repo-pre-15/src",
        ],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
    )
    changed_sources = set(completed.stdout.splitlines())
    expected_sources = {
        "repo-pre-15/src/dir.c",
        "repo-pre-15/src/namei.c",
    }
    if changed_sources != expected_sources:
        raise SystemExit(f"unexpected B10 source write set: {sorted(changed_sources)}")

    dir_source = current["dir.c"]
    namei_source = current["namei.c"]
    validator = extract_function(dir_source, "erofs_validate_dirblock")
    fill = extract_function(dir_source, "erofs_fill_dentries")
    readdir = extract_function(dir_source, "erofs_readdir_block")
    previous = extract_function(dir_source, "erofs_previous_dirname")
    read_block = extract_function(namei_source, "erofs_read_dirblock")
    neighbors = extract_function(namei_source, "erofs_validate_dirblock_neighbors")
    find_block = extract_function(namei_source, "erofs_find_target_block")
    namei = extract_function(namei_source, "erofs_namei")
    lookup = extract_function(namei_source, "erofs_lookup")

    require_order(
        validator,
        ["erofs_dirent_namelen", "erofs_dirname_order", "EINTEGRITY"],
        "block lexical validator",
    )
    require_order(
        fill,
        ["erofs_dirent_name", "erofs_nid_is_valid", "bzero", "d_fileno", "erofs_uiodir"],
        "readdir publication boundary",
    )
    require_order(
        readdir,
        [
            "GENERIC_MINDIRSIZ",
            "(size_t)INT_MAX",
            "SIZE_MAX / sizeof(*cookiebuf)",
            "(int)cookie_count",
        ],
        "cookie clamp",
    )
    if "uio->uio_resid / 8" in readdir or "MAX(1" in readdir:
        raise SystemExit("legacy unchecked cookie sizing remains")
    require_order(
        readdir,
        [
            "erofs_validate_dirblock",
            "erofs_previous_dirname",
            "erofs_dirname_order",
            "erofs_fill_dentries",
        ],
        "linear readdir order validation",
    )
    if previous.count("erofs_read_data") != 1 or previous.count("erofs_brelse") != 1:
        raise SystemExit("readdir predecessor ownership is not one acquire/release")
    require_order(read_block, ["erofs_read_data", "erofs_validate_dirblock"], "lookup block read")
    for marker in ("block - 1", "block + 1", "erofs_dirblock_order"):
        if marker not in neighbors:
            raise SystemExit(f"bounded neighbor validation is missing: {marker}")
    if find_block.count("while (head <= back)") != 1 or re.search(r"\bfor\s*\(", find_block):
        raise SystemExit("lookup is no longer a single binary block search")
    require_order(
        find_block,
        ["erofs_read_dirblock", "erofs_validate_dirblock_neighbors", "erofs_dirnamecmp"],
        "touched-block lookup validation",
    )
    require_order(namei, ["erofs_nid_is_valid", "*nid = found_nid", "*d_type"], "lookup NID publication")
    require_order(
        lookup,
        ["erofs_namei", "(cnp->cn_flags & ISDOTDOT) == 0", "vn_vget_ino", "erofs_vget"],
        "FreeBSD self-NID lock boundary",
    )
    if lookup.count("vn_vget_ino") != 1 or lookup.count("erofs_vget") != 1:
        raise SystemExit("FreeBSD dotdot/ordinary vnode acquisition changed shape")
    for marker in ("vref(dvp)", "cache_enter(dvp, NULL, cnp)", "cache_enter(dvp, vp, cnp)"):
        if marker not in lookup:
            raise SystemExit(f"FreeBSD lookup behavior is missing: {marker}")
    if re.search(r"return\s*\(\s*-E[A-Z0-9_]+", dir_source + namei_source):
        raise SystemExit("negative Linux errno entered B10")

    dir_order = [
        "erofs_validate_dirblock",
        "erofs_fill_dentries",
        "erofs_readdir_block",
    ]
    if [function_position(dir_source, name) for name in dir_order] != sorted(
        function_position(dir_source, name) for name in dir_order
    ):
        raise SystemExit("dir core order does not retain validator-before-Linux-core shape")
    namei_order = [
        "find_target_dirent",
        "erofs_find_target_block",
        "erofs_namei",
        "erofs_lookup",
    ]
    if [function_position(namei_source, name) for name in namei_order] != sorted(
        function_position(namei_source, name) for name in namei_order
    ):
        raise SystemExit("namei core order diverges from the Linux reference shape")

    return {
        "changed_sources": sorted(changed_sources),
        "dir_core_order": dir_order,
        "namei_core_order": namei_order,
        "full_directory_state_added": False,
        "positive_errno": True,
        "vfs_cache_lookup_preserved": "vfs_cache_lookup" in current["erofs_vnops.c"],
    }


def validate_block(names: list[str]) -> None:
    if not names or any(not name for name in names):
        raise ValueError(EINTEGRITY)
    encoded = [name.encode("ascii") for name in names]
    if any(left >= right for left, right in zip(encoded, encoded[1:])):
        raise ValueError(EINTEGRITY)


def validate_boundary(left: list[str], right: list[str]) -> None:
    if left[-1].encode("ascii") >= right[0].encode("ascii"):
        raise ValueError(EINTEGRITY)


def lookup_model(blocks: list[list[str]], target: str) -> tuple[str, int, list[int]]:
    head = 0
    back = len(blocks) - 1
    candidate = None
    reads = 0
    touched = []
    target_bytes = target.encode("ascii")
    try:
        while head <= back:
            mid = head + (back - head) // 2
            touched.append(mid)
            validate_block(blocks[mid])
            reads += 1
            if mid > 0:
                validate_block(blocks[mid - 1])
                validate_boundary(blocks[mid - 1], blocks[mid])
                reads += 1
            if mid + 1 < len(blocks):
                validate_block(blocks[mid + 1])
                validate_boundary(blocks[mid], blocks[mid + 1])
                reads += 1
            first = blocks[mid][0].encode("ascii")
            if target_bytes < first:
                back = mid - 1
                continue
            candidate = mid
            if target_bytes == first:
                return PASS, reads, touched
            head = mid + 1
        if candidate is None:
            return "ENOENT", reads, touched
        names = [name.encode("ascii") for name in blocks[candidate]]
        left = 1
        right = len(names) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if names[mid] == target_bytes:
                return PASS, reads, touched
            if names[mid] < target_bytes:
                left = mid + 1
            else:
                right = mid - 1
        return "ENOENT", reads, touched
    except ValueError:
        return EINTEGRITY, reads, touched


def readdir_model(blocks: list[list[str]]) -> tuple[str, list[str]]:
    output = []
    previous = None
    try:
        for block in blocks:
            validate_block(block)
            if previous is not None:
                validate_boundary(previous, block)
            output.extend(block)
            previous = block
    except ValueError:
        return EINTEGRITY, output
    return PASS, output


def run_order_cases(spec: dict[str, object]) -> list[dict[str, object]]:
    results = []
    for case in spec["order_cases"]:
        lookup_status, reads, touched = lookup_model(case["blocks"], case["target"])
        readdir_status, raw = readdir_model(case["blocks"])
        if lookup_status != case["expected_lookup"]:
            raise SystemExit(f"lookup order oracle mismatch: {case['id']}")
        if readdir_status != case["expected_readdir"]:
            raise SystemExit(f"readdir order oracle mismatch: {case['id']}")
        if "expected_raw" in case and raw != case["expected_raw"]:
            raise SystemExit(f"raw directory order changed: {case['id']}")
        results.append(
            {
                "id": case["id"],
                "lookup": lookup_status,
                "readdir": readdir_status,
                "lookup_reads": reads,
                "lookup_touched": touched,
                "raw_entries": raw if readdir_status == PASS else None,
            }
        )
    return results


def nid_is_valid(config: dict[str, object], case: dict[str, object]) -> bool:
    uint64_max = (1 << 64) - 1
    metabox_bit = config["metabox_bit"]
    nid = case["nid"]
    low = nid & (metabox_bit - 1)
    if low > (uint64_max >> 5):
        return False
    offset = low << 5
    in_metabox = bool(nid & metabox_bit)
    if in_metabox:
        if not case["metabox_feature"] or case["metabox_size"] is None:
            return False
        return offset <= case["metabox_size"] and config["compact_inode_size"] <= case["metabox_size"] - offset
    primary = config["primary"]
    if primary["blocks"] > (uint64_max >> primary["blkszbits"]):
        return False
    metadata = primary["meta_blkaddr"] << primary["blkszbits"]
    if offset > uint64_max - metadata:
        return False
    offset += metadata
    image_size = primary["blocks"] << primary["blkszbits"]
    compact = config["compact_inode_size"]
    return (
        offset <= image_size
        and compact <= image_size - offset
        and offset <= primary["mediasize"]
        and compact <= primary["mediasize"] - offset
    )


def run_nid_cases(config: dict[str, object]) -> list[dict[str, object]]:
    results = []
    for case in config["cases"]:
        valid = nid_is_valid(config, case)
        lookup = EINTEGRITY if not valid else PASS
        if valid and case["relation"] == "ordinary" and case["nid"] == case["current_nid"]:
            lookup = EINTEGRITY
        readdir = PASS if valid else EINTEGRITY
        if valid != case["expected_valid"] or lookup != case["expected_lookup"] or readdir != case["expected_readdir"]:
            raise SystemExit(f"NID oracle mismatch: {case['id']}")
        results.append({"id": case["id"], "valid": valid, "lookup": lookup, "readdir": readdir})
    return results


def run_cookie_cases(config: dict[str, object]) -> list[dict[str, int]]:
    results = []
    allocation_limit = config["size_max"] // 8
    for case in config["cases"]:
        count = 0
        if case["resid"] > 0:
            count = case["resid"] // config["min_dirent_size"]
            count = min(count, config["int_max"], allocation_limit)
        if count != case["expected"]:
            raise SystemExit(f"cookie clamp oracle mismatch: {case['id']}")
        results.append({"id": case["id"], "resid": case["resid"], "cookies": count})
    return results


def complexity_result(config: dict[str, int]) -> dict[str, object]:
    blocks = config["directory_bytes"] // config["block_size"]
    if blocks != config["expected_blocks"]:
        raise SystemExit("TC153 sparse directory block denominator changed")
    max_iterations = blocks.bit_length()
    if max_iterations != config["max_binary_iterations"]:
        raise SystemExit("TC153 binary iteration bound mismatch")
    scenarios = []
    for target in (0, blocks // 2, blocks - 1):
        head = 0
        back = blocks - 1
        iterations = 0
        reads = 0
        while head <= back:
            mid = head + (back - head) // 2
            iterations += 1
            reads += 1 + int(mid > 0) + int(mid + 1 < blocks)
            if mid == target:
                break
            if mid < target:
                head = mid + 1
            else:
                back = mid - 1
        if iterations > config["max_binary_iterations"] or reads > config["max_reads_with_neighbors"]:
            raise SystemExit("TC153 lookup exceeded the logarithmic neighbor-read bound")
        scenarios.append({"target_block": target, "iterations": iterations, "reads": reads})
    return {
        "directory_bytes": config["directory_bytes"],
        "blocks": blocks,
        "bound": "O(log n) selected blocks plus two neighbors per selection",
        "scenarios": scenarios,
    }


def main() -> None:
    args = parse_args()
    spec = json.loads(args.spec.read_text(encoding="ascii"))
    if spec.get("schema") != 1 or spec.get("batch") != "B10":
        raise SystemExit("invalid B10 fixture identity")
    if spec.get("candidates") != ["P15-012", "P15-054", "P15-055", "P15-078", "P15-080"]:
        raise SystemExit("B10 candidate list changed")

    report = {
        "schema": 1,
        "batch": "B10",
        "source": source_checks(args.root, args.dut, args.baseline),
        "order": run_order_cases(spec),
        "nid": run_nid_cases(spec["nid"]),
        "cookie": run_cookie_cases(spec["cookie"]),
        "complexity": complexity_result(spec["complexity"]),
    }
    args.report.write_text(
        json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
        encoding="ascii",
    )
    print(f"TC163-directory-order PASS {len(report['order'])} cases")
    print(f"TC173-readdir-boundaries PASS {len(report['nid']) + len(report['cookie'])} cases")
    print("TC153-large-directory-block-index PASS O(log n)+bounded-neighbors")


if __name__ == "__main__":
    main()
