///|
/// A viewing scale derived from one observed H1 interval or the final cutoff.
/// The interval index refers to Analysis.intervals and may be passed to slice.
pub struct ScaleRecommendation {
  scale : Double
  interval_index : Int?
  reason : String
  observed_lifetime : Double?
  censored : Bool
} derive(@debug.Debug)

///|
pub extend ScaleRecommendation with @debug.Debug::{to_repr}

///|
/// Rank H1 classes by observed lifetime, then select a scale within each
/// interval. For alive classes, the observed lifetime is only a lower bound.
/// If there are no loops, include an exact pre-merge H0 event when components
/// do merge. The final cutoff is always appended as a baseline view.
pub fn recommend_scales(
  analysis : Analysis,
  max_loops? : Int = 5,
) -> Array[ScaleRecommendation] raise TopologyError {
  if max_loops < 1 || max_loops > 10 {
    raise TopologyError("scale recommendations require 1..10 loop suggestions")
  }
  let indices : Array[Int] = []
  for i = 0; i < analysis.intervals.length(); i = i + 1 {
    if analysis.intervals[i].dimension == 1 {
      indices.push(i)
    }
  }
  indices.sort_by(fn(i, j) {
    let a = observed_lifetime(analysis, analysis.intervals[i])
    let b = observed_lifetime(analysis, analysis.intervals[j])
    if a > b {
      -1
    } else if a < b {
      1
    } else {
      i - j
    }
  })
  let result : Array[ScaleRecommendation] = []
  for i = 0; i < indices.length() && result.length() < max_loops; i = i + 1 {
    let index = indices[i]
    let interval = analysis.intervals[index]
    let censored = interval.death is None
    let end = match interval.death {
      Some(death) => death
      None => analysis.filtration.cutoff
    }
    let middle = interval.birth + (end - interval.birth) / 2.0
    let scale = if middle >= end { interval.birth } else { middle }
    result.push({
      scale,
      interval_index: Some(index),
      reason: if censored {
        "h1_alive_at_cutoff"
      } else {
        "h1_finite"
      },
      observed_lifetime: Some(observed_lifetime(analysis, interval)),
      censored,
    })
  }
  if result.is_empty() {
    let mut best_scale = analysis.filtration.cutoff
    let mut best_h0 = betti(analysis, 0, analysis.filtration.cutoff)
    let final_h0 = best_h0
    for event in betti_events(analysis) {
      if event.scale < analysis.filtration.cutoff && event.h0 > best_h0 {
        best_h0 = event.h0
        best_scale = event.scale
      }
    }
    if best_h0 > final_h0 && best_h0 > 1 {
      result.push({
        scale: best_scale,
        interval_index: None,
        reason: "h0_before_merges",
        observed_lifetime: None,
        censored: false,
      })
    }
  }
  result.push({
    scale: analysis.filtration.cutoff,
    interval_index: None,
    reason: "final_cutoff",
    observed_lifetime: None,
    censored: false,
  })
  result
}

///|
fn recommendation_number(value : Double?) -> Json {
  match value {
    Some(number) => number.to_json()
    None => Json::null()
  }
}

///|
fn recommendation_index(value : Int?) -> Json {
  match value {
    Some(index) => index.to_json()
    None => Json::null()
  }
}

///|
/// Structured explanation with the exact scale and Betti counts for each view.
pub fn recommendations_json(
  analysis : Analysis,
  max_loops? : Int = 5,
) -> Json raise TopologyError {
  let suggestions : Array[Json] = []
  for item in recommend_scales(analysis, max_loops~) {
    suggestions.push({
      "scale": item.scale,
      "interval_index": recommendation_index(item.interval_index),
      "reason": item.reason,
      "observed_lifetime": recommendation_number(item.observed_lifetime),
      "censored": item.censored,
      "h0": betti(analysis, 0, item.scale),
      "h1": betti(analysis, 1, item.scale),
    })
  }
  {
    "schema_version": 1,
    "cutoff": analysis.filtration.cutoff,
    "recommendations": suggestions,
    "method": "H1 classes ranked by observed lifetime; view at interval midpoint, falling back to birth if needed; without H1 choose an exact pre-merge H0 event when available; append final cutoff",
    "censoring_note": "alive at cutoff means only an observed lower bound, not infinite persistence",
    "interpretation_note": "viewing suggestions only; not an optimal filtration threshold or a physical diagnosis",
  }
}

///|
pub fn recommendations_csv(
  analysis : Analysis,
  max_loops? : Int = 5,
) -> String raise TopologyError {
  let mut text = "rank,scale,interval_index,reason,observed_lifetime,censored,H0,H1\n"
  let suggestions = recommend_scales(analysis, max_loops~)
  for i, item in suggestions {
    let index = match item.interval_index {
      Some(value) => value.to_string()
      None => ""
    }
    let lifetime = match item.observed_lifetime {
      Some(value) => value.to_string()
      None => ""
    }
    text += "\{i + 1},\{item.scale},\{index},\{item.reason},\{lifetime},\{item.censored},\{betti(analysis, 0, item.scale)},\{betti(analysis, 1, item.scale)}\n"
  }
  text
}