/// RFC 5322 date-time parsing and formatting.
///
/// The Date header in email messages follows the date-time production
/// defined in RFC 5322 section 3.3:
///
///   date-time = [ day-of-week "," ] date time
///   date      = day month year
///   time      = time-of-day [ zone ]
///
/// This module parses such values into the structured EmailDate type
/// and provides validation helpers.

///|
/// Parse an RFC 5322 date-time string into an EmailDate.
///
/// Accepts formats such as:
///   "Mon, 01 Jan 2026 12:34:56 +0800"
///   "1 Jan 2026 12:34:56 GMT"
///   "Tue, 2 Feb 2026 08:00:00 -0500 (EST)"
///
/// Returns None if the value cannot be parsed.
pub fn parse_email_date(input : String) -> EmailDate? {
  let s = input.trim(chars=" \t\r\n").to_owned()
  if s.length() == 0 {
    return None
  }

  // Split into tokens by whitespace
  let tokens = tokenize_ws(s)

  // Find the comma-separated day-of-week prefix if present
  let mut start = 0
  if tokens.length() >= 1 && tokens[0].has_suffix(",") {
    start = 1
  }

  // Now expect: day month year time [zone]
  // Some dates include an obsolete two-digit year or missing time.
  if tokens.length() < start + 4 {
    return None
  }

  // Day of month
  let day_str = tokens[start]
  let day = parse_int(day_str)
  match day {
    None => None
    Some(d) if d >= 1 && d <= 31 => {
      // Month
      let month_str = tokens[start + 1]
      let month = month_name_to_num(month_str)
      match month {
        None => None
        Some(m) => {
          // Year
          let year_str = tokens[start + 2]
          let year = parse_year(year_str)
          match year {
            None => None
            Some(y) => {
              // Time (HH:MM[:SS])
              let time_str = tokens[start + 3]
              let time = parse_time(time_str)
              match time {
                None => None
                Some((hour, minute, second)) => {
                  // Optional zone
                  let tz = if tokens.length() > start + 4 {
                    parse_timezone(tokens[start + 4])
                  } else {
                    Some((0, 0, 1))
                  }
                  match tz {
                    None => None
                    Some((tz_h, tz_m, tz_sign)) =>
                      Some(EmailDate::{
                        year: y,
                        month: m,
                        day: d,
                        hour,
                        minute,
                        second,
                        tz_hour: tz_h,
                        tz_minute: tz_m,
                        tz_sign,
                      })
                  }
                }
              }
            }
          }
        }
      }
    }
    Some(_) => None
  }
}

///|
/// Split a string into whitespace-separated tokens.
fn tokenize_ws(input : String) -> Array[String] {
  let tokens : Array[String] = []
  let mut sb = StringBuilder::new()
  let mut in_token = false
  for c in input {
    if c == ' ' || c == '\t' {
      if in_token {
        tokens.push(sb.to_string())
        sb = StringBuilder::new()
        in_token = false
      }
    } else if c == '(' || c == ')' {
      // Comments: skip everything until matching close paren
      if in_token {
        tokens.push(sb.to_string())
        sb = StringBuilder::new()
        in_token = false
      }
    } else {
      sb.write_char(c)
      in_token = true
    }
  }
  if in_token {
    tokens.push(sb.to_string())
  }
  tokens
}

///|
/// Parse a decimal integer from a string.
fn parse_int(s : String) -> Int? {
  if s.length() == 0 {
    return None
  }
  let mut result = 0
  let mut i = 0
  let len = s.length()
  let mut started = false
  while i < len {
    let c = s.get_char(i).unwrap()
    if c >= '0' && c <= '9' {
      result = result * 10 + (c.to_int() - '0'.to_int())
      started = true
    } else {
      return None
    }
    i = i + 1
  }
  if started {
    Some(result)
  } else {
    None
  }
}

///|
/// Parse a year, handling two-digit and four-digit forms.
fn parse_year(s : String) -> Int? {
  let len = s.length()
  if len == 2 {
    let y = parse_int(s)
    match y {
      None => None
      Some(v) =>
        // RFC 5322: two-digit years 00-49 map to 2000-2049,
        // 50-99 map to 1950-1999.
        if v >= 50 {
          Some(1900 + v)
        } else {
          Some(2000 + v)
        }
    }
  } else if len == 4 {
    parse_int(s)
  } else {
    None
  }
}

///|
/// Parse a time of day "HH:MM[:SS]".
fn parse_time(s : String) -> (Int, Int, Int)? {
  let parts = split_char(s, ':')
  if parts.length() < 2 {
    return None
  }
  let hour = parse_int(parts[0])
  let minute = parse_int(parts[1])
  let second = if parts.length() >= 3 { parse_int(parts[2]) } else { Some(0) }
  match (hour, minute, second) {
    (Some(h), Some(m), Some(sec)) =>
      if h >= 0 && h <= 23 && m >= 0 && m <= 59 && sec >= 0 && sec <= 60 {
        Some((h, m, sec))
      } else {
        None
      }
    _ => None
  }
}

///|
/// Split a string by a delimiter character into an array of strings.
fn split_char(s : String, delim : Char) -> Array[String] {
  let parts : Array[String] = []
  let mut sb = StringBuilder::new()
  for c in s {
    if c == delim {
      parts.push(sb.to_string())
      sb = StringBuilder::new()
    } else {
      sb.write_char(c)
    }
  }
  parts.push(sb.to_string())
  parts
}

///|
/// Map a three-letter month abbreviation to its number (1-12).
fn month_name_to_num(s : String) -> Int? {
  let lower = s.to_lower()
  if lower == "jan" {
    Some(1)
  } else if lower == "feb" {
    Some(2)
  } else if lower == "mar" {
    Some(3)
  } else if lower == "apr" {
    Some(4)
  } else if lower == "may" {
    Some(5)
  } else if lower == "jun" {
    Some(6)
  } else if lower == "jul" {
    Some(7)
  } else if lower == "aug" {
    Some(8)
  } else if lower == "sep" {
    Some(9)
  } else if lower == "oct" {
    Some(10)
  } else if lower == "nov" {
    Some(11)
  } else if lower == "dec" {
    Some(12)
  } else {
    None
  }
}

///|
/// Parse a timezone token into (hour, minute, sign).
/// Supports numeric offsets (+0800, -0500, +05:30) and named zones.
fn parse_timezone(s : String) -> (Int, Int, Int)? {
  if s.length() == 0 {
    return Some((0, 0, 1))
  }

  // Named zones
  let upper = s.to_upper()
  match upper {
    "UT" | "UTC" | "GMT" | "Z" => return Some((0, 0, 1))
    "EST" => return Some((5, 0, -1))
    "EDT" => return Some((4, 0, -1))
    "CST" => return Some((6, 0, -1))
    "CDT" => return Some((5, 0, -1))
    "MST" => return Some((7, 0, -1))
    "MDT" => return Some((6, 0, -1))
    "PST" => return Some((8, 0, -1))
    "PDT" => return Some((7, 0, -1))
    _ => ()
  }

  // Numeric zone: sign + 4 digits (HHMM) or sign + HH:MM
  if s.length() < 2 {
    return None
  }
  let sign_char = s.get_char(0).unwrap()
  let sign = if sign_char == '-' {
    -1
  } else if sign_char == '+' {
    1
  } else {
    return None
  }
  let rest = s.unsafe_substring(start=1, end=s.length())

  if rest.length() == 4 {
    let hh = parse_int(rest.unsafe_substring(start=0, end=2))
    let mm = parse_int(rest.unsafe_substring(start=2, end=4))
    match (hh, mm) {
      (Some(h), Some(m)) =>
        if h <= 23 && m <= 59 {
          Some((h, m, sign))
        } else {
          None
        }
      _ => None
    }
  } else if rest.contains(":") {
    let parts = split_char(rest, ':')
    if parts.length() == 2 {
      let hh = parse_int(parts[0])
      let mm = parse_int(parts[1])
      match (hh, mm) {
        (Some(h), Some(m)) =>
          if h <= 23 && m <= 59 {
            Some((h, m, sign))
          } else {
            None
          }
        _ => None
      }
    } else {
      None
    }
  } else {
    None
  }
}

///|
/// Validate a parsed EmailDate: checks ranges and month/day consistency.
pub fn validate_date(d : EmailDate) -> Bool {
  if d.month < 1 || d.month > 12 {
    return false
  }
  if d.day < 1 || d.day > days_in_month(d.year, d.month) {
    return false
  }
  if d.hour < 0 || d.hour > 23 {
    return false
  }
  if d.minute < 0 || d.minute > 59 {
    return false
  }
  if d.second < 0 || d.second > 60 {
    return false
  }
  if d.tz_hour < 0 || d.tz_hour > 23 || d.tz_minute < 0 || d.tz_minute > 59 {
    return false
  }
  true
}

///|
/// Get the number of days in a given month, accounting for leap years.
pub fn days_in_month(year : Int, month : Int) -> Int {
  match month {
    1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
    4 | 6 | 9 | 11 => 30
    2 => if is_leap_year(year) { 29 } else { 28 }
    _ => 0
  }
}

///|
/// Check whether a year is a leap year.
pub fn is_leap_year(year : Int) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

///|
/// Get the numeric timezone offset of an EmailDate in minutes.
pub fn tz_offset_minutes(d : EmailDate) -> Int {
  (d.tz_hour * 60 + d.tz_minute) * d.tz_sign
}

///|
/// Convert an EmailDate to a Unix-like timestamp (seconds since epoch).
/// Uses a simplified days-from-civil algorithm.
pub fn to_timestamp(d : EmailDate) -> Int {
  let y = if d.month <= 2 { d.year - 1 } else { d.year }
  let y_base = if y >= 0 { y } else { y - 399 }
  let era = y_base / 400
  let yoe = y - era * 400
  let mp = (d.month + 9) % 12
  let doy = (153 * mp + 2) / 5 + d.day - 1
  let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
  let days = era * 146097 + doe - 719468
  days * 86400 +
  d.hour * 3600 +
  d.minute * 60 +
  d.second -
  tz_offset_minutes(d) * 60
}

///|
/// Parse a Date header value from a Message and return the EmailDate.
pub fn parse_date_header(value : String) -> EmailDate? {
  parse_email_date(value)
}

///|
/// Get the parsed Date from a Message, if present and valid.
pub fn message_date(msg : Message) -> EmailDate? {
  let value = get_header(msg, "Date")
  match value {
    None => None
    Some(v) => parse_email_date(v)
  }
}

///|
/// Format an EmailDate in RFC 5322 format with full weekday.
pub fn format_rfc5322_date(d : EmailDate) -> String {
  email_date_to_string(d)
}

///|
/// Compare two EmailDates: returns -1, 0, or 1 based on their timestamps.
pub fn compare_dates(a : EmailDate, b : EmailDate) -> Int {
  let ta = to_timestamp(a)
  let tb = to_timestamp(b)
  if ta < tb {
    -1
  } else if ta > tb {
    1
  } else {
    0
  }
}