// The response side of cookies — FastAPI's `response.set_cookie` and
// `delete_cookie`. `Context::cookie` already reads what a request carries; this
// is what writes one back.
//
// They are functions over a response rather than methods on it because
// `@moonasgi.Response` belongs to another package, and each returns a new
// response with one more `Set-Cookie` header. That is also the correct wire
// shape: two cookies are two headers, never one merged field, since `Set-Cookie`
// is the one header a proxy may not fold on commas.

///|
/// How far a cross-site request may carry a cookie. `Strict` sends it only on
/// same-site requests, `Lax` also on a top-level navigation (the default, and
/// what stops a cross-site form post from carrying a session), `Unrestricted`
/// sends it everywhere — it is the wire value `None`, which browsers honour only
/// on a `Secure` cookie.
pub(all) enum SameSite {
  Strict
  Lax
  Unrestricted
} derive(Eq)

///|
/// The attribute's wire spelling.
fn same_site_name(s : SameSite) -> String {
  match s {
    Strict => "Strict"
    Lax => "Lax"
    Unrestricted => "None"
  }
}

///|
/// Drop the octets a cookie name, value, path or domain may not carry: RFC 6265's
/// cookie-octet is printable ASCII minus space, `"`, `,`, `;` and `\`.
///
/// Dropped rather than escaped, so what `Context::cookie` reads back is the same
/// text that was set — an escape the writer applies and the reader does not know
/// about is worse than a character that never survives. The security half is that
/// a `\r\n` smuggled into a name or value cannot open a header of its own.
fn cookie_safe(s : String) -> String {
  let sb = StringBuilder()
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i].to_int()
    if c > 0x20 && c < 0x7F && c != 0x22 && c != 0x2C && c != 0x3B && c != 0x5C {
      sb.write_char(s[i].unsafe_to_char())
    }
  }
  sb.to_string()
}

///|
/// Drop everything outside an HTTP-date's alphabet — letters, digits, space,
/// comma, colon, hyphen and plus. `Expires` is the one attribute whose value
/// legitimately holds spaces and a comma, so it cannot go through `cookie_safe`;
/// this keeps the header un-splittable all the same.
fn date_safe(s : String) -> String {
  let sb = StringBuilder()
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i].to_int()
    let ok = (c >= 0x30 && c <= 0x39) ||
      (c >= 0x41 && c <= 0x5A) ||
      (c >= 0x61 && c <= 0x7A) ||
      c == 0x20 ||
      c == 0x2C ||
      c == 0x3A ||
      c == 0x2D ||
      c == 0x2B
    if ok {
      sb.write_char(s[i].unsafe_to_char())
    }
  }
  sb.to_string()
}

///|
/// Render one `Set-Cookie` field value. Attributes follow RFC 6265 §4.1.1's
/// order, which is the order every browser and log reader expects to see them in.
fn cookie_header(
  name : String,
  value : String,
  max_age : Int?,
  expires : String?,
  path : String,
  domain : String?,
  secure : Bool,
  http_only : Bool,
  same_site : SameSite?,
) -> String {
  let sb = StringBuilder()
  sb.write_string(cookie_safe(name))
  sb.write_string("=")
  sb.write_string(cookie_safe(value))
  match expires {
    Some(d) => {
      sb.write_string("; Expires=")
      sb.write_string(date_safe(d))
    }
    None => ()
  }
  match max_age {
    Some(secs) => {
      sb.write_string("; Max-Age=")
      sb.write_string(secs.to_string())
    }
    None => ()
  }
  match domain {
    Some(d) => {
      sb.write_string("; Domain=")
      sb.write_string(cookie_safe(d))
    }
    None => ()
  }
  if path != "" {
    sb.write_string("; Path=")
    sb.write_string(cookie_safe(path))
  }
  if secure {
    sb.write_string("; Secure")
  }
  if http_only {
    sb.write_string("; HttpOnly")
  }
  match same_site {
    Some(s) => {
      sb.write_string("; SameSite=")
      sb.write_string(same_site_name(s))
    }
    None => ()
  }
  sb.to_string()
}

///|
/// Add a `Set-Cookie` header to `resp` (← FastAPI's `response.set_cookie`),
/// returning the response that carries it; the original is untouched, so a
/// handler can hand the same base response to two callers.
///
/// `max_age` is the lifetime in seconds and `expires` an HTTP-date; giving
/// neither makes it a session cookie. `path` defaults to `/`, and passing `""`
/// omits the attribute so the cookie scopes to the request's own directory.
/// `http_only` keeps it away from scripts, `secure` keeps it off plaintext
/// connections, and `same_site` defaults to `Lax` — the browser default, and the
/// one that stops a cross-site form post from carrying a session.
pub fn set_cookie(
  resp : @moonasgi.Response,
  name : String,
  value : String,
  max_age? : Int,
  expires? : String,
  path? : String = "/",
  domain? : String,
  secure? : Bool = false,
  http_only? : Bool = false,
  same_site? : SameSite? = Some(Lax),
) -> @moonasgi.Response {
  let header = cookie_header(
    name, value, max_age, expires, path, domain, secure, http_only, same_site,
  )
  @moonasgi.Response::new(
    resp.status,
    [..resp.headers, ("set-cookie", header)],
    resp.body,
  )
}

///|
/// Expire the cookie named `name` (← FastAPI's `response.delete_cookie`): an
/// empty value with `Max-Age=0` and a date in the past, so a browser that honours
/// only one of the two still drops it.
///
/// A cookie is identified by name, domain and path together, so `path` and
/// `domain` must match what set it — otherwise this writes a second, differently
/// scoped cookie and the original survives.
pub fn delete_cookie(
  resp : @moonasgi.Response,
  name : String,
  path? : String = "/",
  domain? : String,
  secure? : Bool = false,
  http_only? : Bool = false,
  same_site? : SameSite? = Some(Lax),
) -> @moonasgi.Response {
  set_cookie(
    resp,
    name,
    "",
    max_age=0,
    expires="Thu, 01 Jan 1970 00:00:00 GMT",
    path~,
    domain?,
    secure~,
    http_only~,
    same_site~,
  )
}