#!/usr/bin/env python3
"""Standalone reference client: bound LAS curve -> local interval offset -> portable result.
Python standard library and documented HTTP routes. Browser login uses the
companion application_access.py reference helper; no internal package imports.
"""
import argparse,base64,hashlib,json,math,os,pathlib,sys,urllib.request,urllib.error,urllib.parse,uuid

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self,*args,**kwargs):raise ValueError('Redirect refused; verify the configured gateway URL')

def main():
    p=argparse.ArgumentParser(description=__doc__)
    p.add_argument('--url',required=True);p.add_argument('--project',required=True);p.add_argument('--binding',required=True)
    p.add_argument('--start',type=float,required=True);p.add_argument('--stop',type=float,required=True);p.add_argument('--offset',type=float,required=True)
    p.add_argument('--credentials',type=pathlib.Path,help='Private browser-authorized application file')
    p.add_argument('--output',type=pathlib.Path,default=pathlib.Path('curve-result'))
    args=p.parse_args();url=urllib.parse.urlsplit(args.url)
    if url.scheme!='https' and not(url.scheme=='http' and url.hostname in ('localhost','127.0.0.1','::1')):raise ValueError('Use HTTPS or a loopback demo gateway')
    if url.username or url.password or url.query or url.fragment:raise ValueError('Gateway URL must not contain credentials/query/fragment')
    if not all(math.isfinite(v) for v in (args.start,args.stop,args.offset)) or args.start>args.stop:raise ValueError('Finite ordered interval and offset required')
    token=os.environ.get('OPHIOLITE_TOKEN','')
    access=None
    if args.credentials or not token:
        from application_access import Session
        access=Session(args.credentials) if args.credentials else Session()
    opener=urllib.request.build_opener(NoRedirect)
    def call(op,**body):
        req=urllib.request.Request(args.url.rstrip('/')+'/api/v1/projects/'+urllib.parse.quote(args.project,safe='')+'/applications/'+op,
             data=json.dumps({'project_id':args.project,**body},allow_nan=False).encode(),headers={'Content-Type':'application/json',**(access.headers(args.url,args.project) if access else {'Authorization':'Bearer '+token})})
        try:
            with opener.open(req,timeout=60) as response:return json.load(response)
        except urllib.error.HTTPError as e:
            error=json.loads(e.read()).get('error','Request failed')
            raise ValueError(f'{op}: HTTP {e.code}: {error}. Keep the output directory; renew credentials and repeat the same command if needed.') from None
    os.umask(0o077);args.output.mkdir(parents=True,exist_ok=True)
    def write_json(path,value):
        temporary=path.with_suffix(path.suffix+'.tmp')
        with temporary.open('w') as f:
            json.dump(value,f,indent=2,allow_nan=False,sort_keys=True);f.flush();os.fsync(f.fileno())
        temporary.replace(path)
    state_path=args.output/'run.json'
    config={'url':args.url,'project':args.project,'binding':args.binding,'parameters':{'start':args.start,'stop':args.stop,'offset':args.offset}}
    if state_path.exists():
        state=json.loads(state_path.read_text())
        if state['config']!=config:raise ValueError('Output directory belongs to another request; choose a new directory')
    else:
        binding=next((b for b in call('list')['bindings'] if b['id']==args.binding),None)
        if not binding:raise ValueError('Binding unavailable to this credential')
        state={'config':config,'generation':binding['generation'],'command_id':uuid.uuid4().hex}
        write_json(state_path,state)
    run=call('start',id=args.binding,generation=state['generation'],command_id=state['command_id'],application_version='interval-offset/1',parameters=config['parameters'])
    recovered=run['state']=='published'
    if not recovered:
        changes_path=args.output/'changes.json'
        if changes_path.exists():
            changes=json.loads(changes_path.read_text())
        else:
            curve=call('read',id=run['id']);original=call('original',id=run['id'])
            raw=base64.b64decode(original['payload_base64'],validate=True)
            if hashlib.sha256(raw).hexdigest()!=curve['source_sha256']:raise ValueError('Original checksum mismatch')
            (args.output/'original.las').write_bytes(raw)
            (args.output/'input.json').write_text(json.dumps(curve,indent=2,allow_nan=False))
            changes=[{'index':i,'value':v+args.offset} for i,(d,v) in enumerate(zip(curve['axis'],curve['values'])) if args.start<=d<=args.stop and v is not None and args.offset!=0]
            if len(changes)>1000:raise ValueError('At most 1,000 changed samples; choose a smaller depth interval and a new output directory')
            write_json(changes_path,changes)
        run=call('publish',id=run['id'],changes=changes)
    artifact=call('download',id=run['id']);raw=base64.b64decode(artifact['payload_base64'],validate=True)
    if hashlib.sha256(raw).hexdigest()!=run['receipt']['manifest']['sha256']:raise ValueError('Output checksum mismatch')
    (args.output/'result.las').write_bytes(raw);write_json(args.output/'receipt.json',run)
    print('Recovered existing publication; source unchanged. Run:' if recovered else 'Published portable LAS; source unchanged. Run:',run['id']);print('Output:',args.output/'result.las');print('Provenance is script-declared. This is not scientific approval.')

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