"""
Practice: fallbacks and a circuit breaker. Lab 13's script 02 (02_fallbacks.py): 120 support emails
at 2 a second, gpt-5.4-mini broken from 15 s to 40 s, a backup model (gpt-5.4-nano) and a canned
reply, and a breaker in front of mini. Build the chain and the breaker, then work out why the
breaker tripped so late when mini hung, and check it against the recorded waits.

Fill in the TODOs, then run the tests:
    pytest test_breaker_practice.py        (locally)
or press "Run tests" on the chapter page.
"""


class CircuitOpen(Exception):
    """Raised instead of calling the service while the breaker is open."""


def fallback(options, canned=None):
    """Try each (name, fn) in `options` in order; the first fn() that returns wins.

    Return {"served_by": name, "result": fn(), "errors": [(name, error class name), ...]} with one
    entry in "errors" for each option that raised before it. If every option raises, return
    {"served_by": "canned", "result": canned, "errors": [...]} when `canned` is not None, and
    otherwise raise the last error."""
    # TODO
    return {"served_by": None, "result": None, "errors": []}


class CircuitBreaker:
    """closed:    calls go through. `failures` failures within `window` seconds → open.
    open:      allow() is False. After `cooldown` seconds → half_open.
    half_open: allow() is True once (a test call, the "probe"); then False until it's recorded.
               record(True) → closed; record(False) → open again, with a fresh cooldown.
    In any state, record(True) clears the failure count and closes the breaker.

    `clock` is a function returning the time in seconds. Every change of state is appended to
    `history` as (time, new state)."""

    def __init__(self, failures=5, window=10.0, cooldown=5.0, clock=None):
        self.failures, self.window, self.cooldown = failures, window, cooldown
        self.clock = clock
        self.state = "closed"
        self.history = []
        self._fails = []
        self._opened_at = 0.0
        self._probing = False

    def _set(self, state):
        if state != self.state:
            self.state = state
            self.history.append((self.clock(), state))

    def allow(self):
        # TODO
        return True

    def record(self, ok):
        # TODO
        pass

    def call(self, fn):
        """Raise CircuitOpen if allow() is False. Otherwise return fn(); record(True) if it
        returns, record(False) and re-raise if it raises."""
        # TODO
        return fn()


def seconds_per_failure(limit, retries, hangs=True):
    """How long one email takes to count as failed, on average. Each of the retries + 1 attempts
    waits `limit` seconds when the model hangs (0 when it errors at once). Before retry k
    (k = 0, 1, ...) the OpenAI SDK sleeps min(0.5 * 2**k, 8) * (1 - 0.25 * random()) seconds:
    use the average, 0.875 of min(0.5 * 2**k, 8)."""
    # TODO
    return 0.0


def trip_time(outage_start, per_failure, failures=5, rate=2.0):
    """When the breaker opens, in seconds: emails arrive `rate` a second from `outage_start`,
    and each is counted as failed `per_failure` seconds after it was sent. The breaker opens on
    the `failures`-th failure (assume they all land within its window)."""
    # TODO
    return 0.0


def typical_wait(seconds, start=15.0, end=40.0, rate=2.0):
    """The median wait of the emails sent during the outage. Email i was sent at i / rate seconds;
    keep those with start <= i / rate < end. The median of an even count is the mean of the two
    middle values."""
    # TODO
    return 0.0
