#!/usr/bin/env python3
"""Read-only comparison with the published C-07 six-case record (Python 3.11+).

This compares recorded JSON, not repositories or restored files. MATCH is not
a backup safety certificate or proof that somebody ran the experiment.
"""

import argparse
import copy
import hashlib
import json
from pathlib import Path
import re
import sys

REFERENCE_SHA256 = "3e99c97cfc1bdcf1617cc690247f6eb9e4c3c488fe83338d5abe1e990edbd6b4"
MAX_BYTES = 2 * 1024 * 1024
IDS = [f"S{i}" for i in range(1, 7)]


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("Duplicate JSON object key")
        result[key] = value
    return result


def read_record(path):
    # 読取サイズを限定し、JSON内のpathを開いたり実行したりしない。
    with path.open("rb") as handle:
        raw = handle.read(MAX_BYTES + 1)
    if len(raw) > MAX_BYTES:
        raise ValueError("JSON exceeds the 2 MiB limit")
    data = json.loads(raw.decode("utf-8-sig"), object_pairs_hook=unique_object,
                      parse_constant=lambda _: (_ for _ in ()).throw(ValueError("Non-finite JSON number")))
    return data, hashlib.sha256(raw).hexdigest()


def full_id(value):
    return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value) is not None


def normalized(data):
    """Remove only documented run-dependent fields; preserve all result detail."""
    data = copy.deepcopy(data)
    meta = data["metadata"]
    version = meta.pop("python")
    if not isinstance(version, str) or not re.fullmatch(r"3\.\d+\.\d+", version):
        raise ValueError("Expected a Python 3 version")
    if int(version.split(".")[1]) < 11:
        raise ValueError("Expected Python 3.11 or later")
    platform = meta["platform"]
    if platform["system"] != "Windows":
        raise ValueError("Reference experiment requires Windows")
    for key in ("release", "machine"):
        if not isinstance(platform.pop(key), str):
            raise ValueError("Invalid platform metadata")
    cases = data["cases"]
    if not isinstance(cases, list) or len(cases) != 6:
        raise ValueError("Expected exactly six cases")
    if sorted(c["case"] for c in cases) != IDS:
        raise ValueError("Missing, duplicate or unknown case ID")
    cases.sort(key=lambda c: c["case"])
    snapshots = [c["snapshot_id"] for c in cases]
    if not all(full_id(value) for value in snapshots):
        raise ValueError("Invalid snapshot ID")
    if not snapshots[0] == snapshots[1] == snapshots[2]:
        raise ValueError("S1/S2/S3 must identify the same original snapshot")
    newer = cases[4].pop("newer_snapshot_id")
    if not full_id(newer) or newer == snapshots[4]:
        raise ValueError("S5 requires distinct old and newer snapshots")
    for case in cases:
        case.pop("snapshot_id")
        for key in ("missing", "unexpected", "parser_failures"):
            if not isinstance(case[key], list) or not all(isinstance(p, str) for p in case[key]):
                raise ValueError("Expected a list of paths")
            if len(set(case[key])) != len(case[key]):
                raise ValueError("Duplicate path in result list")
            case[key] = sorted(case[key])
        if not isinstance(case["file_results"], list):
            raise ValueError("Expected a list of file results")
        case["file_results"].sort(key=lambda f: f["path"])
    corruption = cases[2]["corruption"]
    before = corruption.pop("pack_sha256_before")
    after = corruption.pop("pack_sha256_after")
    pack_id = corruption.pop("pack_id")
    offset = corruption.pop("byte_offset")
    length = corruption.pop("pack_bytes")
    if not all(full_id(value) for value in (before, after, pack_id)) or before != pack_id or before == after:
        raise ValueError("Invalid S3 pack hash relationships")
    if type(offset) is not int or type(length) is not int or not 16 <= offset < length:
        raise ValueError("Invalid S3 byte location")
    return data


def differences(expected, actual, path="record"):
    # boolとintを同一視しない。入力値は表示せず、差のある場所だけ返す。
    if type(expected) is not type(actual):
        return [path + ": type differs"]
    if isinstance(expected, dict):
        errors = []
        if expected.keys() != actual.keys():
            errors.append(path + ": fields differ")
        for key in expected.keys() & actual.keys():
            errors.extend(differences(expected[key], actual[key], path + "." + key))
        return sorted(errors)
    if isinstance(expected, list):
        if len(expected) != len(actual):
            return [path + ": item count differs"]
        return [error for i, (a, b) in enumerate(zip(expected, actual))
                for error in differences(a, b, f"{path}[{i}]")]
    return [] if expected == actual else [path + ": value differs"]


def compare(reference, candidate):
    return differences(normalized(reference), normalized(candidate))


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--results", required=True, type=Path,
                        help="results.json from your fresh synthetic lab")
    args = parser.parse_args()
    if sys.version_info < (3, 11):
        parser.error("Python 3.11 or later is required")
    try:
        reference, digest = read_record(Path(__file__).with_name("results.json"))
        if digest != REFERENCE_SHA256:
            raise ValueError("Reference results.json hash differs; use the unchanged published record")
        candidate, candidate_digest = read_record(args.results)
        errors = compare(reference, candidate)
    except (OSError, ValueError, KeyError, TypeError, AttributeError, RecursionError):
        print("INVALID: unreadable/unsupported record or reference; see lab-readme.txt.")
        return 2
    if errors:
        print("MISMATCH: recorded outcomes differ from the published six-case reference.")
        for error in errors[:30]:
            print(error)
        if len(errors) > 30:
            print(f"... {len(errors) - 30} additional differences")
        return 1
    print("MATCH: recorded conditions and stable results match all six reference cases.")
    if candidate_digest == digest:
        print("Input is byte-identical to the reference; this is not evidence of a new run.")
    print("Run-dependent IDs, pack layout, and Python/Windows release labels may differ.")
    print("JSON comparison only: no restored files were read or rehashed; no backup safety claim.")
    return 0


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