"""Diagnostic-only FastAPI wrapper: await suspension, app time, GC and loop lag.

Set SKIRR_TAIL_OUTPUT to a JSON path. Never use this wrapper for throughput claims.
"""
import asyncio
import atexit
from collections import Counter
import gc
import json
import os
from pathlib import Path
import time
from apps import fastapi_app

histograms = {}
yields = Counter()
counts = Counter()
started = False
gc_started = {}
clock = time.perf_counter_ns


def record(name, ns):
    histogram = histograms.setdefault(name, Counter())
    histogram[min(ns // 1000, 1000000)] += 1


def dump():
    Path(os.environ['SKIRR_TAIL_OUTPUT']).write_text(json.dumps({
        'pid': os.getpid(), 'diagnostic_only': True, 'counts': counts,
        'await_yields': yields, 'microsecond_histograms': histograms,
    }))


def collection(phase, info):
    generation = info['generation']
    if phase == 'start':
        gc_started[generation] = clock()
    elif generation in gc_started:
        record(f'gc_gen{generation}', clock() - gc_started.pop(generation))


class Observed:
    def __init__(self, operation, kind):
        self.iterator = operation.__await__()
        self.kind = kind

    def __await__(self):
        return self

    def __iter__(self):
        return self

    def __next__(self):
        result = next(self.iterator)
        yields[self.kind] += 1
        return result

    def send(self, value):
        if value is None:
            return next(self)
        result = self.iterator.send(value)
        yields[self.kind] += 1
        return result

    def throw(self, error):
        method = getattr(self.iterator, 'throw', None)
        if method is None:
            raise error
        return method(error)


def initialize():
    gc.callbacks.append(collection)
    atexit.register(dump)
    loop = asyncio.get_running_loop()
    last_dump = clock()

    def heartbeat(due):
        nonlocal last_dump
        now = clock()
        record('loop_lag', max(0, now - due))
        if now - last_dump > 1000000000:
            dump()
            last_dump = now
        loop.call_later(.001, heartbeat, clock() + 1000000)

    loop.call_later(.001, heartbeat, clock() + 1000000)


async def app(scope, receive, send):
    global started
    if not started:
        initialize()
        started = True
    begin = clock()

    async def observed_send(message):
        kind = message['type']
        start = clock()
        try:
            return await Observed(send(message), kind)
        finally:
            record(kind, clock() - start)
            counts[kind] += 1

    try:
        await fastapi_app(scope, receive, observed_send)
    finally:
        record('app', clock() - begin)
        counts['requests'] += 1