///|
/// Cron expression frequency and schedule analyzer.
/// Analyzes how many times a cron expression fires per time period,
/// schedule density, and other statistical properties.

///|
/// Result of a frequency analysis.
pub struct FrequencyAnalysis {
  per_minute : Int // max executions per minute
  per_hour : Int // max executions per hour
  per_day : Int // max executions per day
  per_week : Int // max executions per week
  per_month : Int // max executions per month (best case)
  per_year : Int // max executions per year
  max_gap_minutes : Int // longest gap between executions (minutes)
  avg_gap_minutes : Int // average gap between executions (minutes)
}

///|
/// Analyze the frequency of a cron expression.
/// Returns statistics about how often the expression fires.
pub fn analyze_frequency(fields : Array[CronField]) -> FrequencyAnalysis {
  let per_minute = execs_per_minute(fields)
  let per_hour = execs_per_hour(fields)
  let per_day = execs_per_day(fields)
  let per_week = execs_per_week(fields)
  let per_month = execs_per_month(fields)
  let per_year = execs_per_year(fields)
  let max_gap = max_gap_between_executions(fields)
  let avg_gap = avg_gap_between_executions(fields)

  {
    per_minute,
    per_hour,
    per_day,
    per_week,
    per_month,
    per_year,
    max_gap_minutes: max_gap,
    avg_gap_minutes: avg_gap,
  }
}

///|
/// Offset for 6-field format: index 0 = seconds, so minutes are at index 1.
fn field_offset(fields : Array[CronField]) -> Int {
  if fields.length() == 6 {
    1
  } else {
    0
  }
}

///|
/// Maximum possible executions in a single minute.
fn execs_per_minute(fields : Array[CronField]) -> Int {
  if fields.length() == 6 {
    fields[0].values.length() // seconds determine per-minute rate
  } else {
    1 // standard cron: at most 1 per minute
  }
}

///|
/// Maximum possible executions per hour.
fn execs_per_hour(fields : Array[CronField]) -> Int {
  let off = field_offset(fields)
  let mins = fields[off].values.length() // minute field
  let secs = if fields.length() == 6 { fields[0].values.length() } else { 1 }
  mins * secs
}

///|
/// Maximum possible executions per day.
fn execs_per_day(fields : Array[CronField]) -> Int {
  let per_hour = execs_per_hour(fields)
  let off = field_offset(fields)
  let hours = fields[1 + off].values.length() // hour field
  per_hour * hours
}

///|
/// Maximum possible executions per week.
fn execs_per_week(fields : Array[CronField]) -> Int {
  let per_day = execs_per_day(fields)
  let off = field_offset(fields)
  let dom_field = fields[2 + off]
  let dow_field = fields[3 + off + 1]

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

  if dow_restricted {
    // Count matching days of week
    let matching_days = dow_field.values.length()
    per_day * matching_days
  } else if dom_restricted {
    // Day of month covers all days in a 31-day month pattern
    per_day * dom_field.values.length()
  } else {
    per_day * 7
  }
}

///|
/// Maximum possible executions per month (best case = 31-day month).
fn execs_per_month(fields : Array[CronField]) -> Int {
  let per_day = execs_per_day(fields)
  let off = field_offset(fields)
  let dom_field = fields[2 + off]
  let dow_field = fields[4 + off]

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

  let days_per_month = if dom_restricted {
    dom_field.values.length()
  } else if dow_restricted {
    // Approximate: ~4.3 weeks * matching days
    let weekly_matches = dow_field.values.length()
    weekly_matches * 31 / 7
  } else {
    31
  }

  per_day * days_per_month
}

///|
/// Maximum possible executions per year.
fn execs_per_year(fields : Array[CronField]) -> Int {
  let per_day = execs_per_day(fields)
  let off = field_offset(fields)
  let dom_field = fields[2 + off]
  let month_field = fields[3 + off]
  let dow_field = fields[4 + off]

  let dom_restricted = !field_is_all_v(dom_field)
  let dow_restricted = !field_is_all_v(dow_field)
  let month_restricted = !field_is_all_v(month_field)

  let days_per_year = if dom_restricted && month_restricted {
    // Count actual days per year based on month and day combinations
    dom_field.values.length() * month_field.values.length()
  } else if dom_restricted {
    dom_field.values.length() * 12
  } else if dow_restricted {
    // Approx 52 weeks * matching weekdays
    dow_field.values.length() * 52
  } else {
    365
  }

  per_day * days_per_year
}

///|
/// Estimate the maximum gap between consecutive executions, in minutes.
fn max_gap_between_executions(fields : Array[CronField]) -> Int {
  let off = field_offset(fields)
  let hour_field = fields[1 + off]
  let minute_field = fields[off]
  let min_values = minute_field.values
  let hour_values = hour_field.values

  if min_values.length() == 0 {
    return 525600 // effectively never
  }

  // Find max gap between consecutive minutes
  let mut max_minute_gap = 0
  for i = 0; i < min_values.length() - 1; i = i + 1 {
    let gap = min_values[i + 1] - min_values[i]
    if gap > max_minute_gap {
      max_minute_gap = gap
    }
  }
  // Also check wrap-around: 59 - last + first + 1
  let wrap_gap = 60 - min_values[min_values.length() - 1] + min_values[0]
  if wrap_gap > max_minute_gap {
    max_minute_gap = wrap_gap
  }

  // Find max gap between consecutive hours
  let mut max_hour_gap = 0
  if hour_values.length() > 0 {
    for i = 0; i < hour_values.length() - 1; i = i + 1 {
      let gap = hour_values[i + 1] - hour_values[i]
      if gap > max_hour_gap {
        max_hour_gap = gap
      }
    }
    let wrap_hour_gap = 24 -
      hour_values[hour_values.length() - 1] +
      hour_values[0]
    if wrap_hour_gap > max_hour_gap {
      max_hour_gap = wrap_hour_gap
    }
  }

  // The max gap is the larger of:
  // 1. The gap within an hour (max_minute_gap minutes)
  // 2. The gap between the last minute of one hour and first of the next
  if max_hour_gap > 0 {
    let inter_hour_gap = 60 -
      min_values[min_values.length() - 1] +
      min_values[0] +
      (max_hour_gap - 1) * 60
    if inter_hour_gap > max_minute_gap {
      max_minute_gap = inter_hour_gap
    }
  }

  max_minute_gap
}

///|
/// Estimate average gap between executions, in minutes.
fn avg_gap_between_executions(fields : Array[CronField]) -> Int {
  let per_day = execs_per_day(fields)
  if per_day == 0 {
    return 525600
  }
  // Average gap = total minutes in a day / number of executions per day
  1440 / per_day
}

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

///|
/// Format a FrequencyAnalysis as a human-readable string.
pub fn frequency_to_string(analysis : FrequencyAnalysis) -> String {
  let mut result = ""
  result = result + "Frequency Analysis:\n"
  result = result + "  Per minute: " + analysis.per_minute.to_string() + "\n"
  result = result + "  Per hour:   " + analysis.per_hour.to_string() + "\n"
  result = result + "  Per day:    " + analysis.per_day.to_string() + "\n"
  result = result + "  Per week:   " + analysis.per_week.to_string() + "\n"
  result = result + "  Per month:  " + analysis.per_month.to_string() + "\n"
  result = result + "  Per year:   " + analysis.per_year.to_string() + "\n"
  result = result +
    "  Max gap:    " +
    analysis.max_gap_minutes.to_string() +
    " minutes\n"
  result = result +
    "  Avg gap:    " +
    analysis.avg_gap_minutes.to_string() +
    " minutes"
  result
}

///|
/// Get a schedule density as a category.
pub enum ScheduleDensity {
  VerySparse // < 1 per day
  Sparse // 1-5 per day
  Moderate // 6-24 per day
  Frequent // 25-144 per day
  Dense // > 144 per day
}

///|
/// Classify the schedule density.
pub fn classify_density(analysis : FrequencyAnalysis) -> ScheduleDensity {
  let per_day = analysis.per_day
  let per_year = analysis.per_year
  // If it fires less than once per day on average over a year
  if per_year < 365 {
    VerySparse
  } else if per_day < 1 {
    VerySparse
  } else if per_day <= 5 {
    Sparse
  } else if per_day <= 24 {
    Moderate
  } else if per_day <= 144 {
    Frequent
  } else {
    Dense
  }
}

///|
/// Get a human-readable density label.
pub fn density_to_string(density : ScheduleDensity) -> String {
  match density {
    VerySparse => "Very sparse (less than once per day)"
    Sparse => "Sparse (1-5 times per day)"
    Moderate => "Moderate (6-24 times per day)"
    Frequent => "Frequent (25-144 times per day)"
    Dense => "Dense (more than 144 times per day)"
  }
}

///|
/// Check if a cron expression would ever fire.
pub fn is_impossible(fields : Array[CronField]) -> Bool {
  // Check for Feb 30 or other impossible day-month combinations
  let off = field_offset(fields)
  let dom_field = fields[2 + off]
  let month_field = fields[3 + off]

  // If day-of-month has values > 30 and month only includes February
  if !field_is_all_v(month_field) {
    let month_vals = month_field.values
    let only_feb = month_vals.length() == 1 && month_vals[0] == 2
    if only_feb {
      for i = 0; i < dom_field.values.length(); i = i + 1 {
        if dom_field.values[i] > 29 {
          return true
        }
      }
    }

    // Check each month-day combination
    let mut possible = false
    for mi = 0; mi < month_vals.length() && !possible; mi = mi + 1 {
      let m = month_vals[mi]
      if m < 1 || m > 12 {
        return true
      }
      for di = 0; di < dom_field.values.length() && !possible; di = di + 1 {
        let d = dom_field.values[di]
        if d >= 1 && d <= 31 {
          // Check rough validity (Feb 30 is impossible regardless of year)
          if m == 2 && d > 29 {
            // impossible
          } else if (m == 4 || m == 6 || m == 9 || m == 11) && d == 31 {
            // impossible
          } else {
            possible = true
          }
        }
      }
    }
    !possible
  } else {
    false
  }
}