///|
/// The parsed inputs of a run, before configuration or services exist.
priv struct ParsedArguments {
  matches : @argparse.Matches
  discovery_options : @conventions.DiscoveryOptions
  overrides : @config.ConfigSource?
}

///|
/// A help or version request of the argument parse, with the text that argparse
/// renders. It is not a failure: the driver writes the text one time to standard
/// output, and the run ends with code 0 and the reason `Requested`, before the
/// configuration load (design D6). It is an error, as `StopRequested` is from the
/// factory, so that it ends the run at the parse in each driver.
priv suberror DisplayRequested {
  DisplayRequested(String)
}

///|
/// Parse once before the driver loads configuration or calls the factory.
/// The driver supplies the resolved variable snapshot, shared by all readers.
/// A parse error raises `UsageError`. A help or version request raises
/// `DisplayRequested`. The parse never ends the process.
fn[S] parse_arguments(host : Host[S], env~ : @sorted_map.SortedMap[String, String]) -> ParsedArguments raise {
  let command = match host.make_command {
    Some(make) => make(host.name.to_string())
    None => @argparse.Command(host.name.to_string())
  }
  let parsed = command.try_parse(argv=host.args[:], env=Map::from_iter(env.iter())) catch {
    error => raise UsageError(error.to_string())
  }
  let matches = match parsed {
    Parsed(matches) => matches
    Help(text) | Version(text) => raise DisplayRequested(text)
  }
  let environment = standard_option(matches, "environment")
  let config = match standard_option(matches, "config") {
    Some(path) => @config.SourceSelection::Explicit(path)
    None => @config.SourceSelection::Discover
  }
  let project = standard_option(matches, "project")
  let isolated = matches.flags.get("isolated").unwrap_or(false)
  let profiles = profile_options(matches)
  let overrides = if host.mappings.is_empty() { None } else { Some(to_overrides(matches, host.mappings)) }
  { matches, discovery_options: @conventions.DiscoveryOptions::new(environment?, config~, isolated~, project?, profiles~), overrides }
}

///|
/// Standard profile lists are parsed at the argument boundary, including
/// when no profile policy is enabled. The global argparse result preserves
/// repeated option order across subcommands.
fn profile_options(matches : @argparse.Matches) -> @config.ProfileSelection raise UsageError {
  let values = matches.values.get("profiles").unwrap_or([])
  let disabled = matches.flags.get("no-profiles").unwrap_or(false)
  if disabled && !values.is_empty() {
    raise UsageError("--no-profiles conflicts with --profiles")
  }
  if disabled { return Disabled }
  if values.is_empty() { return Unspecified }
  let names = []
  for value in values {
    let parsed = @config.ProfileNames::parse_csv(value, origin=Discovery) catch {
      error => raise UsageError(error.message())
    }
    names.append(parsed.names())
  }
  Selected(@config.ProfileNames::new(names))
}

///|
/// Empty standard values fail at the argument boundary. The host resolves and
/// parses the environment name next. Config and project selectors keep
/// conventions text.
fn standard_option(matches : @argparse.Matches, id : String) -> String? raise UsageError {
  guard matches.values.get(id) is Some([.., value]) else { return None }
  if value.is_empty() { raise UsageError("--\{id} requires a non-empty value") }
  Some(value)
}

///|
/// Set the command factory. The run gives it the application name and parses
/// its command once, before configuration and hooks.
pub fn[S] command(builder : HostBuilder[S], make : (String) -> @argparse.Command, mappings? : Array[Mapping] = []) -> HostBuilder[S] {
  { ..builder, make_command: Some(make), mappings: mappings.copy() }
}