/**
 * Segment (leg) aware capacity accounting for corridor bookings.
 *
 * A train's route is an ordered list of stops; a booking occupies only the
 * edges between its own origin and destination. Capacity (wagons / weight /
 * length) is therefore tracked PER EDGE, not per train: two bookings whose
 * legs don't overlap (Addis→Dire and Dire→Djibouti) consume the same wagon
 * budget on disjoint edges and can share physical wagons.
 *
 * Legacy schedules without route milestones degrade to a single-edge corridor
 * ([origin, destination]) where this is exactly the old train-wide math.
 */

export interface Capacity {
  wagons: number;
  weightTons: number;
  lengthMeters: number;
}

/** Half-open edge span along the stop list: occupies edges [fromEdge, toEdge). */
export interface CorridorLeg {
  fromEdge: number;
  toEdge: number;
}

export function addCapacity(a: Capacity, b: Capacity): Capacity {
  return {
    wagons: a.wagons + b.wagons,
    weightTons: a.weightTons + b.weightTons,
    lengthMeters: a.lengthMeters + b.lengthMeters,
  };
}

export function subtractCapacity(a: Capacity, b: Capacity): Capacity {
  return {
    wagons: a.wagons - b.wagons,
    weightTons: a.weightTons - b.weightTons,
    lengthMeters: a.lengthMeters - b.lengthMeters,
  };
}

export function capacityFits(need: Capacity, budget: Capacity): boolean {
  return (
    need.wagons <= budget.wagons &&
    need.weightTons <= budget.weightTons &&
    need.lengthMeters <= budget.lengthMeters
  );
}

/**
 * Ordered stop yard ids for a schedule. Route milestones (already ordered by
 * sequence) when there are at least two; otherwise the schedule's own
 * origin/destination pair — the legacy two-stop pseudo-route.
 */
export function stopYardsFor(
  milestoneYardIdsInOrder: string[] | null | undefined,
  originStationId: string,
  destinationStationId: string,
): string[] {
  if (milestoneYardIdsInOrder && milestoneYardIdsInOrder.length >= 2) {
    return milestoneYardIdsInOrder;
  }
  return [originStationId, destinationStationId];
}

/** Overage a locomotive may absorb beyond its base caps. */
export interface OverageTolerance {
  weightTons: number;
  lengthMeters: number;
}

/**
 * Per-edge capacity budget along a schedule's stop list.
 *
 * `initial` must be the BASE caps (locomotive floored by rule caps, WITHOUT the
 * overage tolerance). The tolerance is passed separately and is spendable only
 * by admitting a unit WHOLE via {@link fits} — e.g. base 3500T + 90T tolerance,
 * 3560T already committed: a 25T booking still boards entire (3585 ≤ 3590), a
 * 210T booking does not. {@link remainingFor} deliberately excludes the
 * tolerance (and goes negative once it is consumed), so split/partial offers
 * sized from it can only fill up to the base cap and never spend the tolerance.
 */
export class CorridorBudget {
  private readonly edges: Capacity[];
  private readonly stopIndex: Map<string, number>;
  private readonly tolerance: OverageTolerance;

  constructor(
    readonly stops: string[],
    initial: Capacity,
    tolerance?: Partial<OverageTolerance> | null,
  ) {
    const edgeCount = Math.max(1, stops.length - 1);
    this.edges = Array.from({ length: edgeCount }, () => ({ ...initial }));
    this.stopIndex = new Map(stops.map((yardId, i) => [yardId, i]));
    this.tolerance = {
      weightTons: tolerance?.weightTons ?? 0,
      lengthMeters: tolerance?.lengthMeters ?? 0,
    };
  }

  /** The leg between two stops, or null when they aren't on this corridor in order. */
  legOf(originYardId: string, destinationYardId: string): CorridorLeg | null {
    const from = this.stopIndex.get(originYardId);
    const to = this.stopIndex.get(destinationYardId);
    if (from == null || to == null || from >= to) return null;
    return { fromEdge: from, toEdge: to };
  }

  /** Every edge — for whole-route consumers and unknown-leg fallbacks. */
  fullLeg(): CorridorLeg {
    return { fromEdge: 0, toEdge: this.edges.length };
  }

  /**
   * The leg a booking occupies; bookings whose yards aren't on the corridor
   * (legacy data drift) conservatively occupy the whole route so capacity is
   * never double-booked against them.
   */
  legForYards(originYardId: string, destinationYardId: string): CorridorLeg {
    return this.legOf(originYardId, destinationYardId) ?? this.fullLeg();
  }

  /**
   * Remaining BASE capacity usable by this leg = min across its edges. Excludes
   * the overage tolerance and goes negative once a whole-unit admission has
   * spent it — sizing a split from this can therefore never reach into the
   * tolerance, and yields nothing at all once the base cap is exhausted.
   */
  remainingFor(leg: CorridorLeg): Capacity {
    let min = { ...this.edges[leg.fromEdge] };
    for (let i = leg.fromEdge + 1; i < leg.toEdge; i++) {
      const e = this.edges[i];
      min = {
        wagons: Math.min(min.wagons, e.wagons),
        weightTons: Math.min(min.weightTons, e.weightTons),
        lengthMeters: Math.min(min.lengthMeters, e.lengthMeters),
      };
    }
    return min;
  }

  /**
   * Whether a unit fits WHOLE on this leg. This is the only place the overage
   * tolerance may be spent: the unit boards entirely or not at all, so weight
   * and length may dip into the tolerance. Admission keeps the invariant
   * `remaining ≥ -tolerance` on every edge, i.e. the train never exceeds
   * base + tolerance no matter how many small units board in the overage zone.
   */
  fits(need: Capacity, leg: CorridorLeg): boolean {
    const remaining = this.remainingFor(leg);
    return (
      need.wagons <= remaining.wagons &&
      need.weightTons <= remaining.weightTons + this.tolerance.weightTons &&
      need.lengthMeters <= remaining.lengthMeters + this.tolerance.lengthMeters
    );
  }

  subtract(need: Capacity, leg: CorridorLeg): void {
    for (let i = leg.fromEdge; i < leg.toEdge; i++) {
      this.edges[i] = subtractCapacity(this.edges[i], need);
    }
  }

  add(freed: Capacity, leg: CorridorLeg): void {
    for (let i = leg.fromEdge; i < leg.toEdge; i++) {
      this.edges[i] = addCapacity(this.edges[i], freed);
    }
  }

  /**
   * Train-wide FULL across ALL capacity axes: true when no edge can board even
   * one more loaded wagon. `perWagon` is the smallest gross weight and length
   * a future wagon could add (lightest wagon type at rated payload); weight and
   * length may dip into the overage tolerance, mirroring {@link fits}. Checked
   * per edge — an edge with slots free but no pull weight is just as closed as
   * one with no slots. A slot-only check misses weight-bound trains: PW2 at
   * 37 × 95.2T = 3522.4T of 3500+90T has 7 length-derived slots free but no
   * weight room for wagon 38, and its window must read FULL.
   */
  isExhausted(perWagon: { grossWeightTons: number; lengthMeters: number }): boolean {
    return this.edges.every(
      (e) =>
        e.wagons <= 0 ||
        e.weightTons + this.tolerance.weightTons < perWagon.grossWeightTons ||
        e.lengthMeters + this.tolerance.lengthMeters < perWagon.lengthMeters,
    );
  }

  /**
   * The most open edge — when even this has no wagon slots left, nothing can
   * board anywhere and the schedule's window is genuinely FULL. (A train can be
   * full on one leg while another still has room, so train-wide FULL keys on
   * the max, not the min.)
   */
  maxRemaining(): Capacity {
    return this.edges.reduce(
      (max, e) => ({
        wagons: Math.max(max.wagons, e.wagons),
        weightTons: Math.max(max.weightTons, e.weightTons),
        lengthMeters: Math.max(max.lengthMeters, e.lengthMeters),
      }),
      { ...this.edges[0] },
    );
  }
}
