"""ASGI copies preserve request streams, response bytes, and public contracts."""
from contextvars import ContextVar
import json
import re
import time
from uuid import uuid4
import urllib.request

REQUEST_ID = ContextVar('trace_request_id', default=None)
EVENT_ATTEMPT = ContextVar('trace_event_attempt', default=None)
PATHS = {'/api/v1/cameras/start', '/api/v1/cameras/stop',
         '/internal/secure-control/start', '/internal/secure-control/stop',
         '/api/v1/maintenance/roi-policy', '/internal/secure-control/roi-policy',
         '/internal/secure-control/training/settings'}


class Capture:
    def __init__(self, app, recorder, component):
        self.app, self.recorder, self.component = app, recorder, component

    async def __call__(self, scope, receive, send):
        if scope['type'] != 'http' or scope.get('path') not in PATHS or scope.get('method') not in ('POST','PATCH'):
            return await self.app(scope, receive, send)
        incoming = dict(scope.get('headers', [])).get(b'x-contextra-trace-id', b'').decode('ascii', 'ignore')
        request_id = incoming if self.component == 'runtime' and re.fullmatch('[0-9a-f]{32}', incoming) else uuid4().hex
        context = REQUEST_ID.set(request_id)
        request, response = bytearray(), bytearray()
        status, complete, failed = None, False, False
        begin, started = time.monotonic(), time.time_ns()//1000000

        def capture(target, chunk):
            if len(target) <= 65536:
                target.extend(chunk[:65537-len(target)])

        async def recv():
            msg = await receive()
            if msg['type'] == 'http.request':
                capture(request, msg.get('body', b''))
            return msg

        async def output(msg):
            nonlocal status, complete
            if msg['type'] == 'http.response.start':
                status = msg['status']
            elif msg['type'] == 'http.response.body':
                capture(response, msg.get('body', b''))
                complete = not msg.get('more_body', False)
            await send(msg)

        try:
            return await self.app(scope, recv, output)
        except BaseException:
            failed = True
            raise
        finally:
            try:
                camera = None
                try:
                    doc = json.loads(request)
                    value = doc.get('cameraId')
                    if type(value) in (str, int):
                        camera = str(value)[:128]
                except Exception:
                    pass
                result = {}
                try:
                    result = json.loads(response)
                    if not isinstance(result, dict):
                        result = {}
                except Exception:
                    pass
                self.recorder.emit(dict(direction='WEB_TO_AI', component=self.component, phase='HTTP',
                    requestId=request_id, cameraId=camera, path=scope['path'], method=scope['method'],
                    startedAtMs=started, finishedAtMs=time.time_ns()//1000000,
                    elapsedMs=round((time.monotonic()-begin)*1000, 3), httpStatus=status,
                    outcome='UNKNOWN' if failed or not complete else 'HTTP_RESPONSE',
                    error=result.get('reason') or result.get('error') or result.get('code'),
                    message=result.get('message'), operationId=result.get('operationId'),
                    requestBody=bytes(request), responseBody=bytes(response)))
            except Exception:
                pass
            finally:
                REQUEST_ID.reset(context)


def relay_sink(url, token):
    def send(batch):
        req = urllib.request.Request(url, json.dumps(batch).encode(),
            {'Authorization':'Bearer '+token, 'Content-Type':'application/json'})
        with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(req, timeout=2) as response:
            if response.status != 202:
                raise ValueError('TRACE_RELAY_UNAVAILABLE')
            result = json.loads(response.read(4096))
            if result.get('dropped'):
                raise ValueError('TRACE_REMOTE_QUEUE_DROPPED')
    return send
