///|
fn escape_key(key : String) -> String {
  key.replace_all(old="~", new="~0").replace_all(old="/", new="~1")
}

///|
fn segments(pointer : String) -> Array[String] raise InputError {
  if pointer == "" {
    return []
  }
  if !pointer.has_prefix("/") {
    raise InputError("config reference must be a JSON pointer")
  }
  let parts = pointer.split("/").collect()
  ignore(parts.remove(0))
  parts.map(part => {
    let chars = part.to_owned().to_array()
    for i = 0; i < chars.length(); i = i + 1 {
      if chars[i] == '~' &&
        (
          i + 1 == chars.length() ||
          (chars[i + 1] != '0' && chars[i + 1] != '1')
        ) {
        raise InputError("invalid JSON pointer escape")
      }
    }
    part
    .to_owned()
    .replace_all(old="~1", new="/")
    .replace_all(old="~0", new="~")
  })
}

///|
fn at(root : Value, pointer : String) -> Value raise {
  let mut current = root
  for part in segments(pointer) {
    match current {
      Object(fields) =>
        current = match fields.get(part) {
          Some(v) => v
          None =>
            raise InputError("missing configuration reference: " + pointer)
        }
      Array(items) => {
        if part == "" ||
          (part.length() > 1 && part.has_prefix("0")) ||
          !part.to_array().all(c => c >= '0' && c <= '9') {
          raise InputError("invalid canonical array index")
        }
        let n = num(parse(part)) catch {
          _ => raise InputError("invalid array pointer")
        }
        if n < 0.0 || n.floor() != n || n >= items.length().to_double() {
          raise InputError("array pointer out of range")
        }
        current = items[n.to_int()]
      }
      _ => raise InputError("reference traverses scalar: " + pointer)
    }
  }
  current
}

///|
fn kind(value : Value) -> String {
  match value {
    Null => "null"
    Bool(_) => "boolean"
    Number(_) => "number"
    String(_) => "string"
    Array(_) => "array"
    Object(_) => "object"
  }
}

///|
let kinds : Array[String] = [
  "any", "null", "boolean", "number", "string", "array", "object",
]

///|
let strategies : Array[String] = ["replace", "append", "unique"]

///|
let clauses : Array[String] = [
  "path", "required", "type", "enum", "pattern", "min_length", "max_length", "min",
  "max",
]

///|
/// Array merge strategy: one default mode plus optional per-path overrides.
/// Per-path keys are JSON Pointers matched exactly against merge paths.
priv struct ArrayStrategy {
  default : String
  per_path : Map[String, String]
}

///|
fn canonical_pointer(pointer : String) -> String raise {
  "/" + segments(pointer).map(escape_key).join("/")
}

///|
fn array_strategy(request : Value) -> ArrayStrategy raise {
  match get(request, "arrays") {
    Null => { default: "replace", per_path: Map([]), }
    String(mode) =>
      if strategies.contains(mode) {
        { default: mode, per_path: Map([]), }
      } else {
        raise InputError("unknown array merge strategy: " + mode)
      }
    Object(fields) => {
      let result : Map[String, String] = Map([])
      let default = optional_str(
        fields.get("default").unwrap_or(Null),
        "replace",
      )
      if !strategies.contains(default) {
        raise InputError("unknown array merge strategy: " + default)
      }
      for key, value in fields {
        if key == "default" {
          continue
        }
        if !strategies.contains(key) {
          raise InputError("unknown array merge strategy: " + key)
        }
        for pointer in string_list(value) {
          let normalized = canonical_pointer(pointer)
          if result.contains(normalized) {
            raise InputError("array strategy assigned twice: " + normalized)
          }
          result[normalized] = key
        }
      }
      { default, per_path: result, }
    }
    _ => raise InputError("arrays must be a strategy or a path map")
  }
}

///|
fn ArrayStrategy::at(self : ArrayStrategy, path : String) -> String {
  self.per_path.get(path).unwrap_or(self.default)
}

///|
let nesting_limit : Int32 = 128

///|
fn note_origin(
  origins : Map[String, Array[String]],
  path : String,
  source : String,
) -> Unit {
  let history = origins.get(path).unwrap_or([])
  if history.is_empty() || history[history.length() - 1] != source {
    history.push(source)
  }
  origins[path] = history
}

///|
fn track_tree(
  value : Value,
  path : String,
  source : String,
  origins : Map[String, Array[String]],
) -> Unit {
  note_origin(origins, path, source)
  match value {
    Object(fields) =>
      for key, child in fields {
        track_tree(child, path + "/" + escape_key(key), source, origins)
      }
    Array(items) =>
      for i = 0; i < items.length(); i = i + 1 {
        track_tree(items[i], path + "/" + i.to_string(), source, origins)
      }
    _ => ()
  }
}

///|
fn merge(
  base : Value,
  overlay : Value,
  path : String,
  source : String,
  arrays : ArrayStrategy,
  strict : Bool,
  origins : Map[String, Array[String]],
  shadowed : Array[(String, String, String)],
  depth : Int32,
) -> Value raise {
  if depth > nesting_limit {
    raise InputError("configuration nesting limit")
  }
  note_origin(origins, path, source)
  match (base, overlay) {
    (Object(old), Object(next)) => {
      let result = old.copy()
      for key, value in next {
        let child = path + "/" + escape_key(key)
        result[key] = match old.get(key) {
          Some(previous) =>
            merge(
              previous,
              value,
              child,
              source,
              arrays,
              strict,
              origins,
              shadowed,
              depth + 1,
            )
          None => {
            track_tree(value, child, source, origins)
            value
          }
        }
      }
      Object(result)
    }
    (Array(old), Array(next)) if arrays.at(path) != "replace" => {
      let result = old.copy()
      for value in next {
        if arrays.at(path) == "append" || !result.contains(value) {
          track_tree(
            value,
            path + "/" + result.length().to_string(),
            source,
            origins,
          )
          result.push(value)
        }
      }
      Array(result)
    }
    _ => {
      if strict &&
        base != Null &&
        overlay != Null &&
        kind(base) != kind(overlay) {
        raise InputError("type-changing overlay at " + path)
      }
      // This layer replaced a different value: name the layer it overrode.
      if base != overlay {
        let history = origins.get(path).unwrap_or([])
        if history.length() >= 2 && history[history.length() - 2] != source {
          shadowed.push((path, history[history.length() - 2], source))
        }
      }
      track_tree(overlay, path, source, origins)
      overlay
    }
  }
}

///|
priv struct Resolution {
  root : Value
  env : Map[String, Value]
  secret_paths : Array[String]
  secret_keys : Array[String]
  secret_env : Array[String]
  visiting : Map[String, Bool]
  resolved : Map[String, Value]
  tainted : Map[String, Bool]
}

///|
fn under(path : String, parent : String) -> Bool {
  path == parent || path.has_prefix(parent + "/")
}

///|
/// The unescaped last key of a pointer, or "" for the root.
fn last_key(path : String) -> String {
  match path.rev_find("/") {
    Some(index) =>
      path[index + 1:]
      .to_owned()
      .replace_all(old="~1", new="/")
      .replace_all(old="~0", new="~")
    None => ""
  }
}

///|
fn Resolution::sensitive(self : Resolution, path : String) -> Bool {
  self.secret_paths.any(parent => under(path, parent)) ||
  (path != "" && self.secret_keys.contains(last_key(path))) ||
  self.tainted.get(path).unwrap_or(false)
}

///|
/// Whether any path strictly below `path` in the merged tree is sensitive by
/// designation (explicit path or key name).
fn Resolution::hides_secret(
  self : Resolution,
  value : Value,
  path : String,
) -> Bool {
  match value {
    Object(fields) =>
      fields
      .iter()
      .any(entry => {
        let child = path + "/" + escape_key(entry.0)
        self.sensitive(child) || self.hides_secret(entry.1, child)
      })
    Array(items) => {
      for i = 0; i < items.length(); i = i + 1 {
        let child = path + "/" + i.to_string()
        if self.sensitive(child) || self.hides_secret(items[i], child) {
          return true
        }
      }
      false
    }
    _ => false
  }
}

///|
/// Whether a pointer resolves inside root; a pointer that traverses a scalar
/// or leaves an array counts as absent, so it can fall back to a default.
fn present(root : Value, pointer : String) -> Bool raise {
  let mut current = root
  for part in segments(pointer) {
    match current {
      Object(fields) =>
        current = match fields.get(part) {
          Some(v) => v
          None => return false
        }
      Array(items) => {
        let n = num(parse(part)) catch { _ => return false }
        if n < 0.0 || n.floor() != n || n >= items.length().to_double() {
          return false
        }
        current = items[n.to_int()]
      }
      _ => return false
    }
  }
  true
}

///|
/// A reference is `${env:NAME}`, `${config:/pointer}` or either form with a
/// `:-literal` fallback used only when the target is absent.
fn Resolution::reference(
  self : Resolution,
  token : String,
  owner : String,
  depth : Int32,
) -> Value raise {
  let (target, fallback) = match token.split_once(":-") {
    Some((name, text)) => (name.to_owned(), Some(text.to_owned()))
    None => (token, None)
  }
  if target.has_prefix("env:") {
    let name = target[4:].to_owned()
    match self.env.get(name) {
      Some(v) => {
        if self.secret_env.contains(name) {
          self.tainted[owner] = true
        }
        v
      }
      None =>
        match fallback {
          Some(text) => String(text)
          None => raise InputError("missing environment variable: " + name)
        }
    }
  } else if target.has_prefix("config:") {
    let path = target[7:].to_owned()
    if !present(self.root, path) {
      match fallback {
        Some(text) => return String(text)
        None => raise InputError("missing configuration reference: " + path)
      }
    }
    let value = self.resolve(path, depth + 1)
    if self.sensitive(path) ||
      self.hides_secret(at(self.root, path), path) ||
      self.tainted
      .keys()
      .any(child => under(child, path) && self.tainted[child]) {
      self.tainted[owner] = true
    }
    value
  } else {
    raise InputError("interpolation must start with env: or config:")
  }
}

///|
fn Resolution::interpolate(
  self : Resolution,
  source : String,
  owner : String,
  depth : Int32,
) -> Value raise {
  let mut remaining = source
  let mut result = ""
  while true {
    match remaining.split_once("${") {
      None => return String(result + remaining)
      Some((prefix, suffix)) => {
        let (token, after) = match suffix.split_once("}") {
          Some(pair) => pair
          None => raise InputError("unterminated interpolation at " + owner)
        }
        let value = self.reference(token.to_owned(), owner, depth)
        if prefix.is_empty() && result == "" && after.is_empty() {
          return value
        }
        let embedded = match value {
          String(s) => s
          Number(_) | Bool(_) => canonical(value)
          _ => raise InputError("cannot embed structured configuration value")
        }
        result += prefix.to_owned() + embedded
        remaining = after.to_owned()
      }
    }
  } nobreak {
    raise InputError("interpolation failed")
  }
}

///|
fn Resolution::resolve(
  self : Resolution,
  path : String,
  depth : Int32,
) -> Value raise {
  if depth > nesting_limit {
    raise InputError("interpolation depth limit")
  }
  match self.resolved.get(path) {
    Some(v) => return v
    None => ()
  }
  if self.visiting.get(path).unwrap_or(false) {
    raise InputError("interpolation cycle at " + path)
  }
  self.visiting[path] = true
  let value = match at(self.root, path) {
    String(source) => self.interpolate(source, path, depth)
    Object(fields) => {
      let output : Map[String, Value] = Map([])
      for key, _ in fields {
        output[key] = self.resolve(path + "/" + escape_key(key), depth + 1)
      }
      Object(output)
    }
    Array(items) => {
      let output : Array[Value] = []
      for i = 0; i < items.length(); i = i + 1 {
        output.push(self.resolve(path + "/" + i.to_string(), depth + 1))
      }
      Array(output)
    }
    value => value
  }
  self.visiting[path] = false
  self.resolved[path] = value
  value
}

///|
fn Resolution::redact(
  self : Resolution,
  value : Value,
  path : String,
  masked : Array[String],
) -> Value {
  if self.sensitive(path) {
    masked.push(path)
    return String("***")
  }
  match value {
    Object(fields) =>
      Object(
        fields.map((key, child) => {
          self.redact(child, path + "/" + escape_key(key), masked)
        }),
      )
    Array(items) => {
      let result : Array[Value] = []
      for i = 0; i < items.length(); i = i + 1 {
        result.push(self.redact(items[i], path + "/" + i.to_string(), masked))
      }
      Array(result)
    }
    other => other
  }
}

///|
/// Returns the first violated clause of a constraint, checked in the fixed
/// order type, enum, pattern, min_length, max_length, min, max, or "" when
/// the value satisfies all of them. Patterns are unanchored searches in the
/// toolchain's regex dialect; lengths count UTF-16 code units of strings or
/// elements of arrays.
fn violation(value : Value, constraint : Value) -> String raise {
  let expected = optional_str(get(constraint, "type"), "any")
  if !kinds.contains(expected) {
    raise InputError("unknown constraint type: " + expected)
  }
  if expected != "any" && expected != kind(value) {
    return "type mismatch"
  }
  if get(constraint, "enum") != Null &&
    !arr(get(constraint, "enum")).contains(value) {
    return "not in enum"
  }
  if get(constraint, "pattern") != Null {
    let pattern = str(get(constraint, "pattern"))
    let regex = @string.Regex::Regex(pattern) catch {
      _ => raise InputError("invalid constraint pattern: " + pattern)
    }
    match value {
      String(text) =>
        if regex.execute(text) is None {
          return "pattern mismatch"
        }
      _ => return "pattern requires string"
    }
  }
  let length = match value {
    String(text) => Some(text.length())
    Array(items) => Some(items.length())
    _ => None
  }
  if get(constraint, "min_length") != Null {
    match length {
      None => return "length requires string or array"
      Some(n) =>
        if n.to_double() < num(get(constraint, "min_length")) {
          return "below minimum length"
        }
    }
  }
  if get(constraint, "max_length") != Null {
    match length {
      None => return "length requires string or array"
      Some(n) =>
        if n.to_double() > num(get(constraint, "max_length")) {
          return "above maximum length"
        }
    }
  }
  if get(constraint, "min") != Null {
    if kind(value) != "number" {
      return "numeric minimum requires number"
    }
    if num(value) < num(get(constraint, "min")) {
      return "below minimum"
    }
  }
  if get(constraint, "max") != Null {
    if kind(value) != "number" {
      return "numeric maximum requires number"
    }
    if num(value) > num(get(constraint, "max")) {
      return "above maximum"
    }
  }
  ""
}

///|
fn check_constraints(
  value : Value,
  constraints : Array[Value],
) -> Array[Value] raise {
  let issues : Array[Value] = []
  for constraint in constraints {
    for clause, _ in obj(constraint) {
      if !clauses.contains(clause) {
        raise InputError("unknown constraint clause: " + clause)
      }
    }
    let path = str(get(constraint, "path"))
    ignore(segments(path))
    let selected = Some(at(value, path)) catch { _ => None }
    let message = match selected {
      None =>
        if truth(get(constraint, "required")) {
          "required value missing"
        } else {
          ""
        }
      Some(v) => violation(v, constraint)
    }
    if message != "" {
      issues.push(record([("path", String(path)), ("reason", String(message))]))
    }
  }
  issues
}

///|
fn describe_layers(layers : Array[Value]) -> Value raise {
  Array(
    layers.map(layer => {
      let fields = [("name", get(layer, "name"))]
      if get(layer, "source") != Null {
        fields.push(("source", String(str(get(layer, "source")))))
      }
      record(fields)
    }),
  )
}

///|
fn resolution_of(request : Value, config : Value) -> Resolution raise {
  let resolution : Resolution = {
    root: config,
    env: if get(request, "env") == Null {
      Map([])
    } else {
      obj(get(request, "env"))
    },
    secret_paths: optional_strings(get(request, "sensitive")),
    secret_keys: optional_strings(get(request, "sensitive_keys")),
    secret_env: optional_strings(get(request, "secret_env")),
    visiting: Map([]),
    resolved: Map([]),
    tainted: Map([]),
  }
  for path in resolution.secret_paths {
    ignore(segments(path))
  }
  if resolution.secret_keys.contains("") {
    raise InputError("sensitive key names must not be empty")
  }
  resolution
}

///|
/// Derives a constraint set from the resolved values observed at each leaf
/// path after every layer: the consistent observed type, an enum for small
/// string sets, numeric bounds from the observations, and required for
/// every surviving path. Masked paths are never inferred, so no secret
/// value can leak into a constraint.
fn infer_report(request : Value) -> Value raise {
  let max_enum = optional_num(get(request, "max_enum"), 8.0).to_int()
  if max_enum < 1 || max_enum > 64 {
    raise InputError("max_enum must be between 1 and 64")
  }
  let arrays = array_strategy(request)
  let observations : Map[String, Array[Value]] = Map([])
  let masked : Array[String] = []
  let mut config = Object(Map([]))
  let names : Map[String, Bool] = Map([])
  for layer in arr(get(request, "layers")) {
    let source = str(get(layer, "name"))
    if names.contains(source) {
      raise InputError("duplicate layer name: " + source)
    }
    names[source] = true
    let value = get(layer, "value")
    ignore(obj(value))
    config = merge(
      config,
      value,
      "",
      source,
      arrays,
      truth(get(request, "strict_types")),
      Map([]),
      [],
      0,
    )
    let resolution = resolution_of(request, config)
    let resolved = resolution.resolve("", 0)
    for pointer, leaf in resolved.flatten() {
      if resolution.sensitive(pointer) {
        continue
      }
      let seen = observations.get(pointer).unwrap_or([])
      seen.push(leaf)
      observations[pointer] = seen
    }
  }
  ignore(masked)
  let paths = observations.keys().collect()
  sort_lexical(paths)
  let constraints : Array[Value] = []
  for path in paths {
    let seen = observations[path]
    let fields = [("path", String(path)), ("required", Bool(true))]
    let kinds : Array[String] = []
    for leaf in seen {
      if leaf != Null && !kinds.contains(kind(leaf)) {
        kinds.push(kind(leaf))
      }
    }
    if kinds.length() == 1 {
      fields.push(("type", String(kinds[0])))
      if kinds[0] == "number" {
        let mut low = seen[0]
        let mut high = seen[0]
        for leaf in seen {
          match leaf {
            Number(n) => {
              low = Number(if num(low) > n { n } else { num(low) })
              high = Number(if num(high) < n { n } else { num(high) })
            }
            _ => ()
          }
        }
        fields.push(("min", low))
        fields.push(("max", high))
      }
      if kinds[0] == "string" {
        let values : Array[String] = []
        for leaf in seen {
          match leaf {
            String(text) => if !values.contains(text) { values.push(text) }
            _ => ()
          }
        }
        if values.length() >= 1 && values.length() <= max_enum {
          sort_lexical(values)
          fields.push(("enum", strings(values)))
        }
      }
    }
    constraints.push(record(fields))
  }
  // The inferred set must accept every observed configuration.
  let checked = match request {
    Object(fields_request) => {
      let copy = fields_request.copy()
      copy["constraints"] = Array(constraints)
      merge_report(Object(copy))
    }
    other => other
  }
  record([
    ("constraints", Array(constraints)),
    ("constraint_count", Number(constraints.length().to_double())),
    ("path_count", Number(observations.length().to_double())),
    ("issues", get(checked, "issues")),
    ("valid", get(checked, "valid")),
  ])
}

///|
fn merge_report(request : Value) -> Value raise {
  let arrays = array_strategy(request)
  let origins : Map[String, Array[String]] = Map([])
  let shadowed : Array[(String, String, String)] = []
  let mut config = Object(Map([]))
  let names : Map[String, Bool] = Map([])
  let layers = arr(get(request, "layers"))
  for layer in layers {
    let source = str(get(layer, "name"))
    if names.contains(source) {
      raise InputError("duplicate layer name: " + source)
    }
    names[source] = true
    let value = get(layer, "value")
    ignore(obj(value))
    config = merge(
      config,
      value,
      "",
      source,
      arrays,
      truth(get(request, "strict_types")),
      origins,
      shadowed,
      0,
    )
  }
  let resolution = resolution_of(request, config)
  let resolved = resolution.resolve("", 0)
  let constraints = if get(request, "constraints") == Null {
    []
  } else {
    arr(get(request, "constraints"))
  }
  let issues = check_constraints(resolved, constraints)
  let masked : Array[String] = []
  let redacted = resolution.redact(resolved, "", masked)
  sort_lexical(masked)
  let history = origins.map((_, sources) => strings(sources))
  shadowed.sort_by((a, b) => {
    String::lexical_compare(a.0 + "\u{0}" + a.1, b.0 + "\u{0}" + b.1)
  })
  record([
    ("config", if truth(get(request, "reveal")) { resolved } else { redacted }),
    ("redacted", redacted),
    ("masked", strings(masked)),
    (
      "shadowed",
      Array(
        shadowed.map(entry => {
          record([
            ("path", String(entry.0)),
            ("layer", String(entry.1)),
            ("by", String(entry.2)),
          ])
        }),
      ),
    ),
    ("origins", Object(history)),
    ("layers", describe_layers(layers)),
    ("issues", Array(issues)),
    ("valid", Bool(issues.is_empty())),
  ])
}

///|
let formats : Array[String] = ["json", "flat", "dotenv"]

///|
/// dotenv key for a pointer: segments upper-cased, anything outside ASCII
/// letters and digits becomes "_", segments joined with "_".
fn dotenv_key(pointer : String) -> String raise {
  segments(pointer)
  .map(segment => {
    segment
    .to_upper()
    .iter()
    .map(c => {
      if c.is_ascii_alphabetic() || c.is_ascii_digit() {
        c
      } else {
        '_'
      }
    })
    .collect()
    |> String::from_array
  })
  .join("_")
}

///|
/// A dotenv value is written bare when it is a plain string without
/// whitespace, quotes, "#" or "=", and JSON-quoted otherwise; non-string
/// leaves use their JSON form.
fn dotenv_value(value : Value) -> String {
  match value {
    String(text) =>
      if text != "" &&
        !text
        .iter()
        .any(c => {
          c == ' ' ||
          c == '\t' ||
          c == '\n' ||
          c == '\r' ||
          c == '"' ||
          c == '\'' ||
          c == '#' ||
          c == '='
        }) {
        text
      } else {
        value.stringify()
      }
    other => canonical(other)
  }
}

///|
fn render_export(config : Value, format : String) -> String raise {
  match format {
    "json" => canonical(config)
    "flat" => canonical(Object(config.flatten()))
    _ => {
      let flat = config.flatten()
      let keys : Map[String, String] = Map([])
      let lines : Array[String] = []
      for pointer, _ in flat {
        let key = dotenv_key(pointer)
        match keys.get(key) {
          Some(other) =>
            raise InputError(
              "dotenv key collision: " +
              key +
              " from " +
              other +
              " and " +
              pointer,
            )
          None => keys[key] = pointer
        }
      }
      let names = keys.keys().collect()
      sort_lexical(names)
      for name in names {
        lines.push(name + "=" + dotenv_value(flat[keys[name]]))
      }
      lines.join("\n") + "\n"
    }
  }
}

///|
fn export_report(request : Value) -> Value raise {
  let format = optional_str(get(request, "format"), "json")
  if !formats.contains(format) {
    raise InputError("unknown export format: " + format)
  }
  let report = merge_report(request)
  record([
    ("text", String(render_export(get(report, "config"), format))),
    ("format", String(format)),
    ("redacted", Bool(!truth(get(request, "reveal")))),
    ("layers", get(report, "layers")),
    ("issues", get(report, "issues")),
    ("valid", get(report, "valid")),
  ])
}

///|
fn diff_report(request : Value) -> Value raise {
  let before = merge_report(get(request, "old"))
  let after = merge_report(get(request, "new"))
  let ignored = optional_strings(get(request, "ignore")).map(canonical_pointer)
  let changes : Array[Value] = []
  config_diff(
    get(before, "redacted"),
    get(after, "redacted"),
    "",
    changes,
    get(before, "origins"),
    get(after, "origins"),
  )
  // Hidden values compare equal, so an override of a secret is reported
  // through its origin chain instead.
  let before_masked = string_list(get(before, "masked"))
  let after_masked = string_list(get(after, "masked"))
  for path in before_masked {
    if after_masked.contains(path) {
      let previous = get(get(before, "origins"), path)
      let current = get(get(after, "origins"), path)
      if previous != current {
        changes.push(
          record([
            ("path", String(path)),
            ("type", String("masked_override")),
            ("before_origins", previous),
            ("after_origins", current),
          ]),
        )
      }
    }
  }
  // Ignored subtrees are dropped after the walk so masked overrides are filtered too.
  let kept = changes.filter(change => {
    !ignored.any(parent => {
      under(str(get(change, "path")) catch { _ => "" }, parent)
    })
  })
  record([
    ("changes", Array(kept)),
    ("equal", Bool(kept.is_empty())),
    ("ignored", strings(ignored)),
    ("redacted", Bool(true)),
  ])
}

///|
pub fn run(request : Value) -> Value raise {
  match get(request, "operation") {
    String("audit") => audit_report(request)
    Null | String("merge") => merge_report(request)
    String("diff") => diff_report(request)
    String("export") => export_report(request)
    String("infer") => infer_report(request)
    String(other) => raise InputError("unknown operation: " + other)
    _ => raise InputError("operation must be a string")
  }
}

///|
fn change(
  path : String,
  kind : String,
  values : Array[(String, Value)],
  before_origins : Value,
  after_origins : Value,
) -> Value {
  let fields = [("path", String(path)), ("type", String(kind))]
  for pair in values {
    fields.push(pair)
  }
  if get(before_origins, path) != Null {
    fields.push(("before_origins", get(before_origins, path)))
  }
  if get(after_origins, path) != Null {
    fields.push(("after_origins", get(after_origins, path)))
  }
  record(fields)
}

///|
fn config_diff(
  before : Value,
  after : Value,
  path : String,
  changes : Array[Value],
  before_origins : Value,
  after_origins : Value,
) -> Unit {
  if before == after {
    return
  }
  match (before, after) {
    (Object(a), Object(b)) => {
      let keys : Map[String, Bool] = Map([])
      for key, _ in a {
        keys[key] = true
      }
      for key, _ in b {
        keys[key] = true
      }
      let sorted = keys.keys().collect()
      sort_lexical(sorted)
      for key in sorted {
        let next = path + "/" + escape_key(key)
        if !a.contains(key) {
          changes.push(
            change(
              next,
              "added",
              [("after", b[key])],
              before_origins,
              after_origins,
            ),
          )
        } else if !b.contains(key) {
          changes.push(
            change(
              next,
              "removed",
              [("before", a[key])],
              before_origins,
              after_origins,
            ),
          )
        } else {
          config_diff(
            a[key],
            b[key],
            next,
            changes,
            before_origins,
            after_origins,
          )
        }
      }
    }
    _ =>
      changes.push(
        change(
          path,
          "changed",
          [("before", before), ("after", after)],
          before_origins,
          after_origins,
        ),
      )
  }
}