///|
/// Outbound signing schemes of popular WebHook providers.
///
/// MoonHook is the delivery side, so each variant knows how to sign a raw
/// request body. Because the library also ships a minimal receiver, the same
/// variant can verify the matching provider header or body field again — the
/// sender and the receiver share one implementation.
///
/// - `GitHub`: `X-Hub-Signature-256: sha256=`, HMAC-SHA256 over the raw
///   body. Matches the example in GitHub's "Validating webhook deliveries".
/// - `Stripe`: `Stripe-Signature: t=,v1=`, HMAC-SHA256 over
///   `"."`; several `v1` values may be present during secret rotation.
/// - `Slack`: `X-Slack-Signature: v0=` together with
///   `X-Slack-Request-Timestamp`, HMAC-SHA256 over `"v0::"`.
/// - `FeishuBot`: the signature travels inside the JSON body as `timestamp`
///   and `sign`, where
///   `sign = base64(HMAC-SHA256(key = "\n", message = ""))`, as in
///   the official custom-bot sample.
/// - `Shopify`: `X-Shopify-Hmac-Sha256: `, HMAC-SHA256 over the raw
///   body.
pub(all) enum SignatureProvider {
  GitHub
  Stripe
  Slack
  FeishuBot
  Shopify
} derive(Eq, @debug.Debug)

///|
/// The header GitHub uses for the raw-body HMAC-SHA256 signature.
pub const HEADER_GITHUB_SIGNATURE : String = "X-Hub-Signature-256"

///|
/// The header Stripe uses for `t=,v1=`.
pub const HEADER_STRIPE_SIGNATURE : String = "Stripe-Signature"

///|
/// The header Slack uses for `v0=`.
pub const HEADER_SLACK_SIGNATURE : String = "X-Slack-Signature"

///|
/// The header Slack uses for the request timestamp, in seconds.
pub const HEADER_SLACK_TIMESTAMP : String = "X-Slack-Request-Timestamp"

///|
/// The header Shopify uses for the raw-body HMAC-SHA256 signature, base64.
pub const HEADER_SHOPIFY_SIGNATURE : String = "X-Shopify-Hmac-Sha256"

///|
/// HMAC-SHA256 of `message` under `key`, lowercase hex.
fn hmac_hex(key : String, message : String) -> String {
  bytes_to_hex(hmac_sha256(@utf8.encode(key), @utf8.encode(message)))
}

///|
/// HMAC-SHA256 of `message` under `key`, standard base64 with padding.
fn hmac_base64(key : String, message : String) -> String {
  @base64.encode(
    Bytes::from_array(hmac_sha256(@utf8.encode(key), @utf8.encode(message))),
  )
}

///|
/// `"."`, the Stripe signed payload.
fn stripe_signed_payload(timestamp_s : Int, payload : String) -> String {
  timestamp_s.to_string() + "." + payload
}

///|
/// `"v0::"`, the Slack signed payload.
fn slack_signed_payload(timestamp_s : Int, payload : String) -> String {
  "v0:" + timestamp_s.to_string() + ":" + payload
}

///|
/// The Feishu custom-bot signature:
/// `base64(HMAC-SHA256(key = "\n", message = ""))`.
///
/// The timestamp is the HMAC *key* material here, not part of the message,
/// which is why this scheme does not reuse the signed-payload helpers.
fn feishu_bot_sign(secret : String, timestamp_s : Int) -> String {
  hmac_base64(timestamp_s.to_string() + "\n" + secret, "")
}

///|
/// Parses a timestamp expressed in seconds, accepting surrounding whitespace.
fn parse_seconds(value : String) -> Int? {
  let parsed = @string.parse_int(value.trim()) catch { _ => return None }
  Some(parsed)
}

///|
/// True when `timestamp_s` is inside the allowed window. A negative
/// `tolerance_s` disables the window check, which is useful for schemes
/// without a timestamp.
fn within_window(timestamp_s : Int, now_s : Int, tolerance_s : Int) -> Bool {
  if tolerance_s < 0 {
    true
  } else {
    let delta = if timestamp_s > now_s {
      timestamp_s - now_s
    } else {
      now_s - timestamp_s
    }
    delta <= tolerance_s
  }
}

///|
/// Splits `k=v` pairs separated by commas, lowercasing the key.
fn split_signed_header(value : String) -> Array[(String, String)] {
  let parts : Array[(String, String)] = []
  for part in value.split(",") {
    match part.trim().split_once("=") {
      Some((key, raw)) =>
        parts.push((key.to_owned().to_lower(), raw.to_owned()))
      None => ()
    }
  }
  parts
}

///|
/// Strips a `prefix` such as `v0=` when present.
fn strip_signature_scheme(value : String, prefix : String) -> String {
  let trimmed = value.trim()
  if trimmed.has_prefix(prefix) {
    trimmed[prefix.length():].to_owned()
  } else {
    trimmed.to_owned()
  }
}

///|
/// The signature value for `payload`, without any scheme prefix: lowercase hex
/// for GitHub, Stripe and Slack, standard base64 for Shopify and Feishu.
///
/// `timestamp_s` is only used by the schemes that bind a timestamp (Stripe,
/// Slack, Feishu); GitHub and Shopify sign the raw body alone.
pub fn SignatureProvider::sign_value(
  self : SignatureProvider,
  secret : String,
  payload : String,
  timestamp_s : Int,
) -> String {
  match self {
    GitHub => hmac_hex(secret, payload)
    Shopify => hmac_base64(secret, payload)
    Slack => hmac_hex(secret, slack_signed_payload(timestamp_s, payload))
    Stripe => hmac_hex(secret, stripe_signed_payload(timestamp_s, payload))
    FeishuBot => feishu_bot_sign(secret, timestamp_s)
  }
}

///|
/// The request headers a delivery must carry for this scheme, already formatted
/// the way the provider expects.
///
/// `FeishuBot` returns no header: its signature lives in the JSON body, which
/// `feishu_bot_text_body` builds.
pub fn SignatureProvider::sign_headers(
  self : SignatureProvider,
  secret : String,
  payload : String,
  timestamp_s : Int,
) -> Array[(String, String)] {
  let headers : Array[(String, String)] = []
  match self {
    GitHub =>
      headers.push(
        (HEADER_GITHUB_SIGNATURE, "sha256=" + hmac_hex(secret, payload)),
      )
    Shopify =>
      headers.push((HEADER_SHOPIFY_SIGNATURE, hmac_base64(secret, payload)))
    Slack => {
      headers.push((HEADER_SLACK_TIMESTAMP, timestamp_s.to_string()))
      headers.push(
        (
          HEADER_SLACK_SIGNATURE,
          "v0=" + hmac_hex(secret, slack_signed_payload(timestamp_s, payload)),
        ),
      )
    }
    Stripe =>
      headers.push(
        (
          HEADER_STRIPE_SIGNATURE,
          "t=" +
          timestamp_s.to_string() +
          ",v1=" +
          hmac_hex(secret, stripe_signed_payload(timestamp_s, payload)),
        ),
      )
    FeishuBot => ()
  }
  headers
}

///|
/// The header names this scheme uses, in the order `sign_headers` produces
/// them. `FeishuBot` returns an empty array.
pub fn SignatureProvider::header_names(
  self : SignatureProvider,
) -> Array[String] {
  let names : Array[String] = []
  for header in self.sign_headers("", "", 0) {
    names.push(header.0)
  }
  names
}

///|
/// A short human readable description, used by the CLI and diagnostics.
pub fn SignatureProvider::describe(self : SignatureProvider) -> String {
  match self {
    GitHub => "github (sha256= over the raw body)"
    Stripe => "stripe (t=,v1= over \".\")"
    Slack => "slack (v0= over \"v0::\")"
    FeishuBot => "feishu-bot (base64 sign in the JSON body)"
    Shopify => "shopify (base64 over the raw body)"
  }
}

///|
/// Parses a provider name, case insensitively: `github`, `stripe`, `slack`,
/// `feishu` (also `feishu-bot` / `lark`) and `shopify`.
pub fn SignatureProvider::parse(name : String) -> SignatureProvider? {
  match name.trim().to_lower() {
    "github" => Some(GitHub)
    "stripe" => Some(Stripe)
    "slack" => Some(Slack)
    "feishu" | "feishu-bot" | "lark" => Some(FeishuBot)
    "shopify" => Some(Shopify)
    _ => None
  }
}

///|
/// Every supported provider, useful for help output and iteration.
pub fn signature_providers() -> Array[SignatureProvider] {
  [GitHub, Stripe, Slack, FeishuBot, Shopify]
}

///|
/// Verifies an incoming request that was signed with this scheme.
///
/// Header lookup is case insensitive. `tolerance_s` bounds the age of the
/// signature for the timestamped schemes; pass a negative value to skip the
/// window check (GitHub and Shopify carry no timestamp at all). Verification is
/// always a constant-time comparison and never throws: malformed input simply
/// fails.
pub fn SignatureProvider::verify(
  self : SignatureProvider,
  secret : String,
  headers : Array[(String, String)],
  payload : String,
  now_s : Int,
  tolerance_s : Int,
) -> Bool {
  match self {
    GitHub =>
      match find_header(headers, HEADER_GITHUB_SIGNATURE) {
        Some(value) => verify_signature_header(secret, payload, value)
        None => false
      }
    Shopify =>
      match find_header(headers, HEADER_SHOPIFY_SIGNATURE) {
        Some(value) =>
          constant_time_eq(
            hmac_base64(secret, payload),
            strip_signature_scheme(value, ""),
          )
        None => false
      }
    Slack =>
      match
        (
          find_header(headers, HEADER_SLACK_SIGNATURE),
          find_header(headers, HEADER_SLACK_TIMESTAMP),
        ) {
        (Some(signature), Some(timestamp)) =>
          match parse_seconds(timestamp) {
            Some(timestamp_s) =>
              within_window(timestamp_s, now_s, tolerance_s) &&
              constant_time_eq(
                hmac_hex(secret, slack_signed_payload(timestamp_s, payload)),
                strip_signature_scheme(signature, "v0="),
              )
            None => false
          }
        _ => false
      }
    Stripe =>
      match find_header(headers, HEADER_STRIPE_SIGNATURE) {
        Some(value) => {
          let mut timestamp_s : Int? = None
          let candidates : Array[String] = []
          for part in split_signed_header(value) {
            if part.0 == "t" {
              timestamp_s = parse_seconds(part.1)
            }
            if part.0 == "v1" {
              candidates.push(part.1)
            }
          }
          match timestamp_s {
            None => false
            Some(timestamp_s) =>
              if !within_window(timestamp_s, now_s, tolerance_s) {
                false
              } else {
                let expected = hmac_hex(
                  secret,
                  stripe_signed_payload(timestamp_s, payload),
                )
                let mut matched = false
                for candidate in candidates {
                  if constant_time_eq(expected, candidate) {
                    matched = true
                  }
                }
                matched
              }
          }
        }
        None => false
      }
    FeishuBot => {
      let json = @json.parse(payload) catch { _ => return false }
      guard json is Object(map) else { return false }
      let timestamp = match json_string_field(map, "timestamp") {
        Some(value) => Some(value)
        None =>
          match json_timestamp_field(map, "timestamp") {
            Some(value) => Some(value.to_string())
            None => None
          }
      }
      match (timestamp, json_string_field(map, "sign")) {
        (Some(timestamp), Some(sign)) =>
          match parse_seconds(timestamp) {
            Some(timestamp_s) =>
              within_window(timestamp_s, now_s, tolerance_s) &&
              constant_time_eq(feishu_bot_sign(secret, timestamp_s), sign)
            None => false
          }
        _ => false
      }
    }
  }
}

///|
/// Builds the JSON body of a Feishu custom-bot text message, including the
/// `timestamp` and `sign` fields the bot expects.
pub fn feishu_bot_text_body(
  secret : String,
  text : String,
  timestamp_s : Int,
) -> String {
  Json::object({
    "timestamp": Json::string(timestamp_s.to_string()),
    "sign": Json::string(feishu_bot_sign(secret, timestamp_s)),
    "msg_type": Json::string("text"),
    "content": Json::object({ "text": Json::string(text) }),
  }).stringify()
}

///|
/// Returns a copy of `request` with this provider's signature headers appended.
///
/// The body and the MoonHook native signature are left untouched, so a delivery
/// can satisfy the provider scheme and MoonHook's own receiver at the same
/// time. For `FeishuBot` the signature belongs in the body: build it with
/// `feishu_bot_text_body` and use that as the event payload.
pub fn SignedRequest::with_provider_signature(
  self : SignedRequest,
  provider : SignatureProvider,
  secret : String,
  timestamp_s : Int,
) -> SignedRequest {
  let headers = self.headers.copy()
  for header in provider.sign_headers(secret, self.body, timestamp_s) {
    headers.push(header)
  }
  {
    http_method: self.http_method,
    url: self.url,
    headers,
    body: self.body,
    signature: self.signature,
  }
}