///|
/// The buffer handed to the native updater for its failure message.
///
/// The native side truncates rather than growing, so this is not a two-pass
/// query like the rest of the ABI: these messages are fixed sentences written
/// in `proton_update.c`, not paths or payloads, and none of them approaches
/// this length.
let update_message_capacity = 512

///|
#borrow(parent_dir, out_stage, error)
extern "C" fn proton_update_stage_begin_ffi(
  parent_dir : Bytes,
  expected_size : Int64,
  target_revision : Int64,
  out_stage : Ref[Int64],
  error : FixedArray[Byte],
  error_len : Int,
) -> Int = "proton_update_stage_begin_revision"

///|
#borrow(chunk, error)
extern "C" fn proton_update_stage_write_ffi(
  stage : Int64,
  chunk : Bytes,
  chunk_len : Int,
  error : FixedArray[Byte],
  error_len : Int,
) -> Int = "proton_update_stage_write"

///|
#borrow(out_outcome, error)
extern "C" fn proton_update_stage_install_ffi(
  stage : Int64,
  out_outcome : Ref[Int],
  error : FixedArray[Byte],
  error_len : Int,
) -> Int = "proton_update_stage_install_outcome"

///|
#borrow(out_revision, error)
extern "C" fn proton_update_current_revision_ffi(
  out_revision : Ref[Int64],
  error : FixedArray[Byte],
  error_len : Int,
) -> Int = "proton_update_current_revision"

///|
#borrow(error)
extern "C" fn proton_update_cleanup_previous_ffi(
  error : FixedArray[Byte],
  error_len : Int,
) -> Int = "proton_update_cleanup_previous"

///|
#borrow(error)
extern "C" fn proton_update_stage_abort_ffi(
  stage : Int64,
  error : FixedArray[Byte],
  error_len : Int,
) -> Int = "proton_update_stage_abort"

///|
#borrow(archive, parent_dir, error)
extern "C" fn proton_update_install_ffi(
  archive : Bytes,
  archive_len : Int,
  parent_dir : Bytes,
  error : FixedArray[Byte],
  error_len : Int,
) -> Int = "proton_update_install"

///|
#borrow(error)
extern "C" fn proton_update_relaunch_ffi(
  error : FixedArray[Byte],
  error_len : Int,
) -> Int = "proton_update_relaunch"

///|
/// A private native file that receives one artifact stream while it is checked.
///
/// The file path is deliberately not exposed. The native updater owns it from
/// creation through expansion, which keeps verification and installation tied
/// to the same bytes.
struct UpdateStage {
  handle : Int64
}

///|
/// What consuming an authenticated update stage changed.
pub enum UpdateInstallOutcome {
  Installed
  AlreadyInstalled
} derive(Debug, Eq)

///|
/// Reads a C string out of a fixed buffer the native side wrote into.
///
/// The buffer is sized for the longest value the native side may produce, so
/// the terminator is what says where the value ends. Decoding the whole buffer
/// would carry its padding into the string.
fn update_text(buffer : FixedArray[Byte]) -> String {
  // The native side writes through snprintf, so a terminator is always within
  // the buffer; stopping one short of the end keeps the slice in bounds even
  // if that ever stopped being true.
  let mut length = 0
  while length + 1 < buffer.length() && buffer[length] != b'\x00' {
    length = length + 1
  }
  if length == 0 {
    return ""
  }
  @ffi.from_cstr(Bytes::from_array(buffer[0:length + 1]))
}

///|
/// Turns a native updater status into a result.
///
/// The updater reports through a caller-supplied buffer rather than
/// `proton_last_error_message`, because it is compiled without the runtime
/// state it would otherwise depend on — that is what lets the replacement
/// logic be tested against throwaway directories instead of a real install.
fn update_check(
  status : Int,
  message : FixedArray[Byte],
) -> Unit raise NativeError {
  if status >= 0 {
    return
  }
  raise Status(status~, message=update_text(message))
}

///|
/// Starts a private update staging transaction in an explicit directory.
///
/// Application updates should use `update_stage_begin_for_current_app` so the
/// stage and installed bundle are guaranteed to be on the same filesystem.
pub fn update_stage_begin(
  parent_dir : String,
  expected_size : Int64,
  target_revision : UInt64,
) -> UpdateStage raise NativeError {
  let message = FixedArray::make(update_message_capacity, b'\x00')
  let handle = Ref(0L)
  update_check(
    proton_update_stage_begin_ffi(
      @ffi.to_cstr(parent_dir),
      expected_size,
      target_revision.reinterpret_as_int64(),
      handle,
      message,
      message.length(),
    ),
    message,
  )
  { handle: handle.val }
}

///|
/// Starts a private update stage beside the running application bundle.
///
/// Keeping the stage on the installed application's filesystem makes the
/// final bundle replacement an atomic rename even when the application runs
/// from an external volume.
pub fn update_stage_begin_for_current_app(
  expected_size : Int64,
  target_revision : UInt64,
) -> UpdateStage raise NativeError {
  update_stage_begin("", expected_size, target_revision)
}

///|
/// Reads the installed application's monotonic update revision.
///
/// This is an optimistic check for process-local coordination. Installation
/// repeats it while holding the native cross-process commit lock.
pub fn update_current_revision() -> UInt64 raise NativeError {
  let message = FixedArray::make(update_message_capacity, b'\x00')
  let revision = Ref(0L)
  update_check(
    proton_update_current_revision_ffi(revision, message, message.length()),
    message,
  )
  revision.val.reinterpret_as_uint64()
}

///|
/// Removes older bundles retained by completed application updates.
///
/// Call this only after the replacement has completed application startup.
/// Native code restricts deletion to Proton-reserved sibling names with the
/// same signing identity and an older update revision.
pub fn update_cleanup_previous() -> Unit raise NativeError {
  let message = FixedArray::make(update_message_capacity, b'\x00')
  update_check(
    proton_update_cleanup_previous_ffi(message, message.length()),
    message,
  )
}

///|
/// Appends one downloaded chunk to a private update stage.
pub fn UpdateStage::write(
  self : UpdateStage,
  chunk : Bytes,
) -> Unit raise NativeError {
  let message = FixedArray::make(update_message_capacity, b'\x00')
  update_check(
    proton_update_stage_write_ffi(
      self.handle,
      chunk,
      chunk.length(),
      message,
      message.length(),
    ),
    message,
  )
}

///|
/// Consumes a complete stage and installs the application it contains.
pub fn UpdateStage::install(
  self : UpdateStage,
) -> UpdateInstallOutcome raise NativeError {
  let message = FixedArray::make(update_message_capacity, b'\x00')
  let outcome = Ref(0)
  update_check(
    proton_update_stage_install_ffi(
      self.handle,
      outcome,
      message,
      message.length(),
    ),
    message,
  )
  match outcome.val {
    0 => Installed
    1 => AlreadyInstalled
    value =>
      raise InvalidPayload(
        context="update install outcome",
        message="unknown native value \{value}",
      )
  }
}

///|
/// Discards an unfinished stage and all bytes written to it.
pub fn UpdateStage::abort(self : UpdateStage) -> Unit raise NativeError {
  let message = FixedArray::make(update_message_capacity, b'\x00')
  update_check(
    proton_update_stage_abort_ffi(self.handle, message, message.length()),
    message,
  )
}

///|
/// Installs an authenticated update archive over the running application.
///
/// Expansion, bundle signature validation, and replacement form one native
/// transaction. The expanded bundle path never crosses the FFI boundary, so
/// another task cannot replace the validated bundle before installation.
pub fn update_install(
  archive : Bytes,
  parent_dir : String,
) -> Unit raise NativeError {
  let message = FixedArray::make(update_message_capacity, b'\x00')
  update_check(
    proton_update_install_ffi(
      archive,
      archive.length(),
      @ffi.to_cstr(parent_dir),
      message,
      message.length(),
    ),
    message,
  )
}

///|
/// Starts the replaced application.
///
/// The caller is expected to exit afterwards: two copies of the same
/// application running against the same state is worse than a moment with
/// none.
pub fn update_relaunch() -> Unit raise NativeError {
  let message = FixedArray::make(update_message_capacity, b'\x00')
  update_check(proton_update_relaunch_ffi(message, message.length()), message)
}