#!/usr/bin/env python3
"""Relay companion: explicit, project-scoped download to a user-selected folder.
Python 3.9+. No dependencies. Keys are read from an environment variable or hidden prompt.
Only approved latest versions are downloaded. No uploads, deletion, or shell execution.
"""
import argparse, getpass, hashlib, json, os, pathlib, re, time, urllib.request, urllib.error
BASE=os.environ.get('TRASENT_URL','https://relay-drop-dawid.iwaniuk-dawid.chatgpt.site').rstrip('/')
CHUNK=5*1024*1024

def main():
 p=argparse.ArgumentParser(description=__doc__)
 p.add_argument('--project',required=True);p.add_argument('--folder',required=True)
 p.add_argument('--once',action='store_true',help='Run once instead of watching')
 args=p.parse_args()
 if not re.fullmatch(r'[a-f0-9-]{36}',args.project):p.error('Invalid project ID')
 token=os.environ.get('RELAY_API_KEY') or getpass.getpass('Relay project key (hidden): ')
 folder=pathlib.Path(args.folder).expanduser().resolve();folder.mkdir(parents=True,exist_ok=True)
 print('Relay will save approved files ONLY in:',folder)
 state_file=folder/'.relay-state.json'
 try:state=json.loads(state_file.read_text())
 except (FileNotFoundError,ValueError):state={}
 def call(path,method='GET',body=None,headers=None):
  h={'Authorization':'Bearer '+token,'User-Agent':'RelayCompanion/1.0','Accept':'application/json'};h.update(headers or {})
  if body is not None:body=json.dumps(body).encode();h['Content-Type']='application/json'
  return urllib.request.urlopen(urllib.request.Request(BASE+'/api/relay/'+path,method=method,headers=h,data=body),timeout=60)
 def persist():
  temporary=state_file.with_suffix('.tmp');temporary.write_text(json.dumps(state,indent=2));temporary.replace(state_file)
 while True:
  try:
   with call('projects/'+args.project) as r:snapshot=json.load(r)
   superseded={a['supersedes'] for a in snapshot['assets'] if a['supersedes']}
   for a in snapshot['assets']:
    if a['status']!='approved' or a['id'] in superseded:continue
    name=re.sub(r'[<>:"/\\|?*\x00-\x1f]','_',a['name']).strip('. ') or 'file'
    target=folder/(a['id'][:8]+'-v'+str(a['version'])+'-'+name)
    if state.get(a['id'])==a['manifest_hash'] and target.exists():continue
    partial=target.with_name(target.name+'.relay-part')
    offset=partial.stat().st_size if partial.exists() else 0
    if offset>a['size']:partial.unlink();offset=0
    if offset<a['size']:
     with call('assets/'+a['id']+'/download',headers={'Range':'bytes='+str(offset)+'-'} if offset else {}) as r:
      if offset and r.status!=206:offset=0
      with partial.open('ab' if offset else 'wb') as out:
       while True:
        chunk=r.read(1024*1024)
        if not chunk:break
        out.write(chunk)
    if partial.stat().st_size!=a['size']:raise ValueError('Incomplete file: '+name)
    parts=[];whole=hashlib.sha256()
    with partial.open('rb') as f:
     while True:
      chunk=f.read(CHUNK)
      if not chunk:break
      parts.append(hashlib.sha256(chunk).hexdigest());whole.update(chunk)
    manifest=hashlib.sha256(':'.join(parts).encode()).hexdigest()
    if manifest!=a['manifest_hash']:
     partial.unlink();raise ValueError('Integrity mismatch, removed partial file: '+name)
    partial.replace(target)
    with call('projects/'+args.project+'/events','POST',{'type':'device_received','assetId':a['id'],'detail':{'device':'relay-companion','bytes':a['size'],'sha256':whole.hexdigest()}}) as r:r.read()
    state[a['id']]=manifest;persist();print('Verified and saved:',target)
   if args.once:break
   time.sleep(15)
  except urllib.error.HTTPError as e:
   if e.code in (401,403,410):raise SystemExit('Access expired, revoked or project disabled. '+str(e.code))
   print('Server error:',e.code)
   if args.once:raise SystemExit(1)
   time.sleep(15)
  except (OSError,ValueError) as e:
   print('Transfer paused; it will resume:',e)
   if args.once:raise SystemExit(1)
   time.sleep(15)
if __name__=='__main__':
 try:main()
 except KeyboardInterrupt:print('\nRelay stopped. Partial downloads retained for resume.')
