///|
/// How long after launch the automatic check waits before contacting the
/// endpoint.
///
/// Long enough that the check is not competing with the window coming up, and
/// short enough that a user who quits quickly still eventually sees an update
/// on some later run.
let update_check_delay_ms : Int = 20 * 1000

///|
/// The width of the random spread added to that delay.
///
/// Without it, every installation of a popular application arrives at the
/// endpoint at the same moment after a release, which is a load pattern the
/// publisher did not ask for and cannot smooth from their side.
let update_check_jitter_ms : Int = 40 * 1000

///|
/// The channel this process resolved from `proton.project.json`, once it is running.
///
/// Process-global because the update channel is a property of the installed
/// application, not of a window or a request. The renderer-facing extension
/// reads it through `check_for_update`, which is the only way it can: a page
/// must never be able to name an endpoint.
let active_update_channel : Ref[ResolvedUpdateChannel?] = Ref(None)

///|
/// What asking the channel concluded.
pub enum UpdateCheck {
  /// This application has no update channel configured.
  NotConfigured
  /// Nothing newer is on offer.
  UpToDate
  /// A newer release is available and fully authenticated.
  Available(PendingUpdate)
}

///|
/// Asks the configured channel whether a newer release is on offer.
///
/// Available to the application at any time, not only at launch. Returns
/// `NotConfigured` rather than `UpToDate` when there is no channel, because
/// "nobody told us where to look" and "we looked and found nothing" are
/// different answers and only one of them is worth showing a user.
pub async fn check_for_update() -> UpdateCheck {
  match active_update_channel.val {
    None => NotConfigured
    Some(channel) =>
      match channel.poll() {
        Some(update) => Available(update)
        None => UpToDate
      }
  }
}

///|
/// An update the channel offered, and the means to take it.
///
/// Being handed one means the manifest was signed by a trusted key, is fresh,
/// and carries a revision newer than the installed one. Nothing has been
/// downloaded.
pub struct PendingUpdate {
  version : String
  revision : UInt64
  notes_url : String?
  size : Int64
  channel : @updater.UpdateChannel
  offer : @updater.AvailableUpdate
}

///|
/// The version on offer.
pub fn PendingUpdate::version(self : PendingUpdate) -> String {
  self.version
}

///|
/// The signed monotonic release order.
pub fn PendingUpdate::revision(self : PendingUpdate) -> UInt64 {
  self.revision
}

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

///|
/// Where the release notes for it live, when the manifest says.
pub fn PendingUpdate::notes_url(self : PendingUpdate) -> String? {
  self.notes_url
}

///|
/// How many bytes taking it will transfer.
pub fn PendingUpdate::size(self : PendingUpdate) -> Int64 {
  self.size
}

///|
/// Downloads this update and installs it over the running application.
///
/// Downloaded chunks are written only to a private native stage while their
/// size, digest, and signature are checked. The stage cannot be installed
/// unless that authentication completes, and the expanded bundle's own code
/// signature is checked before the installed application is touched. The
/// application is not restarted — see `restart`.
///
/// This is deliberately not something the framework does on its own. Checking
/// without being asked is a reasonable default; installing without being asked
/// changes the code someone is running, and only the application knows whether
/// this is a moment when that is acceptable.
pub async fn PendingUpdate::install(
  self : PendingUpdate,
  on_progress? : async (Int64) -> Unit noraise = _ => (),
) -> UpdateInstallOutcome {
  match
    self.channel.download_and_install(
      @updater.http_fetch_artifact,
      self.offer,
      on_progress~,
    ) {
    @updater.Installed => Installed
    @updater.AlreadyInstalled => AlreadyInstalled
  }
}

///|
/// Starts the installed replacement. The caller should exit afterwards.
///
/// Returning without an error means the system accepted the request, not that
/// the new version is running: it decides that afterwards and does not report
/// back.
pub fn PendingUpdate::restart(_self : PendingUpdate) -> Unit raise {
  @updater.relaunch()
}

///|
/// Builds the channel described by `proton.project.json`.
fn ResolvedUpdateChannel::open(
  self : ResolvedUpdateChannel,
) -> @updater.UpdateChannel raise {
  let platform = @native.runtime_info().platform_id
  let current_revision = @native.update_current_revision()
  @updater.UpdateChannel::new(
    self.config.endpoint(),
    self.config.public_keys(),
    platform,
    current_revision,
  )
}

///|
/// Asks the channel whether a newer release is on offer.
async fn ResolvedUpdateChannel::poll(
  self : ResolvedUpdateChannel,
) -> PendingUpdate? {
  // The staging directory is resolved when an update is taken, not here. A
  // check that has found nothing should not have created anything.
  let channel = self.open()
  let bound = @updater.oldest_accepted(self.config.freshness_days())
  match channel.check(@updater.http_fetch, bound) {
    UpToDate => None
    Available(offer) =>
      Some(PendingUpdate::{
        version: offer.version().to_string(),
        revision: offer.revision(),
        notes_url: offer.notes_url(),
        size: offer.size(),
        channel,
        offer,
      })
  }
}

///|
/// Runs the check once the application is up, if it is configured to.
///
/// Every refusal here is swallowed after being reported. An update endpoint
/// that is unreachable, serving a malformed manifest, or signed by a key this
/// build does not trust must not be able to disturb an application that was
/// launched to do something else — which is exactly what happens the moment
/// this is allowed to raise into startup.
fn start_update_check(
  channel : ResolvedUpdateChannel?,
  handlers : Array[async (PendingUpdate) -> Unit noraise],
  tasks : @async.TaskGroup[Unit],
) -> Unit {
  guard channel is Some(channel) else { return }
  guard checks_on_launch(channel, handlers.length()) else { return }
  tasks.spawn_bg(allow_failure=true, () => {
    @async.sleep(update_check_delay_ms + update_check_jitter())
    let offered = channel.poll() catch {
      error => {
        println("proton: update check failed: \{error}")
        return
      }
    }
    if offered is Some(update) {
      for handler in handlers {
        handler(update)
      }
    }
  })
}

///|
/// Confirms that this application reached its running state by removing older
/// bundles retained for launch recovery.
///
/// Cleanup cannot be a startup precondition: the current application is
/// already healthy, and a permissions or filesystem failure beside it should
/// not turn that successful launch into a failure.
fn cleanup_previous_update() -> Unit {
  @native.update_cleanup_previous() catch {
    error =>
      println("proton: previous update cleanup failed: " + error.message())
  }
}

///|
/// Whether launching should contact the update endpoint.
///
/// An application that registered no handler is not asked to, even when
/// `proton.project.json` declares a channel. Contacting a server on every launch is a
/// privacy decision, and one that nothing in the application is prepared to act
/// on is a request made on a user's behalf for no reason.
fn checks_on_launch(channel : ResolvedUpdateChannel, handlers : Int) -> Bool {
  handlers > 0 && channel.config.checks_on_launch()
}

///|
/// A spread to add to the check delay.
///
/// Derived from the clock rather than a random source, because the only
/// property needed is that two installations disagree, and two installations
/// do not start at the same millisecond.
fn update_check_jitter() -> Int {
  (@env.now() % update_check_jitter_ms.to_uint64()).to_int()
}