///|
/// A small immutable patch language for configuration and event documents.
pub(all) enum CborPatch {
  Set(Array[CborPathSegment], CborValue)
  Remove(Array[CborPathSegment])
  Merge(Array[CborPathSegment], CborValue)
} derive(Eq, Debug)

///|
/// Create a replacement patch.
pub fn cbor_patch_set(
  path : Array[CborPathSegment],
  value : CborValue,
) -> CborPatch {
  Set(path, value)
}

///|
/// Create a removal patch.
pub fn cbor_patch_remove(path : Array[CborPathSegment]) -> CborPatch {
  Remove(path)
}

///|
/// Create an object merge patch.
pub fn cbor_patch_merge(
  path : Array[CborPathSegment],
  value : CborValue,
) -> CborPatch {
  Merge(path, value)
}

///|
/// Return the path targeted by a patch.
pub fn cbor_patch_path(patch : CborPatch) -> Array[CborPathSegment] {
  match patch {
    Set(path, _) | Remove(path) | Merge(path, _) => path
  }
}

///|
/// Return a short operation name for metrics and audit logs.
pub fn cbor_patch_operation(patch : CborPatch) -> String {
  match patch {
    Set(_, _) => "set"
    Remove(_) => "remove"
    Merge(_, _) => "merge"
  }
}

///|
fn remove_path(
  value : CborValue,
  path : Array[CborPathSegment],
) -> CborValue raise CborError {
  if path.length() == 0 {
    raise SemanticError("patch cannot remove the document root")
  }
  let head = path[0]
  let tail = path[1:].to_owned()
  if tail.length() == 0 {
    match head {
      Key(name) => cbor_object_remove(value, name)
      Index(index) =>
        match value {
          Array(items) => {
            if index < 0 || index >= items.length() {
              raise SemanticError("patch array index is out of range")
            }
            let result = []
            for i = 0; i < items.length(); i = i + 1 {
              if i != index {
                result.push(items[i])
              }
            }
            Array(result)
          }
          _ => raise SemanticError("patch remove requires an array")
        }
    }
  } else {
    match head {
      Key(name) => {
        let child = cbor_query_at(value, [Key(name)])
        let updated = remove_path(child, tail)
        cbor_object_set(value, name, updated)
      }
      Index(index) =>
        match value {
          Array(items) => {
            if index < 0 || index >= items.length() {
              raise SemanticError("patch array index is out of range")
            }
            let updated = remove_path(items[index], tail)
            let result = []
            for item in items {
              result.push(item)
            }
            result[index] = updated
            Array(result)
          }
          _ => raise SemanticError("patch remove requires an array")
        }
    }
  }
}

///|
fn apply_one_patch(
  value : CborValue,
  patch : CborPatch,
) -> CborValue raise CborError {
  let path = cbor_patch_path(patch)
  if path.length() == 0 {
    match patch {
      Set(_, replacement) => replacement
      Remove(_) => raise SemanticError("patch cannot remove the document root")
      Merge(_, overlay) => cbor_object_merge(value, overlay)
    }
  } else {
    match patch {
      Set(_, replacement) =>
        if path.length() == 1 {
          match path[0] {
            Key(name) => cbor_object_set(value, name, replacement)
            Index(_) => cbor_set_path(value, path, replacement)
          }
        } else {
          cbor_set_path(value, path, replacement)
        }
      Remove(_) => remove_path(value, path)
      Merge(_, overlay) => {
        let selected = cbor_query_at(value, path)
        let merged = cbor_object_merge(selected, overlay)
        cbor_set_path(value, path, merged)
      }
    }
  }
}

///|
/// Apply patches in order, returning a new document.
pub fn cbor_apply_patches(
  value : CborValue,
  patches : Array[CborPatch],
) -> CborValue raise CborError {
  let mut result = value
  for patch in patches {
    result = apply_one_patch(result, patch)
  }
  result
}

///|
/// Apply exactly one patch and return its operation label with the result.
pub fn cbor_apply_patch_with_label(
  value : CborValue,
  patch : CborPatch,
) -> (String, CborValue) raise CborError {
  let operation = cbor_patch_operation(patch)
  (operation, apply_one_patch(value, patch))
}

///|
/// Construct patches for changed or newly added top-level object fields.
pub fn cbor_diff_objects(
  before : CborValue,
  after : CborValue,
) -> Array[CborPatch] raise CborError {
  match (before, after) {
    (Map(before_entries), Map(after_entries)) => {
      let patches = []
      for entry in after_entries {
        match entry.0 {
          Text(name) =>
            match map_value(before, name) {
              Some(old) =>
                if old != entry.1 {
                  patches.push(Set([Key(name)], entry.1))
                }
              None => patches.push(Set([Key(name)], entry.1))
            }
          _ => raise SemanticError("patch diff requires text object keys")
        }
      }
      for entry in before_entries {
        match entry.0 {
          Text(name) =>
            if !cbor_object_has(after, name) {
              patches.push(Remove([Key(name)]))
            }
          _ => raise SemanticError("patch diff requires text object keys")
        }
      }
      patches
    }
    _ => raise SemanticError("patch diff requires two objects")
  }
}

///|
/// Return a deterministic audit line for a patch batch.
pub fn cbor_patch_audit_line(patches : Array[CborPatch]) -> String {
  let builder = StringBuilder::new()
  builder.write_string("patches=")
  builder.write_string(patches.length().to_string())
  builder.write_string(" operations=")
  for i = 0; i < patches.length(); i = i + 1 {
    if i > 0 {
      builder.write_string(",")
    }
    builder.write_string(cbor_patch_operation(patches[i]))
  }
  builder.to_string()
}

///|
/// Return a patch list that removes every top-level key in an object.
pub fn cbor_clear_object(value : CborValue) -> Array[CborPatch] raise CborError {
  match value {
    Map(entries) => {
      let patches = []
      for entry in entries {
        match entry.0 {
          Text(name) => patches.push(Remove([Key(name)]))
          _ => raise SemanticError("patch clear requires text object keys")
        }
      }
      patches
    }
    _ => raise SemanticError("patch clear requires an object")
  }
}

///|
/// Return true when applying a patch does not change the encoded document.
pub fn cbor_patch_is_noop(
  value : CborValue,
  patch : CborPatch,
) -> Bool raise CborError {
  encode(value) == encode(apply_one_patch(value, patch))
}

///|
/// Apply a batch and reject documents that exceed a node budget.
pub fn cbor_apply_patches_bounded(
  value : CborValue,
  patches : Array[CborPatch],
  max_nodes : Int,
) -> CborValue raise CborError {
  let result = cbor_apply_patches(value, patches)
  if cbor_value_node_count(result) > max_nodes {
    raise SemanticError("patch result exceeds node budget")
  }
  result
}