///|
/// Numeric summary for one column or one group.
pub(all) struct CsvNumericSummary {
  count : Int
  sum : Double
  average : Double?
  min : Double?
  max : Double?
} derive(Eq, Debug)

///|
/// Count non-empty values in a column.
pub fn column_count_non_empty(table : CsvTable, column : String) -> Int {
  match aggregation_column_index(table.headers, column) {
    Some(index) => {
      let mut count = 0
      for row in table.rows {
        if !aggregation_cell_at(row, index).is_empty() {
          count += 1
        }
      }
      count
    }
    None => 0
  }
}

///|
/// Count empty or missing values in a column.
pub fn column_count_empty(table : CsvTable, column : String) -> Int {
  match aggregation_column_index(table.headers, column) {
    Some(index) => {
      let mut count = 0
      for row in table.rows {
        if aggregation_cell_at(row, index).is_empty() {
          count += 1
        }
      }
      count
    }
    None => 0
  }
}

///|
/// Return unique non-empty values in first-seen order.
pub fn column_unique_values(table : CsvTable, column : String) -> Array[String] {
  match aggregation_column_index(table.headers, column) {
    Some(index) => {
      let values : Array[String] = Array::new()
      for row in table.rows {
        let value = aggregation_cell_at(row, index)
        if !value.is_empty() && !aggregation_contains(values, value) {
          values.push(value)
        }
      }
      values
    }
    None => []
  }
}

///|
/// Return the sum of numeric values in a column.
pub fn column_sum(table : CsvTable, column : String) -> Double {
  column_numeric_summary(table, column).sum
}

///|
/// Return the average of numeric values in a column.
pub fn column_average(table : CsvTable, column : String) -> Double? {
  column_numeric_summary(table, column).average
}

///|
/// Return the minimum numeric value in a column.
pub fn column_min(table : CsvTable, column : String) -> Double? {
  column_numeric_summary(table, column).min
}

///|
/// Return the maximum numeric value in a column.
pub fn column_max(table : CsvTable, column : String) -> Double? {
  column_numeric_summary(table, column).max
}

///|
/// Summarize numeric values in a column, ignoring non-numeric and empty values.
pub fn column_numeric_summary(
  table : CsvTable,
  column : String,
) -> CsvNumericSummary {
  match aggregation_column_index(table.headers, column) {
    Some(index) => {
      let mut count = 0
      let mut sum = 0.0
      let mut min : Double? = None
      let mut max : Double? = None
      for row in table.rows {
        match aggregation_parse_double(aggregation_cell_at(row, index)) {
          Some(value) => {
            count += 1
            sum += value
            min = Some(
              match min {
                Some(current) => if value < current { value } else { current }
                None => value
              },
            )
            max = Some(
              match max {
                Some(current) => if value > current { value } else { current }
                None => value
              },
            )
          }
          None => ()
        }
      }
      let average = if count == 0 {
        None
      } else {
        Some(sum / count.to_double())
      }
      { count, sum, average, min, max }
    }
    None => { count: 0, sum: 0.0, average: None, min: None, max: None }
  }
}

///|
/// Count rows per group value and return a two-column table.
pub fn table_group_count(table : CsvTable, group_column : String) -> CsvTable {
  match aggregation_column_index(table.headers, group_column) {
    Some(group_index) => {
      let groups : Array[String] = Array::new()
      let counts : Array[Int] = Array::new()
      for row in table.rows {
        let key = aggregation_cell_at(row, group_index)
        match aggregation_index_of(groups, key) {
          Some(index) => counts[index] += 1
          None => {
            groups.push(key)
            counts.push(1)
          }
        }
      }
      let rows : Array[Array[String]] = Array::new()
      for i in 0.. { headers: [group_column, "count"], rows: [] }
  }
}

///|
/// Sum numeric values per group and return a two-column table.
pub fn table_group_sum(
  table : CsvTable,
  group_column : String,
  value_column : String,
) -> CsvTable {
  match
    (
      aggregation_column_index(table.headers, group_column),
      aggregation_column_index(table.headers, value_column),
    ) {
    (Some(group_index), Some(value_index)) => {
      let groups : Array[String] = Array::new()
      let sums : Array[Double] = Array::new()
      for row in table.rows {
        let key = aggregation_cell_at(row, group_index)
        let value = aggregation_parse_double(
          aggregation_cell_at(row, value_index),
        ).unwrap_or(0.0)
        match aggregation_index_of(groups, key) {
          Some(index) => sums[index] += value
          None => {
            groups.push(key)
            sums.push(value)
          }
        }
      }
      let rows : Array[Array[String]] = Array::new()
      for i in 0.. { headers: [group_column, "sum_" + value_column], rows: [] }
  }
}

///|
/// Average numeric values per group and return a two-column table.
pub fn table_group_average(
  table : CsvTable,
  group_column : String,
  value_column : String,
) -> CsvTable {
  match
    (
      aggregation_column_index(table.headers, group_column),
      aggregation_column_index(table.headers, value_column),
    ) {
    (Some(group_index), Some(value_index)) => {
      let groups : Array[String] = Array::new()
      let sums : Array[Double] = Array::new()
      let counts : Array[Int] = Array::new()
      for row in table.rows {
        let key = aggregation_cell_at(row, group_index)
        match aggregation_parse_double(aggregation_cell_at(row, value_index)) {
          Some(value) =>
            match aggregation_index_of(groups, key) {
              Some(index) => {
                sums[index] += value
                counts[index] += 1
              }
              None => {
                groups.push(key)
                sums.push(value)
                counts.push(1)
              }
            }
          None => ()
        }
      }
      let rows : Array[Array[String]] = Array::new()
      for i in 0.. { headers: [group_column, "average_" + value_column], rows: [] }
  }
}

///|
/// Build a compact Markdown report for one numeric column.
pub fn column_summary_markdown(table : CsvTable, column : String) -> String {
  let summary = column_numeric_summary(table, column)
  let out = StringBuilder()
  out.write_string("### Column `\{column}`\n\n")
  out.write_string("| metric | value |\n")
  out.write_string("| --- | --- |\n")
  out.write_string("| numeric_count | \{summary.count} |\n")
  out.write_string("| sum | \{summary.sum} |\n")
  match summary.average {
    Some(value) => out.write_string("| average | \{value} |\n")
    None => out.write_string("| average |  |\n")
  }
  match summary.min {
    Some(value) => out.write_string("| min | \{value} |\n")
    None => out.write_string("| min |  |\n")
  }
  match summary.max {
    Some(value) => out.write_string("| max | \{value} |")
    None => out.write_string("| max |  |")
  }
  out.to_string()
}

///|
fn aggregation_column_index(headers : Array[String], column : String) -> Int? {
  for i in 0.. String {
  if index >= 0 && index < row.length() {
    row[index]
  } else {
    ""
  }
}

///|
fn aggregation_contains(values : Array[String], value : String) -> Bool {
  for item in values {
    if item == value {
      return true
    }
  }
  false
}

///|
fn aggregation_index_of(values : Array[String], value : String) -> Int? {
  for i in 0.. Double? {
  try @string.parse_double(value) catch {
    _ => None
  } noraise {
    number => Some(number)
  }
}