#!/usr/bin/env python3
"""Verify one day of the InfraRabbit AI Crawl Ledger without trusting the site. Python 3.8+, standard library only.

    python verify.py https://infrarabbit.com/crawlers 2026-10-26                  # the whole day
    python verify.py https://infrarabbit.com/crawlers 2026-10-26 nytimes.com bbc.co.uk

1. Downloads the day statement (<base>/proofs/<day>.txt) and the manifest (<base>/data/manifest/<day>.tsv).
2. Checks the manifest's SHA-256 against the statement, rebuilds the Merkle root from its lines (RFC 6962: leaf =
   SHA-256(0x00 || line), node = SHA-256(0x01 || left || right), split at the largest power of two below n) and checks
   the root and the leaf count. Prints "root OK".
3. For each domain: finds its robots.txt line, downloads the body by hash (<base>/raw/<sha256>) and checks the hash,
   fetches the audit path (<base>/api/v1/proof/<day>?domain=...) and checks it against the root. Prints "leaf OK".
4. The statement itself is anchored in Bitcoin by OpenTimestamps: download <base>/proofs/<day>.ots and run
   `ots verify -f <day>.txt <day>.txt.ots` (pip install opentimestamps-client) to check that independently.
Exit status 0 only when every check passed."""
import hashlib
import json
import sys
import urllib.request


def get(url: str) -> bytes:
    req = urllib.request.Request(url, headers={"User-Agent": "infrarabbit-verify/2"})
    with urllib.request.urlopen(req, timeout=60) as r:
        return r.read()


def leaf_hash(line: bytes) -> bytes:
    return hashlib.sha256(b"\x00" + line).digest()


def node(a: bytes, b: bytes) -> bytes:
    return hashlib.sha256(b"\x01" + a + b).digest()


def root_of(lines) -> tuple:
    stack, n = [], 0
    for line in lines:
        n += 1
        h, size = leaf_hash(line), 1
        while stack and stack[-1][0] == size:
            size += stack[-1][0]
            h = node(stack.pop()[1], h)
        stack.append((size, h))
    if not stack:
        return hashlib.sha256(b"").digest(), 0
    h = stack[-1][1]
    for _size, left in reversed(stack[:-1]):
        h = node(left, h)
    return h, n


def check_path(line: bytes, path: list, root_hex: str) -> bool:
    h = leaf_hash(line)
    for step in path:
        sib = bytes.fromhex(step[1:])
        h = node(sib, h) if step[0] == "L" else node(h, sib)
    return h.hex() == root_hex


def main(argv) -> int:
    if len(argv) < 3:
        print(__doc__)
        return 2
    base, day, domains = argv[1].rstrip("/"), argv[2], argv[3:]
    ok = True
    statement = get(f"{base}/proofs/{day}.txt").decode()
    fields = dict(line.split(": ", 1) for line in statement.splitlines()[1:] if ": " in line)
    print(f"statement: {statement.splitlines()[0]}  day {fields.get('day')}  leaves {fields.get('leaves')}")
    print(f"statement sha256: {hashlib.sha256(statement.encode()).hexdigest()}  (this is what OpenTimestamps anchors)")
    manifest = get(f"{base}/data/manifest/{day}.tsv")
    msha = hashlib.sha256(manifest).hexdigest()
    if msha != fields.get("manifest_sha256"):
        print(f"manifest FAIL: sha256 {msha} != statement {fields.get('manifest_sha256')}")
        ok = False
    lines = manifest.split(b"\n")
    if lines and lines[-1] == b"":
        lines.pop()
    root, n = root_of(lines)
    if root.hex() == fields.get("root") and str(n) == fields.get("leaves") and fields.get("day") == day:
        print(f"root OK  {root.hex()}  ({n} leaves)")
    else:
        print(f"root FAIL  rebuilt {root.hex()} with {n} leaves; statement says {fields.get('root')} with {fields.get('leaves')}")
        ok = False
    by_key = {}
    for line in lines:
        parts = line.decode().split("\t")
        if len(parts) >= 8:
            by_key[(parts[1], parts[2])] = (line, parts)
    for d in domains:
        hit = by_key.get((d, "robots"))
        if hit is None:
            print(f"{d}: no robots.txt line on {day}")
            ok = False
            continue
        line, parts = hit
        http, kind, sha = parts[3], parts[4], parts[6]
        msg = f"{d}: robots.txt http {http} {'error ' + kind if kind != '-' else ''}".rstrip()
        if sha != "-":
            body = get(f"{base}/raw/{sha}")
            if hashlib.sha256(body).hexdigest() != sha:
                print(f"{msg}  body FAIL (hash mismatch)")
                ok = False
                continue
            msg += f"  body sha256 OK ({len(body)} bytes)"
        proof = json.loads(get(f"{base}/api/v1/proof/{day}?domain={d}"))
        if proof.get("leaf", "").encode() != line or not check_path(line, proof.get("audit_path", []), fields.get("root", "")):
            print(f"{msg}  leaf FAIL")
            ok = False
        else:
            print(f"{msg}  leaf OK (index {proof.get('index')}, path {len(proof.get('audit_path', []))} steps)")
    print("OpenTimestamps: download", f"{base}/proofs/{day}.ots", f"and run: ots verify -f {day}.txt {day}.txt.ots")
    print("ALL OK" if ok else "SOME CHECKS FAILED")
    return 0 if ok else 1


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