///|
/// Returns the shared unsupported-platform error message.
fn unsupported_error_message() -> String {
  "shell package currently supports Windows, macOS, and Linux native builds"
}

///|
/// Returns `true` when the current native target has a shell backend that
/// this package knows how to call.
pub fn is_supported() -> Bool {
  is_supported_ffi()
}

///|
/// Verifies that the current native platform is supported by this package.
///
/// Returns `Ok(())` on supported platforms and an explanatory `Err(String)` on
/// unsupported targets.
pub fn ensure_supported() -> Result[Unit, String] {
  if is_supported() {
    Ok(())
  } else {
    Err(unsupported_error_message())
  }
}

///|
/// A platform shell request could not be completed.
pub suberror ShellError {
  ShellError(operation~ : String, target~ : String, reason~ : String)
} derive(Debug)

///|
pub extend ShellError with Debug::{to_repr}

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

///|
impl Show for ShellError with fn output(self, logger) {
  let ShellError(operation~, target~, reason~) = self
  logger.write_string("shell.\{operation} failed for \{target}: \{reason}")
}

///|
async fn run_tool(
  operation : String,
  target : String,
  tool : String,
  args : Array[String],
) -> Unit raise ShellError {
  let status = @process.run(tool, args) catch {
    error => raise ShellError(operation~, target~, reason=error.to_string())
  }
  if status != 0 {
    raise ShellError(
      operation~,
      target~,
      reason="\{tool} exited with status \{status}",
    )
  }
}

///|
fn check_native_result(
  ok : Bool,
  operation : String,
  target : String,
) -> Unit raise ShellError {
  if !ok {
    raise ShellError(operation~, target~, reason="native shell request failed")
  }
}

///|
fn file_uri(path : String) -> String raise ShellError {
  let absolute = if path.has_prefix("/") {
    path
  } else {
    let cwd = match @env.current_dir() {
      Some(cwd) => cwd
      None =>
        raise ShellError(
          operation="revealItem",
          target=path,
          reason="cannot determine current directory",
        )
    }
    cwd + "/" + path
  }
  // Encode each component, preserving separators and the requested symlink entry.
  "file://" +
  absolute.split("/").map(component => @percent.encode(component)).join("/")
}

///|
/// Opens a URL or filesystem path with its default application.
///
/// Uses ShellExecuteW on Windows, /usr/bin/open on macOS, and gio open on Linux.
/// Linux requires gio and a desktop session with a default handler. Raises
/// ShellError on failure. Waiting for an external tool yields to other tasks;
/// cancellation terminates and reaps the tool using async's process lifecycle.
/// Success means the platform accepted the request, not that the target app
/// finished starting.
pub async fn open(target : String) -> Unit raise ShellError {
  match @async.platform {
    Windows =>
      check_native_result(open_ffi(@ffi.to_cstr(target)), "open", target)
    MacOS => run_tool("open", target, "/usr/bin/open", ["--", target])
    Linux => run_tool("open", target, "gio", ["open", "--", target])
  }
}

///|
/// Opens the native file manager and selects the filesystem entry at `path`.
///
/// Uses Explorer on Windows, /usr/bin/open -R on macOS, and FileManager1
/// ShowItems via dbus-send on Linux. Linux requires dbus-send and a file manager
/// implementing that session-bus interface. Raises ShellError on failure.
/// External-tool waits yield and participate in task cancellation.
pub async fn reveal_item(path : String) -> Unit raise ShellError {
  match @async.platform {
    Windows =>
      check_native_result(
        reveal_item_ffi(@ffi.to_cstr(path)),
        "revealItem",
        path,
      )
    MacOS => run_tool("revealItem", path, "/usr/bin/open", ["-R", "--", path])
    Linux =>
      run_tool("revealItem", path, "dbus-send", [
        "--session",
        "--print-reply=literal",
        "--dest=org.freedesktop.FileManager1",
        "/org/freedesktop/FileManager1",
        "org.freedesktop.FileManager1.ShowItems",
        "array:string:" + file_uri(path),
        "string:",
      ])
  }
}

///|
/// Moves `path` to the operating-system trash or recycle bin.
///
/// Uses the recycle bin on Windows, Finder Trash on macOS, and gio trash on
/// Linux. Raises ShellError on failure. The Linux tool wait yields and
/// participates in task cancellation.
pub async fn trash_item(path : String) -> Unit raise ShellError {
  match @async.platform {
    Linux => run_tool("trashItem", path, "gio", ["trash", "--", path])
    Windows | MacOS =>
      check_native_result(trash_item_ffi(@ffi.to_cstr(path)), "trashItem", path)
  }
}

///|
/// Plays the operating system's default alert sound.
///
/// On Windows this calls `MessageBeep`. On macOS and Linux it writes the
/// terminal bell escape to stderr. Returns `Ok(())` when the platform has a
/// beep backend.
pub fn beep() -> Result[Unit, String] {
  if !is_supported() {
    return Err(unsupported_error_message())
  }
  if beep_ffi() {
    Ok(())
  } else {
    Err("shell.beep failed")
  }
}