///|
/// Cron expression parser.
/// Parses standard 5-field and extended 6-field (with seconds) cron expressions,
/// plus common aliases.

///|
/// Parse a full cron expression string into an array of CronField.
/// Supports 5-field format, 6-field format (seconds first), and named aliases.
pub fn parse_cron_expression(
  raw : String,
) -> Result[Array[CronField], CronError] {
  let expression = raw.trim().to_owned()
  if expression == "" {
    return Err({ kind: EmptyExpression, expression: "" })
  }

  // Try alias first
  match resolve_alias(expression) {
    Some(expanded) => parse_fields(expanded, expression)
    None => parse_fields(expression, expression)
  }
}

///|
/// Determine the field types based on the number of fields.
/// Returns the appropriate field type array.
fn field_types_for_count(count : Int) -> Array[FieldType]? {
  match count {
    5 => Some([Minute, Hour, DayOfMonth, Month, DayOfWeek])
    6 => Some([Second, Minute, Hour, DayOfMonth, Month, DayOfWeek])
    _ => None
  }
}

///|
/// Parse a cron expression with 5 or 6 fields.
fn parse_fields(
  expression : String,
  original : String,
) -> Result[Array[CronField], CronError] {
  let fields = split_cron_fields(expression)
  let count = fields.length()

  let field_types = match field_types_for_count(count) {
    Some(types) => types
    None => return Err({ kind: WrongFieldCount(count), expression: original })
  }

  let result : Array[CronField] = []

  for i = 0; i < count; i = i + 1 {
    let raw = fields[i].trim().to_owned()
    if raw == "" {
      return Err({
        kind: InvalidFieldValue(field_key(field_types[i])),
        expression: original,
      })
    }
    match parse_field(field_types[i], raw) {
      Some(field) => result.push(field)
      None =>
        return Err({
          kind: InvalidFieldValue(field_key(field_types[i])),
          expression: original,
        })
    }
  }

  Ok(result)
}

///|
/// Resolve a named alias to its standard 5-field form.
fn resolve_alias(raw : String) -> String? {
  match raw {
    "@hourly" => Some("0 * * * *")
    "@daily" | "@midnight" => Some("0 0 * * *")
    "@weekly" => Some("0 0 * * 0")
    "@monthly" => Some("0 0 1 * *")
    "@yearly" | "@annually" => Some("0 0 1 1 *")
    _ => None
  }
}

///|
/// Check if a parsed expression uses the 6-field (seconds) format.
pub fn is_six_field(fields : Array[CronField]) -> Bool {
  fields.length() == 6
}

///|
/// Check if a parsed expression uses the 5-field (standard) format.
pub fn is_five_field(fields : Array[CronField]) -> Bool {
  fields.length() == 5
}

///|
/// Split a cron expression into 5 whitespace-separated fields.
fn split_cron_fields(s : String) -> Array[String] {
  let result : Array[String] = []
  let mut start = 0
  let mut i = 0
  // Skip leading whitespace
  while i < s.length() && is_space_code(s[i].to_int()) {
    i = i + 1
  }
  start = i
  while i < s.length() {
    if is_space_code(s[i].to_int()) {
      if start < i {
        result.push(s[start:i].to_owned())
      }
      // Skip consecutive whitespace
      while i < s.length() && is_space_code(s[i].to_int()) {
        i = i + 1
      }
      start = i
    } else {
      i = i + 1
    }
  }
  if start < s.length() {
    result.push(s[start:s.length()].to_owned())
  }
  result
}

///|
/// Check if an integer character code is whitespace.
fn is_space_code(code : Int) -> Bool {
  code == 32 || code == 9 || code == 13 || code == 10
}

///|
/// Get the list of known alias names.
pub fn alias_names() -> Array[String] {
  [
    "@hourly", "@daily", "@midnight", "@weekly", "@monthly", "@yearly", "@annually",
  ]
}

///|
/// Check if a string is a known alias.
pub fn is_alias(s : String) -> Bool {
  match resolve_alias(s) {
    Some(_) => true
    None => false
  }
}