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

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


def sha256_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def sha256_path(path: Path) -> str:
    return sha256_bytes(path.read_bytes())


def load_spec(path: Path) -> dict[str, object]:
    spec = json.loads(path.read_text(encoding="ascii"))
    if (
        spec.get("schema") != 1
        or spec.get("batch") != "B27"
        or spec.get("candidate") != "P15-083"
        or spec.get("test") != "TC176-stream-runtime"
    ):
        raise SystemExit("invalid B27 stream fixture spec")
    if set(spec.get("codecs", {})) != {"lzma", "deflate", "zstd"}:
        raise SystemExit("B27 codec set changed")
    return spec


def make_source(root: Path, spec: dict[str, object]) -> bytes:
    root.mkdir(parents=True)
    source_spec = spec["source"]
    content = b"".join(
        f"P15-083-{index % 64:02d}:alpha-beta-gamma-delta:{(index * 17) % 256:02x}\n".encode("ascii")
        for index in range(source_spec["line_count"])
    )
    if len(content) != source_spec["size"] or sha256_bytes(content) != source_spec["sha256"]:
        raise SystemExit("B27 deterministic payload identity changed")
    payload = root / "payload.bin"
    payload.write_bytes(content)
    os.utime(payload, (0, 0))
    os.utime(root, (0, 0))
    return content


def run(argv: list[str], cwd: Path) -> None:
    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{completed.stdout}"
        )


def build_codec(
    codec: str,
    codec_spec: dict[str, object],
    spec: dict[str, object],
    source_dir: Path,
    images: Path,
) -> list[dict[str, object]]:
    valid = images / f"{codec}-valid.erofs"
    run(["mkfs.erofs", *codec_spec["mkfs_args"], str(valid), str(source_dir)], images)
    valid_data = valid.read_bytes()
    if (
        len(valid_data) != codec_spec["expected_image_size"]
        or sha256_bytes(valid_data) != codec_spec["expected_image_sha256"]
    ):
        raise SystemExit(f"{codec} valid image is not the G04 reproducible fixture")

    extent = codec_spec["extent"]
    start = extent["physical_offset"]
    end = start + extent["physical_length"]
    block = valid_data[start:end]
    leading = next((index for index, value in enumerate(block) if value), len(block))
    stream = block[leading:]
    if leading != extent["leading_zero_bytes"] or len(stream) != extent["stream_bytes"]:
        raise SystemExit(f"{codec} pcluster padding/stream boundary changed")

    tail = bytes.fromhex(spec["tail_bytes_hex"])
    if not tail or any(value == 0 for value in tail) or leading <= len(tail):
        raise SystemExit("B27 trailing mutation is not nonzero or lacks pcluster room")
    variants: dict[str, bytearray] = {}
    tail_data = bytearray(valid_data)
    tail_data[start + leading - len(tail) : end] = stream + tail
    variants["tail"] = tail_data

    truncated_data = bytearray(valid_data)
    truncated_data[start + leading : end] = b"\0" + stream[:-1]
    variants["truncated"] = truncated_data

    corruption_start = codec_spec["corruption_start"]
    if not (codec_spec["gate"]["partial_consumed"] < corruption_start < len(stream)):
        raise SystemExit(f"{codec} corruption is not after partial oracle consumption")
    corrupt_data = bytearray(valid_data)
    corrupt_data[start + leading : end] = (
        stream[:corruption_start] + b"\0" * (len(stream) - corruption_start)
    )
    if corrupt_data == valid_data:
        raise SystemExit(f"{codec} corruption mutation changed no bytes")
    variants["corrupt"] = corrupt_data

    records = [
        {
            "class": "valid",
            "codec": codec,
            "expected_errno": 0,
            "path": valid.name,
            "sha256": sha256_path(valid),
        }
    ]
    for kind, data in variants.items():
        path = images / f"{codec}-{kind}.erofs"
        path.write_bytes(data)
        records.append(
            {
                "class": kind,
                "codec": codec,
                "expected_errno": 0 if kind == "corrupt-partial" else 97,
                "path": path.name,
                "sha256": sha256_path(path),
            }
        )
    return records


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 = load_spec(args.spec)
    if args.output.exists():
        raise SystemExit(f"refusing existing B27 output: {args.output}")
    if shutil.which("mkfs.erofs") is None:
        raise SystemExit("mkfs.erofs is required")
    args.output.mkdir(parents=True)
    source_dir = args.output / "source"
    images = args.output / "images"
    images.mkdir()
    make_source(source_dir, spec)
    records = []
    for codec in sorted(spec["codecs"]):
        records.extend(
            build_codec(codec, spec["codecs"][codec], spec, source_dir, images)
        )
    index = {
        "batch": "B27",
        "candidate": "P15-083",
        "fixture_count": len(records),
        "fixtures": records,
        "schema": 1,
        "source_sha256": spec["source"]["sha256"],
        "status": "READY",
        "test": "TC176-stream-runtime",
    }
    (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()
