///|
/// Sets a cookie on the response with the given name, value, and optional attributes.
pub fn HttpResponse::set_cookie(
  self : HttpResponse,
  name : String,
  value : String,
  max_age? : Int,
  path? : String,
  domain? : String,
  secure? : Bool,
  http_only? : Bool,
  same_site? : SameSiteOption,
) -> Unit {
  let item = CookieItem(
    name~,
    value~,
    max_age?,
    path?,
    domain?,
    secure?,
    http_only?,
    same_site?,
  )
  self.cookies.set(name, item)
}

///|
/// Retrieves a cookie by name from the request's Cookie header, if present.
pub fn HttpRequest::get_cookie(
  self : HttpRequest,
  name : String,
) -> CookieItem? {
  match self.get_header("cookie") {
    Some(cookie) => parse_cookie(cookie).get(name)
    None => None
  }
}

///|
/// Deletes a cookie by setting its value to empty and max-age to 0.
pub fn HttpResponse::delete_cookie(self : HttpResponse, key : String) -> Unit {
  self.set_cookie(key, "", max_age=0)
}

///|
/// Serializes an array of cookie items into a semicolon-separated string.
pub fn cookie_to_string(cookie : Array[CookieItem]) -> String {
  cookie.map(x => x.to_string()).join(";")
}

///|
/// Parses a raw cookie header string into a map of cookie names to `CookieItem` values.
pub fn parse_cookie(cookie : StringView) -> Map[String, CookieItem] {
  fn dequote(value : StringView) -> StringView {
    if value is ['"', .. rest, '"'] {
      rest
    } else {
      value
    }
  }

  let res = Map::new()
  let mut last_cookie_item : (String, CookieItem)? = None
  fn on_key_value(key : StringView, value : StringView) -> Unit {
    let key = key.to_string()
    let value = dequote(value).to_string()
    if last_cookie_item is Some((name, item)) {
      let new_item = lexmatch key with longest {
        "(?i:path)" => { ..item, path: Some(value) }
        "(?i:domain)" => { ..item, domain: Some(value) }
        "(?i:max-age)" =>
          {
            ..item,
            max_age: try @string.parse_int(value) catch {
              _ => None
            } noraise {
              x => Some(x)
            },
          }
        "(?i:secure)" => { ..item, secure: Some(true) }
        "(?i:httponly)" => { ..item, http_only: Some(true) }
        "(?i:samesite)" => {
          let same_site = lexmatch value with longest {
            "(?i:lax)" => Some(Lax)
            "(?i:strict)" => Some(Strict)
            "(?i:none)" => Some(SameSiteNone)
            _ => None
          }
          { ..item, same_site, }
        }
        _ => item
      }
      if !physical_equal(new_item, item) {
        res.set(name, new_item)
        last_cookie_item = Some((name, new_item))
        return
      }
    }
    let item = CookieItem(name=key, value~)
    res.set(key, item)
    last_cookie_item = Some((key, item))
  }

  for curr = cookie {
    // TODO: more spec compliance
    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
    lexmatch curr with longest {
      ("[ \t]+", rest) => continue rest
      (
        ("[^ \t=;]([ \t]*[^ \t=;])*" as key)
        "[ \t]*"
        "="
        "[ \t]*"
        ("[^ \t;]([ \t]*[^ \t;])*" as value)
        "[ \t]*"
        ";?",
        rest
      ) => {
        on_key_value(key, value)
        continue rest
      }
      (("[^ \t=;]([ \t]*[^ \t=;])*" as key) "[ \t]*" "(=[ \t]*)?" ";?", rest) => {
        on_key_value(key, "")
        continue rest
      }
      "" => break
      _ => break
    }
  }
  res
}