///|
/// A transparent distribution of weighted text lengths.
pub struct LengthStats {
  count : Int
  total : Double
  minimum : Double
  maximum : Double
  mean : Double
  median : Double
  p90 : Double
} derive(Debug, ToJson)

///|
fn sorted_copy(values : Array[Double]) -> Array[Double] {
  let copy = values.copy()
  copy.sort()
  copy
}

///|
fn percentile(values : Array[Double], fraction : Double) -> Double {
  if values.is_empty() {
    return 0.0
  }
  let sorted = sorted_copy(values)
  let position = ((sorted.length() - 1).to_double() * fraction).round().to_int()
  sorted[position.max(0).min(sorted.length() - 1)]
}

///|
/// Compute min/max/mean/median/p90 from weighted units.
pub fn length_stats(units : Array[TextUnit]) -> LengthStats {
  let values = units.map(fn(unit) { unit.char_weight })
  if values.is_empty() {
    return {
      count: 0,
      total: 0.0,
      minimum: 0.0,
      maximum: 0.0,
      mean: 0.0,
      median: 0.0,
      p90: 0.0,
    }
  }
  let total = values.fold(init=0.0, (sum, value) => sum + value)
  {
    count: values.length(),
    total,
    minimum: values.fold(init=values[0], (a, b) => a.min(b)),
    maximum: values.fold(init=values[0], (a, b) => a.max(b)),
    mean: total / values.length().to_double(),
    median: percentile(values, 0.5),
    p90: percentile(values, 0.9),
  }
}

///|
/// Compute a weighted length ratio for two segmented documents.
pub fn length_ratio(
  source : Array[TextUnit],
  target : Array[TextUnit],
) -> Double {
  let left = length_stats(source).total
  let right = length_stats(target).total
  if left < 0.0001 {
    0.0
  } else {
    right / left
  }
}

///|
/// Count units by paragraph while keeping empty paragraphs visible in reports.
pub fn paragraph_histogram(units : Array[TextUnit]) -> Map[Int, Int] {
  let histogram : Map[Int, Int] = Map([])
  for unit in units {
    histogram[unit.paragraph_index] = histogram.get_or_default(
        unit.paragraph_index,
        0,
      ) +
      1
  }
  histogram
}

///|
/// Return all unit indexes whose normalized text is unusually short.
pub fn short_unit_indexes(
  units : Array[TextUnit],
  threshold? : Double = 2.0,
) -> Array[Int] {
  let indexes = []
  for unit in units {
    if unit.char_weight <= threshold {
      indexes.push(unit.id)
    }
  }
  indexes
}

///|
/// Return all repeated normalized units and their frequency.
pub fn repeated_units(units : Array[TextUnit]) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for unit in units {
    counts[unit.normalized] = counts.get_or_default(unit.normalized, 0) + 1
  }
  let repeated : Map[String, Int] = Map([])
  for key, count in counts {
    if count > 1 {
      repeated[key] = count
    }
  }
  repeated
}

///|
/// Serialize basic statistics as one stable CSV row.
pub fn stats_to_csv_row(label : String, stats : LengthStats) -> String {
  "\{label},\{stats.count},\{stats.total},\{stats.minimum},\{stats.maximum},\{stats.mean},\{stats.median},\{stats.p90}"
}

///|
/// Produce a deterministic text profile used in issue reports.
pub fn text_profile(
  text : String,
  options? : AlignOptions = default_options(),
) -> String {
  let units = segment_text(text, options~)
  let stats = length_stats(units)
  let histogram = paragraph_histogram(units)
  "units=\{stats.count};mean=\{stats.mean};p90=\{stats.p90};paragraphs=\{histogram.length()}"
}