///|
/// HL7 v2 field-level action used by the message sanitizer.
pub(all) enum Hl7Action {
  Retain
  Redact
  Replace(String)
  Remove
} derive(Debug, Eq)

///|
pub(all) struct Hl7Field {
  segment_name : String
  segment_index : Int
  field_number : Int
  value : String
} derive(Debug, Eq)

///|
pub(all) struct Hl7Segment {
  name : String
  fields : Array[String]
  index : Int
} derive(Debug, Eq)

///|
pub(all) struct Hl7Message {
  field_separator : Char
  repetition_separator : Char
  component_separator : Char
  escape_character : Char
  subcomponent_separator : Char
  version : String
  segments : Array[Hl7Segment]
} derive(Debug)

///|
pub(all) struct Hl7FieldRule {
  segment_name : String
  field_number : Int
  component_number : Int
  label : String
  action : Hl7Action
  required : Bool
} derive(Debug, Eq)

///|
pub(all) struct Hl7ValidationIssue {
  code : String
  message : String
  segment_index : Int
  field_number : Int
  severity : DiagnosticSeverity
} derive(Debug, Eq)

///|
pub fn hl7_action_name(action : Hl7Action) -> String {
  match action {
    Retain => "retain"
    Redact => "redact"
    Replace(_) => "replace"
    Remove => "remove"
  }
}

///|
pub fn hl7_field_rule(
  segment_name : String,
  field_number : Int,
  label : String,
  action : Hl7Action,
) -> Hl7FieldRule {
  {
    segment_name: segment_name.to_upper(),
    field_number,
    component_number: 0,
    label,
    action,
    required: false,
  }
}

///|
pub fn hl7_field_rule_component(
  segment_name : String,
  field_number : Int,
  component_number : Int,
  label : String,
  action : Hl7Action,
) -> Hl7FieldRule {
  {
    segment_name: segment_name.to_upper(),
    field_number,
    component_number,
    label,
    action,
    required: false,
  }
}

///|
pub fn hl7_default_rules() -> Array[Hl7FieldRule] {
  [
    { ..hl7_field_rule("PID", 3, "patient identifier", Redact), required: true },
    { ..hl7_field_rule("PID", 5, "patient name", Redact), required: true },
    { ..hl7_field_rule("PID", 7, "date of birth", Redact), required: false },
    { ..hl7_field_rule("PID", 11, "patient address", Redact), required: false },
    { ..hl7_field_rule("PID", 13, "patient phone", Redact), required: false },
    { ..hl7_field_rule("PID", 14, "alternate phone", Redact), required: false },
    { ..hl7_field_rule("PID", 18, "account number", Redact), required: false },
    {
      ..hl7_field_rule("PV1", 7, "attending provider", Redact),
      required: false,
    },
    {
      ..hl7_field_rule("PV1", 8, "referring provider", Redact),
      required: false,
    },
    { ..hl7_field_rule("PV1", 19, "visit number", Redact), required: false },
    {
      ..hl7_field_rule("ORC", 2, "placer order number", Redact),
      required: false,
    },
    {
      ..hl7_field_rule("ORC", 3, "filler order number", Redact),
      required: false,
    },
    {
      ..hl7_field_rule("OBR", 2, "placer order number", Redact),
      required: false,
    },
    {
      ..hl7_field_rule("OBR", 3, "filler order number", Redact),
      required: false,
    },
    { ..hl7_field_rule("OBX", 5, "observation value", Redact), required: false },
    { ..hl7_field_rule("NTE", 3, "clinical note", Redact), required: false },
  ]
}

///|
fn hl7_trim_line(line : String) -> String {
  line.trim(chars="\r\n").to_owned()
}

///|
pub fn hl7_segment_lines(text : String) -> Array[String] {
  let normalized = text.replace(old="\r\n", new="\r")
  let first = normalized.split("\r").to_array()
  let lines = []
  for item in first {
    let second = item.to_owned().split("\n").to_array()
    for line in second {
      let clean = hl7_trim_line(line.to_owned())
      if !clean.is_empty() {
        lines.push(clean)
      }
    }
  }
  lines
}

///|
pub fn hl7_segment_name(line : String) -> String {
  if line.length() < 3 {
    ""
  } else {
    line[:3].to_upper().to_owned()
  }
}

///|
pub fn hl7_parse_segment(line : String, index : Int) -> Hl7Segment? {
  let clean = hl7_trim_line(line)
  let name = hl7_segment_name(clean)
  if name.length() != 3 {
    None
  } else {
    let parts = clean.split("|").to_array()
    let fields = []
    for i in 1.. Array[Hl7Segment] {
  let segments = []
  for i in 0.. segments.push(segment)
      None => ()
    }
  }
  segments
}

///|
pub fn hl7_message_type(segments : Array[Hl7Segment]) -> String {
  match segments.get(0) {
    Some(segment) if segment.name == "MSH" =>
      if segment.fields.length() >= 8 {
        segment.fields[7]
      } else {
        ""
      }
    _ => ""
  }
}

///|
pub fn hl7_version(segments : Array[Hl7Segment]) -> String {
  match segments.get(0) {
    Some(segment) if segment.name == "MSH" =>
      if segment.fields.length() >= 11 {
        segment.fields[10]
      } else {
        ""
      }
    _ => ""
  }
}

///|
pub fn hl7_parse(text : String) -> Hl7Message {
  let lines = hl7_segment_lines(text)
  let segments = hl7_parse_segments(lines)
  let field_separator = '|'
  let repetition_separator = '~'
  let component_separator = '^'
  let escape_character = '\\'
  let subcomponent_separator = '&'
  let version = hl7_version(segments)
  {
    field_separator,
    repetition_separator,
    component_separator,
    escape_character,
    subcomponent_separator,
    version,
    segments,
  }
}

///|
pub fn Hl7Message::segment_count(self : Hl7Message) -> Int {
  self.segments.length()
}

///|
pub fn Hl7Message::segment_names(self : Hl7Message) -> Array[String] {
  self.segments.map(fn(segment) { segment.name })
}

///|
pub fn Hl7Message::has_segment(self : Hl7Message, name : String) -> Bool {
  self.segments.any(fn(segment) { segment.name == name.to_upper() })
}

///|
pub fn Hl7Message::segments_named(
  self : Hl7Message,
  name : String,
) -> Array[Hl7Segment] {
  self.segments.filter(fn(segment) { segment.name == name.to_upper() })
}

///|
pub fn Hl7Message::field(
  self : Hl7Message,
  segment_name : String,
  segment_number : Int,
  field_number : Int,
) -> Hl7Field? {
  let selected = self.segments.filter(fn(segment) {
    segment.name == segment_name.to_upper()
  })
  match selected.get(segment_number) {
    Some(segment) =>
      if field_number > 0 && field_number <= segment.fields.length() {
        Some({
          segment_name: segment.name,
          segment_index: segment.index,
          field_number,
          value: segment.fields[field_number - 1],
        })
      } else {
        None
      }
    None => None
  }
}

///|
pub fn Hl7Message::fields_named(
  self : Hl7Message,
  segment_name : String,
  field_number : Int,
) -> Array[Hl7Field] {
  let result = []
  let mut ordinal = 0
  for segment in self.segments {
    if segment.name == segment_name.to_upper() {
      if field_number > 0 && field_number <= segment.fields.length() {
        result.push({
          segment_name: segment.name,
          segment_index: segment.index,
          field_number,
          value: segment.fields[field_number - 1],
        })
      }
      ordinal += 1
    }
  }
  ignore(ordinal)
  result
}

///|
pub fn Hl7Message::all_fields(self : Hl7Message) -> Array[Hl7Field] {
  let result = []
  for segment in self.segments {
    for i in 0.. String {
  segment_name.to_upper() + "-" + "\{field_number}"
}

///|
pub fn hl7_field_path(field : Hl7Field) -> String {
  hl7_path(field.segment_name, field.field_number)
}

///|
pub fn hl7_component_values(value : String, separator : Char) -> Array[String] {
  value
  .split(separator.to_string())
  .to_array()
  .map(fn(item) { item.to_owned() })
}

///|
pub fn hl7_repeat_values(value : String, separator : Char) -> Array[String] {
  hl7_component_values(value, separator)
}

///|
pub fn hl7_subcomponent_values(
  value : String,
  separator : Char,
) -> Array[String] {
  hl7_component_values(value, separator)
}

///|
pub fn hl7_field_components(
  message : Hl7Message,
  field : Hl7Field,
) -> Array[String] {
  hl7_component_values(field.value, message.component_separator)
}

///|
pub fn hl7_render_segment(segment : Hl7Segment, separator : Char) -> String {
  segment.name +
  separator.to_string() +
  segment.fields.join(separator.to_string())
}

///|
pub fn Hl7Message::render(self : Hl7Message) -> String {
  self.segments
  .map(fn(segment) { hl7_render_segment(segment, self.field_separator) })
  .join("\r")
}

///|
pub fn Hl7Message::render_with_newline(
  self : Hl7Message,
  newline : String,
) -> String {
  self.segments
  .map(fn(segment) { hl7_render_segment(segment, self.field_separator) })
  .join(newline)
}

///|
pub fn hl7_escape(value : String, message : Hl7Message) -> String {
  let builder = StringBuilder()
  for c in value {
    match c {
      '|' => builder.write_string("\\F")
      '^' => builder.write_string("\\S")
      '~' => builder.write_string("\\R")
      '&' => builder.write_string("\\T")
      '\\' => builder.write_string("\\E")
      '\r' => builder.write_string("\\.br\\")
      '\n' => builder.write_string("\\.br\\")
      _ => builder.write_char(c)
    }
  }
  ignore(message)
  builder.to_string()
}

///|
pub fn hl7_unescape(value : String) -> String {
  value
  .replace(old="\\F", new="|")
  .replace(old="\\S", new="^")
  .replace(old="\\R", new="~")
  .replace(old="\\T", new="&")
  .replace(old="\\E", new="\\")
  .replace(old="\\.br\\", new="\n")
}

///|
fn hl7_rule_matches(rule : Hl7FieldRule, field : Hl7Field) -> Bool {
  rule.segment_name == field.segment_name &&
  rule.field_number == field.field_number
}

///|
pub fn hl7_matching_rule(
  rules : Array[Hl7FieldRule],
  field : Hl7Field,
) -> Hl7FieldRule? {
  let mut result : Hl7FieldRule? = None
  for rule in rules {
    if hl7_rule_matches(rule, field) {
      result = Some(rule)
    }
  }
  result
}

///|
pub fn hl7_rule_paths(rules : Array[Hl7FieldRule]) -> Array[String] {
  rules.map(fn(rule) { hl7_path(rule.segment_name, rule.field_number) })
}

///|
pub fn hl7_required_paths(rules : Array[Hl7FieldRule]) -> Array[String] {
  rules
  .filter(fn(rule) { rule.required })
  .map(fn(rule) { hl7_path(rule.segment_name, rule.field_number) })
}

///|
pub fn hl7_set_field(
  message : Hl7Message,
  segment_index : Int,
  field_number : Int,
  value : String,
) -> Hl7Message {
  let segments = message.segments.copy()
  match segments.get(segment_index) {
    Some(segment) if field_number > 0 => {
      let fields = segment.fields.copy()
      while fields.length() < field_number {
        fields.push("")
      }
      fields[field_number - 1] = value
      segments[segment_index] = { ..segment, fields, }
    }
    _ => ()
  }
  { ..message, segments, }
}

///|
pub fn hl7_remove_field(
  message : Hl7Message,
  segment_index : Int,
  field_number : Int,
) -> Hl7Message {
  hl7_set_field(message, segment_index, field_number, "")
}

///|
pub fn hl7_apply_action(
  value : String,
  action : Hl7Action,
  config : RedactionConfig,
) -> String raise DeidError {
  match action {
    Retain => value
    Redact => redact_with_config(value, config).text
    Replace(replacement) => replacement
    Remove => ""
  }
}

///|
pub fn hl7_redact(
  text : String,
  config : RedactionConfig,
  rules : Array[Hl7FieldRule],
) -> String raise DeidError {
  let message = hl7_parse(text)
  let mut result = message
  for field in message.all_fields() {
    match hl7_matching_rule(rules, field) {
      Some(rule) => {
        let value = hl7_apply_action(field.value, rule.action, config)
        result = hl7_set_field(
          result,
          field.segment_index,
          field.field_number,
          value,
        )
      }
      None => ()
    }
  }
  result.render()
}

///|
pub fn hl7_redact_default(
  text : String,
  config : RedactionConfig,
) -> String raise DeidError {
  hl7_redact(text, config, hl7_default_rules())
}

///|
pub fn hl7_sensitive_fields(
  message : Hl7Message,
  rules : Array[Hl7FieldRule],
) -> Array[Hl7Field] {
  message
  .all_fields()
  .filter(fn(field) { hl7_matching_rule(rules, field) is Some(_) })
}

///|
pub fn hl7_redaction_paths(
  message : Hl7Message,
  rules : Array[Hl7FieldRule],
) -> Array[String] {
  hl7_sensitive_fields(message, rules).map(hl7_field_path)
}

///|
pub fn hl7_message_checksum(message : Hl7Message) -> String {
  stable_hash(message.render())
}

///|
pub fn hl7_validate(message : Hl7Message) -> Array[Hl7ValidationIssue] {
  let issues = []
  if message.segments.is_empty() {
    issues.push({
      code: "HL7_EMPTY",
      message: "message has no segments",
      segment_index: -1,
      field_number: 0,
      severity: Error,
    })
  } else if message.segments[0].name != "MSH" {
    issues.push({
      code: "HL7_NO_MSH",
      message: "first segment must be MSH",
      segment_index: 0,
      field_number: 0,
      severity: Error,
    })
  }
  for segment in message.segments {
    if segment.name.length() != 3 {
      issues.push({
        code: "HL7_SEGMENT_NAME",
        message: "segment name must contain three characters",
        segment_index: segment.index,
        field_number: 0,
        severity: Error,
      })
    }
    for value in segment.fields {
      if value.contains("\r") || value.contains("\n") {
        issues.push({
          code: "HL7_FIELD_BREAK",
          message: "field contains a line break",
          segment_index: segment.index,
          field_number: 0,
          severity: Warning,
        })
      }
    }
  }
  issues
}

///|
pub fn hl7_validate_required(
  message : Hl7Message,
  rules : Array[Hl7FieldRule],
) -> Array[Hl7ValidationIssue] {
  let issues = hl7_validate(message)
  for rule in rules.filter(fn(item) { item.required }) {
    let values = message.fields_named(rule.segment_name, rule.field_number)
    if values.is_empty() ||
      values.all(fn(field) { field.value.trim().is_empty() }) {
      issues.push({
        code: "HL7_REQUIRED",
        message: "required field is missing: " +
        hl7_path(rule.segment_name, rule.field_number),
        segment_index: -1,
        field_number: rule.field_number,
        severity: Error,
      })
    }
  }
  issues
}

///|
pub fn hl7_is_valid(message : Hl7Message) -> Bool {
  !hl7_validate(message).any(fn(issue) { issue.severity == Error })
}

///|
pub fn hl7_issue_text(issue : Hl7ValidationIssue) -> String {
  [
    "\{issue.code}",
    "\{severity_name(issue.severity)}",
    "segment=\{issue.segment_index}",
    "field=\{issue.field_number}",
    issue.message,
  ].join(" ")
}

///|
pub fn hl7_issues_text(issues : Array[Hl7ValidationIssue]) -> String {
  issues.map(hl7_issue_text).join("\n")
}

///|
pub fn hl7_message_summary(message : Hl7Message) -> String {
  [
    "segments=\{message.segment_count()}",
    "message_type=\{hl7_message_type(message.segments)}",
    "version=\{message.version}",
    "checksum=\{hl7_message_checksum(message)}",
    "valid=\{hl7_is_valid(message)}",
  ].join("\n")
}

///|
pub fn hl7_field_summary(field : Hl7Field) -> String {
  [
    "path=\{hl7_field_path(field)}",
    "segment_index=\{field.segment_index}",
    "value_length=\{field.value.length()}",
  ].join("\n")
}

///|
pub fn hl7_fields_json(fields : Array[Hl7Field]) -> String {
  "[" +
  fields
  .map(fn(field) {
    "{" +
    "\"path\":\{json_escape(hl7_field_path(field))}," +
    "\"segment_index\":\{field.segment_index}," +
    "\"field_number\":\{field.field_number}," +
    "\"value_length\":\{field.value.length()}" +
    "}"
  })
  .join(",") +
  "]"
}

///|
pub fn hl7_redaction_report(
  input : String,
  output : String,
  rules : Array[Hl7FieldRule],
) -> String {
  let before = hl7_parse(input)
  let after = hl7_parse(output)
  let paths = hl7_rule_paths(rules).join(",")
  [
    "input_checksum=\{hl7_message_checksum(before)}",
    "output_checksum=\{hl7_message_checksum(after)}",
    "sensitive_paths=\{paths}",
    "input_segments=\{before.segment_count()}",
    "output_segments=\{after.segment_count()}",
    "output_valid=\{hl7_is_valid(after)}",
  ].join("\n")
}

///|
pub fn hl7_preserves_segment_order(
  left : Hl7Message,
  right : Hl7Message,
) -> Bool {
  left.segment_names() == right.segment_names()
}

///|
pub fn hl7_preserves_non_sensitive_fields(
  before : Hl7Message,
  after : Hl7Message,
  rules : Array[Hl7FieldRule],
) -> Bool {
  let mut result = true
  for field in before.all_fields() {
    if hl7_matching_rule(rules, field) is None {
      match after.field(field.segment_name, 0, field.field_number) {
        Some(other) => if other.value != field.value { result = false }
        None => result = false
      }
    }
  }
  result
}

///|
pub fn hl7_message_without_notes(message : Hl7Message) -> Hl7Message {
  let segments = message.segments.filter(fn(segment) { segment.name != "NTE" })
  { ..message, segments, }
}

///|
pub fn hl7_segment_count_by_name(message : Hl7Message) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for segment in message.segments {
    counts[segment.name] = counts.get_or_default(segment.name, 0) + 1
  }
  counts
}

///|
pub fn hl7_field_lengths(message : Hl7Message) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for field in message.all_fields() {
    result[hl7_field_path(field)] = field.value.length()
  }
  result
}

///|
pub fn hl7_empty_field_paths(message : Hl7Message) -> Array[String] {
  message
  .all_fields()
  .filter(fn(field) { field.value.is_empty() })
  .map(hl7_field_path)
}

///|
pub fn hl7_nonempty_field_count(message : Hl7Message) -> Int {
  message.all_fields().filter(fn(field) { !field.value.is_empty() }).length()
}

///|
pub fn hl7_repeated_segment_names(message : Hl7Message) -> Array[String] {
  let counts = hl7_segment_count_by_name(message)
  let names = []
  for name, count in counts {
    if count > 1 {
      names.push(name)
    }
  }
  names.sort()
  names
}

///|
pub fn hl7_normalize_newlines(text : String) -> String {
  hl7_parse(text).render()
}

///|
pub fn hl7_safe_for_storage(
  text : String,
  config : RedactionConfig,
) -> Bool raise DeidError {
  let output = hl7_redact_default(text, config)
  let message = hl7_parse(output)
  let mut leakage = false
  for field in hl7_sensitive_fields(message, hl7_default_rules()) {
    let findings = scan(field.value) catch { _ => [] }
    if !findings.is_empty() {
      leakage = true
    }
  }
  hl7_is_valid(message) && !leakage
}