///|
/// Turn on environments: environment files such as `.Production.`
/// next to each project-tier primary. The name comes from the `environment`
/// option, then from `_ENVIRONMENT` in the variable set of the run,
/// then from `default`. With none of them, no environment file loads.
pub fn environments(
  conventions : Conventions,
  default? : String,
) -> Conventions {
  { ..conventions, environments: Some({ default, }) }
}

///|
/// The environment name of the run, or `None` when environments are off or no
/// source gives a name. The sources, from high to low: the `environment` value
/// of `options`, `_ENVIRONMENT` in `env_vars` (an empty value counts as
/// unset), and the default of `environments`. A name that breaks the name rules
/// raises `InvalidEnvironmentName`, which names the source of the name.
pub fn environment_name(
  conventions : Conventions,
  options? : DiscoveryOptions = DiscoveryOptions::new(),
  env_vars : Map[String, String],
) -> String? raise @config.ConfigError {
  guard conventions.environments is Some(environments) else { return None }
  let variable = "\{conventions.prefix}_ENVIRONMENT"
  let given = match (options.environment, env_vars.get(variable)) {
    (Some(name), _) => Some((name, "the discovery options"))
    (None, Some(name)) if !name.is_empty() => Some((name, variable))
    _ =>
      environments.default.map(name => {
        (name, "the default of \{conventions.display}")
      })
  }
  guard given is Some((name, source)) else { return None }
  check_environment_name(name, source~)
  Some(name)
}

///|
/// Check `name` with the environment name rules: it is not empty, it holds no
/// `/`, `\`, `.` or control character, and it is not `user` or `local` in any
/// letter case. A broken rule raises `InvalidEnvironmentName` with a reason
/// that names `source`, for example `--environment`.
pub fn check_environment_name(
  name : String,
  source~ : String,
) -> Unit raise @config.ConfigError {
  let reason = if name.is_empty() {
    Some("the name is empty")
  } else if name.to_lower() is ("user" | "local") {
    Some("the name is reserved")
  } else if name.iter().any(c => c is ('/' | '\\' | '.') || c.is_control()) {
    Some("the name contains /, \\, . or a control character")
  } else {
    None
  }
  if reason is Some(reason) {
    raise InvalidEnvironmentName(name~, reason="\{reason} (from \{source})")
  }
}

///|
/// The environment-file source next to a primary in `dir` with the file stem
/// `stem`: `..`, with the role `EnvironmentFile`.
fn Conventions::environment_file(
  self : Conventions,
  dir : @config.FileTreePath,
  stem : String,
  name : String,
  extensions : Array[String],
) -> @config.ConfigSource raise @config.ConfigError {
  @config.ConfigSource::candidates(
    texts(extensions.map(extension => dir.child("\{stem}.\{name}.\{extension}"))),
    display=self.display,
    role=EnvironmentFile,
    name=self.display,
  )
}