"""Read-only access to the Moser worm-problem source packages, by content.

The first layer of the AMRAL site (/moser/) publishes the programme's fifteen round packages as
their original zips, unmodified, each listed with its size and SHA-256 in the case manifest. They
are pinned here from that manifest, as it stood at the site revision named in SITE_REV; fetch_source.py
downloads them into data/source/ and keeps nothing that does not match. Zips are read in memory,
never extracted.

Every member gets a stable id  <round id>!/<member path>  and a SHA-256 over its bytes.
"""
import hashlib
import io
import zipfile
from dataclasses import dataclass
from pathlib import Path

TREE = Path(__file__).resolve().parents[1]
SOURCE_DIR = TREE / "data" / "source"
SITE = "https://amral.evemisslab.com"
SITE_REV = "d7a8e7a4397a314a5743f557c9ab62ea28741d19"       # the site commit whose /moser/manifest.json is pinned

# round id -> (package filename, size, sha256, member count), as the manifest lists them, in the programme's order
SOURCES = {
    "round-v0.1": ("Moser_Skew_Lab_v0.1.zip", 330977, "d7a832b43af9e2537335a1c9be767b39069f914e53e20b42edf5864997f736da", 13),
    "round-v0.2.2": ("Moser_Skew_Lab_v0.2.2.zip", 998480, "4375eba4e285a87c400383914dadf8fe3a5ef8327583674f1434e409b1a2fd40", 29),
    "round-v0.3": ("Moser_Skew_Lab_v0.3.zip", 392633, "9f4a3644d3e6bd8c089599a96067fe09d08fa5af2f1beb399e97a76df40ca3bc", 13),
    "round-v0.4.1": ("Moser_Skew_Lab_v0.4.1.zip", 498465, "acc4235cb12e35ef597e353cbfd35c5f054502c463c113ab1d455760bf3b1485", 16),
    "round-v0.5": ("Moser_Skew_Lab_v0.5.zip", 427879, "2a86975f16ef806febc8ba73f0264552840877266c0dd5f349b721d872379bb8", 17),
    "round-v0.6.1": ("Moser_Skew_Lab_v0.6.1.zip", 779949, "6fddf5567de9272064a30869a4c358d274346e69b70a7c5bcc993b115a19bfa1", 18),
    "round-v0.7": ("Moser_Skew_Lab_v0.7.zip", 359988, "61b02f8d8864a5b8cfb183e1c14203e57e360816d602cfb3b3ebbaba321f41ec", 21),
    "round-v0.8.3": ("Moser_Skew_Lab_v0.8.3.zip", 1613363, "26ea8d85f9f4678c181c7e07dcf6f504603cbfd1be5fb907e0760d6bfca86f12", 30),
    "round-v0.9": ("Moser_Skew_Lab_v0.9.zip", 203678, "2cc4d8b3c09a0c7af2437b892d008b9bbfadd0782350f1ef1a47451197302fa5", 16),
    "round-v0.10": ("Moser_Skew_Lab_v0.10.zip", 401392, "f269d6424bc9027bdfb3d56fb4d317c0f447e7e3d550410b04cf3313dbfbd990", 14),
    "round-v0.11": ("Moser_Skew_Lab_v0.11.zip", 180933, "82f6ea0be019ae7b4bb5aeacb78940c7aeb635459eddb26a24d714b9ddf30bf2", 19),
    "round-v0.12": ("Moser_Skew_Lab_v0.12.zip", 106356, "08b394e1ed9165d0e97973717a8cb4b81500478c28a7b589c6473075f6ae961a", 15),
    "round-v0.13": ("Moser_Skew_Lab_v0.13.zip", 265050, "35ea4218bb7396137f226d4b3b8d9ae34b6581306b2df84f28f7defea2f880e4", 18),
    "round-v0.14": ("Moser_Skew_Lab_v0.14.zip", 1443174, "8f39ba0d03645d0a02b2631d12b09242d3d2327d46b155b9725d65b76bfcfe27", 21),
    "round-v0.15": ("Moser_Skew_Lab_v0.15.zip", 4037381, "21f48cf6db6301e0538d5010168358f064e07612891c088cff1f59ee3d8eb443", 22),
}


def sha256(data):
    return hashlib.sha256(data).hexdigest()


@dataclass(frozen=True)
class Member:
    id: str
    path: str
    size: int
    sha256: str


def package_url(round_id):
    return f"{SITE}/moser/packages/{SOURCES[round_id][0]}"


def source_bytes(round_id):
    path = SOURCE_DIR / SOURCES[round_id][0]
    if not path.exists():
        raise FileNotFoundError(f"{path.name} missing - run code/fetch_source.py first")
    return path.read_bytes()


def check_pins(blobs):
    """blobs: {round id: bytes}. Returns failure strings; [] when every package matches its pin."""
    failures = []
    for rid, (name, size, digest, _) in SOURCES.items():
        data = blobs.get(rid)
        if data is None:
            failures.append(f"{rid}: missing")
        elif len(data) != size or sha256(data) != digest:
            failures.append(f"{rid}: {name} size {len(data)} sha256 {sha256(data)[:16]}... != pinned {size} {digest[:16]}...")
    return failures


def members(round_id, data, keep=lambda m: False):
    """Yield (Member, bytes-or-None) for every file in a package; bytes only where keep(member) is true."""
    zf = zipfile.ZipFile(io.BytesIO(data))
    for info in zf.infolist():
        if info.is_dir():
            continue
        name = info.filename
        if not (info.flag_bits & 0x800):                 # UTF-8 bytes stored without the flag: undo cp437
            try:
                name = name.encode("cp437").decode("utf-8")
            except (UnicodeEncodeError, UnicodeDecodeError):
                pass
        b = zf.read(info)
        m = Member(f"{round_id}!/{name}", name, len(b), sha256(b))
        yield (m, b) if keep(m) else (m, None)


def all_sources():
    """{round id: bytes} for every pinned package, refusing if any does not match its pin."""
    blobs = {rid: source_bytes(rid) for rid in SOURCES}
    failures = check_pins(blobs)
    if failures:
        raise RuntimeError("pin mismatch:\n  " + "\n  ".join(failures))
    return blobs
