#!/usr/bin/env python3
"""datatap-verify.py — verify a DataTap audit-package ZIP offline (#463).

Recomputes the SHA-256 of every artifact in the bundle and the package
chain hash, H(n) = sha256(hex(H(n-1)) || hex(sha_n)) with H(0) = 64 zeros,
over the manifest in order, and compares them with what the package
recorded. No network, no dependencies beyond the Python standard library.

    python3 datatap-verify.py PKG-0007.zip

Exit code 0 when every file and the chain verify, 1 otherwise. Reads
manifest.json when the bundle carries one; older bundles fall back to the
manifest table in COVER-SHEET.html.
"""
import hashlib, json, re, sys, zipfile

GENESIS = "0" * 64


def manifest_from_cover(html):
    rows = re.findall(r"<tr><td>([^<]+)</td><td>(\d+) B</td><td class=m>([0-9a-f]{64})</td></tr>", html)
    chain = re.search(r"<b>Integrity chain:</b> <span class=m>([0-9a-f]{64}|—)</span>", html)
    return {
        "files": [{"filename": f, "size": int(s), "sha256": h} for f, s, h in rows],
        "recorded_chain": chain.group(1) if chain and chain.group(1) != "—" else None,
    }


def chain_hash(shas):
    h = GENESIS
    for sha in shas:
        h = hashlib.sha256((h + sha).encode("ascii")).hexdigest()
    return h


def main():
    if len(sys.argv) < 2:
        print(__doc__)
        return 2

    failures = 0
    with zipfile.ZipFile(sys.argv[1]) as z:
        names = set(z.namelist())
        if "manifest.json" in names:
            manifest = json.loads(z.read("manifest.json").decode("utf-8"))
        elif "COVER-SHEET.html" in names:
            manifest = manifest_from_cover(z.read("COVER-SHEET.html").decode("utf-8"))
        else:
            print("FAIL  no manifest.json or COVER-SHEET.html in the bundle")
            return 1

        files = manifest.get("files", [])
        if not files:
            print("FAIL  manifest lists no files")
            return 1

        for f in files:
            name = f["filename"]
            if name not in names:
                print(f"FAIL  {name}: missing from bundle")
                failures += 1
                continue
            data = z.read(name)
            sha = hashlib.sha256(data).hexdigest()
            size_ok = ("size" not in f) or (len(data) == int(f["size"]))
            if sha == f["sha256"] and size_ok:
                print(f"PASS  {name}  sha256 {sha[:16]}…  {len(data)} B")
            else:
                print(f"FAIL  {name}: sha256 {sha} != recorded {f['sha256']}" + ("" if size_ok else f"; size {len(data)} != {f['size']}"))
                failures += 1

        computed = chain_hash([f["sha256"] for f in files])
        recorded = manifest.get("recorded_chain") or manifest.get("chain")
        if recorded is None:
            print(f"INFO  chain {computed} (package recorded no chain — unreleased or legacy)")
        elif computed == recorded:
            print(f"PASS  chain {computed}")
        else:
            print(f"FAIL  chain {computed} != recorded {recorded}")
            failures += 1

    print("RESULT", "PASS" if failures == 0 else f"FAIL ({failures} problem(s))")
    return 0 if failures == 0 else 1


if __name__ == "__main__":
    sys.exit(main())
