///|
/// Cron expression normalizer and optimizer.
/// Converts expressions to canonical form, simplifies redundant patterns,
/// and provides expression-level utilities.

///|
/// Normalize a cron expression string to its canonical form.
/// This simplifies the expression while preserving its meaning.
pub fn normalize_expression(raw : String) -> Result[String, CronError] {
  let fields = match parse_cron_expression(raw) {
    Ok(f) => f
    Err(e) => return Err(e)
  }
  Ok(normalize_fields(fields))
}

///|
/// Normalize an already-parsed field array into a canonical string.
pub fn normalize_fields(fields : Array[CronField]) -> String {
  let mut result = ""
  for i = 0; i < fields.length(); i = i + 1 {
    if i > 0 {
      result = result + " "
    }
    result = result + normalize_single_field(fields[i])
  }
  result
}

///|
/// Normalize a single field to its most compact representation.
fn normalize_single_field(field : CronField) -> String {
  let values = field.values
  let (min, max) = field_range(field.field_type)
  let total = max - min + 1

  // If matches all values, use "*"
  if values.length() == total {
    let mut is_all = true
    for i = 0; i < total && is_all; i = i + 1 {
      if values[i] != min + i {
        is_all = false
      }
    }
    if is_all {
      return "*"
    }
  }

  // If empty, return "*" as default
  if values.length() == 0 {
    return "*"
  }

  // Check for step pattern: */N
  if values.length() > 1 {
    let step = values[1] - values[0]
    if step > 1 {
      let mut is_step = true
      let mut expected = values[0]
      for i = 0; i < values.length() && is_step; i = i + 1 {
        if values[i] != expected {
          is_step = false
        }
        expected = expected + step
      }
      if is_step && values[0] == min && values[values.length() - 1] + step > max {
        return "*/" + step.to_string()
      }
      if is_step {
        return values[0].to_string() +
          "-" +
          values[values.length() - 1].to_string() +
          "/" +
          step.to_string()
      }
    }
  }

  // Check for consecutive range (2 or more values)
  if values.length() >= 2 {
    let mut is_consec = true
    for i = 1; i < values.length() && is_consec; i = i + 1 {
      if values[i] != values[i - 1] + 1 {
        is_consec = false
      }
    }
    if is_consec {
      return values[0].to_string() +
        "-" +
        values[values.length() - 1].to_string()
    }
  }

  // Check for step pattern within a range
  if values.length() >= 3 {
    let step = values[1] - values[0]
    if step > 1 {
      let mut is_step = true
      for i = 1; i < values.length() && is_step; i = i + 1 {
        if values[i] - values[i - 1] != step {
          is_step = false
        }
      }
      if is_step {
        return values[0].to_string() +
          "-" +
          values[values.length() - 1].to_string() +
          "/" +
          step.to_string()
      }
    }
  }

  // Try to represent as contiguous ranges
  let result = compact_to_ranges(values)
  if result.length() < values.length() * 2 {
    // Ranges representation is shorter than listing all values
    return join_parts(result)
  }

  // Fall back to comma-separated list
  let mut s = ""
  for i = 0; i < values.length(); i = i + 1 {
    if i > 0 {
      s = s + ","
    }
    s = s + values[i].to_string()
  }
  s
}

///|
/// Compact a sorted array of values into range strings.
/// e.g., [1,2,3,5,6,7,10] -> ["1-3","5-7","10"]
pub fn compact_to_ranges(values : Array[Int]) -> Array[String] {
  if values.length() == 0 {
    return []
  }
  let result : Array[String] = []
  let mut range_start = values[0]
  let mut range_end = values[0]

  for i = 1; i < values.length(); i = i + 1 {
    if values[i] == range_end + 1 {
      range_end = values[i]
    } else {
      // End of a range
      if range_start == range_end {
        result.push(range_start.to_string())
      } else if range_end == range_start + 1 {
        result.push(range_start.to_string())
        result.push(range_end.to_string())
      } else {
        result.push(range_start.to_string() + "-" + range_end.to_string())
      }
      range_start = values[i]
      range_end = values[i]
    }
  }

  // Don't forget the last range
  if range_start == range_end {
    result.push(range_start.to_string())
  } else if range_end == range_start + 1 {
    result.push(range_start.to_string())
    result.push(range_end.to_string())
  } else {
    result.push(range_start.to_string() + "-" + range_end.to_string())
  }

  result
}

///|
/// Join parts with commas.
pub fn join_parts(parts : Array[String]) -> String {
  let mut result = ""
  for i = 0; i < parts.length(); i = i + 1 {
    if i > 0 {
      result = result + ","
    }
    result = result + parts[i]
  }
  result
}

///|
/// Check if two cron expressions are semantically equivalent.
/// This compares the expanded field values rather than the raw strings.
pub fn are_equivalent(a : Array[CronField], b : Array[CronField]) -> Bool {
  if a.length() != b.length() {
    return false
  }
  for i = 0; i < a.length(); i = i + 1 {
    if !fields_equal(a[i], b[i]) {
      return false
    }
  }
  true
}

///|
/// Compare two fields for value equality.
fn fields_equal(a : CronField, b : CronField) -> Bool {
  if a.values.length() != b.values.length() {
    return false
  }
  for i = 0; i < a.values.length(); i = i + 1 {
    if a.values[i] != b.values[i] {
      return false
    }
  }
  true
}

///|
/// Find the intersection of two cron fields (values present in both).
pub fn field_intersection(a : CronField, b : CronField) -> Array[Int] {
  let result : Array[Int] = []
  let mut i = 0
  let mut j = 0
  while i < a.values.length() && j < b.values.length() {
    if a.values[i] == b.values[j] {
      result.push(a.values[i])
      i = i + 1
      j = j + 1
    } else if a.values[i] < b.values[j] {
      i = i + 1
    } else {
      j = j + 1
    }
  }
  result
}

///|
/// Find the union of two cron fields (all values from both).
pub fn field_union(a : CronField, b : CronField) -> Array[Int] {
  let result : Array[Int] = []
  let mut i = 0
  let mut j = 0
  while i < a.values.length() && j < b.values.length() {
    if a.values[i] == b.values[j] {
      result.push(a.values[i])
      i = i + 1
      j = j + 1
    } else if a.values[i] < b.values[j] {
      result.push(a.values[i])
      i = i + 1
    } else {
      result.push(b.values[j])
      j = j + 1
    }
  }
  while i < a.values.length() {
    result.push(a.values[i])
    i = i + 1
  }
  while j < b.values.length() {
    result.push(b.values[j])
    j = j + 1
  }
  result
}

///|
/// Count the number of distinct execution times per field.
pub fn field_cardinality(field : CronField) -> Int {
  field.values.length()
}

///|
/// Get the effective range (min..max) of a field's values.
pub fn field_effective_range(field : CronField) -> (Int, Int) {
  if field.values.length() == 0 {
    let (min, max) = field_range(field.field_type)
    (min, max)
  } else {
    (field.values[0], field.values[field.values.length() - 1])
  }
}