#!/usr/bin/env python3
"""Daily PNG sync, Python 3.9+, Linux. Config and state belong outside web root."""
import argparse, datetime, fcntl, hashlib, json, os, re, struct, sys, tempfile, urllib.request, urllib.error, zlib
from pathlib import Path
from zoneinfo import ZoneInfo

def atomic_write(path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp = tempfile.mkstemp(prefix='.'+path.name+'.', dir=str(path.parent))
    try:
        with os.fdopen(fd, 'wb') as f:
            f.write(data); f.flush(); os.fsync(f.fileno())
        os.chmod(tmp, 0o644 if path.suffix=='.png' else 0o600)
        os.replace(tmp, path)
    finally:
        if os.path.exists(tmp): os.unlink(tmp)

def valid_png(data):
    if not data.startswith(b'\x89PNG\r\n\x1a\n'): return False
    pos=8; first=True
    while pos+12<=len(data):
        size=struct.unpack('>I',data[pos:pos+4])[0]; kind=data[pos+4:pos+8]; end=pos+12+size
        if end>len(data): return False
        if first and (kind!=b'IHDR' or size!=13): return False
        first=False
        if zlib.crc32(data[pos+4:pos+8+size]) & 0xffffffff != struct.unpack('>I',data[pos+8+size:end])[0]: return False
        if kind==b'IEND': return size==0 and end==len(data)
        pos=end
    return False

def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--config',required=True)
    args=parser.parse_args()
    cfg=json.loads(Path(args.config).read_text(encoding='utf-8-sig'))
    endpoint=cfg.get('endpoint','https://shengtu.494499.xyz/api.php?action=reference_generate')
    if not endpoint.startswith('https://'): raise ValueError('Use an HTTPS endpoint')
    key=os.environ.get('SHENGTU_KEY') or cfg.get('key','')
    if not key or key=='YOUR_KEY': raise ValueError('Configure a valid key privately')
    ids=cfg['image_ids']
    if not isinstance(ids,list) or not ids or any(not isinstance(i,str) or not re.fullmatch(r'[a-zA-Z0-9_-]{1,40}',i) for i in ids): raise ValueError('Invalid image_ids')
    ids=list(dict.fromkeys(ids))
    output=Path(cfg['output_dir']).resolve(); state=Path(cfg['state_dir']).resolve()
    if state==output or output in state.parents: raise ValueError('state_dir must be outside output_dir')
    state.mkdir(parents=True,exist_ok=True,mode=0o700)
    with (state/'sync.lock').open('a') as lock:
        try: fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
        except BlockingIOError:
            print('Another synchronization is running'); return 0
        request=json.loads(Path(cfg['request_file']).read_text(encoding='utf-8-sig'))
        required=['name','website','period','draw_time','previous_period','previous_draw_time','previous_draw']
        if any(k not in request for k in required): raise ValueError('Missing required request parameters')
        day=datetime.datetime.now(ZoneInfo('Asia/Shanghai')).date().isoformat()
        errors=0
        for image_id in ids:
            payload={k:request[k] for k in required}; payload['image_id']=image_id
            body=json.dumps(payload,ensure_ascii=False,sort_keys=True).encode()
            fingerprint=hashlib.sha256(day.encode()+b'\n'+body).hexdigest()
            record=state/(image_id+'.json'); target=output/(image_id+'.png')
            previous=json.loads(record.read_text()) if record.exists() else {}
            if previous.get('fingerprint')==fingerprint and target.exists() and hashlib.sha256(target.read_bytes()).hexdigest()==previous.get('sha256'):
                print(image_id+': already synchronized today'); continue
            try:
                req=urllib.request.Request(endpoint,data=body,headers={'Content-Type':'application/json','Authorization':'Bearer '+key,'User-Agent':'ShengtuReferenceClient/1.0'},method='POST')
                with urllib.request.urlopen(req,timeout=120) as response:
                    data=response.read(20*1024*1024+1)
                    version=response.headers.get('X-Image-Version','')
                    if response.headers.get_content_type()!='image/png' or len(data)>20*1024*1024 or not valid_png(data): raise ValueError('Response is not a complete PNG')
                atomic_write(target,data)
                metadata={'fingerprint':fingerprint,'sha256':hashlib.sha256(data).hexdigest(),'version':version,'period':request['period'],'date':day}
                atomic_write(record,json.dumps(metadata,ensure_ascii=False).encode())
                print(image_id+': saved '+str(target))
            except urllib.error.HTTPError as error:
                errors+=1; print(image_id+': HTTP '+str(error.code)+'; existing image retained',file=sys.stderr)
            except Exception as error:
                errors+=1; print(image_id+': '+type(error).__name__+'; existing image retained; check before retrying',file=sys.stderr)
        return 1 if errors else 0

if __name__=='__main__':
    try: sys.exit(main())
    except Exception as error:
        print('Configuration error: '+str(error),file=sys.stderr); sys.exit(2)
