// ═══════════════════════════════════════════════════════════════════════════
// audit-slot.rules.ts
// Pure scheduling rules for the audit-request time-slot system.
// NO database / Nest imports here, so it is trivially unit-testable.
// ═══════════════════════════════════════════════════════════════════════════

export interface SlotConfig {
  /** Audits that may START in the same time slot (2 = two auditors in parallel). */
  slotsPerTime: number;
  /** Minimum minutes between two audits that share the same "seat". 30 or 60. */
  gapMinutes: number;
  /** First / last allowed START time, minutes from midnight. */
  dayStartMin: number;
  lastStartMin: number;
  /** Start-time step in minutes. */
  stepMin: number;
  /** No audit may START inside [from, to). */
  lunch: { fromMin: number; toMin: number };
  /** Hard cap of audits per calendar day (existing business rule). */
  maxPerDay: number;
}

export const DEFAULT_SLOT_CONFIG: SlotConfig = {
  slotsPerTime: 2,
  gapMinutes: 60,
  dayStartMin: 9 * 60, // 09:00
  lastStartMin: 17 * 60 + 30, // 17:30  (working day ends 18:00)
  stepMin: 30,
  lunch: { fromMin: 13 * 60, toMin: 14 * 60 },
  maxPerDay: 8, 
};

export type SlotReason =
  | 'Closed'
  | 'Day full'
  | 'Fully booked'
  | 'Too close to another audit'
  | 'Time passed'
  | 'Outside booking window';

// ─── time helpers ──────────────────────────────────────────────────────────

/** "10:30" | "10:30:00" → 630.  Returns null when not a valid time. */
export function toMinutes(t: string | null | undefined): number | null {
  if (!t) return null;
  const m = /^(\d{1,2}):(\d{2})(?::\d{2})?$/.exec(String(t).trim());
  if (!m) return null;
  const h = +m[1];
  const mi = +m[2];
  if (h > 23 || mi > 59) return null;
  return h * 60 + mi;
}

/** 630 → "10:30" */
export function toHHMM(min: number): string {
  return `${String(Math.floor(min / 60)).padStart(2, '0')}:${String(min % 60).padStart(2, '0')}`;
}

/** 630 → "10:30 AM" */
export function toLabel(min: number): string {
  const h = Math.floor(min / 60);
  const m = min % 60;
  const p = h >= 12 ? 'PM' : 'AM';
  return `${h % 12 || 12}:${String(m).padStart(2, '0')} ${p}`;
}

/** Every allowed START time (lunch excluded), in minutes. */
export function buildStartTimes(cfg: SlotConfig = DEFAULT_SLOT_CONFIG): number[] {
  const out: number[] = [];
  for (let m = cfg.dayStartMin; m <= cfg.lastStartMin; m += cfg.stepMin) {
    if (m >= cfg.lunch.fromMin && m < cfg.lunch.toMin) continue;
    out.push(m);
  }
  return out;
}

export function isValidStartTime(min: number | null, cfg: SlotConfig = DEFAULT_SLOT_CONFIG): boolean {
  return min !== null && buildStartTimes(cfg).includes(min);
}

// ─── the capacity rule ─────────────────────────────────────────────────────

/**
 * How many audits can still START at `t`, given the audits already booked
 * (`booked`, minutes from midnight) on the same day?
 *
 * Model: there are `slotsPerTime` parallel "seats". One seat can host two
 * audits only if their start times are at least `gapMinutes` apart.
 * Seats are NOT bound to a named auditor – the coordinator assigns the real
 * auditor later – so we only need the *load*: the largest number of audits
 * that would overlap within a window of `gap` minutes that contains `t`.
 *
 *   free = slotsPerTime − (max audits already inside any window [s, s+gap) that contains t)
 *
 * Candidate window starts are the existing bookings in (t−gap, t] plus `t`
 * itself – a maximal window can always be slid right until its left edge
 * touches a booking, so those are the only ones worth checking.
 */
export function freeSeatsAt(
  booked: number[],
  t: number,
  cfg: SlotConfig = DEFAULT_SLOT_CONFIG,
): number {
  const gap = cfg.gapMinutes;
  const starts = new Set<number>([t]);
  for (const b of booked) if (b <= t && t - b < gap) starts.add(b);

  let worst = 0;
  for (const s of starts) {
    const load = booked.filter((b) => b >= s && b < s + gap).length;
    if (load > worst) worst = load;
  }
  return Math.max(0, cfg.slotsPerTime - worst);
}

// ─── slot grid ─────────────────────────────────────────────────────────────

export interface SlotCell {
  time: string; // "10:30"
  label: string; // "10:30 AM"
  total: number; // seats per time (2)
  free: number; // seats still open (0..2)
  status: 'open' | 'limited' | 'closed';
  reason?: SlotReason;
}

export interface SessionBlock {
  key: 'morning' | 'evening';
  label: string;
  from: string;
  to: string;
  slots: SlotCell[];
}

export interface BuildGridInput {
  /** Start times (minutes) of audits already holding capacity that day. */
  booked: number[];
  /** Total audits that day, including ones with no time. Used for the day cap. */
  dayCount: number;
  dateClosed: boolean;
  outsideWindow: boolean;
  /** Minutes before which slots are "passed" (only set for today). */
  notBeforeMin: number | null;
  cfg?: SlotConfig;
}

export function buildSlotGrid(input: BuildGridInput): SessionBlock[] {
  const cfg = input.cfg ?? DEFAULT_SLOT_CONFIG;
  const dayFull = input.dayCount >= cfg.maxPerDay;

  const cell = (t: number): SlotCell => {
    const base = { time: toHHMM(t), label: toLabel(t), total: cfg.slotsPerTime };
    const closed = (reason: SlotReason): SlotCell => ({ ...base, free: 0, status: 'closed', reason });

    if (input.dateClosed) return closed('Closed');
    if (input.outsideWindow) return closed('Outside booking window');
    if (input.notBeforeMin !== null && t <= input.notBeforeMin) return closed('Time passed');
    if (dayFull) return closed('Day full');

    const free = freeSeatsAt(input.booked, t, cfg);
    if (free === 0) {
      const exact = input.booked.filter((b) => b === t).length;
      return closed(exact >= cfg.slotsPerTime ? 'Fully booked' : 'Too close to another audit');
    }
    return { ...base, free, status: free === cfg.slotsPerTime ? 'open' : 'limited' };
  };

  const starts = buildStartTimes(cfg);
  const morning = starts.filter((m) => m < cfg.lunch.fromMin);
  const evening = starts.filter((m) => m >= cfg.lunch.toMin);
  return [
    {
      key: 'morning',
      label: 'Morning',
      from: toLabel(morning[0]),
      to: toLabel(morning[morning.length - 1]),
      slots: morning.map(cell),
    },
    {
      key: 'evening',
      label: 'Evening',
      from: toLabel(evening[0]),
      to: toLabel(evening[evening.length - 1]),
      slots: evening.map(cell),
    },
  ];
}

/** Open times closest to `t`, for "try these instead" suggestions. */
export function nearestOpen(sessions: SessionBlock[], t: string, n = 3): string[] {
  const target = toMinutes(t) ?? 0;
  return sessions
    .flatMap((s) => s.slots)
    .filter((c) => c.free > 0 && c.time !== t)
    .sort((a, b) => Math.abs(toMinutes(a.time)! - target) - Math.abs(toMinutes(b.time)! - target))
    .slice(0, n)
    .map((c) => c.time)
    .sort();
}
