///|
pub enum InstallOutcome {
  Installed
  AlreadyInstalled
} derive(Debug, Eq)

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

///|
pub extend InstallOutcome with Debug::{to_repr}

///|
/// An authenticated artifact retained until installation or explicit discard.
pub struct PreparedUpdate {
  priv mut stage : @native.UpdateStage?
  priv revision : UInt64
}

///|
/// Releases an unused download. Repeated calls are harmless.
pub fn PreparedUpdate::discard(self : PreparedUpdate) -> Unit raise UpdateError {
  if self.stage is Some(stage) {
    stage.abort() catch {
      error =>
        raise InstallFailed(
          step="discard downloaded update",
          detail=error.message(),
        )
    }
    self.stage = None
  }
}

///|
/// Consumes the download, applying the replacement or preparing its installer.
/// Application callers use `ApplicationContext::quit_and_install` so this runs
/// only after application cleanup. The native commit lock rechecks revision.
pub fn PreparedUpdate::install(
  self : PreparedUpdate,
) -> InstallOutcome raise UpdateError {
  guard self.stage is Some(stage) else {
    raise InstallFailed(
      step="install downloaded update",
      detail="the download has already been consumed",
    )
  }
  self.stage = None
  let outcome = stage.install() catch {
    error => {
      if error.is_update_busy() {
        raise InstallBusy
      }
      if error.is_update_rollback() {
        raise RollbackRejected(
          target=self.revision,
          installed=current_revision(),
        )
      }
      if error.is_update_revision_mismatch() {
        raise RevisionMismatch(detail=error.message())
      }
      raise InstallFailed(
        step="validate and replace the application",
        detail=error.message(),
      )
    }
  }
  match outcome {
    @native.Installed => Installed
    @native.AlreadyInstalled => AlreadyInstalled
  }
}

///|
/// Downloads and authenticates an artifact without changing the installation.
/// The caller owns the returned stage and must install or discard it.
pub async fn UpdateChannel::download(
  self : UpdateChannel,
  fetch : ArtifactFetch,
  update : AvailableUpdate,
  on_progress? : async (Int64) -> Unit noraise = _ => (),
) -> PreparedUpdate raise UpdateError {
  let installed = current_revision()
  if update.revision < installed {
    raise RollbackRejected(target=update.revision, installed~)
  }
  let stage = @native.update_stage_begin_for_current_app(
    update.size,
    update.revision,
  ) catch {
    error =>
      raise InstallFailed(
        step="prepare a private artifact stage",
        detail=error.message(),
      )
  }
  let prepared = PreparedUpdate::{
    stage: Some(stage),
    revision: update.revision,
  }
  let transferred = Ref(false)
  defer (if !transferred.val { prepared.discard() catch { _ => () } })
  self.download_into(
    fetch,
    update,
    fn(chunk) -> Unit raise UpdateError {
      stage.write(chunk) catch {
        error =>
          raise InstallFailed(
            step="write the private artifact stage",
            detail=error.message(),
          )
      }
    },
    on_progress,
  )
  transferred.val = true
  prepared
}

///|
/// Starts the installed replacement.
///
/// The caller is expected to exit once this returns. Two copies of the same
/// application running against the same state is a worse outcome than a moment
/// with none, and the new process is already starting by the time this
/// returns.
pub fn relaunch() -> Unit raise UpdateError {
  @native.update_relaunch() catch {
    error =>
      raise InstallFailed(
        step="start the new application",
        detail=error.message(),
      )
  }
}

///|
/// Returns the revision recorded by the running application artifact.
pub fn current_revision() -> UInt64 raise UpdateError {
  @native.update_current_revision() catch {
    error =>
      raise InstallFailed(
        step="read the installed update revision",
        detail=error.message(),
      )
  }
}

///|
/// Removes the retained backup from a successfully relaunched application.
pub fn cleanup_previous() -> Unit raise UpdateError {
  @native.update_cleanup_previous() catch {
    error =>
      raise InstallFailed(
        step="clean up the previous application artifact",
        detail=error.message(),
      )
  }
}