///|
/// Errors produced by a filesystem adapter.
///
/// The loader translates these values into `LoadError`, so adapters never
/// leak platform-specific error types through the public loading API.
pub(all) suberror FileSystemError {
  NotFound(path~ : String)
  PermissionDenied(path~ : String)
  InvalidPath(path~ : String, message~ : String)
  ReadByteLimitExceeded(path~ : String, limit~ : Int)
  GlobMatchLimitExceeded(path~ : String, limit~ : Int)
  Other(message~ : String)
} derive(Debug, Eq)

///|
/// The backend boundary used by the portable configuration loader.
///
/// Implementations provide text, an identity used exclusively for cycle
/// detection, deterministic-enough glob candidates, and an explicitly
/// injected home directory. The loader sorts glob output itself and never
/// consults process state.
///
/// Implementations must honor supplied limits before returning an oversized
/// value. Adapters with streaming or metadata support should stop I/O before
/// allocation; adapters without it must reject before transferring the value
/// to the loader and document the residual backend allocation boundary.
pub(open) trait FileSystem {
  fn read_text(Self, String, max_bytes? : Int) -> Result[
    String,
    FileSystemError,
  ]
  fn canonical_identity(Self, String) -> Result[String, FileSystemError]
  fn glob(Self, String, max_matches? : Int) -> Result[
    Array[String],
    FileSystemError,
  ]
  fn home_dir(Self) -> String?
}

///|
fn filesystem_error_message(error : FileSystemError) -> String {
  match error {
    NotFound(path~) => "file not found: \{path}"
    PermissionDenied(path~) => "permission denied: \{path}"
    InvalidPath(path~, message~) => "invalid path \{path}: \{message}"
    ReadByteLimitExceeded(path~, limit~) =>
      "file exceeds byte limit \{limit}: \{path}"
    GlobMatchLimitExceeded(path~, limit~) =>
      "glob exceeds match limit \{limit}: \{path}"
    Other(message~) => message
  }
}