"""Stage/verify browser-only maps. Activation occurs with the browser's Apache config.

The APK catalog, its objects, game services, and all previous map data are read-only.
"""
import gzip,hashlib,json,pathlib,re,shutil,sys,tarfile,zipfile
task,manifest_hash=sys.argv[1:];assert re.fullmatch('[a-z0-9-]+',task)
root=pathlib.Path('/var/www/html/ro-th-online')
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
manifest_path=root/'staging'/(task+'-maps.json');assert sha(manifest_path)==manifest_hash
m=json.loads(manifest_path.read_text());assert m['task']==task
archive=root/'staging'/(task+'-maps.tar.gz');assert sha(archive)==m['archiveHash']
dest=root/'content/browser-maps'/task
if dest.exists():
    assert not dest.is_symlink() and dest.resolve().parent==(root/'content/browser-maps').resolve()
    assert not (dest/'verified.json').exists(),'Already prepared'
    assert not (root/'deploy'/task).exists(),'Release already active'
    assert task not in pathlib.Path('/etc/apache2/sites-enabled/zz-ro-th-download-ssl.conf').read_text()
    allowed={r['map']+'.bytes' for r in m['maps']}
    for p in dest.iterdir():
        assert p.name in allowed and p.is_file() and not p.is_symlink()
else:dest.mkdir(parents=True,exist_ok=False)
catalog_path=pathlib.Path('/var/www/html/test-apk/patches/Maps/latest.json.gz')
catalog_hash=sha(catalog_path);cat=json.loads(gzip.decompress(catalog_path.read_bytes()))
expected={row['map']+'/'+n for row in m['maps'] for n in row['payload']}
assert shutil.disk_usage(root).free>sum(row['bytes'] for row in m['maps'])+1024*1024*1024
with tarfile.open(archive) as tar:
    members=tar.getmembers();assert len(members)==len(expected) and {i.name for i in members}==expected
    assert all(i.isfile() for i in members)
    for row in m['maps']:
        assert re.fullmatch('[A-Za-z0-9_@-]+',row['map'])
        path=dest/(row['map']+'.bytes')
        record=next(e for e in cat['maps'] if e['name']==row['map'])
        obj=cat['objects'][record['package']]
        oldpath=catalog_path.parent/'objects'/(obj['sha256']+obj['extension'])
        assert sha(oldpath)==obj['sha256']
        external={e['name']:cat['objects'][e['blob']] for e in record['files']}
        changes=[]
        reuse=False
        if path.exists():
            try:
                with zipfile.ZipFile(path) as previous_result:
                    reuse=previous_result.testzip() is None and {n:hashlib.sha256(previous_result.read(n)).hexdigest() for n in previous_result.namelist()}==row['entries']
            except zipfile.BadZipFile:pass
            if not reuse:path.unlink()
        with zipfile.ZipFile(oldpath) as old,zipfile.ZipFile(path,'r' if reuse else 'x',compression=zipfile.ZIP_DEFLATED,compresslevel=6) as new:
            oldmanifest=json.loads(old.read('manifest.json'))
            oldterrain={e['file'] for e in oldmanifest['meshes'] if e.get('terrain')}
            for name,digest in row['entries'].items():
                assert '/' not in name and '\\' not in name
                if name in old.namelist():previous=old.read(name)
                elif name in external:
                    obj=external[name];previous=(catalog_path.parent/'objects'/(obj['sha256']+obj['extension'])).read_bytes()
                    assert hashlib.sha256(previous).hexdigest()==obj['sha256']
                else:previous=None
                before=hashlib.sha256(previous).hexdigest() if previous is not None else None
                if name in row['payload']:
                    raw=tar.extractfile(row['map']+'/'+name).read()
                else:raw=previous
                assert raw is not None and hashlib.sha256(raw).hexdigest()==digest,(row['map'],name,'Published source differs')
                if not reuse:new.writestr(name,raw)
                if before!=digest:changes.append(name)
            # Only manifest/ground mesh/native files may differ from published data.
            assert all(n=='manifest.json' or n in oldterrain or n.startswith('ground_native_') for n in changes), (row['map'],changes)
            for name in old.namelist():
                assert name in row['entries'],(row['map'],'Removed '+name)
            # The published APK catalog includes a separate BGM object. It remains
            # served by the gateway and is not part of the project map ZIP.
            videos={v['file'] for v in oldmanifest.get('videos',[])}
            newmanifest=json.loads(tar.extractfile(row['map']+'/manifest.json').read())
            assert {v['file'] for v in newmanifest.get('videos',[])}==videos,'Video references changed'
            assert set(external)-set(row['entries']) <= {'bgm.mp3'}|videos,(row['map'],'Unexpected external map data')
            row['preservedExternalFiles']=sorted(set(external)-set(row['entries']))
        row['verifiedDifferences']=changes
        row['sourceArchiveHash']=row.pop('sha256')
        row.update(sha256=sha(path),bytes=path.stat().st_size)
        with zipfile.ZipFile(path) as verified:
            assert verified.testzip() is None
            assert {n:hashlib.sha256(verified.read(n)).hexdigest() for n in verified.namelist()}==row['entries']
        path.chmod(0o644)
assert sha(catalog_path)==catalog_hash
dest.chmod(0o755)
m.update(verified=True,apkCatalogHash=catalog_hash)
(dest/'verified.json').write_text(json.dumps(m,indent=2))
print(json.dumps(dict(verified=True,maps=len(m['maps']),apkCatalogHash=catalog_hash,archiveHash=m['archiveHash'])))
