#!/usr/bin/env python3
"""Check compute.pangle.online's price tape for yourself. Stdlib only.

    curl -sO https://compute.pangle.online/verify.py && python3 verify.py

It imports nothing from us, trusts nothing we say at runtime, and re-derives
every published number from the files themselves. If it prints a mismatch,
we are wrong — that is the point of publishing it.

WHAT IT CHECKS, per sealed day:
  · the sealed tape's sha256 against the manifest        (nothing was substituted)
  · that the file REBUILDS byte-for-byte from its rows   (it is reproducible, not just checkable)
  · the row count against the manifest
  · the Merkle root, rebuilt from scratch                (the day's contents)
  · the chained root, linking to the previous day        (the day's ORDER)
  · the .ots proof's sha256                              (the Bitcoin attestation is the one named)

WHAT IT CANNOT CHECK: whether the prices were true when recorded. Anchoring
proves a set of rows existed at a point in time and has not changed since. It
says nothing about whether we read the market correctly. Those are different
claims and we try not to blur them.

Note the User-Agent below. It is not decoration: the CDN in front of us returns
403 to Python's default `Python-urllib/x.y` on everything except /api/, so a
script without it gets refused every file it needs and no explanation.
"""
import gzip
import hashlib
import io
import json
import sys
import urllib.request

BASE = "https://compute.pangle.online"
UA = {"User-Agent": "tape-verifier/1.0 (+https://compute.pangle.online/verify.py)"}


def get(path):
    req = urllib.request.Request(BASE + path, headers=UA)
    with urllib.request.urlopen(req, timeout=90) as r:
        return r.read()


def merkle(leaves):
    """sha256 of each line, sorted ascending as hex, paired upward, last node
    duplicated on an odd level, hashing the two children as CONCATENATED TEXT."""
    lvl = sorted(leaves)
    if not lvl:
        return None
    while len(lvl) > 1:
        if len(lvl) % 2:
            lvl = lvl + [lvl[-1]]
        lvl = [hashlib.sha256((lvl[i] + lvl[i + 1]).encode()).hexdigest()
               for i in range(0, len(lvl), 2)]
    return lvl[0]


def check_day(date):
    man = json.loads(get(f"/data/days/{date}.json"))
    out, bad = [], 0

    raw = get(man["tape"]["url"])
    if hashlib.sha256(raw).hexdigest() == man["tape"]["sha256"]:
        out.append("sha256 matches the manifest")
    else:
        out.append("SHA256 MISMATCH — the file served is not the file named"); bad += 1

    lines = gzip.decompress(raw).decode().splitlines()
    header, rows = lines[0], [l for l in lines[1:] if l.strip()]

    buf = io.BytesIO()
    with gzip.GzipFile(filename="", mode="wb", fileobj=buf, compresslevel=9, mtime=0) as gz:
        gz.write((header + "\n").encode())
        for r in rows:
            gz.write((r + "\n").encode())
    if hashlib.sha256(buf.getvalue()).hexdigest() == man["tape"]["sha256"]:
        out.append("rebuilds byte-for-byte from its own rows")
    else:
        out.append("does not rebuild — checkable, but not reproducible"); bad += 1

    if len(rows) == man["tape"]["rows"]:
        out.append(f"{len(rows):,} rows, as stated")
    else:
        out.append(f"ROW COUNT {len(rows)} != stated {man['tape']['rows']}"); bad += 1

    root = merkle(hashlib.sha256(r.encode()).hexdigest() for r in rows)
    if root == man["merkle_root"]:
        out.append("merkle root reproduced")
    else:
        out.append(f"MERKLE MISMATCH — got {root[:16]}…"); bad += 1

    ch = hashlib.sha256((man["prev_chained_root"] + (root or "")).encode()).hexdigest()
    if ch == man["chained_root"]:
        out.append("chained root reproduced")
    else:
        out.append("CHAIN MISMATCH — this day does not link to the previous one"); bad += 1

    p = (man.get("proof") or {})
    if p.get("url") and p.get("sha256"):
        if hashlib.sha256(get(p["url"])).hexdigest() == p["sha256"]:
            out.append("ots proof matches (run `ots verify` for the Bitcoin half)")
        else:
            out.append("OTS PROOF MISMATCH"); bad += 1
    else:
        out.append("no ots proof published for this day")
    return out, bad, man


def main():
    print(f"verifying {BASE}\n")
    idx = json.loads(get("/data/days/index.json"))
    days = idx.get("sealed_days") or []
    if not days:
        print("no sealed days published yet")
        return 0

    total_bad, prev = 0, None
    for d in days:
        lines, bad, man = check_day(d)
        total_bad += bad
        print(f"{d}")
        for l in lines:
            print(f"   {'✗' if l.isupper() or ' MISMATCH' in l or l.startswith(('SHA','ROW','MERKLE','CHAIN','OTS','does not')) else '·'} {l}")
        if prev and man["prev_chained_root"] != prev:
            print("   ✗ this day's prev_chained_root does not match the day before it")
            total_bad += 1
        prev = man["chained_root"]
        print()

    print(f"{len(days)} sealed day(s), {total_bad} problem(s)")
    if total_bad:
        print("\nSomething does not check out. That is worth telling us about —")
        print("the whole reason this script exists is so you do not have to take our word.")
    else:
        print("\nEvery published number re-derived from the files themselves.")
        print("This proves the rows have not changed since they were anchored.")
        print("It does NOT prove the prices were right when recorded — different claim.")
    return 1 if total_bad else 0


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