///|
pub struct StableSignature {
  value : String
} derive(Eq, Debug)

///|
pub fn StableSignature::new(value : String) -> StableSignature {
  { value, }
}

///|
fn utf8_bytes(value : String) -> Bytes {
  @utf8.encode(value)
}

///|
pub fn sha256_hex(message : String) -> String {
  let bytes = utf8_bytes(message)
  @crypto.bytes_to_hex_string(@crypto.sha256(bytes))
}

///|
pub fn hmac_sha256_hex(secret : String, message : String) -> String {
  let key = utf8_bytes(secret)
  let payload = utf8_bytes(message)
  @crypto.bytes_to_hex_string(
    @crypto.hmac(@crypto.SHA256::new(), key.view(), payload.view()),
  )
}

///|
/// Compatibility helper for deterministic audit and idempotency digests.
pub fn stable_sign(secret : String, canonical : String) -> StableSignature {
  StableSignature::new(hmac_sha256_hex(secret, canonical))
}

///|
pub fn constant_time_equal(left : String, right : String) -> Bool {
  let left_bytes = utf8_bytes(left)
  let right_bytes = utf8_bytes(right)
  let max_length = if left_bytes.length() > right_bytes.length() {
    left_bytes.length()
  } else {
    right_bytes.length()
  }
  let mut difference = left_bytes.length() ^ right_bytes.length()
  for i in 0.. String {
  strip_prefix(trim_ascii(signature), prefix)
}

///|
fn github_signature(
  request : WebhookRequest,
  policy : VerificationPolicy,
) -> String {
  "sha256=" + hmac_sha256_hex(policy.secret, request.body)
}

///|
fn generic_signature(
  request : WebhookRequest,
  policy : VerificationPolicy,
) -> String {
  policy.provider.signature_prefix +
  hmac_sha256_hex(policy.secret, canonical_base(request, policy.provider))
}

///|
fn feishu_signature(
  request : WebhookRequest,
  policy : VerificationPolicy,
) -> String {
  let timestamp = request.header(policy.provider.timestamp_header).unwrap_or("")
  let nonce = request.header("x-lark-request-nonce").unwrap_or("")
  sha256_hex(timestamp + nonce + policy.secret + request.body)
}

///|
fn stripe_signature_for_timestamp(
  request : WebhookRequest,
  policy : VerificationPolicy,
  timestamp : String,
) -> String {
  hmac_sha256_hex(policy.secret, timestamp + "." + request.body)
}

///|
pub fn expected_signature(
  request : WebhookRequest,
  policy : VerificationPolicy,
) -> String {
  match policy.provider.name {
    "github" => github_signature(request, policy)
    "feishu" => feishu_signature(request, policy)
    "stripe" => {
      let parsed = parse_stripe_signature(
        request.signature(policy.provider.signature_header),
      )
      if parsed.timestamp_text == "" {
        ""
      } else {
        "v1=" +
        stripe_signature_for_timestamp(request, policy, parsed.timestamp_text)
      }
    }
    _ => generic_signature(request, policy)
  }
}

///|
fn verify_stripe_signature(
  request : WebhookRequest,
  policy : VerificationPolicy,
) -> Bool {
  let parsed = parse_stripe_signature(
    request.signature(policy.provider.signature_header),
  )
  if parsed.timestamp_text == "" || parsed.v1_signatures.length() == 0 {
    return false
  }
  let expected = stripe_signature_for_timestamp(
    request,
    policy,
    parsed.timestamp_text,
  )
  for signature in parsed.v1_signatures {
    if constant_time_equal(signature, expected) {
      return true
    }
  }
  false
}

///|
pub fn verify_signature(
  request : WebhookRequest,
  policy : VerificationPolicy,
) -> Bool {
  let actual = request.signature(policy.provider.signature_header)
  if actual == "" {
    false
  } else if policy.provider.name == "stripe" {
    verify_stripe_signature(request, policy)
  } else {
    constant_time_equal(trim_ascii(actual), expected_signature(request, policy))
  }
}

///|
fn sign_stripe_request(
  request : WebhookRequest,
  policy : VerificationPolicy,
) -> WebhookRequest {
  let timestamp = if request.received_at > 0 {
    request.received_at.to_string()
  } else {
    policy.now.to_string()
  }
  let signature = stripe_signature_for_timestamp(request, policy, timestamp)
  request.with_replaced_header(
    policy.provider.signature_header,
    "t=" + timestamp + ",v1=" + signature,
  )
}

///|
pub fn sign_request(
  request : WebhookRequest,
  policy : VerificationPolicy,
) -> WebhookRequest {
  if policy.provider.name == "stripe" {
    sign_stripe_request(request, policy)
  } else {
    request.with_replaced_header(
      policy.provider.signature_header,
      expected_signature(request, policy),
    )
  }
}