"""Publish exactly three Preview archives to APK catalog and browser aliases."""
import copy,gzip,hashlib,io,json,os,shutil,subprocess,sys,tarfile,urllib.request,zipfile
from pathlib import Path
MODE,HASH=sys.argv[1:]
assert MODE in ('prepare','activate')
ROOT=Path('/var/www/html/ro-th-online'); APK=Path('/var/www/html/test-apk/patches/Maps')
TASK='payon-preview-20260928'; STAGE=ROOT/'staging'/TASK; BACK=ROOT/'deploy'/TASK
CONFIG=Path('/etc/apache2/sites-enabled/zz-ro-th-download-ssl.conf').resolve(strict=True)
INDEX=ROOT/'mobile/index.html'; LATEST=APK/'latest.json.gz'
NAMES=['payon_in01','payon_in02','payon_in03']
def sha(p):
    with open(p,'rb') as f:return hashlib.file_digest(f,'sha256').hexdigest()
def checks(m):
    assert sha(INDEX)==m['previousIndexHash'],'Live browser changed'
    assert sha(CONFIG)==m['previousConfigHash'],'Live config changed'
    assert sha(LATEST)==m['previousCatalogHash'],'Live APK catalog changed'
def remote(path):
    with urllib.request.urlopen('https://ro-th.online'+path,timeout=60) as r:
        assert r.status==200
        return r.read()
def reload():
    subprocess.run(['apache2ctl','configtest'],check=True)
    subprocess.run(['systemctl','reload','apache2'],check=True)
if MODE=='prepare':
    archive=ROOT/'staging'/(TASK+'.tar.gz');assert sha(archive)==HASH
    STAGE.mkdir(exist_ok=False)
    allowed={'release.json','index.html','ro-th-online-ssl.conf','map-refresh.js',*[n+'.bytes' for n in NAMES]}
    with tarfile.open(archive) as t:
        assert {m.name for m in t.getmembers()}==allowed and len(t.getmembers())==len(allowed)
        for item in t.getmembers():
            assert item.isfile()
            with t.extractfile(item) as src,open(STAGE/item.name,'xb') as out:shutil.copyfileobj(src,out)
    m=json.loads((STAGE/'release.json').read_text(encoding='utf-8-sig'));checks(m)
    for name,digest in m['files'].items():assert sha(STAGE/name)==digest
    assert INDEX.read_text().replace('  <script>','  <script src="/Mobile/map-refresh.js?v=payon-preview-20260928"></script>\n  <script>',1).replace('\r\n','\n')==(STAGE/'index.html').read_text().replace('\r\n','\n')
    old=json.loads(gzip.decompress(LATEST.read_bytes()));cat=copy.deepcopy(old);cat['release']=TASK
    m['expandedDifferences']={};m['previousRelease']=old['release']
    for name in NAMES:
        entry=next(e for e in cat['maps'] if e['name']==name)
        oldobj=cat['objects'][entry['package']]
        oldfiles={f['name']:cat['objects'][f['blob']] for f in entry['files']}
        newfile=STAGE/(name+'.bytes');digest=sha(newfile)
        with zipfile.ZipFile(newfile) as z,zipfile.ZipFile(APK/'objects'/(oldobj['sha256']+oldobj['extension'])) as prev:
            assert z.testzip() is None
            manifest=json.loads(z.read('manifest.json'));assert manifest['name']==name
            assert not manifest.get('videos'),'External videos require separate objects'
            required={'cells.bytes',manifest['walkSurface'],manifest['groundLightmap']}
            for mesh in manifest['meshes']:required.update((mesh['file'],mesh['texture']))
            water=manifest.get('water')
            if water:required.update([water['file'],*water['textures']])
            assert required<=set(z.namelist()),'Archive has external dependencies'
            differences=[]
            for file in z.namelist():
                if file in prev.namelist():data=prev.read(file)
                elif file in oldfiles:
                    obj=oldfiles[file];data=(APK/'objects'/(obj['sha256']+obj['extension'])).read_bytes()
                else:data=None
                if data!=z.read(file):differences.append(file)
            m['expandedDifferences'][name]=differences
        obj={'sha256':digest,'size':newfile.stat().st_size,'extension':''}
        if obj not in cat['objects']:cat['objects'].append(obj)
        entry['package']=cat['objects'].index(obj);entry['files']=[]
    assert [e for e in cat['maps'] if e['name'] not in NAMES]==[e for e in old['maps'] if e['name'] not in NAMES]
    encoded=gzip.compress(json.dumps(cat,separators=(',',':')).encode(),mtime=0)
    (STAGE/'catalog.json.gz').write_bytes(encoded)
    m['catalogHash']=hashlib.sha256(encoded).hexdigest();m['maps']=dict((e['name'],cat['objects'][e['package']]) for e in cat['maps'] if e['name'] in NAMES)
    (STAGE/'verified.json').write_text(json.dumps(m,indent=2))
    print(json.dumps({'prepared':TASK,'expandedDifferenceCounts':{k:len(v) for k,v in m['expandedDifferences'].items()}}))
else:
    m=json.loads((STAGE/'verified.json').read_text());checks(m)
    for name,digest in m['files'].items():assert sha(STAGE/name)==digest
    assert sha(STAGE/'catalog.json.gz')==m['catalogHash']
    BACK.mkdir(exist_ok=False)
    for src,name in [(INDEX,'index-before.html'),(CONFIG,'ssl-before.conf'),(LATEST,'catalog-before.json.gz')]:shutil.copy2(src,BACK/name)
    script=ROOT/'mobile/map-refresh.js';assert not script.exists()
    for name,obj in m['maps'].items():
        dest=APK/'objects'/obj['sha256']
        if dest.exists():assert sha(dest)==obj['sha256']
        else:shutil.copyfile(STAGE/(name+'.bytes'),dest);dest.chmod(0o644)
    release=APK/'releases'/(TASK+'.json.gz');assert not release.exists()
    shutil.copyfile(STAGE/'catalog.json.gz',release);release.chmod(0o644)
    try:
        shutil.copyfile(STAGE/'map-refresh.js',script);script.chmod(0o644)
        shutil.copyfile(STAGE/'ro-th-online-ssl.conf',CONFIG);reload()
        for name,obj in m['maps'].items():
            assert hashlib.sha256(remote('/content/maps/'+name+'/map.bytes')).hexdigest()==obj['sha256']
            assert hashlib.sha256(remote('/test-apk/patches/Maps/objects/'+obj['sha256'])).hexdigest()==obj['sha256']
        assert hashlib.sha256(remote('/Mobile/map-refresh.js')).hexdigest()==m['files']['map-refresh.js']
        # Atomic catalog/page replacement; retain existing permissions/ownership.
        for src,dest in [(STAGE/'catalog.json.gz',LATEST),(STAGE/'index.html',INDEX)]:
            temp=dest.with_name(dest.name+'.payon-pending');shutil.copyfile(src,temp)
            stat=dest.stat();os.chmod(temp,stat.st_mode);os.chown(temp,stat.st_uid,stat.st_gid);os.replace(temp,dest)
        assert hashlib.sha256(remote('/test-apk/patches/Maps/latest.json.gz')).hexdigest()==m['catalogHash']
        for url in ['/Mobile/','/Mobile/?installed=1']:
            assert hashlib.sha256(remote(url)).hexdigest()==m['files']['index.html']
    except Exception:
        for src,dest in [(BACK/'index-before.html',INDEX),(BACK/'ssl-before.conf',CONFIG),(BACK/'catalog-before.json.gz',LATEST)]:shutil.copy2(src,dest)
        reload();raise
    m['verified']=True
    (BACK/'release.json').write_text(json.dumps(m,indent=2));print(json.dumps(m,indent=2))
