///|
/// A successful transaction and a guarded RFC 6902 patch that reverses it.
/// The rollback's first operation tests the entire result, so it refuses to
/// overwrite a document that has changed since this receipt was produced.
pub(all) struct PatchReceipt {
  value : Json
  rollback : Json
  paths : Array[String]
}

///|
fn receipt_op(name : String, path : String) -> Json {
  Json::object(Map([("op", Json::string(name)), ("path", Json::string(path))]))
}

///|
fn receipt_value_op(name : String, path : String, value : Json) -> Json {
  Json::object(
    Map([
      ("op", Json::string(name)),
      ("path", Json::string(path)),
      ("value", value),
    ]),
  )
}

///|
fn receipt_parent(tokens : Array[String]) -> Array[String] {
  let parent : Array[String] = []
  for i = 0; i + 1 < tokens.length(); i = i + 1 {
    parent.push(tokens[i])
  }
  parent
}

///|
fn receipt_common_parent(
  source : Array[String],
  destination : Array[String],
) -> Array[String] {
  let common : Array[String] = []
  let length = Int::min(source.length(), destination.length())
  for i = 0; i < length; i = i + 1 {
    if source[i] != destination[i] {
      break
    }
    common.push(source[i])
  }
  common
}

///|
/// Generate one inverse operation for one already validated forward step.
/// Move restores its smallest shared ancestor: reversing two edits to the
/// same array using only their original indexes is not generally correct.
fn receipt_inverse(
  before : Json,
  operation : Map[String, Json],
  index : Int,
) -> Json? raise PatchError {
  let name = required_string(operation, "op", index)
  let path = required_string(operation, "path", index)
  let tokens = parse_pointer(path)
  match name {
    "test" => None
    "replace" =>
      Some(
        receipt_value_op(
          "replace",
          path,
          get_tokens(before, tokens, index, path),
        ),
      )
    "remove" =>
      Some(
        receipt_value_op("add", path, get_tokens(before, tokens, index, path)),
      )
    "add" | "copy" => {
      if tokens.length() == 0 {
        return Some(receipt_value_op("replace", "", before))
      }
      let parent_tokens = receipt_parent(tokens)
      let parent_path = format_pointer(parent_tokens)
      let parent = get_tokens(before, parent_tokens, index, parent_path)
      match parent {
        Object(fields) =>
          match fields.get(tokens[tokens.length() - 1]) {
            Some(previous) => Some(receipt_value_op("replace", path, previous))
            None => Some(receipt_op("remove", path))
          }
        Array(items) => {
          let last = tokens[tokens.length() - 1]
          let inverse_path = if last == "-" {
            parent_path + "/" + items.length().to_string()
          } else {
            path
          }
          Some(receipt_op("remove", inverse_path))
        }
        _ => patch_fail("missing_path", index, path, "path traverses a scalar")
      }
    }
    "move" => {
      let source = required_string(operation, "from", index)
      let source_tokens = parse_pointer(source)
      if source_tokens == tokens {
        return None
      }
      let common = receipt_common_parent(source_tokens, tokens)
      let common_path = format_pointer(common)
      Some(
        receipt_value_op(
          "replace",
          common_path,
          get_tokens(before, common, index, common_path),
        ),
      )
    }
    _ => patch_fail("invalid_patch", index, path, "unknown operation")
  }
}

///|
/// Apply an RFC 6902 patch and create an operation-aware rollback receipt.
/// The receipt supports at most 4095 input operations because its rollback
/// needs one additional guard operation within the 4096-operation limit.
pub fn apply_with_receipt(
  document : Json,
  patch : Json,
) -> PatchReceipt raise PatchError {
  guard patch is Array(operations) else {
    patch_fail("invalid_patch", -1, "", "patch must be an array")
  }
  if operations.length() > 4095 {
    patch_fail("limit_exceeded", -1, "", "receipt exceeds 4095 operations")
  }
  let mut current = document
  let inverses : Array[Json] = []
  let paths : Array[String] = []
  for i = 0; i < operations.length(); i = i + 1 {
    guard operations[i] is Object(fields) else {
      patch_fail("invalid_patch", i, "", "operation must be an object")
    }
    let name = required_string(fields, "op", i)
    let path = required_string(fields, "path", i)
    let single = Json::array([operations[i]])
    let next = apply(current, single) catch {
      PatchError(diagnostic) =>
        patch_fail(diagnostic.code, i, diagnostic.path, diagnostic.message)
    }
    match receipt_inverse(current, fields, i) {
      Some(inverse) => inverses.push(inverse)
      None => ()
    }
    if name != "test" {
      paths.push(path)
      if name == "move" {
        paths.push(required_string(fields, "from", i))
      }
    }
    current = next
  }
  let rollback : Array[Json] = [receipt_value_op("test", "", current)]
  for i = inverses.length(); i > 0; i = i - 1 {
    rollback.push(inverses[i - 1])
  }
  { value: current, rollback: Json::array(rollback), paths, }
}

///|
/// Serialize a receipt for CLI users without introducing a second patch
/// format. `rollback` is an ordinary RFC 6902 patch for `apply`.
pub fn receipt_json(receipt : PatchReceipt) -> Json {
  let paths : Array[Json] = []
  for path in receipt.paths {
    paths.push(Json::string(path))
  }
  Json::object(
    Map([
      ("value", receipt.value),
      ("rollback", receipt.rollback),
      ("paths", Json::array(paths)),
    ]),
  )
}

///|
pub fn apply_with_receipt_json(
  document_text : String,
  patch_text : String,
) -> Json raise PatchError {
  let document = @json.parse(document_text) catch {
    _ => patch_fail("invalid_json", -1, "", "document is not valid JSON")
  }
  let patch = @json.parse(patch_text) catch {
    _ => patch_fail("invalid_json", -1, "", "patch is not valid JSON")
  }
  receipt_json(apply_with_receipt(document, patch))
}