///|
/// Solvent shortlist scoring for practical extraction design decisions.
pub(all) struct SolventOption {
  name : String
  distribution_coefficient : Double
  selectivity : Double
  density_difference : Double
  viscosity : Double
  volatility : Double
  cost_index : Double
  safety_index : Double
} derive(Debug, Eq, ToJson)

///|
pub fn SolventOption::new(
  name~ : String,
  distribution_coefficient~ : Double,
  selectivity~ : Double,
  density_difference~ : Double,
  viscosity~ : Double,
  volatility~ : Double,
  cost_index~ : Double,
  safety_index~ : Double,
) -> SolventOption {
  {
    name,
    distribution_coefficient,
    selectivity,
    density_difference,
    viscosity,
    volatility,
    cost_index,
    safety_index,
  }
}

///|
pub(all) struct SolventScore {
  option : SolventOption
  extraction_score : Double
  handling_score : Double
  sustainability_score : Double
  total_score : Double
  rank : Int
  recommendation : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ScreeningWeights {
  extraction : Double
  handling : Double
  sustainability : Double
} derive(Debug, Eq, ToJson)

///|
pub fn SolventOption::validate(self : SolventOption) -> Bool {
  self.distribution_coefficient >= 0.0 &&
  self.selectivity >= 0.0 &&
  self.density_difference >= 0.0 &&
  self.viscosity > 0.0 &&
  self.volatility >= 0.0 &&
  self.cost_index >= 0.0 &&
  self.safety_index >= 0.0 &&
  self.safety_index <= 1.0
}

///|
pub fn normalized_score(value : Double, reference : Double) -> Double {
  if reference <= 0.0 {
    0.0
  } else {
    (value / reference).min(1.0).max(0.0)
  }
}

///|
pub fn solvent_extraction_score(option : SolventOption) -> Double {
  let distribution = normalized_score(option.distribution_coefficient, 10.0)
  let selectivity = normalized_score(option.selectivity, 20.0)
  let phase_split = normalized_score(option.density_difference, 500.0)
  0.5 * distribution + 0.35 * selectivity + 0.15 * phase_split
}

///|
pub fn solvent_handling_score(option : SolventOption) -> Double {
  let viscosity_score = 1.0 / (1.0 + option.viscosity / 10.0)
  let volatility_score = 1.0 / (1.0 + option.volatility / 10.0)
  let cost_score = 1.0 / (1.0 + option.cost_index)
  0.4 * viscosity_score + 0.3 * volatility_score + 0.3 * cost_score
}

///|
pub fn solvent_sustainability_score(option : SolventOption) -> Double {
  0.7 * option.safety_index + 0.3 / (1.0 + option.volatility)
}

///|
pub fn score_solvent(
  option : SolventOption,
  weights : ScreeningWeights,
) -> SolventScore raise @domain.ExtractionError {
  if !option.validate() {
    raise @domain.ExtractionError::InvalidArgument(
      message="solvent option is invalid",
    )
  }
  let total_weight = weights.extraction +
    weights.handling +
    weights.sustainability
  if total_weight <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="screening weights sum to zero",
    )
  }
  let extraction = solvent_extraction_score(option)
  let handling = solvent_handling_score(option)
  let sustainability = solvent_sustainability_score(option)
  let total = (
      weights.extraction * extraction +
      weights.handling * handling +
      weights.sustainability * sustainability
    ) /
    total_weight
  {
    option,
    extraction_score: extraction,
    handling_score: handling,
    sustainability_score: sustainability,
    total_score: total,
    rank: 0,
    recommendation: if total >= 0.8 {
      "preferred"
    } else if total >= 0.6 {
      "shortlist"
    } else {
      "review"
    },
  }
}

///|
pub fn score_solvents(
  options : ArrayView[SolventOption],
  weights : ScreeningWeights,
) -> Array[SolventScore] raise @domain.ExtractionError {
  let scores = []
  for option in options {
    scores.push(score_solvent(option, weights))
  }
  for index in 0.. scores[index].total_score {
        rank += 1
      }
    }
    let old = scores[index]
    scores[index] = {
      option: old.option,
      extraction_score: old.extraction_score,
      handling_score: old.handling_score,
      sustainability_score: old.sustainability_score,
      total_score: old.total_score,
      rank,
      recommendation: old.recommendation,
    }
  }
  scores
}

///|
pub fn top_solvent(scores : ArrayView[SolventScore]) -> SolventScore? {
  if scores.length() == 0 {
    None
  } else {
    let mut best = scores[0]
    for score in scores {
      if score.total_score > best.total_score {
        best = score
      }
    }
    Some(best)
  }
}

///|
pub fn solvent_score_gap(scores : ArrayView[SolventScore]) -> Double {
  if scores.length() < 2 {
    return 0.0
  }
  let mut best = scores[0].total_score
  let mut second = -1.0
  for score in scores {
    if score.total_score > best {
      second = best
      best = score.total_score
    } else if score.total_score > second && score.total_score < best {
      second = score.total_score
    }
  }
  if second < 0.0 {
    0.0
  } else {
    best - second
  }
}

///|
pub fn sensitivity_to_weight(
  options : ArrayView[SolventOption],
  extraction_weight : Double,
  handling_weight : Double,
  sustainability_weight : Double,
) -> Array[Double] raise @domain.ExtractionError {
  let scores = score_solvents(options, {
    extraction: extraction_weight,
    handling: handling_weight,
    sustainability: sustainability_weight,
  })
  [
    for score in scores => score.total_score
  ]
}

///|
pub fn solvent_screening_markdown(scores : ArrayView[SolventScore]) -> String {
  let lines = [
    "| Rank | Solvent | Extraction | Handling | Sustainability | Total | Recommendation |",
    "|---:|---|---:|---:|---:|---:|---|",
  ]
  for score in scores {
    lines.push(
      "| " +
      score.rank.to_string() +
      " | " +
      score.option.name +
      " | " +
      score.extraction_score.to_string() +
      " | " +
      score.handling_score.to_string() +
      " | " +
      score.sustainability_score.to_string() +
      " | " +
      score.total_score.to_string() +
      " | " +
      score.recommendation +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn solvent_screening_csv(scores : ArrayView[SolventScore]) -> String {
  let lines = [
    "rank,name,extraction_score,handling_score,sustainability_score,total_score,recommendation",
  ]
  for score in scores {
    lines.push(
      score.rank.to_string() +
      "," +
      score.option.name +
      "," +
      score.extraction_score.to_string() +
      "," +
      score.handling_score.to_string() +
      "," +
      score.sustainability_score.to_string() +
      "," +
      score.total_score.to_string() +
      "," +
      score.recommendation,
    )
  }
  lines.join("\n")
}

///|
pub fn safety_gate(option : SolventOption, minimum_safety : Double) -> Bool {
  option.safety_index >= minimum_safety &&
  option.volatility <= 10.0 &&
  option.viscosity <= 100.0
}

///|
pub fn phase_split_gate(
  option : SolventOption,
  minimum_density_difference : Double,
) -> Bool {
  option.density_difference >= minimum_density_difference &&
  option.distribution_coefficient > 0.0
}

///|
pub fn solvent_is_usable(
  option : SolventOption,
  minimum_safety : Double,
  minimum_density_difference : Double,
) -> Bool {
  option.validate() &&
  safety_gate(option, minimum_safety) &&
  phase_split_gate(option, minimum_density_difference)
}

///|
pub fn usable_solvents(
  options : ArrayView[SolventOption],
  minimum_safety : Double,
  minimum_density_difference : Double,
) -> Array[SolventOption] {
  let result = []
  for option in options {
    if solvent_is_usable(option, minimum_safety, minimum_density_difference) {
      result.push(option)
    }
  }
  result
}

///|
pub fn screening_weights_equal() -> ScreeningWeights {
  { extraction: 1.0, handling: 1.0, sustainability: 1.0 }
}