#!/usr/bin/env python3
"""CC0-1.0. Bounded HDF5 VDS dependency experiment using private temporary files."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone

import h5py
import numpy as np

CASES = ("present", "missing-file", "missing-dataset", "substituted-source",
         "legitimate-zero", "relocated-vds-only", "relocated-complete", "prefix-override")
LEFT = [11, 12, 13]
RIGHT = [21, 22, 23]
REPLACEMENT = [91, 92, 93]


def require(condition, message):
    if not condition:
        raise RuntimeError(message)


def sha256(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def source(path, values, name="data"):
    with h5py.File(path, "w", libver="v110", driver="sec2") as handle:
        handle.create_dataset(name, data=np.array(values, dtype="<i4"), track_times=False)


def inspect(directory):
    result = {}
    with h5py.File(directory / "view.h5", "r", driver="sec2") as handle:
        for name in ("default", "sentinel"):
            dataset = handle[name]
            result[name] = {
                "values": dataset[:].tolist(), "shape": list(dataset.shape),
                "dtype": dataset.dtype.str, "fillValue": int(dataset.fillvalue),
                "isVirtual": bool(dataset.is_virtual),
                "mappings": [{"file": m.file_name, "dataset": m.dset_name}
                             for m in dataset.virtual_sources()],
            }
    with h5py.File(directory / "materialized.h5", "r", driver="sec2") as handle:
        dataset = handle["data"]
        result["materialized"] = {"values": dataset[:].tolist(),
                                  "isVirtual": bool(dataset.is_virtual)}
    return result


def run_case(root, case, repetition):
    case_dir = root / (str(repetition) + "-" + case)
    original = case_dir / "original"
    original.mkdir(parents=True)
    right = [0, 0, 0] if case == "legitimate-zero" else RIGHT
    source(original / "a.h5", LEFT)
    source(original / "b.h5", right)
    layout = h5py.VirtualLayout(shape=(2, 3), dtype="<i4")
    for i, name in enumerate(("a.h5", "b.h5")):
        layout[i, :] = h5py.VirtualSource(name, "/data", shape=(3,))
    with h5py.File(original / "view.h5", "w", libver="v110", driver="sec2") as handle:
        handle.create_virtual_dataset("default", layout)
        handle.create_virtual_dataset("sentinel", layout, fillvalue=-999)
    # Materialize only while all original dependencies are present and validated.
    expected = [LEFT, right]
    with h5py.File(original / "view.h5", "r") as handle:
        materialized = handle["default"][:]
        require(materialized.tolist() == expected, "Initial VDS differs from fixture")
    with h5py.File(original / "materialized.h5", "w", libver="v110") as handle:
        handle.create_dataset("data", data=materialized, track_times=False)
    baseline = {name: sha256(original / name)
                for name in ("view.h5", "a.h5", "b.h5", "materialized.h5")}
    target = original
    prefix = None
    if case == "missing-file":
        (original / "b.h5").unlink()
    elif case == "missing-dataset":
        source(original / "b.h5", RIGHT, name="not-data")
    elif case == "substituted-source":
        source(original / "b.h5", REPLACEMENT)
    elif case.startswith("relocated-"):
        target = case_dir / "restored"
        target.mkdir()
        for name in ("view.h5", "materialized.h5"):
            shutil.copyfile(original / name, target / name)
        if case == "relocated-complete":
            for name in ("a.h5", "b.h5"):
                shutil.copyfile(original / name, target / name)
        # No original source remains available to accidentally rescue the restore.
        shutil.rmtree(original)
    elif case == "prefix-override":
        prefix = case_dir / "override"
        prefix.mkdir()
        source(prefix / "b.h5", REPLACEMENT)

    cwd = case_dir / "empty-reader-cwd"
    cwd.mkdir()
    env = dict(os.environ)
    env.pop("HDF5_VDS_PREFIX", None)
    if prefix:
        env["HDF5_VDS_PREFIX"] = str(prefix)
    completed = subprocess.run(
        [sys.executable, str(Path(__file__).resolve()), "--read", str(target)],
        cwd=cwd, env=env, capture_output=True, text=True, timeout=30, check=True)
    observed = json.loads(completed.stdout)
    actual = {name: sha256(target / name) if (target / name).exists() else None
              for name in baseline}
    row = {
        "repetition": repetition, "case": case, "readerExitCode": completed.returncode,
        "readerStderr": completed.stderr, "prefixOverride": prefix is not None,
        "baselineFileSha256": baseline, "colocatedFileSha256": actual,
        "overrideFileSha256": sha256(prefix / "b.h5") if prefix else None,
        "viewHashMatches": actual["view.h5"] == baseline["view.h5"],
        "colocatedSourceHashesMatch": all(actual[n] == baseline[n] for n in ("a.h5", "b.h5")),
        "expectedAtCapture": expected, "observed": observed,
    }
    require(row["viewHashMatches"], "View file changed")
    require(observed["materialized"]["values"] == expected, "Materialized control changed")
    require(not observed["materialized"]["isVirtual"], "Control must not be virtual")
    wanted_default, wanted_sentinel = expected, expected
    if case in ("missing-file", "missing-dataset"):
        wanted_default, wanted_sentinel = [LEFT, [0] * 3], [LEFT, [-999] * 3]
    elif case == "relocated-vds-only":
        wanted_default, wanted_sentinel = [[0] * 3, [0] * 3], [[-999] * 3, [-999] * 3]
    elif case in ("substituted-source", "prefix-override"):
        wanted_default = wanted_sentinel = [LEFT, REPLACEMENT]
    for name, wanted in (("default", wanted_default), ("sentinel", wanted_sentinel)):
        record = observed[name]
        require(record["values"] == wanted, case + ": unexpected " + name + " values")
        require(record["shape"] == [2, 3] and record["isVirtual"], "VDS layout changed")
        require(record["mappings"] == [{"file": n, "dataset": "/data"} for n in ("a.h5", "b.h5")],
                "Mappings changed")
    return row


def run(repetitions, temp_dir=None):
    require(1 <= repetitions <= 20, "Repetitions must be in [1, 20]")
    with tempfile.TemporaryDirectory(prefix="hesela-vds-", dir=temp_dir) as temp:
        rows = [run_case(Path(temp), case, r)
                for r in range(1, repetitions + 1) for case in CASES]
    return {
        "schemaVersion": 1, "experiment": "hdf5-vds-dependencies",
        "evidenceType": "measured-api-behavior", "repetitions": repetitions,
        "casesPerRepetition": len(CASES),
        "generatedAt": datetime.now(timezone.utc).isoformat(),
        "sourceSha256": sha256(__file__),
        "requirementsSha256": sha256(Path(__file__).with_name("hdf5-vds-requirements.txt")),
        "environment": {"python": platform.python_version(), "kernel": platform.release(),
                        "machine": platform.machine(), "h5py": h5py.__version__,
                        "hdf5": h5py.version.hdf5_version, "numpy": np.__version__,
                        "driver": "sec2", "libver": "v110"},
        "method": "Eight fresh private fixtures per repetition. Each VDS maps two three-element int32 arrays into fixed (2,3) shape, once with default fill and once with -999. Every final observation uses a fresh Python process and empty working directory; HDF5_VDS_PREFIX is unset except in the named override case. All handles close before mutations. A separately materialized baseline is a control.",
        "limits": ["Synthetic integers without physical units; not a scientific instrument dataset.",
                   "One host and pinned library build; repetitions are not independent reliability trials.",
                   "No power failure, concurrent writer, corrupt source, permissions fault or network filesystem test.",
                   "Fixed relative filenames only; no unlimited selections, printf mappings, nested VDS or external links.",
                   "Colocated hashes cover named files beside the VDS, not the library's complete resolution environment.",
                   "The materialized control preserves this array only, not every group, attribute or scientific dependency."],
        "observations": rows,
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repetitions", type=int, default=3)
    parser.add_argument("--temp-dir")
    parser.add_argument("--read", type=Path, help=argparse.SUPPRESS)
    args = parser.parse_args()
    if args.read:
        print(json.dumps(inspect(args.read)))
    else:
        # Clear inherited VDS resolution before the initial in-process materialization.
        os.environ.pop("HDF5_VDS_PREFIX", None)
        print(json.dumps(run(args.repetitions, args.temp_dir), indent=2))


if __name__ == "__main__":
    main()
