#!/usr/bin/env python3
"""Python 3.9+. Usage: python3 label-workflow.py request.json booking.json label.pdf
Set PACSORT_API_KEY and PACSORT_BASE_URL in your environment first.
Keep booking.json: subsequent runs resume its assignment instead of POSTing again.
"""
import json, os, sys, time
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError

base = os.environ['PACSORT_BASE_URL'].rstrip('/')
key = os.environ['PACSORT_API_KEY']
request_file, state_file, pdf_file = map(Path, sys.argv[1:4])

def api(path, data=None):
    req = Request(base + path, data=data, headers={
        'Authorization': 'Bearer ' + key, 'Accept': 'application/json',
        'Content-Type': 'application/json'})
    with urlopen(req, timeout=30) as response:
        return json.load(response)

if state_file.exists():
    assignment = json.loads(state_file.read_text())
    if not assignment.get('id'):
        sys.exit('Uncertain previous POST. Reconcile in the portal before rebooking.')
else:
    payload = json.loads(request_file.read_text())
    # Exclusive creation prevents two processes from POSTing with this state file.
    with state_file.open('x') as state:
        json.dump({'bookingAttempted': True}, state)
    assignment = api('/assignment/single', json.dumps(payload).encode())
    state_file.write_text(json.dumps(assignment))
    if not assignment.get('id'):
        sys.exit('No assignment ID in response. Reconcile before rebooking.')

from urllib.parse import quote, urlparse
for attempt in range(60):
    try:
        assignment = api('/assignment/single/' + quote(str(assignment['id']), safe=''))
    except HTTPError as error:
        if error.code == 429 or error.code >= 500:
            # Bounded read retry. Retry-After can also be an HTTP date.
            from email.utils import parsedate_to_datetime
            import random
            delay = min(30, 2 ** min(attempt, 5)) + random.random()
            retry_after = error.headers.get('Retry-After')
            if retry_after:
                try:
                    delay = float(retry_after)
                except ValueError:
                    delay = max(0, parsedate_to_datetime(retry_after).timestamp() - time.time())
            if delay > 60:
                sys.exit('Long server backoff requested. Resume this booking later.')
            time.sleep(max(0, delay))
            continue
        raise
    status = assignment.get('status')
    if status in ('cancelled', 'report_denied'):
        sys.exit('Booking needs attention: ' + status)
    url = assignment.get('labelsUrl')
    if url:
        if urlparse(url).scheme != 'https':
            sys.exit('Expected an HTTPS label URL.')
        # Signed download URL: never forward the PacSort API key to storage.
        with urlopen(url, timeout=30) as response:
            pdf = response.read()
        if not pdf.startswith(b'%PDF-'):
            sys.exit('Download is not a PDF. Inspect the response before printing.')
        pdf_file.write_bytes(pdf)
        print('Saved', pdf_file)
        break
    if status == 'complete':
        sys.exit('Complete without a label URL. Inspect the booking in the portal.')
    time.sleep(5)
else:
    sys.exit('Polling limit reached. Resume with the same booking file; do not rebook.')
