#!/usr/bin/env python3
"""Manual public-API handoff: prepare an exact input OR publish reviewed output."""
import argparse,base64,hashlib,json,os,pathlib,sys,tempfile,urllib.request,urllib.parse,urllib.error,uuid
from application_access import Session,NoRedirect,origin

def digest(data):return hashlib.sha256(data).hexdigest()
def read(path):return json.loads(path.read_text())
def write(path,data):
    raw=json.dumps(data,indent=2,allow_nan=False).encode()
    if path.exists():
        if read(path)!=data:raise ValueError(f'{path.name} belongs to different work; use a new run folder')
        return
    fd,tmp=tempfile.mkstemp(dir=path.parent,prefix='.checkpoint-')
    try:
        with os.fdopen(fd,'wb') as f:f.write(raw);f.flush();os.fsync(f.fileno())
        os.link(tmp,path)  # Exclusive creation: never replace another run's checkpoint.
    finally:os.unlink(tmp)
def call(args,op,body):
    raw=json.dumps({'project_id':args.project,**body},allow_nan=False,separators=(',',':')).encode()
    if len(raw)>2100000:raise ValueError('Request exceeds 2,100,000 bytes; this bounded profile cannot publish it')
    access=Session(args.credentials)
    req=urllib.request.Request(args.url.rstrip('/')+'/api/v1/projects/'+urllib.parse.quote(args.project,safe='')+'/applications/'+op,data=raw,headers={'Content-Type':'application/json',**access.headers(args.url,args.project)})
    try:
        with urllib.request.build_opener(NoRedirect).open(req,timeout=120) as r:return json.load(r)
    except urllib.error.HTTPError as e:
        raise ValueError(f'{op}: HTTP {e.code}. Keep this run folder. Check access/login, profile and limits, then repeat the same step.') from None

def main():
    p=argparse.ArgumentParser(description=__doc__);p.add_argument('--url',required=True);p.add_argument('--project',required=True);p.add_argument('--credentials',type=pathlib.Path,required=True)
    sub=p.add_subparsers(dest='action',required=True)
    a=sub.add_parser('prepare');a.add_argument('--binding',required=True);a.add_argument('--script',type=pathlib.Path,required=True);a.add_argument('--parameters',type=pathlib.Path,required=True);a.add_argument('--work',type=pathlib.Path,required=True)
    a=sub.add_parser('publish');a.add_argument('--work',type=pathlib.Path,required=True)
    args=p.parse_args();origin(args.url);os.umask(0o077);args.work.mkdir(parents=True,exist_ok=True);state_path=args.work/'run.json'
    if args.action=='prepare':
        code=args.script.read_bytes();parameters=read(args.parameters)
        parameters={**parameters,'code_sha256':digest(code)}
        config={'url':args.url,'project':args.project,'binding':args.binding,'parameters':parameters}
        if state_path.exists():
            state=read(state_path)
            if state['config']!=config:raise ValueError('Changed code, parameters or binding: use a new run folder')
        else:
            b=next((b for b in call(args,'list',{})['bindings'] if b['id']==args.binding),None)
            if not b or b.get('publication_profile')!='las-derived-curves/1':raise ValueError('Choose an authorized append-derived-curves configuration')
            state={'config':config,'generation':b['generation'],'command_id':uuid.uuid4().hex};write(state_path,state)
        run=call(args,'start',{'id':args.binding,'generation':state['generation'],'command_id':state['command_id'],'application_version':'manual-rock-physics/1','parameters':parameters})
        source=call(args,'original',{'id':run['id']});raw=base64.b64decode(source['payload_base64'],validate=True)
        if digest(raw)!=source['sha256']:raise ValueError('Input checksum mismatch')
        for name,data in [('input.las',raw),('calculation.py',code)]:
            path=args.work/name
            if path.exists() and path.read_bytes()!=data:raise ValueError(name+' differs; stop and inspect')
            if not path.exists():path.write_bytes(data)
        write(args.work/'resolved.json',run);write(args.work/'input-reference.json',{k:v for k,v in source.items() if k!='payload_base64'})
        print('Exact input ready. No calculation or publication performed. Run:',run['id'])
    else:
        state=read(state_path)
        if state['config']['url']!=args.url or state['config']['project']!=args.project:raise ValueError('Wrong gateway/project for this run')
        if digest((args.work/'calculation.py').read_bytes())!=state['config']['parameters']['code_sha256']:raise ValueError('Calculation changed; prepare a new run')
        run=read(args.work/'resolved.json');curves=read(args.work/'curves.json')
        body={'id':run['id'],'publication_profile':'las-derived-curves/1','derived_curves':curves}
        write(args.work/'publication.json',body) # Freezes the intended request before sending.
        receipt=call(args,'publish',body);write(args.work/'receipt.json',receipt)
        result=call(args,'download',{'id':run['id']});raw=base64.b64decode(result['payload_base64'],validate=True)
        if digest(raw)!=receipt['receipt']['manifest']['sha256']:raise ValueError('Result checksum mismatch')
        target=args.work/'result.las'
        if target.exists() and target.read_bytes()!=raw:raise ValueError('Existing result differs')
        if not target.exists():target.write_bytes(raw)
        print('Committed derived asset:',run['id']);print('Revision:',receipt['receipt']['output_reference']['revision']);print('Publication does not change sharing. Manage recipients explicitly in Workspace.')

if __name__=='__main__':
    try:main()
    except (ValueError,OSError) as e:print(str(e),file=sys.stderr);sys.exit(1)
