///|
/// Returns whether the native backend is available on the current target.
extern "C" fn platform_supported_ffi() -> Bool = "mb_clipboard_platform_supported"

///|
/// Reads raw clipboard text bytes from the native backend.
extern "C" fn read_text_ffi() -> Bytes = "mb_clipboard_read_text"

///|
/// Writes UTF-8 encoded text bytes to the native backend.
#borrow(text)
extern "C" fn write_text_ffi(text : Bytes) -> Int32 = "mb_clipboard_write_text"

///|
/// Returns the last backend error code.
extern "C" fn last_error_code_ffi() -> Int32 = "mb_clipboard_last_error_code"

///|
/// Returns the last backend error message.
extern "C" fn last_error_message_ffi() -> Bytes = "mb_clipboard_last_error_message"

///|
/// Decodes clipboard bytes and maps an empty string to `None`.
fn decode_optional_text(bytes : Bytes) -> String? {
  let value = @utf8.decode_lossy(bytes[:])
  if value.is_empty() {
    None
  } else {
    Some(value)
  }
}

///|
/// Decodes an error message or falls back to a default string.
fn decode_error_message(bytes : Bytes, default : String) -> String {
  let value = @utf8.decode_lossy(bytes[:])
  if value.is_empty() {
    default
  } else {
    value
  }
}

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

///|
/// Converts a support probe and its native diagnostic into the public result form.
fn ensure_supported_result(
  supported : Bool,
  error_message : Bytes,
) -> Result[Unit, String] {
  if supported {
    Ok(())
  } else {
    Err(decode_error_message(error_message, unsupported_error_message()))
  }
}

///|
/// Converts raw read outputs into the public read result.
fn read_text_result(
  supported : Bool,
  bytes : Bytes,
  error_code : Int32,
  error_message : Bytes,
) -> Result[String?, String] {
  if !supported {
    Err(decode_error_message(error_message, unsupported_error_message()))
  } else if error_code == 0 {
    Ok(decode_optional_text(bytes))
  } else {
    Err(decode_error_message(error_message, "clipboard read failed"))
  }
}

///|
/// Converts raw write outputs into the public write result.
fn write_text_result(
  supported : Bool,
  status : Int32,
  error_message : Bytes,
) -> Result[Unit, String] {
  if !supported {
    Err(decode_error_message(error_message, unsupported_error_message()))
  } else if status == 0 {
    Ok(())
  } else {
    Err(decode_error_message(error_message, "clipboard write failed"))
  }
}

///|
/// Runs a read through injectable callbacks for testing.
fn read_text_with(
  supported : Bool,
  read : () -> Bytes,
  get_error_code : () -> Int32,
  get_error_message : () -> Bytes,
) -> Result[String?, String] {
  if supported {
    read_text_result(true, read(), get_error_code(), get_error_message())
  } else {
    read_text_result(false, b"", 0, get_error_message())
  }
}

///|
/// Runs a write through injectable callbacks for testing.
fn write_text_with(
  supported : Bool,
  text : String,
  write : (Bytes) -> Int32,
  get_error_message : () -> Bytes,
) -> Result[Unit, String] {
  if supported {
    write_text_result(true, write(@utf8.encode(text)), get_error_message())
  } else {
    write_text_result(false, 0, get_error_message())
  }
}

///|
/// Returns `true` when the current native target has a clipboard backend that
/// this package knows how to call.
///
/// On Linux this also checks whether a supported clipboard backend
/// is available on `PATH`, so the result reflects both platform support and
/// runtime tool availability.
///
/// This function performs only a capability probe. It does not read or modify
/// clipboard contents.
///
/// # Example
/// ```mbt check
/// test "is_supported reports capability without touching clipboard" {
///   let supported = @proton_clipboard.is_supported()
///   match supported {
///     true => ()
///     false => ()
///   }
/// }
/// ```
pub fn is_supported() -> Bool {
  platform_supported_ffi()
}

///|
/// Verifies that the current native platform is supported by this package.
///
/// Returns `Ok(())` when a backend is available and an explanatory `Err(String)`
/// on unsupported targets or when required runtime tools are missing. Use this
/// when you want to fail fast before attempting a clipboard read or write.
///
/// This is a convenience wrapper around `is_supported()` for callers that
/// prefer `Result`-based control flow.
///
/// # Example
/// ```mbt check
/// test "ensure_supported gives a result-oriented capability check" {
///   match @proton_clipboard.ensure_supported() {
///     Ok(_) => ()
///     Err(message) => assert_true(!message.is_empty())
///   }
/// }
/// ```
pub fn ensure_supported() -> Result[Unit, String] {
  let supported = is_supported()
  ensure_supported_result(supported, last_error_message_ffi())
}

///|
/// Reads the current clipboard text decoded from UTF-8.
///
/// The underlying implementation uses `@utf8.decode_lossy`, so any invalid
/// UTF-8 byte sequences are replaced with the Unicode replacement character
/// (U+FFFD) instead of causing an error.
///
/// Successful reads return `Ok(Some(text))`. If the clipboard currently has no
/// text content, this returns `Ok(None)`. Unsupported platforms and backend
/// failures are reported as `Err(String)`.
///
/// Linux tool I/O and process waits yield to other tasks and support cancellation.
/// Only available on the `native` target.
///
/// # Example
/// ```mbt check
/// async test "read_text returns text, emptiness, or an error" {
///   match @proton_clipboard.read_text() {
///     Ok(Some(text)) => assert_true(text.length() >= 0)
///     Ok(None) => ()
///     Err(message) => assert_true(!message.is_empty())
///   }
/// }
/// ```
pub async fn read_text() -> Result[String?, String] noraise {
  if @async.platform is Linux {
    return read_linux_text()
  }
  let supported = is_supported()
  read_text_with(
    supported, read_text_ffi, last_error_code_ffi, last_error_message_ffi,
  )
}

///|
/// Replaces the current clipboard text with `text`.
///
/// Returns `Ok(())` when the write succeeds. Unsupported platforms and backend
/// failures are reported as `Err(String)`. Passing an empty string clears the
/// clipboard text for backends that model an empty clipboard as empty text.
///
/// The string is encoded as UTF-8 before it is passed to the native backend.
///
/// # Example
/// ```mbt check
/// async test "write_text accepts a string and reports success or failure" {
///   match @proton_clipboard.write_text("") {
///     Ok(_) => ()
///     Err(message) => assert_true(!message.is_empty())
///   }
/// }
/// ```
pub async fn write_text(text : String) -> Result[Unit, String] noraise {
  if @async.platform is Linux {
    return write_linux_text(text)
  }
  let supported = is_supported()
  write_text_with(supported, text, write_text_ffi, last_error_message_ffi)
}

///|
async fn read_tool(
  tool : String,
  args : Array[String],
) -> Result[String?, String] noraise {
  try {
    let stderr = @process.redirect_to_file("/dev/null")
    defer stderr.close()
    let (status, output) = @process.collect_stdout(tool, args, stderr~)
    if status == 0 {
      Ok(decode_optional_text(output.binary()))
    } else {
      Err("\{tool} exited with status \{status}")
    }
  } catch {
    error => Err("\{tool}: \{error}")
  }
}

///|
async fn write_tool(
  tool : String,
  args : Array[String],
  text : String,
) -> Result[Unit, String] noraise {
  try {
    let output = @process.redirect_to_file("/dev/null")
    defer output.close()
    let (stdin, writer) = @process.write_to_process()
    defer writer.close()
    let status = @async.with_task_group(group => {
      let process = @process.spawn(
        group,
        tool,
        args,
        stdin~,
        stdout=output,
        stderr=output,
      )
      writer.write(text)
      // Clipboard tools consume until EOF before publishing the selection.
      writer.close()
      process.wait()
    })
    // A successful tool may have forked an owner that serves later pastes.
    // Only wait for the invoked process; do not terminate its descendants.
    if status == 0 {
      Ok(())
    } else {
      Err("\{tool} exited with status \{status}")
    }
  } catch {
    error => Err("\{tool}: \{error}")
  }
}

///|
async fn read_linux_text() -> Result[String?, String] noraise {
  let errors : Array[String] = []
  for
    (tool, args) in [
      ("wl-paste", ["-n"]),
      ("xclip", ["-selection", "clipboard", "-o"]),
      ("xsel", ["--clipboard", "--output"]),
    ] {
    match read_tool(tool, args) {
      Ok(text) => return Ok(text)
      Err(error) => errors.push(error)
    }
  }
  Err("Failed to read clipboard: " + errors.join("; "))
}

///|
async fn write_linux_text(text : String) -> Result[Unit, String] noraise {
  let errors : Array[String] = []
  for
    (tool, args) in [
      ("wl-copy", []),
      ("xclip", ["-selection", "clipboard"]),
      ("xsel", ["--clipboard", "--input"]),
    ] {
    match write_tool(tool, args, text) {
      Ok(_) => return Ok(())
      Err(error) => errors.push(error)
    }
  }
  Err("Failed to write clipboard: " + errors.join("; "))
}