///|
fn base64url(input : BytesView) -> String {
  @base64.encode(input, kind=Url, padding=false)
}

///|
fn token_char_allowed(c : Char) -> Bool {
  (c >= 'A' && c <= 'Z') ||
  (c >= 'a' && c <= 'z') ||
  (c >= '0' && c <= '9') ||
  c == '-' ||
  c == '_'
}

///|
/// Check the lexical requirements for an ACME challenge token. The protocol
/// requires at least 128 bits of entropy; 22 base64url characters are the
/// shortest representation that can carry that many bits.
pub fn validate_challenge_token(token : String) -> Bool {
  if token.length() < 22 {
    return false
  }
  for c in token {
    if !token_char_allowed(c) {
      return false
    }
  }
  true
}

///|
/// RFC 8555 key authorization: token + "." + account-key JWK thumbprint.
pub fn key_authorization(
  token : String,
  account_thumbprint : String,
) -> String raise AcmeError {
  if !validate_challenge_token(token) {
    raise malformed("challenge", "invalid challenge token")
  }
  if account_thumbprint.length() == 0 {
    raise malformed("challenge", "empty account-key thumbprint")
  }
  token + "." + account_thumbprint
}

///|
/// URL path provisioned by an HTTP-01 responder.
pub fn http01_path(token : String) -> String raise AcmeError {
  if !validate_challenge_token(token) {
    raise malformed("http-01", "invalid challenge token")
  }
  "/.well-known/acme-challenge/" + token
}

///|
/// DNS TXT value for `_acme-challenge.`.
pub fn dns01_txt_value(
  token : String,
  account_thumbprint : String,
) -> String raise AcmeError {
  let authorization = key_authorization(token, account_thumbprint)
  base64url(@sha2.hash(@utf8.encode(authorization[:])[:])[:])
}

///|
/// RFC 7638 thumbprint of a caller-supplied canonical public JWK. Callers that
/// use P-256 should prefer `p256_jwk_thumbprint` below.
pub fn jwk_thumbprint(canonical_public_jwk : String) -> String {
  base64url(@sha2.hash(@utf8.encode(canonical_public_jwk[:])[:])[:])
}

///|
/// Canonical public JWK for an uncompressed P-256 key. Coordinates are the
/// 32-byte unsigned big-endian x and y values, not a DER SubjectPublicKeyInfo.
pub fn canonical_p256_jwk(
  x : BytesView,
  y : BytesView,
) -> String raise AcmeError {
  if x.length() != 32 || y.length() != 32 {
    raise malformed("jwk", "P-256 coordinates must each contain 32 bytes")
  }
  @json.dumps(
    Json::object({
      "crv": Json::string("P-256"),
      "kty": Json::string("EC"),
      "x": Json::string(base64url(x)),
      "y": Json::string(base64url(y)),
    }),
    sort=true,
  )
}

///|
pub fn p256_jwk_thumbprint(
  x : BytesView,
  y : BytesView,
) -> String raise AcmeError {
  jwk_thumbprint(canonical_p256_jwk(x, y))
}

///|
/// A framework-neutral HTTP-01 resource that can be installed in a router,
/// static-file adapter, or test server.
pub(all) struct Http01Resource {
  path : String
  body : String
  content_type : String
} derive(Eq, Debug)

///|
pub fn Http01Resource::new(
  token : String,
  account_thumbprint : String,
) -> Http01Resource raise AcmeError {
  {
    path: http01_path(token),
    body: key_authorization(token, account_thumbprint),
    content_type: "application/octet-stream",
  }
}

///|
/// Normalize a DNS identifier into the owner name used by DNS-01. A leading
/// wildcard label is removed as required by RFC 8555.
pub fn dns01_record_name(identifier : String) -> String raise AcmeError {
  let name = if identifier.has_prefix("*.") {
    identifier[2:].to_owned()
  } else {
    identifier
  }
  if name.length() == 0 {
    raise malformed("dns-01", "empty DNS identifier")
  }
  "_acme-challenge." + name
}