///|
/// The most a cookie's name and value may take together, in UTF-8 bytes
/// (RFC 6265bis-22 §5.6 step 5); a longer one is ignored whole. Attributes
/// are bounded one by one, by `max_attribute_bytes`.
let max_cookie_bytes : Int32 = 4096

///|
/// The most one attribute value may take, in UTF-8 bytes (RFC 6265bis-22
/// §5.6); a longer attribute is ignored.
let max_attribute_bytes : Int32 = 1024

///|
/// A `Set-Cookie` value parsed by RFC 6265bis-22 §5.6, before the storage
/// model decides what it means for the request.
priv struct SetCookie {
  /// Empty for a cookie without `=` in its name-value pair.
  name : String
  value : String
  /// The last valid `Expires`, in Unix milliseconds.
  expires_ms : Int64?
  /// The last valid `Max-Age`, in seconds; it overrides `Expires`.
  max_age : Int64?
  /// The last `Domain`, lowercased, one leading dot removed; `None` when
  /// there is none or the last one is empty (§5.7 step 10: host-only).
  domain : String?
  /// The last `Path` value, which may still need the default path.
  path : String?
  secure : Bool
  http_only : Bool
  same_site : SameSite?
}

///|
/// Parses a `Set-Cookie` value as RFC 6265bis-22 §5.6 does, or returns
/// `None` when it is ignored: a control character other than HTAB anywhere,
/// a name and value longer than `max_cookie_bytes` together, or, by §5.7
/// step 2, an empty name and an empty value. A pair without `=` is a value
/// with an empty name. Unknown and malformed attributes are skipped.
fn parse_set_cookie(text : String) -> SetCookie? {
  guard !has_control(text) else { return None }
  let (pair, attributes) = match text.split_once(";") {
    Some((pair, attributes)) => (pair, attributes)
    None => (text[:], ""[:])
  }
  let (name, value) = match pair.split_once("=") {
    Some((name, value)) => (trim_wsp(name), trim_wsp(value))
    None => ("", trim_wsp(pair))
  }
  guard !(name.is_empty() && value.is_empty()) &&
    utf8_length(name) + utf8_length(value) <= max_cookie_bytes else {
    return None
  }
  let mut expires_ms = None
  let mut max_age = None
  let mut domain = None
  let mut path = None
  let mut secure = false
  let mut http_only = false
  let mut same_site = None
  for av in attributes.split(";") {
    let (key, value) = match av.split_once("=") {
      Some((key, value)) => (trim_wsp(key), trim_wsp(value))
      None => (trim_wsp(av), "")
    }
    if utf8_length(value) > max_attribute_bytes {
      continue
    }
    match key.to_lower() {
      "expires" =>
        if parse_cookie_date(value) is Some(ms) {
          expires_ms = Some(ms)
        }
      "max-age" =>
        if parse_max_age(value) is Some(seconds) {
          max_age = Some(seconds)
        }
      "domain" => {
        let value = value.strip_prefix(".").unwrap_or(value[:]).to_owned()
        domain = if value.is_empty() { None } else { Some(value.to_lower()) }
      }
      "path" => path = Some(value)
      "secure" => secure = true
      "httponly" => http_only = true
      "samesite" =>
        same_site = match value.to_lower() {
          "strict" => Some(SameSite::Strict)
          "lax" => Some(SameSite::Lax)
          "none" => Some(SameSite::NoRestriction)
          _ => None
        }
      _ => ()
    }
  }
  Some({
    name,
    value,
    expires_ms,
    max_age,
    domain,
    path,
    secure,
    http_only,
    same_site,
  })
}

///|
/// RFC 6265bis-22 §5.6.2: an optional `-` then digits. Magnitudes of 10^18 and
/// more read as 10^18, far past the lifetime cap the caller applies.
fn parse_max_age(text : String) -> Int64? {
  let (negative, digits) = match text.strip_prefix("-") {
    Some(rest) => (true, rest)
    None => (false, text[:])
  }
  guard !digits.is_empty() else { return None }
  let mut value = 0L
  for c in digits {
    guard c.is_ascii_digit() else { return None }
    value = if value < 100_000_000_000_000_000L {
      value * 10L + (c.to_int() - '0'.to_int()).to_int64()
    } else {
      1_000_000_000_000_000_000L
    }
  }
  Some(if negative { -value } else { value })
}

///|
/// Strips leading and trailing SP and HTAB, the WSP of RFC 6265bis-22 §5.6.
fn trim_wsp(text : StringView) -> String {
  text.trim(chars=" \t").to_owned()
}

///|
/// Whether `text` holds a %x00-08 / %x0A-1F / %x7F character. Such a value
/// is ignored whole: it could otherwise carry CR LF into a `Cookie` header.
fn has_control(text : String) -> Bool {
  for c in text {
    let n = c.to_int()
    if (n < 0x20 && n != 0x09) || n == 0x7F {
      return true
    }
  }
  false
}

///|
/// The UTF-8 length of `text`.
fn utf8_length(text : StringView) -> Int32 {
  let mut bytes = 0
  for c in text {
    let n = c.to_int()
    bytes += if n < 0x80 {
      1
    } else if n < 0x800 {
      2
    } else if n < 0x10000 {
      3
    } else {
      4
    }
  }
  bytes
}