///|
/// Run the host without ending the process. Hooks share one context and services
/// record. Start hooks run in registration order, stop hooks in reverse order.
/// Stop requests cancel the command task and its defers finish before shutdown.
/// Shutdown runs protected from outside cancellation, within one time budget.
/// It reports each shutdown timeout for every exit code, and other failures only
/// for code 1 or 2, in failure order, through `error_report` or as
/// `: ` on standard error. Report errors do not change the outcome.
///
/// A help or version request of the arguments writes the argparse text one
/// time with `println` and returns code 0 with the reason `Requested`, before
/// the configuration load, the factory and the hooks. The console driver then
/// ends the process with that code.
///
/// A console driver gives `signals`. The run registers the probe before the
/// first hook, so each cancellation check reads it, and a watcher task reads it
/// every 50 ms until the run ends. The first signal is a stop request with the
/// reason `Signal(kind)`, and it cancels the command task.
pub async fn[S] run(host : Host[S], command : Hook[S], signals? : SignalProbe) -> RunOutcome {
  run_with_directory_reader(host, command, signals?)
}

///|
/// The time between two reads of the signal probe by the watcher task.
let signal_watch_interval : Int = 50

///|
async fn[S] run_with_directory_reader(host : Host[S], command : Hook[S], read_directory? : () -> @lander_core.AbsolutePath raise WorkingDirectoryError = process_directory, signals? : SignalProbe) -> RunOutcome {
  let result = execute(host, command, read_directory~, signals?)
  @async.protect_from_cancel(() => {
    for error in result.failures {
      if error is ShutdownTimedOut(_) || result.exit_code == ExitCode::failure() || result.exit_code == usage_exit_code() {
        try {
          match host.report {
            Some(report) => report(error, result.exit_code)
            None =>
              match host.report_writer {
                Some(writer) => writer(report_text(host.name, error))
                None => write_report_line(report_text(host.name, error))
              }
          }
        } catch { _ => () }
      }
    }
  })
  result
}

///|
async fn[S] execute(host : Host[S], command : Hook[S], read_directory~ : () -> @lander_core.AbsolutePath raise WorkingDirectoryError, signals? : SignalProbe) -> RunOutcome {
  let inputs = configuration_inputs(host, read_directory~) catch {
    DisplayRequested(text) => {
      println(text)
      return with_signal(requested_outcome(), signals)
    }
    err => return with_signal(outcome(err), signals)
  }
  let config = @host.load(inputs.sources) catch { err => return with_signal(outcome(err), signals) }
  let configured = finish_configuration(host, inputs, config) catch {
    StopRequested => return with_signal(requested_outcome(), signals)
    err => return with_signal(outcome(err), signals)
  }
  let ctx = configured.context
  let lifetime = ctx.lifetime
  guard signals is Some(probe) else { return run_engine(host, configured, command) }
  lifetime.register_stop_probe(probe.request)
  @async.with_task_group(group => {
    group.spawn_bg(no_wait=true, () => {
      while true {
        @async.sleep(signal_watch_interval)
        ignore(lifetime.probe_stop())
      }
    })
    run_engine(host, configured, command)
  })
}

///|
/// A signal that arrived before the engine started still selects the exit code.
/// The reason stays the first event of the run.
fn with_signal(result : RunOutcome, signals : SignalProbe?) -> RunOutcome {
  guard signals is Some(probe) && (probe.request)() is Some(Signal(kind)) else { return result }
  { ..result, signal: Some(kind), exit_code: kind.exit_code() }
}

///|
async fn[S] run_engine(host : Host[S], configured : ConfiguredRun[S], command : Hook[S]) -> RunOutcome {
  let ctx = configured.context
  let lifetime = ctx.lifetime
  lifetime.task_cancelled.val = @async.is_being_cancelled
  defer { lifetime.task_cancelled.val = () => false }
  let mut step = lifetime.start_engine(host.services)
  while step is Run(pending, action) {
    if action == Raise(Stopping) { break }
    step = run_step(host, ctx, command, pending, action)
  }
  if @async.is_being_cancelled() { lifetime.stop_application() }
  @async.protect_from_cancel(() => {
    let mut interrupted = ActionResult::Interrupted
    let completed = @async.with_timeout_opt(configured.shutdown_timeout.to_millis(), () => {
      while step is Run(pending, action) {
        let result = perform_step(host, ctx, command, pending, action)
        // Keep the current token for timed_out, even if a hook absorbs cancellation.
        if @async.is_being_cancelled() {
          interrupted = result.unwrap_or(Interrupted)
          break
        }
        step = reply_step(ctx, pending, result)
      }
    })
    if completed is None && step is Run(pending, _) {
      ignore(lifetime.probe_stop())
      step = pending.timed_out(limit=configured.shutdown_timeout, observed_signal?=lifetime.signal, result=interrupted)
    }
    // The timeout reply skips hooks and leaves only the stopped event.
    while step is Run(pending, action) {
      step = run_step(host, ctx, command, pending, action)
    }
  })
  guard step is Finish(result) else { abort("the engine finishes after its last action") }
  lifetime.reason = Some(result.reason)
  { ..result, exit_code: select_exit_code(result.signal, result.failures, result.timed_out,
      match lifetime.exit_code { Some(code) => Some(code); None => result.requested_code }) }
}

///|
async fn[S] run_step(host : Host[S], ctx : Context[S], command : Hook[S], pending : Pending, action : Action) -> Step {
  reply_step(ctx, pending, perform_step(host, ctx, command, pending, action))
}

///|
async fn[S] perform_step(host : Host[S], ctx : Context[S], command : Hook[S], pending : Pending, action : Action) -> ActionResult? {
  let lifetime = ctx.lifetime
  if pending.engine.reason is Some(reason) { lifetime.reason = Some(reason) }
  @async.handle_cancellation(() => {
    try {
      match action {
        Hook(reference, phase) => {
          if host.services[reference.index].hook(phase) is Some(hook) { hook(ctx) }
        }
        RunCommand => run_command(ctx, command)
        Raise(event) => lifetime.raise_event(event)
      }
      ActionResult::Returned
    } catch {
      StopRequested => { lifetime.stop_application(); Interrupted }
      error => Failed(error)
    }
  })
}

///|
fn[S] reply_step(ctx : Context[S], pending : Pending, completed : ActionResult?) -> Step {
  if completed is None { ctx.lifetime.stop_application() }
  ctx.lifetime.reply(pending, completed.unwrap_or(Interrupted))
}

///|
async fn[S] run_command(ctx : Context[S], command : Hook[S]) -> Unit {
  let lifetime = ctx.lifetime
  @async.with_task_group(group => {
    let task = group.spawn(() => command(ctx), allow_failure=true)
    lifetime.cancel_command.val = Some(() => task.cancel())
    defer { lifetime.cancel_command.val = None }
    if lifetime.probe_stop() { task.cancel() }
    task.wait() catch {
      @async.WaitedTaskAlreadyCancelled => lifetime.stop_application()
      error => raise error
    }
  })
}