///|
/// Sensitivity class used by safe message diagnostics.
pub(all) enum FieldSensitivity {
  PublicField
  MaskedAccountData
  RedactedTrackData
  RedactedSecret
  SummarizedChipData
} derive(Eq, Debug)

///|
/// One field in a diagnostic view that never contains raw secrets.
pub(all) struct SafeFieldDiagnostic {
  field : Int
  name : String
  logical_length : Int
  sensitivity : FieldSensitivity
  display : String
} derive(Eq, Debug)

///|
/// Classify common payment data elements for safe logging.
pub fn field_sensitivity(field : Int) -> FieldSensitivity {
  match field {
    2 => MaskedAccountData
    35 | 45 => RedactedTrackData
    52 | 53 | 64 | 96 | 128 => RedactedSecret
    55 => SummarizedChipData
    _ => PublicField
  }
}

///|
/// Mask a PAN without trusting that the input has already been validated.
fn safe_pan_mask(value : String) -> String {
  if value.length() < 12 || value.length() > 19 {
    return "[REDACTED PAN len=\{value.length()}]"
  }
  for i = 0; i < value.length(); i = i + 1 {
    if !ascii_digit(value[i]) {
      return "[REDACTED PAN len=\{value.length()}]"
    }
  }
  mask_pan(value)
}

///|
/// Produce a Track 2 summary without expiry, service code, or discretionary data.
fn safe_track2_summary(value : String) -> String {
  match parse_track2(value) {
    Ok(track) =>
      "\{safe_pan_mask(track.pan)}="
    Err(_) => "[REDACTED TRACK2 len=\{value.length()}]"
  }
}

///|
/// Collect BER-TLV tags without retaining any values.
fn collect_tlv_tags(nodes : Array[BerTlv], tags : Array[String]) -> Unit {
  for node in nodes {
    tags.push(node.tag)
    if node.constructed {
      collect_tlv_tags(node.children, tags)
    }
  }
}

///|
/// Join text without relying on a platform-specific collection formatter.
fn join_diagnostic_text(values : Array[String], separator : String) -> String {
  let output = StringBuilder()
  for i = 0; i < values.length(); i = i + 1 {
    if i > 0 {
      output.write_string(separator)
    }
    output.write_string(values[i])
  }
  output.to_string()
}

///|
/// Summarize DE55 by encoded size and tags only.
fn safe_de55_summary(value : String) -> String {
  match parse_de55(value) {
    Err(_) => "[REDACTED DE55 invalid-encoding hex-len=\{value.length()}]"
    Ok(nodes) => {
      let tags : Array[String] = []
      collect_tlv_tags(nodes, tags)
      "DE55 bytes=\{value.length() / 2} tags=[\{join_diagnostic_text(tags, ",")}]"
    }
  }
}

///|
/// Render one field value according to its sensitivity class.
pub fn safe_field_display(field : Int, value : String) -> String {
  match field_sensitivity(field) {
    PublicField => value
    MaskedAccountData => safe_pan_mask(value)
    RedactedTrackData =>
      if field == 35 {
        safe_track2_summary(value)
      } else {
        "[REDACTED TRACK1 len=\{value.length()}]"
      }
    RedactedSecret =>
      match field {
        52 => "[REDACTED PIN DATA bytes=\{value.length() / 2}]"
        53 => "[REDACTED SECURITY CONTROL len=\{value.length()}]"
        64 | 128 => "[REDACTED MAC bytes=\{value.length() / 2}]"
        96 => "[REDACTED KEY MANAGEMENT bytes=\{value.length() / 2}]"
        _ => "[REDACTED len=\{value.length()}]"
      }
    SummarizedChipData => safe_de55_summary(value)
  }
}

///|
/// Build structured, log-safe diagnostics in field order.
pub fn safe_field_diagnostics(
  message : IsoMessage,
  packager : Packager,
) -> Array[SafeFieldDiagnostic] {
  let output : Array[SafeFieldDiagnostic] = []
  for entry in message.fields {
    let name = match packager.spec(entry.number) {
      Some(spec) => spec.name
      None => "unknown field"
    }
    output.push({
      field: entry.number,
      name,
      logical_length: entry.value.length(),
      sensitivity: field_sensitivity(entry.number),
      display: safe_field_display(entry.number, entry.value),
    })
  }
  output
}

///|
/// Render a deterministic multi-line message dump suitable for application logs.
pub fn safe_message_dump(message : IsoMessage, packager : Packager) -> String {
  let output = StringBuilder()
  output.write_string("MTI \{message.mti} fields=\{message.field_count()}\n")
  for item in safe_field_diagnostics(message, packager) {
    output.write_string(
      "DE\{item.field} \{item.name} len=\{item.logical_length} value=\{item.display}\n",
    )
  }
  output.to_string()
}

///|
/// Explain the wire layout alongside safe field displays.
pub fn safe_layout_dump(
  message : IsoMessage,
  packager : Packager,
  profile : WireProfile,
) -> Result[String, IsoError] {
  let layout = match message_layout(packager, profile, message) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let output = StringBuilder()
  for line in layout {
    output.write_string(line)
    output.write_char('\n')
  }
  output.write_string("-- safe values --\n")
  output.write_string(safe_message_dump(message, packager))
  Ok(output.to_string())
}

///|
/// Return only field numbers whose values require masking or redaction.
pub fn sensitive_fields(message : IsoMessage) -> Array[Int] {
  let fields : Array[Int] = []
  for entry in message.fields {
    if field_sensitivity(entry.number) != PublicField {
      fields.push(entry.number)
    }
  }
  fields
}

///|
/// Defensive check used by tests and host applications before emitting a dump.
pub fn diagnostic_contains_raw_value(
  diagnostic : String,
  field : Int,
  raw_value : String,
) -> Bool {
  if field_sensitivity(field) == PublicField || raw_value.length() == 0 {
    return false
  }
  diagnostic.contains(raw_value)
}