///|
/// A value-free description of a change between two resolved configurations.
/// `path` contains literal object keys, so a key containing a period is not
/// confused with a nested path. Lists are compared as one value.
pub(all) struct ConfigChange {
  path : Array[String]
  kind : String
  before_type : String?
  after_type : String?
} derive(Debug, Eq, ToJson)

///|
pub extend ConfigChange with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend ConfigChange with Eq::{not_equal, equal}

///|
pub extend ConfigChange with ToJson::{to_json}

///|
fn push_config_change(
  output : Array[ConfigChange],
  path : Array[String],
  kind : String,
  before_type : String?,
  after_type : String?,
) -> Unit raise ParseError {
  if output.length() >= 4096 {
    raise Invalid("configuration diff exceeds 4096 changes")
  }
  output.push({ path: path.copy(), kind, before_type, after_type, })
}

///|
fn compare_config_trees(
  before : Value,
  after : Value,
  path : Array[String],
  output : Array[ConfigChange],
  depth : Int,
  work : Work,
) -> Unit raise ParseError {
  tree_step(before, depth, work)
  tree_step(after, depth, work)
  match (before, after) {
    (Object(old), Object(new)) => {
      let keys = old.keys().collect()
      for key in new.keys() {
        if !old.contains(key) {
          keys.push(key)
        }
      }
      keys.sort_by(java_key_compare)
      for key in keys {
        work.spend(key.length())
        if path.length() >= 32 {
          raise Invalid("configuration diff path depth limit")
        }
        let child_path = path.copy()
        child_path.push(key)
        match (old.get(key), new.get(key)) {
          (Some(a), Some(b)) =>
            compare_config_trees(a, b, child_path, output, depth + 1, work)
          (Some(a), None) =>
            push_config_change(
              output,
              child_path,
              "removed",
              Some(value_type(a)),
              None,
            )
          (None, Some(b)) =>
            push_config_change(
              output,
              child_path,
              "added",
              None,
              Some(value_type(b)),
            )
          _ => ()
        }
      }
    }
    _ =>
      if !value_equals(before, after) {
        push_config_change(
          output,
          path,
          "changed",
          Some(value_type(before)),
          Some(value_type(after)),
        )
      }
  }
}

///|
/// Compare two already resolved HOCON object roots without disclosing values.
/// Additions and removals are reported at their nearest object key; lists are
/// atomic. The caller decides whether each change is allowed for its migration.
/// Rejects unresolved values and bounds work, nesting, paths and report size.
pub fn diff_resolved_configs(
  before : Value,
  after : Value,
) -> Array[ConfigChange] raise ParseError {
  let old = value_unwrapped(before)
  let new = value_unwrapped(after)
  ignore(object_fields(old))
  ignore(object_fields(new))
  let output = []
  compare_config_trees(old, new, [], output, 0, tree_work())
  output
}