///|
/// A configuration failure. Each case is one failure class, so a caller can
/// match it without a string compare. The core, conventions, and binding
/// capabilities raise these cases and have no error type of their own.
pub(all) suberror ConfigError {
  /// A profile input or provider breaks a typed profile rule.
  Profile(failure~ : ProfileFailure)
  /// A format could not parse the text of a source.
  Parse(
    format~ : String,
    origin~ : String,
    message~ : String,
    line~ : Int?,
    column~ : Int?
  )
  /// The text uses a YAML feature that the profile does not allow.
  UnsupportedYaml(origin~ : String, feature~ : String, line~ : Int, column~ : Int)
  /// The root of a source tree is not a struct.
  InvalidRoot(origin~ : String, found~ : String)
  /// A struct in a source tree holds the same field name twice.
  DuplicateKey(origin~ : String, key~ : KeyPath)
  /// An explicit file does not exist.
  FileNotFound(path~ : String)
  /// No registered format reads the extension of a file.
  UnsupportedFormat(path~ : String, extensions~ : Array[String])
  /// More than one candidate file exists at one location.
  Ambiguous(display~ : String, location~ : String, candidates~ : Array[String])
  /// The text is not a valid key path.
  InvalidKeyPath(text~ : String, reason~ : String)
  /// The text of a file-tree path breaks a path rule.
  InvalidFilePath(text~ : String, rule~ : FilePathRule)
  /// An environment variable name gives a key path with an empty segment.
  InvalidEnvKey(variable~ : String)
  /// A merge-by-key list item has no key value, or repeats a key value.
  MergeKey(key~ : KeyPath, index~ : Int, source~ : String)
  /// A secret reference has a payload of the wrong shape.
  InvalidSecret(key~ : KeyPath, origin~ : String, reason~ : String)
  /// No value is at the key path.
  KeyNotFound(key~ : KeyPath)
  /// A value could not be written in the format.
  Render(format~ : String, key~ : KeyPath, reason~ : String)
  /// A source has a lower priority than the source before it.
  SourceOrder(
    source~ : String,
    priority~ : Int,
    previous~ : String,
    previous_priority~ : Int
  )
  /// The start directory of discovery is outside the boundary directory.
  OutsideBoundary(start~ : String, boundary~ : String)
  /// An environment name breaks the name rules.
  InvalidEnvironmentName(name~ : String, reason~ : String)
  /// A format-layer policy breaks a named construction or registration rule.
  ConventionPolicy(input~ : String, rule~ : ConventionRule)
  /// No project in the workspace matches the selector.
  UnknownProject(selector~ : String, workspace~ : String)
  /// More than one project matches the selector by name.
  AmbiguousProject(selector~ : String, matches~ : Array[String])
  /// A bound value failed to decode.
  Decode(failure~ : BindFailure)
  /// A bound value failed validation.
  Validation(failures~ : Array[BindFailure])
} derive(Eq, Debug)

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

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

///|
/// Text for people that describes the failure. Paths show as the error holds
/// them; the host adapter gives them in the `/` form.
pub fn ConfigError::message(self : ConfigError) -> String {
  match self {
    Profile(failure~) => failure.message()
    Parse(format~, origin~, message~, line~, column~) => {
      let position = match (line, column) {
        (Some(line), Some(column)) => " at line \{line}, column \{column}"
        (Some(line), None) => " at line \{line}"
        _ => ""
      }
      "Failed to parse \{format} configuration \{origin}\{position}: \{message}"
    }
    UnsupportedYaml(origin~, feature~, line~, column~) =>
      "Unsupported YAML feature in \{origin} at line \{line}, column \{column}: \{feature}"
    InvalidRoot(origin~, found~) =>
      "The configuration root of \{origin} must be a struct, not a \{found}"
    DuplicateKey(origin~, key~) =>
      "Duplicate configuration key \{key} in \{origin}"
    FileNotFound(path~) => "Configuration file not found: \{path}"
    UnsupportedFormat(path~, extensions~) => {
      let registered = if extensions.is_empty() {
        "no format is registered"
      } else {
        "registered extensions: \{extensions.join(", ")}"
      }
      "No configuration format reads \{path}; \{registered}"
    }
    Ambiguous(display~, candidates~, ..) =>
      "Ambiguous \{display} configuration; found: \{candidates.join(", ")}"
    InvalidKeyPath(text~, reason~) =>
      "Invalid configuration key path \"\{text}\": \{reason}"
    InvalidFilePath(text~, rule~) =>
      "Invalid file path \"\{text}\": \{rule.describe()}"
    InvalidEnvKey(variable~) =>
      "Invalid configuration environment variable \{variable}: a key segment is empty"
    MergeKey(key~, index~, source~) =>
      "Cannot merge list \{key} by key: item \{index} from \{source} has no key value or repeats one"
    InvalidSecret(key~, origin~, reason~) =>
      "Invalid secret reference \{key} in \{origin}: \{reason}"
    KeyNotFound(key~) => "Configuration key not found: \{key}"
    Render(format~, key~, reason~) =>
      "Cannot write configuration key \{key} as \{format}: \{reason}"
    SourceOrder(source~, priority~, previous~, previous_priority~) =>
      "Configuration source \{source} has priority \{priority}, lower than priority \{previous_priority} of the source \{previous} before it"
    OutsideBoundary(start~, boundary~) =>
      "The start directory \{start} is outside the boundary \{boundary}"
    InvalidEnvironmentName(name~, reason~) =>
      "Invalid environment name \"\{name}\": \{reason}"
    ConventionPolicy(input~, rule~) =>
      "Invalid convention policy \{input.escape()}: \{rule.description()}"
    UnknownProject(selector~, workspace~) =>
      "No project in the workspace \{workspace} matches \{selector}"
    AmbiguousProject(selector~, matches~) =>
      "More than one project matches \{selector}: \{matches.join(", ")}; select a member by its path"
    Decode(failure~) => failure.lines()
    Validation(failures~) => failures.map(BindFailure::lines).join("\n")
  }
}