#!/usr/bin/env python3
"""Download, verify and prepare the supplied CAD source snapshots (Python 3.12+)."""
import argparse
import hashlib
import json
import pathlib
import shutil
import subprocess
import sys
import tarfile
import urllib.parse
import urllib.request

def digest(path):
    result=hashlib.sha256()
    with path.open('rb') as source:
        for block in iter(lambda:source.read(1024*1024),b''):
            result.update(block)
    return result.hexdigest()

def check_name(value):
    if pathlib.PurePath(value).name!=value or value in ('.','..'):
        raise ValueError('Unsafe manifest filename')
    return value

def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--base-url',help='Directory URL containing this script and SOURCES-MANIFEST.json')
    parser.add_argument('--work-dir',default='layerdream-cad-source',help='New destination directory')
    args=parser.parse_args()
    if sys.version_info<(3,12):
        parser.error('Python 3.12 or newer is required')
    if shutil.which('git') is None:
        parser.error('Git is required')
    here=pathlib.Path(__file__).resolve().parent
    root=pathlib.Path(args.work_dir).resolve()
    if root.exists():
        parser.error('Destination already exists. Choose a new --work-dir to preserve your changes.')
    root.mkdir(parents=True)
    downloads=root/'downloads';downloads.mkdir()
    archives=root/'archives';archives.mkdir()
    snapshots=root/'snapshots';snapshots.mkdir()
    base=args.base_url.rstrip('/')+'/' if args.base_url else None

    def obtain(filename):
        filename=check_name(filename)
        dest=downloads/filename
        local=here/filename
        if local.is_file():
            shutil.copyfile(local,dest)
        elif base:
            url=urllib.parse.urljoin(base,urllib.parse.quote(filename))
            print('Downloading',filename,flush=True)
            with urllib.request.urlopen(url,timeout=180) as source,dest.open('wb') as output:
                shutil.copyfileobj(source,output)
        else:
            raise FileNotFoundError(f'Missing {filename}; place all downloads beside this script or supply --base-url')
        return dest

    manifest_path=obtain('SOURCES-MANIFEST.json')
    manifest=json.loads(manifest_path.read_text())
    components={item['component']:item for item in manifest['components']}
    for item in manifest['components']:
        target=archives/check_name(item['delivery_archive'])
        with target.open('wb') as output:
            for part in item['downloads']:
                download=obtain(part['file'])
                if download.stat().st_size!=part['bytes'] or digest(download)!=part['sha256']:
                    raise RuntimeError('Download checksum mismatch: '+part['file'])
                with download.open('rb') as source:
                    shutil.copyfileobj(source,output)
        if target.stat().st_size!=item['delivery_archive_bytes'] or digest(target)!=item['delivery_archive_sha256']:
            raise RuntimeError('Reassembled archive checksum mismatch: '+target.name)
        print('Verified',item['component'],item['commit'],flush=True)
        with tarfile.open(target,'r:gz') as archive:
            archive.extractall(snapshots,filter='data')

    def checkout(name,dest):
        item=components[name]
        source=snapshots/item['delivery_root']
        subprocess.run(['git','clone','--no-hardlinks',str(source),str(dest)],check=True)
        actual=subprocess.check_output(['git','-C',str(dest),'rev-parse','HEAD'],text=True).strip()
        if actual!=item['commit']:
            raise RuntimeError('Wrong Git commit after extraction: '+name)
        subprocess.run(['git','-C',str(dest),'fsck','--full'],check=True)
        subprocess.run(['git','-C',str(dest),'remote','set-url','origin',item['repository']+'.git'],check=True)

    ocjs=root/'ocjs'
    checkout('opencascade-js',ocjs)
    deps=ocjs/'deps';deps.mkdir(exist_ok=True)
    for name,directory in [('occt','OCCT'),('rapidjson','rapidjson'),('freetype','freetype')]:
        checkout(name,deps/directory)
    for name,dest in [('replicad',root/'replicad'),('emsdk',deps/'emsdk'),('emscripten',root/'emscripten-source')]:
        shutil.copytree(snapshots/components[name]['delivery_root'],dest,symlinks=True)
    notices=root/'source-notices';notices.mkdir()
    for supplement in manifest.get('supplements',[]):
        download=obtain(supplement['file'])
        if download.stat().st_size!=supplement['bytes'] or digest(download)!=supplement['sha256']:
            raise RuntimeError('Source supplement checksum mismatch: '+supplement['file'])
        shutil.copyfile(download,notices/download.name)
    shutil.copyfile(manifest_path,root/'SOURCES-MANIFEST.json')
    print('\nSource preparation complete:',root)
    print('Next: follow BUILDING.md. Compiler/tool downloads and the actual build have not run.')

if __name__=='__main__':
    main()
