#!/usr/bin/env python3
"""Explicitly request write-only Supatrace access. Owner approves in browser.
Saves credentials only to the --config path requested by the caller, mode 0600.
Never prints keys. No capture, discovery, or uploads happen in this command.
"""
import argparse,hashlib,json,pathlib,re,secrets,time,urllib.request,urllib.error,urllib.parse
from upload import atomic_write,ORIGINS,DEFAULT_ORIGIN

def post(origin,path,payload):
 class NoRedirect(urllib.request.HTTPRedirectHandler):
  def redirect_request(self,*args,**kwargs):return None
 body=json.dumps(payload).encode()
 req=urllib.request.Request(origin+path,body,headers={'Content-Type':'application/json','x-amz-content-sha256':hashlib.sha256(body).hexdigest()},method='POST')
 with urllib.request.build_opener(NoRedirect).open(req,timeout=30) as r:
  raw=r.read(8193)
  if len(raw)>8192:raise ValueError('Unexpected response size')
  return json.loads(raw)

def main():
 p=argparse.ArgumentParser(description=__doc__);p.add_argument('--origin',default=DEFAULT_ORIGIN);p.add_argument('--config',required=True);p.add_argument('--project');a=p.parse_args()
 if a.origin not in ORIGINS:p.error('Only '+' or '.join(ORIGINS)+' is supported')
 if a.project and not re.fullmatch(r'proj_[a-f0-9]{32}',a.project):p.error('Invalid project id')
 path=pathlib.Path(a.config)
 if path.exists():
  state=json.loads(path.read_text())
  bound=state.get('project_id') or state.get('requested_project_id')
  if a.project and bound and a.project!=bound:p.error('Existing config belongs to a different project; do not replace its access implicitly')
  if state.get('approved'):print('Already connected. Revoke in your workspace before replacing access.');return
 else:
  key=secrets.token_urlsafe(32);start=post(a.origin,'/api/setup/start'+('?project='+urllib.parse.quote(a.project) if a.project else ''),{'key_hash':hashlib.sha256(key.encode()).hexdigest()});state={**start,'origin':a.origin,'write_key':key,'approved':False,**({'requested_project_id':a.project} if a.project else {})};atomic_write(path,json.dumps(state).encode())
 print('Ask your owner to open '+a.origin+'/app/ and approve this code: '+state['user_code'])
 for _ in range(60):
  try:
   status=post(a.origin,'/api/setup/poll',{'device_secret':state['device_secret']})
   if status.get('approved'):
    project=status.get('project_id')
    if not isinstance(project,str) or not re.fullmatch(r'proj_[a-f0-9]{32}',project):raise ValueError('Approved project was not verified')
    if a.project and project!=a.project:raise ValueError('Approved project did not match requested project')
    state['project_id']=project
    state['approved']=True;state.pop('device_secret',None);atomic_write(path,json.dumps(state).encode());print('Connected with write-only access. No traces have been uploaded.');return
  except urllib.error.HTTPError as e:
   if e.code==410:raise SystemExit('Setup expired. No new key was granted by this response. Start a new setup after removing this pending config.')
   if e.code not in (429,500,502,503):raise SystemExit('Setup could not complete; response code '+str(e.code))
  except (OSError,ValueError):pass
  time.sleep(10)
 raise SystemExit('Not yet confirmed. Rerun with the same config to continue; no key is printed or discarded.')
if __name__=='__main__':main()
