///|
/// A typed action for a structured resource field.
pub(all) enum ResourceAction {
  Retain
  Redact
  Hash
  Remove
  Review
} derive(Debug, Eq)

///|
pub(all) struct ResourceField {
  path : String
  value : String
  kind : PhiKind
  action : ResourceAction
  required : Bool
  source : String
} derive(Debug, Eq)

///|
pub(all) struct ResourceDocument {
  resource_type : String
  mut resource_id : String
  mut fields : Array[ResourceField]
  mut metadata : Map[String, String]
} derive(Debug)

///|
pub(all) struct ResourcePolicy {
  name : String
  default_action : ResourceAction
  sensitive_paths : Array[String]
  protected_paths : Array[String]
  required_paths : Array[String]
  config : RedactionConfig
} derive(Debug, Eq)

///|
pub(all) struct ResourceIssue {
  path : String
  code : String
  message : String
  severity : DiagnosticSeverity
} derive(Debug, Eq)

///|
pub(all) struct ResourceReport {
  resource_type : String
  resource_id : String
  input_fields : Int
  output_fields : Int
  changed_fields : Int
  removed_fields : Int
  review_fields : Int
  issues : Array[ResourceIssue]
  checksum : String
} derive(Debug)

///|
pub fn resource_action_name(action : ResourceAction) -> String {
  match action {
    Retain => "retain"
    Redact => "redact"
    Hash => "hash"
    Remove => "remove"
    Review => "review"
  }
}

///|
pub fn resource_kind_for_path(path : String) -> PhiKind {
  let value = path.to_lower()
  if value.contains("name") || value.contains("patient") {
    PersonName
  } else if value.contains("phone") || value.contains("telecom") {
    Phone
  } else if value.contains("email") {
    Email
  } else if value.contains("birth") || value.contains("date") {
    Date
  } else if value.contains("address") || value.contains("street") {
    Address
  } else if value.contains("insurance") || value.contains("coverage") {
    Insurance
  } else if value.contains("mrn") || value.contains("medical") {
    MedicalRecord
  } else if value.contains("identifier") || value.has_suffix(".id") {
    IdNumber
  } else if value.contains("hospital") || value.contains("organization") {
    Organization
  } else {
    Custom("structured")
  }
}

///|
pub fn resource_field(
  path : String,
  value : String,
  action : ResourceAction,
) -> ResourceField {
  {
    path,
    value,
    kind: resource_kind_for_path(path),
    action,
    required: false,
    source: "input",
  }
}

///|
pub fn resource_field_with_kind(
  path : String,
  value : String,
  kind : PhiKind,
  action : ResourceAction,
) -> ResourceField {
  { ..resource_field(path, value, action), kind, }
}

///|
pub fn ResourceDocument::new(
  resource_type : String,
  resource_id : String,
  fields : Array[ResourceField],
) -> ResourceDocument {
  { resource_type, resource_id, fields, metadata: Map([]) }
}

///|
pub fn ResourceDocument::empty(resource_type : String) -> ResourceDocument {
  ResourceDocument::new(resource_type, "", [])
}

///|
pub fn ResourceDocument::field_count(self : ResourceDocument) -> Int {
  self.fields.length()
}

///|
pub fn ResourceDocument::has_path(
  self : ResourceDocument,
  path : String,
) -> Bool {
  self.fields.any(fn(field) { field.path == path })
}

///|
pub fn ResourceDocument::get(
  self : ResourceDocument,
  path : String,
) -> ResourceField? {
  let mut result : ResourceField? = None
  for field in self.fields {
    if field.path == path {
      result = Some(field)
    }
  }
  result
}

///|
pub fn ResourceDocument::values(
  self : ResourceDocument,
  path : String,
) -> Array[String] {
  self.fields
  .filter(fn(field) { field.path == path })
  .map(fn(field) { field.value })
}

///|
pub fn ResourceDocument::paths(self : ResourceDocument) -> Array[String] {
  self.fields.map(fn(field) { field.path })
}

///|
pub fn ResourceDocument::add(
  self : ResourceDocument,
  field : ResourceField,
) -> Unit {
  self.fields.push(field)
}

///|
pub fn ResourceDocument::put(
  self : ResourceDocument,
  field : ResourceField,
) -> Unit {
  let mut replaced = false
  for i in 0.. Int {
  let kept = self.fields.filter(fn(field) { field.path != path })
  let removed = self.fields.length() - kept.length()
  self.fields = kept
  removed
}

///|
pub fn ResourceDocument::with_metadata(
  self : ResourceDocument,
  key : String,
  value : String,
) -> Unit {
  self.metadata[key] = value
}

///|
pub fn ResourceDocument::metadata_value(
  self : ResourceDocument,
  key : String,
) -> String? {
  self.metadata.get(key)
}

///|
pub fn ResourceDocument::copy(self : ResourceDocument) -> ResourceDocument {
  {
    resource_type: self.resource_type,
    resource_id: self.resource_id,
    fields: self.fields.copy(),
    metadata: self.metadata.copy(),
  }
}

///|
pub fn ResourcePolicy::default() -> ResourcePolicy {
  {
    name: "clinical-resource",
    default_action: Retain,
    sensitive_paths: [
      "Patient.name", "Patient.identifier", "Patient.birthDate", "Patient.address",
      "Patient.telecom", "Observation.note", "Encounter.subject", "Coverage.subscriber",
    ],
    protected_paths: [],
    required_paths: [],
    config: RedactionConfig::default(),
  }
}

///|
pub fn ResourcePolicy::strict() -> ResourcePolicy {
  {
    ..ResourcePolicy::default(),
    name: "strict-resource",
    default_action: Redact,
    config: { ..RedactionConfig::default(), policy: RedactionPolicy::strict() },
  }
}

///|
pub fn resource_policy_with_paths(
  policy : ResourcePolicy,
  paths : Array[String],
) -> ResourcePolicy {
  { ..policy, sensitive_paths: paths }
}

///|
pub fn resource_policy_protect(
  policy : ResourcePolicy,
  paths : Array[String],
) -> ResourcePolicy {
  let guarded = policy.protected_paths.copy()
  guarded.append(paths)
  { ..policy, protected_paths: guarded }
}

///|
pub fn resource_policy_require(
  policy : ResourcePolicy,
  paths : Array[String],
) -> ResourcePolicy {
  let required = policy.required_paths.copy()
  required.append(paths)
  { ..policy, required_paths: required }
}

///|
pub fn resource_policy_action(
  policy : ResourcePolicy,
  path : String,
) -> ResourceAction {
  if policy.protected_paths.contains(path) {
    Retain
  } else if policy.sensitive_paths.contains(path) {
    Redact
  } else {
    policy.default_action
  }
}

///|
pub fn resource_path_matches(pattern : String, path : String) -> Bool {
  if pattern == "*" {
    true
  } else if pattern.has_suffix(".*") {
    path.has_prefix(pattern[:pattern.length() - 1])
  } else if pattern.has_prefix("*.") {
    path.has_suffix(pattern[1:])
  } else {
    pattern == path
  }
}

///|
pub fn resource_any_path_matches(
  patterns : Array[String],
  path : String,
) -> Bool {
  patterns.any(fn(pattern) { resource_path_matches(pattern, path) })
}

///|
pub fn resource_effective_action(
  policy : ResourcePolicy,
  field : ResourceField,
) -> ResourceAction {
  if resource_any_path_matches(policy.protected_paths, field.path) {
    Retain
  } else if resource_any_path_matches(policy.sensitive_paths, field.path) {
    Redact
  } else if field.action != Retain {
    field.action
  } else {
    policy.default_action
  }
}

///|
pub fn resource_apply_action(
  field : ResourceField,
  action : ResourceAction,
  config : RedactionConfig,
) -> ResourceField raise DeidError {
  match action {
    Retain => { ..field, action: Retain }
    Redact => {
      let result = redact_with_config(field.value, config)
      { ..field, value: result.text, action: Redact }
    }
    Hash => {
      let replacement = "[\{phi_kind_name(field.kind)}]#\{stable_hash(field.value)}"
      { ..field, value: replacement, action: Hash }
    }
    Remove => { ..field, value: "", action: Remove }
    Review => { ..field, action: Review }
  }
}

///|
pub fn resource_redact(
  document : ResourceDocument,
  policy : ResourcePolicy,
) -> ResourceDocument raise DeidError {
  let result = document.copy()
  let fields = document.fields.copy()
  result.fields = []
  for field in fields {
    let action = resource_effective_action(policy, field)
    result.fields.push(resource_apply_action(field, action, policy.config))
  }
  result
}

///|
pub fn resource_changed_paths(
  before : ResourceDocument,
  after : ResourceDocument,
) -> Array[String] {
  let paths = []
  for field in before.fields {
    match after.get(field.path) {
      Some(other) => if other.value != field.value { paths.push(field.path) }
      None => paths.push(field.path)
    }
  }
  paths
}

///|
pub fn resource_removed_paths(
  before : ResourceDocument,
  after : ResourceDocument,
) -> Array[String] {
  before.paths().filter(fn(path) { !after.has_path(path) })
}

///|
pub fn resource_review_paths(document : ResourceDocument) -> Array[String] {
  document.fields
  .filter(fn(field) { field.action == Review })
  .map(fn(field) { field.path })
}

///|
pub fn resource_validate(
  document : ResourceDocument,
  policy : ResourcePolicy,
) -> Array[ResourceIssue] {
  let issues = []
  if document.resource_type.trim().is_empty() {
    issues.push({
      path: "",
      code: "RESOURCE_TYPE",
      message: "resource type is empty",
      severity: Error,
    })
  }
  for path in policy.required_paths {
    match document.get(path) {
      Some(field) if !field.value.trim().is_empty() => ()
      _ =>
        issues.push({
          path,
          code: "REQUIRED_FIELD",
          message: "required field is missing",
          severity: Error,
        })
    }
  }
  for field in document.fields {
    if field.path.trim().is_empty() {
      issues.push({
        path: field.path,
        code: "EMPTY_PATH",
        message: "field path is empty",
        severity: Error,
      })
    }
    if field.value.length() > 1000000 {
      issues.push({
        path: field.path,
        code: "FIELD_TOO_LARGE",
        message: "field exceeds one million code units",
        severity: Warning,
      })
    }
  }
  issues
}

///|
pub fn resource_report(
  before : ResourceDocument,
  after : ResourceDocument,
  policy : ResourcePolicy,
) -> ResourceReport {
  let issues = resource_validate(after, policy)
  let changed = resource_changed_paths(before, after)
  let removed = resource_removed_paths(before, after)
  {
    resource_type: after.resource_type,
    resource_id: after.resource_id,
    input_fields: before.field_count(),
    output_fields: after.field_count(),
    changed_fields: changed.length(),
    removed_fields: removed.length(),
    review_fields: resource_review_paths(after).length(),
    issues,
    checksum: stable_hash(resource_render(after)),
  }
}

///|
pub fn resource_render(document : ResourceDocument) -> String {
  let lines = [
    "resource_type=\{document.resource_type}",
    "resource_id=\{document.resource_id}",
  ]
  for key, value in document.metadata {
    lines.push("meta.\{key}=\{value}")
  }
  for field in document.fields {
    lines.push("\{field.path}=\{field.value}")
  }
  lines.join("\n")
}

///|
pub fn resource_parse(text : String) -> ResourceDocument {
  let fields = []
  let mut resource_type = ""
  let mut resource_id = ""
  let metadata : Map[String, String] = Map([])
  for field in parse_fields(text) {
    if field.key == "resource_type" {
      resource_type = field.value
    } else if field.key == "resource_id" {
      resource_id = field.value
    } else if field.key.has_prefix("meta.") {
      metadata[field.key[5:].to_owned()] = field.value
    } else {
      fields.push(resource_field(field.key, field.value, Retain))
    }
  }
  { resource_type, resource_id, fields, metadata }
}

///|
pub fn resource_from_map(
  resource_type : String,
  resource_id : String,
  values : Map[String, String],
) -> ResourceDocument {
  let document = ResourceDocument::empty(resource_type)
  document.resource_id = resource_id
  for path, value in values {
    document.fields.push(resource_field(path, value, Retain))
  }
  document
}

///|
pub fn resource_to_map(document : ResourceDocument) -> Map[String, String] {
  let result : Map[String, String] = Map([])
  for field in document.fields {
    result[field.path] = field.value
  }
  result
}

///|
pub fn resource_report_text(report : ResourceReport) -> String {
  [
    "resource_type=\{report.resource_type}",
    "resource_id=\{report.resource_id}",
    "input_fields=\{report.input_fields}",
    "output_fields=\{report.output_fields}",
    "changed_fields=\{report.changed_fields}",
    "removed_fields=\{report.removed_fields}",
    "review_fields=\{report.review_fields}",
    "issues=\{report.issues.length()}",
    "checksum=\{report.checksum}",
  ].join("\n")
}

///|
pub fn resource_issue_text(issue : ResourceIssue) -> String {
  [
    "\{severity_name(issue.severity)}",
    "\{issue.code}",
    "\{issue.path}",
    issue.message,
  ].join(" ")
}

///|
pub fn resource_issues_text(issues : Array[ResourceIssue]) -> String {
  issues.map(resource_issue_text).join("\n")
}

///|
pub fn resource_report_json(report : ResourceReport) -> String {
  let issues = report.issues
    .map(fn(issue) {
      "{" +
      "\"path\":\{json_escape(issue.path)}," +
      "\"code\":\{json_escape(issue.code)}," +
      "\"severity\":\{json_escape(severity_name(issue.severity))}," +
      "\"message\":\{json_escape(issue.message)}" +
      "}"
    })
    .join(",")
  "{" +
  "\"resource_type\":\{json_escape(report.resource_type)}," +
  "\"resource_id\":\{json_escape(report.resource_id)}," +
  "\"input_fields\":\{report.input_fields}," +
  "\"output_fields\":\{report.output_fields}," +
  "\"changed_fields\":\{report.changed_fields}," +
  "\"removed_fields\":\{report.removed_fields}," +
  "\"review_fields\":\{report.review_fields}," +
  "\"checksum\":\{json_escape(report.checksum)}," +
  "\"issues\":[" +
  issues +
  "]}"
}

///|
pub fn resource_is_safe(
  before : ResourceDocument,
  after : ResourceDocument,
  policy : ResourcePolicy,
) -> Bool {
  let report = resource_report(before, after, policy)
  !report.issues.any(fn(issue) { issue.severity == Error }) &&
  resource_removed_paths(before, after).all(fn(path) {
    resource_any_path_matches(policy.sensitive_paths, path)
  })
}

///|
pub fn resource_non_sensitive_values_preserved(
  before : ResourceDocument,
  after : ResourceDocument,
  policy : ResourcePolicy,
) -> Bool {
  let mut safe = true
  for field in before.fields {
    if resource_effective_action(policy, field) == Retain {
      match after.get(field.path) {
        Some(other) => if other.value != field.value { safe = false }
        None => safe = false
      }
    }
  }
  safe
}

///|
pub fn resource_sensitive_path_count(
  document : ResourceDocument,
  policy : ResourcePolicy,
) -> Int {
  document.fields
  .filter(fn(field) {
    resource_any_path_matches(policy.sensitive_paths, field.path)
  })
  .length()
}

///|
pub fn resource_action_counts(document : ResourceDocument) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for field in document.fields {
    let key = resource_action_name(field.action)
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn resource_kind_counts(document : ResourceDocument) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for field in document.fields {
    let key = phi_kind_name(field.kind)
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn resource_paths_by_kind(
  document : ResourceDocument,
  kind : PhiKind,
) -> Array[String] {
  document.fields
  .filter(fn(field) { field.kind == kind })
  .map(fn(field) { field.path })
}

///|
pub fn resource_empty_values(document : ResourceDocument) -> Array[String] {
  document.fields
  .filter(fn(field) { field.value.trim().is_empty() })
  .map(fn(field) { field.path })
}

///|
pub fn resource_duplicate_paths(document : ResourceDocument) -> Array[String] {
  let counts : Map[String, Int] = Map([])
  for field in document.fields {
    counts[field.path] = counts.get_or_default(field.path, 0) + 1
  }
  let result = []
  for path, count in counts {
    if count > 1 {
      result.push(path)
    }
  }
  result.sort()
  result
}

///|
pub fn resource_deduplicate(document : ResourceDocument) -> ResourceDocument {
  let result = document.copy()
  let seen : Map[String, Unit] = Map([])
  result.fields = result.fields.filter(fn(field) {
    if seen.contains(field.path) {
      false
    } else {
      seen[field.path] = ()
      true
    }
  })
  result
}

///|
pub fn resource_prefix_paths(
  document : ResourceDocument,
  prefix : String,
) -> ResourceDocument {
  let result = document.copy()
  result.fields = result.fields.map(fn(field) {
    { ..field, path: prefix + field.path }
  })
  result
}

///|
pub fn resource_select_paths(
  document : ResourceDocument,
  paths : Array[String],
) -> ResourceDocument {
  let result = document.copy()
  result.fields = result.fields.filter(fn(field) {
    resource_any_path_matches(paths, field.path)
  })
  result
}

///|
pub fn resource_exclude_paths(
  document : ResourceDocument,
  paths : Array[String],
) -> ResourceDocument {
  let result = document.copy()
  result.fields = result.fields.filter(fn(field) {
    !resource_any_path_matches(paths, field.path)
  })
  result
}