///|
pub(all) struct RetrofitScenario {
  name : String
  rainfall_multiplier : Double
  runoff_multiplier : Double
  infiltration_multiplier : Double
  target_capture_ratio : Double
  available_area_m2 : Double
  sediment_load_index : Double
  cost_tier : CostTier
  capital_budget : Double
  annual_budget : Double
} derive(Eq, Debug)

///|
pub(all) struct ScenarioResult {
  name : String
  adjusted_rain_mm : Double
  adjusted_runoff_m3 : Double
  recommended_area_m2 : Double
  candidate_best_name : String
  candidate_best_score : Int
  treatment_capture_ratio : Double
  treatment_bypass_m3 : Double
  treatment_resilience_score : Int
  maintenance_priority : MaintenancePriority
  maintenance_risk_score : Int
  total_capital_cost : Double
  lifecycle_cost : Double
  budget_fit_score : Int
  overall_score : Int
  status : PlanStatus
  notes : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct ScenarioComparison {
  scenario_count : Int
  best_name : String
  worst_name : String
  average_score : Double
  max_runoff_m3 : Double
  min_capture_ratio : Double
  results : Array[ScenarioResult]
} derive(Eq, Debug)

///|
pub fn retrofit_scenario(
  name : String,
  rainfall_multiplier : Double,
  available_area_m2 : Double,
) -> RetrofitScenario {
  scenario_detail(
    name,
    rainfall_multiplier,
    1.0,
    1.0,
    0.90,
    available_area_m2,
    0.35,
    Standard,
    60000.0,
    9000.0,
  )
}

///|
pub fn scenario_with_budget(
  name : String,
  rainfall_multiplier : Double,
  runoff_multiplier : Double,
  infiltration_multiplier : Double,
  target_capture_ratio : Double,
  available_area_m2 : Double,
  sediment_load_index : Double,
  cost_tier : CostTier,
  capital_budget : Double,
  annual_budget : Double,
) -> RetrofitScenario {
  scenario_detail(
    name, rainfall_multiplier, runoff_multiplier, infiltration_multiplier, target_capture_ratio,
    available_area_m2, sediment_load_index, cost_tier, capital_budget, annual_budget,
  )
}

///|
pub fn default_stress_scenarios(
  available_area_m2 : Double,
) -> Array[RetrofitScenario] {
  [
    scenario_with_budget(
      "baseline",
      1.0,
      1.0,
      1.0,
      0.90,
      available_area_m2,
      0.35,
      Standard,
      60000.0,
      9000.0,
    ),
    scenario_with_budget(
      "compact retrofit",
      0.85,
      0.95,
      1.10,
      0.85,
      available_area_m2 * 0.70,
      0.25,
      Economy,
      42000.0,
      6500.0,
    ),
    scenario_with_budget(
      "climate stress",
      1.35,
      1.10,
      0.80,
      0.90,
      available_area_m2,
      0.60,
      Durable,
      85000.0,
      12000.0,
    ),
    scenario_with_budget(
      "showcase upgrade",
      1.20,
      1.00,
      1.15,
      0.95,
      available_area_m2 * 1.25,
      0.30,
      Showcase,
      120000.0,
      16000.0,
    ),
  ]
}

///|
pub fn evaluate_retrofit_scenario(
  catchments : Array[Catchment],
  base_event : RainEvent,
  spec : GardenSpec,
  scenario : RetrofitScenario,
) -> Result[ScenarioResult, SpongeError] {
  match validate_scenario(scenario) {
    Some(error) => Err(error)
    None => {
      let adjusted_event = rain_event(
        scenario.name + " rain",
        base_event.depth_mm * scenario.rainfall_multiplier,
      )
      let adjusted_catchments = adjusted_catchments_for_scenario(
        catchments,
        scenario.runoff_multiplier,
      )
      let adjusted_spec = {
        ..spec,
        infiltration_rate_mm_per_hr: spec.infiltration_rate_mm_per_hr *
        scenario.infiltration_multiplier,
        target_capture_ratio: scenario.target_capture_ratio,
      }
      match
        size_rain_garden(adjusted_catchments, adjusted_event, adjusted_spec) {
        Err(error) => Err(error)
        Ok(garden_plan) =>
          match scenario_candidates(scenario) {
            Err(error) => Err(error)
            Ok(candidates) =>
              match
                compare_candidate_areas(
                  adjusted_catchments, adjusted_event, adjusted_spec, candidates,
                ) {
                Err(error) => Err(error)
                Ok(candidate_comparison) =>
                  match
                    recommend_facility_mix(
                      adjusted_catchments,
                      adjusted_event,
                      scenario.available_area_m2,
                    ) {
                    Err(error) => Err(error)
                    Ok(train_plan) =>
                      match
                        maintenance_plan(
                          garden_plan,
                          soil_profile(EngineeredMedia),
                          scenario.sediment_load_index,
                          service_age_for_scenario(scenario),
                        ) {
                        Err(error) => Err(error)
                        Ok(maintenance) =>
                          match
                            estimate_treatment_train_cost(
                              train_plan,
                              cost_profile(scenario.cost_tier),
                            ) {
                            Err(error) => Err(error)
                            Ok(cost_summary) =>
                              match
                                budget_fit_score(
                                  cost_summary,
                                  scenario.capital_budget,
                                  scenario.annual_budget,
                                ) {
                                Err(error) => Err(error)
                                Ok(budget_score) => {
                                  let notes = scenario_notes(
                                    garden_plan, train_plan, maintenance, cost_summary,
                                    scenario,
                                  )
                                  let status = scenario_status(
                                    garden_plan.status,
                                    train_plan.status,
                                    maintenance.priority,
                                    budget_score,
                                  )
                                  let overall = scenario_overall_score(
                                    garden_plan,
                                    candidate_comparison.best_score,
                                    train_plan,
                                    maintenance,
                                    budget_score,
                                    notes.length(),
                                  )
                                  Ok({
                                    name: scenario.name,
                                    adjusted_rain_mm: adjusted_event.depth_mm,
                                    adjusted_runoff_m3: garden_plan.runoff_volume_m3,
                                    recommended_area_m2: garden_plan.recommended_area_m2,
                                    candidate_best_name: candidate_comparison.best_name,
                                    candidate_best_score: candidate_comparison.best_score,
                                    treatment_capture_ratio: train_plan.capture_ratio,
                                    treatment_bypass_m3: train_plan.bypass_volume_m3,
                                    treatment_resilience_score: train_plan.resilience_score,
                                    maintenance_priority: maintenance.priority,
                                    maintenance_risk_score: maintenance.risk_score,
                                    total_capital_cost: cost_summary.total_capital_cost,
                                    lifecycle_cost: cost_summary.lifecycle_cost,
                                    budget_fit_score: budget_score,
                                    overall_score: overall,
                                    status,
                                    notes,
                                  })
                                }
                              }
                          }
                      }
                  }
              }
          }
      }
    }
  }
}

///|
pub fn compare_retrofit_scenarios(
  catchments : Array[Catchment],
  base_event : RainEvent,
  spec : GardenSpec,
  scenarios : Array[RetrofitScenario],
) -> Result[ScenarioComparison, SpongeError] {
  if scenarios.length() == 0 {
    return Err(InvalidScenarioInput)
  }
  let results : Array[ScenarioResult] = []
  let mut best_name = ""
  let mut worst_name = ""
  let mut best_score = -1
  let mut worst_score = 101
  let mut score_sum = 0
  let mut max_runoff = 0.0
  let mut min_capture = 2.0
  for scenario in scenarios {
    match evaluate_retrofit_scenario(catchments, base_event, spec, scenario) {
      Err(error) => return Err(error)
      Ok(result) => {
        if result.overall_score > best_score {
          best_score = result.overall_score
          best_name = result.name
        }
        if result.overall_score < worst_score {
          worst_score = result.overall_score
          worst_name = result.name
        }
        if result.adjusted_runoff_m3 > max_runoff {
          max_runoff = result.adjusted_runoff_m3
        }
        if result.treatment_capture_ratio < min_capture {
          min_capture = result.treatment_capture_ratio
        }
        score_sum = score_sum + result.overall_score
        results.push(result)
      }
    }
  }
  Ok({
    scenario_count: results.length(),
    best_name,
    worst_name,
    average_score: score_sum.to_double() / results.length().to_double(),
    max_runoff_m3: max_runoff,
    min_capture_ratio: min_capture,
    results,
  })
}

///|
pub fn RetrofitScenario::to_json(self : RetrofitScenario) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"name\":\"" + escape_json(self.name) + "\",")
  out.write_string(
    "\"rainfall_multiplier\":" + self.rainfall_multiplier.to_string() + ",",
  )
  out.write_string(
    "\"runoff_multiplier\":" + self.runoff_multiplier.to_string() + ",",
  )
  out.write_string(
    "\"infiltration_multiplier\":" +
    self.infiltration_multiplier.to_string() +
    ",",
  )
  out.write_string(
    "\"target_capture_ratio\":" + self.target_capture_ratio.to_string() + ",",
  )
  out.write_string(
    "\"available_area_m2\":" + self.available_area_m2.to_string() + ",",
  )
  out.write_string(
    "\"sediment_load_index\":" + self.sediment_load_index.to_string() + ",",
  )
  out.write_string("\"cost_tier\":\"" + self.cost_tier.label() + "\",")
  out.write_string(
    "\"capital_budget\":" + self.capital_budget.to_string() + ",",
  )
  out.write_string("\"annual_budget\":" + self.annual_budget.to_string())
  out.write_string("}")
  out.to_string()
}

///|
pub fn ScenarioResult::to_json(self : ScenarioResult) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"name\":\"" + escape_json(self.name) + "\",")
  out.write_string(
    "\"adjusted_rain_mm\":" + self.adjusted_rain_mm.to_string() + ",",
  )
  out.write_string(
    "\"adjusted_runoff_m3\":" + self.adjusted_runoff_m3.to_string() + ",",
  )
  out.write_string(
    "\"recommended_area_m2\":" + self.recommended_area_m2.to_string() + ",",
  )
  out.write_string(
    "\"candidate_best_name\":\"" + escape_json(self.candidate_best_name) + "\",",
  )
  out.write_string(
    "\"candidate_best_score\":" + self.candidate_best_score.to_string() + ",",
  )
  out.write_string(
    "\"treatment_capture_ratio\":" +
    self.treatment_capture_ratio.to_string() +
    ",",
  )
  out.write_string(
    "\"treatment_bypass_m3\":" + self.treatment_bypass_m3.to_string() + ",",
  )
  out.write_string(
    "\"treatment_resilience_score\":" +
    self.treatment_resilience_score.to_string() +
    ",",
  )
  out.write_string(
    "\"maintenance_priority\":\"" + self.maintenance_priority.label() + "\",",
  )
  out.write_string(
    "\"maintenance_risk_score\":" +
    self.maintenance_risk_score.to_string() +
    ",",
  )
  out.write_string(
    "\"total_capital_cost\":" + self.total_capital_cost.to_string() + ",",
  )
  out.write_string(
    "\"lifecycle_cost\":" + self.lifecycle_cost.to_string() + ",",
  )
  out.write_string(
    "\"budget_fit_score\":" + self.budget_fit_score.to_string() + ",",
  )
  out.write_string("\"overall_score\":" + self.overall_score.to_string() + ",")
  out.write_string("\"status\":\"" + self.status.label() + "\",")
  out.write_string("\"notes\":" + scenario_string_array_to_json(self.notes))
  out.write_string("}")
  out.to_string()
}

///|
pub fn ScenarioComparison::to_json(self : ScenarioComparison) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string(
    "\"scenario_count\":" + self.scenario_count.to_string() + ",",
  )
  out.write_string("\"best_name\":\"" + escape_json(self.best_name) + "\",")
  out.write_string("\"worst_name\":\"" + escape_json(self.worst_name) + "\",")
  out.write_string("\"average_score\":" + self.average_score.to_string() + ",")
  out.write_string("\"max_runoff_m3\":" + self.max_runoff_m3.to_string() + ",")
  out.write_string(
    "\"min_capture_ratio\":" + self.min_capture_ratio.to_string() + ",",
  )
  out.write_string("\"results\":[")
  for index, result in self.results {
    if index > 0 {
      out.write_string(",")
    }
    out.write_string(result.to_json())
  }
  out.write_string("]}")
  out.to_string()
}

///|
pub fn ScenarioComparison::to_markdown(self : ScenarioComparison) -> String {
  let out = StringBuilder::new()
  out.write_string("# SpongeKit Scenario Comparison\n\n")
  out.write_string("- Scenarios: " + self.scenario_count.to_string() + "\n")
  out.write_string("- Best: " + self.best_name + "\n")
  out.write_string("- Worst: " + self.worst_name + "\n")
  out.write_string("- Average score: " + self.average_score.to_string() + "\n")
  out.write_string("- Max runoff: " + self.max_runoff_m3.to_string() + " m3\n")
  out.write_string(
    "- Min train capture: " + self.min_capture_ratio.to_string() + "\n\n",
  )
  out.write_string(
    "| Scenario | Rain mm | Runoff m3 | Area m2 | Capture | Budget | Score | Status |\n",
  )
  out.write_string("| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |\n")
  for result in self.results {
    out.write_string(
      "| " +
      result.name +
      " | " +
      result.adjusted_rain_mm.to_string() +
      " | " +
      result.adjusted_runoff_m3.to_string() +
      " | " +
      result.recommended_area_m2.to_string() +
      " | " +
      result.treatment_capture_ratio.to_string() +
      " | " +
      result.budget_fit_score.to_string() +
      " | " +
      result.overall_score.to_string() +
      " | " +
      result.status.label() +
      " |\n",
    )
  }
  out.to_string()
}

///|
fn scenario_detail(
  name : String,
  rainfall_multiplier : Double,
  runoff_multiplier : Double,
  infiltration_multiplier : Double,
  target_capture_ratio : Double,
  available_area_m2 : Double,
  sediment_load_index : Double,
  cost_tier : CostTier,
  capital_budget : Double,
  annual_budget : Double,
) -> RetrofitScenario {
  {
    name,
    rainfall_multiplier,
    runoff_multiplier,
    infiltration_multiplier,
    target_capture_ratio,
    available_area_m2,
    sediment_load_index,
    cost_tier,
    capital_budget,
    annual_budget,
  }
}

///|
fn validate_scenario(scenario : RetrofitScenario) -> SpongeError? {
  if scenario.rainfall_multiplier <= 0.0 ||
    scenario.runoff_multiplier <= 0.0 ||
    scenario.infiltration_multiplier <= 0.0 {
    Some(InvalidScenarioInput)
  } else if scenario.target_capture_ratio <= 0.0 ||
    scenario.target_capture_ratio > 1.0 {
    Some(InvalidScenarioInput)
  } else if scenario.available_area_m2 <= 0.0 {
    Some(InvalidScenarioInput)
  } else if scenario.sediment_load_index < 0.0 ||
    scenario.sediment_load_index > 1.0 {
    Some(InvalidSedimentLoad)
  } else if scenario.capital_budget <= 0.0 || scenario.annual_budget <= 0.0 {
    Some(InvalidCostProfile)
  } else {
    None
  }
}

///|
fn adjusted_catchments_for_scenario(
  catchments : Array[Catchment],
  runoff_multiplier : Double,
) -> Array[Catchment] {
  let adjusted : Array[Catchment] = []
  for catchment in catchments {
    adjusted.push({
      ..catchment,
      runoff_coefficient: scenario_clamp_double(
        catchment.runoff_coefficient * runoff_multiplier,
        0.0,
        1.0,
      ),
    })
  }
  adjusted
}

///|
fn scenario_candidates(
  scenario : RetrofitScenario,
) -> Result[Array[AreaCandidate], SpongeError] {
  if scenario.available_area_m2 <= 0.0 {
    Err(InvalidScenarioInput)
  } else {
    Ok([
      area_candidate("compact footprint", scenario.available_area_m2 * 0.50),
      area_candidate("balanced footprint", scenario.available_area_m2 * 0.75),
      area_candidate("full footprint", scenario.available_area_m2),
    ])
  }
}

///|
fn service_age_for_scenario(scenario : RetrofitScenario) -> Int {
  if scenario.sediment_load_index >= 0.70 {
    14
  } else if scenario.sediment_load_index >= 0.50 {
    8
  } else if scenario.rainfall_multiplier >= 1.25 {
    6
  } else {
    3
  }
}

///|
fn scenario_notes(
  garden_plan : GardenPlan,
  train_plan : TreatmentTrainPlan,
  maintenance : MaintenancePlan,
  cost_summary : TrainCostSummary,
  scenario : RetrofitScenario,
) -> Array[String] {
  let notes : Array[String] = []
  if garden_plan.recommended_area_m2 > scenario.available_area_m2 {
    notes.push("recommended garden area exceeds available footprint")
  }
  if train_plan.capture_ratio < scenario.target_capture_ratio {
    notes.push("treatment train capture is below scenario target")
  }
  if train_plan.bypass_volume_m3 > 0.0 {
    notes.push("scenario keeps bypass volume after treatment train")
  }
  if maintenance.priority == ServiceSoon || maintenance.priority == Restore {
    notes.push("maintenance priority is elevated")
  }
  if cost_summary.total_capital_cost > scenario.capital_budget {
    notes.push("capital cost exceeds budget")
  }
  if cost_summary.annual_maintenance_cost > scenario.annual_budget {
    notes.push("annual maintenance exceeds budget")
  }
  if scenario.rainfall_multiplier >= 1.25 {
    notes.push("rainfall multiplier represents a stress condition")
  }
  notes
}

///|
fn scenario_status(
  garden_status : PlanStatus,
  train_status : PlanStatus,
  priority : MaintenancePriority,
  budget_score : Int,
) -> PlanStatus {
  if garden_status == Invalid || train_status == Invalid {
    Invalid
  } else if priority == Restore || budget_score < 35 {
    NeedsReview
  } else if garden_status != Ready || train_status != Ready {
    NeedsReview
  } else {
    Ready
  }
}

///|
fn scenario_overall_score(
  garden_plan : GardenPlan,
  candidate_score : Int,
  train_plan : TreatmentTrainPlan,
  maintenance : MaintenancePlan,
  budget_score : Int,
  note_count : Int,
) -> Int {
  let area_fit = if garden_plan.recommended_area_m2 <= 0.0 {
    5
  } else if garden_plan.capture_ratio >= 1.0 {
    12
  } else {
    (garden_plan.capture_ratio * 12.0).to_int()
  }
  let train_points = (scenario_clamp_double(train_plan.capture_ratio, 0.0, 1.0) *
  28.0).to_int()
  let candidate_points = (candidate_score.to_double() * 0.18).to_int()
  let resilience_points = (train_plan.resilience_score.to_double() * 0.20).to_int()
  let budget_points = (budget_score.to_double() * 0.22).to_int()
  let maintenance_penalty = maintenance_penalty_points(maintenance.priority)
  scenario_clamp_int(
    area_fit +
    train_points +
    candidate_points +
    resilience_points +
    budget_points -
    maintenance_penalty -
    note_count * 3,
    0,
    100,
  )
}

///|
fn maintenance_penalty_points(priority : MaintenancePriority) -> Int {
  match priority {
    Routine => 0
    Watch => 5
    ServiceSoon => 12
    Restore => 22
  }
}

///|
fn scenario_clamp_double(value : Double, low : Double, high : Double) -> Double {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
fn scenario_clamp_int(value : Int, low : Int, high : Int) -> Int {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
fn scenario_string_array_to_json(values : Array[String]) -> String {
  let out = StringBuilder::new()
  out.write_string("[")
  for index, value in values {
    if index > 0 {
      out.write_string(",")
    }
    out.write_string("\"" + escape_json(value) + "\"")
  }
  out.write_string("]")
  out.to_string()
}