#!/usr/bin/env python3
"""Download the explicit scientific package from the public site and check its bytes."""
import argparse
import hashlib
from io import BytesIO
import json
from pathlib import Path
from urllib.parse import urljoin,urlparse
from urllib.request import Request,urlopen
import zipfile
USER_AGENT='SandboxPhysics-Public-Tutorial/1.0'

def read_url(url,maximum,media_type=None):
    with urlopen(Request(url,headers={'User-Agent':USER_AGENT}),timeout=90)as response:
        if response.status!=200:raise ValueError('Unexpected HTTP status')
        if media_type and response.headers.get_content_type()!=media_type.split(';')[0]:
            raise ValueError('Unexpected scientific media type: '+url)
        content=response.read(maximum+1)
    if len(content)>maximum:raise ValueError('Scientific response exceeds its byte limit')
    return content

def verify(content,entry):
    if len(content)!=entry['bytes']or hashlib.sha256(content).hexdigest()!=entry['sha256']:
        raise ValueError('Scientific bytes/SHA256 failed: '+entry['download_name'])

def safe_file(folder,name):
    if Path(name).name!=name or name in ('','.','..'):raise ValueError('Unsafe scientific filename')
    target=folder/name
    if target.is_symlink()or target.is_dir():raise ValueError('Destination must be an ordinary scientific file')
    return target

def download(base_url,output,archive=False):
    base_url=base_url.rstrip('/')+'/'
    original=urlparse(base_url)
    if original.scheme not in ('http','https'):raise ValueError('Use an HTTP scientific source')
    manifest_bytes=read_url(urljoin(base_url,'public-reproduction.json'),2_000_000,'application/json')
    manifest=json.loads(manifest_bytes);output.mkdir(parents=True,exist_ok=True);receipt=[]
    for entry in manifest['files']:
        if bool(entry.get('isArchive'))!=archive:continue
        if not 0<entry['bytes']<25000000:raise ValueError('Oversized scientific material')
        target=safe_file(output,entry['download_name']);url=urljoin(base_url,entry['url'])
        requested=urlparse(url)
        if(requested.scheme,requested.netloc)!=(original.scheme,original.netloc):raise ValueError('Scientific URL changed origin')
        cached=target.exists()
        content=target.read_bytes()if cached else read_url(url,entry['bytes'],entry['media_type'])
        verify(content,entry);target.write_bytes(content)
        receipt.append({'name':entry['download_name'],'bytes':len(content),'sha256':entry['sha256'],'source':'verified-local-cache'if cached else'HTTP'})
        print('PASS',entry['download_name'],len(content),receipt[-1]['source'],flush=True)
        if archive:
            with zipfile.ZipFile(BytesIO(content))as package:
                if sum(item.file_size for item in package.infolist())>50000000:raise ValueError('Oversized unpacked materials')
                for item in package.infolist():
                    destination=safe_file(output,item.filename)
                    if item.file_size>=25000000 or(item.external_attr>>16)&0o170000==0o120000:raise ValueError('Invalid scientific ZIP member')
                    contents=package.read(item)
                    if destination.exists()and destination.read_bytes()!=contents:raise ValueError('Existing material differs: '+item.filename)
                    destination.write_bytes(contents)
            hashes=json.loads((output/'hashes.json').read_text())
            for item in hashes['files']:
                verify(safe_file(output,item['path']).read_bytes(),{**item,'download_name':item['path']})
    safe_file(output,'public-reproduction.json').write_bytes(manifest_bytes)
    report={'source':'Named anonymous HTTP client; cache entries are explicitly distinguished.','manifest_sha256':hashlib.sha256(manifest_bytes).hexdigest(),'files':receipt,'archive_mode':archive}
    safe_file(output,'download-receipt.json').write_text(json.dumps(report,indent=2)+'\n')
    print('Scientific package verified. Observed GPI pixels use the separate fixed-author-range downloader.',flush=True)

if __name__=='__main__':
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--base-url',default='https://sandboxphysics.com/stories/direct-imaging/')
    parser.add_argument('--output',type=Path,default=Path('direct-imaging-tutorial'))
    parser.add_argument('--archive',action='store_true',help='Download and verify one complete scientific ZIP')
    args=parser.parse_args();download(args.base_url,args.output,args.archive)
