///|
/// A named operating scenario for repeatable screening.
pub(all) struct OperatingScenario {
  name : String
  reaction : Reaction
  feed : Feed
  volume : Double
  thermal_mode : ThermalMode
  exchange : HeatExchange?
} derive(Debug, ToJson)

///|
/// Construct an isothermal scenario.
pub fn OperatingScenario::isothermal(
  name~ : String,
  reaction~ : Reaction,
  feed~ : Feed,
  volume~ : Double,
) -> OperatingScenario {
  { name, reaction, feed, volume, thermal_mode: Isothermal, exchange: None }
}

///|
/// Construct an adiabatic scenario.
pub fn OperatingScenario::adiabatic(
  name~ : String,
  reaction~ : Reaction,
  feed~ : Feed,
  volume~ : Double,
) -> OperatingScenario {
  { name, reaction, feed, volume, thermal_mode: Adiabatic, exchange: None }
}

///|
/// Construct a jacketed scenario.
pub fn OperatingScenario::jacketed(
  name~ : String,
  reaction~ : Reaction,
  feed~ : Feed,
  volume~ : Double,
  exchange~ : HeatExchange,
) -> OperatingScenario {
  {
    name,
    reaction,
    feed,
    volume,
    thermal_mode: NonIsothermal,
    exchange: Some(exchange),
  }
}

///|
/// Evaluate a scenario with the declared thermal mode.
pub fn evaluate_scenario(scenario : OperatingScenario) -> DesignPoint {
  match scenario.exchange {
    Some(hx) =>
      design_pfr(
        scenario.reaction,
        scenario.feed,
        scenario.volume,
        thermal_mode=scenario.thermal_mode,
        exchange=hx,
      )
    None =>
      design_pfr(
        scenario.reaction,
        scenario.feed,
        scenario.volume,
        thermal_mode=scenario.thermal_mode,
      )
  }
}

///|
/// Evaluate several scenarios.
pub fn evaluate_scenarios(
  scenarios : ArrayView[OperatingScenario],
) -> Array[DesignPoint] {
  let result : Array[DesignPoint] = []
  for scenario in scenarios {
    result.push(evaluate_scenario(scenario))
  }
  result
}

///|
/// Find the hottest scenario.
pub fn hottest_scenario(
  scenarios : ArrayView[OperatingScenario],
) -> (String, Double)? {
  if scenarios.length() == 0 {
    None
  } else {
    let mut name = scenarios[0].name
    let mut temperature = evaluate_scenario(scenarios[0]).outlet_temperature
    for scenario in scenarios {
      let candidate = evaluate_scenario(scenario)
      if candidate.outlet_temperature > temperature {
        name = scenario.name
        temperature = candidate.outlet_temperature
      }
    }
    Some((name, temperature))
  }
}

///|
/// Find the highest-conversion scenario.
pub fn highest_conversion_scenario(
  scenarios : ArrayView[OperatingScenario],
) -> (String, Double)? {
  if scenarios.length() == 0 {
    None
  } else {
    let mut name = scenarios[0].name
    let mut conversion = evaluate_scenario(scenarios[0]).conversion
    for scenario in scenarios {
      let candidate = evaluate_scenario(scenario)
      if candidate.conversion > conversion {
        name = scenario.name
        conversion = candidate.conversion
      }
    }
    Some((name, conversion))
  }
}

///|
/// A temperature safety envelope for a scenario.
pub fn scenario_safety(
  scenario : OperatingScenario,
  maximum_temperature : Double,
) -> Bool {
  evaluate_scenario(scenario).outlet_temperature <= maximum_temperature
}

///|
/// A conversion target envelope for a scenario.
pub fn scenario_meets_conversion(
  scenario : OperatingScenario,
  minimum_conversion : Double,
) -> Bool {
  evaluate_scenario(scenario).conversion >= clamp_conversion(minimum_conversion)
}

///|
/// Evaluate a scenario across a volume grid.
pub fn scenario_volume_sweep(
  scenario : OperatingScenario,
  volumes : ArrayView[Double],
) -> Array[ReactorSweepPoint] {
  let result : Array[ReactorSweepPoint] = []
  for volume in volumes {
    let point = match scenario.exchange {
      Some(hx) =>
        design_pfr(
          scenario.reaction,
          scenario.feed,
          volume,
          thermal_mode=scenario.thermal_mode,
          exchange=hx,
        )
      None =>
        design_pfr(
          scenario.reaction,
          scenario.feed,
          volume,
          thermal_mode=scenario.thermal_mode,
        )
    }
    result.push({
      volume,
      conversion: point.conversion,
      outlet_temperature: point.outlet_temperature,
    })
  }
  result
}

///|
/// Compare two scenarios across conversion, temperature, and cost.
pub fn compare_scenarios(
  first : OperatingScenario,
  second : OperatingScenario,
  assumptions : CostAssumptions,
) -> Double {
  let first_point = evaluate_scenario(first)
  let second_point = evaluate_scenario(second)
  let first_cost = estimate_annual_cost(first_point, assumptions)
  let second_cost = estimate_annual_cost(second_point, assumptions)
  second_point.conversion -
  first_point.conversion -
  (second_cost.total - first_cost.total) / 1.0e6
}

///|
/// Apply a temperature offset to an operating scenario.
pub fn shift_scenario_temperature(
  scenario : OperatingScenario,
  offset : Double,
) -> OperatingScenario {
  {
    ..scenario,
    feed: { ..scenario.feed, temperature: scenario.feed.temperature + offset },
  }
}

///|
/// Apply a rate multiplier to an operating scenario.
pub fn scale_scenario_rate(
  scenario : OperatingScenario,
  multiplier : Double,
) -> OperatingScenario {
  {
    ..scenario,
    reaction: {
      ..scenario.reaction,
      k_ref: scenario.reaction.k_ref * multiplier.max(0.0),
    },
  }
}

///|
/// Apply a flow multiplier to an operating scenario.
pub fn scale_scenario_flow(
  scenario : OperatingScenario,
  multiplier : Double,
) -> OperatingScenario {
  {
    ..scenario,
    feed: {
      ..scenario.feed,
      volumetric_flow: scenario.feed.volumetric_flow * multiplier.max(0.0),
    },
  }
}

///|
/// Generate a Cartesian set of rate and temperature scenarios.
pub fn scenario_matrix(
  base : OperatingScenario,
  rate_multipliers : ArrayView[Double],
  temperature_offsets : ArrayView[Double],
) -> Array[OperatingScenario] {
  let result : Array[OperatingScenario] = []
  for rate in rate_multipliers {
    for offset in temperature_offsets {
      result.push(
        shift_scenario_temperature(scale_scenario_rate(base, rate), offset),
      )
    }
  }
  result
}

///|
/// Count scenarios that meet both safety and conversion targets.
pub fn feasible_scenario_count(
  scenarios : ArrayView[OperatingScenario],
  minimum_conversion : Double,
  maximum_temperature : Double,
) -> Int {
  scenarios.fold(init=0, fn(acc, scenario) {
    if scenario_meets_conversion(scenario, minimum_conversion) &&
      scenario_safety(scenario, maximum_temperature) {
      acc + 1
    } else {
      acc
    }
  })
}

///|
/// Return a scenario's normalized performance score.
pub fn scenario_score(
  scenario : OperatingScenario,
  target_conversion : Double,
  maximum_temperature : Double,
) -> Double {
  let point = evaluate_scenario(scenario)
  let penalty = if point.outlet_temperature <= maximum_temperature {
    0.0
  } else {
    1.0
  }
  point.conversion / clamp_conversion(target_conversion).max(1.0e-12) - penalty
}

///|
/// Select the highest-scoring scenario.
pub fn best_scenario(
  scenarios : ArrayView[OperatingScenario],
  target_conversion : Double,
  maximum_temperature : Double,
) -> OperatingScenario? {
  if scenarios.length() == 0 {
    None
  } else {
    let mut best = scenarios[0]
    let mut score = scenario_score(best, target_conversion, maximum_temperature)
    for scenario in scenarios {
      let candidate = scenario_score(
        scenario, target_conversion, maximum_temperature,
      )
      if candidate > score {
        best = scenario
        score = candidate
      }
    }
    Some(best)
  }
}

///|
/// Build a scenario comparison CSV.
pub fn scenarios_to_csv(scenarios : ArrayView[OperatingScenario]) -> String {
  let mut output = "name,volume,conversion,temperature,rate\n"
  for scenario in scenarios {
    let point = evaluate_scenario(scenario)
    output = output +
      scenario.name +
      "," +
      format_double(point.volume) +
      "," +
      format_double(point.conversion) +
      "," +
      format_double(point.outlet_temperature) +
      "," +
      format_double(point.rate_at_outlet) +
      "\n"
  }
  output
}