"""Five isolated SetGet recipes. Python 3, standard library only.
Usage: python3 recipes.py <explicit base URL> <json|ci|worker|heartbeat|agent>
All demos create public random keys, use absolute expiry, and delete their own key.
"""
import argparse
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
import json
import time
import urllib.error
import urllib.parse
import urllib.request


class API:
    def __init__(self, base):
        url = urllib.parse.urlsplit(base)
        if url.scheme not in ('http', 'https') or not url.netloc or url.username or url.password or url.query or url.fragment:
            raise ValueError('Provide an explicit HTTP(S) base URL without credentials, query or fragment.')
        self.base = base.rstrip('/')

    def call(self, path, method='GET', document=None, text=None):
        body = json.dumps(document).encode() if document is not None else (text.encode() if text is not None else None)
        request = urllib.request.Request(self.base + path, data=body, method=method,
            headers={'Content-Type': 'application/json' if document is not None else 'text/plain'})
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                return response.status, response.read()
        except urllib.error.HTTPError as error:
            if error.code in (304, 404):
                return error.code, error.read()
            raise  # Never automatically replay a mutation.

    def read(self, key):
        status, body = self.call('/v1/keys/' + key)
        if status == 404:
            raise RuntimeError('Key expired or is unavailable. Start a new demo.')
        if status != 200:
            raise RuntimeError(f'Read failed: {status}')
        return json.loads(body)['value']


def run(base, recipe):
    api = API(base)
    values = {'json': json.dumps({'build': 'ready', 'checks': [True, None], 'note': 'héllo'}),
              'ci': 'passed', 'worker': 'pending',
              'heartbeat': datetime.now(timezone.utc).isoformat(),
              'agent': json.dumps({'task': 'demo', 'summary': 'Three checks passed', 'status': 'ready'})}
    value = values[recipe]
    status, body = api.call('/v1/keys', 'POST', document={'value': value,
        'content_type': 'application/json' if recipe in ('json', 'agent') else 'text/plain',
        'capabilities': False, 'ttl_seconds': 600, 'ttl_mode': 'absolute'})
    if status != 201:
        raise RuntimeError(f'Create failed: {status}; inspect state before retrying.')
    key = json.loads(body)['id']
    try:
        if api.read(key) != value:
            raise RuntimeError('Read-back did not match the published value.')
        if recipe == 'json':
            print('JSON verified: ' + json.dumps(json.loads(value), ensure_ascii=False))
        elif recipe == 'ci':
            status, svg = api.call('/badge/status/' + key)
            if status != 200 or b'<svg' not in svg or b'#4c1' not in svg:
                raise RuntimeError('Badge did not render.')
            print('CI status verified: passed; public SVG badge rendered.')
        elif recipe == 'worker':
            def wait(last):
                return api.call('/poll/' + key + '?' + urllib.parse.urlencode({'last': last, 'timeout': 1}))
            # A timeout is normal, not proof of failure or key existence.
            if wait('pending')[0] != 304:
                raise RuntimeError('Expected an unchanged-value timeout.')
            with ThreadPoolExecutor(max_workers=1) as executor:
                pending = executor.submit(wait, 'pending')
                status, _ = api.call('/v1/keys/' + key, 'PUT', text='complete')
                if status != 200:
                    raise RuntimeError(f'Worker write failed: {status}')
                status, body = pending.result()
                if status == 304:
                    # Bounded reread handles a timeout racing with the write.
                    changed = api.read(key)
                else:
                    changed = json.loads(body)['value']
                if changed != 'complete':
                    raise RuntimeError('Worker completion not observed.')
            print('Worker verified: normal timeout, then completion observed.')
        elif recipe == 'heartbeat':
            # Metadata does not renew a key. This demo also uses absolute expiry.
            status, _ = api.call('/v1/keys/' + key + '/meta')
            if status != 200:
                raise RuntimeError('Heartbeat metadata unavailable.')
            age = time.time() - datetime.fromisoformat(api.read(key)).timestamp()
            if not -5 <= age < 600:
                raise RuntimeError('Heartbeat stale or clocks disagree.')
            print('Heartbeat verified: timestamp is fresh (under 600 seconds).')
        else:
            # A separate client models the reader in another process or agent.
            handoff = json.loads(API(base).read(key))
            if handoff['status'] != 'ready':
                raise RuntimeError('Agent result is not ready.')
            print('Agent handoff verified: Three checks passed')
    finally:
        status, _ = api.call('/v1/keys/' + key, 'DELETE')
        if status not in (204, 404):
            raise RuntimeError(f'Cleanup failed: {status}; key {key} expires within ten minutes.')
    if api.call('/v1/keys/' + key)[0] != 404:
        raise RuntimeError('Deleted key remains visible.')
    print('Cleanup verified: temporary key is gone.')


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('base_url')
    parser.add_argument('recipe', choices=('json', 'ci', 'worker', 'heartbeat', 'agent'))
    args = parser.parse_args()
    run(args.base_url, args.recipe)
