#!/usr/bin/env python3
"""Check that a CAIN-42 Fabrication Audit is about YOUR code, byte for byte. No CAIN imports, standard library only.

    curl -sO https://cainstudio.online/audit-code-check.py
    python3 -I audit-code-check.py evidence.zip /path/to/your/repo [--ext .py ...]

Run verify_keystone.py from the zip first (with the key pinned): that proves the record was signed by the issuer and
not changed. This script then proves the signed record describes the code you have. It reads `scanned_manifest` from
the signed record.json, rebuilds the same manifest from your folder with the same rules the auditor's tool uses, and
compares:
  * the manifest digest (SHA-256 of the manifest's canonical JSON);
  * every file: same set of paths, same per-file SHA-256.

How the tool hashes a file: the file is read as UTF-8 text with universal newlines (CRLF becomes LF), then SHA-256 of
that text encoded as UTF-8. So `sha256sum` matches for LF files only. Folders skipped: see SKIP_DIRS. Files over
2,000,000 bytes or not valid UTF-8 are excluded by the tool, and are listed as excluded in the report.

Exit 0: the audit is about exactly this code. Exit 1: it is not (differences printed). Exit 2: bad input.
"""
import hashlib
import json
import sys
import zipfile
from pathlib import Path

SKIP_DIRS = {".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv", "env", "dist", "build", ".tox",
             ".mypy_cache", ".pytest_cache", ".ruff_cache", "site-packages", "htmlcov", ".idea", ".vscode", ".next",
             "target", "vendor"}
MAX_FILE_BYTES = 2_000_000


def rebuild(root: Path, exts):
    if root.is_file():
        return {root.name: hashlib.sha256(root.read_text(encoding="utf-8", errors="replace").encode()).hexdigest()}
    out = {}
    for p in sorted(root.rglob("*")):
        if not p.is_file():
            continue
        rel = p.relative_to(root)
        if any(part in SKIP_DIRS for part in rel.parts[:-1]) or p.suffix.lower() not in exts:
            continue
        if p.stat().st_size > MAX_FILE_BYTES:
            continue
        try:
            text = p.read_text(encoding="utf-8")
        except (UnicodeDecodeError, OSError):
            continue
        out[rel.as_posix()] = hashlib.sha256(text.encode("utf-8")).hexdigest()
    return out


def main(argv):
    if len(argv) < 2:
        print(__doc__)
        return 2
    exts = tuple(argv[i + 1].lower() for i, a in enumerate(argv) if a == "--ext") or (".py",)
    try:
        rec = json.loads(zipfile.ZipFile(argv[0]).read("record.json"))
    except (OSError, KeyError, ValueError, zipfile.BadZipFile) as e:
        print(f"cannot read record.json from {argv[0]}: {e}")
        return 2
    signed_text, signed_digest = rec.get("scanned_manifest"), rec.get("scanned_manifest_sha256")
    if not isinstance(signed_text, str) or not isinstance(signed_digest, str):
        print("this record has no scanned_manifest: it is not a Fabrication Audit record")
        return 2
    if hashlib.sha256(signed_text.encode()).hexdigest() != signed_digest:
        print("the record's manifest does not match its own digest: run verify_keystone.py, the record is damaged")
        return 1
    signed = json.loads(signed_text)
    mine = rebuild(Path(argv[1]), exts)
    my_digest = hashlib.sha256(json.dumps(mine, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
    only_signed = sorted(set(signed) - set(mine))
    only_mine = sorted(set(mine) - set(signed))
    changed = sorted(f for f in set(signed) & set(mine) if signed[f] != mine[f])
    print(f"signed manifest:  {len(signed)} files  {signed_digest}")
    print(f"your code:        {len(mine)} files  {my_digest}")
    for label, items in (("in the audit, not in your folder", only_signed), ("in your folder, not in the audit", only_mine),
                         ("changed since the audit", changed)):
        for f in items[:50]:
            print(f"  {label}: {f}")
        if len(items) > 50:
            print(f"  ... {len(items) - 50} more {label}")
    if my_digest == signed_digest:
        print("MATCH: the signed audit is about exactly this code")
        return 0
    print("DIFFERENT: the signed audit is not about this exact code")
    return 1


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