///|
pub(all) enum SoilClass {
  Sand
  SandyLoam
  Loam
  ClayLoam
  Clay
  UrbanFill
  EngineeredMedia
} derive(Eq, Debug)

///|
pub(all) struct SoilProfile {
  kind : SoilClass
  name : String
  infiltration_rate_mm_per_hr : Double
  porosity : Double
  runoff_adjustment : Double
  clogging_risk : Double
  maintenance_interval_days : Int
} derive(Eq, Debug)

///|
pub(all) enum MaintenancePriority {
  Routine
  Watch
  ServiceSoon
  Restore
} derive(Eq, Debug)

///|
pub(all) enum MaintenanceAction {
  InspectInlet
  RemoveSediment
  RenewMulch
  CheckOverflow
  ReplantBareSoil
  LoosenSurface
} derive(Eq, Debug)

///|
pub(all) struct MaintenancePlan {
  priority : MaintenancePriority
  interval_days : Int
  risk_score : Int
  actions : Array[MaintenanceAction]
  note : String
} derive(Eq, Debug)

///|
pub fn soil_profile(kind : SoilClass) -> SoilProfile {
  match kind {
    Sand => profile(kind, "sand", 75.0, 0.35, 0.85, 0.20, 120)
    SandyLoam => profile(kind, "sandy loam", 45.0, 0.32, 0.90, 0.30, 90)
    Loam => profile(kind, "loam", 25.0, 0.28, 1.00, 0.45, 75)
    ClayLoam => profile(kind, "clay loam", 12.0, 0.24, 1.10, 0.65, 45)
    Clay => profile(kind, "clay", 5.0, 0.18, 1.20, 0.85, 30)
    UrbanFill => profile(kind, "urban fill", 8.0, 0.20, 1.15, 0.75, 30)
    EngineeredMedia =>
      profile(kind, "engineered media", 50.0, 0.30, 0.95, 0.25, 120)
  }
}

///|
pub fn SoilClass::label(self : SoilClass) -> String {
  match self {
    Sand => "sand"
    SandyLoam => "sandy_loam"
    Loam => "loam"
    ClayLoam => "clay_loam"
    Clay => "clay"
    UrbanFill => "urban_fill"
    EngineeredMedia => "engineered_media"
  }
}

///|
pub fn MaintenancePriority::label(self : MaintenancePriority) -> String {
  match self {
    Routine => "routine"
    Watch => "watch"
    ServiceSoon => "service_soon"
    Restore => "restore"
  }
}

///|
pub fn MaintenanceAction::label(self : MaintenanceAction) -> String {
  match self {
    InspectInlet => "inspect_inlet"
    RemoveSediment => "remove_sediment"
    RenewMulch => "renew_mulch"
    CheckOverflow => "check_overflow"
    ReplantBareSoil => "replant_bare_soil"
    LoosenSurface => "loosen_surface"
  }
}

///|
pub fn spec_with_soil(
  spec : GardenSpec,
  soil : SoilProfile,
) -> Result[GardenSpec, SpongeError] {
  match validate_soil(soil) {
    Some(error) => Err(error)
    None =>
      Ok({
        ..spec,
        media_porosity: soil.porosity,
        infiltration_rate_mm_per_hr: soil.infiltration_rate_mm_per_hr,
      })
  }
}

///|
pub fn adjusted_catchment_for_soil(
  catchment : Catchment,
  soil : SoilProfile,
) -> Result[Catchment, SpongeError] {
  match validate_soil(soil) {
    Some(error) => Err(error)
    None => {
      let adjusted = clamp_double(
        catchment.runoff_coefficient * soil.runoff_adjustment,
        0.0,
        1.0,
      )
      Ok({ ..catchment, runoff_coefficient: adjusted })
    }
  }
}

///|
pub fn blended_soil_profile(
  name : String,
  soils : Array[SoilProfile],
  weights : Array[Double],
) -> Result[SoilProfile, SpongeError] {
  if soils.length() != weights.length() || soils.length() == 0 {
    return Err(InvalidWeight)
  }
  let mut total_weight = 0.0
  let mut infiltration = 0.0
  let mut porosity = 0.0
  let mut runoff_adjustment = 0.0
  let mut clogging_risk = 0.0
  for index, soil in soils {
    match validate_soil(soil) {
      Some(error) => return Err(error)
      None => ()
    }
    let weight = weights[index]
    if weight < 0.0 {
      return Err(InvalidWeight)
    }
    total_weight = total_weight + weight
    infiltration = infiltration + soil.infiltration_rate_mm_per_hr * weight
    porosity = porosity + soil.porosity * weight
    runoff_adjustment = runoff_adjustment + soil.runoff_adjustment * weight
    clogging_risk = clogging_risk + soil.clogging_risk * weight
  }
  if total_weight <= 0.0 {
    return Err(InvalidWeight)
  }
  let risk = clogging_risk / total_weight
  Ok({
    kind: EngineeredMedia,
    name,
    infiltration_rate_mm_per_hr: infiltration / total_weight,
    porosity: porosity / total_weight,
    runoff_adjustment: runoff_adjustment / total_weight,
    clogging_risk: risk,
    maintenance_interval_days: interval_for_risk(risk),
  })
}

///|
pub fn maintenance_plan(
  plan : GardenPlan,
  soil : SoilProfile,
  sediment_load_index : Double,
  months_since_service : Int,
) -> Result[MaintenancePlan, SpongeError] {
  match validate_soil(soil) {
    Some(error) => Err(error)
    None =>
      if sediment_load_index < 0.0 || sediment_load_index > 1.0 {
        Err(InvalidSedimentLoad)
      } else {
        let risk = maintenance_risk_score(
          plan, soil, sediment_load_index, months_since_service,
        )
        let priority = priority_for_risk(risk)
        Ok({
          priority,
          interval_days: soil.maintenance_interval_days,
          risk_score: risk,
          actions: actions_for_priority(priority, sediment_load_index, plan),
          note: note_for_priority(priority),
        })
      }
  }
}

///|
pub fn SoilProfile::to_json(self : SoilProfile) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"kind\":\"" + self.kind.label() + "\",")
  out.write_string("\"name\":\"" + escape_json(self.name) + "\",")
  out.write_string(
    "\"infiltration_rate_mm_per_hr\":" +
    self.infiltration_rate_mm_per_hr.to_string() +
    ",",
  )
  out.write_string("\"porosity\":" + self.porosity.to_string() + ",")
  out.write_string(
    "\"runoff_adjustment\":" + self.runoff_adjustment.to_string() + ",",
  )
  out.write_string("\"clogging_risk\":" + self.clogging_risk.to_string() + ",")
  out.write_string(
    "\"maintenance_interval_days\":" +
    self.maintenance_interval_days.to_string(),
  )
  out.write_string("}")
  out.to_string()
}

///|
pub fn MaintenancePlan::to_json(self : MaintenancePlan) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"priority\":\"" + self.priority.label() + "\",")
  out.write_string("\"interval_days\":" + self.interval_days.to_string() + ",")
  out.write_string("\"risk_score\":" + self.risk_score.to_string() + ",")
  out.write_string("\"actions\":[")
  for index, action in self.actions {
    if index > 0 {
      out.write_string(",")
    }
    out.write_string("\"" + action.label() + "\"")
  }
  out.write_string("],\"note\":\"" + escape_json(self.note) + "\"}")
  out.to_string()
}

///|
fn profile(
  kind : SoilClass,
  name : String,
  infiltration_rate_mm_per_hr : Double,
  porosity : Double,
  runoff_adjustment : Double,
  clogging_risk : Double,
  maintenance_interval_days : Int,
) -> SoilProfile {
  {
    kind,
    name,
    infiltration_rate_mm_per_hr,
    porosity,
    runoff_adjustment,
    clogging_risk,
    maintenance_interval_days,
  }
}

///|
fn validate_soil(soil : SoilProfile) -> SpongeError? {
  if soil.infiltration_rate_mm_per_hr <= 0.0 {
    Some(InvalidInfiltrationRate)
  } else if soil.porosity < 0.0 || soil.porosity > 0.8 {
    Some(InvalidStorageDepth)
  } else if soil.runoff_adjustment < 0.0 {
    Some(InvalidRunoffCoefficient)
  } else if soil.clogging_risk < 0.0 || soil.clogging_risk > 1.0 {
    Some(InvalidSedimentLoad)
  } else {
    None
  }
}

///|
fn maintenance_risk_score(
  plan : GardenPlan,
  soil : SoilProfile,
  sediment_load_index : Double,
  months_since_service : Int,
) -> Int {
  let service_age = if months_since_service < 0 {
    0
  } else {
    months_since_service
  }
  let drawdown_risk = if plan.drawdown_hours > 48.0 {
    30
  } else if plan.drawdown_hours > 24.0 {
    18
  } else if plan.drawdown_hours > 12.0 {
    8
  } else {
    0
  }
  let capture_risk = if plan.capture_ratio < 0.70 {
    20
  } else if plan.capture_ratio < 0.90 {
    10
  } else {
    0
  }
  let soil_risk = (soil.clogging_risk * 30.0).to_int()
  let sediment_risk = (sediment_load_index * 25.0).to_int()
  let age_risk = if service_age >= 12 {
    15
  } else if service_age >= 6 {
    8
  } else {
    0
  }
  clamp_int(
    drawdown_risk + capture_risk + soil_risk + sediment_risk + age_risk,
    0,
    100,
  )
}

///|
fn priority_for_risk(risk : Int) -> MaintenancePriority {
  if risk >= 75 {
    Restore
  } else if risk >= 50 {
    ServiceSoon
  } else if risk >= 25 {
    Watch
  } else {
    Routine
  }
}

///|
fn actions_for_priority(
  priority : MaintenancePriority,
  sediment_load_index : Double,
  plan : GardenPlan,
) -> Array[MaintenanceAction] {
  let actions : Array[MaintenanceAction] = [InspectInlet, CheckOverflow]
  if priority == Watch || priority == ServiceSoon || priority == Restore {
    actions.push(RemoveSediment)
  }
  if sediment_load_index >= 0.50 {
    actions.push(RenewMulch)
  }
  if plan.drawdown_hours > 24.0 {
    actions.push(LoosenSurface)
  }
  if priority == Restore {
    actions.push(ReplantBareSoil)
  }
  actions
}

///|
fn note_for_priority(priority : MaintenancePriority) -> String {
  match priority {
    Routine => "routine inspection is enough for the current design card"
    Watch => "watch inlet sediment and repeat drawdown observation after storms"
    ServiceSoon => "schedule sediment removal and surface maintenance soon"
    Restore =>
      "restore media surface and vegetation before relying on capture results"
  }
}

///|
fn interval_for_risk(risk : Double) -> Int {
  if risk >= 0.75 {
    30
  } else if risk >= 0.55 {
    45
  } else if risk >= 0.35 {
    75
  } else {
    120
  }
}

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

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