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

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


EXTENT_RE = re.compile(
    r"^\s*0:\s*(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*:\s*"
    r"(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*$",
    re.MULTILINE,
)


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


def run(argv: list[str], cwd: Path) -> str:
    completed = subprocess.run(
        argv,
        cwd=cwd,
        check=False,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        timeout=120,
    )
    if completed.returncode != 0:
        raise SystemExit(
            f"command failed ({completed.returncode}): {' '.join(argv)}\n"
            f"{completed.stdout}"
        )
    return completed.stdout


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--spec", type=Path, required=True)
    parser.add_argument("--output", 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") != "B33"
        or spec.get("test") != "TC168-cache-inflight"
    ):
        raise SystemExit("invalid B33 cache fixture spec")
    if args.output.exists():
        raise SystemExit(f"refusing existing B33 output: {args.output}")
    for tool in ("dump.erofs", "mkfs.erofs"):
        if shutil.which(tool) is None:
            raise SystemExit(f"{tool} is required")

    pattern = b"Pre15-B33-cache-policy:0123456789abcdef\n"
    expected = spec["source"]
    source = (pattern * ((expected["size"] + len(pattern) - 1) // len(pattern)))[
        : expected["size"]
    ]
    if hashlib.sha256(source).hexdigest() != expected["sha256"]:
        raise SystemExit("B33 deterministic source identity changed")

    source_dir = args.output / "source"
    images = args.output / "images"
    source_dir.mkdir(parents=True)
    images.mkdir()
    payload = source_dir / "payload.bin"
    payload.write_bytes(source)
    os.utime(payload, (0, 0))
    os.utime(source_dir, (0, 0))

    valid = images / "lz4-valid.erofs"
    run(["mkfs.erofs", *spec["mkfs_args"], str(valid), str(source_dir)], images)
    extent_text = run(
        ["dump.erofs", "-e", "--path=/payload.bin", str(valid)], images
    )
    match = EXTENT_RE.search(extent_text)
    if match is None:
        raise SystemExit("B33 single extent could not be parsed")
    logical, logical_end, logical_length, physical, physical_end, physical_length = (
        map(int, match.groups())
    )
    if (
        logical != 0
        or logical_end != expected["size"]
        or logical_length != expected["size"]
        or physical_end - physical != physical_length
        or physical_length != 4096
    ):
        raise SystemExit("B33 fixture is not one 64 KiB logical pcluster")

    valid_data = valid.read_bytes()
    if physical + physical_length > len(valid_data):
        raise SystemExit("B33 physical extent exceeds image")
    pcluster = valid_data[physical : physical + physical_length]
    leading = next(
        (index for index, value in enumerate(pcluster) if value), len(pcluster)
    )
    stream = pcluster[leading:]
    if leading == len(pcluster) or len(stream) < 2:
        raise SystemExit("B33 LZ4 stream boundary is empty")
    truncated_data = bytearray(valid_data)
    truncated_data[physical + leading : physical + physical_length] = (
        b"\0" + stream[:-1]
    )
    if truncated_data == valid_data:
        raise SystemExit("B33 truncated mutation changed no bytes")
    truncated = images / "lz4-truncated.erofs"
    truncated.write_bytes(truncated_data)

    records = []
    for image_class, path, expected_errno in (
        ("valid", valid, 0),
        ("truncated", truncated, 97),
    ):
        records.append(
            {
                "class": image_class,
                "expected_errno": expected_errno,
                "path": path.name,
                "sha256": sha256_path(path),
            }
        )
    index = {
        "batch": "B33",
        "decoded_size": len(source),
        "fixture_count": len(records),
        "fixtures": records,
        "leading_zero_bytes": leading,
        "physical_length": physical_length,
        "physical_offset": physical,
        "schema": 1,
        "source_sha256": sha256_path(payload),
        "status": "READY",
        "test": "TC168-cache-inflight",
    }
    (args.output / "fixture-index.json").write_text(
        json.dumps(index, indent=2, sort_keys=True) + "\n", encoding="ascii"
    )
    print(json.dumps(index, indent=2, sort_keys=True))


if __name__ == "__main__":
    main()
