///|
/// An HTTP-date (RFC 9110 §5.6.7) in Unix milliseconds: IMF-fixdate, or one of
/// the two obsolete forms a recipient must also accept, rfc850-date and
/// asctime-date. HTTP-date is case-sensitive. `now_unix_ms` places
/// rfc850-date's two-digit year.
fn http_date_ms(s : String, now_unix_ms : Int64) -> Int64? {
  match imf_fixdate(s) {
    Some(ms) => Some(ms)
    None =>
      match rfc850_date(s, now_unix_ms) {
        Some(ms) => Some(ms)
        None => asctime_date(s)
      }
  }
}

///|
/// `Sun, 06 Nov 1994 08:49:37 GMT`
fn imf_fixdate(s : String) -> Int64? {
  guard s.length() == 29 &&
    s[3] == ',' &&
    s[4] == ' ' &&
    s[7] == ' ' &&
    s[11] == ' ' &&
    s[16] == ' ' &&
    s.has_suffix(" GMT") else {
    return None
  }
  guard short_day_name(s, 0) is Some(weekday) else { return None }
  guard date_digits(s, 5, 2) is Some(day) else { return None }
  guard month_name(s, 8) is Some(month) else { return None }
  guard date_digits(s, 12, 4) is Some(year) else { return None }
  guard time_of_day(s, 17) is Some(time) else { return None }
  date_ms(weekday, year, month, day, time)
}

///|
/// `Sunday, 06-Nov-94 08:49:37 GMT`. A two-digit year that would put the date
/// more than 50 years after `now_unix_ms` is taken from the century before.
fn rfc850_date(s : String, now_unix_ms : Int64) -> Int64? {
  guard s.find(", ") is Some(comma) else { return None }
  guard long_day_name(s[:comma]) is Some(weekday) else { return None }
  let at = comma + 2
  guard s.length() == at + 22 &&
    s[at + 2] == '-' &&
    s[at + 6] == '-' &&
    s[at + 9] == ' ' &&
    s.has_suffix(" GMT") else {
    return None
  }
  guard date_digits(s, at, 2) is Some(day) else { return None }
  guard month_name(s, at + 3) is Some(month) else { return None }
  guard date_digits(s, at + 7, 2) is Some(two_digits) else { return None }
  guard time_of_day(s, at + 10) is Some(time) else { return None }
  let now_year = year_of(now_unix_ms)
  let year = now_year - now_year % 100 + two_digits
  let year = if year > now_year + 50 { year - 100 } else { year }
  date_ms(weekday, year, month, day, time)
}

///|
/// `Sun Nov  6 08:49:37 1994`: the day is two digits, or a space and one.
fn asctime_date(s : String) -> Int64? {
  guard s.length() == 24 &&
    s[3] == ' ' &&
    s[7] == ' ' &&
    s[10] == ' ' &&
    s[19] == ' ' else {
    return None
  }
  guard short_day_name(s, 0) is Some(weekday) else { return None }
  guard month_name(s, 4) is Some(month) else { return None }
  let day = if s[8] == ' ' {
    date_digits(s, 9, 1)
  } else {
    date_digits(s, 8, 2)
  }
  guard day is Some(day) else { return None }
  guard time_of_day(s, 11) is Some(time) else { return None }
  guard date_digits(s, 20, 4) is Some(year) else { return None }
  date_ms(weekday, year, month, day, time)
}

///|
fn date_digits(s : String, start : Int32, count : Int32) -> Int32? {
  let mut n = 0
  for i = start; i < start + count; i = i + 1 {
    guard s[i] >= '0' && s[i] <= '9' else { return None }
    n = n * 10 + s[i].to_int() - 48
  }
  Some(n)
}

///|
/// `HH:MM:SS` at `at`, unvalidated.
fn time_of_day(s : String, at : Int32) -> (Int32, Int32, Int32)? {
  guard s[at + 2] == ':' && s[at + 5] == ':' else { return None }
  guard date_digits(s, at, 2) is Some(hour) else { return None }
  guard date_digits(s, at + 3, 2) is Some(minute) else { return None }
  guard date_digits(s, at + 6, 2) is Some(second) else { return None }
  Some((hour, minute, second))
}

///|
/// `day-name` at `at`, Sunday as 0.
fn short_day_name(s : String, at : Int32) -> Int32? {
  match (s[at], s[at + 1], s[at + 2]) {
    ('S', 'u', 'n') => Some(0)
    ('M', 'o', 'n') => Some(1)
    ('T', 'u', 'e') => Some(2)
    ('W', 'e', 'd') => Some(3)
    ('T', 'h', 'u') => Some(4)
    ('F', 'r', 'i') => Some(5)
    ('S', 'a', 't') => Some(6)
    _ => None
  }
}

///|
/// `day-name-l`, Sunday as 0.
fn long_day_name(name : StringView) -> Int32? {
  match name {
    "Sunday" => Some(0)
    "Monday" => Some(1)
    "Tuesday" => Some(2)
    "Wednesday" => Some(3)
    "Thursday" => Some(4)
    "Friday" => Some(5)
    "Saturday" => Some(6)
    _ => None
  }
}

///|
fn month_name(s : String, at : Int32) -> Int32? {
  match (s[at], s[at + 1], s[at + 2]) {
    ('J', 'a', 'n') => Some(1)
    ('F', 'e', 'b') => Some(2)
    ('M', 'a', 'r') => Some(3)
    ('A', 'p', 'r') => Some(4)
    ('M', 'a', 'y') => Some(5)
    ('J', 'u', 'n') => Some(6)
    ('J', 'u', 'l') => Some(7)
    ('A', 'u', 'g') => Some(8)
    ('S', 'e', 'p') => Some(9)
    ('O', 'c', 't') => Some(10)
    ('N', 'o', 'v') => Some(11)
    ('D', 'e', 'c') => Some(12)
    _ => None
  }
}

///|
/// Validates the fields, the weekday included, and converts them to Unix
/// milliseconds. Second 60 is accepted and lands on the next second.
fn date_ms(
  weekday : Int32,
  year : Int32,
  month : Int32,
  day : Int32,
  time : (Int32, Int32, Int32),
) -> Int64? {
  let (hour, minute, second) = time
  let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
  let days = match month {
    2 => if leap { 29 } else { 28 }
    4 | 6 | 9 | 11 => 30
    _ => 31
  }
  guard year >= 1 &&
    day >= 1 &&
    day <= days &&
    hour < 24 &&
    minute < 60 &&
    second <= 60 else {
    return None
  }
  let days = days_from_civil(year, month, day)
  guard ((days + 4) % 7 + 7) % 7 == weekday else { return None }
  Some(
    ((days.to_int64() * 24 + hour.to_int64()) * 60 + minute.to_int64()) * 60000 +
    second.to_int64() * 1000,
  )
}

///|
/// The Gregorian year of a Unix time, clamped to 0–99999 so that an extreme
/// clock cannot overflow the year arithmetic.
fn year_of(unix_ms : Int64) -> Int32 {
  let day = if unix_ms >= 0L {
    unix_ms / 86400000L
  } else {
    (unix_ms + 1L) / 86400000L - 1L
  }
  // The inverse of `days_from_civil`, on March-based eras.
  let z = day + 719468L
  let era = (if z >= 0L { z } else { z - 146096L }) / 146097L
  let doe = z - era * 146097L
  let yoe = (doe - doe / 1460L + doe / 36524L - doe / 146096L) / 365L
  let doy = doe - (365L * yoe + yoe / 4L - yoe / 100L)
  let march_month = (5L * doy + 2L) / 153L
  let year = yoe + era * 400L + (if march_month >= 10L { 1L } else { 0L })
  year.max(0L).min(99999L).to_int()
}

///|
// March-based Gregorian eras, with 1970-01-01 as day zero.
fn days_from_civil(year : Int32, month : Int32, day : Int32) -> Int32 {
  let y = year - (if month <= 2 { 1 } else { 0 })
  let era = y / 400
  let yoe = y - era * 400
  let m = month + (if month > 2 { -3 } else { 9 })
  let doy = (153 * m + 2) / 5 + day - 1
  era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468
}