// The night's arrival shape: dead at 9, slammed from 10:30, tapering after 1:30,
// nobody left to turn away by 3. Pure data + maths — QueueManager consumes this
// and the Phaser scene only draws the result, so nothing here touches the engine.

export interface ArrivalCurvePoint {
  clockMin: number;
  perMin: number;
}

/**
 * Control points, linearly interpolated between. Tuned so a full night delivers
 * ~70 patrons (assert in tests via expectedTotalArrivals): enough that the rope
 * backs up during the slam, few enough that the queue is still clearable.
 */
export const ARRIVAL_CURVE: readonly ArrivalCurvePoint[] = [
  // Tuning pass 2026-07-19: the old peak (0.42/min) sat below even a careful
  // player's throughput, so the 10:30-12:30 "slam" was fiction — measured queue
  // avg 0.1-0.2. A night now delivers ~120 punters and the slam outruns you.
  { clockMin: 0, perMin: 0.12 }, // 9:00 PM — staff, and one bloke who thinks it's a pub
  { clockMin: 45, perMin: 0.2 }, // 9:45 PM
  { clockMin: 90, perMin: 0.55 }, // 10:30 PM — the pre-drinks let out
  { clockMin: 150, perMin: 0.72 }, // 11:30 PM — peak
  { clockMin: 210, perMin: 0.45 }, // 12:30 AM — slam ends
  { clockMin: 270, perMin: 0.2 }, // 1:30 AM
  { clockMin: 330, perMin: 0.07 }, // 2:30 AM — last drinks
  { clockMin: 360, perMin: 0 }, // 3:00 AM
];

/**
 * Longest gap one roll may produce. Held down deliberately: the 9 PM lull is
 * meant to read as quiet, not as a broken game, and an uncapped exponential draw
 * at the opening rate can hand the player a full minute of empty street.
 */
const MAX_GAP_MIN = 22;
/** Floor so two patrons never materialise on the same frame. */
const MIN_GAP_MIN = 0.25;
/** Nobody is coming. Finite on purpose: callers divide by and loop on this. */
const NO_ARRIVALS_GAP_MIN = 999;

/** Arrivals per in-game minute at `clockMin`, clamped flat outside the curve. */
export function arrivalsPerMin(clockMin: number): number {
  const first = ARRIVAL_CURVE[0];
  const last = ARRIVAL_CURVE[ARRIVAL_CURVE.length - 1];
  if (!first || !last) return 0;
  if (clockMin <= first.clockMin) return first.perMin;
  if (clockMin >= last.clockMin) return last.perMin;

  for (let i = 1; i < ARRIVAL_CURVE.length; i++) {
    const a = ARRIVAL_CURVE[i - 1]!;
    const b = ARRIVAL_CURVE[i]!;
    if (clockMin <= b.clockMin) {
      const span = b.clockMin - a.clockMin;
      const t = span === 0 ? 0 : (clockMin - a.clockMin) / span;
      return a.perMin + (b.perMin - a.perMin) * t;
    }
  }
  // Unreachable given the clamps above, but NaN in must not become NaN out.
  return last.perMin;
}

/**
 * In-game minutes until the next patron walks up. `roll` is a 0..1 uniform from
 * an RngStream; the exponential draw is what makes arrivals clump — three at once
 * then a lull — instead of ticking over like a metronome.
 */
export function nextArrivalGapMin(clockMin: number, roll: number, scale = 1): number {
  // `scale` is the venue's arrival multiplier (ladder, design §6): a dive runs
  // at 0.8x this curve, the rooftop above 1x. Guarded so a junk scale reads as 1.
  const s = Number.isFinite(scale) && scale > 0 ? scale : 1;
  const rate = arrivalsPerMin(clockMin) * s;
  if (!(rate > 0)) return NO_ARRIVALS_GAP_MIN;

  const safeRoll = Number.isFinite(roll) ? Math.min(Math.max(roll, 0), 0.999999) : 0;
  const gap = -Math.log(1 - safeRoll) / rate;
  return Math.min(MAX_GAP_MIN, Math.max(MIN_GAP_MIN, gap));
}

/** Integral of the curve over the whole night. Tuning knob, not gameplay. */
export function expectedTotalArrivals(): number {
  let total = 0;
  for (let i = 1; i < ARRIVAL_CURVE.length; i++) {
    const a = ARRIVAL_CURVE[i - 1]!;
    const b = ARRIVAL_CURVE[i]!;
    total += ((a.perMin + b.perMin) / 2) * (b.clockMin - a.clockMin);
  }
  return total;
}
