///|
/// Fetches the whole body at a URL.
///
/// The network is a parameter rather than a dependency so that the sequence
/// below — verify, decode, compare, verify again — can be exercised end to end
/// without one. That sequence is the part worth testing exhaustively; an HTTP
/// client is not.
pub type Fetch = async (String) -> Bytes raise UpdateError

///|
/// Streams an artifact into a consumer without retaining the whole body.
///
/// The expected size is authenticated manifest data. A transport may use it to
/// reject a mismatching response header early; `UpdateChannel` independently
/// enforces it against the chunks actually delivered.
pub type ArtifactFetch = async (
  String,
  Int64,
  async (Bytes) -> Unit raise UpdateError,
) -> Unit raise UpdateError

///|
fn artifact_size_after_chunk(
  expected : Int64,
  received : Int64,
  chunk_size : Int64,
) -> Int64 raise UpdateError {
  guard chunk_size <= expected - received else {
    let actual = if received > 0x7fffffffffffffffL - chunk_size {
      0x7fffffffffffffffL
    } else {
      received + chunk_size
    }
    raise ArtifactSizeMismatch(expected~, actual~)
  }
  received + chunk_size
}

///|
/// An update channel, resolved from configuration.
pub struct UpdateChannel {
  endpoint : String
  trusted_keys : Array[@rsa.PublicKey]
  platform : String
  current_revision : UInt64
}

///|
/// Resolves a channel from its configured form.
///
/// Trusted keys are parsed once, here, so that a key which cannot be used is
/// reported when the channel is built rather than at the moment a signature
/// needs checking.
pub fn UpdateChannel::new(
  endpoint : String,
  public_keys : Array[String],
  platform : String,
  current_revision : UInt64,
) -> UpdateChannel raise UpdateError {
  let trusted_keys : Array[@rsa.PublicKey] = []
  for text in public_keys {
    let key = @rsa.PublicKey::parse(text) catch {
      error => raise UntrustedKey(detail=error.message())
    }
    trusted_keys.push(key)
  }
  UpdateChannel::{ endpoint, trusted_keys, platform, current_revision }
}

///|
/// What a check concluded.
pub enum CheckOutcome {
  /// Nothing newer is on offer for this platform.
  UpToDate
  /// A newer release is available and has been fully authenticated.
  Available(AvailableUpdate)
} derive(Eq, Debug)

///|
/// A release that passed every check short of being downloaded.
pub struct AvailableUpdate {
  version : @manifest.Version
  revision : UInt64
  notes_url : String?
  url : String
  size : Int64
  sha256_hex : String
  signature_hex : String
} derive(Eq, Debug)

///|
pub fn AvailableUpdate::version(self : AvailableUpdate) -> @manifest.Version {
  self.version
}

///|
pub fn AvailableUpdate::revision(self : AvailableUpdate) -> UInt64 {
  self.revision
}

///|
pub fn AvailableUpdate::notes_url(self : AvailableUpdate) -> String? {
  self.notes_url
}

///|
pub fn AvailableUpdate::url(self : AvailableUpdate) -> String {
  self.url
}

///|
pub fn AvailableUpdate::size(self : AvailableUpdate) -> Int64 {
  self.size
}

///|
/// Checks the channel for a newer release.
///
/// `oldest_accepted` is the earliest publication instant this client will
/// believe. It is supplied rather than derived from a clock inside this
/// function: the calendar arithmetic that turns a freshness window into an
/// instant belongs where a date library is available, and keeping it out means
/// the whole check can be tested by naming the instant directly.
///
/// The order of the steps is the point. The signature is checked before the
/// document is parsed, so no attacker-supplied structure is interpreted until
/// it is known to come from a trusted key. Freshness and revision are checked
/// after, because both read fields that only the signature makes trustworthy.
pub async fn UpdateChannel::check(
  self : UpdateChannel,
  fetch : Fetch,
  oldest_accepted : @manifest.Timestamp,
) -> CheckOutcome raise UpdateError {
  let document = fetch(self.endpoint)
  let signature_url = "\{self.endpoint}.sig"
  let signature = decode_hex_body(fetch(signature_url), signature_url)
  guard self.verifies(sha256_of(document), signature) else {
    raise ManifestSignatureInvalid
  }
  let text = bytes_to_utf8(document)
  let manifest = @manifest.Manifest::parse(text) catch {
    error => raise ManifestUndecodable(detail=error.message())
  }
  let published_at = manifest.published_at()
  guard !published_at.is_before(oldest_accepted) else {
    raise ManifestStale(
      published_at=published_at.to_text(),
      oldest_accepted=oldest_accepted.to_text(),
    )
  }
  guard manifest.revision() > self.current_revision else { return UpToDate }
  match manifest.platform(self.platform) {
    None => UpToDate
    Some(update) =>
      Available(AvailableUpdate::{
        version: manifest.version(),
        revision: manifest.revision(),
        notes_url: manifest.notes_url(),
        url: update.url(),
        size: update.size(),
        sha256_hex: update.sha256_hex(),
        signature_hex: update.signature_hex(),
      })
  }
}

///|
/// Streams an available update into a staging consumer and authenticates it.
///
/// No complete artifact buffer is built here. Each accepted chunk is counted,
/// hashed and passed to `consume`; an oversized stream is refused before its
/// first out-of-bounds chunk reaches staging. The caller must discard staging
/// when this function raises, because bytes are not trusted until it returns.
async fn UpdateChannel::download_into(
  self : UpdateChannel,
  fetch : ArtifactFetch,
  update : AvailableUpdate,
  consume : async (Bytes) -> Unit raise UpdateError,
  on_progress : async (Int64) -> Unit noraise,
) -> Unit raise UpdateError {
  let received = Ref(0L)
  let hasher = @crypto.SHA256::new()
  fetch(update.url, update.size, async fn(chunk) -> Unit raise UpdateError {
    let chunk_size = chunk.length().to_int64()
    let actual_size = artifact_size_after_chunk(
      update.size,
      received.val,
      chunk_size,
    )
    consume(chunk)
    hasher.update(chunk)
    received.val = actual_size
    on_progress(actual_size)
  })
  guard received.val == update.size else {
    raise ArtifactSizeMismatch(expected=update.size, actual=received.val)
  }
  let digest = Bytes::from_array(hasher.finalize().iter().collect()[:])
  let actual_digest = @crypto.bytes_to_hex_string(digest)
  guard actual_digest == update.sha256_hex else {
    raise ArtifactDigestMismatch(
      expected=update.sha256_hex,
      actual=actual_digest,
    )
  }
  let signature = decode_hex_body_text(update.signature_hex, update.url)
  guard self.verifies(digest, signature) else { raise ArtifactSignatureInvalid }
}

///|
/// Reports whether any trusted key signed this digest.
///
/// Every key is tried. A reserve key distributed before it is used is what
/// makes rotation possible without a transition release, so a signature from
/// any configured key is as good as a signature from the first.
fn UpdateChannel::verifies(
  self : UpdateChannel,
  digest : Bytes,
  signature : Bytes,
) -> Bool {
  for key in self.trusted_keys {
    if @rsa.verify_pkcs1_sha256(key, digest, signature) {
      return true
    }
  }
  false
}

///|
fn sha256_of(data : Bytes) -> Bytes {
  let hasher = @crypto.SHA256::new()
  hasher.update(data)
  let digest = hasher.finalize()
  Bytes::from_array(digest.iter().collect()[:])
}