///|
/// Columnar table schema for analysis pipelines.
pub struct TableSchema {
  names : Array[String]
  types : Array[String]
  required : Array[Bool]
  version : String
}

///|
/// Numeric table profile.
pub struct TableProfile {
  rows : Int
  columns : Int
  complete_rows : Int
  missing_cells : Int
  duplicate_rows : Int
  schema_fingerprint : UInt64
  data_fingerprint : UInt64
  passes : Bool
}

///|
/// Group-level aggregate with count, mean and dispersion.
pub struct GroupAggregate {
  key : Int
  count : Int
  total : Double
  mean : Double
  variance : Double
  minimum : Double
  maximum : Double
}

///|
/// Result of a keyed inner or left join.
pub struct TableJoinResult {
  left_indices : Array[Int]
  right_indices : Array[Int]
  matched : Int
  unmatched_left : Int
  unmatched_right : Int
  passes : Bool
}

///|
/// A deterministic rolling summary for one key and time index.
pub struct TableWindowSummary {
  key : Int
  time : Int
  count : Int
  mean : Double
  standard_deviation : Double
  minimum : Double
  maximum : Double
}

///|
fn table_unique_int(values : Array[Int]) -> Array[Int] {
  let result : Array[Int] = Array::new()
  for value in values {
    if !result.contains(value) {
      result.push(value)
    }
  }
  result
}

///|
fn table_min(values : Array[Double]) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let mut result = values[0]
  for value in values[1:] {
    if value < result {
      result = value
    }
  }
  result
}

///|
fn table_max(values : Array[Double]) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let mut result = values[0]
  for value in values[1:] {
    if value > result {
      result = value
    }
  }
  result
}

///|
/// Creates a schema with stable defaults.
pub fn table_schema(
  names : Array[String],
  types? : Array[String] = [],
  required? : Array[Bool] = [],
  version? : String = "1",
) -> TableSchema {
  let actual_types : Array[String] = Array::new(capacity=names.length())
  let actual_required : Array[Bool] = Array::new(capacity=names.length())
  for i in 0.. UInt64 {
  let rows : Array[Array[Double]] = Array::new()
  for i in 0.. Array[Array[Double]] {
  let result : Array[Array[Double]] = Array::new(capacity=table.length())
  for row in table {
    let selected = Array::new(capacity=indexes.length())
    for index in indexes {
      if index >= 0 && index < row.length() {
        selected.push(row[index])
      }
    }
    result.push(selected)
  }
  result
}

///|
/// Filters rows using an explicit mask.
pub fn table_filter_rows(
  table : Array[Array[Double]],
  mask : Array[Bool],
) -> Array[Array[Double]] {
  let result : Array[Array[Double]] = Array::new()
  for i in 0.. Array[Array[Double]] {
  let result : Array[Array[Double]] = Array::new()
  for row in table {
    if column >= 0 && column < row.length() {
      let passes = if keep_above {
        row[column] >= threshold
      } else {
        row[column] <= threshold
      }
      if passes {
        result.push(row)
      }
    }
  }
  result
}

///|
/// Adds a numeric column, truncating to the existing row count when needed.
pub fn table_add_column(
  table : Array[Array[Double]],
  column : Array[Double],
) -> Array[Array[Double]] {
  let result : Array[Array[Double]] = Array::new(capacity=table.length())
  for i in 0.. Array[Array[Double]] {
  let result = table.copy()
  for i in 0..= 0 &&
      column_index < result[i].length() &&
      i < values.length() {
      result[i][column_index] = values[i]
    }
  }
  result
}

///|
/// Profiles a numeric table and its schema.
pub fn profile_table(
  table : Array[Array[Double]],
  schema : TableSchema,
) -> TableProfile {
  let missing = assess_matrix(table).missing_cells
  let complete = complete_row_count(table)
  let duplicates = duplicate_row_flags(table)
    .filter(fn(value) { value })
    .length()
  let columns = if table.length() == 0 { 0 } else { table[0].length() }
  let passes = table.length() == complete &&
    duplicates == 0 &&
    schema.names.length() == columns
  {
    rows: table.length(),
    columns,
    complete_rows: complete,
    missing_cells: missing,
    duplicate_rows: duplicates,
    schema_fingerprint: table_schema_fingerprint(schema),
    data_fingerprint: matrix_checksum(table),
    passes,
  }
}

///|
/// Counts rows without non-finite cells.
pub fn complete_row_count(table : Array[Array[Double]]) -> Int {
  let mut result = 0
  for row in table {
    let mut complete = true
    for value in row {
      if !is_finite(value) {
        complete = false
      }
    }
    if complete {
      result += 1
    }
  }
  result
}

///|
/// Aggregates a value by integer group key.
pub fn group_aggregate(
  keys : Array[Int],
  values : Array[Double],
) -> Array[GroupAggregate] {
  let n = keys.length().min(values.length())
  let groups = table_unique_int(keys[:n].to_owned())
  let result : Array[GroupAggregate] = Array::new(capacity=groups.length())
  for key in groups {
    let selected = Array::new()
    for i in 0.. Array[Array[Double]] {
  let n = keys.length().min(values.length()).min(weights.length())
  let groups = table_unique_int(keys[:n].to_owned())
  let result : Array[Array[Double]] = Array::new(capacity=groups.length())
  for key in groups {
    let mut numerator = 0.0
    let mut denominator = 0.0
    for i in 0.. Array[Array[Double]] {
  let n = keys.length().min(treatment.length()).min(outcomes.length())
  let groups = table_unique_int(keys[:n].to_owned())
  let result : Array[Array[Double]] = Array::new(capacity=groups.length())
  for key in groups {
    let treated = Array::new()
    let control = Array::new()
    for i in 0.. TableJoinResult {
  let left_indices : Array[Int] = Array::new()
  let right_indices : Array[Int] = Array::new()
  let matched_right : Array[Int] = Array::new()
  for i in 0..= 0 {
      left_indices.push(i)
      right_indices.push(match_index)
      matched_right.push(match_index)
    }
  }
  {
    left_indices,
    right_indices,
    matched: left_indices.length(),
    unmatched_left: left_keys.length() - left_indices.length(),
    unmatched_right: right_keys.length() - matched_right.length(),
    passes: left_indices.length() > 0 ||
    (left_keys.length() == 0 && right_keys.length() == 0),
  }
}

///|
/// Performs a left keyed join and marks absent right rows with -1.
pub fn table_left_join(
  left_keys : Array[Int],
  right_keys : Array[Int],
) -> TableJoinResult {
  let left_indices : Array[Int] = Array::new()
  let right_indices : Array[Int] = Array::new()
  let used : Array[Int] = Array::new()
  for i in 0..= 0 {
      used.push(match_index)
    }
  }
  {
    left_indices,
    right_indices,
    matched: used.length(),
    unmatched_left: left_keys.length() - used.length(),
    unmatched_right: right_keys.length() - used.length(),
    passes: true,
  }
}

///|
/// Calculates a lag within each integer group after assuming row order is time order.
pub fn group_lag(
  keys : Array[Int],
  values : Array[Double],
  lag : Int,
  fill? : Double = 0.0,
) -> Array[Double] {
  let result = Array::make(keys.length().min(values.length()), fill)
  if lag <= 0 {
    return values[:result.length()].to_owned()
  }
  for i in lag.. Array[Double] {
  let n = keys.length().min(values.length())
  let result = Array::make(n, 0.0)
  let width = if window > 0 { window } else { 1 }
  for i in 0.. width { i + 1 - width } else { 0 }
    let end = i + 1
    for j in start.. Array[Double] {
  let n = keys.length().min(values.length())
  let result = Array::make(n, 0.0)
  let width = if window > 0 { window } else { 1 }
  for i in 0.. width { i + 1 - width } else { 0 }
    let end = i + 1
    for j in start.. Array[TableWindowSummary] {
  let n = keys.length().min(times.length()).min(values.length())
  let result : Array[TableWindowSummary] = Array::new(capacity=n)
  let width = if window > 0 { window } else { 1 }
  for i in 0..= times[i] - width + 1 {
        selected.push(values[j])
      }
    }
    result.push({
      key: keys[i],
      time: times[i],
      count: selected.length(),
      mean: mean_or(selected, 0.0),
      standard_deviation: std_dev(selected),
      minimum: table_min(selected),
      maximum: table_max(selected),
    })
  }
  result
}