///|
/// A new analysis at a later cutoff can reveal more processed lots and
/// shipments. Existing rows remain visible in the earlier report.
pub(all) struct ImpactDelta {
  recall_id : String
  previous_as_of : Int64
  current_as_of : Int64
  previous_lot_count : Int32
  current_lot_count : Int32
  previous_shipment_count : Int32
  current_shipment_count : Int32
  new_lots : Array[AffectedLot]
  new_shipments : Array[AffectedShipment]
  newly_shipped_g : Int64
} derive(Eq, Debug, ToJson, FromJson)

///|
/// A cutoff comparison is monotone for one immutable ledger. This function
/// never treats a previously affected shipment as resolved or safe.
pub fn compare_cutoffs(
  ledger : Ledger,
  request : RecallRequest,
  current_as_of : Int64,
) -> Result[ImpactDelta, ValidationReport] {
  if !valid_time(current_as_of) || current_as_of < request.as_of {
    return Err(
      report_from([
        issue(
          "error",
          "invalid-delta-cutoff",
          request.id,
          "new cutoff must be valid and no earlier than previous cutoff",
        ),
      ]),
    )
  }
  let previous = match analyze(ledger, request) {
    Err(validation) => return Err(validation)
    Ok(value) => value
  }
  let current_request = { ..request, as_of: current_as_of, }
  let current = match analyze(ledger, current_request) {
    Err(validation) => return Err(validation)
    Ok(value) => value
  }
  let old_lots : Map[String, Bool] = Map([])
  let old_shipments : Map[String, Bool] = Map([])
  for lot in previous.affected_lots {
    old_lots[lot.lot_id] = true
  }
  for shipment in previous.affected_shipments {
    old_shipments[shipment.shipment_id] = true
  }
  let new_lots : Array[AffectedLot] = []
  let new_shipments : Array[AffectedShipment] = []
  let mut newly_shipped_g = 0L
  for lot in current.affected_lots {
    if !old_lots.contains(lot.lot_id) {
      new_lots.push(lot)
    }
  }
  for shipment in current.affected_shipments {
    if !old_shipments.contains(shipment.shipment_id) {
      new_shipments.push(shipment)
      newly_shipped_g += shipment.quantity_g
    }
  }
  Ok({
    recall_id: request.id,
    previous_as_of: request.as_of,
    current_as_of,
    previous_lot_count: previous.affected_lots.length(),
    current_lot_count: current.affected_lots.length(),
    previous_shipment_count: previous.affected_shipments.length(),
    current_shipment_count: current.affected_shipments.length(),
    new_lots,
    new_shipments,
    newly_shipped_g,
  })
}

///|
pub fn render_delta_markdown(delta : ImpactDelta) -> String {
  let out = StringBuilder()
  out.write_string(
    "# New recall exposure: \{markdown_cell(delta.recall_id)}\n\n",
  )
  out.write_string(
    "Previous cutoff: \{delta.previous_as_of}; current cutoff: \{delta.current_as_of}.\n\n",
  )
  out.write_string(
    "This report lists newly discovered scope only. Previously affected records remain in scope.\n\n",
  )
  out.write_string("## New lots\n\n")
  out.write_string("| Lot | Product | Facility | Evidence path |\n")
  out.write_string("| --- | --- | --- | --- |\n")
  for lot in delta.new_lots {
    out.write_string(
      "| \{markdown_cell(lot.lot_id)} | \{markdown_cell(lot.product)} | \{markdown_cell(lot.facility)} | \{markdown_cell(path_text(lot.path))} |\n",
    )
  }
  out.write_string("\n## New shipments\n\n")
  out.write_string(
    "| Shipment | Consignee | Lot | Quantity (g) | Evidence path |\n",
  )
  out.write_string("| --- | --- | --- | ---: | --- |\n")
  for shipment in delta.new_shipments {
    out.write_string(
      "| \{markdown_cell(shipment.shipment_id)} | \{markdown_cell(shipment.consignee)} | \{markdown_cell(shipment.lot_id)} | \{shipment.quantity_g} | \{markdown_cell(path_text(shipment.path))} |\n",
    )
  }
  out.write_string("\nNewly shipped for review: \{delta.newly_shipped_g} g.\n")
  out.to_string()
}