///|
/// A lifecycle state for a pseudonym vault.
pub(all) enum VaultState {
  Active
  Frozen
  Retired
} derive(Debug, Eq)

///|
/// One in-memory mapping. Snapshot APIs can omit the original value.
pub(all) struct VaultEntry {
  scope : String
  kind : PhiKind
  original : String
  token : String
  fingerprint : String
  created_sequence : Int
  active : Bool
} derive(Debug, Eq)

///|
pub(all) struct VaultStats {
  scope : String
  state : VaultState
  entries : Int
  active_entries : Int
  kinds : Int
  checksum : String
} derive(Debug, Eq)

///|
pub(all) struct PseudonymVault {
  scope : String
  salt : String
  prefix : String
  mut entries : Map[String, VaultEntry]
  mut reverse : Map[String, String]
  mut aliases : Map[String, String]
  mut sequence : Int
  mut state : VaultState
} derive(Debug)

///|
pub fn PseudonymVault::new(
  scope : String,
  salt : String,
  prefix : String,
) -> PseudonymVault {
  {
    scope: if scope.trim().is_empty() {
      "default"
    } else {
      scope
    },
    salt,
    prefix,
    entries: Map([]),
    reverse: Map([]),
    aliases: Map([]),
    sequence: 0,
    state: Active,
  }
}

///|
pub fn PseudonymVault::default() -> PseudonymVault {
  PseudonymVault::new("default", "moonbit-vault", "PX")
}

///|
pub fn vault_state_name(state : VaultState) -> String {
  match state {
    Active => "active"
    Frozen => "frozen"
    Retired => "retired"
  }
}

///|
fn vault_key(scope : String, kind : PhiKind, original : String) -> String {
  scope + "\u{1f}" + phi_kind_name(kind) + "\u{1f}" + original
}

///|
fn vault_token_key(scope : String, token : String) -> String {
  scope + "\u{1f}" + token
}

///|
fn vault_fingerprint(
  salt : String,
  scope : String,
  kind : PhiKind,
  original : String,
) -> String {
  stable_hash(salt + "\u{1f}" + vault_key(scope, kind, original))
}

///|
fn vault_token_candidate(
  prefix : String,
  kind : PhiKind,
  fingerprint : String,
) -> String {
  let clean_prefix = if prefix.is_empty() { "PX" } else { prefix }
  clean_prefix + "_" + phi_kind_name(kind) + "_" + fingerprint
}

///|
pub fn PseudonymVault::is_active(self : PseudonymVault) -> Bool {
  self.state == Active
}

///|
pub fn PseudonymVault::is_frozen(self : PseudonymVault) -> Bool {
  self.state == Frozen
}

///|
pub fn PseudonymVault::is_retired(self : PseudonymVault) -> Bool {
  self.state == Retired
}

///|
pub fn PseudonymVault::freeze(self : PseudonymVault) -> Unit {
  self.state = Frozen
}

///|
pub fn PseudonymVault::unfreeze(self : PseudonymVault) -> Unit {
  if self.state == Frozen {
    self.state = Active
  }
}

///|
pub fn PseudonymVault::retire(self : PseudonymVault) -> Unit {
  self.state = Retired
}

///|
pub fn PseudonymVault::scope(self : PseudonymVault) -> String {
  self.scope
}

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

///|
pub fn PseudonymVault::active_size(self : PseudonymVault) -> Int {
  self.entries.values().filter(fn(item) { item.active }).length()
}

///|
pub fn PseudonymVault::contains(
  self : PseudonymVault,
  original : String,
  kind : PhiKind,
) -> Bool {
  self.entries.contains(vault_key(self.scope, kind, original))
}

///|
pub fn PseudonymVault::lookup(
  self : PseudonymVault,
  original : String,
  kind : PhiKind,
) -> VaultEntry? {
  self.entries.get(vault_key(self.scope, kind, original))
}

///|
pub fn PseudonymVault::lookup_token(
  self : PseudonymVault,
  token : String,
) -> VaultEntry? {
  match self.reverse.get(vault_token_key(self.scope, token)) {
    Some(key) => self.entries.get(key)
    None => None
  }
}

///|
fn PseudonymVault::collision_token(
  self : PseudonymVault,
  candidate : String,
  fingerprint : String,
) -> String {
  match self.reverse.get(vault_token_key(self.scope, candidate)) {
    None => candidate
    Some(key) =>
      if key.has_suffix(fingerprint) {
        candidate
      } else {
        candidate + "_" + stable_hash(key)
      }
  }
}

///|
pub fn PseudonymVault::token(
  self : PseudonymVault,
  original : String,
  kind : PhiKind,
) -> String {
  let key = vault_key(self.scope, kind, original)
  match self.entries.get(key) {
    Some(entry) => entry.token
    None =>
      if self.state == Active {
        let fingerprint = vault_fingerprint(
          self.salt,
          self.scope,
          kind,
          original,
        )
        self.collision_token(
          vault_token_candidate(self.prefix, kind, fingerprint),
          fingerprint,
        )
      } else {
        "[UNMAPPED]"
      }
  }
}

///|
pub fn PseudonymVault::lookup_or_create(
  self : PseudonymVault,
  original : String,
  kind : PhiKind,
) -> String {
  let key = vault_key(self.scope, kind, original)
  match self.entries.get(key) {
    Some(entry) => entry.token
    None =>
      if self.state != Active {
        "[UNMAPPED]"
      } else {
        let fingerprint = vault_fingerprint(
          self.salt,
          self.scope,
          kind,
          original,
        )
        let token = self.collision_token(
          vault_token_candidate(self.prefix, kind, fingerprint),
          fingerprint,
        )
        self.sequence += 1
        let entry = {
          scope: self.scope,
          kind,
          original,
          token,
          fingerprint,
          created_sequence: self.sequence,
          active: true,
        }
        self.entries[key] = entry
        self.reverse[vault_token_key(self.scope, token)] = key
        token
      }
  }
}

///|
pub fn PseudonymVault::lookup_or_create_for_finding(
  self : PseudonymVault,
  finding : Finding,
) -> String {
  self.lookup_or_create(finding.text, finding.kind)
}

///|
pub fn PseudonymVault::token_for_kind(
  self : PseudonymVault,
  original : String,
  kind : PhiKind,
  fallback : String,
) -> String {
  let token = self.lookup_or_create(original, kind)
  if token == "[UNMAPPED]" {
    fallback
  } else {
    token
  }
}

///|
pub fn PseudonymVault::deactivate(
  self : PseudonymVault,
  original : String,
  kind : PhiKind,
) -> Bool {
  let key = vault_key(self.scope, kind, original)
  match self.entries.get(key) {
    Some(entry) => {
      self.entries[key] = { ..entry, active: false }
      true
    }
    None => false
  }
}

///|
pub fn PseudonymVault::reactivate(
  self : PseudonymVault,
  original : String,
  kind : PhiKind,
) -> Bool {
  let key = vault_key(self.scope, kind, original)
  match self.entries.get(key) {
    Some(entry) => {
      self.entries[key] = { ..entry, active: true }
      true
    }
    None => false
  }
}

///|
pub fn PseudonymVault::add_alias(
  self : PseudonymVault,
  alias_value : String,
  canonical : String,
  kind : PhiKind,
) -> Bool {
  match self.entries.get(vault_key(self.scope, kind, canonical)) {
    Some(_) => {
      self.aliases[vault_key(self.scope, kind, alias_value)] = canonical
      true
    }
    None => false
  }
}

///|
pub fn PseudonymVault::resolve_alias(
  self : PseudonymVault,
  value : String,
  kind : PhiKind,
) -> String {
  match self.aliases.get(vault_key(self.scope, kind, value)) {
    Some(canonical) => canonical
    None => value
  }
}

///|
pub fn PseudonymVault::alias_count(self : PseudonymVault) -> Int {
  self.aliases.length()
}

///|
pub fn PseudonymVault::reidentify(
  self : PseudonymVault,
  token : String,
) -> String? {
  match self.lookup_token(token) {
    Some(entry) => if entry.active { Some(entry.original) } else { None }
    None => None
  }
}

///|
pub fn PseudonymVault::kind_for_token(
  self : PseudonymVault,
  token : String,
) -> PhiKind? {
  self.lookup_token(token).map(fn(entry) { entry.kind })
}

///|
pub fn PseudonymVault::entries_for_kind(
  self : PseudonymVault,
  kind : PhiKind,
) -> Array[VaultEntry] {
  self.entries.values().filter(fn(entry) { entry.kind == kind }).to_array()
}

///|
pub fn PseudonymVault::entries_for_namespace(
  self : PseudonymVault,
) -> Array[VaultEntry] {
  self.entries.values().to_array()
}

///|
pub fn PseudonymVault::tokens(self : PseudonymVault) -> Array[String] {
  self.entries.values().map(fn(entry) { entry.token }).to_array()
}

///|
pub fn PseudonymVault::originals(self : PseudonymVault) -> Array[String] {
  self.entries
  .values()
  .filter(fn(entry) { entry.active })
  .map(fn(entry) { entry.original })
  .to_array()
}

///|
pub fn PseudonymVault::fingerprints(self : PseudonymVault) -> Array[String] {
  self.entries.values().map(fn(entry) { entry.fingerprint }).to_array()
}

///|
pub fn PseudonymVault::stats(self : PseudonymVault) -> VaultStats {
  let kinds : Map[String, Unit] = Map([])
  for entry in self.entries.values() {
    kinds[phi_kind_name(entry.kind)] = ()
  }
  {
    scope: self.scope,
    state: self.state,
    entries: self.size(),
    active_entries: self.active_size(),
    kinds: kinds.length(),
    checksum: self.checksum(),
  }
}

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

///|
pub fn PseudonymVault::ordered_entries(
  self : PseudonymVault,
) -> Array[VaultEntry] {
  let entries = self.entries.values().to_array()
  entries.sort_by(fn(left, right) {
    left.created_sequence - right.created_sequence
  })
  entries
}

///|
pub fn vault_entry_metadata(entry : VaultEntry) -> String {
  [
    "scope=\{entry.scope}",
    "kind=\{phi_kind_name(entry.kind)}",
    "token=\{entry.token}",
    "fingerprint=\{entry.fingerprint}",
    "sequence=\{entry.created_sequence}",
    "active=\{entry.active}",
  ].join("\n")
}

///|
pub fn PseudonymVault::kind_counts(self : PseudonymVault) -> Map[String, Int] {
  let counts : Map[String, Int] = Map([])
  for entry in self.entries.values() {
    let key = phi_kind_name(entry.kind)
    counts[key] = counts.get_or_default(key, 0) + 1
  }
  counts
}

///|
pub fn PseudonymVault::active_tokens(self : PseudonymVault) -> Array[String] {
  self.entries
  .values()
  .filter(fn(entry) { entry.active })
  .map(fn(entry) { entry.token })
  .to_array()
}

///|
pub fn PseudonymVault::inactive_tokens(self : PseudonymVault) -> Array[String] {
  self.entries
  .values()
  .filter(fn(entry) { !entry.active })
  .map(fn(entry) { entry.token })
  .to_array()
}

///|
pub fn PseudonymVault::entry_by_sequence(
  self : PseudonymVault,
  sequence : Int,
) -> VaultEntry? {
  let mut result : VaultEntry? = None
  for entry in self.entries.values() {
    if entry.created_sequence == sequence {
      result = Some(entry)
    }
  }
  result
}

///|
pub fn PseudonymVault::next_sequence(self : PseudonymVault) -> Int {
  self.sequence + 1
}

///|
pub fn PseudonymVault::namespace_checksum(self : PseudonymVault) -> String {
  stable_hash(self.scope + "\u{1f}" + self.salt)
}

///|
pub fn vault_entry_json(entry : VaultEntry, include_original : Bool) -> String {
  let original = if include_original {
    ",\"original\":\{json_escape(entry.original)}"
  } else {
    ""
  }
  "{" +
  "\"scope\":\{json_escape(entry.scope)}," +
  "\"kind\":\{json_escape(phi_kind_name(entry.kind))}," +
  "\"token\":\{json_escape(entry.token)}," +
  "\"fingerprint\":\{json_escape(entry.fingerprint)}," +
  "\"sequence\":\{entry.created_sequence}," +
  "\"active\":\{entry.active}" +
  original +
  "}"
}

///|
pub fn PseudonymVault::snapshot_json(self : PseudonymVault) -> String {
  "[" +
  self
  .ordered_entries()
  .map(fn(entry) { vault_entry_json(entry, false) })
  .join(",") +
  "]"
}

///|
pub fn PseudonymVault::export_json(
  self : PseudonymVault,
  include_original : Bool,
) -> String {
  "{" +
  "\"scope\":\{json_escape(self.scope)}," +
  "\"state\":\{json_escape(vault_state_name(self.state))}," +
  "\"checksum\":\{json_escape(self.checksum())}," +
  "\"entries\":[" +
  self
  .ordered_entries()
  .map(fn(entry) { vault_entry_json(entry, include_original) })
  .join(",") +
  "]}"
}

///|
pub fn PseudonymVault::export_lines(
  self : PseudonymVault,
  include_original : Bool,
) -> Array[String] {
  let lines = [
    "vault-version=1",
    "scope=\{self.scope}",
    "state=\{vault_state_name(self.state)}",
    "checksum=\{self.checksum()}",
  ]
  for entry in self.ordered_entries() {
    let original = if include_original { entry.original } else { "" }
    lines.push(
      [
        entry.token,
        phi_kind_name(entry.kind),
        entry.fingerprint,
        "\{entry.created_sequence}",
        "\{entry.active}",
        original,
      ].join("\t"),
    )
  }
  lines
}

///|
pub fn PseudonymVault::export_text(
  self : PseudonymVault,
  include_original : Bool,
) -> String {
  self.export_lines(include_original).join("\n")
}

///|
pub fn vault_kind_from_name(name : String) -> PhiKind {
  match name {
    "name" => PersonName
    "id" => IdNumber
    "phone" => Phone
    "date" => Date
    "email" => Email
    "address" => Address
    "medical_record" => MedicalRecord
    "insurance" => Insurance
    "organization" => Organization
    value => Custom(value)
  }
}

///|
pub fn vault_entry_from_line(scope : String, line : String) -> VaultEntry? {
  let parts = line.split("\t").to_array()
  if parts.length() < 5 {
    None
  } else {
    Some({
      scope,
      kind: vault_kind_from_name(parts[1].to_owned()),
      original: if parts.length() >= 6 {
        parts[5].to_owned()
      } else {
        ""
      },
      token: parts[0].to_owned(),
      fingerprint: parts[2].to_owned(),
      created_sequence: decimal_value(parts[3].to_owned()),
      active: parts[4].to_owned() == "true",
    })
  }
}

///|
pub fn import_vault_lines(
  scope : String,
  salt : String,
  prefix : String,
  lines : Array[String],
) -> PseudonymVault {
  let vault = PseudonymVault::new(scope, salt, prefix)
  for line in lines {
    match vault_entry_from_line(vault.scope, line) {
      Some(entry) if line.contains("\t") => {
        let key = vault_key(vault.scope, entry.kind, entry.original)
        vault.entries[key] = entry
        vault.reverse[vault_token_key(vault.scope, entry.token)] = key
        if entry.created_sequence > vault.sequence {
          vault.sequence = entry.created_sequence
        }
      }
      _ => ()
    }
  }
  vault
}

///|
pub fn PseudonymVault::vault_export_safe(self : PseudonymVault) -> Bool {
  let exported = self.export_text(false)
  !exported.contains("original=") &&
  exported.contains("fingerprint") &&
  !exported.contains("\t\t")
}

///|
pub fn PseudonymVault::vault_reidentification_round_trip(
  self : PseudonymVault,
  original : String,
  kind : PhiKind,
) -> Bool {
  let token = self.lookup_or_create(original, kind)
  self.reidentify(token) == Some(original)
}

///|
pub fn PseudonymVault::vault_rotation_preview(
  self : PseudonymVault,
  next_salt : String,
) -> Map[String, String] {
  let result : Map[String, String] = Map([])
  for entry in self.entries.values() {
    let fingerprint = vault_fingerprint(
      next_salt,
      self.scope,
      entry.kind,
      entry.original,
    )
    result[entry.token] = vault_token_candidate(
      self.prefix,
      entry.kind,
      fingerprint,
    )
  }
  result
}

///|
pub fn PseudonymVault::vault_rotation_report(
  self : PseudonymVault,
  next_salt : String,
) -> String {
  let preview = self.vault_rotation_preview(next_salt)
  let lines = [
    "scope=\{self.scope}",
    "current_checksum=\{self.checksum()}",
    "next_salt_checksum=\{stable_hash(next_salt)}",
    "entries=\{preview.length()}",
  ]
  for old_token, new_token in preview {
    lines.push("\{old_token} -> \{new_token}")
  }
  lines.join("\n")
}

///|
pub fn vault_merge(
  left : PseudonymVault,
  right : PseudonymVault,
) -> PseudonymVault {
  let result = left
  for entry in right.ordered_entries() {
    let key = vault_key(result.scope, entry.kind, entry.original)
    if !result.entries.contains(key) {
      let normalized = { ..entry, scope: result.scope }
      result.entries[key] = normalized
      result.reverse[vault_token_key(result.scope, normalized.token)] = key
      if normalized.created_sequence > result.sequence {
        result.sequence = normalized.created_sequence
      }
    }
  }
  result
}

///|
pub fn vault_merge_conflicts(
  left : PseudonymVault,
  right : PseudonymVault,
) -> Array[String] {
  let conflicts = []
  for entry in right.entries.values() {
    match left.lookup(entry.original, entry.kind) {
      Some(existing) if existing.token != entry.token =>
        conflicts.push(entry.original)
      _ => ()
    }
  }
  conflicts
}

///|
pub fn vault_compare(left : PseudonymVault, right : PseudonymVault) -> String {
  let conflicts = vault_merge_conflicts(left, right)
  [
    "left=\{left.size()}",
    "right=\{right.size()}",
    "conflicts=\{conflicts.length()}",
    "same_scope=\{left.scope == right.scope}",
  ].join("\n")
}

///|
pub fn PseudonymVault::vault_audit_line(self : PseudonymVault) -> String {
  let stats = self.stats()
  [
    "scope=\{stats.scope}",
    "state=\{vault_state_name(stats.state)}",
    "entries=\{stats.entries}",
    "active=\{stats.active_entries}",
    "kinds=\{stats.kinds}",
    "checksum=\{stats.checksum}",
  ].join(" ")
}

///|
pub fn PseudonymVault::redact_findings(
  self : PseudonymVault,
  input : String,
  findings : Array[Finding],
) -> DeidResult {
  let mapped = findings.map(fn(item) {
    { ..item, replacement: self.lookup_or_create(item.text, item.kind) }
  })
  let (text, offsets) = apply_findings(input, mapped)
  {
    text,
    findings: mapped,
    offsets,
    audit: build_audit(input, text, mapped, offsets),
  }
}

///|
pub fn PseudonymVault::redact(
  self : PseudonymVault,
  input : String,
) -> DeidResult raise DeidError {
  self.redact_findings(input, scan(input))
}

///|
pub fn PseudonymVault::redact_with_rules(
  self : PseudonymVault,
  input : String,
  rules : Array[Rule],
) -> DeidResult raise DeidError {
  self.redact_findings(input, scan(input, rules~))
}

///|
pub fn PseudonymVault::redact_batch(
  self : PseudonymVault,
  items : Array[BatchItem],
) -> Array[BatchResult] raise DeidError {
  let result = []
  for item in items {
    result.push({ id: item.id, result: self.redact(item.text) })
  }
  result
}

///|
pub fn vault_token_is_well_formed(token : String) -> Bool {
  let parts = token.split("_").to_array()
  parts.length() >= 3 &&
  parts[0].length() > 0 &&
  parts[1].length() > 0 &&
  parts[2].length() >= 8 &&
  all_ascii_hex(parts[2].to_owned())
}

///|
pub fn vault_tokens_are_unique(tokens : Array[String]) -> Bool {
  let seen : Map[String, Unit] = Map([])
  let mut unique = true
  for token in tokens {
    if seen.contains(token) {
      unique = false
    } else {
      seen[token] = ()
    }
  }
  unique
}