"""parts.py: the Part E operations toolkit, assembled in one place for the capstone.

Each block is a COMPACT version of a primitive you built in M31-M33; the full, commented versions
live in those modules. Every block is labeled with its source so you can trace each line back.
Dependency-free, offline, deterministic.
"""
from dataclasses import dataclass, field

# =========================================================================================
# from M31 — incident response & on-call: define "healthy", alert on the burn, record, learn
# =========================================================================================
SEVERITY = {"sev1": "critical (page now)", "sev2": "major", "sev3": "minor (ticket)"}


@dataclass
class SLO:
    name: str
    objective: float = 0.99

    @property
    def error_budget(self):
        return 1.0 - self.objective


def sli(outcomes):
    return sum(1 for ok in outcomes if ok) / len(outcomes) if outcomes else 1.0


def burn_rate(slo, outcomes):
    allowed = slo.error_budget
    bad = 1.0 - sli(outcomes)
    return float("inf") if allowed == 0 and bad else (bad / allowed if allowed else 0.0)


def alert(slo, fast_window, slow_window):
    """Two-window burn-rate alert -> (action, severity, reason)."""
    fast, slow = burn_rate(slo, fast_window), burn_rate(slo, slow_window)
    if fast >= 14.4 and slow >= 1.0:
        return ("page", "sev1", f"fast burn {fast:.0f}x (budget gone in hours)")
    if slow >= 6.0:
        return ("page", "sev2", f"sustained burn {slow:.0f}x")
    if slow >= 1.0:
        return ("ticket", "sev3", f"slow burn {slow:.0f}x")
    return ("ok", None, f"within budget (burn {slow:.2f}x)")


@dataclass
class Incident:
    id: str
    severity: str
    title: str
    status: str = "open"
    cause: str = ""
    timeline: list = field(default_factory=list)
    mitigations: list = field(default_factory=list)

    def log(self, note):
        self.timeline.append(note)

    def mitigate(self, action):
        self.status = "mitigated"
        self.mitigations.append(action)
        self.log(f"MITIGATE: {action}")

    def resolve(self, cause):
        self.status = "resolved"
        self.cause = cause
        self.log(f"RESOLVED: {cause}")


def write_postmortem(inc):
    lines = [f"# Postmortem: {inc.title}", "",
             f"- Incident: {inc.id}  | Severity: {inc.severity}  | Status: {inc.status}",
             f"- Root cause: {inc.cause}", "", "## Timeline"]
    lines += [f"- {n}" for n in inc.timeline]
    lines += ["", "## Action items (blameless)",
              "- [ ] Keep the regression eval below in the gate (M26).",
              "- [ ] Require a canary on the next release (M33)."]
    return "\n".join(lines)


def to_eval_case(inc):
    return {"id": f"regression-{inc.id}", "origin": "incident",
            "scenario": inc.title, "root_cause": inc.cause,
            "expect": "service stays within SLO under this condition"}


# =========================================================================================
# from M32 — support desk & AIOps: triage a ticket, correlate an alert storm
# =========================================================================================
ROUTING = {"sev1": ("L2", 15), "sev2": ("L2", 60), "sev3": ("L1", 240)}
RULES = [("sev1", ["down", "outage", "cannot", "all users", "503"]),
         ("sev2", ["wrong answer", "error", "slow", "timeout", "since this morning"]),
         ("sev3", ["question", "how do i", "typo"])]


def triage(text):
    low = text.lower()
    best_sev, best_hits = "sev3", 0
    for sev, kws in RULES:
        hits = sum(1 for k in kws if k in low)
        if hits > best_hits:
            best_sev, best_hits = sev, hits
    confidence = min(1.0, 0.4 + 0.3 * best_hits) if best_hits else 0.2
    return {"severity": best_sev, "confidence": round(confidence, 2)}


def route(triaged):
    if triaged["confidence"] < 0.5:
        return {"tier": "human", "sla": None, "note": "low confidence -> human triage"}
    tier, sla = ROUTING[triaged["severity"]]
    return {"tier": tier, "sla": sla, "note": f"{triaged['severity']} -> {tier} (SLA {sla}m)"}


def correlate(alerts, window=5):
    """Group alerts by (service, symptom) within a time window -> incidents."""
    open_group, incidents = {}, []
    for a in sorted(alerts, key=lambda x: x["t"]):
        key = (a["service"], a["symptom"])
        g = open_group.get(key)
        if g and a["t"] - g["start"] <= window:
            g["alerts"].append(a)
        else:
            g = {"service": a["service"], "symptom": a["symptom"], "start": a["t"], "alerts": [a]}
            open_group[key] = g
            incidents.append(g)
    return incidents


# =========================================================================================
# from M33 — data & release ops: roll back a bad deploy, canary the fix, reindex stale data
# =========================================================================================
class ReleaseManager:
    def __init__(self):
        self.versions, self.live, self.last_good = {}, None, None

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

    def deploy(self, name):
        self.live = self.last_good = name

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

    def canary(self, candidate, eval_set, scorer, min_pass=0.8, regress_tol=0.0):
        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 "reject"}

    def release(self, candidate, eval_set, scorer, **kw):
        r = self.canary(candidate, eval_set, scorer, **kw)
        if r["decision"] == "promote":
            self.last_good, self.live = self.live, candidate
        r["live_after"] = self.live
        return r

    def rollback(self):
        if self.last_good:
            self.live = self.last_good
        return self.live
