///|
pub(all) enum CostTier {
  Economy
  Standard
  Durable
  Showcase
} derive(Eq, Debug)

///|
pub(all) struct CostProfile {
  tier : CostTier
  currency : String
  construction_per_m2 : Double
  excavation_per_m3 : Double
  media_per_m3 : Double
  planting_per_m2 : Double
  pretreatment_each : Double
  inspection_per_visit : Double
  sediment_service_per_visit : Double
  annual_maintenance_rate : Double
  design_markup_rate : Double
  contingency_rate : Double
  lifespan_years : Int
} derive(Eq, Debug)

///|
pub(all) struct FacilityCost {
  name : String
  kind : FacilityKind
  currency : String
  footprint_area_m2 : Double
  capital_cost : Double
  annual_maintenance_cost : Double
  lifecycle_cost : Double
  annualized_cost : Double
  cost_per_captured_m3 : Double
  recommended_visits_per_year : Int
  notes : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct TrainCostSummary {
  currency : String
  facility_count : Int
  total_capital_cost : Double
  annual_maintenance_cost : Double
  lifecycle_cost : Double
  annualized_cost : Double
  captured_volume_m3 : Double
  cost_per_captured_m3 : Double
  highest_cost_name : String
  highest_cost : Double
  items : Array[FacilityCost]
  notes : Array[String]
} derive(Eq, Debug)

///|
pub fn default_cost_profile() -> CostProfile {
  cost_profile(Standard)
}

///|
pub fn cost_profile(tier : CostTier) -> CostProfile {
  match tier {
    Economy =>
      cost_profile_detail(
        tier, "CNY", 380.0, 90.0, 240.0, 55.0, 800.0, 120.0, 420.0, 0.035, 0.08,
        0.10, 10,
      )
    Standard =>
      cost_profile_detail(
        tier, "CNY", 520.0, 120.0, 320.0, 85.0, 1200.0, 180.0, 650.0, 0.045, 0.10,
        0.12, 12,
      )
    Durable =>
      cost_profile_detail(
        tier, "CNY", 720.0, 160.0, 420.0, 120.0, 1800.0, 220.0, 760.0, 0.040, 0.12,
        0.15, 15,
      )
    Showcase =>
      cost_profile_detail(
        tier, "CNY", 960.0, 210.0, 560.0, 180.0, 2600.0, 280.0, 920.0, 0.055, 0.15,
        0.18, 15,
      )
  }
}

///|
pub fn custom_cost_profile(
  currency : String,
  construction_per_m2 : Double,
  excavation_per_m3 : Double,
  media_per_m3 : Double,
  planting_per_m2 : Double,
  pretreatment_each : Double,
  inspection_per_visit : Double,
  sediment_service_per_visit : Double,
  annual_maintenance_rate : Double,
  design_markup_rate : Double,
  contingency_rate : Double,
  lifespan_years : Int,
) -> CostProfile {
  cost_profile_detail(
    Standard,
    currency,
    construction_per_m2,
    excavation_per_m3,
    media_per_m3,
    planting_per_m2,
    pretreatment_each,
    inspection_per_visit,
    sediment_service_per_visit,
    annual_maintenance_rate,
    design_markup_rate,
    contingency_rate,
    lifespan_years,
  )
}

///|
pub fn estimate_facility_cost(
  contribution : FacilityContribution,
  profile : CostProfile,
) -> Result[FacilityCost, SpongeError] {
  match validate_cost_profile(profile) {
    Some(error) => Err(error)
    None => {
      let storage_depth_m = storage_depth_from_contribution(contribution)
      let storage_volume_m3 = contribution.storage_capacity_m3
      let base = contribution.footprint_area_m2 * profile.construction_per_m2
      let excavation = storage_volume_m3 * profile.excavation_per_m3
      let media = storage_volume_m3 *
        profile.media_per_m3 *
        media_multiplier(contribution.kind)
      let planting = contribution.footprint_area_m2 *
        profile.planting_per_m2 *
        planting_multiplier(contribution.kind)
      let pretreatment = profile.pretreatment_each *
        pretreatment_multiplier(contribution.kind)
      let direct_cost = base + excavation + media + planting + pretreatment
      let markup = direct_cost *
        (profile.design_markup_rate + profile.contingency_rate)
      let capital = direct_cost + markup
      let visits = recommended_visits(contribution)
      let visit_cost = visits.to_double() * profile.inspection_per_visit
      let sediment_cost = profile.sediment_service_per_visit *
        sediment_service_multiplier(contribution)
      let annual_maintenance = capital * profile.annual_maintenance_rate +
        visit_cost +
        sediment_cost
      let lifecycle = capital +
        annual_maintenance * profile.lifespan_years.to_double()
      let annualized = if profile.lifespan_years <= 0 {
        lifecycle
      } else {
        lifecycle / profile.lifespan_years.to_double()
      }
      let cost_per_capture = if contribution.captured_volume_m3 <= 0.0 {
        lifecycle
      } else {
        lifecycle / contribution.captured_volume_m3
      }
      Ok({
        name: contribution.name,
        kind: contribution.kind,
        currency: profile.currency,
        footprint_area_m2: contribution.footprint_area_m2,
        capital_cost: capital,
        annual_maintenance_cost: annual_maintenance,
        lifecycle_cost: lifecycle,
        annualized_cost: annualized,
        cost_per_captured_m3: cost_per_capture,
        recommended_visits_per_year: visits,
        notes: cost_notes(contribution, storage_depth_m, cost_per_capture),
      })
    }
  }
}

///|
pub fn estimate_treatment_train_cost(
  plan : TreatmentTrainPlan,
  profile : CostProfile,
) -> Result[TrainCostSummary, SpongeError] {
  match validate_cost_profile(profile) {
    Some(error) => Err(error)
    None => {
      let items : Array[FacilityCost] = []
      let notes : Array[String] = []
      let mut capital = 0.0
      let mut annual = 0.0
      let mut lifecycle = 0.0
      let mut highest_name = ""
      let mut highest_cost = -1.0
      for contribution in plan.contributions {
        match estimate_facility_cost(contribution, profile) {
          Err(error) => return Err(error)
          Ok(item) => {
            if item.capital_cost > highest_cost {
              highest_name = item.name
              highest_cost = item.capital_cost
            }
            capital = capital + item.capital_cost
            annual = annual + item.annual_maintenance_cost
            lifecycle = lifecycle + item.lifecycle_cost
            items.push(item)
          }
        }
      }
      if plan.capture_ratio < 0.80 {
        notes.push("capital plan has low hydrologic efficiency")
      }
      if plan.bypass_volume_m3 > 0.0 {
        notes.push("cost summary includes an overflow remainder")
      }
      if highest_cost > capital * 0.60 && items.length() > 1 {
        notes.push("one facility dominates capital cost")
      }
      let annualized = if profile.lifespan_years <= 0 {
        lifecycle
      } else {
        lifecycle / profile.lifespan_years.to_double()
      }
      let cost_per_capture = if plan.total_captured_m3 <= 0.0 {
        lifecycle
      } else {
        lifecycle / plan.total_captured_m3
      }
      Ok({
        currency: profile.currency,
        facility_count: items.length(),
        total_capital_cost: capital,
        annual_maintenance_cost: annual,
        lifecycle_cost: lifecycle,
        annualized_cost: annualized,
        captured_volume_m3: plan.total_captured_m3,
        cost_per_captured_m3: cost_per_capture,
        highest_cost_name: highest_name,
        highest_cost,
        items,
        notes,
      })
    }
  }
}

///|
pub fn budget_fit_score(
  summary : TrainCostSummary,
  capital_budget : Double,
  annual_budget : Double,
) -> Result[Int, SpongeError] {
  if capital_budget <= 0.0 || annual_budget <= 0.0 {
    return Err(InvalidCostProfile)
  }
  let capital_ratio = summary.total_capital_cost / capital_budget
  let annual_ratio = summary.annual_maintenance_cost / annual_budget
  let capture_bonus = if summary.captured_volume_m3 >= 10.0 {
    15
  } else if summary.captured_volume_m3 >= 5.0 {
    10
  } else {
    5
  }
  let capital_points = ratio_points(capital_ratio, 45)
  let annual_points = ratio_points(annual_ratio, 35)
  Ok(cost_clamp_int(capital_points + annual_points + capture_bonus, 0, 100))
}

///|
pub fn CostTier::label(self : CostTier) -> String {
  match self {
    Economy => "economy"
    Standard => "standard"
    Durable => "durable"
    Showcase => "showcase"
  }
}

///|
pub fn CostProfile::to_json(self : CostProfile) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"tier\":\"" + self.tier.label() + "\",")
  out.write_string("\"currency\":\"" + escape_json(self.currency) + "\",")
  out.write_string(
    "\"construction_per_m2\":" + self.construction_per_m2.to_string() + ",",
  )
  out.write_string(
    "\"excavation_per_m3\":" + self.excavation_per_m3.to_string() + ",",
  )
  out.write_string("\"media_per_m3\":" + self.media_per_m3.to_string() + ",")
  out.write_string(
    "\"planting_per_m2\":" + self.planting_per_m2.to_string() + ",",
  )
  out.write_string(
    "\"pretreatment_each\":" + self.pretreatment_each.to_string() + ",",
  )
  out.write_string(
    "\"inspection_per_visit\":" + self.inspection_per_visit.to_string() + ",",
  )
  out.write_string(
    "\"sediment_service_per_visit\":" +
    self.sediment_service_per_visit.to_string() +
    ",",
  )
  out.write_string(
    "\"annual_maintenance_rate\":" +
    self.annual_maintenance_rate.to_string() +
    ",",
  )
  out.write_string(
    "\"design_markup_rate\":" + self.design_markup_rate.to_string() + ",",
  )
  out.write_string(
    "\"contingency_rate\":" + self.contingency_rate.to_string() + ",",
  )
  out.write_string("\"lifespan_years\":" + self.lifespan_years.to_string())
  out.write_string("}")
  out.to_string()
}

///|
pub fn FacilityCost::to_json(self : FacilityCost) -> 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("\"currency\":\"" + escape_json(self.currency) + "\",")
  out.write_string(
    "\"footprint_area_m2\":" + self.footprint_area_m2.to_string() + ",",
  )
  out.write_string("\"capital_cost\":" + self.capital_cost.to_string() + ",")
  out.write_string(
    "\"annual_maintenance_cost\":" +
    self.annual_maintenance_cost.to_string() +
    ",",
  )
  out.write_string(
    "\"lifecycle_cost\":" + self.lifecycle_cost.to_string() + ",",
  )
  out.write_string(
    "\"annualized_cost\":" + self.annualized_cost.to_string() + ",",
  )
  out.write_string(
    "\"cost_per_captured_m3\":" + self.cost_per_captured_m3.to_string() + ",",
  )
  out.write_string(
    "\"recommended_visits_per_year\":" +
    self.recommended_visits_per_year.to_string() +
    ",",
  )
  out.write_string("\"notes\":" + cost_string_array_to_json(self.notes))
  out.write_string("}")
  out.to_string()
}

///|
pub fn TrainCostSummary::to_json(self : TrainCostSummary) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"currency\":\"" + escape_json(self.currency) + "\",")
  out.write_string(
    "\"facility_count\":" + self.facility_count.to_string() + ",",
  )
  out.write_string(
    "\"total_capital_cost\":" + self.total_capital_cost.to_string() + ",",
  )
  out.write_string(
    "\"annual_maintenance_cost\":" +
    self.annual_maintenance_cost.to_string() +
    ",",
  )
  out.write_string(
    "\"lifecycle_cost\":" + self.lifecycle_cost.to_string() + ",",
  )
  out.write_string(
    "\"annualized_cost\":" + self.annualized_cost.to_string() + ",",
  )
  out.write_string(
    "\"captured_volume_m3\":" + self.captured_volume_m3.to_string() + ",",
  )
  out.write_string(
    "\"cost_per_captured_m3\":" + self.cost_per_captured_m3.to_string() + ",",
  )
  out.write_string(
    "\"highest_cost_name\":\"" + escape_json(self.highest_cost_name) + "\",",
  )
  out.write_string("\"highest_cost\":" + self.highest_cost.to_string() + ",")
  out.write_string("\"notes\":" + cost_string_array_to_json(self.notes) + ",")
  out.write_string("\"items\":[")
  for index, item in self.items {
    if index > 0 {
      out.write_string(",")
    }
    out.write_string(item.to_json())
  }
  out.write_string("]}")
  out.to_string()
}

///|
pub fn TrainCostSummary::to_markdown(self : TrainCostSummary) -> String {
  let out = StringBuilder::new()
  out.write_string("# SpongeKit Cost Summary\n\n")
  out.write_string("- Currency: " + self.currency + "\n")
  out.write_string("- Facilities: " + self.facility_count.to_string() + "\n")
  out.write_string(
    "- Capital cost: " + self.total_capital_cost.to_string() + "\n",
  )
  out.write_string(
    "- Annual maintenance: " + self.annual_maintenance_cost.to_string() + "\n",
  )
  out.write_string(
    "- Lifecycle cost: " + self.lifecycle_cost.to_string() + "\n",
  )
  out.write_string(
    "- Cost per captured m3: " + self.cost_per_captured_m3.to_string() + "\n\n",
  )
  out.write_string("| Facility | Kind | Capital | Annual O&M | Visits/year |\n")
  out.write_string("| --- | --- | ---: | ---: | ---: |\n")
  for item in self.items {
    out.write_string(
      "| " +
      item.name +
      " | " +
      item.kind.label() +
      " | " +
      item.capital_cost.to_string() +
      " | " +
      item.annual_maintenance_cost.to_string() +
      " | " +
      item.recommended_visits_per_year.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 cost_profile_detail(
  tier : CostTier,
  currency : String,
  construction_per_m2 : Double,
  excavation_per_m3 : Double,
  media_per_m3 : Double,
  planting_per_m2 : Double,
  pretreatment_each : Double,
  inspection_per_visit : Double,
  sediment_service_per_visit : Double,
  annual_maintenance_rate : Double,
  design_markup_rate : Double,
  contingency_rate : Double,
  lifespan_years : Int,
) -> CostProfile {
  {
    tier,
    currency,
    construction_per_m2,
    excavation_per_m3,
    media_per_m3,
    planting_per_m2,
    pretreatment_each,
    inspection_per_visit,
    sediment_service_per_visit,
    annual_maintenance_rate,
    design_markup_rate,
    contingency_rate,
    lifespan_years,
  }
}

///|
fn validate_cost_profile(profile : CostProfile) -> SpongeError? {
  if profile.construction_per_m2 < 0.0 ||
    profile.excavation_per_m3 < 0.0 ||
    profile.media_per_m3 < 0.0 ||
    profile.planting_per_m2 < 0.0 ||
    profile.pretreatment_each < 0.0 ||
    profile.inspection_per_visit < 0.0 ||
    profile.sediment_service_per_visit < 0.0 {
    Some(InvalidCostProfile)
  } else if profile.annual_maintenance_rate < 0.0 ||
    profile.design_markup_rate < 0.0 ||
    profile.contingency_rate < 0.0 {
    Some(InvalidCostProfile)
  } else if profile.lifespan_years <= 0 {
    Some(InvalidCostProfile)
  } else {
    None
  }
}

///|
fn media_multiplier(kind : FacilityKind) -> Double {
  match kind {
    RainGarden => 1.00
    Bioswale => 0.85
    PermeablePavement => 1.15
    Cistern => 0.35
    GreenRoof => 1.25
    TreeTrench => 1.10
    DryWell => 0.70
  }
}

///|
fn planting_multiplier(kind : FacilityKind) -> Double {
  match kind {
    RainGarden => 1.00
    Bioswale => 0.90
    PermeablePavement => 0.10
    Cistern => 0.05
    GreenRoof => 1.35
    TreeTrench => 1.20
    DryWell => 0.20
  }
}

///|
fn pretreatment_multiplier(kind : FacilityKind) -> Double {
  match kind {
    RainGarden => 1.00
    Bioswale => 1.15
    PermeablePavement => 0.75
    Cistern => 0.90
    GreenRoof => 0.45
    TreeTrench => 1.20
    DryWell => 0.65
  }
}

///|
fn sediment_service_multiplier(contribution : FacilityContribution) -> Double {
  if contribution.status != Ready {
    1.20
  } else if contribution.kind == Bioswale || contribution.kind == TreeTrench {
    1.05
  } else if contribution.kind == Cistern {
    0.75
  } else {
    0.90
  }
}

///|
fn recommended_visits(contribution : FacilityContribution) -> Int {
  if contribution.status != Ready {
    6
  } else if contribution.kind == Cistern {
    3
  } else if contribution.kind == PermeablePavement {
    4
  } else if contribution.resilience_score < 60 {
    5
  } else {
    4
  }
}

///|
fn storage_depth_from_contribution(
  contribution : FacilityContribution,
) -> Double {
  if contribution.footprint_area_m2 <= 0.0 {
    0.0
  } else {
    contribution.storage_capacity_m3 / contribution.footprint_area_m2
  }
}

///|
fn cost_notes(
  contribution : FacilityContribution,
  storage_depth_m : Double,
  cost_per_capture : Double,
) -> Array[String] {
  let notes : Array[String] = []
  if contribution.status != Ready {
    notes.push("hydraulic review can increase construction contingency")
  }
  if storage_depth_m > 0.70 && contribution.kind != Cistern {
    notes.push("deep storage may require excavation review")
  }
  if cost_per_capture > 50000.0 {
    notes.push("high cost per captured cubic meter")
  }
  if contribution.footprint_area_m2 < 5.0 {
    notes.push("small facility has mobilization overhead risk")
  }
  notes
}

///|
fn ratio_points(ratio : Double, max_points : Int) -> Int {
  if ratio <= 0.70 {
    max_points
  } else if ratio <= 0.90 {
    (max_points.to_double() * 0.85).to_int()
  } else if ratio <= 1.00 {
    (max_points.to_double() * 0.70).to_int()
  } else if ratio <= 1.20 {
    (max_points.to_double() * 0.35).to_int()
  } else {
    0
  }
}

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

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