"""release_ops.py: ship a new version of the AI app without breaking it.

Passing CI (M26) means a build is *probably* fine, not that it is safe to send to everyone. Release
operations add the safety net: VERSION every release, CANARY it (run the candidate next to the live
baseline on a small eval set), and PROMOTE only if quality holds, otherwise ROLLBACK to the last-good
version. Plus secrets: rotate them with a GRACE WINDOW so a rotation never causes an outage.
Deterministic, offline; the "agents" are plain functions so the release machinery is the whole point.
"""


class ReleaseManager:
    """A tiny version registry with canary-gated promotion and one-call rollback."""

    def __init__(self):
        self.versions = {}     # name -> agent function (input -> answer)
        self.live = None       # the version currently serving traffic
        self.last_good = None  # the version to roll back to

    def register(self, name, fn):
        self.versions[name] = fn

    def deploy(self, name):
        """Set the first live version (no canary for the very first deploy)."""
        self.live = name
        self.last_good = name

    def _pass_rate(self, name, eval_set, scorer):
        if not eval_set:
            return 1.0
        fn = self.versions[name]
        return sum(1 for inp, expected in eval_set if scorer(fn(inp), expected)) / len(eval_set)

    def canary(self, candidate, eval_set, scorer, min_pass=0.8, regress_tol=0.0):
        """Score candidate vs the live baseline. Promote only if it clears the bar AND does not
        regress against what is already live. Returns the decision and both pass rates."""
        cand = self._pass_rate(candidate, eval_set, scorer)
        base = self._pass_rate(self.live, eval_set, scorer) if self.live else 0.0
        ok = cand >= min_pass and cand >= base - regress_tol
        return {"candidate": candidate, "cand_pass": round(cand, 2), "base_pass": round(base, 2),
                "decision": "promote" if ok else "rollback"}

    def release(self, candidate, eval_set, scorer, **kw):
        """Canary, then promote or stay. On promote, the old live becomes last_good (the rollback target)."""
        result = self.canary(candidate, eval_set, scorer, **kw)
        if result["decision"] == "promote":
            self.last_good = self.live
            self.live = candidate
        # on rollback the candidate never goes live; `live` is unchanged
        result["live_after"] = self.live
        return result

    def rollback(self):
        """Emergency: return to the last-good version."""
        if self.last_good:
            self.live = self.last_good
        return self.live


class SecretStore:
    """Versioned secrets with a rotation grace window.

    Rotating a key by hard-swapping it breaks every client still using the old one. Instead, keep the
    PREVIOUS secret valid for a grace window: new and old both work until in-flight clients have moved,
    then expire the old one. This is how you rotate credentials with zero downtime.
    """

    def __init__(self):
        self.current = None
        self.previous = None    # still accepted during the grace window

    def set(self, value):
        self.current = value
        self.previous = None

    def rotate(self, new_value):
        self.previous = self.current    # old key keeps working during grace
        self.current = new_value

    def is_valid(self, value, grace=True) -> bool:
        return value == self.current or (grace and self.previous is not None and value == self.previous)

    def expire_grace(self):
        """Close the grace window: only the newest secret is valid from now on."""
        self.previous = None
