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

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

///|
pub fn Header::normalized_name(self : Header) -> String {
  ascii_lower(self.name)
}

///|
pub(all) enum HttpMethod {
  Get
  Post
  Put
  Patch
  Delete
  Unknown(String)
} derive(Eq, Debug)

///|
pub fn method_name(http_method : HttpMethod) -> String {
  match http_method {
    Get => "GET"
    Post => "POST"
    Put => "PUT"
    Patch => "PATCH"
    Delete => "DELETE"
    Unknown(value) => value
  }
}

///|
pub struct WebhookRequest {
  provider : String
  http_method : HttpMethod
  path : String
  body : String
  headers : Array[Header]
  received_at : Int
  remote_ip : String
} derive(Eq, Debug)

///|
pub fn WebhookRequest::new(
  provider : String,
  http_method : HttpMethod,
  path : String,
  body : String,
  received_at : Int,
  headers? : Array[Header] = [],
  remote_ip? : String = "",
) -> WebhookRequest {
  { provider, http_method, path, body, headers, received_at, remote_ip }
}

///|
pub fn WebhookRequest::header(self : WebhookRequest, name : String) -> String? {
  let wanted = ascii_lower(name)
  for i in 0.. WebhookRequest {
  let old = self.headers
  let header = Header::new(name, value)
  let headers = Array::makei(old.length() + 1, i => {
    if i < old.length() {
      old[i]
    } else {
      header
    }
  })
  { ..self, headers, }
}

///|
pub fn WebhookRequest::without_header(
  self : WebhookRequest,
  name : String,
) -> WebhookRequest {
  let wanted = ascii_lower(name)
  let mut count = 0
  for header in self.headers {
    if header.normalized_name() != wanted {
      count = count + 1
    }
  }
  let mut offset = 0
  let headers = Array::makei(count, _ => {
    while self.headers[offset].normalized_name() == wanted {
      offset = offset + 1
    }
    let header = self.headers[offset]
    offset = offset + 1
    header
  })
  { ..self, headers, }
}

///|
pub fn WebhookRequest::with_replaced_header(
  self : WebhookRequest,
  name : String,
  value : String,
) -> WebhookRequest {
  self.without_header(name).with_header(name, value)
}

///|
pub fn WebhookRequest::signature(
  self : WebhookRequest,
  header_name : String,
) -> String {
  match self.header(header_name) {
    Some(value) => value
    None => ""
  }
}

///|
pub fn WebhookRequest::timestamp(
  self : WebhookRequest,
  header_name : String,
) -> Int {
  match self.header(header_name) {
    Some(value) => parse_positive_int(value)
    None => 0
  }
}

///|
pub fn WebhookRequest::event_type(
  self : WebhookRequest,
  header_name : String,
) -> String {
  match self.header(header_name) {
    Some(value) => value
    None => ""
  }
}

///|
pub fn WebhookRequest::event_id(
  self : WebhookRequest,
  header_name : String,
) -> String {
  match self.header(header_name) {
    Some(value) => value
    None => ""
  }
}

///|
pub struct ProviderProfile {
  name : String
  signature_header : String
  timestamp_header : String
  event_type_header : String
  event_id_header : String
  signature_prefix : String
  timestamp_tolerance_seconds : Int
} derive(Eq, Debug)

///|
pub fn ProviderProfile::new(
  name : String,
  signature_header : String,
  timestamp_header : String,
  event_type_header : String,
  event_id_header : String,
  signature_prefix? : String = "",
  timestamp_tolerance_seconds? : Int = 300,
) -> ProviderProfile {
  {
    name,
    signature_header,
    timestamp_header,
    event_type_header,
    event_id_header,
    signature_prefix,
    timestamp_tolerance_seconds,
  }
}

///|
pub fn ProviderProfile::github() -> ProviderProfile {
  ProviderProfile::new(
    "github",
    "x-hub-signature-256",
    "",
    "x-github-event",
    "x-github-delivery",
    signature_prefix="sha256=",
  )
}

///|
pub fn ProviderProfile::feishu() -> ProviderProfile {
  ProviderProfile::new(
    "feishu", "x-lark-signature", "x-lark-request-timestamp", "x-lark-event-type",
    "x-lark-request-nonce",
  )
}

///|
pub fn ProviderProfile::stripe() -> ProviderProfile {
  ProviderProfile::new("stripe", "stripe-signature", "", "", "")
}

///|
pub fn ProviderProfile::generic() -> ProviderProfile {
  ProviderProfile::new(
    "generic", "x-webhook-signature", "x-webhook-timestamp", "x-webhook-event", "x-webhook-id",
  )
}

///|
pub fn provider_catalog() -> Array[ProviderProfile] {
  [
    ProviderProfile::github(),
    ProviderProfile::feishu(),
    ProviderProfile::stripe(),
    ProviderProfile::generic(),
  ]
}

///|
pub fn find_provider(name : String) -> ProviderProfile {
  let providers = provider_catalog()
  for i in 0.. VerificationPolicy {
  {
    provider: ProviderProfile::github(),
    secret: "demo-secret",
    now: 1700000000,
    require_signature: true,
    require_timestamp: false,
    require_event_id: true,
    allow_unknown_event: true,
  }
}

///|
pub fn VerificationPolicy::for_provider(
  provider : ProviderProfile,
) -> VerificationPolicy {
  {
    ..VerificationPolicy::default(),
    provider,
    require_timestamp: provider.name != "github",
  }
}

///|
pub fn VerificationPolicy::with_secret(
  self : VerificationPolicy,
  secret : String,
) -> VerificationPolicy {
  { ..self, secret, }
}

///|
pub fn VerificationPolicy::with_clock(
  self : VerificationPolicy,
  now : Int,
) -> VerificationPolicy {
  { ..self, now, }
}

///|
pub fn VerificationPolicy::with_unknown_events(
  self : VerificationPolicy,
  allowed : Bool,
) -> VerificationPolicy {
  { ..self, allow_unknown_event: allowed }
}

///|
pub(all) enum VerificationStatus {
  Accepted
  Rejected
  Skipped
} derive(Eq, Debug)

///|
pub(all) enum DiagnosticCode {
  MissingSignature
  SignatureMismatch
  MissingTimestamp
  TimestampOutsideWindow
  MissingEventId
  ReplayDetected
  DuplicateIdempotencyKey
  UnknownEvent
  ProviderMismatch
  AcceptedDiagnostic
} derive(Eq, Debug)

///|
pub struct GuardDiagnostic {
  code : DiagnosticCode
  message : String
} derive(Eq, Debug)

///|
pub fn GuardDiagnostic::new(
  code : DiagnosticCode,
  message : String,
) -> GuardDiagnostic {
  { code, message }
}

///|
pub fn diagnostic_name(code : DiagnosticCode) -> String {
  match code {
    MissingSignature => "missing_signature"
    SignatureMismatch => "signature_mismatch"
    MissingTimestamp => "missing_timestamp"
    TimestampOutsideWindow => "timestamp_outside_window"
    MissingEventId => "missing_event_id"
    ReplayDetected => "replay_detected"
    DuplicateIdempotencyKey => "duplicate_idempotency_key"
    UnknownEvent => "unknown_event"
    ProviderMismatch => "provider_mismatch"
    AcceptedDiagnostic => "accepted"
  }
}

///|
pub struct VerificationResult {
  status : VerificationStatus
  provider : String
  event_type : String
  event_id : String
  diagnostics : Array[GuardDiagnostic]
} derive(Eq, Debug)

///|
pub fn VerificationResult::new(
  status : VerificationStatus,
  provider : String,
  event_type : String,
  event_id : String,
  diagnostics? : Array[GuardDiagnostic] = [],
) -> VerificationResult {
  { status, provider, event_type, event_id, diagnostics }
}

///|
pub fn VerificationResult::accepted(
  provider : String,
  event_type : String,
  event_id : String,
) -> VerificationResult {
  VerificationResult::new(Accepted, provider, event_type, event_id, diagnostics=[
    GuardDiagnostic::new(AcceptedDiagnostic, "accepted"),
  ])
}

///|
pub fn VerificationResult::rejected(
  provider : String,
  event_type : String,
  event_id : String,
  diagnostics : Array[GuardDiagnostic],
) -> VerificationResult {
  VerificationResult::new(
    Rejected,
    provider,
    event_type,
    event_id,
    diagnostics~,
  )
}

///|
pub fn VerificationResult::is_ok(self : VerificationResult) -> Bool {
  self.status == Accepted
}

///|
pub fn VerificationResult::to_text(self : VerificationResult) -> String {
  let builder = StringBuilder::new()
  builder.write_string("status=")
  match self.status {
    Accepted => builder.write_string("accepted")
    Rejected => builder.write_string("rejected")
    Skipped => builder.write_string("skipped")
  }
  builder.write_string(" provider=")
  builder.write_string(self.provider)
  builder.write_string(" event=")
  builder.write_string(self.event_type)
  builder.write_string(" id=")
  builder.write_string(self.event_id)
  for i in 0..