///|
/// The most a `Set-Cookie` value may take, in UTF-8 bytes of its name, value
/// and attributes together: the per-cookie size RFC 6265 §6.1 asks user
/// agents to support. A longer one is ignored whole.
let max_cookie_bytes : Int32 = 4096

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

///|
/// A `Set-Cookie` value parsed by RFC 6265 §5.2, before the storage model
/// decides what it means for the request.
priv struct SetCookie {
  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 non-empty `Domain`, lowercased, one leading dot removed.
  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, or returns `None` when RFC 6265 §5.2 ignores
/// it: no `=` in the name-value pair, an empty name, a control character
/// other than HTAB anywhere (RFC 6265bis), or more than `max_cookie_bytes`.
/// Unknown and malformed attributes are skipped.
fn parse_set_cookie(text : String) -> SetCookie? {
  guard utf8_length(text) <= max_cookie_bytes && !has_control(text) else {
    return None
  }
  let (pair, attributes) = match text.split_once(";") {
    Some((pair, attributes)) => (pair, attributes)
    None => (text[:], ""[:])
  }
  guard pair.split_once("=") is Some((name, value)) else { return None }
  let name = trim_wsp(name)
  guard !name.is_empty() 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()
        if !value.is_empty() {
          domain = 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: trim_wsp(value),
    expires_ms,
    max_age,
    domain,
    path,
    secure,
    http_only,
    same_site,
  })
}

///|
/// RFC 6265 §5.2.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 whitespace of RFC 6265 §5.2.
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
}