import type {
  BatchBoardBookingDetail,
  BatchBoardScheduleDetail,
} from "@/types/trainScheduling";

/**
 * Client-side forecast of what the batch engine WOULD select if it ran right now.
 *
 * The real selection only happens once the document-review window closes and staff
 * hit "run batch". Before that, operations can only see the *current* per-booking
 * state (READY / SELECTED / …). This module simulates the engine's greedy fill so
 * the board can show the likely winners + waiting list live, during OPEN and
 * DOC_REVIEW, before anything is committed.
 *
 * It mirrors the engine (booking-batch.service): rank government-first, then
 * priority score desc, then oldest booked; greedily board each booking while it
 * fits ALL THREE capacity axes at once — wagon slots, max pull weight (tons), and
 * train length (metres). The first booking that busts any axis, and everyone after
 * it, drops to the waiting list. Purely a projection; the server stays the source
 * of truth for the real run.
 */

export interface ForecastLimits {
  /** Wagon-slot cap (schedule.maxWagons), or null if unknown. */
  maxWagons: number | null;
  /** Locomotive max pull weight in tons, or null. */
  maxWeightTons: number | null;
  /** Max train length in metres, or null. */
  maxLengthMeters: number | null;
}

/** Which capacity axis stopped a booking from boarding (for the "why not" hint). */
export type BlockingAxis = "wagons" | "weight" | "length";

export interface ForecastRow {
  booking: BatchBoardBookingDetail;
  /** 1-based rank across the whole eligible pool. */
  rank: number;
  /** True → boards in the simulated batch; false → simulated waiting list. */
  selected: boolean;
  /** Cumulative wagons/weight/length AFTER this booking (only when selected). */
  cumulativeWagons: number;
  cumulativeWeightTons: number;
  cumulativeLengthMeters: number;
  /** If not selected, the first axis that would have overflowed. */
  blockedBy: BlockingAxis | null;
}

export interface ForecastResult {
  rows: ForecastRow[];
  selected: ForecastRow[];
  waiting: ForecastRow[];
  /** Bookings excluded from the sim entirely (expired / no signed contract). */
  ineligible: BatchBoardBookingDetail[];
  limits: ForecastLimits;
  /** Totals of the simulated batch. */
  usedWagons: number;
  usedWeightTons: number;
  usedLengthMeters: number;
  /** True once any axis is at/over its cap — train is "full" in the sim. */
  full: boolean;
}

/** Engine rank order: government first, then priority desc, then oldest booked. */
export function rankBookings(
  bookings: BatchBoardBookingDetail[],
): BatchBoardBookingDetail[] {
  const time = (b: BatchBoardBookingDetail) =>
    b.fullyExecutedAt
      ? new Date(b.fullyExecutedAt).getTime()
      : Number.MAX_SAFE_INTEGER;
  return [...bookings].sort((a, b) => {
    if (a.isGovernment !== b.isGovernment) return a.isGovernment ? -1 : 1;
    if (b.priorityScore !== a.priorityScore)
      return b.priorityScore - a.priorityScore;
    return time(a) - time(b);
  });
}

/**
 * A booking can compete in the batch only once its contract is signed. Expired
 * bookings and pending-contract bookings never board, so they're pulled out of the
 * sim (surfaced separately so they don't vanish from the board).
 */
function isEligible(b: BatchBoardBookingDetail): boolean {
  return b.state !== "EXPIRED" && b.state !== "PENDING_CONTRACT";
}

const round2 = (n: number) => Math.round(n * 100) / 100;

/** Would adding `add` to `used` exceed `cap`? (cap null ⇒ axis unconstrained.) */
function overflows(used: number, add: number, cap: number | null): boolean {
  return cap != null && used + add > cap;
}

export function simulateBatch(
  bookings: BatchBoardBookingDetail[],
  limits: ForecastLimits,
): ForecastResult {
  const ranked = rankBookings(bookings);
  const eligible = ranked.filter(isEligible);
  const ineligible = ranked.filter((b) => !isEligible(b));

  const rows: ForecastRow[] = [];
  let wagons = 0;
  let weight = 0;
  let length = 0;
  // Once the train is full we stop boarding, but keep ranking the rest as waiting.
  let full = false;

  eligible.forEach((booking, i) => {
    let blockedBy: BlockingAxis | null = null;
    if (!full) {
      if (overflows(wagons, booking.wagons, limits.maxWagons))
        blockedBy = "wagons";
      else if (overflows(weight, booking.weightTons, limits.maxWeightTons))
        blockedBy = "weight";
      else if (overflows(length, booking.lengthMeters, limits.maxLengthMeters))
        blockedBy = "length";
    }
    // Strict fill: the first booking that doesn't fit closes the train, so lower-
    // priority bookings can't leapfrog it even if they'd individually fit. Matches
    // the engine's greedy pass.
    const selected = !full && blockedBy === null;
    if (selected) {
      wagons += booking.wagons;
      weight = round2(weight + booking.weightTons);
      length = round2(length + booking.lengthMeters);
    } else {
      full = true;
    }
    rows.push({
      booking,
      rank: i + 1,
      selected,
      cumulativeWagons: selected ? wagons : 0,
      cumulativeWeightTons: selected ? weight : 0,
      cumulativeLengthMeters: selected ? length : 0,
      blockedBy: selected ? null : (blockedBy ?? firstBindingAxis(limits)),
    });
  });

  return {
    rows,
    selected: rows.filter((r) => r.selected),
    waiting: rows.filter((r) => !r.selected),
    ineligible,
    limits,
    usedWagons: wagons,
    usedWeightTons: weight,
    usedLengthMeters: length,
    full,
  };
}

/** When the train closed on an earlier booking, name the tightest axis for the hint. */
function firstBindingAxis(limits: ForecastLimits): BlockingAxis {
  if (limits.maxWagons != null) return "wagons";
  if (limits.maxWeightTons != null) return "weight";
  return "length";
}

/** Pull the three capacity caps off the board detail response. */
export function limitsFromDetail(
  data: BatchBoardScheduleDetail,
): ForecastLimits {
  return {
    maxWagons: data.capacity.maxWagons ?? null,
    maxWeightTons:
      data.capacity.maxWeightTons ??
      data.locomotive?.maxPullWeightTons ??
      null,
    maxLengthMeters:
      data.capacity.maxLengthMeters ??
      data.locomotive?.maxTrainLengthMeters ??
      null,
  };
}

/**
 * The forecast is meaningful before the batch is committed — i.e. while bookings
 * are still being taken or reviewed. Once the engine has run (PAYMENT onward) the
 * real per-booking state is the truth, so we stop showing the projection.
 */
export function forecastIsLive(
  phase: BatchBoardScheduleDetail["windowPhase"],
): boolean {
  return phase === "PRE_WINDOW" || phase === "OPEN" || phase === "DOC_REVIEW";
}
