///|
pub(all) struct InventoryRow {
  lot_id : String
  product : String
  facility : String
  produced_g : Int64
  processed_g : Int64
  shipped_g : Int64
  on_hand_g : Int64
} derive(Debug, ToJson, FromJson)

///|
pub(all) struct InventoryReport {
  as_of : Int64
  rows : Array[InventoryRow]
} derive(Debug, ToJson, FromJson)

///|
fn used_by_processing(ledger : Ledger, as_of : Int64) -> Map[String, Int64] {
  let consumed : Map[String, Int64] = Map([])
  for event in ledger.transformations {
    if event.occurred_at <= as_of {
      for portion in event.inputs {
        consumed[portion.lot_id] = consumed.get(portion.lot_id).unwrap_or(0L) +
          portion.quantity_g
      }
    }
  }
  consumed
}

///|
fn used_by_shipments(ledger : Ledger, as_of : Int64) -> Map[String, Int64] {
  let shipped : Map[String, Int64] = Map([])
  for shipment in ledger.shipments {
    if shipment.shipped_at <= as_of {
      shipped[shipment.lot_id] = shipped.get(shipment.lot_id).unwrap_or(0L) +
        shipment.quantity_g
    }
  }
  shipped
}

///|
/// Reconstruct in-scope on-hand stock from immutable lot and event records.
/// This is a ledger calculation, not a sensor or warehouse count. Differences
/// from physical stock must be reconciled by the operator before action.
pub fn inventory_at(
  ledger : Ledger,
  as_of : Int64,
) -> Result[InventoryReport, ValidationReport] {
  let validation = validate_ledger(ledger)
  if !validation.valid {
    return Err(validation)
  }
  if !valid_time(as_of) {
    return Err(
      report_from([
        issue(
          "error", "invalid-cutoff", "inventory", "as_of must be a valid Unix second",
        ),
      ]),
    )
  }
  let consumed = used_by_processing(ledger, as_of)
  let shipped = used_by_shipments(ledger, as_of)
  let rows : Array[InventoryRow] = []
  for lot in ledger.lots {
    if lot.produced_at <= as_of {
      let processed_g = consumed.get(lot.id).unwrap_or(0L)
      let shipped_g = shipped.get(lot.id).unwrap_or(0L)
      rows.push({
        lot_id: lot.id,
        product: lot.product,
        facility: lot.facility,
        produced_g: lot.quantity_g,
        processed_g,
        shipped_g,
        on_hand_g: lot.quantity_g - processed_g - shipped_g,
      })
    }
  }
  Ok({ as_of, rows, })
}

///|
pub fn InventoryReport::for_lot(
  self : InventoryReport,
  id : String,
) -> InventoryRow? {
  for row in self.rows {
    if row.lot_id == id {
      return Some(row)
    }
  }
  None
}