///|
/// Data minimization controls which structured fields are needed for a
/// downstream purpose before de-identification is applied.
pub(all) enum MinimizationAction {
  KeepRequired
  KeepOptional
  RemoveField
  RedactField
  AggregateField
} derive(Debug, Eq)

///|
pub(all) struct MinimizationRule {
  path_pattern : String
  purpose : String
  action : MinimizationAction
  justification : String
  priority : Int
} derive(Debug, Eq)

///|
pub(all) struct MinimizationPolicy {
  purpose : String
  rules : Array[MinimizationRule]
  default_action : MinimizationAction
  config : RedactionConfig
} derive(Debug, Eq)

///|
pub(all) struct MinimizationResult {
  original : ResourceDocument
  minimized : ResourceDocument
  removed_paths : Array[String]
  redacted_paths : Array[String]
  aggregated_paths : Array[String]
  issues : Array[String]
  checksum : String
} derive(Debug)

///|
pub fn minimization_action_name(action : MinimizationAction) -> String {
  match action {
    KeepRequired => "keep_required"
    KeepOptional => "keep_optional"
    RemoveField => "remove"
    RedactField => "redact"
    AggregateField => "aggregate"
  }
}

///|
pub fn minimization_rule(
  path_pattern : String,
  purpose : String,
  action : MinimizationAction,
  justification : String,
  priority : Int,
) -> MinimizationRule {
  { path_pattern, purpose, action, justification, priority }
}

///|
pub fn MinimizationPolicy::new(
  purpose : String,
  rules : Array[MinimizationRule],
) -> MinimizationPolicy {
  {
    purpose,
    rules,
    default_action: RemoveField,
    config: RedactionConfig::default(),
  }
}

///|
pub fn MinimizationPolicy::default() -> MinimizationPolicy {
  MinimizationPolicy::new("analytics", [
    minimization_rule(
      "Patient.name",
      "analytics",
      RedactField,
      "retain linkage without identity",
      100,
    ),
    minimization_rule(
      "Patient.identifier",
      "analytics",
      RemoveField,
      "not needed",
      100,
    ),
    minimization_rule(
      "Patient.birthDate",
      "analytics",
      AggregateField,
      "age band only",
      80,
    ),
    minimization_rule(
      "Patient.address",
      "analytics",
      RemoveField,
      "not needed",
      100,
    ),
    minimization_rule(
      "Observation.value",
      "analytics",
      KeepOptional,
      "measurement required",
      10,
    ),
    minimization_rule(
      "Observation.note",
      "analytics",
      RedactField,
      "clinical context may be useful",
      60,
    ),
  ])
}

///|
pub fn MinimizationPolicy::with_default(
  policy : MinimizationPolicy,
  action : MinimizationAction,
) -> MinimizationPolicy {
  { ..policy, default_action: action }
}

///|
pub fn MinimizationPolicy::with_config(
  policy : MinimizationPolicy,
  config : RedactionConfig,
) -> MinimizationPolicy {
  { ..policy, config, }
}

///|
pub fn MinimizationPolicy::rules_for_purpose(
  self : MinimizationPolicy,
  purpose : String,
) -> Array[MinimizationRule] {
  self.rules.filter(fn(rule) { rule.purpose == purpose })
}

///|
pub fn MinimizationPolicy::action_for(
  self : MinimizationPolicy,
  path : String,
) -> MinimizationAction {
  let mut selected : MinimizationRule? = None
  for rule in self.rules {
    if resource_path_matches(rule.path_pattern, path) {
      match selected {
        Some(previous) if previous.priority >= rule.priority => ()
        _ => selected = Some(rule)
      }
    }
  }
  match selected {
    Some(rule) => rule.action
    None => self.default_action
  }
}

///|
pub fn MinimizationPolicy::justification_for(
  self : MinimizationPolicy,
  path : String,
) -> String {
  let mut selected : MinimizationRule? = None
  for rule in self.rules {
    if resource_path_matches(rule.path_pattern, path) {
      match selected {
        Some(previous) if previous.priority >= rule.priority => ()
        _ => selected = Some(rule)
      }
    }
  }
  match selected {
    Some(rule) => rule.justification
    None => "default policy"
  }
}

///|
pub fn MinimizationPolicy::is_well_formed(self : MinimizationPolicy) -> Bool {
  self.purpose.trim().length() > 0 &&
  self.rules.all(fn(rule) {
    rule.path_pattern.trim().length() > 0 &&
    rule.purpose.trim().length() > 0 &&
    rule.justification.trim().length() > 0 &&
    rule.priority >= 0
  })
}

///|
pub fn MinimizationPolicy::checksum(self : MinimizationPolicy) -> String {
  stable_hash(
    self.purpose +
    ":" +
    self.rules
    .map(fn(rule) {
      rule.path_pattern +
      ":" +
      minimization_action_name(rule.action) +
      ":\{rule.priority}"
    })
    .join("\n"),
  )
}

///|
pub fn aggregate_resource_value(field : ResourceField) -> String {
  match field.kind {
    Date =>
      match parse_calendar_date(field.value) {
        Some(date) => date.to_month()
        None => "[DATE]"
      }
    PersonName => "[NAME]"
    Address => "[ADDRESS]"
    Phone => "[PHONE]"
    Email => "[EMAIL]"
    IdNumber | MedicalRecord | Insurance => "[ID]"
    Organization => "[ORG]"
    Custom(_) => "[VALUE]"
  }
}

///|
pub fn apply_minimization_action(
  field : ResourceField,
  action : MinimizationAction,
  config : RedactionConfig,
) -> ResourceField raise DeidError {
  match action {
    KeepRequired | KeepOptional => field
    RemoveField => { ..field, value: "", action: Remove }
    RedactField => resource_apply_action(field, ResourceAction::Redact, config)
    AggregateField =>
      {
        ..field,
        value: aggregate_resource_value(field),
        action: ResourceAction::Hash,
      }
  }
}

///|
pub fn minimize_resource(
  document : ResourceDocument,
  policy : MinimizationPolicy,
) -> MinimizationResult raise DeidError {
  let minimized = ResourceDocument::empty(document.resource_type)
  minimized.resource_id = document.resource_id
  minimized.metadata = document.metadata.copy()
  let removed = []
  let redacted = []
  let aggregated = []
  let issues = []
  for field in document.fields {
    let action = policy.action_for(field.path)
    let transformed = apply_minimization_action(field, action, policy.config)
    match action {
      RemoveField => removed.push(field.path)
      RedactField => redacted.push(field.path)
      AggregateField => aggregated.push(field.path)
      _ => ()
    }
    if action != RemoveField || field.required {
      minimized.fields.push(transformed)
    }
    if policy.justification_for(field.path).is_empty() {
      issues.push("missing justification: \{field.path}")
    }
  }
  {
    original: document,
    minimized,
    removed_paths: removed,
    redacted_paths: redacted,
    aggregated_paths: aggregated,
    issues,
    checksum: stable_hash(resource_render(minimized)),
  }
}

///|
pub fn MinimizationResult::changed(self : MinimizationResult) -> Bool {
  resource_render(self.original) != resource_render(self.minimized)
}

///|
pub fn MinimizationResult::removed_count(self : MinimizationResult) -> Int {
  self.removed_paths.length()
}

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

///|
pub fn MinimizationResult::summary(self : MinimizationResult) -> String {
  [
    "resource_type=\{self.minimized.resource_type}",
    "original_fields=\{self.original.field_count()}",
    "minimized_fields=\{self.minimized.field_count()}",
    "removed=\{self.removed_count()}",
    "redacted=\{self.redacted_count()}",
    "aggregated=\{self.aggregated_paths.length()}",
    "issues=\{self.issues.length()}",
    "changed=\{self.changed()}",
    "checksum=\{self.checksum}",
  ].join("\n")
}

///|
pub fn MinimizationResult::to_json(self : MinimizationResult) -> String {
  "{" +
  "\"resource_type\":\{json_escape(self.minimized.resource_type)}," +
  "\"original_fields\":\{self.original.field_count()}," +
  "\"minimized_fields\":\{self.minimized.field_count()}," +
  "\"removed_paths\":[" +
  self.removed_paths.map(json_escape).join(",") +
  "]," +
  "\"redacted_paths\":[" +
  self.redacted_paths.map(json_escape).join(",") +
  "]," +
  "\"aggregated_paths\":[" +
  self.aggregated_paths.map(json_escape).join(",") +
  "]," +
  "\"issues\":[" +
  self.issues.map(json_escape).join(",") +
  "]," +
  "\"checksum\":\{json_escape(self.checksum)}" +
  "}"
}

///|
pub fn minimization_is_monotonic(
  original : ResourceDocument,
  minimized : ResourceDocument,
  policy : MinimizationPolicy,
) -> Bool {
  minimized.fields.all(fn(field) {
    match original.get(field.path) {
      Some(before) =>
        policy.action_for(field.path) == KeepRequired ||
        policy.action_for(field.path) == KeepOptional ||
        before.value != field.value
      None => false
    }
  })
}

///|
pub fn minimization_path_counts(
  result : MinimizationResult,
) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for path in result.removed_paths {
    ignore(path)
    counts["removed"] = counts.get_or_default("removed", 0) + 1
  }
  for path in result.redacted_paths {
    ignore(path)
    counts["redacted"] = counts.get_or_default("redacted", 0) + 1
  }
  for path in result.aggregated_paths {
    ignore(path)
    counts["aggregated"] = counts.get_or_default("aggregated", 0) + 1
  }
  counts
}

///|
pub fn minimization_has_restricted_removal(
  original : ResourceDocument,
  result : MinimizationResult,
) -> Bool {
  result.removed_paths.any(fn(path) {
    match original.get(path) {
      Some(field) => compliance_class_for_kind(field.kind) == Restricted
      None => false
    }
  })
}

///|
pub fn minimization_report(
  document : ResourceDocument,
  policy : MinimizationPolicy,
) -> String raise DeidError {
  let result = minimize_resource(document, policy)
  [
    result.summary(),
    "policy=\{policy.purpose}",
    "policy_checksum=\{policy.checksum()}",
    "restricted_removal=\{minimization_has_restricted_removal(document, result)}",
    "monotonic=\{minimization_is_monotonic(document, result.minimized, policy)}",
  ].join("\n")
}