"""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...). The normal
    customers' average time is scaled down so the OVERALL average checkout
    time stays at --service-mean; only the tail (a few extreme checkouts) changes
  - 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

Writes 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."""
    ts = []; t = 0.0
    while True:
        t += rng.expovariate(lam)
        if t >= T: break
        ts.append(t)
    if slow_share > 0 and slow_share < 1:
        normal_mean = (service_mean - slow_share * slow_mean) / (1 - slow_share)
        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()   # finished customers leave
            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; 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="average 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="average minutes for a 'slow' customer")
    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_minutes, 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 won mean wait in {r['shared_wins_mean']}/{r['n_streams']} streams, "
          f"slowest-10% in {r['shared_wins_slow10']}/{r['n_streams']}")
    return out

if __name__ == "__main__":
    main()
