import csv
import http.client
import io
import json
import socket
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

TRANSIENT_STATUSES = {408, 429, 500, 502, 503, 504}


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, request, response, code, message, headers, new_url):
        return None


OPENER = urllib.request.build_opener(NoRedirect)


def request(stage, url, method, content_type, body):
    for attempt in range(3):
        delay = 2 ** attempt
        req = urllib.request.Request(url, data=body, method=method, headers={
            'Content-Type': content_type,
            'User-Agent': 'YoinkRecipe/1.0',
        })

        try:
            with OPENER.open(req, timeout=60) as response:
                if not 200 <= response.status < 300:
                    raise RuntimeError(f'{stage}: unexpected HTTP {response.status}')
                return response.read()
        except urllib.error.HTTPError as error:
            detail = error.read(500).decode('utf-8', errors='replace')
            if error.code not in TRANSIENT_STATUSES or attempt == 2:
                raise RuntimeError(f'{stage}: HTTP {error.code}: {detail}') from error
            retry_after = error.headers.get('Retry-After', '')
            if retry_after.isdigit():
                delay = int(retry_after)
                if delay > 30:
                    raise RuntimeError(f'{stage}: HTTP {error.code}; retry later ({delay}s requested)') from error
            print(f'{stage}: HTTP {error.code}; retry {attempt + 1}/2', file=sys.stderr)
        except urllib.error.URLError as error:
            reason = error.reason
            temporary = isinstance(reason, (TimeoutError, ConnectionError)) or (
                isinstance(reason, socket.gaierror) and reason.errno == socket.EAI_AGAIN
            )
            if not temporary or attempt == 2:
                raise RuntimeError(f'{stage}: transport failed: {reason}') from error
            print(f'{stage}: temporary connection failure; retry {attempt + 1}/2', file=sys.stderr)
        except (TimeoutError, ConnectionError, http.client.IncompleteRead) as error:
            if attempt == 2:
                raise RuntimeError(f'{stage}: response interrupted: {error}') from error
            print(f'{stage}: response interrupted; retry {attempt + 1}/2', file=sys.stderr)

        time.sleep(max(1, delay))

    raise RuntimeError(f'{stage}: retries exhausted')


def main():
    output = io.StringIO(newline='')
    writer = csv.writer(output)
    writer.writerow(['sample', 'checks_passed', 'checks_total'])
    writer.writerows([['demo-alpha', 8, 10], ['demo-beta', 10, 10]])
    data = output.getvalue().encode('utf-8')
    filename = 'public-report.csv'
    with Path(filename).open('xb') as local_file:
        local_file.write(data)

    presign_body = json.dumps({
        'filename': filename,
        'contentType': 'text/csv',
        'size': len(data),
    }).encode('utf-8')
    signed = json.loads(request('Presign', 'https://yoink.zip/api/v1/presign', 'POST', 'application/json', presign_body))
    if not isinstance(signed, dict) or any(not isinstance(signed.get(key), str) or not signed[key]
                                         for key in ['uploadUrl', 'publicUrl', 'contentType']):
        raise RuntimeError('Presign returned missing or invalid fields')
    if not signed['uploadUrl'].startswith('https://') or not signed['publicUrl'].startswith('https://files.yoink.zip/'):
        raise RuntimeError('Presign returned invalid URLs')

    request('Upload', signed['uploadUrl'], 'PUT', signed['contentType'], data)
    print(signed['publicUrl'])


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