///|
fn join_arguments(arguments : Array[String]) -> String {
  arguments.join(" ")
}

///|
fn frozen_patterns(patterns : Array[String]) -> ReadOnlyArray[String] {
  ReadOnlyArray::from_array(patterns)
}

///|
fn trace_event(
  directive : @syntax.Directive,
  raw : String,
  expanded? : String,
  patterns : Array[String],
  decision : TraceDecision,
  message? : String,
) -> TraceEvent {
  {
    keyword: canonical_keyword(directive.keyword),
    raw,
    expanded,
    source: directive.location,
    matched_patterns: frozen_patterns(patterns),
    decision,
    message,
  }
}

///|
fn resolved_value(
  directive : @syntax.Directive,
  raw : String,
  expanded : String,
  patterns : Array[String],
) -> ResolvedValue {
  {
    keyword: canonical_keyword(directive.keyword),
    value: expanded,
    raw,
    expanded,
    source: directive.location,
    matched_patterns: frozen_patterns(patterns),
  }
}

///|
/// Host block matching remains based on the caller's original host, while
/// later token expansion and Match predicates observe effective HostName,
/// User, and Port values obtained from earlier directives.
fn context_after_values(
  context : ResolveContext,
  values : Map[String, ResolvedValue],
) -> ResolveContext {
  let host = match values.get("hostname") {
    Some(hostname) => hostname.expanded
    None => context.host
  }
  let remote_user = match values.get("user") {
    Some(user) => Some(user.expanded)
    None => context.remote_user
  }
  let port = match values.get("port") {
    Some(value) => {
      let parsed : Int = @strconv.from_str(value.expanded) catch {
        _ => context.port
      }
      parsed
    }
    None => context.port
  }
  {
    host,
    original_host: context.original_host,
    port,
    remote_user,
    local_user: context.local_user,
    home: context.home,
    local_hostname: context.local_hostname,
    tags: context.tags,
  }
}

///|
fn first_set_is_active(
  values : Map[String, ResolvedValue],
  keyword : String,
) -> Bool {
  if keyword == "proxycommand" || keyword == "proxyjump" {
    values.contains("proxycommand") || values.contains("proxyjump")
  } else {
    values.contains(keyword)
  }
}

///|
fn append_resolved(
  directive : @syntax.Directive,
  raw : String,
  expanded : String,
  patterns : Array[String],
  values : Map[String, ResolvedValue],
  ordered : Array[ResolvedValue],
  trace : Array[TraceEvent],
) -> Unit {
  let keyword = canonical_keyword(directive.keyword)
  let value = resolved_value(directive, raw, expanded, patterns)
  if !values.contains(keyword) {
    values[keyword] = value
  }
  ordered.push(value)
  trace.push(trace_event(directive, raw, expanded~, patterns, Appended))
}

///|
fn sendenv_pattern_matches(pattern : String, value : String) -> Bool {
  let mut pattern_index = 0
  let mut value_index = 0
  let mut star_index = -1
  let mut star_value_index = 0
  while value_index < value.length() {
    if pattern_index < pattern.length() &&
      (
        pattern[pattern_index] == '?' ||
        pattern[pattern_index] == value[value_index]
      ) {
      pattern_index += 1
      value_index += 1
    } else if pattern_index < pattern.length() && pattern[pattern_index] == '*' {
      star_index = pattern_index
      star_value_index = value_index
      pattern_index += 1
    } else if star_index >= 0 {
      star_value_index += 1
      value_index = star_value_index
      pattern_index = star_index + 1
    } else {
      return false
    }
  }
  while pattern_index < pattern.length() && pattern[pattern_index] == '*' {
    pattern_index += 1
  }
  pattern_index == pattern.length()
}

///|
fn remove_sendenv_pattern(
  values : Map[String, ResolvedValue],
  ordered : Array[ResolvedValue],
  pattern : String,
) -> Unit {
  let remaining = ordered.filter(item => {
    item.keyword != "sendenv" || !sendenv_pattern_matches(pattern, item.value)
  })
  ordered.clear()
  ordered.append(remaining)
  values.remove("sendenv")
  for value in ordered {
    if value.keyword == "sendenv" {
      values["sendenv"] = value
      return
    }
  }
}

///|
fn apply_sendenv(
  directive : @syntax.Directive,
  patterns : Array[String],
  values : Map[String, ResolvedValue],
  ordered : Array[ResolvedValue],
  trace : Array[TraceEvent],
) -> Unit {
  for argument in directive.arguments {
    if argument.has_prefix("-") && argument.length() > 1 {
      let removal_pattern = argument[1:].to_owned()
      remove_sendenv_pattern(values, ordered, removal_pattern)
      trace.push(
        trace_event(
          directive,
          argument,
          expanded=argument,
          patterns,
          Accepted,
          message="negative SendEnv pattern removes matching accumulated names",
        ),
      )
    } else {
      append_resolved(
        directive, argument, argument, patterns, values, ordered, trace,
      )
    }
  }
}

///|
fn apply_setenv(
  directive : @syntax.Directive,
  patterns : Array[String],
  context : ResolveContext,
  values : Map[String, ResolvedValue],
  ordered : Array[ResolvedValue],
  trace : Array[TraceEvent],
) -> Unit raise ResolveError {
  for argument in directive.arguments {
    match argument.find("=") {
      None =>
        trace.push(
          trace_event(
            directive,
            argument,
            patterns,
            Rejected,
            message="SetEnv requires NAME=value",
          ),
        )
      Some(index) =>
        if index == 0 {
          trace.push(
            trace_event(
              directive,
              argument,
              patterns,
              Rejected,
              message="SetEnv variable name is empty",
            ),
          )
        } else {
          let name = argument[:index].to_owned()
          let raw_value = argument[index + 1:].to_owned()
          let key = "setenv.\{name}"
          if values.contains(key) {
            trace.push(
              trace_event(
                directive,
                argument,
                patterns,
                Shadowed,
                message="a previous SetEnv value for this name is already active",
              ),
            )
          } else {
            let specification = spec("setenv", SetEnv, Text, sensitive=true)
            let expanded_value = expand_tokens_at(
              raw_value,
              specification,
              context,
              directive.location,
            )
            let expanded = "\{name}=\{expanded_value}"
            let value = resolved_value(directive, argument, expanded, patterns)
            values[key] = value
            ordered.push(value)
            trace.push(
              trace_event(directive, argument, expanded~, patterns, Accepted),
            )
          }
        }
    }
  }
}

///|
fn apply_directive(
  directive : @syntax.Directive,
  patterns : Array[String],
  context : ResolveContext,
  values : Map[String, ResolvedValue],
  ordered : Array[ResolvedValue],
  trace : Array[TraceEvent],
) -> Unit raise ResolveError {
  let keyword = canonical_keyword(directive.keyword)
  let raw = join_arguments(directive.arguments)
  match directive_spec(keyword) {
    None =>
      trace.push(
        trace_event(
          directive,
          raw,
          patterns,
          Unsupported,
          message="directive is outside the P0 support matrix",
        ),
      )
    Some(specification) =>
      match specification.merge {
        FirstSet =>
          if first_set_is_active(values, keyword) {
            trace.push(
              trace_event(
                directive,
                raw,
                patterns,
                Shadowed,
                message="first obtained value remains active",
              ),
            )
          } else {
            match validate_directive_arguments(keyword, directive.arguments) {
              Some(message) =>
                trace.push(
                  trace_event(directive, raw, patterns, Rejected, message~),
                )
              None => {
                let normalized = normalized_directive_value(
                  specification,
                  directive.arguments,
                )
                let expanded = expand_tokens_at(
                  normalized,
                  specification,
                  context,
                  directive.location,
                )
                let value = resolved_value(directive, raw, expanded, patterns)
                values[keyword] = value
                ordered.push(value)
                trace.push(
                  trace_event(directive, raw, expanded~, patterns, Accepted),
                )
              }
            }
          }
        Append =>
          match validate_directive_arguments(keyword, directive.arguments) {
            Some(message) =>
              trace.push(
                trace_event(directive, raw, patterns, Rejected, message~),
              )
            None =>
              if keyword == "sendenv" {
                apply_sendenv(directive, patterns, values, ordered, trace)
              } else {
                let normalized = normalized_directive_value(
                  specification,
                  directive.arguments,
                )
                let expanded = expand_tokens_at(
                  normalized,
                  specification,
                  context,
                  directive.location,
                )
                append_resolved(
                  directive, raw, expanded, patterns, values, ordered, trace,
                )
              }
          }
        SetEnv =>
          match validate_directive_arguments(keyword, directive.arguments) {
            Some(message) =>
              trace.push(
                trace_event(directive, raw, patterns, Rejected, message~),
              )
            None =>
              apply_setenv(directive, patterns, context, values, ordered, trace)
          }
      }
  }
}

///|
fn apply_directives(
  directives : Array[@syntax.Directive],
  patterns : Array[String],
  context : ResolveContext,
  values : Map[String, ResolvedValue],
  ordered : Array[ResolvedValue],
  trace : Array[TraceEvent],
) -> ResolveContext raise ResolveError {
  let mut effective_context = context
  for directive in directives {
    apply_directive(
      directive, patterns, effective_context, values, ordered, trace,
    )
    effective_context = context_after_values(effective_context, values)
  }
  effective_context
}

///|
fn match_context(context : ResolveContext) -> @pattern.MatchContext {
  {
    host: context.host,
    original_host: context.original_host,
    remote_user: context.remote_user,
    local_user: context.local_user,
    tags: context.tags,
  }
}

///|
fn pattern_error_summary(error : @pattern.PatternError) -> String {
  match error {
    EmptyPatternList => "Match condition has an empty pattern list"
    EmptyPattern(..) => "Match condition contains an empty pattern"
    PatternTooLong(..) => "Match pattern exceeds the configured resource limit"
    TooManyPatterns(..) => "Match condition has too many patterns"
    MissingMatchArgument(..) => "Match condition requires a pattern list"
    InvalidAllCondition => "Match all must be the only condition"
    UnsupportedMatchCondition(name~) => "unsupported Match condition: \{name}"
  }
}

///|
fn invalid_match_event(
  block : @syntax.MatchBlock,
  message : String,
) -> TraceEvent {
  {
    keyword: "match",
    raw: block.conditions.join(" "),
    expanded: None,
    source: block.location,
    matched_patterns: ReadOnlyArray::default(),
    decision: Unsupported,
    message: Some(message),
  }
}

///|
/// A Match line is an AND over its predicates. Parsing errors are converted to
/// a trace event rather than propagated as a pattern implementation detail.
fn match_block_matches(
  block : @syntax.MatchBlock,
  context : ResolveContext,
  trace : Array[TraceEvent],
) -> Bool {
  let predicates = @pattern.parse_match_predicates(block.conditions) catch {
    error => {
      trace.push(invalid_match_event(block, pattern_error_summary(error)))
      []
    }
  }
  if predicates.is_empty() {
    return false
  }
  let projected = match_context(context)
  for predicate in predicates {
    if !predicate.matches(projected) {
      return false
    }
  }
  true
}

///|
/// Resolve an already parsed configuration. The pure resolver has no file or
/// environment access; callers supply every context value explicitly.
pub fn resolve_explained(
  config : @syntax.Config,
  context : ResolveContext,
) -> ResolveOutcome raise ResolveError {
  let values : Map[String, ResolvedValue] = Map([])
  let ordered : Array[ResolvedValue] = []
  let trace : Array[TraceEvent] = []
  let mut effective_context = context
  for item in config.items {
    match item {
      Global(directives) =>
        effective_context = apply_directives(
          directives,
          ["*"],
          effective_context,
          values,
          ordered,
          trace,
        )
      Host(block) =>
        if @pattern.patterns_match(block.patterns, context.host) {
          effective_context = apply_directives(
            block.directives,
            block.patterns,
            effective_context,
            values,
            ordered,
            trace,
          )
        }
      Match(block) =>
        if match_block_matches(block, effective_context, trace) {
          effective_context = apply_directives(
            block.directives,
            ["Match " + block.conditions.join(" ")],
            effective_context,
            values,
            ordered,
            trace,
          )
        }
    }
  }
  { config: { values, ordered }, trace: ReadOnlyArray::from_array(trace) }
}

///|
/// Resolve without retaining the explain trace. It shares exactly the same
/// evaluator as `resolve_explained`, preventing drift between both APIs.
pub fn resolve(
  config : @syntax.Config,
  context : ResolveContext,
) -> ResolvedConfig raise ResolveError {
  resolve_explained(config, context).config
}

///|
/// Resolve in validation-strict mode. It shares the same evaluator and trace
/// as the lenient API, then promotes the first rejected supported directive to
/// a checked error. Unsupported directives stay observable through lint and
/// explain rather than changing the default compatibility policy.
pub fn resolve_strict(
  config : @syntax.Config,
  context : ResolveContext,
) -> ResolvedConfig raise ResolveError {
  let outcome = resolve_explained(config, context)
  for event in outcome.trace {
    if event.decision == Rejected {
      let message = match event.message {
        Some(value) => value
        None => "invalid directive value"
      }
      raise InvalidDirectiveValue(
        keyword=event.keyword,
        value=event.raw,
        message~,
        source=event.source,
      )
    }
  }
  outcome.config
}