///|
/// An HTTP Entity Tag (RFC 7232 ยง2.3).
pub(all) struct EntityTag {
  tag : String // Entity tag enclosed in double quotes: e.g. "\"123-456\""
  is_weak : Bool // True if prefixed with W/
} derive(Eq, Debug)

///|
/// Construct an EntityTag from a raw tag string and weak flag.
pub fn EntityTag::new(raw_tag : String, weak : Bool) -> EntityTag {
  let tag = if raw_tag.has_prefix("\"") && raw_tag.has_suffix("\"") {
    raw_tag
  } else {
    "\"" + raw_tag + "\""
  }
  { tag, is_weak: weak, }
}

///|
/// Generate an EntityTag from file metadata (size and modification time in seconds).
pub fn EntityTag::from_metadata(
  size : Int64,
  mtime_sec : Int64,
  weak : Bool,
) -> EntityTag {
  let tag = "\"\{size}-\{mtime_sec}\""
  { tag, is_weak: weak, }
}

///|
/// Format EntityTag as an HTTP header value (e.g. "\"123-456\"" or "W/\"123-456\"").
pub fn EntityTag::to_header_value(self : EntityTag) -> String {
  if self.is_weak {
    "W/" + self.tag
  } else {
    self.tag
  }
}

///|
/// Parse an EntityTag from a header string.
pub fn EntityTag::parse(raw : String) -> EntityTag? {
  let trimmed = raw.trim()
  if trimmed.has_prefix("W/\"") &&
    trimmed.has_suffix("\"") &&
    trimmed.length() >= 4 {
    Some({ tag: trimmed[2:].to_owned(), is_weak: true, })
  } else if trimmed.has_prefix("\"") &&
    trimmed.has_suffix("\"") &&
    trimmed.length() >= 2 {
    Some({ tag: trimmed.to_owned(), is_weak: false, })
  } else {
    None
  }
}

///|
/// Compare a client-supplied raw ETag against the server's EntityTag.
pub fn etag_matches(
  client_raw : String,
  server_tag : EntityTag,
  weak_compare : Bool,
) -> Bool {
  let client_trimmed = client_raw.trim()
  if client_trimmed == "*" {
    return true
  }
  let is_client_weak = client_trimmed.has_prefix("W/")
  let client_opaque_tag = if is_client_weak {
    client_trimmed[2:].to_owned()
  } else {
    client_trimmed.to_owned()
  }
  let server_opaque_tag = server_tag.tag

  if weak_compare {
    client_opaque_tag == server_opaque_tag
  } else if is_client_weak || server_tag.is_weak {
    false
  } else {
    client_opaque_tag == server_opaque_tag
  }
}

///|
/// Check if an If-None-Match header value matches the server ETag.
pub fn if_none_match_matches(
  header_val : String,
  server_tag : EntityTag,
  weak_compare : Bool,
) -> Bool {
  let tags = header_val.split(",").to_array()
  for raw in tags {
    let trimmed = raw.trim().to_owned()
    if etag_matches(trimmed, server_tag, weak_compare) {
      return true
    }
  }
  false
}

///|
/// Pure HTTP date formatter generating RFC 7231 IMF-fixdate:
/// "Sun, 06 Nov 1994 08:49:37 GMT"
pub fn format_http_date(epoch_sec : Int64) -> String {
  let days_since_epoch = (epoch_sec / 86400L).to_int()
  let mut rem_sec = (epoch_sec % 86400L).to_int()
  if rem_sec < 0 {
    rem_sec = rem_sec + 86400
  }
  let hour = rem_sec / 3600
  let minute = rem_sec % 3600 / 60
  let second = rem_sec % 60

  let mut day_of_week = (4 + days_since_epoch) % 7
  if day_of_week < 0 {
    day_of_week = day_of_week + 7
  }
  let wkdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
  let months = [
    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
    "Dec",
  ]

  let mut y = 1970
  let mut d = days_since_epoch
  if d >= 0 {
    while true {
      let leap = is_leap_year(y)
      let days_in_year = if leap { 366 } else { 365 }
      if d < days_in_year {
        break
      }
      d = d - days_in_year
      y = y + 1
    }
  } else {
    while d < 0 {
      y = y - 1
      let leap = is_leap_year(y)
      let days_in_year = if leap { 366 } else { 365 }
      d = d + days_in_year
    }
  }

  let leap = is_leap_year(y)
  let month_days = if leap {
    [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  } else {
    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  }
  let mut m = 0
  while m < 12 {
    if d < month_days[m] {
      break
    }
    d = d - month_days[m]
    m = m + 1
  }
  let day = d + 1

  "\{wkdays[day_of_week]}, \{pad2(day)} \{months[m]} \{y} \{pad2(hour)}:\{pad2(minute)}:\{pad2(second)} GMT"
}

///|
fn is_leap_year(y : Int) -> Bool {
  (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}

///|
fn pad2(n : Int) -> String {
  if n < 10 {
    "0" + n.to_string()
  } else {
    n.to_string()
  }
}

///|
/// Parse an HTTP date string into UTC epoch seconds.
/// Supports IMF-fixdate ("Sun, 06 Nov 1994 08:49:37 GMT"),
/// RFC 850 ("Sunday, 06-Nov-94 08:49:37 GMT"),
/// asctime ("Sun Nov  6 08:49:37 1994"),
/// and JS toString() ("Fri Sep 11 2026 15:20:00 GMT+0800").
/// Safely returns None on illegal / overflow dates (e.g. C003 "275760-09-24").
pub fn parse_http_date(date_str : String) -> Int64? {
  let s = date_str.trim()
  if s.length() < 10 || s.length() > 64 {
    return None
  }
  if s.has_prefix("275760") {
    return None
  }

  let tokens = split_date_tokens(s)
  if tokens.length() < 5 {
    return None
  }

  let parsed = extract_date_fields(tokens)
  guard parsed is Some((y, m, d, hh, mm, ss)) else { return None }
  if y < 1970 ||
    y > 9999 ||
    m < 1 ||
    m > 12 ||
    d < 1 ||
    d > 31 ||
    hh < 0 ||
    hh > 23 ||
    mm < 0 ||
    mm > 59 ||
    ss < 0 ||
    ss > 60 {
    return None
  }

  let mut days = 0L
  for cur_y in 1970.. Array[String] {
  let tokens : Array[String] = []
  let mut cur = StringBuilder()
  for c in s {
    if c == ' ' || c == '\t' || c == ',' || c == '-' || c == ':' {
      if !cur.is_empty() {
        tokens.push(cur.to_string())
        cur = StringBuilder()
      }
    } else {
      cur.write_char(c)
    }
  }
  if !cur.is_empty() {
    tokens.push(cur.to_string())
  }
  tokens
}

///|
fn month_name_to_int(m : String) -> Int? {
  match m.to_lower() {
    "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 extract_date_fields(
  tokens : Array[String],
) -> (Int, Int, Int, Int, Int, Int)? {
  let mut year : Int? = None
  let mut month : Int? = None
  let mut day : Int? = None
  let time_tokens : Array[Int] = []

  for t in tokens {
    if month_name_to_int(t) is Some(m) {
      month = Some(m)
      continue
    }
    match parse_int(t) {
      Some(n) =>
        if n >= 1000 && n <= 9999 {
          year = Some(n)
        } else if n >= 70 && n <= 99 && year is None {
          year = Some(1900 + n)
        } else if n >= 0 && n <= 69 && year is None && time_tokens.length() >= 3 {
          year = Some(2000 + n)
        } else if day is None && month is Some(_) && n >= 1 && n <= 31 {
          day = Some(n)
        } else if day is None && n >= 1 && n <= 31 {
          day = Some(n)
        } else {
          time_tokens.push(n)
        }
      None => ()
    }
  }

  guard year is Some(y) else { return None }
  guard month is Some(m) else { return None }
  guard day is Some(d) else { return None }
  if time_tokens.length() < 3 {
    return None
  }
  let hh = time_tokens[0]
  let mm = time_tokens[1]
  let ss = time_tokens[2]
  Some((y, m, d, hh, mm, ss))
}

///|
fn parse_int(s : String) -> Int? {
  let mut n = 0
  if s.length() == 0 {
    return None
  }
  for c in s {
    if c < '0' || c > '9' {
      return None
    }
    n = n * 10 + (c.to_int() - '0'.to_int())
  }
  Some(n)
}

///|
/// Determine if a request satisfies cache conditions to return 304 Not Modified.
pub fn should_return_304(
  if_none_match : String?,
  if_modified_since : String?,
  server_mtime_sec : Int64,
  server_etag : EntityTag,
  weak_compare : Bool,
) -> Bool {
  if if_none_match is None && if_modified_since is None {
    return false
  }

  if if_modified_since is Some(ims) {
    match parse_http_date(ims) {
      None => return false
      Some(client_time_sec) =>
        if client_time_sec < server_mtime_sec {
          return false
        }
    }
  }

  if if_none_match is Some(inm) {
    if !if_none_match_matches(inm, server_etag, weak_compare) {
      return false
    }
  }

  true
}

///|
/// Cache-Control header strategy.
pub(all) enum CachePolicy {
  MaxAge(Int)
  NoCache
  Exact(String)
} derive(Eq, Debug)

///|
/// Format CachePolicy to header string value.
pub fn format_cache_control(policy : CachePolicy) -> String {
  match policy {
    MaxAge(sec) =>
      if sec < 0 {
        "no-cache, no-store, must-revalidate"
      } else {
        "max-age=" + sec.to_string()
      }
    NoCache => "no-cache, no-store, must-revalidate"
    Exact(val) => val
  }
}


///|
pub extend EntityTag with Eq::{not_equal, equal}

///|
pub extend EntityTag with Debug::{to_repr}

///|
pub extend CachePolicy with Eq::{not_equal, equal}

///|
pub extend CachePolicy with Debug::{to_repr}