///|
pub(all) enum FacilityKind {
  RainGarden
  Bioswale
  PermeablePavement
  Cistern
  GreenRoof
  TreeTrench
  DryWell
} derive(Eq, Debug)

///|
pub(all) struct FacilitySpec {
  kind : FacilityKind
  name : String
  footprint_area_m2 : Double
  storage_depth_mm : Double
  infiltration_rate_mm_per_hr : Double
  drain_delay_hours : Double
  pretreatment_score : Double
  vegetation_score : Double
  max_capture_ratio : Double
  overflow_priority : Int
} derive(Eq, Debug)

///|
pub(all) struct FacilityContribution {
  name : String
  kind : FacilityKind
  footprint_area_m2 : Double
  storage_capacity_m3 : Double
  event_runoff_m3 : Double
  captured_volume_m3 : Double
  bypass_volume_m3 : Double
  capture_ratio : Double
  drawdown_hours : Double
  resilience_score : Int
  status : PlanStatus
  notes : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct TreatmentTrainPlan {
  event_name : String
  facility_count : Int
  runoff_volume_m3 : Double
  footprint_area_m2 : Double
  total_capacity_m3 : Double
  total_captured_m3 : Double
  bypass_volume_m3 : Double
  capture_ratio : Double
  weighted_drawdown_hours : Double
  resilience_score : Int
  status : PlanStatus
  contributions : Array[FacilityContribution]
  notes : Array[String]
} derive(Eq, Debug)

///|
pub fn facility_template(
  kind : FacilityKind,
  name : String,
  footprint_area_m2 : Double,
) -> FacilitySpec {
  match kind {
    RainGarden =>
      facility_spec(
        kind, name, footprint_area_m2, 260.0, 25.0, 0.0, 0.75, 0.85, 1.00, 3,
      )
    Bioswale =>
      facility_spec(
        kind, name, footprint_area_m2, 180.0, 18.0, 1.0, 0.85, 0.75, 0.85, 2,
      )
    PermeablePavement =>
      facility_spec(
        kind, name, footprint_area_m2, 140.0, 10.0, 2.0, 0.60, 0.20, 0.70, 5,
      )
    Cistern =>
      facility_spec(
        kind, name, footprint_area_m2, 900.0, 2.0, 36.0, 0.50, 0.10, 0.95, 1,
      )
    GreenRoof =>
      facility_spec(
        kind, name, footprint_area_m2, 90.0, 8.0, 4.0, 0.70, 0.80, 0.55, 4,
      )
    TreeTrench =>
      facility_spec(
        kind, name, footprint_area_m2, 220.0, 16.0, 2.0, 0.80, 0.90, 0.80, 2,
      )
    DryWell =>
      facility_spec(
        kind, name, footprint_area_m2, 700.0, 30.0, 6.0, 0.35, 0.00, 0.90, 1,
      )
  }
}

///|
pub fn facility_with_hydraulics(
  kind : FacilityKind,
  name : String,
  footprint_area_m2 : Double,
  storage_depth_mm : Double,
  infiltration_rate_mm_per_hr : Double,
  drain_delay_hours : Double,
  pretreatment_score : Double,
  vegetation_score : Double,
  max_capture_ratio : Double,
  overflow_priority : Int,
) -> FacilitySpec {
  facility_spec(
    kind, name, footprint_area_m2, storage_depth_mm, infiltration_rate_mm_per_hr,
    drain_delay_hours, pretreatment_score, vegetation_score, max_capture_ratio, overflow_priority,
  )
}

///|
pub fn facility_storage_capacity_m3(
  facility : FacilitySpec,
) -> Result[Double, SpongeError] {
  match validate_facility(facility) {
    Some(error) => Err(error)
    None => Ok(facility.footprint_area_m2 * facility.storage_depth_mm / 1000.0)
  }
}

///|
pub fn evaluate_facility(
  event_runoff_m3 : Double,
  facility : FacilitySpec,
) -> Result[FacilityContribution, SpongeError] {
  if event_runoff_m3 < 0.0 {
    return Err(InvalidScenarioInput)
  }
  match validate_facility(facility) {
    Some(error) => Err(error)
    None => {
      let capacity = facility.footprint_area_m2 *
        facility.storage_depth_mm /
        1000.0
      let max_event_capture = event_runoff_m3 * facility.max_capture_ratio
      let captured = facility_min_double(capacity, max_event_capture)
      let bypass = if event_runoff_m3 <= captured {
        0.0
      } else {
        event_runoff_m3 - captured
      }
      let capture_ratio = if event_runoff_m3 <= 0.0 {
        1.0
      } else {
        captured / event_runoff_m3
      }
      let drawdown = facility_drawdown_hours(facility)
      let notes = facility_notes(facility, event_runoff_m3, captured, drawdown)
      let status = facility_status(capture_ratio, drawdown, notes)
      let resilience = facility_resilience_score(
        facility,
        capture_ratio,
        drawdown,
        notes.length(),
      )
      Ok({
        name: facility.name,
        kind: facility.kind,
        footprint_area_m2: facility.footprint_area_m2,
        storage_capacity_m3: capacity,
        event_runoff_m3,
        captured_volume_m3: captured,
        bypass_volume_m3: bypass,
        capture_ratio,
        drawdown_hours: drawdown,
        resilience_score: resilience,
        status,
        notes,
      })
    }
  }
}

///|
pub fn evaluate_treatment_train(
  catchments : Array[Catchment],
  event : RainEvent,
  facilities : Array[FacilitySpec],
) -> Result[TreatmentTrainPlan, SpongeError] {
  if facilities.length() == 0 {
    return Err(InvalidFacilitySpec)
  }
  match total_runoff_volume(catchments, event) {
    Err(error) => Err(error)
    Ok(runoff) => {
      let contributions : Array[FacilityContribution] = []
      let train_notes : Array[String] = []
      let mut remaining = runoff.volume_m3
      let mut footprint = 0.0
      let mut total_capacity = 0.0
      let mut captured_total = 0.0
      let mut weighted_drawdown = 0.0
      let mut resilience_sum = 0
      let mut review_count = 0
      for facility in facilities {
        match evaluate_facility(remaining, facility) {
          Err(error) => return Err(error)
          Ok(contribution) => {
            contributions.push(contribution)
            remaining = contribution.bypass_volume_m3
            footprint = footprint + contribution.footprint_area_m2
            total_capacity = total_capacity + contribution.storage_capacity_m3
            captured_total = captured_total + contribution.captured_volume_m3
            weighted_drawdown = weighted_drawdown +
              contribution.drawdown_hours * contribution.captured_volume_m3
            resilience_sum = resilience_sum + contribution.resilience_score
            if contribution.status != Ready {
              review_count = review_count + 1
            }
          }
        }
      }
      let capture_ratio = if runoff.volume_m3 <= 0.0 {
        1.0
      } else {
        captured_total / runoff.volume_m3
      }
      let normalized_drawdown = if captured_total <= 0.0 {
        0.0
      } else {
        weighted_drawdown / captured_total
      }
      let resilience = if facilities.length() == 0 {
        0
      } else {
        resilience_sum / facilities.length()
      }
      if runoff.volume_m3 <= 0.0 {
        train_notes.push("event creates no runoff for treatment train")
      }
      if capture_ratio < 0.80 {
        train_notes.push("treatment train captures less than eighty percent")
      } else if capture_ratio < 0.90 {
        train_notes.push("treatment train is near the common capture target")
      }
      if remaining > 0.0 {
        train_notes.push("overflow remains after the final facility")
      }
      if review_count > 0 {
        train_notes.push("one or more facilities need review")
      }
      let status = if train_notes.length() == 0 { Ready } else { NeedsReview }
      Ok({
        event_name: event.name,
        facility_count: facilities.length(),
        runoff_volume_m3: runoff.volume_m3,
        footprint_area_m2: footprint,
        total_capacity_m3: total_capacity,
        total_captured_m3: captured_total,
        bypass_volume_m3: remaining,
        capture_ratio,
        weighted_drawdown_hours: normalized_drawdown,
        resilience_score: resilience,
        status,
        contributions,
        notes: train_notes,
      })
    }
  }
}

///|
pub fn recommend_facility_mix(
  catchments : Array[Catchment],
  event : RainEvent,
  available_area_m2 : Double,
) -> Result[TreatmentTrainPlan, SpongeError] {
  if available_area_m2 <= 0.0 {
    return Err(InvalidFacilitySpec)
  }
  let facilities = if available_area_m2 < 12.0 {
    [
      facility_template(
        RainGarden,
        "compact rain garden",
        available_area_m2 * 0.60,
      ),
      facility_template(Cistern, "overflow cistern", available_area_m2 * 0.40),
    ]
  } else if available_area_m2 < 35.0 {
    [
      facility_template(
        RainGarden,
        "rain garden cell",
        available_area_m2 * 0.50,
      ),
      facility_template(TreeTrench, "tree trench", available_area_m2 * 0.30),
      facility_template(
        PermeablePavement,
        "pavement apron",
        available_area_m2 * 0.20,
      ),
    ]
  } else {
    [
      facility_template(
        Bioswale,
        "street edge bioswale",
        available_area_m2 * 0.35,
      ),
      facility_template(
        RainGarden,
        "central rain garden",
        available_area_m2 * 0.35,
      ),
      facility_template(
        PermeablePavement,
        "permeable overflow path",
        available_area_m2 * 0.20,
      ),
      facility_template(
        TreeTrench,
        "tree trench reserve",
        available_area_m2 * 0.10,
      ),
    ]
  }
  evaluate_treatment_train(catchments, event, facilities)
}

///|
pub fn FacilityKind::label(self : FacilityKind) -> String {
  match self {
    RainGarden => "rain_garden"
    Bioswale => "bioswale"
    PermeablePavement => "permeable_pavement"
    Cistern => "cistern"
    GreenRoof => "green_roof"
    TreeTrench => "tree_trench"
    DryWell => "dry_well"
  }
}

///|
pub fn FacilitySpec::to_json(self : FacilitySpec) -> 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(
    "\"footprint_area_m2\":" + self.footprint_area_m2.to_string() + ",",
  )
  out.write_string(
    "\"storage_depth_mm\":" + self.storage_depth_mm.to_string() + ",",
  )
  out.write_string(
    "\"infiltration_rate_mm_per_hr\":" +
    self.infiltration_rate_mm_per_hr.to_string() +
    ",",
  )
  out.write_string(
    "\"drain_delay_hours\":" + self.drain_delay_hours.to_string() + ",",
  )
  out.write_string(
    "\"pretreatment_score\":" + self.pretreatment_score.to_string() + ",",
  )
  out.write_string(
    "\"vegetation_score\":" + self.vegetation_score.to_string() + ",",
  )
  out.write_string(
    "\"max_capture_ratio\":" + self.max_capture_ratio.to_string() + ",",
  )
  out.write_string(
    "\"overflow_priority\":" + self.overflow_priority.to_string(),
  )
  out.write_string("}")
  out.to_string()
}

///|
pub fn FacilityContribution::to_json(self : FacilityContribution) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"name\":\"" + escape_json(self.name) + "\",")
  out.write_string("\"kind\":\"" + self.kind.label() + "\",")
  out.write_string(
    "\"footprint_area_m2\":" + self.footprint_area_m2.to_string() + ",",
  )
  out.write_string(
    "\"storage_capacity_m3\":" + self.storage_capacity_m3.to_string() + ",",
  )
  out.write_string(
    "\"event_runoff_m3\":" + self.event_runoff_m3.to_string() + ",",
  )
  out.write_string(
    "\"captured_volume_m3\":" + self.captured_volume_m3.to_string() + ",",
  )
  out.write_string(
    "\"bypass_volume_m3\":" + self.bypass_volume_m3.to_string() + ",",
  )
  out.write_string("\"capture_ratio\":" + self.capture_ratio.to_string() + ",")
  out.write_string(
    "\"drawdown_hours\":" + self.drawdown_hours.to_string() + ",",
  )
  out.write_string(
    "\"resilience_score\":" + self.resilience_score.to_string() + ",",
  )
  out.write_string("\"status\":\"" + self.status.label() + "\",")
  out.write_string("\"notes\":" + facility_string_array_to_json(self.notes))
  out.write_string("}")
  out.to_string()
}

///|
pub fn TreatmentTrainPlan::to_json(self : TreatmentTrainPlan) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"event\":\"" + escape_json(self.event_name) + "\",")
  out.write_string(
    "\"facility_count\":" + self.facility_count.to_string() + ",",
  )
  out.write_string(
    "\"runoff_volume_m3\":" + self.runoff_volume_m3.to_string() + ",",
  )
  out.write_string(
    "\"footprint_area_m2\":" + self.footprint_area_m2.to_string() + ",",
  )
  out.write_string(
    "\"total_capacity_m3\":" + self.total_capacity_m3.to_string() + ",",
  )
  out.write_string(
    "\"total_captured_m3\":" + self.total_captured_m3.to_string() + ",",
  )
  out.write_string(
    "\"bypass_volume_m3\":" + self.bypass_volume_m3.to_string() + ",",
  )
  out.write_string("\"capture_ratio\":" + self.capture_ratio.to_string() + ",")
  out.write_string(
    "\"weighted_drawdown_hours\":" +
    self.weighted_drawdown_hours.to_string() +
    ",",
  )
  out.write_string(
    "\"resilience_score\":" + self.resilience_score.to_string() + ",",
  )
  out.write_string("\"status\":\"" + self.status.label() + "\",")
  out.write_string(
    "\"notes\":" + facility_string_array_to_json(self.notes) + ",",
  )
  out.write_string("\"contributions\":[")
  for index, contribution in self.contributions {
    if index > 0 {
      out.write_string(",")
    }
    out.write_string(contribution.to_json())
  }
  out.write_string("]}")
  out.to_string()
}

///|
pub fn TreatmentTrainPlan::to_markdown(self : TreatmentTrainPlan) -> String {
  let out = StringBuilder::new()
  out.write_string("# SpongeKit Treatment Train\n\n")
  out.write_string("- Event: " + self.event_name + "\n")
  out.write_string("- Status: " + self.status.label() + "\n")
  out.write_string("- Facilities: " + self.facility_count.to_string() + "\n")
  out.write_string(
    "- Footprint: " + self.footprint_area_m2.to_string() + " m2\n",
  )
  out.write_string("- Runoff: " + self.runoff_volume_m3.to_string() + " m3\n")
  out.write_string(
    "- Captured: " + self.total_captured_m3.to_string() + " m3\n",
  )
  out.write_string("- Bypass: " + self.bypass_volume_m3.to_string() + " m3\n")
  out.write_string("- Capture ratio: " + self.capture_ratio.to_string() + "\n")
  out.write_string(
    "- Resilience score: " + self.resilience_score.to_string() + "\n\n",
  )
  out.write_string(
    "| Facility | Kind | Area m2 | Capture | Bypass m3 | Score |\n",
  )
  out.write_string("| --- | --- | ---: | ---: | ---: | ---: |\n")
  for contribution in self.contributions {
    out.write_string(
      "| " +
      contribution.name +
      " | " +
      contribution.kind.label() +
      " | " +
      contribution.footprint_area_m2.to_string() +
      " | " +
      contribution.capture_ratio.to_string() +
      " | " +
      contribution.bypass_volume_m3.to_string() +
      " | " +
      contribution.resilience_score.to_string() +
      " |\n",
    )
  }
  if self.notes.length() > 0 {
    out.write_string("\n## Notes\n\n")
    for note in self.notes {
      out.write_string("- " + note + "\n")
    }
  }
  out.to_string()
}

///|
fn facility_spec(
  kind : FacilityKind,
  name : String,
  footprint_area_m2 : Double,
  storage_depth_mm : Double,
  infiltration_rate_mm_per_hr : Double,
  drain_delay_hours : Double,
  pretreatment_score : Double,
  vegetation_score : Double,
  max_capture_ratio : Double,
  overflow_priority : Int,
) -> FacilitySpec {
  {
    kind,
    name,
    footprint_area_m2,
    storage_depth_mm,
    infiltration_rate_mm_per_hr,
    drain_delay_hours,
    pretreatment_score,
    vegetation_score,
    max_capture_ratio,
    overflow_priority,
  }
}

///|
fn validate_facility(facility : FacilitySpec) -> SpongeError? {
  if facility.footprint_area_m2 <= 0.0 {
    Some(InvalidFacilitySpec)
  } else if facility.storage_depth_mm <= 0.0 {
    Some(InvalidFacilitySpec)
  } else if facility.infiltration_rate_mm_per_hr <= 0.0 {
    Some(InvalidInfiltrationRate)
  } else if facility.drain_delay_hours < 0.0 {
    Some(InvalidFacilitySpec)
  } else if facility.pretreatment_score < 0.0 ||
    facility.pretreatment_score > 1.0 {
    Some(InvalidFacilitySpec)
  } else if facility.vegetation_score < 0.0 || facility.vegetation_score > 1.0 {
    Some(InvalidFacilitySpec)
  } else if facility.max_capture_ratio <= 0.0 ||
    facility.max_capture_ratio > 1.0 {
    Some(InvalidFacilitySpec)
  } else if facility.overflow_priority < 0 {
    Some(InvalidFacilitySpec)
  } else {
    None
  }
}

///|
fn facility_drawdown_hours(facility : FacilitySpec) -> Double {
  facility.storage_depth_mm / facility.infiltration_rate_mm_per_hr +
  facility.drain_delay_hours
}

///|
fn facility_notes(
  facility : FacilitySpec,
  event_runoff_m3 : Double,
  captured_volume_m3 : Double,
  drawdown_hours : Double,
) -> Array[String] {
  let notes : Array[String] = []
  if event_runoff_m3 <= 0.0 {
    notes.push("no runoff is routed to this facility")
  }
  if captured_volume_m3 <= 0.0 && event_runoff_m3 > 0.0 {
    notes.push("facility captures no volume under this event")
  }
  if drawdown_hours > 48.0 {
    notes.push("drawdown exceeds forty eight hours")
  } else if drawdown_hours > 24.0 {
    notes.push("drawdown exceeds twenty four hours")
  }
  if facility.pretreatment_score < 0.40 {
    notes.push("pretreatment is weak for sediment control")
  }
  if facility.vegetation_score < 0.30 &&
    facility.kind != Cistern &&
    facility.kind != DryWell {
    notes.push("vegetation benefit is limited")
  }
  if facility.storage_depth_mm > 750.0 && facility.kind != Cistern {
    notes.push("storage depth may need constructability review")
  }
  notes
}

///|
fn facility_status(
  capture_ratio : Double,
  drawdown_hours : Double,
  notes : Array[String],
) -> PlanStatus {
  if drawdown_hours > 48.0 {
    NeedsReview
  } else if capture_ratio <= 0.0 {
    NeedsReview
  } else if notes.length() >= 3 {
    NeedsReview
  } else {
    Ready
  }
}

///|
fn facility_resilience_score(
  facility : FacilitySpec,
  capture_ratio : Double,
  drawdown_hours : Double,
  note_count : Int,
) -> Int {
  let capture_points = (facility_clamp_double(capture_ratio, 0.0, 1.0) * 45.0).to_int()
  let pretreatment_points = (facility.pretreatment_score * 20.0).to_int()
  let vegetation_points = (facility.vegetation_score * 15.0).to_int()
  let drawdown_points = if drawdown_hours <= 12.0 {
    15
  } else if drawdown_hours <= 24.0 {
    10
  } else if drawdown_hours <= 48.0 {
    4
  } else {
    0
  }
  let priority_bonus = if facility.overflow_priority <= 1 {
    5
  } else if facility.overflow_priority <= 3 {
    3
  } else {
    0
  }
  facility_clamp_int(
    capture_points +
    pretreatment_points +
    vegetation_points +
    drawdown_points +
    priority_bonus -
    note_count * 4,
    0,
    100,
  )
}

///|
fn facility_min_double(left : Double, right : Double) -> Double {
  if left < right {
    left
  } else {
    right
  }
}

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

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

///|
fn facility_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()
}