#!/usr/bin/env python3
"""Download and SHA-256-verify the unchanged rank-eight publication v6 ZIP.

Python 3.9+, standard library only. Run in the intended download directory.
No file is overwritten. The temporary file is removed if a check fails.
"""
import hashlib
import json
import os
from pathlib import Path
import tempfile
from urllib.request import Request, urlopen

BASE = "https://michaelschroeder.ai/research/EightPrimeSupport/"
NAME = "eight_prime_support_companion_v6.zip"
SIZE = 105789211
DIGEST = "f21fb6f72dbba419adf151243adc02cd91389de430821957309594da7a98b858"
USER_AGENT = "Rank8CompanionDownloader/6"


def open_url(url):
    # Identify the client: some hosting filters reject urllib's default agent.
    return urlopen(Request(url, headers={"User-Agent": USER_AGENT}), timeout=60)


def download(base=BASE, directory=Path(".")):
    """The optional arguments support a local, byte-identical transport test."""
    output = directory / NAME
    if output.exists():
        raise FileExistsError(f"Will not overwrite {output}")
    with open_url(base + "download.json") as response:
        manifest = json.load(response)
    if (manifest.get("name"), manifest.get("bytes"), manifest.get("sha256")) != (NAME, SIZE, DIGEST):
        raise ValueError("The manifest does not match publication v6")
    parts = manifest.get("parts", [])
    if len(parts) != 5:
        raise ValueError("Expected exactly five parts")
    for i, part in enumerate(parts):
        expected_name = f"{NAME}.part{i + 1:02}"
        expected_size = 24000000 if i < 4 else 9789211
        digest = part.get("sha256", "")
        if (part.get("name"), part.get("bytes")) != (expected_name, expected_size):
            raise ValueError("Invalid part metadata")
        if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
            raise ValueError("Invalid part digest")
    temporary = None
    try:
        total = 0
        complete = hashlib.sha256()
        with tempfile.NamedTemporaryFile(prefix="rank8-download-", suffix=".partial",
                                         dir=directory, delete=False) as stream:
            temporary = Path(stream.name)
            for i, part in enumerate(parts, 1):
                print(f"Downloading part {i}/5…", flush=True)
                digest, count = hashlib.sha256(), 0
                with open_url(base + part["name"]) as response:
                    while block := response.read(1024 * 1024):
                        count += len(block)
                        if count > part["bytes"]:
                            raise ValueError(f"Part {i} is larger than expected")
                        digest.update(block)
                        complete.update(block)
                        stream.write(block)
                if count != part["bytes"] or digest.hexdigest() != part["sha256"]:
                    raise ValueError(f"Part {i} failed size or SHA-256 verification")
                total += count
            if total != SIZE or complete.hexdigest() != DIGEST:
                raise ValueError("The complete ZIP failed verification")
            stream.flush()
            os.fsync(stream.fileno())
        # Atomic, no-overwrite publication on the same filesystem.
        os.link(temporary, output)
        print(f"Verified and saved: {output}\nSHA-256: {DIGEST}")
        return output
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)


if __name__ == "__main__":
    try:
        download()
    except (OSError, ValueError) as error:
        raise SystemExit(f"Download failed: {error}")
