///|
pub(all) struct KeyPattern {
  id : String
  key : String
  severity : Severity
  minimum_length : Int
  message : String
} derive(Eq, Debug)

///|
pub fn KeyPattern::new(
  id : String,
  key : String,
  severity : Severity,
  minimum_length? : Int = 6,
  message? : String = "Sensitive configuration value",
) -> KeyPattern {
  { id, key, severity, minimum_length, message }
}

///|
pub fn builtin_key_patterns() -> Array[KeyPattern] {
  [
    KeyPattern::new("generic-password", "password", Critical),
    KeyPattern::new("generic-password", "passwd", Critical),
    KeyPattern::new("generic-password", "pwd", High),
    KeyPattern::new("generic-secret", "secret", Critical),
    KeyPattern::new("generic-secret", "client_secret", Critical),
    KeyPattern::new("generic-secret", "client-secret", Critical),
    KeyPattern::new("generic-token", "token", Critical),
    KeyPattern::new("generic-token", "access_token", Critical),
    KeyPattern::new("generic-token", "access-token", Critical),
    KeyPattern::new("generic-token", "refresh_token", Critical),
    KeyPattern::new("generic-token", "refresh-token", Critical),
    KeyPattern::new("generic-api-key", "api_key", Critical),
    KeyPattern::new("generic-api-key", "api-key", Critical),
    KeyPattern::new("generic-api-key", "apikey", Critical),
    KeyPattern::new("generic-private-key", "private_key", Critical),
    KeyPattern::new("generic-private-key", "private-key", Critical),
    KeyPattern::new("generic-signing-key", "signing_key", Critical),
    KeyPattern::new("generic-signing-key", "signing-key", Critical),
    KeyPattern::new("generic-encryption-key", "encryption_key", Critical),
    KeyPattern::new("generic-encryption-key", "encryption-key", Critical),
    KeyPattern::new("generic-auth", "authorization", Critical),
    KeyPattern::new("generic-auth", "auth_token", Critical),
    KeyPattern::new("generic-auth", "auth-token", Critical),
    KeyPattern::new("generic-auth", "bearer", Critical),
    KeyPattern::new("database-password", "db_password", Critical),
    KeyPattern::new("database-password", "database_password", Critical),
    KeyPattern::new("database-url", "database_url", Critical, minimum_length=12),
    KeyPattern::new("database-url", "db_url", Critical, minimum_length=12),
    KeyPattern::new("redis-url", "redis_url", Critical, minimum_length=12),
    KeyPattern::new("redis-password", "redis_password", Critical),
    KeyPattern::new("mongodb-uri", "mongodb_uri", Critical, minimum_length=12),
    KeyPattern::new("postgres-url", "postgres_url", Critical, minimum_length=12),
    KeyPattern::new("mysql-url", "mysql_url", Critical, minimum_length=12),
    KeyPattern::new("jdbc-url", "jdbc_url", Critical, minimum_length=12),
    KeyPattern::new("aws-access-key", "aws_access_key_id", Critical),
    KeyPattern::new("aws-secret-key", "aws_secret_access_key", Critical),
    KeyPattern::new("aws-session-token", "aws_session_token", Critical),
    KeyPattern::new("aws-account", "aws_account_id", Medium),
    KeyPattern::new("azure-client-secret", "azure_client_secret", Critical),
    KeyPattern::new("azure-tenant", "azure_tenant_id", Medium),
    KeyPattern::new("azure-storage-key", "azure_storage_key", Critical),
    KeyPattern::new(
      "gcp-credential",
      "google_application_credentials",
      Critical,
    ),
    KeyPattern::new("gcp-private-key", "google_private_key", Critical),
    KeyPattern::new("gcp-api-key", "google_api_key", Critical),
    KeyPattern::new("github-token", "github_token", Critical),
    KeyPattern::new("github-token", "gh_token", Critical),
    KeyPattern::new("gitlab-token", "gitlab_token", Critical),
    KeyPattern::new("bitbucket-token", "bitbucket_token", Critical),
    KeyPattern::new("npm-token", "npm_token", Critical),
    KeyPattern::new("pypi-token", "pypi_token", Critical),
    KeyPattern::new("cargo-token", "cargo_registry_token", Critical),
    KeyPattern::new("docker-password", "docker_password", Critical),
    KeyPattern::new("docker-token", "docker_token", Critical),
    KeyPattern::new("registry-password", "registry_password", Critical),
    KeyPattern::new("kubernetes-token", "kubernetes_token", Critical),
    KeyPattern::new("kubernetes-token", "k8s_token", Critical),
    KeyPattern::new("helm-password", "helm_password", Critical),
    KeyPattern::new("terraform-token", "terraform_token", Critical),
    KeyPattern::new("vault-token", "vault_token", Critical),
    KeyPattern::new("consul-token", "consul_http_token", Critical),
    KeyPattern::new("nomad-token", "nomad_token", Critical),
    KeyPattern::new("cloudflare-token", "cloudflare_api_token", Critical),
    KeyPattern::new("cloudflare-key", "cloudflare_api_key", Critical),
    KeyPattern::new("digitalocean-token", "digitalocean_access_token", Critical),
    KeyPattern::new("heroku-key", "heroku_api_key", Critical),
    KeyPattern::new("vercel-token", "vercel_token", Critical),
    KeyPattern::new("netlify-token", "netlify_auth_token", Critical),
    KeyPattern::new("fly-token", "fly_api_token", Critical),
    KeyPattern::new("openai-key", "openai_api_key", Critical),
    KeyPattern::new("anthropic-key", "anthropic_api_key", Critical),
    KeyPattern::new("gemini-key", "gemini_api_key", Critical),
    KeyPattern::new("deepseek-key", "deepseek_api_key", Critical),
    KeyPattern::new("cohere-key", "cohere_api_key", Critical),
    KeyPattern::new("huggingface-token", "huggingface_token", Critical),
    KeyPattern::new("replicate-token", "replicate_api_token", Critical),
    KeyPattern::new("groq-key", "groq_api_key", Critical),
    KeyPattern::new("mistral-key", "mistral_api_key", Critical),
    KeyPattern::new("slack-token", "slack_token", Critical),
    KeyPattern::new("slack-secret", "slack_signing_secret", Critical),
    KeyPattern::new("discord-token", "discord_token", Critical),
    KeyPattern::new("telegram-token", "telegram_bot_token", Critical),
    KeyPattern::new("teams-secret", "teams_client_secret", Critical),
    KeyPattern::new("twilio-token", "twilio_auth_token", Critical),
    KeyPattern::new("sendgrid-key", "sendgrid_api_key", Critical),
    KeyPattern::new("mailgun-key", "mailgun_api_key", Critical),
    KeyPattern::new("postmark-token", "postmark_server_token", Critical),
    KeyPattern::new("stripe-secret", "stripe_secret_key", Critical),
    KeyPattern::new("stripe-webhook", "stripe_webhook_secret", Critical),
    KeyPattern::new("paypal-secret", "paypal_client_secret", Critical),
    KeyPattern::new("shopify-token", "shopify_access_token", Critical),
    KeyPattern::new("square-token", "square_access_token", Critical),
    KeyPattern::new("adyen-key", "adyen_api_key", Critical),
    KeyPattern::new("sentry-token", "sentry_auth_token", Critical),
    KeyPattern::new("datadog-key", "datadog_api_key", Critical),
    KeyPattern::new("newrelic-key", "new_relic_license_key", Critical),
    KeyPattern::new("grafana-token", "grafana_service_account_token", Critical),
    KeyPattern::new("honeycomb-key", "honeycomb_api_key", Critical),
    KeyPattern::new("splunk-token", "splunk_hec_token", Critical),
    KeyPattern::new("elastic-password", "elastic_password", Critical),
    KeyPattern::new("oauth-secret", "oauth_client_secret", Critical),
    KeyPattern::new("oidc-secret", "oidc_client_secret", Critical),
    KeyPattern::new("jwt-secret", "jwt_secret", Critical),
    KeyPattern::new("session-secret", "session_secret", Critical),
    KeyPattern::new("cookie-secret", "cookie_secret", Critical),
    KeyPattern::new("csrf-secret", "csrf_secret", Critical),
    KeyPattern::new("webhook-secret", "webhook_secret", Critical),
    KeyPattern::new("basic-password", "basic_auth_password", Critical),
    KeyPattern::new("ldap-password", "ldap_bind_password", Critical),
    KeyPattern::new("smtp-password", "smtp_password", Critical),
    KeyPattern::new("ssh-passphrase", "ssh_passphrase", Critical),
    KeyPattern::new("keystore-password", "keystore_password", Critical),
    KeyPattern::new("truststore-password", "truststore_password", Critical),
    KeyPattern::new("certificate-password", "certificate_password", Critical),
  ]
}

///|
fn ascii_lower_string(text : String) -> String {
  let out = StringBuilder::new()
  for c in text {
    out.write_char(
      if c >= 'A' && c <= 'Z' {
        (c.to_int() + 32).unsafe_to_char()
      } else {
        c
      },
    )
  }
  out.to_string()
}

///|
fn skip_horizontal_space(chars : Array[Char], position : Int) -> Int {
  let mut i = position
  while i < chars.length() && (chars[i] == ' ' || chars[i] == '\t') {
    i = i + 1
  }
  i
}

///|
fn find_assignment_separator(chars : Array[Char], position : Int) -> Int {
  let i = skip_horizontal_space(chars, position)
  if i < chars.length() && (chars[i] == '=' || chars[i] == ':') {
    i
  } else {
    -1
  }
}

///|
fn parse_assignment_value(chars : Array[Char], position : Int) -> Span {
  let mut start = skip_horizontal_space(chars, position)
  if start >= chars.length() {
    return { start, end: start }
  }
  let quote = if chars[start] == '"' || chars[start] == '\'' {
    chars[start]
  } else {
    '\u0000'
  }
  if quote != '\u0000' {
    start = start + 1
  }
  let mut end = start
  if quote != '\u0000' {
    while end < chars.length() &&
          chars[end] != quote &&
          chars[end] != '\n' &&
          chars[end] != '\r' {
      end = end + 1
    }
  } else {
    while end < chars.length() &&
          chars[end] != '\n' &&
          chars[end] != '\r' &&
          chars[end] != ',' &&
          chars[end] != '}' {
      end = end + 1
    }
    while end > start && (chars[end - 1] == ' ' || chars[end - 1] == '\t') {
      end = end - 1
    }
  }
  { start, end }
}

///|
fn scan_key_pattern(
  chars : Array[Char],
  lower : Array[Char],
  pattern : KeyPattern,
  preview : Bool,
) -> Array[Finding] {
  let out : Array[Finding] = []
  let needle = pattern.key.to_array()
  let mut from = 0
  while from < lower.length() {
    let position = index_of_from(lower, needle, from)
    if position < 0 {
      break
    }
    let before_ok = position == 0 || is_word_boundary(lower[position - 1])
    let after_position = position + needle.length()
    let after_ok = after_position >= lower.length() ||
      is_word_boundary(lower[after_position])
    if before_ok && after_ok {
      let separator = find_assignment_separator(chars, after_position)
      if separator >= 0 {
        let value = parse_assignment_value(chars, separator + 1)
        if value.length() >= pattern.minimum_length {
          let confidence = match pattern.id {
            "generic-password"
            | "generic-secret"
            | "generic-token"
            | "generic-api-key"
            | "generic-private-key"
            | "generic-signing-key"
            | "generic-encryption-key"
            | "generic-auth" => 88
            _ => 99
          }
          out.push(
            Finding::new(
              pattern.id,
              Custom,
              pattern.severity,
              value.start,
              value.end,
              confidence,
              safe_preview(chars, value.start, value.end, preview),
              pattern.message,
            ),
          )
        }
      }
    }
    from = position + needle.length()
  }
  out
}

///|
fn scan_contextual_assignments(
  chars : Array[Char],
  preview : Bool,
) -> Array[Finding] {
  let lower = ascii_lower_string(slice_chars(chars, 0, chars.length())).to_array()
  let out : Array[Finding] = []
  for pattern in builtin_key_patterns() {
    for finding in scan_key_pattern(chars, lower, pattern, preview) {
      out.push(finding)
    }
  }
  out
}

///|
fn scan_pem_blocks(chars : Array[Char], preview : Bool) -> Array[Finding] {
  let out : Array[Finding] = []
  let begin = "-----BEGIN ".to_array()
  let end_marker = "-----END ".to_array()
  let mut from = 0
  while from < chars.length() {
    let start = index_of_from(chars, begin, from)
    if start < 0 {
      break
    }
    let end_start = index_of_from(chars, end_marker, start + begin.length())
    if end_start < 0 {
      break
    }
    let mut end = end_start
    while end < chars.length() && chars[end] != '\n' && chars[end] != '\r' {
      end = end + 1
    }
    let header_end = index_of_from(
      chars,
      "-----".to_array(),
      start + begin.length(),
    )
    let label = if header_end > start {
      slice_chars(chars, start + begin.length(), header_end)
    } else {
      "PEM"
    }
    let severity = if label.contains("PRIVATE KEY") { Critical } else { Medium }
    out.push(
      Finding::new(
        "pem-block",
        Custom,
        severity,
        start,
        end,
        99,
        safe_preview(chars, start, end, preview),
        "PEM encoded " + label,
      ),
    )
    from = end
  }
  out
}

///|
fn scan_connection_strings(
  chars : Array[Char],
  preview : Bool,
) -> Array[Finding] {
  let schemes = [
    "postgres://", "postgresql://", "mysql://", "mongodb://", "mongodb+srv://", "redis://",
    "rediss://", "amqp://", "amqps://",
  ]
  let out : Array[Finding] = []
  for scheme in schemes {
    let needle = scheme.to_array()
    let mut from = 0
    while from < chars.length() {
      let start = index_of_from(chars, needle, from)
      if start < 0 {
        break
      }
      let mut end = start + needle.length()
      let mut at = -1
      while end < chars.length() &&
            chars[end] != ' ' &&
            chars[end] != '\t' &&
            chars[end] != '\n' &&
            chars[end] != '\r' &&
            chars[end] != '"' &&
            chars[end] != '\'' {
        if chars[end] == '@' {
          at = end
        }
        end = end + 1
      }
      if at > start &&
        index_of_from(chars, ":".to_array(), start + needle.length()) < at {
        out.push(
          Finding::new(
            "credential-url",
            Custom,
            Critical,
            start,
            end,
            97,
            safe_preview(chars, start, end, preview),
            "Connection URL containing credentials",
          ),
        )
      }
      from = start + needle.length()
    }
  }
  out
}