#!/usr/bin/env python3
"""Generate deterministic B21 superblock trust-order fixtures."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import struct
import subprocess


SUPER = 1024
FEATURE_COMPAT_SB_CHKSUM = 0x00000001
FEATURE_INCOMPAT_XATTR_PREFIXES = 0x00000040
UNKNOWN_INCOMPAT = 0x80000000
CRC32C_POLY = 0x82F63B78


def crc32c(data: bytes | bytearray, seed: int = 0xFFFFFFFF) -> int:
    value = seed
    for byte in data:
        value ^= byte
        for _ in range(8):
            value = (value >> 1) ^ (CRC32C_POLY if value & 1 else 0)
    return value & 0xFFFFFFFF


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


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


class Image:
    def __init__(self, data: bytes | bytearray):
        self.data = bytearray(data)
        if len(self.data) < SUPER + 144:
            raise ValueError("image is shorter than the extended superblock")
        if self.u32(SUPER) != 0xE0F5E1E2:
            raise ValueError("image has the wrong EROFS magic")
        if not self.u32(SUPER + 8) & FEATURE_COMPAT_SB_CHKSUM:
            raise ValueError("image does not declare a superblock checksum")

    @classmethod
    def load(cls, path: Path) -> "Image":
        return cls(path.read_bytes())

    def clone(self) -> "Image":
        return Image(self.data)

    def u16(self, offset: int) -> int:
        return struct.unpack_from("<H", self.data, offset)[0]

    def u32(self, offset: int) -> int:
        return struct.unpack_from("<I", self.data, offset)[0]

    def u64(self, offset: int) -> int:
        return struct.unpack_from("<Q", self.data, offset)[0]

    def put_u16(self, offset: int, value: int) -> None:
        struct.pack_into("<H", self.data, offset, value)

    def put_u32(self, offset: int, value: int) -> None:
        struct.pack_into("<I", self.data, offset, value)

    @property
    def block_bits(self) -> int:
        return self.data[SUPER + 12]

    @property
    def block_size(self) -> int:
        return 1 << self.block_bits

    @property
    def checksum_end(self) -> int:
        span = self.block_size
        if span > SUPER:
            span -= SUPER
        return SUPER + span

    def checksum_valid(self) -> bool:
        expected = self.u32(SUPER + 4)
        canonical = bytearray(self.data[SUPER : self.checksum_end])
        struct.pack_into("<I", canonical, 4, 0)
        kernel = crc32c(self.data[SUPER + 8 : self.checksum_end], 0x5045B54A)
        return expected == crc32c(canonical) == kernel

    def update_checksum(self) -> None:
        self.put_u32(SUPER + 4, 0)
        checksum = crc32c(self.data[SUPER : self.checksum_end])
        self.put_u32(SUPER + 4, checksum)
        if not self.checksum_valid():
            raise AssertionError("updated B21 checksum does not verify")

    def inode_offset(self, nid: int) -> int:
        return (self.u32(SUPER + 40) << self.block_bits) + (nid << 5)

    def fix_packed_inode_mode(self) -> None:
        packed_nid = self.u64(SUPER + 96)
        if packed_nid == 0:
            raise ValueError("B21 seed did not materialize a packed prefix carrier")
        self.put_u16(self.inode_offset(packed_nid) + 4, 0o100644)


def load_spec(path: Path) -> dict:
    spec = json.loads(path.read_text(encoding="ascii"))
    if spec.get("schema") != 1 or spec.get("batch") != "B21":
        raise ValueError("invalid B21 fixture spec identity")
    if not isinstance(spec.get("cases"), list) or not spec["cases"]:
        raise ValueError("B21 fixture spec has no cases")
    return spec


def create_source(root: Path) -> None:
    root.mkdir(parents=True)
    control = root / "control.txt"
    prefix = root / "prefix.bin"
    control.write_bytes(b"B21 superblock control\n")
    prefix.write_bytes(b"B21 prefix control\n")
    control.chmod(0o644)
    prefix.chmod(0o644)
    os.setxattr(prefix, b"user.company.branch.leaf", b"B21-prefix-value")
    for path in (control, prefix, root):
        os.utime(path, (0, 0), follow_symlinks=False)


def build_seed(spec: dict, output: Path, work: Path) -> dict[str, object]:
    mkfs = shutil.which("mkfs.erofs")
    if mkfs is None:
        raise SystemExit("mkfs.erofs is required for B21 fixtures")
    version_run = subprocess.run(
        [mkfs, "-V"], check=False, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    version = version_run.stdout.splitlines()[0] if version_run.stdout else ""
    if version != spec["seed"]["mkfs_version"]:
        raise SystemExit(
            f"mkfs.erofs version differs: expected {spec['seed']['mkfs_version']!r}, "
            f"got {version!r}"
        )
    source = work / "source"
    create_source(source)
    command = [
        mkfs,
        "-d0",
        "-T0",
        "--all-time",
        "--all-root",
        "--workers=1",
        "--sort=path",
        f"-U{spec['seed']['uuid']}",
        "-x2",
        "-Eforce-inode-extended",
        "--xattr-prefix=user.company.",
        str(output),
        str(source),
    ]
    completed = subprocess.run(
        command, check=False, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    if completed.returncode != 0:
        raise SystemExit(
            f"mkfs.erofs failed with {completed.returncode}:\n{completed.stdout}"
        )
    image = Image.load(output)
    image.fix_packed_inode_mode()
    image.update_checksum()
    output.write_bytes(image.data)
    if not image.u32(SUPER + 80) & FEATURE_INCOMPAT_XATTR_PREFIXES:
        raise ValueError("B21 seed does not declare xattr prefixes")
    return {"version": version}


def mutate(image: Image, mutation: str) -> list[dict[str, int | str]]:
    changes: list[dict[str, int | str]] = []

    def record(field: str, offset: int, size: int) -> None:
        changes.append({"field": field, "offset": offset, "size": size})

    if mutation == "none":
        return changes
    if mutation in {"unknown-feature", "feature-prefix"}:
        offset = SUPER + 80
        image.put_u32(offset, image.u32(offset) | UNKNOWN_INCOMPAT)
        record("feature_incompat", offset, 4)
    if mutation == "blocks":
        offset = SUPER + 36
        image.put_u32(offset, 0xFFFFFFFF)
        record("blocks_lo", offset, 4)
    elif mutation == "extslots":
        offset = SUPER + 13
        image.data[offset] = 0xFF
        record("sb_extslots", offset, 1)
    elif mutation == "dirblk":
        offset = SUPER + 90
        image.data[offset] = 1
        record("dirblkbits", offset, 1)
    elif mutation in {"prefix-offset", "feature-prefix"}:
        offset = SUPER + 92
        image.put_u32(offset, 0xFFFFFFFF)
        record("xattr_prefix_start", offset, 4)
    elif mutation != "unknown-feature":
        raise ValueError(f"unknown B21 mutation: {mutation}")
    return changes


def generate(spec_path: Path, output: Path, work: Path) -> int:
    spec = load_spec(spec_path)
    if output.exists() or work.exists():
        raise SystemExit("B21 output and work paths must not already exist")
    output.mkdir(parents=True)
    work.mkdir(parents=True)
    seed_path = work / "seed.erofs"
    build = build_seed(spec, seed_path, work / "seed-work")
    seed = Image.load(seed_path)
    seed_hash = sha256(seed_path)
    cases = []
    for item in spec["cases"]:
        image = seed.clone()
        changes = mutate(image, item["mutation"])
        if item["authenticated"] and item["mutation"] != "none":
            image.update_checksum()
        path = output / f"{item['id']}.erofs"
        path.write_bytes(image.data)
        checksum_valid = image.checksum_valid()
        if checksum_valid != item["authenticated"]:
            raise AssertionError(f"checksum state mismatch for {item['id']}")
        cases.append(
            {
                **item,
                "changes": changes,
                "checksum_valid": checksum_valid,
                "filename": path.name,
                "sha256": sha256(path),
                "size": path.stat().st_size,
            }
        )
    index = {
        "batch": "B21",
        "build": build,
        "case_count": len(cases),
        "cases": cases,
        "damaged_count": sum(item["class"] == "damaged" for item in cases),
        "legal_count": sum(item["class"] == "legal" for item in cases),
        "schema": 1,
        "seed_sha256": seed_hash,
    }
    write_json(output / "fixture-index.json", index)
    print(json.dumps({"status": "PASS", **index}, sort_keys=True))
    return 0


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--spec", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    parser.add_argument("--work", type=Path, required=True)
    args = parser.parse_args()
    return generate(args.spec, args.output, args.work)


if __name__ == "__main__":
    raise SystemExit(main())
