///|
/// An argument id that can supply a configuration override. The standard
/// discovery ids cannot be mapped.
pub struct OverrideOption {
  priv id : String
} derive(Eq, Debug)

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

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

///|
/// A mapping id is empty or is reserved for host discovery.
pub suberror MappingError {
  InvalidOverrideOption(input~ : String, reason~ : String)
} derive(Debug)

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

///|
/// Parse a non-empty argparse id. Reject `environment`, `config`, `isolated`
/// `project`, `profiles` and `no-profiles`, which supply host discovery inputs
/// rather than config values.
pub fn OverrideOption::parse(text : String) -> OverrideOption raise MappingError {
  if text.is_empty() {
    raise InvalidOverrideOption(input=text, reason="the argument id must not be empty")
  }
  match text {
    "environment" | "config" | "isolated" | "project" | "profiles" | "no-profiles" =>
      raise InvalidOverrideOption(input=text, reason="the argument id is reserved for host discovery")
    _ => { id: text }
  }
}

///|
/// The argparse id of the mapping.
pub fn OverrideOption::to_string(self : OverrideOption) -> String {
  self.id
}

///|
/// The value shape of a mapped argument. Match the argparse declaration.
/// Matches omits option actions and count entries whose value is zero.
pub(all) enum MappingAction {
  Flag
  Count
  Set
  Append
}

///|
/// One explicit argument-to-key mapping with its declared value shape.
pub struct Mapping {
  priv option : OverrideOption
  priv key : @config.KeyPath
  priv action : MappingAction
  priv parse : ((String) -> @config.ConfigValue raise)?
}

///|
/// Map a parsed argument id to a parsed configuration key. `action` must match
/// the argument declaration: Flag, Count, Set or Append. The default is Set.
/// Append keeps a list with one value; Count keeps an integer even at zero.
/// With `parse`, convert each text value, or the boolean/count text for a flag.
pub fn map(
  option : OverrideOption,
  key : @config.KeyPath,
  action? : MappingAction = Set,
  parse? : (String) -> @config.ConfigValue raise,
) -> Mapping {
  { option, key, action, parse }
}

///|
/// Convert only explicit Argv and Env values into the priority-700 source.
/// Search the selected subcommand path from the root; a deeper explicit value
/// for the same id wins. A Default value never hides an explicit ancestor.
/// The application still dispatches subcommands. Conversion failures give
/// UsageError before the host loads configuration or creates services.
pub fn to_overrides(matches : @argparse.Matches, mappings : Array[Mapping]) -> @config.ConfigSource raise UsageError {
  let mut tree = @config.fields([])
  for mapping in mappings {
    let mut selected = None
    let mut current = Some(matches)
    while current is Some(node) {
      match node.sources.get(mapping.option.id) {
        Some(Argv) | Some(Env) => selected = Some(node)
        _ => ()
      }
      current = node.subcommand.map(child => child.1)
    }
    if selected is Some(node) {
      let value = mapped_value(node, mapping) catch {
        error => {
          let message = match error {
            UsageError::_ as e => e.message()
            @config.ConfigError::_ as e => e.message()
            e => e.to_string()
          }
          let input = match mapping.action {
            Count => node.flag_counts.get(mapping.option.id).unwrap_or(0).to_string()
            Flag => node.flags.get(mapping.option.id).unwrap_or(false).to_string()
            Set | Append => @debug.to_string(node.values.get(mapping.option.id).unwrap_or([]))
          }
          raise UsageError("Cannot map argument \"\{mapping.option.id}\" with value \{input} to \"\{mapping.key}\": \{message}")
        }
      }
      if value is Some(value) {
        let names = mapping.key.names()
        let mut branch = value
        for i = names.length() - 1; i >= 0; i = i - 1 {
          branch = @config.fields([(names[i], branch)])
        }
        tree = @config.merge(tree, branch) catch {
          error => raise UsageError(error.message())
        }
      }
    }
  }
  @config.ConfigSource::overrides_value(tree, priority=700) catch {
    error => raise UsageError(error.message())
  }
}

///|
fn mapped_value(matches : @argparse.Matches, mapping : Mapping) -> @config.ConfigValue? raise {
  let id = mapping.option.id
  match mapping.action {
    Count => {
      // Argparse omits a zero count from flag_counts, but keeps its flag and
      // source. The explicit-source gate has already excluded Default.
      guard matches.flags.contains(id) else { raise UsageError("Count mapping requires a count flag") }
      let count = matches.flag_counts.get(id).unwrap_or(0)
      Some(match mapping.parse {
        Some(parse) => parse(count.to_string())
        None => @config.int(count.to_int64())
      })
    }
    Flag => {
      guard matches.flags.get(id) is Some(flag) else { raise UsageError("Flag mapping requires a boolean flag") }
      Some(match mapping.parse {
        Some(parse) => parse(flag.to_string())
        None => @config.bool(flag)
      })
    }
    Set | Append => {
      guard matches.values.get(id) is Some(values) else { raise UsageError("Set or Append mapping requires a value argument") }
      let parse = mapping.parse.unwrap_or(text => @config.string(text))
      match mapping.action {
        Append => Some(@config.list(values.map(value => parse(value))))
        _ => match values { [.., last] => Some(parse(last)); _ => None }
      }
    }
  }
}