#!/usr/bin/env python3
"""Generate and verify deterministic repo22 G7 stress fixtures."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
from typing import Any


BLOCK_SIZE = 4096
MIB = 1024 * 1024
SPARSE_SIZE = 4 * 1024 * MIB + BLOCK_SIZE + 1
DEEP_LEVELS = 128
MANY_SMALL_COUNT = 12000
LARGE_DIRECTORY_COUNT = 12000
CONCURRENT_FILE_COUNT = 16
CONCURRENT_FILE_SIZE = 2 * MIB
PRESSURE_FILE_SIZE = 96 * MIB
SEQUENTIAL_FILE_SIZE = 256 * MIB
RANDOM_FILE_SIZE = 64 * MIB
FIXED_UUIDS = {
    "boundaries.erofs": "00000000-0000-0000-0000-000000000071",
    "workloads.erofs": "00000000-0000-0000-0000-000000000072",
}
SPARSE_MARKERS = (
    (0, b"repo22-g7-start"),
    (BLOCK_SIZE - 8, b"g7-block-edge"),
    (2**31 - 8, b"g7-2gib-edge"),
    (2**32 - 8, b"g7-4gib-edge"),
    (SPARSE_SIZE - 16, b"repo22-g7-end!!"),
)
SPARSE_RANGES = (
    ("start", 0, 64),
    ("block-edge", BLOCK_SIZE - 32, 64),
    ("one-gib-hole", 1024 * MIB, BLOCK_SIZE),
    ("two-gib-edge", 2**31 - 32, 64),
    ("four-gib-edge", 2**32 - 32, 64),
    ("eof-edge", SPARSE_SIZE - 64, 64),
)


class FixtureError(RuntimeError):
    pass


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as source:
        while chunk := source.read(MIB):
            digest.update(chunk)
    return digest.hexdigest()


def run(command: list[str], log: Path | None = None) -> str:
    result = subprocess.run(
        command,
        check=False,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
    )
    if log is not None:
        log.write_text(
            "$ " + " ".join(command) + "\n" + result.stdout,
            encoding="utf-8",
        )
    if result.returncode != 0:
        raise FixtureError(
            f"command failed ({result.returncode}): {' '.join(command)}\n"
            f"{result.stdout}"
        )
    return result.stdout


def repeated_bytes(label: str, size: int) -> bytes:
    token = (label + "\n").encode("ascii")
    return (token * ((size + len(token) - 1) // len(token)))[:size]


def write_pattern(
    path: Path,
    size: int,
    seed: str,
    random_period: int,
) -> None:
    remaining = size
    block_index = 0
    with path.open("wb") as output:
        while remaining:
            amount = min(BLOCK_SIZE, remaining)
            if block_index % random_period == 0:
                block = hashlib.shake_256(
                    f"repo22-g7:{seed}:{block_index}".encode("ascii")
                ).digest(amount)
            else:
                block = repeated_bytes(
                    f"repo22-g7:{seed}:{block_index % 97:02d}", amount
                )
            output.write(block)
            remaining -= amount
            block_index += 1


def create_sparse(path: Path) -> None:
    with path.open("wb") as output:
        output.truncate(SPARSE_SIZE)
        for offset, marker in SPARSE_MARKERS:
            output.seek(offset)
            output.write(marker)


def create_boundaries(root: Path) -> dict[str, Any]:
    root.mkdir(parents=True)
    (root / "empty.bin").write_bytes(b"")

    maximum = root / "maximum"
    maximum.mkdir()
    create_sparse(maximum / "sparse-boundary.bin")

    deep = root / "deep"
    deep.mkdir()
    current = deep
    components = []
    for level in range(DEEP_LEVELS):
        component = f"d{level:03d}"
        components.append(component)
        current /= component
        current.mkdir()
    deep_payload = current / "payload.bin"
    deep_payload.write_bytes(repeated_bytes("repo22-g7-deep", BLOCK_SIZE))
    deep_path = str(Path("deep", *components, "payload.bin"))

    long_directory = root / "longname"
    long_directory.mkdir()
    long_name = "n" * 255
    (long_directory / long_name).write_bytes(
        repeated_bytes("repo22-g7-long-name", BLOCK_SIZE)
    )

    many_small = root / "many-small"
    many_small.mkdir()
    for index in range(MANY_SMALL_COUNT):
        shard = many_small / f"shard-{index // 1000:02d}"
        shard.mkdir(exist_ok=True)
        (shard / f"file-{index:05d}.bin").write_bytes(
            repeated_bytes(f"repo22-g7-small:{index:05d}", 1024)
        )

    large_directory = root / "large-dir"
    large_directory.mkdir()
    for index in range(LARGE_DIRECTORY_COUNT):
        (large_directory / f"entry-{index:05d}.txt").write_bytes(
            f"repo22-g7-large-dir:{index:05d}\n".encode("ascii")
        )

    return {
        "deep_path": deep_path,
        "long_name": long_name,
    }


def create_workloads(root: Path) -> None:
    root.mkdir(parents=True)
    concurrent = root / "concurrent"
    concurrent.mkdir()
    for index in range(CONCURRENT_FILE_COUNT):
        write_pattern(
            concurrent / f"reader-{index:02d}.bin",
            CONCURRENT_FILE_SIZE,
            f"concurrent-{index:02d}",
            8,
        )

    pressure = root / "pressure"
    pressure.mkdir()
    write_pattern(
        pressure / "pressure.bin",
        PRESSURE_FILE_SIZE,
        "pressure",
        8,
    )

    sequential = root / "sequential"
    sequential.mkdir()
    write_pattern(
        sequential / "sequential.bin",
        SEQUENTIAL_FILE_SIZE,
        "sequential",
        4,
    )

    random_directory = root / "random"
    random_directory.mkdir()
    write_pattern(
        random_directory / "random.bin",
        RANDOM_FILE_SIZE,
        "random",
        1,
    )


def normalize_modes(root: Path) -> None:
    for path in sorted(root.rglob("*")):
        os.chmod(path, 0o755 if path.is_dir() else 0o644)
    os.chmod(root, 0o755)


def inventory(source_root: Path, output_root: Path) -> list[dict[str, Any]]:
    records = []
    for path in sorted(item for item in source_root.rglob("*") if item.is_file()):
        records.append(
            {
                "sha256": sha256(path),
                "size": path.stat().st_size,
                "path": str(path.relative_to(output_root)),
            }
        )
    return records


def inventory_text(records: list[dict[str, Any]]) -> str:
    lines = ["sha256\tsize\tpath"]
    lines.extend(
        f"{record['sha256']}\t{record['size']}\t{record['path']}"
        for record in records
    )
    return "\n".join(lines) + "\n"


def checksum_text(records: list[dict[str, Any]]) -> str:
    return "".join(
        f"{record['sha256']}  {record['path']}\n" for record in records
    )


def create_images(output_root: Path) -> list[dict[str, Any]]:
    images = output_root / "images"
    logs = output_root / "logs"
    images.mkdir()
    logs.mkdir()
    records = []
    for image_name, source_name in (
        ("boundaries.erofs", "boundaries"),
        ("workloads.erofs", "workloads"),
    ):
        image = images / image_name
        command = [
            "mkfs.erofs",
            "--workers=1",
            "--sort=path",
            "--all-root",
            "-T0",
            "--all-time",
            f"-U{FIXED_UUIDS[image_name]}",
            "-z",
            "lz4",
            str(image),
            str(output_root / "sources" / source_name),
        ]
        run(command, logs / f"mkfs-{source_name}.log")
        run(
            ["fsck.erofs", "-d0", str(image)],
            logs / f"fsck-{source_name}.log",
        )
        records.append(
            {
                "sha256": sha256(image),
                "size": image.stat().st_size,
                "path": str(image.relative_to(output_root)),
                "command": command,
            }
        )
    return records


def write_metadata(
    output_root: Path,
    source_records: list[dict[str, Any]],
    image_records: list[dict[str, Any]],
    boundary_metadata: dict[str, Any],
) -> None:
    source_inventory = inventory_text(source_records)
    image_checksums = checksum_text(image_records)
    (output_root / "SOURCE-INVENTORY.tsv").write_text(
        source_inventory, encoding="utf-8"
    )
    (output_root / "SOURCE-SHA256SUMS").write_text(
        checksum_text(source_records), encoding="utf-8"
    )
    (output_root / "SHA256SUMS").write_text(
        image_checksums, encoding="utf-8"
    )
    (output_root / "DEEP-PATH").write_text(
        boundary_metadata["deep_path"] + "\n", encoding="ascii"
    )
    (output_root / "LONG-NAME").write_text(
        boundary_metadata["long_name"] + "\n", encoding="ascii"
    )
    (output_root / "SPARSE-RANGES.tsv").write_text(
        "".join(
            f"{label}\t{offset}\t{length}\n"
            for label, offset, length in SPARSE_RANGES
        ),
        encoding="ascii",
    )
    manifest = {
        "format": 1,
        "block_size": BLOCK_SIZE,
        "deep_levels": DEEP_LEVELS,
        "deep_path": boundary_metadata["deep_path"],
        "long_name_bytes": len(boundary_metadata["long_name"].encode("ascii")),
        "many_small_count": MANY_SMALL_COUNT,
        "large_directory_count": LARGE_DIRECTORY_COUNT,
        "concurrent_file_count": CONCURRENT_FILE_COUNT,
        "concurrent_file_size": CONCURRENT_FILE_SIZE,
        "pressure_file_size": PRESSURE_FILE_SIZE,
        "sequential_file_size": SEQUENTIAL_FILE_SIZE,
        "random_file_size": RANDOM_FILE_SIZE,
        "sparse_size": SPARSE_SIZE,
        "sparse_markers": [
            {"offset": offset, "hex": marker.hex()}
            for offset, marker in SPARSE_MARKERS
        ],
        "sparse_ranges": [
            {"label": label, "offset": offset, "length": length}
            for label, offset, length in SPARSE_RANGES
        ],
        "source_count": len(source_records),
        "source_inventory_sha256": hashlib.sha256(
            source_inventory.encode("utf-8")
        ).hexdigest(),
        "image_count": len(image_records),
        "images": image_records,
    }
    (output_root / "fixture-manifest.json").write_text(
        json.dumps(manifest, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )


def create(output_root: Path) -> None:
    if output_root.exists():
        raise FixtureError(f"output already exists: {output_root}")
    sources = output_root / "sources"
    sources.mkdir(parents=True)
    boundary_metadata = create_boundaries(sources / "boundaries")
    create_workloads(sources / "workloads")
    normalize_modes(sources)
    source_records = inventory(sources, output_root)
    image_records = create_images(output_root)
    write_metadata(
        output_root,
        source_records,
        image_records,
        boundary_metadata,
    )
    verify(output_root)


def require(condition: bool, message: str) -> None:
    if not condition:
        raise FixtureError(message)


def verify(output_root: Path) -> None:
    manifest_path = output_root / "fixture-manifest.json"
    require(manifest_path.is_file(), "missing fixture-manifest.json")
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    sources = output_root / "sources"
    source_records = inventory(sources, output_root)
    source_text = inventory_text(source_records)
    require(
        source_text == (output_root / "SOURCE-INVENTORY.tsv").read_text(
            encoding="utf-8"
        ),
        "source inventory mismatch",
    )
    require(
        checksum_text(source_records)
        == (output_root / "SOURCE-SHA256SUMS").read_text(encoding="utf-8"),
        "source checksum list mismatch",
    )
    require(len(source_records) == manifest["source_count"], "source count mismatch")
    require(
        hashlib.sha256(source_text.encode("utf-8")).hexdigest()
        == manifest["source_inventory_sha256"],
        "source inventory hash mismatch",
    )

    sparse = sources / "boundaries" / "maximum" / "sparse-boundary.bin"
    require(sparse.stat().st_size == SPARSE_SIZE, "sparse file size mismatch")
    require(
        sparse.stat().st_blocks * 512 < MIB,
        "sparse source unexpectedly consumes at least 1 MiB",
    )
    with sparse.open("rb") as source:
        for offset, marker in SPARSE_MARKERS:
            source.seek(offset)
            require(source.read(len(marker)) == marker, f"marker mismatch at {offset}")

    deep_path = (output_root / "DEEP-PATH").read_text(encoding="ascii").strip()
    require(deep_path == manifest["deep_path"], "deep path metadata mismatch")
    require((sources / "boundaries" / deep_path).is_file(), "deep payload missing")
    require(
        len(Path(deep_path).parts) - 2 == DEEP_LEVELS,
        "deep directory level mismatch",
    )
    long_name = (output_root / "LONG-NAME").read_text(encoding="ascii").strip()
    require(len(long_name.encode("ascii")) == 255, "long name is not 255 bytes")
    require(
        (sources / "boundaries" / "longname" / long_name).is_file(),
        "long-name source missing",
    )
    require(
        sum(1 for path in (sources / "boundaries" / "many-small").rglob("*") if path.is_file())
        == MANY_SMALL_COUNT,
        "many-small count mismatch",
    )
    require(
        sum(1 for path in (sources / "boundaries" / "large-dir").iterdir() if path.is_file())
        == LARGE_DIRECTORY_COUNT,
        "large-directory count mismatch",
    )
    require(
        sum(1 for path in (sources / "workloads" / "concurrent").iterdir() if path.is_file())
        == CONCURRENT_FILE_COUNT,
        "concurrent source count mismatch",
    )

    image_records = []
    logs = output_root / "logs"
    for image in sorted((output_root / "images").glob("*.erofs")):
        run(["fsck.erofs", "-d0", str(image)], logs / f"verify-{image.stem}.log")
        image_records.append(
            {
                "sha256": sha256(image),
                "size": image.stat().st_size,
                "path": str(image.relative_to(output_root)),
            }
        )
    require(len(image_records) == manifest["image_count"], "image count mismatch")
    expected_images = [
        {key: record[key] for key in ("sha256", "size", "path")}
        for record in manifest["images"]
    ]
    require(image_records == expected_images, "image inventory mismatch")
    require(
        checksum_text(image_records)
        == (output_root / "SHA256SUMS").read_text(encoding="utf-8"),
        "image checksum list mismatch",
    )
    sparse_dump = run(
        [
            "dump.erofs",
            "--path=/maximum/sparse-boundary.bin",
            str(output_root / "images" / "boundaries.erofs"),
        ]
    )
    require(f"Size: {SPARSE_SIZE} " in sparse_dump, "image sparse size mismatch")
    print(
        "verified "
        f"sources={len(source_records)} images={len(image_records)} "
        f"inventory_sha256={manifest['source_inventory_sha256']}"
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command", required=True)
    for command in ("create", "verify"):
        subparser = subparsers.add_parser(command)
        subparser.add_argument("--output", type=Path, required=True)
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    try:
        if args.command == "create":
            create(args.output.resolve())
        else:
            verify(args.output.resolve())
    except FixtureError as error:
        print(f"error: {error}")
        return 1
    return 0


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