#!/usr/bin/env python3
"""
The Pricing Guardrail — a price corridor per category, enforced at quote time. Part of "The Engine
Room" on simranjaiswal.in.

    python3 engine.py                                   # reference run, seed 42, writes results.json
    python3 engine.py --seed 7 --floor-mult 0.85 --block-mult 1.08 --drift-z 1.5 --deals-lost 0.2
    python3 engine.py --no-guardrail                    # everything approved: shows the leak instead

Every quote passes one gate: BLOCK below the margin floor, APPROVE inside the corridor, otherwise
NEEDS APPROVAL with a reason code (or an automatic decline and a re-price to the corridor floor).
A drift monitor watches the stream and flags any rep whose last ten discounts sit z above the
category-adjusted population.

Everything here is deterministic. The synthetic quote stream is generated from the printed rules with a
tiny 32-bit PRNG (mulberry32) that the in-browser engine on the case page implements bit-for-bit, so the
page's reference run reproduces this file's results.json exactly. No real quotes, reps or prices.

Stdlib only.
"""
import argparse, json, math, os

# ----------------------------------------------------------------------------- deterministic randomness
class RNG:
    """mulberry32 — identical to the JavaScript port on the case page."""
    M = 0xFFFFFFFF
    def __init__(self, seed): self.a = seed & self.M
    def random(self):
        self.a = (self.a + 0x6D2B79F5) & self.M
        t = self.a
        t = ((t ^ (t >> 15)) * (1 | t)) & self.M
        t = ((t + (((t ^ (t >> 7)) * (61 | t)) & self.M)) & self.M) ^ t
        return ((t ^ (t >> 14)) & self.M) / 4294967296
    def normal(self, mu=0.0, sd=1.0):
        u = self.random() or 1e-12; v = self.random()
        return mu + sd * math.sqrt(-2 * math.log(u)) * math.cos(6.283185307179586 * v)

def rnd(x): return int(math.floor(x + 0.5))                 # Math.round for x >= 0
def r4(x): return math.floor(x * 10000 + 0.5) / 10000       # shares and discounts to 4 dp
def r3(x): return math.floor(x * 1000 + 0.5) / 1000         # printed means to 3 dp
def r2(x): return math.floor(x * 100 + 0.5) / 100           # z-scores to 2 dp

N_QUOTES = 600
N_REPS = 12
N_DRIFT = 3                      # reps whose discount grows over the stream
WEEKS = 12                       # 600 quotes = 12 weeks of 50
CATEGORIES = [("DIAGNOSTIC", 0.62), ("CONSUMABLES", 0.71), ("FURNITURE", 0.58), ("MONITORING", 0.66), ("SURGICAL", 0.55)]
GRANT_P = {"VOLUME": 0.7, "COMPETITOR": 0.85, "STRATEGIC": 0.9}
SLA_P = 0.8                      # the approver answers within 4 h with this probability, else escalate
ROLL = 10                        # the drift monitor's window

# ----------------------------------------------------------------------------- the corridor
def corridor(floor_mult):
    """floor = ceil(cost / floor_mult * 100) / 100 as a share of list; target = floor + 0.10; ceiling = list."""
    out = []
    for name, cost in CATEGORIES:
        floor = math.ceil(cost / floor_mult * 100) / 100
        out.append({"name": name, "cost": cost, "floor": floor, "target": r2(floor + 0.10), "ceiling": 1.0})
    return out

# ----------------------------------------------------------------------------- the synthetic stream
def build_reps(rng):
    """12 reps with a habitual discount ~ N(0.14, 0.07) clipped 0–0.35. The three highest drift draws drift."""
    reps = []
    for r in range(N_REPS):
        habit = r4(min(0.35, max(0.0, rng.normal(0.14, 0.07))))
        score = rng.random()
        reps.append({"id": "R%02d" % (r + 1), "habit": habit, "score": score, "drift": False})
    order = sorted(range(N_REPS), key=lambda i: (-reps[i]["score"], i))
    for i in order[:N_DRIFT]: reps[i]["drift"] = True
    for rep in reps: del rep["score"]
    return reps

def draw_quote(rng, i, reps, rep_n):
    """One quote. Draw order: category, rep, qty (two draws), list price (two draws), noise (two draws), competitor, strategic."""
    cat = int(rng.random() * 5)
    rep = int(rng.random() * N_REPS)
    if rng.random() < 0.15: qty = 12 + int(rng.random() * 9)      # 12–20 with probability 0.15
    else:                   qty = 1 + int(rng.random() * 11)      # 1–11 otherwise
    lst = min(300000, max(2000, rnd(math.exp(rng.normal(9.5, 0.9)))))
    R = reps[rep]
    disc = R["habit"] + (0.004 * rep_n[rep] if R["drift"] else 0.0) + rng.normal(0, 0.05)
    share = r4(min(1.0, max(0.45, 1.0 - disc)))
    competitor = rng.random() < 0.18
    strategic = rng.random() < 0.10
    return {"q": i + 1, "cat": cat, "rep": rep, "qty": qty, "list": lst, "share": share, "competitor": competitor, "strategic": strategic}

# ----------------------------------------------------------------------------- the engine
def decide(rng, Q, C, block_mult, guardrail):
    """BLOCK below cost x block_mult; APPROVE at or above the floor; else NEEDS APPROVAL with a reason
    (VOLUME qty >= 12, COMPETITOR attached, STRATEGIC account) or an automatic decline. Draw order when
    a request is raised: SLA (answered in 4 h?) then the grant."""
    if not guardrail:
        Q["decision"] = "APPROVE"; Q["reason"] = ""; Q["outcome"] = ""; return
    if Q["share"] < C["cost"] * block_mult:
        Q["decision"] = "BLOCK"; Q["reason"] = "BELOW MARGIN FLOOR"; Q["outcome"] = "REPRICED"; return
    if Q["share"] >= C["floor"]:
        Q["decision"] = "APPROVE"; Q["reason"] = ""; Q["outcome"] = ""; return
    Q["decision"] = "NEEDS_APPROVAL"
    if Q["qty"] >= 12:      Q["reason"] = "VOLUME"
    elif Q["competitor"]:   Q["reason"] = "COMPETITOR"
    elif Q["strategic"]:    Q["reason"] = "STRATEGIC"
    else:
        Q["reason"] = "NO REASON"; Q["outcome"] = "AUTO_DECLINED"; return
    escalated = rng.random() >= SLA_P
    granted = rng.random() < GRANT_P[Q["reason"]]
    Q["outcome"] = ("ESCALATED_" if escalated else "") + ("GRANTED" if granted else "DECLINED")

def pop_sd(values, mean):
    s = 0.0
    for v in values: s += (v - mean) * (v - mean)
    return math.sqrt(s / len(values))

def run(seed=42, floor_mult=0.9, block_mult=1.03, drift_z=2.0, deals_lost=0.15, guardrail=True):
    params = {"seed": seed, "floor_mult": floor_mult, "block_mult": block_mult, "drift_z": drift_z, "deals_lost": deals_lost, "guardrail": bool(guardrail)}
    rng = RNG(seed)                          # the world: reps and the quote stream
    rng2 = RNG(seed + 1)                     # the approvers: SLA and grant draws (so every policy sees the same stream)
    cats = corridor(floor_mult)
    reps = build_reps(rng)
    rep_n = [0] * N_REPS                     # quotes seen per rep (drives the drift)
    stream = []
    for i in range(N_QUOTES):
        Q = draw_quote(rng, i, reps, rep_n); rep_n[Q["rep"]] += 1; stream.append(Q)
    rep_disc = [[] for _ in range(N_REPS)]   # every discount per rep, in order
    all_disc = []                            # every discount, in order
    cat_sum = [0.0] * 5; cat_n = [0] * 5
    flagged = {}                             # rep index -> catch record
    quotes = []; log = []; drift_log = []
    for i in range(N_QUOTES):
        Q = stream[i]
        C = cats[Q["cat"]]
        Q["floor"] = C["floor"]; Q["cost"] = C["cost"]
        Q["disc"] = r4(1.0 - Q["share"])
        Q["value"] = Q["list"] * Q["qty"]                                          # ₹ list value
        Q["leak"] = max(0.0, C["floor"] - Q["share"]) * Q["value"]                 # ₹ below the corridor floor
        Q["foregone"] = max(0.0, Q["share"] - C["cost"]) * Q["value"]              # margin the deal carried as quoted
        decide(rng2, Q, C, block_mult, guardrail)
        # ---- the drift monitor: z = (mean of the rep's last 10 discounts - category-adjusted population mean) / (population sd / sqrt(10))
        r = Q["rep"]; rep_disc[r].append(Q["disc"]); all_disc.append(Q["disc"]); cat_sum[Q["cat"]] += Q["disc"]; cat_n[Q["cat"]] += 1
        Q["flag"] = False
        if len(rep_disc[r]) >= ROLL and r not in flagged:
            last = [q for q in quotes if q["rep"] == r][-(ROLL - 1):] + [Q]
            m = 0.0
            for q in last: m += q["disc"]
            m = m / ROLL
            mu = 0.0
            for q in last: mu += cat_sum[q["cat"]] / cat_n[q["cat"]]
            mu = mu / ROLL
            pm = 0.0
            for v in all_disc: pm += v
            pm = pm / len(all_disc)
            sd = pop_sd(all_disc, pm)
            z = (m - mu) / (sd / math.sqrt(ROLL)) if sd > 0 else 0.0
            if z > drift_z:
                flagged[r] = {"rep": reps[r]["id"], "at_quote": Q["q"], "rep_quote_n": len(rep_disc[r]), "rolling_mean": r4(m), "population_mean": r4(mu), "population_sd": r4(sd), "z": r2(z), "drifting": reps[r]["drift"]}
                Q["flag"] = True
                drift_log.append("Q%03d · DRIFT · %s last-%d mean %.3f vs population %.3f · sd %.3f · z %.2f > %.1f → FLAGGED%s" % (Q["q"], reps[r]["id"], ROLL, r3(m), r3(mu), r3(sd), r2(z), drift_z, "" if reps[r]["drift"] else " (habitual, not drifting)"))
        quotes.append(Q)
        if i < 12:
            log.append("Q%03d · %s · %-11s · qty %2d · value %s · quoted %.2f · floor %.2f → %s%s" % (Q["q"], reps[Q["rep"]]["id"], C["name"], Q["qty"], fmt(Q["value"]), r2(Q["share"]), C["floor"], Q["decision"], (" · " + Q["reason"] + (" → " + Q["outcome"] if Q["outcome"] else "")) if Q["reason"] else ""))
    return summarise(params, cats, reps, quotes, flagged, log + drift_log)

def fmt(v):
    """₹ with Indian grouping (12,34,567) — the browser port formats the log the same way."""
    s = str(int(rnd(v)))
    if len(s) <= 3: return "₹" + s
    head, tail, groups = s[:-3], s[-3:], []
    while len(head) > 2: groups.insert(0, head[-2:]); head = head[:-2]
    if head: groups.insert(0, head)
    return "₹" + ",".join(groups) + "," + tail

def summarise(params, cats, reps, quotes, flagged, log):
    def count(fn): return sum(1 for q in quotes if fn(q))
    def value(fn): return int(rnd(sum(q["value"] for q in quotes if fn(q))))
    is_req = lambda q: q["decision"] == "NEEDS_APPROVAL" and q["reason"] != "NO REASON"
    approve = count(lambda q: q["decision"] == "APPROVE")
    granted = count(lambda q: q["outcome"] == "GRANTED")
    declined = count(lambda q: q["outcome"] == "DECLINED")
    auto = count(lambda q: q["outcome"] == "AUTO_DECLINED")
    esc_g = count(lambda q: q["outcome"] == "ESCALATED_GRANTED")
    esc_d = count(lambda q: q["outcome"] == "ESCALATED_DECLINED")
    block = count(lambda q: q["decision"] == "BLOCK")
    repriced = lambda q: q["outcome"] in ("DECLINED", "AUTO_DECLINED", "ESCALATED_DECLINED", "REPRICED")
    leaked = lambda q: q["decision"] == "APPROVE" or q["outcome"] in ("GRANTED", "ESCALATED_GRANTED")
    L = params["deals_lost"]
    gross = 0.0; foregone = 0.0; before = 0.0; after = 0.0
    for q in quotes:
        before += q["leak"]
        if repriced(q): gross += q["leak"]; foregone += q["foregone"]
        if leaked(q): after += q["leak"]
    net = gross * (1 - L) - foregone * L
    weeks = [0] * WEEKS
    for q in quotes:
        if is_req(q): weeks[(q["q"] - 1) // (N_QUOTES // WEEKS)] += 1
    by_cat = []
    for c, C in enumerate(cats):
        rows = [q for q in quotes if q["cat"] == c]
        by_cat.append({"category": C["name"], "cost": C["cost"], "floor": C["floor"], "target": C["target"], "quotes": len(rows),
                       "approve": sum(1 for q in rows if q["decision"] == "APPROVE"),
                       "granted": sum(1 for q in rows if q["outcome"] in ("GRANTED", "ESCALATED_GRANTED")),
                       "declined": sum(1 for q in rows if q["outcome"] in ("DECLINED", "AUTO_DECLINED", "ESCALATED_DECLINED")),
                       "block": sum(1 for q in rows if q["decision"] == "BLOCK"),
                       "list_value": int(rnd(sum(q["value"] for q in rows))), "leak_before": int(rnd(sum(q["leak"] for q in rows)))})
    by_rep = []
    for r, R in enumerate(reps):
        rows = [q for q in quotes if q["rep"] == r]
        m = 0.0
        for q in rows: m += q["disc"]
        last = rows[-ROLL:]
        ml = 0.0
        for q in last: ml += q["disc"]
        by_rep.append({"rep": R["id"], "habit": R["habit"], "drifting": R["drift"], "quotes": len(rows),
                       "mean_discount": r4(m / len(rows)) if rows else 0.0, "last10_mean": r4(ml / len(last)) if last else 0.0,
                       "leak_before": int(rnd(sum(q["leak"] for q in rows))), "leak_after": int(rnd(sum(q["leak"] for q in rows if leaked(q)))),
                       "blocked": sum(1 for q in rows if q["decision"] == "BLOCK"), "requests": sum(1 for q in rows if is_req(q)),
                       "flagged": r in flagged, "caught_at": flagged[r]["at_quote"] if r in flagged else None})
    catches = sorted(flagged.values(), key=lambda f: f["at_quote"])
    return {
        "engine": "pricing-guardrail", "seed": params["seed"], "params": params,
        "world": {"quotes": N_QUOTES, "reps": N_REPS, "drifting_reps": [R["id"] for R in reps if R["drift"]], "weeks": WEEKS, "corridor": cats,
                  "sla_hours": 4, "sla_p": SLA_P, "grant_p": GRANT_P, "rolling_window": ROLL,
                  "list_value": value(lambda q: True)},
        "decisions": {"approve": approve, "needs_approval": {"requests": granted + declined + esc_g + esc_d, "granted": granted, "declined": declined, "auto_declined": auto, "escalated": esc_g + esc_d, "escalated_granted": esc_g, "escalated_declined": esc_d},
                      "block": block, "repriced": declined + auto + esc_d + block, "total": approve + granted + declined + auto + esc_g + esc_d + block},
        "values": {"approve": value(lambda q: q["decision"] == "APPROVE"), "granted": value(lambda q: q["outcome"] in ("GRANTED", "ESCALATED_GRANTED")),
                   "declined": value(lambda q: q["outcome"] in ("DECLINED", "AUTO_DECLINED", "ESCALATED_DECLINED")), "block": value(lambda q: q["decision"] == "BLOCK")},
        "margin": {"leak_before": int(rnd(before)), "leak_after": int(rnd(after)), "protected_gross": int(rnd(gross)), "foregone_on_repriced": int(rnd(foregone)),
                   "deals_lost_assumed": L, "protected_net": int(rnd(net))},
        "approvals_per_week": weeks, "approvals_per_week_mean": r2(sum(weeks) / WEEKS),
        "drift": {"formula": "z = (mean of the rep's last 10 discounts - mean of the category means of those 10 quotes) / (population sd of all discounts so far / sqrt(10)); flag when z > drift_z",
                  "threshold": params["drift_z"], "flagged": catches, "flagged_count": len(catches), "true_positives": sum(1 for f in catches if f["drifting"]), "missed": [R["id"] for r, R in enumerate(reps) if R["drift"] and r not in flagged]},
        "by_category": by_cat, "by_rep": by_rep, "log": log,
    }

if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--seed", type=int, default=42)
    ap.add_argument("--floor-mult", type=float, default=0.9, help="floor = cost / floor_mult (0.85 stricter … 0.95 looser)")
    ap.add_argument("--block-mult", type=float, default=1.03, help="BLOCK when quoted share < cost x block_mult")
    ap.add_argument("--drift-z", type=float, default=2.0)
    ap.add_argument("--deals-lost", type=float, default=0.15, help="share of re-priced deals assumed lost")
    ap.add_argument("--no-guardrail", action="store_true", help="approve everything (shows the leak)")
    ap.add_argument("--out", default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "results.json"))
    a = ap.parse_args()
    res = run(a.seed, a.floor_mult, a.block_mult, a.drift_z, a.deals_lost, not a.no_guardrail)
    with open(a.out, "w") as f: json.dump(res, f, indent=1)
    d = res["decisions"]; n = d["needs_approval"]; m = res["margin"]
    print("stream: %d quotes · %d reps (drifting: %s) · list value %s" % (res["world"]["quotes"], res["world"]["reps"], ", ".join(res["world"]["drifting_reps"]), fmt(res["world"]["list_value"])))
    print("corridor: " + " · ".join("%s floor %.2f target %.2f" % (c["name"], c["floor"], c["target"]) for c in res["world"]["corridor"]))
    print("decisions: approve %d · requests %d (granted %d, declined %d, escalated %d → %d/%d) · auto-declined %d · block %d · re-priced %d · total %d" % (d["approve"], n["requests"], n["granted"], n["declined"], n["escalated"], n["escalated_granted"], n["escalated_declined"], n["auto_declined"], d["block"], d["repriced"], d["total"]))
    print("margin: leak before %s → after %s · protected gross %s · net of %.0f%% deals lost %s" % (fmt(m["leak_before"]), fmt(m["leak_after"]), fmt(m["protected_gross"]), 100 * m["deals_lost_assumed"], fmt(m["protected_net"])))
    print("approval load: %.2f requests / week · %s" % (res["approvals_per_week_mean"], res["approvals_per_week"]))
    for f in res["drift"]["flagged"]: print("  drift: %s caught at Q%03d (their #%d) · rolling %.3f vs pop %.3f · z %.2f%s" % (f["rep"], f["at_quote"], f["rep_quote_n"], f["rolling_mean"], f["population_mean"], f["z"], "" if f["drifting"] else " · habitual"))
    print("drift: %d flagged, %d true positives, missed %s" % (res["drift"]["flagged_count"], res["drift"]["true_positives"], res["drift"]["missed"] or "none"))
    print("wrote", a.out)
