///|
/// Analytics for categorical assignments and roster stability.
///
/// These functions operate on integer labels and are intentionally independent
/// of a particular schedule representation. They can score worker rosters,
/// machine labels, cluster assignments, or any finite categorical plan.
pub struct AssignmentMetrics {
  count : Int
  categories : Int
  minimum : Int
  maximum : Int
  spread : Int
  transitions : Int
  changes : Int
}

///|
/// Count distinct categorical labels.
pub fn assignment_category_count(values : Array[Int]) -> Int {
  let categories : Array[Int] = []
  for value in values {
    if !categories.contains(value) {
      categories.push(value)
    }
  }
  categories.length()
}

///|
/// Return category counts for labels from 0 through category_count - 1.
pub fn assignment_counts(
  values : Array[Int],
  category_count : Int,
) -> Array[Int] {
  let result : Array[Int] = []
  for _ in 0..= 0 && value < result.length() {
      result[value] += 1
    }
  }
  result
}

///|
/// Return the number of adjacent label changes.
pub fn assignment_transitions(values : Array[Int]) -> Int {
  let mut result = 0
  for index in 1.. Int {
  let limit = if previous.length() < current.length() {
    previous.length()
  } else {
    current.length()
  }
  let mut result = if previous.length() == current.length() { 0 } else { 1 }
  for index in 0.. Int {
  let counts = assignment_counts(values, category_count)
  if counts.length() == 0 {
    return 0
  }
  let mut low = counts[0]
  let mut high = counts[0]
  for count in counts {
    if count < low {
      low = count
    }
    if count > high {
      high = count
    }
  }
  high - low
}

///|
/// Return a weighted change penalty.
pub fn assignment_change_penalty(
  previous : Array[Int],
  current : Array[Int],
  penalty : Int,
) -> Int {
  assignment_changes(previous, current) *
  (if penalty < 0 { 0 } else { penalty })
}

///|
/// Summarize an assignment sequence.
pub fn assignment_metrics(
  values : Array[Int],
  category_count : Int,
  previous : Array[Int],
) -> AssignmentMetrics {
  let mut minimum = 0
  let mut maximum = 0
  if values.length() > 0 {
    minimum = values[0]
    maximum = values[0]
    for value in values {
      if value < minimum {
        minimum = value
      }
      if value > maximum {
        maximum = value
      }
    }
  }
  let actual_categories = assignment_category_count(values)
  let categories = if category_count > actual_categories {
    category_count
  } else {
    actual_categories
  }
  {
    count: values.length(),
    categories,
    minimum,
    maximum,
    spread: maximum - minimum,
    transitions: assignment_transitions(values),
    changes: assignment_changes(previous, values),
  }
}

///|
/// Read assignment count.
pub fn AssignmentMetrics::count(self : AssignmentMetrics) -> Int {
  self.count
}

///|
/// Read category count.
pub fn AssignmentMetrics::categories(self : AssignmentMetrics) -> Int {
  self.categories
}

///|
/// Read transition count.
pub fn AssignmentMetrics::transitions(self : AssignmentMetrics) -> Int {
  self.transitions
}

///|
/// Read change count.
pub fn AssignmentMetrics::changes(self : AssignmentMetrics) -> Int {
  self.changes
}

///|
/// Return a stable metric line.
pub fn AssignmentMetrics::describe(self : AssignmentMetrics) -> String {
  "count=\{self.count}, categories=\{self.categories}, spread=\{self.spread}, transitions=\{self.transitions}, changes=\{self.changes}"
}

///|
/// Return a rolling majority label.
pub fn rolling_majority(values : Array[Int], window : Int) -> Array[Int] {
  let result : Array[Int] = []
  if window <= 0 {
    return result
  }
  for index in 0.. best_count || (count == best_count && values[candidate] < best) {
        best = values[candidate]
        best_count = count
      }
    }
    result.push(best)
  }
  result
}

///|
/// Return a stable categorical fingerprint.
pub fn assignment_signature(values : Array[Int]) -> Int {
  let mut result = 17
  for value in values {
    result = result * 31 + value
  }
  result
}

///|
/// Return whether a sequence alternates between two labels.
pub fn assignment_alternates(
  values : Array[Int],
  first : Int,
  second : Int,
) -> Bool {
  for index, value in values {
    let expected = if index % 2 == 0 { first } else { second }
    if value != expected {
      return false
    }
  }
  true
}

///|
/// Return the longest run of one label.
pub fn longest_assignment_run(values : Array[Int]) -> Int {
  if values.length() == 0 {
    return 0
  }
  let mut best = 1
  let mut current = 1
  for index in 1.. best {
        best = current
      }
    } else {
      current = 1
    }
  }
  best
}

///|
/// Return each category's longest consecutive run.
pub fn category_run_lengths(
  values : Array[Int],
  category_count : Int,
) -> Array[Int] {
  let result : Array[Int] = []
  for _ in 0..= 0 && current < result.length() && length > result[current] {
        result[current] = length
      }
      current = values[index]
      length = 1
    }
  }
  if current >= 0 && current < result.length() && length > result[current] {
    result[current] = length
  }
  result
}

///|
/// Return category labels absent from a sequence.
pub fn missing_assignment_categories(
  values : Array[Int],
  category_count : Int,
) -> Array[Int] {
  let result : Array[Int] = []
  for category in 0.. Bool {
  for index in 1..