///|
/// The install stage where a failure was observed. Each stage renders as a
/// stable `[code]` in user-facing error output, so the code stays meaningful
/// even when the wording of a message changes.
pub enum Stage {
  Coordinate
  Resolve
  Transfer
  Digest
  Archive
  Destination
  Compat
  Filesystem
}

///|
/// Stable machine-readable code for a stage, shown as `error[code]`.
pub fn stage_code(stage : Stage) -> String {
  match stage {
    Coordinate => "coordinate"
    Resolve => "resolve"
    Transfer => "transport"
    Digest => "digest"
    Archive => "archive"
    Destination => "destination"
    Compat => "compat"
    Filesystem => "filesystem"
  }
}

///|
/// A classified, user-facing failure: the attempted operation and the object
/// it acted on, the observed reason, and the next actionable step. Source
/// locations are never part of this rendering.
pub(all) suberror InstallError {
  InstallError(
    stage~ : Stage,
    what~ : String,
    target~ : String,
    reason~ : String,
    action~ : String,
  )
}

///|
/// Raise a classified install error. `target` names the object the
/// operation acted on (a coordinate, URL or path; "" when there is none);
/// `action` is the next actionable step shown to the user.
fn[T] abort_install(
  stage : Stage,
  what : String,
  target : String,
  reason : String,
  action~ : String = "",
) -> T raise InstallError {
  raise InstallError(stage~, what~, target~, reason~, action~)
}

///|
/// Message-only shims matching `fail`'s signature, for call sites whose
/// operation and object are already embedded in the reason string.
fn[T] abort_args(reason : String) -> T raise InstallError {
  abort_install(
    Coordinate, "parse command line arguments", "", reason,
    action="run 'moon binstall --help' for usage",
  )
}

///|
fn[T] abort_resolve(reason : String) -> T raise InstallError {
  abort_install(
    Resolve, "resolve the install plan", "", reason,
    action="check the coordinate and try again",
  )
}

///|
fn[T] abort_dest(reason : String) -> T raise InstallError {
  abort_install(Destination, "check the destination", "", reason)
}

///|
fn[T] abort_fs(reason : String) -> T raise InstallError {
  abort_install(
    Filesystem, "access the filesystem", "", reason,
    action="check the path and permissions, then retry",
  )
}

///|
fn[T] abort_digest(reason : String) -> T raise InstallError {
  abort_install(
    Digest, "verify the download", "", reason,
    action="nothing was installed; retry the install",
  )
}

///|
fn[T] abort_compat_msg(reason : String) -> T raise InstallError {
  abort_install(Compat, "verify runtime compatibility", "", reason)
}

///|
/// Render a classified error for stderr: operation, object, observed reason
/// and next step. No source file/line or internal error type is included.
pub fn render_error(error : InstallError) -> String {
  match error {
    InstallError(stage~, what~, target~, reason~, action~) => {
      let lines : Array[String] = [
        "moon-binstall: error[\{stage_code(stage)}]: \{what}",
      ]
      if target != "" {
        lines.push("  target: \{target}")
      }
      lines.push("  reason: \{reason}")
      if action != "" {
        lines.push("  action: \{action}")
      }
      lines.join("\n")
    }
  }
}

///|
/// Debug rendering behind MOON_BINSTALL_DEBUG: every classified field,
/// never credentials or response bodies.
pub impl Show for InstallError with output(
  self : InstallError,
  logger : &Logger,
) -> Unit {
  match self {
    InstallError(stage~, what~, target~, reason~, action~) =>
      logger.write_string(
        "InstallError(stage=\{stage_code(stage)}, what=\{what}, target=\{target}, reason=\{reason}, action=\{action})",
      )
  }
}

///|
/// `fail()` embeds the source location as ` FAILED: `; strip that
/// wrapper for the plain user-facing message.
fn strip_failure_prefix(message : String) -> String {
  match message.find(" FAILED: ") {
    Some(index) => message[index + 8:].to_owned()
    None => message
  }
}

///|
/// Report any failure to stderr and return the process exit code. Classified
/// errors render their stage code, operation, target, reason and action;
/// other failures render as a single sanitized line. The raw error (with
/// source locations) is only shown when MOON_BINSTALL_DEBUG is set.
pub fn report_error(error : Error) -> Int {
  let debug = match @env.get_env_var("MOON_BINSTALL_DEBUG") {
    Some(value) => value != ""
    None => false
  }
  match error {
    InstallError(..) as err => {
      eprintln(render_error(err))
      if debug {
        eprintln("moon-binstall: debug: \{err}")
      }
    }
    Failure(message) => {
      eprintln("moon-binstall: error: \{strip_failure_prefix(message)}")
      if debug {
        eprintln("moon-binstall: debug: \{error}")
      }
    }
    _ => eprintln("moon-binstall: error: \{error}")
  }
  1
}