///|
/// Categories used by the privacy scanner.
pub(all) enum SensitiveKind {
  SensitiveSecret
  SensitiveCredential
  SensitivePersonal
  SensitivePayment
  SensitiveNetwork
} derive(Eq, Debug)

///|
pub fn SensitiveKind::label(self : SensitiveKind) -> String {
  match self {
    SensitiveSecret => "secret"
    SensitiveCredential => "credential"
    SensitivePersonal => "personal"
    SensitivePayment => "payment"
    SensitiveNetwork => "network"
  }
}

///|
pub fn SensitiveKind::severity(self : SensitiveKind) -> Severity {
  match self {
    SensitiveSecret => Critical
    SensitiveCredential => Critical
    SensitivePersonal => Warning
    SensitivePayment => Critical
    SensitiveNetwork => Warning
  }
}

///|
/// How a sensitive value should be represented in safe output.
pub(all) enum RedactionMode {
  RedactFull
  RedactKeepLastFour
  RedactStableToken
} derive(Eq, Debug)

///|
pub fn RedactionMode::label(self : RedactionMode) -> String {
  match self {
    RedactFull => "full"
    RedactKeepLastFour => "keep_last_four"
    RedactStableToken => "stable_token"
  }
}

///|
/// Privacy rules for key-aware and value-aware redaction.
pub struct RedactionPolicy {
  mode : RedactionMode
  secret_key_fragments : Array[String]
  personal_key_fragments : Array[String]
  payment_key_fragments : Array[String]
  network_key_fragments : Array[String]
  allow_keys : Array[String]
  detect_values : Bool
  redact_network_values : Bool
} derive(Debug)

///|
pub fn RedactionPolicy::default() -> RedactionPolicy {
  {
    mode: RedactFull,
    secret_key_fragments: [
      "password", "passwd", "secret", "token", "api_key", "apikey", "access_key",
      "authorization", "credential", "private_key", "cookie", "session",
    ],
    personal_key_fragments: [
      "email", "e_mail", "phone", "mobile", "username", "user_name",
    ],
    payment_key_fragments: [
      "card_number", "credit_card", "iban", "account_number", "payment_token",
    ],
    network_key_fragments: [
      "client_ip", "remote_ip", "source_ip", "destination_ip",
    ],
    allow_keys: [],
    detect_values: true,
    redact_network_values: false,
  }
}

///|
pub fn RedactionPolicy::strict() -> RedactionPolicy {
  {
    ..RedactionPolicy::default(),
    mode: RedactStableToken,
    redact_network_values: true,
  }
}

///|
pub fn RedactionPolicy::key_only() -> RedactionPolicy {
  { ..RedactionPolicy::default(), detect_values: false }
}

///|
pub fn RedactionPolicy::mode(self : RedactionPolicy) -> RedactionMode {
  self.mode
}

///|
pub fn RedactionPolicy::with_mode(
  self : RedactionPolicy,
  mode : RedactionMode,
) -> RedactionPolicy {
  { ..self, mode, }
}

///|
pub fn RedactionPolicy::with_allow_keys(
  self : RedactionPolicy,
  allow_keys : Array[String],
) -> RedactionPolicy {
  { ..self, allow_keys, }
}

///|
pub fn RedactionPolicy::with_value_detection(
  self : RedactionPolicy,
  detect_values : Bool,
) -> RedactionPolicy {
  { ..self, detect_values, }
}

///|
pub fn RedactionPolicy::with_network_values(
  self : RedactionPolicy,
  redact_network_values : Bool,
) -> RedactionPolicy {
  { ..self, redact_network_values, }
}

///|
pub struct PrivacyFinding {
  key : String
  kind : SensitiveKind
  reason : String
  offset : Int
} derive(Eq, Debug)

///|
pub fn PrivacyFinding::key(self : PrivacyFinding) -> String {
  self.key
}

///|
pub fn PrivacyFinding::kind(self : PrivacyFinding) -> SensitiveKind {
  self.kind
}

///|
pub fn PrivacyFinding::reason(self : PrivacyFinding) -> String {
  self.reason
}

///|
pub fn PrivacyFinding::offset(self : PrivacyFinding) -> Int {
  self.offset
}

///|
pub fn PrivacyFinding::severity(self : PrivacyFinding) -> Severity {
  self.kind.severity()
}

///|
/// The privacy-safe projection of one parsed line.
///
/// Reports never include the original sensitive values. The original source is
/// retained only through the parsed result for in-process callers and is not
/// serialized by the report methods.
pub struct RedactionResult {
  parsed : ParseResult
  safe_line : String
  findings : Array[PrivacyFinding]
  redacted_keys : Array[String]
} derive(Debug)

///|
pub fn RedactionResult::parsed(self : RedactionResult) -> ParseResult {
  self.parsed
}

///|
pub fn RedactionResult::safe_line(self : RedactionResult) -> String {
  self.safe_line
}

///|
pub fn RedactionResult::findings(
  self : RedactionResult,
) -> Array[PrivacyFinding] {
  self.findings
}

///|
pub fn RedactionResult::redacted_keys(self : RedactionResult) -> Array[String] {
  self.redacted_keys
}

///|
pub fn RedactionResult::finding_count(self : RedactionResult) -> Int {
  self.findings.length()
}

///|
pub fn RedactionResult::redacted_count(self : RedactionResult) -> Int {
  self.redacted_keys.length()
}

///|
pub fn RedactionResult::is_safe(self : RedactionResult) -> Bool {
  self.findings.length() == 0
}

///|
pub fn RedactionResult::critical_count(self : RedactionResult) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.severity() == Critical {
      count = count + 1
    }
  }
  count
}

///|
pub fn RedactionResult::decision(self : RedactionResult) -> String {
  if self.critical_count() > 0 {
    "sanitize"
  } else if self.finding_count() > 0 {
    "review"
  } else {
    "accept"
  }
}

///|
pub fn RedactionResult::text_report(self : RedactionResult) -> String {
  let mut output = "MoonLogfmt privacy report\n"
  output = output + "findings: " + self.finding_count().to_string() + "\n"
  output = output +
    "redacted_fields: " +
    self.redacted_count().to_string() +
    "\n"
  output = output + "decision: " + self.decision() + "\n"
  output = output + "safe_line: " + self.safe_line + "\n"
  if self.findings.length() == 0 {
    return output + "\nNo sensitive fields detected."
  }
  output = output + "\nFindings:\n"
  for finding in self.findings {
    output = output +
      "- [" +
      finding.severity().label() +
      "] " +
      finding.kind().label() +
      " key=" +
      finding.key() +
      ": " +
      finding.reason() +
      "\n"
  }
  output
}

///|
pub fn RedactionResult::json_report(self : RedactionResult) -> String {
  let mut output = "{"
  output = output + "\"safe_line\":\"" + escape_json(self.safe_line) + "\","
  output = output + "\"findings\":" + self.finding_count().to_string() + ","
  output = output +
    "\"redacted_fields\":" +
    self.redacted_count().to_string() +
    ","
  output = output + "\"decision\":\"" + self.decision() + "\","
  output = output + "\"items\":["
  for index = 0; index < self.findings.length(); index = index + 1 {
    let finding = self.findings[index]
    if index > 0 {
      output = output + ","
    }
    output = output + "{"
    output = output + "\"key\":\"" + escape_json(finding.key()) + "\","
    output = output + "\"kind\":\"" + finding.kind().label() + "\","
    output = output + "\"severity\":\"" + finding.severity().label() + "\","
    output = output + "\"reason\":\"" + escape_json(finding.reason()) + "\","
    output = output + "\"offset\":" + finding.offset().to_string()
    output = output + "}"
  }
  output + "]}"
}

///|
pub fn redact_line(
  line : String,
  policy? : RedactionPolicy = RedactionPolicy::default(),
) -> RedactionResult {
  redact_parsed(parse(line), policy)
}

///|
pub fn redact_parsed(
  parsed : ParseResult,
  policy : RedactionPolicy,
) -> RedactionResult {
  let findings : Array[PrivacyFinding] = []
  let redacted_keys : Array[String] = []
  let safe_parts : Array[String] = []
  for field in parsed.fields() {
    let sensitive = privacy_classify_field(field, policy)
    if sensitive.found {
      findings.push({
        key: field.key(),
        kind: sensitive.kind,
        reason: sensitive.reason,
        offset: field.offset(),
      })
      if !redacted_keys.contains(field.key()) {
        redacted_keys.push(field.key())
      }
      let replacement = privacy_redact_value(field.value(), policy.mode())
      safe_parts.push(privacy_render_pair(field.key(), replacement))
    } else if field.is_flag() {
      safe_parts.push(field.key())
    } else {
      safe_parts.push(privacy_render_pair(field.key(), field.value()))
    }
  }
  { parsed, safe_line: safe_parts.join(" "), findings, redacted_keys }
}

///|
/// Scans without changing policy defaults. The returned safe line can be used
/// directly in diagnostics, fixtures, or support bundles.
pub fn scan_privacy(line : String) -> RedactionResult {
  redact_line(line)
}

///|
priv struct SensitiveMatch {
  found : Bool
  kind : SensitiveKind
  reason : String
}

///|
fn privacy_no_match() -> SensitiveMatch {
  { found: false, kind: SensitivePersonal, reason: "" }
}

///|
fn privacy_classify_field(
  field : Field,
  policy : RedactionPolicy,
) -> SensitiveMatch {
  if policy.allow_keys.contains(field.key()) {
    return privacy_no_match()
  }
  let lower_key = profile_ascii_lower(field.key())
  if privacy_contains_any(lower_key, policy.payment_key_fragments) {
    return {
      found: true,
      kind: SensitivePayment,
      reason: "field name matches a payment-data pattern",
    }
  }
  if privacy_contains_any(lower_key, policy.secret_key_fragments) {
    let kind = if lower_key.contains("password") ||
      lower_key.contains("secret") ||
      lower_key.contains("private_key") {
      SensitiveSecret
    } else {
      SensitiveCredential
    }
    return {
      found: true,
      kind,
      reason: "field name matches a secret or credential pattern",
    }
  }
  if privacy_contains_any(lower_key, policy.personal_key_fragments) {
    return {
      found: true,
      kind: SensitivePersonal,
      reason: "field name matches a personal-data pattern",
    }
  }
  if privacy_contains_any(lower_key, policy.network_key_fragments) {
    return {
      found: true,
      kind: SensitiveNetwork,
      reason: "field name matches a network-identity pattern",
    }
  }
  if !policy.detect_values {
    return privacy_no_match()
  }
  privacy_classify_value(field.value(), policy)
}

///|
fn privacy_classify_value(
  value : String,
  policy : RedactionPolicy,
) -> SensitiveMatch {
  let lower = profile_ascii_lower(value)
  if profile_has_prefix(lower, "bearer ") {
    return {
      found: true,
      kind: SensitiveCredential,
      reason: "value resembles a bearer credential",
    }
  }
  if profile_has_prefix(lower, "basic ") && value.to_array().length() > 12 {
    return {
      found: true,
      kind: SensitiveCredential,
      reason: "value resembles a basic authorization credential",
    }
  }
  if profile_has_prefix(lower, "sk-") ||
    profile_has_prefix(lower, "ghp_") ||
    profile_has_prefix(lower, "github_pat_") ||
    profile_has_prefix(lower, "akia") {
    return {
      found: true,
      kind: SensitiveCredential,
      reason: "value resembles a provider access key",
    }
  }
  if lower.contains("private key") {
    return {
      found: true,
      kind: SensitiveSecret,
      reason: "value contains a private-key marker",
    }
  }
  if privacy_is_jwt(value) {
    return {
      found: true,
      kind: SensitiveCredential,
      reason: "value resembles a three-segment token",
    }
  }
  if privacy_is_payment_card(value) {
    return {
      found: true,
      kind: SensitivePayment,
      reason: "value passes a payment-card checksum",
    }
  }
  if classify_value(value) == ValueEmail {
    return {
      found: true,
      kind: SensitivePersonal,
      reason: "value resembles an email address",
    }
  }
  if privacy_is_phone(value) {
    return {
      found: true,
      kind: SensitivePersonal,
      reason: "value resembles a formatted phone number",
    }
  }
  if policy.redact_network_values && classify_value(value) == ValueIPv4 {
    return {
      found: true,
      kind: SensitiveNetwork,
      reason: "strict policy treats IP addresses as sensitive",
    }
  }
  if privacy_is_high_entropy_secret(value) {
    return {
      found: true,
      kind: SensitiveCredential,
      reason: "long mixed-character value resembles a generated secret",
    }
  }
  privacy_no_match()
}

///|
fn privacy_contains_any(value : String, fragments : Array[String]) -> Bool {
  for fragment in fragments {
    if value.contains(fragment) {
      return true
    }
  }
  false
}

///|
fn privacy_render_pair(key : String, value : String) -> String {
  if needs_quote(value) {
    key + "=\"" + escape_logfmt(value) + "\""
  } else {
    key + "=" + value
  }
}

///|
fn privacy_redact_value(value : String, mode : RedactionMode) -> String {
  match mode {
    RedactFull => "[REDACTED]"
    RedactKeepLastFour => {
      let chars = value.to_array()
      if chars.length() <= 4 {
        "****"
      } else {
        "***" + String::from_array(chars[chars.length() - 4:chars.length()])
      }
    }
    RedactStableToken => "redacted_" + privacy_stable_hash(value).to_string()
  }
}

///|
fn privacy_stable_hash(value : String) -> Int {
  let mut hash = 17
  for char in value.to_array() {
    hash = (hash * 131 + char.to_int()) % 1000003
  }
  if hash < 0 {
    -hash
  } else {
    hash
  }
}

///|
fn privacy_is_jwt(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() < 24 {
    return false
  }
  let mut dots = 0
  let mut segment_length = 0
  for char in chars {
    if char == '.' {
      if segment_length < 4 {
        return false
      }
      dots = dots + 1
      segment_length = 0
    } else if privacy_is_token_char(char) {
      segment_length = segment_length + 1
    } else {
      return false
    }
  }
  dots == 2 && segment_length >= 4
}

///|
fn privacy_is_token_char(char : Char) -> Bool {
  (char >= 'a' && char <= 'z') ||
  (char >= 'A' && char <= 'Z') ||
  profile_is_digit(char) ||
  char == '-' ||
  char == '_' ||
  char == '+' ||
  char == '/' ||
  char == '='
}

///|
fn privacy_is_payment_card(value : String) -> Bool {
  let digits : Array[Int] = []
  for char in value.to_array() {
    if profile_is_digit(char) {
      digits.push(char.to_int() - '0'.to_int())
    } else if char != ' ' && char != '-' {
      return false
    }
  }
  if digits.length() < 13 || digits.length() > 19 {
    return false
  }
  let mut sum = 0
  let mut double_digit = false
  for index = digits.length() - 1; index >= 0; index = index - 1 {
    let mut digit = digits[index]
    if double_digit {
      digit = digit * 2
      if digit > 9 {
        digit = digit - 9
      }
    }
    sum = sum + digit
    double_digit = !double_digit
  }
  sum % 10 == 0
}

///|
fn privacy_is_phone(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() < 10 || chars.length() > 24 {
    return false
  }
  let mut digits = 0
  let mut formatted = false
  for index = 0; index < chars.length(); index = index + 1 {
    let char = chars[index]
    if profile_is_digit(char) {
      digits = digits + 1
    } else if char == '+' && index == 0 {
      formatted = true
    } else if char == ' ' || char == '-' || char == '(' || char == ')' {
      formatted = true
    } else {
      return false
    }
  }
  formatted && digits >= 10 && digits <= 15
}

///|
fn privacy_is_high_entropy_secret(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() < 28 || chars.length() > 256 {
    return false
  }
  let mut lower = false
  let mut upper = false
  let mut digit = false
  let mut symbol = false
  let mut spaces = false
  for char in chars {
    if char >= 'a' && char <= 'z' {
      lower = true
    } else if char >= 'A' && char <= 'Z' {
      upper = true
    } else if profile_is_digit(char) {
      digit = true
    } else if is_space(char) {
      spaces = true
    } else {
      symbol = true
    }
  }
  let mut classes = 0
  if lower {
    classes = classes + 1
  }
  if upper {
    classes = classes + 1
  }
  if digit {
    classes = classes + 1
  }
  if symbol {
    classes = classes + 1
  }
  !spaces && classes >= 3
}