// The handle: what a caller gets back from a send, and how it waits.

///|
/// How often a waiting handle asks the store again, in milliseconds.
let poll_interval : Int64 = 5

///|
/// A run, or a set of runs, a caller can wait on and look at.
///
/// The result type is the one the task returns: `get` decodes the stored JSON
/// back into it. `state` and `revoke` work on anything, since a caller that
/// wants to cancel a run does not care what it would have returned.
pub struct Handle[R] {
  app : App
  ids : Array[String]
  all : Bool
  codec : (Json) -> R raise @spec.Refused
}

///|
/// A handle over `ids` whose value decodes into `R`; `all` makes `get` answer
/// the list of results in the order of `ids`.
pub fn[R : FromJson] Handle::of(
  app : App,
  ids : Array[String],
  all : Bool,
) -> Handle[R] {
  {
    app,
    ids,
    all,
    codec: fn(value) raise @spec.Refused {
      @json.from_json(value) catch {
        error => raise @spec.Refused::Malformed(got=error.to_string())
      }
    },
  }
}

///|
/// A handle over `ids` of runs that already answer in JSON.
pub fn Handle::of_json(
  app : App,
  ids : Array[String],
  all : Bool,
) -> Handle[Json] {
  { app, ids, all, codec: fn(value) { value }, }
}

///|
/// The ids the handle waits on.
pub fn[R] Handle::ids(self : Handle[R]) -> Array[String] {
  self.ids
}

///|
/// Wait for the run and answer its value, or refuse with the reason it will not
/// arrive: `Refused::Failed` when a body raised on its last attempt,
/// `Refused::Timeout` when `timeout` milliseconds passed first, and
/// `Refused::Missing` when the app keeps no results.
pub async fn[R] Handle::get(
  self : Handle[R],
  timeout? : Int64,
) -> R raise @spec.Refused {
  let backend = match self.app.backend {
    Some(backend) => backend
    None => raise @spec.Refused::Missing(kind="backend")
  }
  let limit = timeout.unwrap_or(0L)
  let deadline = if timeout is Some(_) {
    Some(self.app.now() + limit)
  } else {
    None
  }
  while true {
    let mut settled = true
    let values = []
    let mut failure = None
    let mut last_retries = 0
    for id in self.ids {
      match backend.fetch(id) {
        Some(record) =>
          match record.state {
            @spec.Succeeded =>
              values.push(record.result.unwrap_or(Json::null()))
            @spec.Failed => {
              settled = false
              if failure is None {
                failure = Some(record.error.unwrap_or("failed"))
                last_retries = record.retries
              }
            }
            @spec.Revoked => {
              settled = false
              if failure is None {
                failure = Some("revoked")
              }
            }
            _ => settled = false
          }
        None => settled = false
      }
    }
    if settled {
      if self.all {
        return (self.codec)(Json::array(values))
      }
      match values {
        [value, ..] => return (self.codec)(value)
        [] => raise @spec.Refused::Empty(kind="result")
      }
    }
    match failure {
      Some(message) =>
        raise @spec.Refused::Failed(got=message, retries=last_retries)
      None => ()
    }
    match deadline {
      Some(at) =>
        if self.app.now() >= at {
          raise @spec.Refused::Timeout(limit~)
        }
      None => ()
    }
    @wait.nap(poll_interval.to_int())
  } nobreak {
    raise @spec.Refused::Timeout(limit~)
  }
}

///|
/// What the run is doing now, without waiting for it.
///
/// A set reports `Failed` once any part failed, `Revoked` once any part was
/// revoked, `Succeeded` once every part settled, and the state of the first
/// unsettled run otherwise.
pub async fn[R] Handle::state(
  self : Handle[R],
) -> @spec.State raise @spec.Refused {
  let backend = match self.app.backend {
    Some(backend) => backend
    None => raise @spec.Refused::Missing(kind="backend")
  }
  let mut settled = 0
  let mut first_unsettled : @spec.State? = None
  for id in self.ids {
    match backend.fetch(id) {
      Some(record) =>
        match record.state {
          @spec.Failed => return @spec.Failed
          @spec.Revoked => return @spec.Revoked
          @spec.Succeeded => settled = settled + 1
          state => if first_unsettled is None { first_unsettled = Some(state) }
        }
      None =>
        if first_unsettled is None {
          first_unsettled = Some(@spec.Pending)
        }
    }
  }
  if settled == self.ids.length() {
    return @spec.Succeeded
  }
  first_unsettled.unwrap_or(@spec.Pending)
}

///|
/// Cancel the run or the set: it will not be sent again, and with `terminate`
/// a worker in this process that is running it right now stops.
///
/// Terminating reaches only this process's workers; a run already going in
/// another process is left to finish, since nothing else is reachable.
pub async fn[R] Handle::revoke(
  self : Handle[R],
  terminate~ : Bool = false,
) -> Unit raise @spec.Refused {
  let backend = match self.app.backend {
    Some(backend) => backend
    None => raise @spec.Refused::Missing(kind="backend")
  }
  for id in self.ids {
    let revoked : @spec.Outcome = {
      state: @spec.Revoked,
      result: None,
      error: None,
      retries: 0,
      at: self.app.now(),
    }
    backend.store(id, revoked)
    if terminate {
      match self.app.running.get(id) {
        Some(task) => task.cancel()
        None => ()
      }
    }
  }
}