///|
/// HTTP-date parsing errors
pub(all) enum DateError {
  Empty
  InvalidFormat
  InvalidDayName
  InvalidDay
  InvalidMonth
  InvalidYear
  InvalidHour
  InvalidMinute
  InvalidSecond
  NotGmt
} derive(Show, Eq)

///|
/// Day of week
pub(all) enum DayOfWeek {
  Sunday
  Monday
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
} derive(Show, Eq)

///|
/// HTTP-date structure (RFC 9110 Section 5.6.7)
///
/// Supports three formats:
/// - IMF-fixdate: Sun, 06 Nov 1994 08:49:37 GMT (preferred)
/// - RFC 850: Sunday, 06-Nov-94 08:49:37 GMT (obsolete)
/// - ANSI C asctime: Sun Nov  6 08:49:37 1994 (obsolete)
pub(all) struct HttpDate {
  day_of_week : DayOfWeek
  day : Int
  month : Int
  year : Int
  hour : Int
  minute : Int
  second : Int
} derive(Show, Eq)

///|
pub fn HttpDate::new(
  day_of_week : DayOfWeek,
  day : Int,
  month : Int,
  year : Int,
  hour : Int,
  minute : Int,
  second : Int,
) -> Result[HttpDate, DateError] {
  if day < 1 || day > 31 {
    return Err(InvalidDay)
  }
  if month < 1 || month > 12 {
    return Err(InvalidMonth)
  }
  if year < 1 {
    return Err(InvalidYear)
  }
  if hour > 23 {
    return Err(InvalidHour)
  }
  if minute > 59 {
    return Err(InvalidMinute)
  }
  if second > 60 {
    return Err(InvalidSecond)
  }
  Ok({ day_of_week, day, month, year, hour, minute, second })
}

///|
pub fn HttpDate::parse(input : String) -> Result[HttpDate, DateError] {
  let s = trim_string(input)
  if s.length() == 0 {
    return Err(Empty)
  }

  // Detect format by comma position
  match date_index_of(s, ",") {
    Some(comma_pos) => {
      let day_name = string_slice(s, 0, comma_pos)
      let rest = trim_string(string_slice_from(s, comma_pos + 1))

      // IMF-fixdate: Sun, 06 Nov 1994 08:49:37 GMT
      // RFC 850: Sunday, 06-Nov-94 08:49:37 GMT
      if date_index_of(rest, "-") != None {
        parse_rfc850(day_name, rest)
      } else {
        parse_imf_fixdate(day_name, rest)
      }
    }
    None =>
      // ANSI C asctime: Sun Nov  6 08:49:37 1994
      parse_asctime(s)
  }
}

///|
pub fn HttpDate::day_of_week(self : HttpDate) -> DayOfWeek {
  self.day_of_week
}

///|
pub fn HttpDate::day(self : HttpDate) -> Int {
  self.day
}

///|
pub fn HttpDate::month(self : HttpDate) -> Int {
  self.month
}

///|
pub fn HttpDate::year(self : HttpDate) -> Int {
  self.year
}

///|
pub fn HttpDate::hour(self : HttpDate) -> Int {
  self.hour
}

///|
pub fn HttpDate::minute(self : HttpDate) -> Int {
  self.minute
}

///|
pub fn HttpDate::second(self : HttpDate) -> Int {
  self.second
}

///|
pub fn HttpDate::to_string(self : HttpDate) -> String {
  let dow = day_of_week_short_name(self.day_of_week)
  let day = if self.day < 10 {
    "0" + self.day.to_string()
  } else {
    self.day.to_string()
  }
  let mon = month_name(self.month)
  let yr = self.year.to_string()
  let hr = if self.hour < 10 {
    "0" + self.hour.to_string()
  } else {
    self.hour.to_string()
  }
  let mn = if self.minute < 10 {
    "0" + self.minute.to_string()
  } else {
    self.minute.to_string()
  }
  let sc = if self.second < 10 {
    "0" + self.second.to_string()
  } else {
    self.second.to_string()
  }
  dow +
  ", " +
  day +
  " " +
  mon +
  " " +
  yr +
  " " +
  hr +
  ":" +
  mn +
  ":" +
  sc +
  " GMT"
}

///|
/// Helper functions

///|
fn day_of_week_short_name(dow : DayOfWeek) -> String {
  match dow {
    Sunday => "Sun"
    Monday => "Mon"
    Tuesday => "Tue"
    Wednesday => "Wed"
    Thursday => "Thu"
    Friday => "Fri"
    Saturday => "Sat"
  }
}

///|
fn day_of_week_from_name(s : String) -> DayOfWeek? {
  match s {
    "Sun" => Some(Sunday)
    "Sunday" => Some(Sunday)
    "Mon" => Some(Monday)
    "Monday" => Some(Monday)
    "Tue" => Some(Tuesday)
    "Tuesday" => Some(Tuesday)
    "Wed" => Some(Wednesday)
    "Wednesday" => Some(Wednesday)
    "Thu" => Some(Thursday)
    "Thursday" => Some(Thursday)
    "Fri" => Some(Friday)
    "Friday" => Some(Friday)
    "Sat" => Some(Saturday)
    "Saturday" => Some(Saturday)
    _ => None
  }
}

///|
fn parse_imf_fixdate(
  day_name : String,
  rest : String,
) -> Result[HttpDate, DateError] {
  match day_of_week_from_name(day_name) {
    None => Err(InvalidDayName)
    Some(dow) => {
      let parts = split_whitespace(rest)
      if parts.length() != 5 {
        return Err(InvalidFormat)
      }
      let day = match date_parse_int(parts[0]) {
        Some(d) => d
        None => return Err(InvalidDay)
      }
      let month = match parse_month(parts[1]) {
        Some(m) => m
        None => return Err(InvalidMonth)
      }
      let year = match date_parse_int(parts[2]) {
        Some(y) => y
        None => return Err(InvalidYear)
      }
      let (hour, minute, second) = match parse_time(parts[3]) {
        Some(t) => t
        None => return Err(InvalidFormat)
      }
      if parts[4] != "GMT" {
        return Err(NotGmt)
      }
      HttpDate::new(dow, day, month, year, hour, minute, second)
    }
  }
}

///|
fn parse_rfc850(
  day_name : String,
  rest : String,
) -> Result[HttpDate, DateError] {
  match day_of_week_from_name(day_name) {
    None => Err(InvalidDayName)
    Some(dow) => {
      let parts = split_whitespace(rest)
      if parts.length() != 3 {
        return Err(InvalidFormat)
      }

      // Parse day-month-year
      let date_parts = split_string(parts[0], "-")
      if date_parts.length() != 3 {
        return Err(InvalidFormat)
      }
      let day = match date_parse_int(date_parts[0]) {
        Some(d) => d
        None => return Err(InvalidDay)
      }
      let month = match parse_month(date_parts[1]) {
        Some(m) => m
        None => return Err(InvalidMonth)
      }
      let mut year = match date_parse_int(date_parts[2]) {
        Some(y) => y
        None => return Err(InvalidYear)
      }

      // RFC 2616 Section 19.3: 2-digit year correction
      // 00-49 -> 2000-2049, 50-99 -> 1950-1999
      if year < 100 {
        year = if year < 50 { 2000 + year } else { 1900 + year }
      }
      let (hour, minute, second) = match parse_time(parts[1]) {
        Some(t) => t
        None => return Err(InvalidFormat)
      }
      if parts[2] != "GMT" {
        return Err(NotGmt)
      }
      HttpDate::new(dow, day, month, year, hour, minute, second)
    }
  }
}

///|
fn parse_asctime(input : String) -> Result[HttpDate, DateError] {
  let parts = split_whitespace(input)
  if parts.length() != 5 {
    return Err(InvalidFormat)
  }
  match day_of_week_from_name(parts[0]) {
    None => Err(InvalidDayName)
    Some(dow) => {
      let month = match parse_month(parts[1]) {
        Some(m) => m
        None => return Err(InvalidMonth)
      }
      let day = match date_parse_int(parts[2]) {
        Some(d) => d
        None => return Err(InvalidDay)
      }
      let (hour, minute, second) = match parse_time(parts[3]) {
        Some(t) => t
        None => return Err(InvalidFormat)
      }
      let year = match date_parse_int(parts[4]) {
        Some(y) => y
        None => return Err(InvalidYear)
      }
      HttpDate::new(dow, day, month, year, hour, minute, second)
    }
  }
}

///|
fn parse_month(s : String) -> Int? {
  match s {
    "Jan" => Some(1)
    "Feb" => Some(2)
    "Mar" => Some(3)
    "Apr" => Some(4)
    "May" => Some(5)
    "Jun" => Some(6)
    "Jul" => Some(7)
    "Aug" => Some(8)
    "Sep" => Some(9)
    "Oct" => Some(10)
    "Nov" => Some(11)
    "Dec" => Some(12)
    _ => None
  }
}

///|
fn month_name(month : Int) -> String {
  match month {
    1 => "Jan"
    2 => "Feb"
    3 => "Mar"
    4 => "Apr"
    5 => "May"
    6 => "Jun"
    7 => "Jul"
    8 => "Aug"
    9 => "Sep"
    10 => "Oct"
    11 => "Nov"
    12 => "Dec"
    _ => "???"
  }
}

///|
fn parse_time(s : String) -> (Int, Int, Int)? {
  let parts = split_string(s, ":")
  if parts.length() != 3 {
    return None
  }
  let hour = match date_parse_int(parts[0]) {
    Some(h) => h
    None => return None
  }
  let minute = match date_parse_int(parts[1]) {
    Some(m) => m
    None => return None
  }
  let second = match date_parse_int(parts[2]) {
    Some(s) => s
    None => return None
  }
  if hour > 23 {
    return None
  }
  if minute > 59 {
    return None
  }
  if second > 60 {
    return None
  }
  Some((hour, minute, second))
}

///|
fn trim_string(s : String) -> String {
  let mut start = 0
  let len = s.length()
  while start < len {
    let ch = s[start]
    let ci = ch.to_int()
    if ci == 32 || ci == 9 || ci == 10 || ci == 13 {
      start = start + 1
    } else {
      break
    }
  }
  let mut end = len
  while end > start {
    let ch = s[end - 1]
    let ci = ch.to_int()
    if ci == 32 || ci == 9 || ci == 10 || ci == 13 {
      end = end - 1
    } else {
      break
    }
  }
  string_slice(s, start, end)
}

///|
fn date_index_of(s : String, needle : String) -> Int? {
  if needle.length() == 0 {
    return Some(0)
  }
  if needle.length() > s.length() {
    return None
  }
  let mut idx = 0
  while idx <= s.length() - needle.length() {
    let mut is_match = true
    let mut j = 0
    while j < needle.length() {
      if s[idx + j] != needle[j] {
        is_match = false
        break
      }
      j = j + 1
    }
    if is_match {
      return Some(idx)
    }
    idx = idx + 1
  }
  None
}

///|
fn split_whitespace(s : String) -> Array[String] {
  let result = []
  let mut current = ""
  let mut in_ws = false
  let mut idx = 0
  let len = s.length()
  while idx < len {
    let ch = s[idx]
    let ci = ch.to_int()
    if ci == 32 || ci == 9 || ci == 10 || ci == 13 {
      if !in_ws && current.length() > 0 {
        result.push(current)
        current = ""
      }
      in_ws = true
    } else {
      let ch_str = Int::unsafe_to_char(ci).to_string()
      current = current + ch_str
      in_ws = false
    }
    idx = idx + 1
  }
  if current.length() > 0 {
    result.push(current)
  }
  result
}

///|
fn split_string(s : String, sep : String) -> Array[String] {
  let result = []
  let mut start = 0
  let sep_len = sep.length()
  let mut idx = 0
  while idx <= s.length() - sep_len {
    let mut is_match = true
    let mut j = 0
    while j < sep_len {
      if s[idx + j] != sep[j] {
        is_match = false
        break
      }
      j = j + 1
    }
    if is_match {
      result.push(string_slice(s, start, idx))
      start = idx + sep_len
      idx = start
    } else {
      idx = idx + 1
    }
  }
  result.push(string_slice_from(s, start))
  result
}

///|
fn date_parse_int(s : String) -> Int? {
  let mut result = 0
  let mut idx = 0
  let len = s.length()
  if len == 0 {
    return None
  }
  while idx < len {
    let c = s[idx]
    let ci = c.to_int()
    if ci >= 48 && ci <= 57 {
      result = result * 10 + (ci - 48)
    } else {
      return None
    }
    idx = idx + 1
  }
  Some(result)
}

///|
fn string_slice(s : String, start : Int, end : Int) -> String {
  let mut result = ""
  let mut idx = start
  while idx < end {
    let ch = s[idx]
    result = result + Int::unsafe_to_char(ch.to_int()).to_string()
    idx = idx + 1
  }
  result
}

///|
fn string_slice_from(s : String, start : Int) -> String {
  let mut result = ""
  let mut idx = start
  let len = s.length()
  while idx < len {
    let ch = s[idx]
    result = result + Int::unsafe_to_char(ch.to_int()).to_string()
    idx = idx + 1
  }
  result
}