///|
/// Named policy catalog for deployments with multiple data release channels.
pub(all) struct PolicyEntry {
  name : String
  version : String
  policy : RedactionPolicy
  owner : String
  description : String
  enabled : Bool
  checksum : String
} derive(Debug, Eq)

///|
pub(all) struct PolicyCatalog {
  mut entries : Map[String, PolicyEntry]
  mut default_name : String
} derive(Debug)

///|
pub fn PolicyCatalog::new() -> PolicyCatalog {
  { entries: Map([]), default_name: "" }
}

///|
pub fn PolicyCatalog::with_defaults() -> PolicyCatalog {
  let catalog = PolicyCatalog::new()
  catalog.add(PolicyEntry::{
    name: "default",
    version: "1",
    policy: RedactionPolicy::default(),
    owner: "platform",
    description: "balanced clinical text policy",
    enabled: true,
    checksum: "",
  })
  catalog.add(PolicyEntry::{
    name: "strict",
    version: "1",
    policy: RedactionPolicy::strict(),
    owner: "platform",
    description: "preserve-length strict release policy",
    enabled: true,
    checksum: "",
  })
  catalog.add(PolicyEntry::{
    name: "review",
    version: "1",
    policy: RedactionPolicy::review(),
    owner: "platform",
    description: "review-only exploration policy",
    enabled: true,
    checksum: "",
  })
  catalog.default_name = "default"
  catalog
}

///|
pub fn policy_entry_checksum(entry : PolicyEntry) -> String {
  stable_hash(
    entry.name + ":" + entry.version + ":" + policy_summary(entry.policy),
  )
}

///|
pub fn policy_entry_normalized(entry : PolicyEntry) -> PolicyEntry {
  { ..entry, checksum: policy_entry_checksum(entry) }
}

///|
pub fn PolicyCatalog::add(self : PolicyCatalog, entry : PolicyEntry) -> Unit {
  let normalized = policy_entry_normalized(entry)
  self.entries[normalized.name] = normalized
  if self.default_name.is_empty() {
    self.default_name = normalized.name
  }
}

///|
pub fn PolicyCatalog::remove(self : PolicyCatalog, name : String) -> Bool {
  if self.entries.contains(name) {
    self.entries.remove(name)
    if self.default_name == name {
      self.default_name = ""
    }
    true
  } else {
    false
  }
}

///|
pub fn PolicyCatalog::get(self : PolicyCatalog, name : String) -> PolicyEntry? {
  self.entries.get(name)
}

///|
pub fn PolicyCatalog::default_entry(self : PolicyCatalog) -> PolicyEntry? {
  self.entries.get(self.default_name)
}

///|
pub fn PolicyCatalog::set_default(self : PolicyCatalog, name : String) -> Bool {
  match self.entries.get(name) {
    Some(entry) if entry.enabled => {
      self.default_name = name
      true
    }
    _ => false
  }
}

///|
pub fn PolicyCatalog::names(self : PolicyCatalog) -> Array[String] {
  let names = self.entries.keys().to_array()
  names.sort()
  names
}

///|
pub fn PolicyCatalog::enabled(self : PolicyCatalog) -> Array[PolicyEntry] {
  self.entries.values().filter(fn(entry) { entry.enabled }).to_array()
}

///|
pub fn PolicyCatalog::disable(self : PolicyCatalog, name : String) -> Bool {
  match self.entries.get(name) {
    Some(entry) => {
      self.entries[name] = { ..entry, enabled: false }
      if self.default_name == name {
        self.default_name = ""
      }
      true
    }
    None => false
  }
}

///|
pub fn PolicyCatalog::enable(self : PolicyCatalog, name : String) -> Bool {
  match self.entries.get(name) {
    Some(entry) => {
      self.entries[name] = { ..entry, enabled: true }
      true
    }
    None => false
  }
}

///|
pub fn PolicyCatalog::policy_for(
  self : PolicyCatalog,
  name : String,
) -> RedactionPolicy? {
  self.get(name).map(fn(entry) { entry.policy })
}

///|
pub fn PolicyCatalog::default_policy(self : PolicyCatalog) -> RedactionPolicy {
  match self.default_entry() {
    Some(entry) => entry.policy
    None => RedactionPolicy::default()
  }
}

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

///|
pub fn PolicyCatalog::checksum(self : PolicyCatalog) -> String {
  let lines = []
  for name, entry in self.entries {
    lines.push(name + ":" + policy_entry_checksum(entry))
  }
  lines.sort()
  stable_hash(lines.join("\n"))
}

///|
pub fn policy_catalog_summary(catalog : PolicyCatalog) -> String {
  [
    "policies=\{catalog.size()}",
    "enabled=\{catalog.enabled().length()}",
    "default=\{catalog.default_name}",
    "checksum=\{catalog.checksum()}",
  ].join("\n")
}

///|
pub fn policy_catalog_validate(catalog : PolicyCatalog) -> Array[String] {
  let issues = []
  if catalog.default_name.is_empty() {
    issues.push("default policy is not configured")
  } else if catalog.default_entry() is None {
    issues.push("default policy does not exist")
  }
  for name, entry in catalog.entries {
    if name != entry.name {
      issues.push("catalog key differs from policy name: \{name}")
    }
    if entry.version.trim().is_empty() {
      issues.push("policy version is empty: \{name}")
    }
    if entry.enabled && policy_summary(entry.policy).is_empty() {
      issues.push("enabled policy has empty summary: \{name}")
    }
  }
  issues
}

///|
pub fn policy_catalog_is_valid(catalog : PolicyCatalog) -> Bool {
  policy_catalog_validate(catalog).is_empty()
}

///|
pub fn policy_catalog_find_by_owner(
  catalog : PolicyCatalog,
  owner : String,
) -> Array[PolicyEntry] {
  catalog.entries.values().filter(fn(entry) { entry.owner == owner }).to_array()
}

///|
pub fn policy_catalog_find_by_mode(
  catalog : PolicyCatalog,
  mode : ReplacementMode,
) -> Array[PolicyEntry] {
  catalog.entries
  .values()
  .filter(fn(entry) { entry.policy.mode == mode })
  .to_array()
}

///|
pub fn policy_catalog_for_locale(
  catalog : PolicyCatalog,
  locale : LocaleHint,
) -> Array[PolicyEntry] {
  catalog.entries
  .values()
  .filter(fn(entry) {
    entry.policy.locale == locale || entry.policy.locale == Mixed
  })
  .to_array()
}

///|
pub fn policy_catalog_json(catalog : PolicyCatalog) -> String {
  let entries = catalog.entries
    .values()
    .to_array()
    .map(fn(entry) {
      "{" +
      "\"name\":\{json_escape(entry.name)}," +
      "\"version\":\{json_escape(entry.version)}," +
      "\"owner\":\{json_escape(entry.owner)}," +
      "\"description\":\{json_escape(entry.description)}," +
      "\"enabled\":\{entry.enabled}," +
      "\"checksum\":\{json_escape(entry.checksum)}" +
      "}"
    })
    .join(",")
  "{" +
  "\"default\":\{json_escape(catalog.default_name)}," +
  "\"checksum\":\{json_escape(catalog.checksum())}," +
  "\"entries\":[" +
  entries +
  "]}"
}

///|
pub fn policy_catalog_markdown(catalog : PolicyCatalog) -> String {
  let lines = [
    "| Name | Version | Owner | Enabled | Mode | Checksum |", "| --- | --- | --- | --- | --- | --- |",
  ]
  for name in catalog.names() {
    match catalog.get(name) {
      Some(entry) =>
        lines.push(
          "| \{entry.name} | \{entry.version} | \{entry.owner} | \{entry.enabled} | \{mode_name(entry.policy.mode)} | \{entry.checksum} |",
        )
      None => ()
    }
  }
  lines.join("\n")
}

///|
pub fn policy_catalog_clone(catalog : PolicyCatalog) -> PolicyCatalog {
  let result = PolicyCatalog::new()
  result.default_name = catalog.default_name
  for entry in catalog.entries.values() {
    result.add(entry)
  }
  result
}

///|
pub fn policy_catalog_same(left : PolicyCatalog, right : PolicyCatalog) -> Bool {
  left.checksum() == right.checksum() && left.default_name == right.default_name
}

///|
pub fn policy_catalog_merge(
  left : PolicyCatalog,
  right : PolicyCatalog,
) -> PolicyCatalog {
  let result = policy_catalog_clone(left)
  for entry in right.entries.values() {
    result.add(entry)
  }
  if result.default_name.is_empty() {
    result.default_name = right.default_name
  }
  result
}

///|
pub fn policy_catalog_apply(
  catalog : PolicyCatalog,
  name : String,
  input : String,
) -> DeidResult raise DeidError {
  let policy = match catalog.policy_for(name) {
    Some(value) => value
    None => catalog.default_policy()
  }
  redact_with_policy(input, policy)
}

///|
pub fn policy_catalog_apply_default(
  catalog : PolicyCatalog,
  input : String,
) -> DeidResult raise DeidError {
  policy_catalog_apply(catalog, catalog.default_name, input)
}

///|
pub fn policy_catalog_mode_counts(catalog : PolicyCatalog) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for entry in catalog.entries.values() {
    let key = mode_name(entry.policy.mode)
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn policy_catalog_locale_counts(
  catalog : PolicyCatalog,
) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for entry in catalog.entries.values() {
    let key = locale_name(entry.policy.locale)
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}