///|
/// Minimal FHIR-style bundle support built on the structured resource layer.
/// It intentionally models only the resource metadata needed by a sanitizer.
pub(all) enum BundleType {
  Batch
  Transaction
  Collection
  History
} derive(Debug, Eq)

///|
pub(all) struct BundleEntry {
  full_url : String
  resource_type : String
  resource_id : String
  document : ResourceDocument
  position : Int
} derive(Debug)

///|
pub(all) struct ResourceBundle {
  bundle_type : BundleType
  bundle_id : String
  entries : Array[BundleEntry]
  metadata : Map[String, String]
} derive(Debug)

///|
pub(all) struct BundleReport {
  bundle_id : String
  input_entries : Int
  output_entries : Int
  changed_entries : Int
  removed_entries : Int
  issues : Array[ResourceIssue]
  checksum : String
} derive(Debug)

///|
pub fn bundle_type_name(value : BundleType) -> String {
  match value {
    Batch => "batch"
    Transaction => "transaction"
    Collection => "collection"
    History => "history"
  }
}

///|
pub fn ResourceBundle::new(
  bundle_type : BundleType,
  bundle_id : String,
) -> ResourceBundle {
  { bundle_type, bundle_id, entries: [], metadata: Map([]) }
}

///|
pub fn ResourceBundle::add(
  self : ResourceBundle,
  document : ResourceDocument,
  full_url : String,
) -> Unit {
  self.entries.push({
    full_url,
    resource_type: document.resource_type,
    resource_id: document.resource_id,
    document,
    position: self.entries.length(),
  })
}

///|
pub fn ResourceBundle::size(self : ResourceBundle) -> Int {
  self.entries.length()
}

///|
pub fn ResourceBundle::types(self : ResourceBundle) -> Array[String] {
  self.entries.map(fn(entry) { entry.resource_type })
}

///|
pub fn ResourceBundle::ids(self : ResourceBundle) -> Array[String] {
  self.entries.map(fn(entry) { entry.resource_id })
}

///|
pub fn ResourceBundle::find_type(
  self : ResourceBundle,
  resource_type : String,
) -> Array[BundleEntry] {
  self.entries.filter(fn(entry) { entry.resource_type == resource_type })
}

///|
pub fn ResourceBundle::find_id(
  self : ResourceBundle,
  resource_id : String,
) -> BundleEntry? {
  let mut result : BundleEntry? = None
  for entry in self.entries {
    if entry.resource_id == resource_id {
      result = Some(entry)
    }
  }
  result
}

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

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

///|
pub fn ResourceBundle::copy(self : ResourceBundle) -> ResourceBundle {
  let result = ResourceBundle::new(self.bundle_type, self.bundle_id)
  for entry in self.entries {
    result.add(entry.document.copy(), entry.full_url)
  }
  for key, value in self.metadata {
    result.metadata[key] = value
  }
  result
}

///|
pub fn bundle_redact(
  bundle : ResourceBundle,
  policy : ResourcePolicy,
) -> ResourceBundle raise DeidError {
  let output = ResourceBundle::new(bundle.bundle_type, bundle.bundle_id)
  for entry in bundle.entries {
    output.add(resource_redact(entry.document, policy), entry.full_url)
  }
  for key, value in bundle.metadata {
    output.metadata[key] = value
  }
  output
}

///|
pub fn bundle_changed_positions(
  before : ResourceBundle,
  after : ResourceBundle,
) -> Array[Int] {
  let changed = []
  let limit = if before.size() < after.size() {
    before.size()
  } else {
    after.size()
  }
  for i in 0.. Array[Int] {
  if after.size() >= before.size() {
    []
  } else {
    let result = []
    for i in after.size().. Array[ResourceIssue] {
  let issues = []
  if bundle.bundle_id.trim().is_empty() {
    issues.push({
      path: "id",
      code: "BUNDLE_ID",
      message: "bundle id is empty",
      severity: Error,
    })
  }
  let ids : Map[String, Unit] = Map([])
  for entry in bundle.entries {
    if entry.resource_type.trim().is_empty() {
      issues.push({
        path: "entry[\{entry.position}]",
        code: "RESOURCE_TYPE",
        message: "resource type is empty",
        severity: Error,
      })
    }
    if entry.resource_id.trim().is_empty() {
      issues.push({
        path: "entry[\{entry.position}]",
        code: "RESOURCE_ID",
        message: "resource id is empty",
        severity: Warning,
      })
    }
    if ids.contains(entry.resource_id) && !entry.resource_id.is_empty() {
      issues.push({
        path: "entry[\{entry.position}]",
        code: "DUPLICATE_ID",
        message: "resource id is duplicated",
        severity: Warning,
      })
    }
    ids[entry.resource_id] = ()
    issues.append(resource_validate(entry.document, ResourcePolicy::default()))
  }
  issues
}

///|
pub fn bundle_report(
  before : ResourceBundle,
  after : ResourceBundle,
  policy : ResourcePolicy,
) -> BundleReport {
  let issues = bundle_validate(after)
  for entry in after.entries {
    issues.append(resource_validate(entry.document, policy))
  }
  {
    bundle_id: after.bundle_id,
    input_entries: before.size(),
    output_entries: after.size(),
    changed_entries: bundle_changed_positions(before, after).length(),
    removed_entries: bundle_removed_positions(before, after).length(),
    issues,
    checksum: stable_hash(bundle_render(after)),
  }
}

///|
pub fn bundle_report_text(report : BundleReport) -> String {
  [
    "bundle_id=\{report.bundle_id}",
    "input_entries=\{report.input_entries}",
    "output_entries=\{report.output_entries}",
    "changed_entries=\{report.changed_entries}",
    "removed_entries=\{report.removed_entries}",
    "issues=\{report.issues.length()}",
    "checksum=\{report.checksum}",
  ].join("\n")
}

///|
pub fn bundle_render(bundle : ResourceBundle) -> String {
  let lines = [
    "bundle_type=\{bundle_type_name(bundle.bundle_type)}",
    "bundle_id=\{bundle.bundle_id}",
  ]
  for key, value in bundle.metadata {
    lines.push("meta.\{key}=\{value}")
  }
  for entry in bundle.entries {
    lines.push("entry.\{entry.position}.full_url=\{entry.full_url}")
    lines.push("entry.\{entry.position}.resource_type=\{entry.resource_type}")
    lines.push("entry.\{entry.position}.resource_id=\{entry.resource_id}")
    for field in entry.document.fields {
      lines.push("entry.\{entry.position}.\{field.path}=\{field.value}")
    }
  }
  lines.join("\n")
}

///|
pub fn bundle_type_counts(bundle : ResourceBundle) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for entry in bundle.entries {
    counts[entry.resource_type] = counts.get_or_default(entry.resource_type, 0) +
      1
  }
  counts
}

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

///|
pub fn bundle_sensitive_field_count(
  bundle : ResourceBundle,
  policy : ResourcePolicy,
) -> Int {
  bundle.entries.fold(init=0, (sum, entry) => {
    sum + resource_sensitive_path_count(entry.document, policy)
  })
}

///|
pub fn bundle_safe(
  before : ResourceBundle,
  after : ResourceBundle,
  policy : ResourcePolicy,
) -> Bool {
  let report = bundle_report(before, after, policy)
  !report.issues.any(fn(issue) { issue.severity == Error }) &&
  after.entries.all(fn(entry) {
    let before_entry = before.find_id(entry.resource_id)
    match before_entry {
      Some(previous) =>
        resource_is_safe(previous.document, entry.document, policy)
      None => true
    }
  })
}

///|
pub fn bundle_json(bundle : ResourceBundle) -> String {
  let entries = bundle.entries
    .map(fn(entry) {
      "{" +
      "\"full_url\":\{json_escape(entry.full_url)}," +
      "\"resource_type\":\{json_escape(entry.resource_type)}," +
      "\"resource_id\":\{json_escape(entry.resource_id)}," +
      "\"field_count\":\{entry.document.field_count()}," +
      "\"checksum\":\{json_escape(stable_hash(resource_render(entry.document)))}" +
      "}"
    })
    .join(",")
  "{" +
  "\"bundle_type\":\{json_escape(bundle_type_name(bundle.bundle_type))}," +
  "\"bundle_id\":\{json_escape(bundle.bundle_id)}," +
  "\"entries\":[" +
  entries +
  "]}"
}

///|
pub fn bundle_round_trip(
  bundle : ResourceBundle,
  policy : ResourcePolicy,
) -> Bool raise DeidError {
  let rendered = bundle_render(bundle)
  let copy = bundle.copy()
  let output = bundle_redact(copy, policy)
  rendered.length() >= output.bundle_id.length()
}

///|
pub fn bundle_resource_ids_unique(bundle : ResourceBundle) -> Bool {
  let seen : Map[String, Unit] = Map([])
  let mut unique = true
  for entry in bundle.entries {
    if !entry.resource_id.is_empty() && seen.contains(entry.resource_id) {
      unique = false
    }
    seen[entry.resource_id] = ()
  }
  unique
}

///|
pub fn bundle_order_is_stable(bundle : ResourceBundle) -> Bool {
  for i in 0.. Bool {
  bundle.entries.any(fn(entry) { entry.resource_type == resource_type })
}

///|
pub fn bundle_filter_type(
  bundle : ResourceBundle,
  resource_type : String,
) -> ResourceBundle {
  let result = ResourceBundle::new(bundle.bundle_type, bundle.bundle_id)
  for entry in bundle.find_type(resource_type) {
    result.add(entry.document.copy(), entry.full_url)
  }
  result
}

///|
pub fn bundle_without_type(
  bundle : ResourceBundle,
  resource_type : String,
) -> ResourceBundle {
  let result = ResourceBundle::new(bundle.bundle_type, bundle.bundle_id)
  for entry in bundle.entries {
    if entry.resource_type != resource_type {
      result.add(entry.document.copy(), entry.full_url)
    }
  }
  result
}

///|
pub fn bundle_checksum(bundle : ResourceBundle) -> String {
  stable_hash(bundle_render(bundle))
}

///|
pub fn bundle_checksum_stable(bundle : ResourceBundle) -> Bool {
  bundle_checksum(bundle) == bundle_checksum(bundle.copy())
}