///|
/// The SameSite attribute for cookies, controlling cross-site request behavior.
pub(all) enum SameSiteOption {
  Lax
  Strict
  SameSiteNone
} derive(Debug, Eq)

///|
pub impl Show for SameSiteOption with fn output(self, logger) {
  match self {
    Lax => logger.write_string("Lax")
    Strict => logger.write_string("Strict")
    SameSiteNone => logger.write_string("SameSiteNone")
  }
}

///|
pub impl ToJson for SameSiteOption with fn to_json(self : SameSiteOption) -> Json {
  match self {
    Lax => "lax"
    Strict => "strict"
    SameSiteNone => "none"
  }
}

///|
/// Represents an HTTP cookie with its name, value, and optional attributes.
pub(all) struct CookieItem {
  /// Cookie name (the key in `name=value`).
  name : String
  /// Cookie value.
  value : String
  /// Expiry in seconds from now; `0` deletes the cookie.
  max_age : Int?
  /// URL path scope for which the browser sends this cookie.
  path : String?
  /// Domain scope for which the browser sends this cookie.
  domain : String?
  /// If `true`, the cookie is only sent over HTTPS.
  secure : Bool?
  /// If `true`, JavaScript cannot access the cookie (`document.cookie`).
  http_only : Bool?
  /// Cross-site request policy (`Lax`, `Strict`, or `SameSiteNone`).
  same_site : SameSiteOption?
} derive(Eq, Debug)

///|
/// Creates a new `CookieItem` with the given name, value, and optional attributes.
pub fn CookieItem::CookieItem(
  name~ : String,
  value~ : String,
  max_age? : Int,
  path? : String,
  domain? : String,
  secure? : Bool,
  http_only? : Bool,
  same_site? : SameSiteOption,
) -> CookieItem {
  { name, value, max_age, path, domain, secure, http_only, same_site }
}

///|
fn sanitize_cookie_value(s : String) -> String {
  if s.contains("\r") || s.contains("\n") || s.contains(";") {
    let buf = StringBuilder::new()
    for c in s {
      if c != '\r' && c != '\n' && c != ';' {
        buf.write_char(c)
      }
    }
    buf.to_string()
  } else {
    s
  }
}

///|
pub impl Show for CookieItem with fn output(self, logger) -> Unit {
  logger.write_string(sanitize_cookie_value(self.name))
  logger.write_char('=')
  logger.write_string(sanitize_cookie_value(self.value))
  if self.max_age is Some(max_age) {
    logger.write_string("; Max-Age=")
    logger.write_string(max_age.to_string())
  }
  if self.path is Some(path) {
    logger.write_string("; Path=")
    logger.write_string(path)
  }
  if self.domain is Some(domain) {
    logger.write_string("; Domain=")
    logger.write_string(domain)
  }
  if self.secure == Some(true) {
    logger.write_string("; Secure")
  }
  if self.http_only == Some(true) {
    logger.write_string("; HttpOnly")
  }
  if self.same_site is Some(same_site) {
    logger.write_string("; SameSite=")
    logger.write_string(same_site.to_string())
  }
}

///|
pub impl Show for CookieItem with fn to_string(self : CookieItem) -> String {
  let buf = StringBuilder()
  self.output(buf)
  buf.to_string()
}

///|
/// 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([])
  let mut last_cookie_item : (String, CookieItem)? = None
  fn on_key_value(key : StringView, value : StringView) -> Unit {
    let key = key.to_owned()
    let value = dequote(value).to_owned()
    if last_cookie_item is Some((name, item)) {
      let new_item = lexscan key with longest {
        re"^(?i:path)$" => { ..item, path: Some(value) }
        re"^(?i:domain)$" => { ..item, domain: Some(value) }
        re"^(?i:max-age)$" =>
          {
            ..item,
            max_age: try @string.parse_int(value) catch {
              _ => None
            } noraise {
              x => Some(x)
            },
          }
        re"^(?i:secure)$" => { ..item, secure: Some(true) }
        re"^(?i:httponly)$" => { ..item, http_only: Some(true) }
        re"^(?i:samesite)$" => {
          let same_site = lexscan value with longest {
            re"^(?i:lax)$" => Some(Lax)
            re"^(?i:strict)$" => Some(Strict)
            re"^(?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
    lexscan curr with longest {
      (re"^[ \t]+", after=rest) => continue rest
      (
        (re"^[^ \t=;]([ \t]*[^ \t=;])*" as key) +
        re"[ \t]*" +
        re"=" +
        re"[ \t]*" +
        (re"[^ \t;]([ \t]*[^ \t;])*" as value) +
        re"[ \t]*" +
        re";?",
        after=rest,
      ) => {
        on_key_value(key, value)
        continue rest
      }
      (
        (re"^[^ \t=;]([ \t]*[^ \t=;])*" as key) +
        re"[ \t]*" +
        re"(=[ \t]*)?" +
        re";?",
        after=rest,
      ) => {
        on_key_value(key, "")
        continue rest
      }
      _ => break
    }
  }
  res
}