
"""Queue experiment: one shared checkout line vs. three separate lines.

A tiny discrete-event simulator. Every run is PAIRED: the same customers
(same arrival times, same service times) are fed through both setups, so any
difference you see is caused by the line arrangement, not by luck of the draw.

Fictional shop:
  - 3 identical checkout counters
  - customers arrive at random (Poisson process)
  - checkout times are random (exponential); with --slow-share S, a fraction S
    of customers are "very slow" (price checks, card declines...), sized so the
    OVERALL average checkout time stays the same, only the tail gets worse
  - SHARED setup: one line; first free counter takes the head customer
  - SEPARATE setup: 3 lines; each arrival joins the shortest line (counting
    the person being served) and stays there -- no jockeying

Usage examples:
  python queue_sim.py                      # baseline (rho = 0.75)
  python queue_sim.py --rho 0.9            # busier store
  python queue_sim.py --slow-share 0.03    # sprinkle in very slow customers
  python queue_sim.py --streams 50 --seed 11

Outputs one JSON blob to --out (default results_summary.json).
"""
import argparse, json, math, random, statistics as st
from collections import deque

def gen_stream(T, lam, rng, service_mean=6.0, slow_share=0.0, slow_mean=70.0):
    """Poisson arrivals in [0, T); per-customer service times.

    With slow_share > 0, each customer is 'slow' with that probability and
    their mean checkout is scaled so the overall mean stays service_mean.
    """
    ts = []; t = 0.0
    while True:
        t += rng.expovariate(lam)
        if t >= T: break
        ts.append(t)
    if slow_share > 0:
        normal_mean = service_mean * (1 - slow_share)          # 2-guess: slow does the rest
        sv = []
        for _ in ts:
            if rng.random() < slow_share:
                sv.append(rng.expovariate(1.0 / slow_mean))
            else:
                sv.append(rng.expovariate(1.0 / normal_mean))
        return ts, sv
    return ts, [rng.expovariate(1.0 / service_mean) for _ in ts]

def sim_shared(arrivals, services, c=3):
    free = [0.0] * c
    waits = []
    for a, s in zip(arrivals, services):
        j = min(range(c), key=free.__getitem__)
        start = max(a, free[j])
        waits.append(start - a); free[j] = start + s
    return waits

def sim_separate(arrivals, services, c=3, tie_rng=None):
    lines = [deque() for _ in range(c)]
    waits = []
    for a, s in zip(arrivals, services):
        counts = []; heads = []
        for d in lines:
            while d and d[0] <= a: d.popleft()   # drop finished customers
            counts.append(len(d)); heads.append(d[0] if d else 0.0)
        m = min(counts); cand = [l for l in range(c) if counts[l] == m]
        if len(cand) > 1:
            h = min(heads[l] for l in cand)
            cand = [l for l in cand if heads[l] == h]
            l = tie_rng.choice(cand) if tie_rng is not None else cand[0]
        else:
            l = cand[0]
        d = lines[l]
        start = max(a, d[-1] if d else 0.0)
        waits.append(start - a); d.append(start + s)
    return waits

mean = lambda w: sum(w) / len(w)
def top_decile_mean(w):
    s = sorted(w); k = max(1, math.ceil(0.1 * len(s)))
    return mean(s[-k:])

def paired_stream(rho, service_mean, day_min, days, seed, slow_share, slow_mean,
                  n_counters=3, tie_seed=7):
    lam = rho * n_counters / service_mean
    rng = random.Random(seed)
    ar, sv = gen_stream(day_min * days, lam, rng, service_mean, slow_share, slow_mean)
    a = sim_shared(ar, sv)
    b = sim_separate(ar, sv, n_counters, random.Random(tie_seed))
    return a, b

def run_scenario(rho, n_streams, days_per_stream, seed0, slow_share, slow_mean,
                 service_mean=6.0, day_min=480.0, n_counters=3, tie_seed=7):
    """Averages over independent streams; also counts head-to-head wins."""
    S, S10, Sep, Sep10, wins, wins10 = [], [], [], [], 0, 0
    for i in range(n_streams):
        a, b = paired_stream(rho, service_mean, day_min, days_per_stream,
                             seed0 + i, slow_share, slow_mean, n_counters, tie_seed)
        ma, mb = mean(a), mean(b)
        S.append(ma); Sep.append(mb)
        S10.append(top_decile_mean(a)); Sep10.append(top_decile_mean(b))
        wins += ma < mb; wins10 += top_decile_mean(a) < top_decile_mean(b)
    se = lambda v: st.stdev(v) / math.sqrt(len(v)) if len(v) > 1 else 0.0
    return {"n_streams": n_streams, "rho": rho, "slow_share": slow_share,
            "shared_mean": st.mean(S), "shared_mean_se": se(S),
            "sep_mean": st.mean(Sep), "sep_mean_se": se(Sep),
            "shared_slow10": st.mean(S10), "shared_slow10_se": se(S10),
            "sep_slow10": st.mean(Sep10), "sep_slow10_se": se(Sep10),
            "shared_wins_mean": wins, "shared_wins_slow10": wins10}

def main(argv=None):
    p = argparse.ArgumentParser(description="Shared vs separate checkout lines")
    p.add_argument("--rho", type=float, default=0.75, help="how busy the store is (0..1)")
    p.add_argument("--counters", type=int, default=3)
    p.add_argument("--service-mean", type=float, default=6.0, help="avg checkout minutes")
    p.add_argument("--slow-share", type=float, default=0.0, help="fraction of very slow customers")
    p.add_argument("--slow-mean", type=float, default=70.0, help="avg minutes for a slow customer (used internally to keep overall mean)")
    p.add_argument("--streams", type=int, default=30, help="independent 8-day streams to average")
    p.add_argument("--day-minutes", type=float, default=480.0)
    p.add_argument("--days-per-stream", type=float, default=8.0)
    p.add_argument("--seed", type=int, default=31000)
    p.add_argument("--out", default="results_summary.json")
    args = p.parse_args(argv)
    res = run_scenario(args.rho, args.streams, args.days_per_stream, args.seed,
                       args.slow_share, args.slow_mean, args.service_mean,
                       args.day_min, args.counters)
    out = {"what": "shared line vs separate lines, paired simulation",
           "assumptions": [
               "Poisson arrivals; exponential checkout times (same draws in both setups)",
               "SHARED: one FIFO line; first free counter serves its head",
               "SEPARATE: join the shortest line (in-service customer counts) and stay put",
               "no balking/reneging/jockeying; identical counters; unlimited queue space",
           ],
           "config": vars(args),
           "results": res}
    with open(args.out, "w") as f:
        json.dump(out, f, indent=1)
    r = res
    print(f"rho={args.rho} counters={args.counters} mean_service={args.service_mean} "
          f"slow_share={args.slow_share} streams={args.streams}")
    print(f"  mean wait   shared {r['shared_mean']:.2f} ± {r['shared_mean_se']:.2f}   "
          f"separate {r['sep_mean']:.2f} ± {r['sep_mean_se']:.2f}")
    print(f"  slowest 10% shared {r['shared_slow10']:.2f} ± {r['shared_slow10_se']:.2f}   "
          f"separate {r['sep_slow10']:.2f} ± {r['sep_slow10_se']:.2f}")
    print(f"  shared line won on mean wait in {r['shared_wins_mean']}/{r['n_streams']} streams; "
          f"on slowest-10% in {r['shared_wins_slow10']}/{r['n_streams']}")
    return out

if __name__ == "__main__":
    main()
