///|
/// Next execution time calculator.
/// Given a cron expression and a reference time, compute the next matching time(s).

///|
/// Find the next execution time after a given reference time.
/// Returns None if no matching time exists within a reasonable search range.
pub fn next_after(fields : Array[CronField], after : DateTime) -> DateTime? {
  next_matching_time(fields, after, after.year + 10)
}

///|
/// Find the next N execution times after a given reference time.
pub fn next_n(
  fields : Array[CronField],
  after : DateTime,
  n : Int,
) -> Array[DateTime] {
  let result : Array[DateTime] = []
  let mut current = after
  for i = 0; i < n; i = i + 1 {
    match next_after(fields, current) {
      Some(next_time) => {
        result.push(next_time)
        current = next_time
      }
      None => break
    }
  }
  result
}

///|
/// Internal: find the next matching time using optimized field-based jumping.
fn next_matching_time(
  fields : Array[CronField],
  after : DateTime,
  max_year : Int,
) -> DateTime? {
  let has_seconds = fields.length() == 6
  let mut current = if has_seconds {
    add_second(after)
  } else {
    add_minute(after)
  }
  let mut iterations = 0
  // Field-based jumps keep this bound practical even for sparse schedules.
  let max_iterations = if has_seconds { 315360000 } else { 5256000 }

  while current.year <= max_year && iterations < max_iterations {
    iterations = iterations + 1

    // Check if current time matches all fields
    if matches_time(fields, current) {
      return Some(current)
    }

    // Jump optimization: skip to next potentially matching time
    current = skip_to_next_match(fields, current)
  }
  None
}

///|
/// Skip to the next possibly matching time.
/// This optimizes the search by jumping forward in larger increments
/// when a field doesn't match.
fn skip_to_next_match(fields : Array[CronField], dt : DateTime) -> DateTime {
  let offset = if fields.length() == 6 { 1 } else { 0 }

  // Check month
  let month_field = fields[offset + 3]
  if !value_in_field(month_field, dt.month) {
    return skip_month(fields, offset, dt)
  }

  // Check day
  let dom_field = fields[offset + 2]
  let dow_field = fields[offset + 4]
  let dom_ok = value_in_field_or_all(dom_field, dt.day)
  let dow_ok = matches_dow(dow_field, dt)
  let dom_restricted = !field_is_all(dom_field)
  let dow_restricted = !field_is_all(dow_field)

  let day_ok = if dom_restricted && dow_restricted {
    dom_ok || dow_ok
  } else if dom_restricted {
    dom_ok
  } else if dow_restricted {
    dow_ok
  } else {
    true
  }

  if !day_ok {
    return skip_day(dt)
  }

  // Check hour
  let hour_field = fields[offset + 1]
  if !value_in_field(hour_field, dt.hour) {
    return skip_hour(dt)
  }

  // Check minute
  let minute_field = fields[offset]
  if !value_in_field(minute_field, dt.minute) {
    return skip_minute(dt)
  }

  if offset == 1 {
    let second_field = fields[0]
    if !value_in_field(second_field, dt.second) {
      return skip_second(dt)
    }
    add_second(dt)
  } else {
    add_minute(dt)
  }
}

///|
/// Skip to the next matching month.
fn skip_month(
  fields : Array[CronField],
  offset : Int,
  dt : DateTime,
) -> DateTime {
  let month_field = fields[offset + 3]
  match next_value_in_field(month_field, dt.month) {
    Some(m) =>
      if m <= 12 {
        { ..dt, month: m, day: 1, hour: 0, minute: 0, second: 0 }
      } else {
        // Next year
        match next_value_in_field(month_field, 0) {
          Some(nm) =>
            {
              year: dt.year + 1,
              month: nm,
              day: 1,
              hour: 0,
              minute: 0,
              second: 0,
            }
          None =>
            {
              year: dt.year + 1,
              month: 1,
              day: 1,
              hour: 0,
              minute: 0,
              second: 0,
            }
        }
      }
    None =>
      { year: dt.year + 1, month: 1, day: 1, hour: 0, minute: 0, second: 0 }
  }
}

///|
/// Skip to the next day.
fn skip_day(dt : DateTime) -> DateTime {
  let dim = days_in_month(dt.year, dt.month)
  if dt.day + 1 <= dim {
    { ..dt, day: dt.day + 1, hour: 0, minute: 0, second: 0 }
    // Next month
  } else if dt.month + 1 <= 12 {
    { ..dt, month: dt.month + 1, day: 1, hour: 0, minute: 0, second: 0 }
  } else {
    { year: dt.year + 1, month: 1, day: 1, hour: 0, minute: 0, second: 0 }
  }
}

///|
/// Skip to the next matching hour.
fn skip_hour(dt : DateTime) -> DateTime {
  if dt.hour + 1 < 24 {
    { ..dt, hour: dt.hour + 1, minute: 0, second: 0 }
  } else {
    skip_day(dt)
  }
}

///|
/// Skip to the next matching minute.
fn skip_minute(dt : DateTime) -> DateTime {
  if dt.minute + 1 < 60 {
    { ..dt, minute: dt.minute + 1, second: 0 }
  } else {
    skip_hour(dt)
  }
}

///|
/// Advance by one second after a seconds-field mismatch.
fn skip_second(dt : DateTime) -> DateTime {
  if dt.second + 1 < 60 {
    { ..dt, second: dt.second + 1 }
  } else {
    skip_minute(dt)
  }
}

///|
/// Check if a value is in a field's expanded values.
fn value_in_field(field : CronField, value : Int) -> Bool {
  for i = 0; i < field.values.length(); i = i + 1 {
    if field.values[i] == value {
      return true
    }
  }
  false
}

///|
/// Check if a value is in a field, or the field is "all values".
fn value_in_field_or_all(field : CronField, value : Int) -> Bool {
  field_is_all(field) || value_in_field(field, value)
}

///|
/// Check if a field represents all values.
fn field_is_all(field : CronField) -> Bool {
  let (min, max) = field_range(field.field_type)
  if field.values.length() != max - min + 1 {
    return false
  }
  let mut expected = min
  for i = 0; i < field.values.length(); i = i + 1 {
    if field.values[i] != expected {
      return false
    }
    expected = expected + 1
  }
  true
}

///|
/// Find the next value in a field greater than the given value.
fn next_value_in_field(field : CronField, current : Int) -> Int? {
  let mut found : Int? = None
  for i = 0; i < field.values.length(); i = i + 1 {
    if field.values[i] > current {
      match found {
        Some(f) =>
          if field.values[i] < f {
            found = Some(field.values[i])
          } else {
            ()
          }
        None => found = Some(field.values[i])
      }
    }
  }
  found
}

///|
/// Check if a DateTime's day matches the day-of-week field.
fn matches_dow(field : CronField, dt : DateTime) -> Bool {
  let dow = day_of_week(dt.year, dt.month, dt.day)
  for i = 0; i < field.values.length(); i = i + 1 {
    let v = field.values[i]
    if v == dow || (v == 7 && dow == 0) {
      return true
    }
  }
  false
}

///|
/// Get all execution times in a date range (inclusive).
pub fn between(
  fields : Array[CronField],
  start : DateTime,
  end : DateTime,
) -> Array[DateTime] {
  let result : Array[DateTime] = []
  let mut current = start
  // If start matches, include it
  if matches_time(fields, start) {
    result.push(start)
  }
  while true {
    match next_after(fields, current) {
      Some(next) => {
        if next.gt(end) {
          break
        }
        result.push(next)
        current = next
      }
      None => break
    }
  }
  result
}

///|
/// Count how many times an expression fires in a date range.
pub fn count_between(
  fields : Array[CronField],
  start : DateTime,
  end : DateTime,
) -> Int {
  let mut count = 0
  if matches_time(fields, start) {
    count = count + 1
  }
  let mut current = start
  while true {
    match next_after(fields, current) {
      Some(next) => {
        if next.gt(end) {
          break
        }
        count = count + 1
        current = next
      }
      None => break
    }
  }
  count
}

///|
/// Check if there are any execution times in a date range.
pub fn is_any_in_range(
  fields : Array[CronField],
  start : DateTime,
  end : DateTime,
) -> Bool {
  if matches_time(fields, start) {
    return true
  }
  match next_after(fields, start) {
    Some(next) => !next.gt(end)
    None => false
  }
}

///|
/// Find the previous execution time before a given reference time.
pub fn prev_before(fields : Array[CronField], before : DateTime) -> DateTime? {
  // Search backwards by iterating forward from a much earlier point
  // and finding the last execution before the reference time.
  let mut search_start = DateTime::new(before.year - 2, 1, 1, 0, 0)
  let mut last_match : DateTime? = None

  while true {
    match next_after(fields, search_start) {
      Some(next) => {
        if next.ge(before) {
          // We've passed the reference time
          break
        }
        last_match = Some(next)
        search_start = next
      }
      None => break
    }
  }
  last_match
}

///|
/// Estimate the next N execution times with a maximum search bound.
/// Returns fewer than N if not enough matches exist within the bound.
pub fn next_n_bounded(
  fields : Array[CronField],
  after : DateTime,
  n : Int,
  max_bound : DateTime,
) -> Array[DateTime] {
  let result : Array[DateTime] = []
  let mut current = after
  for i = 0; i < n; i = i + 1 {
    match next_after(fields, current) {
      Some(next_time) => {
        if next_time.gt(max_bound) {
          break
        }
        result.push(next_time)
        current = next_time
      }
      None => break
    }
  }
  result
}