///|
pub(all) struct AreaCandidate {
  name : String
  area_m2 : Double
  max_cell_area_m2 : Double
} derive(Eq, Debug)

///|
pub(all) struct CandidatePerformance {
  name : String
  area_m2 : Double
  cell_count : Int
  cell_area_m2 : Double
  storage_capacity_m3 : Double
  runoff_volume_m3 : Double
  captured_volume_m3 : Double
  bypass_volume_m3 : Double
  capture_ratio : Double
  drawdown_hours : Double
  score : Int
  status : PlanStatus
  issues : Array[PlanIssue]
} derive(Eq, Debug)

///|
pub(all) struct CandidateComparison {
  candidate_count : Int
  best_name : String
  best_score : Int
  performances : Array[CandidatePerformance]
} derive(Eq, Debug)

///|
pub fn area_candidate(name : String, area_m2 : Double) -> AreaCandidate {
  { name, area_m2, max_cell_area_m2: 30.0 }
}

///|
pub fn area_candidate_with_cell_limit(
  name : String,
  area_m2 : Double,
  max_cell_area_m2 : Double,
) -> AreaCandidate {
  { name, area_m2, max_cell_area_m2 }
}

///|
pub fn evaluate_candidate_area(
  catchments : Array[Catchment],
  event : RainEvent,
  spec : GardenSpec,
  candidate : AreaCandidate,
) -> Result[CandidatePerformance, SpongeError] {
  if candidate.area_m2 <= 0.0 || candidate.max_cell_area_m2 <= 0.0 {
    return Err(InvalidCandidateArea)
  }
  match total_runoff_volume(catchments, event) {
    Err(error) => Err(error)
    Ok(runoff) =>
      match effective_storage_depth_mm(spec) {
        Err(error) => Err(error)
        Ok(storage_depth) => {
          let capacity = candidate.area_m2 * storage_depth / 1000.0
          let captured = min_double(capacity, runoff.volume_m3)
          let bypass = if runoff.volume_m3 <= capacity {
            0.0
          } else {
            runoff.volume_m3 - capacity
          }
          let capture = if runoff.volume_m3 == 0.0 {
            1.0
          } else {
            captured / runoff.volume_m3
          }
          let cell_count = ceil_to_int(
            candidate.area_m2 / candidate.max_cell_area_m2,
          )
          let cell_area = candidate.area_m2 / cell_count.to_double()
          let drawdown = storage_depth / spec.infiltration_rate_mm_per_hr
          let issues = candidate_issues(
            runoff.volume_m3,
            capture,
            cell_area,
            drawdown,
            spec,
          )
          let status = status_for_issues(issues)
          let score = candidate_score(capture, bypass, drawdown, issues)
          Ok({
            name: candidate.name,
            area_m2: candidate.area_m2,
            cell_count,
            cell_area_m2: cell_area,
            storage_capacity_m3: capacity,
            runoff_volume_m3: runoff.volume_m3,
            captured_volume_m3: captured,
            bypass_volume_m3: bypass,
            capture_ratio: capture,
            drawdown_hours: drawdown,
            score,
            status,
            issues,
          })
        }
      }
  }
}

///|
pub fn compare_candidate_areas(
  catchments : Array[Catchment],
  event : RainEvent,
  spec : GardenSpec,
  candidates : Array[AreaCandidate],
) -> Result[CandidateComparison, SpongeError] {
  if candidates.length() == 0 {
    return Err(InvalidCandidateArea)
  }
  let performances : Array[CandidatePerformance] = []
  let mut best_name = ""
  let mut best_score = -1
  for candidate in candidates {
    match evaluate_candidate_area(catchments, event, spec, candidate) {
      Err(error) => return Err(error)
      Ok(performance) => {
        if performance.score > best_score {
          best_name = performance.name
          best_score = performance.score
        }
        performances.push(performance)
      }
    }
  }
  Ok({
    candidate_count: candidates.length(),
    best_name,
    best_score,
    performances,
  })
}

///|
pub fn CandidatePerformance::to_json(self : CandidatePerformance) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string("\"name\":\"" + escape_json(self.name) + "\",")
  out.write_string("\"area_m2\":" + self.area_m2.to_string() + ",")
  out.write_string("\"cell_count\":" + self.cell_count.to_string() + ",")
  out.write_string("\"cell_area_m2\":" + self.cell_area_m2.to_string() + ",")
  out.write_string(
    "\"storage_capacity_m3\":" + self.storage_capacity_m3.to_string() + ",",
  )
  out.write_string(
    "\"runoff_volume_m3\":" + self.runoff_volume_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("\"score\":" + self.score.to_string() + ",")
  out.write_string("\"status\":\"" + self.status.label() + "\",")
  out.write_string("\"issues\":[")
  for index, issue in self.issues {
    if index > 0 {
      out.write_string(",")
    }
    out.write_string(candidate_issue_to_json(issue))
  }
  out.write_string("]}")
  out.to_string()
}

///|
pub fn CandidateComparison::to_json(self : CandidateComparison) -> String {
  let out = StringBuilder::new()
  out.write_string("{")
  out.write_string(
    "\"candidate_count\":" + self.candidate_count.to_string() + ",",
  )
  out.write_string("\"best_name\":\"" + escape_json(self.best_name) + "\",")
  out.write_string("\"best_score\":" + self.best_score.to_string() + ",")
  out.write_string("\"performances\":[")
  for index, performance in self.performances {
    if index > 0 {
      out.write_string(",")
    }
    out.write_string(performance.to_json())
  }
  out.write_string("]}")
  out.to_string()
}

///|
pub fn CandidateComparison::to_markdown(self : CandidateComparison) -> String {
  let out = StringBuilder::new()
  out.write_string("# SpongeKit Candidate Comparison\n\n")
  out.write_string("- Candidates: " + self.candidate_count.to_string() + "\n")
  out.write_string("- Best: " + self.best_name + "\n")
  out.write_string("- Best score: " + self.best_score.to_string() + "\n\n")
  out.write_string(
    "| Candidate | Area m2 | Capture | Bypass m3 | Score | Status |\n",
  )
  out.write_string("| --- | ---: | ---: | ---: | ---: | --- |\n")
  for performance in self.performances {
    out.write_string(
      "| " +
      performance.name +
      " | " +
      performance.area_m2.to_string() +
      " | " +
      performance.capture_ratio.to_string() +
      " | " +
      performance.bypass_volume_m3.to_string() +
      " | " +
      performance.score.to_string() +
      " | " +
      performance.status.label() +
      " |\n",
    )
  }
  out.to_string()
}

///|
fn candidate_issues(
  runoff_volume : Double,
  capture_ratio : Double,
  cell_area : Double,
  drawdown_hours : Double,
  spec : GardenSpec,
) -> Array[PlanIssue] {
  let issues : Array[PlanIssue] = []
  if runoff_volume == 0.0 {
    issues.push(
      issue(NoRunoff, "candidate receives no runoff under this event"),
    )
  }
  if capture_ratio < spec.target_capture_ratio {
    issues.push(
      issue(
        CaptureBelowTarget,
        "candidate area captures less than the target ratio",
      ),
    )
  }
  if drawdown_hours > spec.target_drawdown_hours {
    issues.push(
      issue(DrawdownTooSlow, "candidate drawdown exceeds the target window"),
    )
  }
  if cell_area > spec.max_cell_area_m2 {
    issues.push(
      issue(OversizedCell, "candidate cell split exceeds the cell limit"),
    )
  }
  issues
}

///|
fn candidate_score(
  capture_ratio : Double,
  bypass_volume : Double,
  drawdown_hours : Double,
  issues : Array[PlanIssue],
) -> Int {
  let capture_points = (clamp_double(capture_ratio, 0.0, 1.0) * 80.0).to_int()
  let bypass_penalty = if bypass_volume > 5.0 {
    12
  } else if bypass_volume > 1.0 {
    6
  } else if bypass_volume > 0.0 {
    3
  } else {
    0
  }
  let drawdown_bonus = if drawdown_hours <= 12.0 {
    15
  } else if drawdown_hours <= 24.0 {
    10
  } else {
    0
  }
  let issue_penalty = issues.length() * 5
  clamp_int(
    capture_points + drawdown_bonus + 5 - bypass_penalty - issue_penalty,
    0,
    100,
  )
}

///|
fn candidate_issue_to_json(issue : PlanIssue) -> String {
  "{\"kind\":\"" +
  issue.kind.label() +
  "\",\"message\":\"" +
  escape_json(issue.message) +
  "\"}"
}