///|
/// Preliminary utility and capacity sizing helpers for early process design.
///
/// These calculations intentionally expose assumptions and margins instead of
/// hiding them in a single black-box equipment factor.
pub(all) struct UtilityLoad {
  name : String
  design_load : Double
  operating_load : Double
  margin : Double
  unit : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct CapacityCheck {
  name : String
  required : Double
  available : Double
  utilization : Double
  margin : Double
  feasible : Bool
} derive(Debug, Eq, ToJson)

///|
pub(all) struct StorageSizing {
  working_volume : Double
  surge_volume : Double
  total_volume : Double
  diameter : Double
  height : Double
  aspect_ratio : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) enum UtilityKind {
  CoolingWater
  ChilledWater
  Steam
  Electricity
  CompressedAir
  Nitrogen
} derive(Debug, Eq)

///|
pub fn utility_name(kind : UtilityKind) -> String {
  match kind {
    CoolingWater => "cooling-water"
    ChilledWater => "chilled-water"
    Steam => "steam"
    Electricity => "electricity"
    CompressedAir => "compressed-air"
    Nitrogen => "nitrogen"
  }
}

///|
pub fn sensible_heat_duty(
  mass_flow : Double,
  heat_capacity : Double,
  inlet_temperature : Double,
  outlet_temperature : Double,
) -> Double raise @domain.ExtractionError {
  if mass_flow < 0.0 {
    raise @domain.ExtractionError::InvalidFlow(
      name="mass flow",
      value=mass_flow,
    )
  }
  if heat_capacity < 0.0 {
    raise @domain.ExtractionError::InvalidCoefficient(
      name="heat capacity",
      value=heat_capacity,
    )
  }
  mass_flow * heat_capacity * (outlet_temperature - inlet_temperature)
}

///|
pub fn latent_heat_duty(
  mass_flow : Double,
  latent_heat : Double,
) -> Double raise @domain.ExtractionError {
  if mass_flow < 0.0 {
    raise @domain.ExtractionError::InvalidFlow(
      name="mass flow",
      value=mass_flow,
    )
  }
  if latent_heat < 0.0 {
    raise @domain.ExtractionError::InvalidCoefficient(
      name="latent heat",
      value=latent_heat,
    )
  }
  mass_flow * latent_heat
}

///|
pub fn cooling_water_flow(
  duty : Double,
  inlet_temperature : Double,
  outlet_temperature : Double,
  heat_capacity? : Double = 4.18,
) -> Double raise @domain.ExtractionError {
  let rise = outlet_temperature - inlet_temperature
  if duty < 0.0 || heat_capacity <= 0.0 || rise <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="cooling-water duty, heat capacity, and temperature rise must be positive",
    )
  }
  duty / (heat_capacity * rise)
}

///|
pub fn chilled_water_flow(
  duty : Double,
  inlet_temperature : Double,
  outlet_temperature : Double,
  heat_capacity? : Double = 4.18,
) -> Double raise @domain.ExtractionError {
  cooling_water_flow(
    duty,
    inlet_temperature,
    outlet_temperature,
    heat_capacity~,
  )
}

///|
pub fn steam_flow(
  duty : Double,
  latent_heat : Double,
  condensate_subcooling? : Double = 0.0,
  heat_capacity? : Double = 4.18,
) -> Double raise @domain.ExtractionError {
  if duty < 0.0 || latent_heat <= 0.0 || condensate_subcooling < 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="steam duty and latent heat are invalid",
    )
  }
  let usable = latent_heat + heat_capacity * condensate_subcooling
  duty / usable
}

///|
pub fn pump_power(
  volumetric_flow : Double,
  pressure_rise : Double,
  efficiency : Double,
) -> Double raise @domain.ExtractionError {
  if volumetric_flow < 0.0 {
    raise @domain.ExtractionError::InvalidFlow(
      name="volumetric flow",
      value=volumetric_flow,
    )
  }
  if pressure_rise < 0.0 {
    raise @domain.ExtractionError::InvalidCoefficient(
      name="pressure rise",
      value=pressure_rise,
    )
  }
  if efficiency <= 0.0 || efficiency > 1.0 {
    raise @domain.ExtractionError::InvalidFraction(
      name="pump efficiency",
      value=efficiency,
    )
  }
  volumetric_flow * pressure_rise / efficiency
}

///|
pub fn fan_power(
  volumetric_flow : Double,
  pressure_rise : Double,
  efficiency : Double,
) -> Double raise @domain.ExtractionError {
  pump_power(volumetric_flow, pressure_rise, efficiency)
}

///|
pub fn compressor_power(
  molar_flow : Double,
  inlet_temperature : Double,
  pressure_ratio : Double,
  heat_capacity_ratio : Double,
  efficiency : Double,
) -> Double raise @domain.ExtractionError {
  if molar_flow < 0.0 || inlet_temperature <= 0.0 || pressure_ratio < 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="compressor input is invalid",
    )
  }
  if heat_capacity_ratio <= 1.0 {
    raise @domain.ExtractionError::InvalidCoefficient(
      name="heat capacity ratio",
      value=heat_capacity_ratio,
    )
  }
  if efficiency <= 0.0 || efficiency > 1.0 {
    raise @domain.ExtractionError::InvalidFraction(
      name="compressor efficiency",
      value=efficiency,
    )
  }
  let exponent = (heat_capacity_ratio - 1.0) / heat_capacity_ratio
  molar_flow *
  inlet_temperature *
  exponent *
  (@math.pow(pressure_ratio, exponent) - 1.0) /
  efficiency
}

///|
pub fn design_load(
  operating_load : Double,
  design_margin? : Double = 0.15,
) -> Double raise @domain.ExtractionError {
  if operating_load < 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="operating load cannot be negative",
    )
  }
  if design_margin < 0.0 || design_margin > 2.0 {
    raise @domain.ExtractionError::OutOfRange(
      name="design margin",
      value=design_margin,
      lower=0.0,
      upper=2.0,
    )
  }
  operating_load * (1.0 + design_margin)
}

///|
pub fn make_utility_load(
  name : String,
  operating_load : Double,
  margin? : Double = 0.15,
  unit? : String = "kW",
) -> UtilityLoad raise @domain.ExtractionError {
  let load = design_load(operating_load, design_margin=margin)
  { name, design_load: load, operating_load, margin, unit }
}

///|
pub fn check_capacity(
  name : String,
  required : Double,
  available : Double,
  design_margin? : Double = 0.0,
) -> CapacityCheck raise @domain.ExtractionError {
  if required < 0.0 || available < 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="capacity cannot be negative",
    )
  }
  let adjusted = design_load(required, design_margin~)
  let utilization = if available == 0.0 { 0.0 } else { adjusted / available }
  {
    name,
    required: adjusted,
    available,
    utilization,
    margin: available - adjusted,
    feasible: adjusted <= available,
  }
}

///|
pub fn storage_volume(
  flow_rate : Double,
  residence_time : Double,
  surge_fraction? : Double = 0.20,
) -> Double raise @domain.ExtractionError {
  if flow_rate < 0.0 || residence_time < 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="flow and residence time are invalid",
    )
  }
  if surge_fraction < 0.0 || surge_fraction > 5.0 {
    raise @domain.ExtractionError::InvalidFraction(
      name="surge fraction",
      value=surge_fraction,
    )
  }
  flow_rate * residence_time * (1.0 + surge_fraction)
}

///|
pub fn size_vertical_tank(
  working_volume : Double,
  surge_fraction? : Double = 0.20,
  aspect_ratio? : Double = 2.5,
) -> StorageSizing raise @domain.ExtractionError {
  if working_volume <= 0.0 || aspect_ratio <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="tank dimensions are invalid",
    )
  }
  let total = working_volume * (1.0 + surge_fraction)
  let radius = @math.pow(total / (aspect_ratio * 3.141592653589793), 1.0 / 3.0)
  let diameter = 2.0 * radius
  let height = aspect_ratio * diameter
  {
    working_volume,
    surge_volume: total - working_volume,
    total_volume: total,
    diameter,
    height,
    aspect_ratio,
  }
}

///|
pub fn size_horizontal_tank(
  working_volume : Double,
  surge_fraction? : Double = 0.20,
  length_to_diameter? : Double = 3.0,
) -> StorageSizing raise @domain.ExtractionError {
  if working_volume <= 0.0 || length_to_diameter <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="tank dimensions are invalid",
    )
  }
  let total = working_volume * (1.0 + surge_fraction)
  let diameter = @math.pow(
    4.0 * total / (3.141592653589793 * length_to_diameter),
    1.0 / 3.0,
  )
  let length = length_to_diameter * diameter
  {
    working_volume,
    surge_volume: total - working_volume,
    total_volume: total,
    diameter,
    height: length,
    aspect_ratio: length_to_diameter,
  }
}

///|
pub fn pipe_area(diameter : Double) -> Double raise @domain.ExtractionError {
  if diameter <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="pipe diameter must be positive",
    )
  }
  3.141592653589793 * diameter * diameter / 4.0
}

///|
pub fn pipe_velocity(
  volumetric_flow : Double,
  diameter : Double,
) -> Double raise @domain.ExtractionError {
  if volumetric_flow < 0.0 {
    raise @domain.ExtractionError::InvalidFlow(
      name="volumetric flow",
      value=volumetric_flow,
    )
  }
  volumetric_flow / pipe_area(diameter)
}

///|
pub fn pipe_diameter_for_velocity(
  volumetric_flow : Double,
  target_velocity : Double,
) -> Double raise @domain.ExtractionError {
  if volumetric_flow < 0.0 || target_velocity <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="flow and velocity are invalid",
    )
  }
  @math.pow(4.0 * volumetric_flow / (3.141592653589793 * target_velocity), 0.5)
}

///|
pub fn pipe_pressure_drop(
  length : Double,
  diameter : Double,
  density : Double,
  velocity : Double,
  friction_factor : Double,
) -> Double raise @domain.ExtractionError {
  if length < 0.0 || density < 0.0 || velocity < 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="pipe pressure inputs are invalid",
    )
  }
  if friction_factor < 0.0 {
    raise @domain.ExtractionError::InvalidCoefficient(
      name="friction factor",
      value=friction_factor,
    )
  }
  friction_factor * length / diameter * density * velocity * velocity / 2.0
}

///|
pub fn parallel_capacity(
  capacities : ArrayView[Double],
) -> Double raise @domain.ExtractionError {
  if capacities.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let mut total = 0.0
  for capacity in capacities {
    if capacity < 0.0 {
      raise @domain.ExtractionError::InvalidArgument(
        message="capacity cannot be negative",
      )
    }
    total += capacity
  }
  total
}

///|
pub fn series_resistance(
  resistances : ArrayView[Double],
) -> Double raise @domain.ExtractionError {
  if resistances.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let mut total = 0.0
  for resistance in resistances {
    if resistance < 0.0 {
      raise @domain.ExtractionError::InvalidArgument(
        message="resistance cannot be negative",
      )
    }
    total += resistance
  }
  total
}

///|
pub fn utility_loads_total(loads : ArrayView[UtilityLoad]) -> Double {
  loads.fold(init=0.0, (sum, load) => sum + load.design_load)
}

///|
pub fn capacity_checks_feasible(checks : ArrayView[CapacityCheck]) -> Bool {
  for check in checks {
    if !check.feasible {
      return false
    }
  }
  true
}

///|
pub fn capacity_checks_worst_utilization(
  checks : ArrayView[CapacityCheck],
) -> Double {
  let mut worst = 0.0
  for check in checks {
    if check.utilization > worst {
      worst = check.utilization
    }
  }
  worst
}

///|
pub fn utility_loads_markdown(loads : ArrayView[UtilityLoad]) -> String {
  let lines = [
    "| Utility | Operating | Design | Margin | Unit |", "|---|---:|---:|---:|---|",
  ]
  for load in loads {
    lines.push(
      "| " +
      load.name +
      " | " +
      load.operating_load.to_string() +
      " | " +
      load.design_load.to_string() +
      " | " +
      load.margin.to_string() +
      " | " +
      load.unit +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn capacity_checks_markdown(checks : ArrayView[CapacityCheck]) -> String {
  let lines = [
    "| Capacity | Required | Available | Utilization | Feasible |", "|---|---:|---:|---:|---|",
  ]
  for check in checks {
    lines.push(
      "| " +
      check.name +
      " | " +
      check.required.to_string() +
      " | " +
      check.available.to_string() +
      " | " +
      check.utilization.to_string() +
      " | " +
      check.feasible.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn thermal_load_split(
  total_duty : Double,
  hot_fraction : Double,
) -> (Double, Double) raise @domain.ExtractionError {
  if total_duty < 0.0 || hot_fraction < 0.0 || hot_fraction > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="thermal split is invalid",
    )
  }
  (total_duty * hot_fraction, total_duty * (1.0 - hot_fraction))
}

///|
pub fn annualized_energy(
  power : Double,
  operating_hours : Double,
  load_factor : Double,
) -> Double raise @domain.ExtractionError {
  if power < 0.0 ||
    operating_hours < 0.0 ||
    load_factor < 0.0 ||
    load_factor > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="annual energy inputs are invalid",
    )
  }
  power * operating_hours * load_factor
}

///|
pub fn carbon_emissions(
  energy : Double,
  emission_factor : Double,
) -> Double raise @domain.ExtractionError {
  if energy < 0.0 || emission_factor < 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="emission inputs are invalid",
    )
  }
  energy * emission_factor
}

///|
pub fn utility_cost(
  consumption : Double,
  unit_price : Double,
) -> Double raise @domain.ExtractionError {
  if consumption < 0.0 || unit_price < 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="utility cost inputs are invalid",
    )
  }
  consumption * unit_price
}