///|
/// Cron expression validation.
/// Validates field values, ranges, and syntactic correctness of cron expressions.

///|
/// Validate a raw cron expression string.
/// Returns Ok(()) if valid, or an error with details.
pub fn validate_expression(raw : String) -> Result[Unit, CronError] {
  let expression = raw.trim().to_owned()
  if expression == "" {
    return Err({ kind: EmptyExpression, expression: "" })
  }

  // First try alias resolution
  match resolve_alias_v(expression) {
    Some(expanded) =>
      match parse_cron_expression(expanded) {
        Ok(_) => Ok(())
        Err(err) => Err({ ..err, expression, })
      }
    None => validate_fields_detailed(expression)
  }
}

///|
/// Detailed field validation with clear error messages.
fn validate_fields_detailed(expression : String) -> Result[Unit, CronError] {
  let fields = split_fields_v(expression)
  let count = fields.length()

  if count != 5 && count != 6 {
    return Err({ kind: WrongFieldCount(count), expression })
  }

  let field_types = match count {
    5 => [Minute, Hour, DayOfMonth, Month, DayOfWeek]
    6 => [Second, Minute, Hour, DayOfMonth, Month, DayOfWeek]
    _ => [Minute, Hour, DayOfMonth, Month, DayOfWeek]
  }

  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,
      })
    }
    match validate_single_field(field_types[i], raw) {
      Err(err) => return Err({ ..err, expression, })
      Ok(_) => ()
    }
  }

  // Also verify the full parse works
  match parse_cron_expression(expression) {
    Ok(_) => Ok(())
    Err(err) => Err(err)
  }
}

///|
/// Validate a single field value.
fn validate_single_field(
  field_type : FieldType,
  raw : String,
) -> Result[Unit, CronError] {
  let (min, max) = field_range(field_type)
  let name = field_key(field_type)

  // Validate characters first
  for i = 0; i < raw.length(); i = i + 1 {
    let code = raw[i].to_int()
    if !is_valid_field_code(code) {
      return Err({ kind: InvalidCharacter(name), expression: "" })
    }
  }

  // Check for common syntax errors
  if raw.contains("--") {
    return Err({ kind: InvalidRange(name), expression: "" })
  }
  if raw.contains(",,") {
    return Err({ kind: InvalidSyntax(name), expression: "" })
  }
  if raw.contains("//") {
    return Err({ kind: InvalidSyntax(name), expression: "" })
  }

  // Validate step values
  if raw.contains("/") {
    let step_val = extract_step(raw)
    match step_val {
      Some(0) => return Err({ kind: StepCannotBeZero(name), expression: "" })
      _ => ()
    }
  }

  // Validate comma-separated items
  if raw.contains(",") {
    let items = split_comma_v(raw)
    for i = 0; i < items.length(); i = i + 1 {
      match validate_field_item(items[i], name, min, max) {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
    }
    return Ok(())
  }

  validate_field_item(raw, name, min, max)
}

///|
/// Validate a single field item (not containing commas).
fn validate_field_item(
  item : String,
  name : String,
  min : Int,
  max : Int,
) -> Result[Unit, CronError] {
  if item == "*" {
    return Ok(())
  }

  // Handle step: */5 or 10-50/5
  if item.contains("/") {
    let parts = split_slash_v(item)
    let base = parts.0
    let step_str = parts.1

    if base == "*" {
      // Check that step is a valid number
      match parse_int_v(step_str) {
        Some(_) => return Ok(())
        None => return Err({ kind: InvalidSyntax(name), expression: "" })
      }
    }

    if base.contains("-") {
      return validate_range(base, name, min, max)
    }

    // N/step: validate N is in range
    match parse_int_v(base) {
      Some(v) => {
        if v < min || v > max {
          return Err({
            kind: FieldValueOutOfRange(name, v, min, max),
            expression: "",
          })
        }
        Ok(())
      }
      None => Err({ kind: InvalidSyntax(name), expression: "" })
    }
  } else if item.contains("-") {
    validate_range(item, name, min, max)
  } else {
    // Simple number
    match parse_int_v(item) {
      Some(v) => {
        if v < min || v > max {
          return Err({
            kind: FieldValueOutOfRange(name, v, min, max),
            expression: "",
          })
        }
        Ok(())
      }
      None => Err({ kind: InvalidSyntax(name), expression: "" })
    }
  }
}

///|
/// Validate a range expression.
fn validate_range(
  raw : String,
  name : String,
  min : Int,
  max : Int,
) -> Result[Unit, CronError] {
  let parts = split_dash_v(raw)
  let start_str = parts.0
  let end_str = parts.1

  match (parse_int_v(start_str), parse_int_v(end_str)) {
    (Some(start), Some(end)) => {
      if start < min || start > max {
        return Err({
          kind: FieldValueOutOfRange(name, start, min, max),
          expression: "",
        })
      }
      if end < min || end > max {
        return Err({
          kind: FieldValueOutOfRange(name, end, min, max),
          expression: "",
        })
      }
      if start > end {
        return Err({ kind: InvalidRange(name), expression: "" })
      }
      Ok(())
    }
    _ => Err({ kind: InvalidRange(name), expression: "" })
  }
}

///|
/// Extract the step value from a "*/N" or "a-b/N" expression.
fn extract_step(raw : String) -> Int? {
  let pos = find_char_v(raw, 47)
  if pos >= 0 && pos + 1 < raw.length() {
    return parse_int_v(raw[pos + 1:raw.length()].to_owned())
  }
  None
}

///|
/// Parse an integer, returning None on failure.
fn parse_int_v(s : String) -> Int? {
  if s == "" {
    return None
  }
  let mut value = 0
  for i = 0; i < s.length(); i = i + 1 {
    let code = s[i].to_int()
    if code < 48 || code > 57 {
      return None
    }
    value = value * 10 + (code - 48)
  }
  Some(value)
}

///|
/// Check if an integer character code is valid in a cron field.
fn is_valid_field_code(code : Int) -> Bool {
  (code >= 48 && code <= 57) || // digits
  code == 42 || // *
  code == 44 || // ,
  code == 45 || // -
  code == 47 // /
}

///|
/// Find the first occurrence of a character code in a string, return index or -1.
fn find_char_v(s : String, needle_code : Int) -> Int {
  for i = 0; i < s.length(); i = i + 1 {
    if s[i].to_int() == needle_code {
      return i
    }
  }
  -1
}

///|
/// Split a string by comma.
fn split_comma_v(s : String) -> Array[String] {
  let result : Array[String] = []
  let mut start = 0
  let mut i = 0
  while i < s.length() {
    if s[i].to_int() == 44 {
      result.push(s[start:i].to_owned())
      start = i + 1
    }
    i = i + 1
  }
  result.push(s[start:s.length()].to_owned())
  result
}

///|
/// Split a string on the first slash.
fn split_slash_v(s : String) -> (String, String) {
  for i = 0; i < s.length(); i = i + 1 {
    if s[i].to_int() == 47 {
      return (s[0:i].to_owned(), s[i + 1:s.length()].to_owned())
    }
  }
  (s, "")
}

///|
/// Split a string on the first dash.
fn split_dash_v(s : String) -> (String, String) {
  for i = 0; i < s.length(); i = i + 1 {
    if s[i].to_int() == 45 {
      return (s[0:i].to_owned(), s[i + 1:s.length()].to_owned())
    }
  }
  (s, "")
}

///|
/// Split whitespace-separated fields.
fn split_fields_v(s : String) -> Array[String] {
  let result : Array[String] = []
  let mut start = 0
  let mut i = 0
  // Skip leading whitespace
  while i < s.length() && is_ws_code(s[i].to_int()) {
    i = i + 1
  }
  start = i
  while i < s.length() {
    if is_ws_code(s[i].to_int()) {
      if start < i {
        result.push(s[start:i].to_owned())
      }
      while i < s.length() && is_ws_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 integer code is whitespace.
fn is_ws_code(code : Int) -> Bool {
  code == 32 || code == 9 || code == 13 || code == 10
}

///|
/// Direct alias resolution.
fn resolve_alias_v(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
  }
}