///|
pub(all) struct SourcePath {
  seed_lot : String
  path : Array[ExposureStep]
} derive(Debug, ToJson, FromJson)

///|
pub(all) struct AttributionRow {
  lot_id : String
  product : String
  sources : Array[SourcePath]
} derive(Debug, ToJson, FromJson)

///|
pub(all) struct ShipmentAttribution {
  shipment_id : String
  lot_id : String
  consignee : String
  sources : Array[SourcePath]
} derive(Debug, ToJson, FromJson)

///|
pub(all) struct AttributionReport {
  recall_id : String
  as_of : Int64
  lots : Array[AttributionRow]
  shipments : Array[ShipmentAttribution]
} derive(Debug, ToJson, FromJson)

///|
fn single_seed_paths(
  edges : Map[String, Array[ExposureStep]],
  seed : String,
) -> Map[String, Array[ExposureStep]] {
  let paths : Map[String, Array[ExposureStep]] = Map([])
  let queue : Array[String] = [seed]
  paths[seed] = []
  let mut head = 0
  while head < queue.length() {
    let current = queue[head]
    head += 1
    let base = paths.get(current).unwrap_or([])
    for edge in edges.get(current).unwrap_or([]) {
      if !paths.contains(edge.to_lot) {
        let path = base.copy()
        path.push(edge)
        paths[edge.to_lot] = path
        queue.push(edge.to_lot)
      }
    }
  }
  paths
}

///|
/// Show which suspect input lots can reach each downstream lot. Each seed
/// gets one shortest processing chain; an output may have several seed sources.
/// This prevents a single representative path from hiding another suspected
/// ingredient in the same product.
pub fn attribute_sources(
  ledger : Ledger,
  request : RecallRequest,
) -> Result[AttributionReport, ValidationReport] {
  let ledger_check = validate_ledger(ledger)
  let request_check = validate_request(ledger, request)
  let findings = ledger_check.diagnostics.copy()
  for diagnostic in request_check.diagnostics {
    findings.push(diagnostic)
  }
  if !ledger_check.valid || !request_check.valid {
    return Err(report_from(findings))
  }
  let edges = exposure_edges(ledger, request.as_of)
  let by_lot : Map[String, Array[SourcePath]] = Map([])
  for seed in request.seed_lots {
    let paths = single_seed_paths(edges, seed)
    for lot_id, path in paths {
      let sources = by_lot.get(lot_id).unwrap_or([])
      sources.push({ seed_lot: seed, path, })
      by_lot[lot_id] = sources
    }
  }
  let lots : Array[AttributionRow] = []
  for lot in ledger.lots {
    match by_lot.get(lot.id) {
      None => ()
      Some(sources) =>
        lots.push({ lot_id: lot.id, product: lot.product, sources, })
    }
  }
  let shipments : Array[ShipmentAttribution] = []
  for shipment in ledger.shipments {
    if shipment.shipped_at <= request.as_of {
      match by_lot.get(shipment.lot_id) {
        None => ()
        Some(sources) =>
          shipments.push({
            shipment_id: shipment.id,
            lot_id: shipment.lot_id,
            consignee: shipment.consignee,
            sources: sources.copy(),
          })
      }
    }
  }
  Ok({ recall_id: request.id, as_of: request.as_of, lots, shipments, })
}