///|
/// An application command can raise this error for invalid usage.
pub(all) suberror UsageError { UsageError(String) }

///|
/// The usage error text for people or a custom report.
pub fn UsageError::message(self : UsageError) -> String {
  let UsageError(message) = self
  message
}

///|
/// The injected configuration tree does not answer these file requests.
/// Only the host makes this error; callers can inspect each request and path.
pub suberror FileTreeError {
  IncompleteFileTree(requests~ : Array[@config.FileRequest])
}

///|
/// Text for people that names every requested path.
pub fn FileTreeError::message(self : FileTreeError) -> String {
  let IncompleteFileTree(requests~) = self
  let paths = requests.map(request => match request {
    ListDir(path) | ReadFile(path) => path.to_string()
  })
  "Incomplete configuration file tree; requested paths: " + paths.join(", ")
}

///|
/// The six hooks of a hosted service.
pub(all) enum HookPhase { Starting; Start; Started; Stopping; Stop; Stopped } derive(Eq, Debug)

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

///|
pub extend HookPhase with @debug.Debug::{to_repr}

///|
/// The hook name as it is in `@hosting.service`, for example `start`.
pub impl Show for HookPhase with output(self, logger) {
  logger.write_string(match self {
    Starting => "starting"
    Start => "start"
    Started => "started"
    Stopping => "stopping"
    Stop => "stop"
    Stopped => "stopped"
  })
}

///|
pub extend HookPhase with Show::{output, to_string}

///|
/// A hook or the command suspended in `run_embedded` on wasm or wasm-gc. That
/// driver has no event loop, so it cannot resume suspended work. Only the host
/// makes this error.
// Only `run_embedded` (wasm, wasm-gc) makes these. Remove when the build system
// can limit a constructor to targets.
#warnings("-6")
pub suberror SuspendError {
  HookSuspended(service~ : ServiceName, phase~ : HookPhase)
  CommandSuspended
}

///|
/// Text for people that names the step and tells how to correct it.
pub fn SuspendError::message(self : SuspendError) -> String {
  let step = match self {
    HookSuspended(service~, phase~) => "The \{phase} hook of service \{service.to_string().escape()}"
    CommandSuspended => "The command"
  }
  step + " suspended. On wasm and wasm-gc, hooks and the command must not wait: remove the wait, or run the host on native or js"
}

///|
/// The stop phase used its full time budget. Only the host makes this error.
pub suberror ShutdownTimedOut { ShutdownTimedOut(@lander_core.TimeLimit) }

///|
/// Text for people that names the shutdown time limit.
pub fn ShutdownTimedOut::message(self : ShutdownTimedOut) -> String {
  let ShutdownTimedOut(limit) = self
  "Shutdown timed out after \{limit}"
}

///|
/// The portable process exit code for this signal.
pub fn SignalKind::exit_code(self : SignalKind) -> ExitCode {
  { code: match self { Interrupt => 130; Terminate => 143; Quit => 131 } }
}

///|
fn usage_exit_code() -> ExitCode { { code: 2 } }

///|
/// Failure phases share code 1; configuration and usage errors in any phase
/// outrank them. This function performs no I/O and never parses an exit code.
fn select_exit_code(signal : SignalKind?, failures : Array[Error], timed_out : Bool, application : ExitCode?) -> ExitCode {
  if signal is Some(kind) { return kind.exit_code() }
  for error in failures {
    if error is (@config.ConfigError::_ | UsageError(_) | IncompleteFileTree(..)) { return usage_exit_code() }
  }
  if !failures.is_empty() || timed_out { return ExitCode::failure() }
  application.unwrap_or(ExitCode::success())
}

///|
fn report_text(name : @lander_core.AppName, error : Error) -> String {
  let message = match error {
    HostError::_ as e => e.message()
    WorkingDirectoryError::_ as e => e.message()
    UsageError::_ as e => e.message()
    FileTreeError::_ as e => e.message()
    SuspendError::_ as e => e.message()
    ShutdownTimedOut::_ as e => e.message()
    @config.ConfigError::_ as e => e.message()
    e => e.to_string()
  }
  "\{name}: \{message}"
}