"""
Solution: batching and serving under load, measured. Lab 12's numbers for one model serving many people
(09_batching.py and serving.py): tokens a second in a batch, the typical (median) wait in a load test, a
queue served one at a time, and when the static-batching "bus" leaves.
"""


def throughput(tokens, wall_s, batch):
    """A batch of `batch` replies wrote `tokens` tokens in `wall_s` seconds, all starting together.
    Return (tokens a second in total, tokens a second for each reply), both rounded to whole numbers."""
    total = tokens / wall_s
    return round(total), round(total / batch)


def p50(values):
    """The median the lab reports: nearest-rank, so always one of the values (the lower middle one
    when the count is even)."""
    v = sorted(values)
    return v[max(1, round(0.5 * len(v))) - 1]


def summarise(requests):
    """Each request is (arrival, start, first_word, done), in seconds. Return a dict of:
    - "first": typical (p50) time from arrival to the first word,
    - "whole": typical time from arrival to the last word,
    - "write": typical time from the first word to the last (writing the reply),
    - "maxwait": the longest time from arrival to the first word."""
    return {
        "first": p50([f - a for a, s, f, d in requests]),
        "whole": p50([d - a for a, s, f, d in requests]),
        "write": p50([d - f for a, s, f, d in requests]),
        "maxwait": max(f - a for a, s, f, d in requests),
    }


def one_at_a_time(arrivals, durations):
    """A server that runs one request at a time, in arrival order. Request i arrives at arrivals[i]
    and takes durations[i] seconds once it starts. Return a list of (start, done) per request."""
    out, free = [], 0.0
    for a, d in zip(arrivals, durations):
        start = max(a, free)
        free = start + d
        out.append((start, free))
    return out


def static_batch(waiting, now, size=8, max_wait=0.5):
    """Static batching, as the lab's scheduler. The server is free from `now`. `waiting` holds the
    sorted arrival times of every request not yet started (some may arrive after `now`). The bus
    leaves at the first moment t >= now when any of these holds:
    - `size` requests have arrived,
    - the oldest request has waited `max_wait` seconds,
    - every request has arrived (nobody else is coming).
    Return (t, how many board): those that have arrived by t, at most `size`."""
    times = [max(now, waiting[0] + max_wait), max(now, waiting[-1])]
    if len(waiting) >= size:
        times.append(max(now, waiting[size - 1]))
    t = min(times)
    return t, min(size, sum(1 for a in waiting if a <= t))
