///|
/// Circuit-level metrics for leaching and washing campaigns.
pub(all) struct CircuitStage {
  stage : Int
  feed_solute : Double
  wash_solvent : Double
  extracted_solute : Double
  retained_solute : Double
  local_recovery : Double
  cumulative_recovery : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct CircuitMetrics {
  stages : Array[CircuitStage]
  feed_solute : Double
  recovered_solute : Double
  residual_solute : Double
  recovery : Double
  solvent_to_solute : Double
  diminishing_returns : Bool
} derive(Debug, Eq, ToJson)

///|
pub fn build_circuit_metrics(
  feed_solute : Double,
  wash_solvent : ArrayView[Double],
  stage_recoveries : ArrayView[Double],
) -> CircuitMetrics raise @domain.ExtractionError {
  if feed_solute < 0.0 ||
    wash_solvent.length() != stage_recoveries.length() ||
    wash_solvent.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="circuit arrays are invalid",
    )
  }
  let stages : Array[CircuitStage] = []
  let mut remaining = feed_solute
  let mut recovered = 0.0
  let mut solvent_total = 0.0
  let mut previous_transfer = 1.0e30
  let mut diminishing = true
  for index in 0.. 1.0 || wash_solvent[index] < 0.0 {
      raise @domain.ExtractionError::InvalidArgument(
        message="stage recovery or wash solvent is invalid",
      )
    }
    let extracted = remaining * recovery
    remaining -= extracted
    recovered += extracted
    solvent_total += wash_solvent[index]
    if extracted > previous_transfer {
      diminishing = false
    }
    previous_transfer = extracted
    stages.push({
      stage: index + 1,
      feed_solute: if index == 0 {
        feed_solute
      } else {
        stages[index - 1].retained_solute
      },
      wash_solvent: wash_solvent[index],
      extracted_solute: extracted,
      retained_solute: remaining,
      local_recovery: recovery,
      cumulative_recovery: if feed_solute == 0.0 {
        0.0
      } else {
        recovered / feed_solute
      },
    })
  }
  {
    stages,
    feed_solute,
    recovered_solute: recovered,
    residual_solute: remaining,
    recovery: if feed_solute == 0.0 {
      0.0
    } else {
      recovered / feed_solute
    },
    solvent_to_solute: if feed_solute == 0.0 {
      0.0
    } else {
      solvent_total / feed_solute
    },
    diminishing_returns: diminishing,
  }
}

///|
pub fn stage_count(metrics : CircuitMetrics) -> Int {
  metrics.stages.length()
}

///|
pub fn circuit_stage(
  metrics : CircuitMetrics,
  stage : Int,
) -> CircuitStage raise @domain.ExtractionError {
  if stage < 1 || stage > metrics.stages.length() {
    raise @domain.ExtractionError::OutOfRange(
      name="stage",
      value=stage.to_double(),
      lower=1.0,
      upper=metrics.stages.length().to_double(),
    )
  }
  metrics.stages[stage - 1]
}

///|
pub fn circuit_marginal_recoveries(metrics : CircuitMetrics) -> Array[Double] {
  [
    for stage in metrics.stages => stage.extracted_solute
  ]
}

///|
pub fn circuit_solvent_efficiency(metrics : CircuitMetrics) -> Double {
  if metrics.solvent_to_solute == 0.0 {
    metrics.recovery
  } else {
    metrics.recovery / metrics.solvent_to_solute
  }
}

///|
pub fn circuit_residual_fraction(metrics : CircuitMetrics) -> Double {
  if metrics.feed_solute == 0.0 {
    0.0
  } else {
    metrics.residual_solute / metrics.feed_solute
  }
}

///|
pub fn circuit_meets_target(metrics : CircuitMetrics, target : Double) -> Bool {
  metrics.recovery >= target
}

///|
pub fn minimum_stages_for_target(
  feed_solute : Double,
  wash_solvent : Double,
  per_stage_recovery : Double,
  target : Double,
  maximum_stages : Int,
) -> Int raise @domain.ExtractionError {
  if feed_solute < 0.0 ||
    wash_solvent < 0.0 ||
    per_stage_recovery <= 0.0 ||
    per_stage_recovery > 1.0 ||
    target < 0.0 ||
    target > 1.0 ||
    maximum_stages < 1 {
    raise @domain.ExtractionError::InvalidArgument(
      message="stage target inputs are invalid",
    )
  }
  let mut remaining = 1.0
  let mut stages = 0
  while stages < maximum_stages && 1.0 - remaining < target {
    stages += 1
    remaining *= 1.0 - per_stage_recovery
  }
  if 1.0 - remaining < target {
    raise @domain.ExtractionError::ConvergenceFailure(
      iterations=maximum_stages,
      residual=target - (1.0 - remaining),
    )
  }
  stages
}

///|
pub fn optimize_wash_allocation(
  feed_solute : Double,
  total_wash : Double,
  stage_count : Int,
  base_recovery : Double,
) -> Array[Double] raise @domain.ExtractionError {
  if total_wash < 0.0 ||
    stage_count < 1 ||
    base_recovery <= 0.0 ||
    base_recovery > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="wash allocation inputs are invalid",
    )
  }
  let allocations = Array::make(
    stage_count,
    total_wash / stage_count.to_double(),
  )
  let mut remaining = feed_solute
  for index in 0.. Array[String] {
  let alerts = []
  for stage in metrics.stages {
    if stage.local_recovery < minimum_stage_recovery {
      alerts.push("stage " + stage.stage.to_string() + " below recovery target")
    }
  }
  if metrics.residual_solute > metrics.feed_solute * 0.1 {
    alerts.push("residual solute exceeds ten percent of feed")
  }
  if !metrics.diminishing_returns {
    alerts.push("marginal recovery is not decreasing")
  }
  alerts
}

///|
pub fn circuit_markdown(metrics : CircuitMetrics) -> String {
  let lines = [
    "| Stage | Feed solute | Wash solvent | Extracted | Retained | Local recovery | Cumulative recovery |",
    "|---:|---:|---:|---:|---:|---:|---:|",
  ]
  for stage in metrics.stages {
    lines.push(
      "| " +
      stage.stage.to_string() +
      " | " +
      stage.feed_solute.to_string() +
      " | " +
      stage.wash_solvent.to_string() +
      " | " +
      stage.extracted_solute.to_string() +
      " | " +
      stage.retained_solute.to_string() +
      " | " +
      stage.local_recovery.to_string() +
      " | " +
      stage.cumulative_recovery.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn circuit_csv(metrics : CircuitMetrics) -> String {
  let lines = [
    "stage,feed_solute,wash_solvent,extracted_solute,retained_solute,local_recovery,cumulative_recovery",
  ]
  for stage in metrics.stages {
    lines.push(
      stage.stage.to_string() +
      "," +
      stage.feed_solute.to_string() +
      "," +
      stage.wash_solvent.to_string() +
      "," +
      stage.extracted_solute.to_string() +
      "," +
      stage.retained_solute.to_string() +
      "," +
      stage.local_recovery.to_string() +
      "," +
      stage.cumulative_recovery.to_string(),
    )
  }
  lines.join("\n")
}