///|
/// An immutable profile policy. Fallback roots and includes have no implicit
/// names. Groups are checked at construction and again as one effective graph.
pub struct Policy {
  priv fallback : @config.ProfileNames
  priv includes : @config.ProfileNames
  priv groups : Array[Group]
}

///|
priv struct Group {
  name : @config.ProfileName
  members : @config.ProfileNames
  origin : @config.ProfileOrigin
}

///|
/// Enable profile processing with no implicit default profile.
pub fn Policy::new() -> Policy {
  { fallback: @config.ProfileNames::new([]), includes: @config.ProfileNames::new([]), groups: [] }
}

///|
/// Set the fallback roots, used only when no higher selector is present.
pub fn fallback(policy : Policy, names : @config.ProfileNames) -> Policy {
  { ..policy, fallback: names }
}

///|
/// Set the policy includes, which precede file includes and selected roots.
pub fn includes(policy : Policy, names : @config.ProfileNames) -> Policy {
  { ..policy, includes: names }
}

///|
/// Define or replace one group. At most 64 groups with 64 members each are
/// permitted. The complete effective graph is checked during resolution.
pub fn group(policy : Policy, name : @config.ProfileName, members : @config.ProfileNames) -> Policy raise @config.ConfigError {
  let definition : Group = { name, members, origin: Policy }
  check_members(definition)
  let groups = policy.groups.filter(existing => existing.name != name)
  groups.push(definition)
  check_group_count(groups)
  { ..policy, groups }
}

///|
priv struct Controls {
  active : (@config.ProfileNames, @config.Origin)?
  defaults : (@config.ProfileNames, @config.Origin)?
  includes : @config.ProfileNames
  groups : Array[Group]
  origins : Array[@config.ProfileOrigin]
  include_origin : @config.ProfileOrigin
}

///|
fn empty_controls() -> Controls {
  { active: None, defaults: None, includes: @config.ProfileNames::new([]), groups: [], origins: [], include_origin: Policy }
}

///|
/// Resolve one immutable activation state. `variable` is a present selector
/// variable's name and value. `controls` is the authoritative base root and
/// its origin. Disabled selections never interpret controls or group graphs.
pub fn resolve(policy : Policy, selection : @config.ProfileSelection, variable? : (String, String), controls? : (@config.ConfigValue, @config.Origin)) -> @config.ProfileResolution raise @config.ConfigError {
  resolve_input(policy, SelectionInput::new(selection, variable?), controls?)
}

///|
/// A parsed selection boundary with its winning source. Preparing it lets
/// discovery decide whether it may read controls without parsing names again.
pub struct SelectionInput {
  priv selection : @config.ProfileSelection
  priv selected_by : @config.ProfileSelector
}

///|
/// Parse a present variable only when the explicit selection is Unspecified.
pub fn SelectionInput::new(selection : @config.ProfileSelection, variable? : (String, String)) -> SelectionInput raise @config.ConfigError {
  let (selection, selected_by) = match (selection, variable) {
    (Unspecified, Some((name, text))) => (@config.ProfileSelection::from_variable(text, variable=name), @config.ProfileSelector::Variable(name))
    (Unspecified, None) => (selection, @config.ProfileSelector::Fallback)
    _ => (selection, @config.ProfileSelector::Discovery)
  }
  { selection, selected_by }
}

///|
/// Whether discovery must suppress control interpretation and profile reads.
pub fn SelectionInput::disabled(self : SelectionInput) -> Bool {
  self.selection is Disabled
}

///|
/// Resolve a prepared typed selection. This never reparses its selected names.
pub fn resolve_input(policy : Policy, input : SelectionInput, controls? : (@config.ConfigValue, @config.Origin)) -> @config.ProfileResolution raise @config.ConfigError {
  let parsed = if input.disabled() { empty_controls() } else {
    match controls {
      Some((root, origin)) => parse_controls(root, _ => origin)
      None => empty_controls()
    }
  }
  resolve_prepared(policy, input, parsed)
}

///|
fn resolve_prepared(policy : Policy, input : SelectionInput, controls : Controls, authorities? : Array[@config.Origin] = []) -> @config.ProfileResolution raise @config.ConfigError {
  let selection = input.selection
  let selected_by = input.selected_by
  if selection is Disabled {
    let empty = @config.ProfileNames::new([])
    return @config.ProfileResolution::new(selected_by, empty, empty, disabled=true)
  }
  let (selector, roots, root_origin) = match selection {
    Selected(names) => (selected_by, names, selector_origin(selected_by))
    Unspecified => match (controls.active, controls.defaults) {
      (Some((names, origin)), _) => (@config.ProfileSelector::BaseActive(origin), names, @config.ProfileOrigin::Control(origin, @config.KeyPath::parse("profiles.active")))
      (None, Some((names, origin))) => (@config.ProfileSelector::BaseDefault(origin), names, @config.ProfileOrigin::Control(origin, @config.KeyPath::parse("profiles.default")))
      _ => (@config.ProfileSelector::Fallback, policy.fallback, @config.ProfileOrigin::Policy)
    }
    Disabled => abort("disabled selection returned before control parsing")
  }
  let groups = policy.groups.filter(group => !controls.groups.any(replacement => replacement.name == group.name)) + controls.groups
  check_group_count(groups)
  let ordered = groups.copy()
  ordered.sort_by((left, right) => left.name.to_string().compare(right.name.to_string()))
  // Validate every definition, including inactive groups, in lexical order.
  let heights : Map[@config.ProfileName, Int] = Map([])
  for definition in ordered { ignore(group_height(definition, ordered, [], heights)) }
  let memo : Map[@config.ProfileName, Array[(@config.ProfileName, @config.ProfileReason)]] = Map([])
  let expanded : Array[(@config.ProfileName, @config.ProfileReason)] = []
  for name in policy.includes.names() {
    append_root(expanded, name, Include(name, Policy), ordered, memo)
  }
  for name in controls.includes.names() {
    append_root(expanded, name, Include(name, controls.include_origin), ordered, memo)
  }
  for name in roots.names() { append_root(expanded, name, Root(name, root_origin), ordered, memo) }
  let resolved = keep_last(expanded)
  let failure_origin = resolved.get(128).map(item => match item.1 {
    Root(_, origin) | Include(_, origin) | GroupMember(_, _, origin) => origin
  }).unwrap_or(root_origin)
  @config.ProfileResolution::new(selector, roots, @config.ProfileNames::new(resolved.map(item => item.0)), reasons=resolved.map(item => item.1), controls=controls.origins, authorities~, failure_origin~)
}

///|
fn selector_origin(selector : @config.ProfileSelector) -> @config.ProfileOrigin raise @config.ConfigError {
  match selector {
    Discovery => Discovery
    Variable(name) => Variable(name)
    BaseActive(origin) | BaseDefault(origin) => Control(origin, @config.KeyPath::parse("profiles"))
    Fallback => Policy
  }
}

///|
fn control_origin(origin : @config.Origin, names : Array[String]) -> @config.ProfileOrigin raise @config.ConfigError {
  Control(origin, @config.KeyPath::from_names(names))
}

///|
fn control_fail(input : String, rule : @config.ProfileRule, origin : @config.ProfileOrigin) -> @config.ConfigError {
  @config.ConfigError::Profile(failure=@config.ProfileFailure::new(input, rule, origin~))
}

///|
fn parse_names(value : @ion.Ion, origin : @config.ProfileOrigin) -> @config.ProfileNames raise @config.ConfigError {
  guard value.kind() is List(items) else { raise control_fail("profile name list", ExpectedList, origin) }
  let names = []
  for item in items {
    guard item.kind() is String(text) else { raise control_fail("profile name", ExpectedString, origin) }
    if text.contains("${") { raise control_fail(text, NonLiteral, origin) }
    names.push(@config.ProfileName::parse(text, origin~))
  }
  @config.ProfileNames::new(names)
}

///|
fn parse_controls(root : @config.ConfigValue, source_for : (Array[String]) -> @config.Origin raise @config.ConfigError) -> Controls raise @config.ConfigError {
  guard root.to_ion().kind() is Struct(fields) else { return empty_controls() }
  let mut table : @ion.Ion? = None
  for field in fields { if field.name().text() == Some("profiles") { table = Some(field.value()) } }
  guard table is Some(table) else { return empty_controls() }
  let origin = control_origin(source_for(["profiles"]), ["profiles"])
  guard table.kind() is Struct(fields) else { raise control_fail("profiles", ExpectedTable, origin) }
  let mut active = None
  let mut defaults = None
  let mut includes = @config.ProfileNames::new([])
  let mut include_origin = @config.ProfileOrigin::Policy
  let groups : Array[Group] = []
  let origins : Array[@config.ProfileOrigin] = [origin]
  for field in fields {
    guard field.name().text() is Some(name) else { continue }
    let source = source_for(["profiles", name])
    let origin = control_origin(source, ["profiles", name])
    origins.push(origin)
    match name {
      "active" => active = Some((parse_names(field.value(), origin), source))
      "default" => defaults = Some((parse_names(field.value(), origin), source))
      "include" => { includes = parse_names(field.value(), origin); include_origin = origin }
      "groups" => {
        guard field.value().kind() is Struct(definitions) else { raise control_fail("profiles.groups", ExpectedTable, origin) }
        let definitions = definitions.copy()
        definitions.sort_by((left, right) => left.name().text().unwrap_or("").compare(right.name().text().unwrap_or("")))
        for definition in definitions {
          guard definition.name().text() is Some(text) else { continue }
          let origin = control_origin(source_for(["profiles", "groups", text]), ["profiles", "groups", text])
          origins.push(origin)
          let group : Group = { name: @config.ProfileName::parse(text, origin~), members: parse_names(definition.value(), origin), origin }
          check_members(group)
          groups.push(group)
        }
      }
      _ => raise control_fail(name, UnknownField, origin)
    }
  }
  check_group_count(groups)
  { active, defaults, includes, groups, origins, include_origin }
}

///|
fn check_members(group : Group) -> Unit raise @config.ConfigError {
  let count = group.members.names().length()
  if count > 64 { raise control_fail(group.name.to_string(), MemberLimit(count), group.origin) }
}

///|
fn check_group_count(groups : Array[Group]) -> Unit raise @config.ConfigError {
  if groups.length() > 64 {
    let origin = groups.last().map(group => group.origin).unwrap_or(@config.ProfileOrigin::Policy)
    raise control_fail("profiles.groups", GroupLimit(groups.length()), origin)
  }
}

///|
fn find_group(groups : Array[Group], name : @config.ProfileName) -> Group? {
  for group in groups { if group.name == name { return Some(group) } }
  None
}

///|
fn group_height(group : Group, groups : Array[Group], stack : Array[@config.ProfileName], memo : Map[@config.ProfileName, Int]) -> Int raise @config.ConfigError {
  if stack.contains(group.name) {
    let index = stack.search(group.name).unwrap_or(0)
    let cycle = @config.ProfileNames::new([..stack[index:].to_owned(), group.name])
    raise @config.ConfigError::Profile(failure=@config.ProfileFailure::new(group.name.to_string(), Cycle, origin=group.origin, cycle~))
  }
  if memo.get(group.name) is Some(height) { return height }
  let mut height = 1
  for child_name in group.members.names() {
    if find_group(groups, child_name) is Some(child) {
      height = height.max(1 + group_height(child, groups, [..stack, group.name], memo))
    }
  }
  if height > 32 { raise control_fail(group.name.to_string(), DepthLimit(height), group.origin) }
  memo[group.name] = height
  height
}

///|
fn append_root(output : Array[(@config.ProfileName, @config.ProfileReason)], name : @config.ProfileName, reason : @config.ProfileReason, groups : Array[Group], memo : Map[@config.ProfileName, Array[(@config.ProfileName, @config.ProfileReason)]]) -> Unit {
  output.push((name, reason))
  if find_group(groups, name) is Some(group) { output.append(expand_group(group, groups, memo)) }
  // Deduplicate after each root. Each array stays bounded by the definitions'
  // distinct names even for a diamond with exponentially many paths.
  let kept = keep_last(output)
  output.clear()
  output.append(kept)
}

///|
fn expand_group(group : Group, groups : Array[Group], memo : Map[@config.ProfileName, Array[(@config.ProfileName, @config.ProfileReason)]]) -> Array[(@config.ProfileName, @config.ProfileReason)] {
  if memo.get(group.name) is Some(expanded) { return expanded }
  let output = []
  for child_name in group.members.names() {
    append_root(output, child_name, GroupMember(child_name, group.name, group.origin), groups, memo)
  }
  memo[group.name] = output
  output
}

///|
fn keep_last(items : Array[(@config.ProfileName, @config.ProfileReason)]) -> Array[(@config.ProfileName, @config.ProfileReason)] {
  let seen : Set[@config.ProfileName] = Set([])
  let kept = []
  for item in items.rev() { if !seen.contains(item.0) { seen.add(item.0); kept.push(item) } }
  kept.rev()
}