export interface CostLayer {
  readonly id: string
  readonly quantityRemaining: number
  readonly unitCost: number
}

export interface ConsumeFifoResult {
  readonly cogs: number
  readonly updatedLayers: CostLayer[]
}

function assertNonNegativeInteger(value: number, field: string): void {
  if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {
    throw new RangeError(`${field} must be a non-negative integer`)
  }
}

function assertFiniteNonNegativeNumber(value: number, field: string): void {
  if (!Number.isFinite(value) || value < 0) {
    throw new RangeError(`${field} must be a finite non-negative number`)
  }
}

function assertLayer(layer: CostLayer, index: number): void {
  if (typeof layer.id !== 'string') {
    throw new TypeError(`layers[${index}].id must be a string`)
  }

  assertNonNegativeInteger(layer.quantityRemaining, `layers[${index}].quantityRemaining`)
  assertFiniteNonNegativeNumber(layer.unitCost, `layers[${index}].unitCost`)
}

export function consumeFifo(
  layers: readonly CostLayer[],
  qty: number,
): ConsumeFifoResult {
  assertNonNegativeInteger(qty, 'qty')

  let availableQuantity = 0
  for (const [index, layer] of layers.entries()) {
    assertLayer(layer, index)
    availableQuantity += layer.quantityRemaining
  }

  if (qty > availableQuantity) {
    throw new RangeError('consumeQuantity cannot exceed available quantity')
  }

  if (qty === 0) {
    return {
      cogs: 0,
      updatedLayers: layers.map((layer) => ({ ...layer })),
    }
  }

  let remainingToConsume = qty
  let cogs = 0

  const updatedLayers = layers.map((layer) => {
    if (remainingToConsume === 0 || layer.quantityRemaining === 0) {
      return { ...layer }
    }

    const consumedQuantity = Math.min(layer.quantityRemaining, remainingToConsume)
    remainingToConsume -= consumedQuantity
    cogs += consumedQuantity * layer.unitCost

    return {
      ...layer,
      quantityRemaining: layer.quantityRemaining - consumedQuantity,
    }
  })

  return { cogs, updatedLayers }
}
