///|
/// Time matching against cron expressions.
/// Determines whether a given DateTime satisfies a parsed cron expression.

///|
/// Check if a DateTime matches all fields of a cron expression.
pub fn matches_time(fields : Array[CronField], dt : DateTime) -> Bool {
  if fields.length() != 5 && fields.length() != 6 {
    return false
  }
  let offset = if fields.length() == 6 { 1 } else { 0 }
  let second_matches = if offset == 1 {
    match_field(fields, 0, dt.second)
  } else {
    dt.second == 0
  }
  second_matches &&
  match_field(fields, offset, dt.minute) &&
  match_field(fields, offset + 1, dt.hour) &&
  matches_day_constraint(fields, dt) &&
  match_field(fields, offset + 3, dt.month)
}

///|
/// Check if a value matches a specific field.
fn match_field(fields : Array[CronField], index : Int, value : Int) -> Bool {
  if index >= fields.length() {
    return false
  }
  let field = fields[index]
  for i = 0; i < field.values.length(); i = i + 1 {
    if field.values[i] == value {
      return true
    }
  }
  false
}

///|
/// Check day-of-month field.
fn match_day_of_month(
  fields : Array[CronField],
  index : Int,
  dt : DateTime,
) -> Bool {
  if index >= fields.length() {
    return false
  }
  let field = fields[index]
  for i = 0; i < field.values.length(); i = i + 1 {
    if field.values[i] == dt.day {
      return true
    }
  }
  false
}

///|
/// Check day-of-week field. Handles the mapping where both 0 and 7 represent Sunday.
fn match_day_of_week(
  fields : Array[CronField],
  index : Int,
  dt : DateTime,
) -> Bool {
  if index >= fields.length() {
    return false
  }
  let field = fields[index]
  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
}

///|
/// Check if any day-of-week or day-of-month would match.
/// When both day-of-month and day-of-week are not restricted to all values,
/// cron semantics say a match on EITHER is sufficient (OR logic).
pub fn matches_day_constraint(fields : Array[CronField], dt : DateTime) -> Bool {
  if fields.length() != 5 && fields.length() != 6 {
    return false
  }
  let offset = if fields.length() == 6 { 1 } else { 0 }
  let dom_index = offset + 2
  let dow_index = offset + 4
  let dom_field = fields[dom_index]
  let dow_field = fields[dow_index]

  let dom_restricted = !is_star_field(dom_field)
  let dow_restricted = !is_star_field(dow_field)

  if dom_restricted && dow_restricted {
    // OR logic: match on either day-of-month or day-of-week
    match_day_of_month(fields, dom_index, dt) ||
    match_day_of_week(fields, dow_index, dt)
  } else if dom_restricted {
    match_day_of_month(fields, dom_index, dt)
  } else if dow_restricted {
    match_day_of_week(fields, dow_index, dt)
  } else {
    true
  }
}

///|
/// Check if a field represents the "all values" wildcard.
fn is_star_field(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
}