///|
pub(all) struct ResolveContext {
  host : String
  original_host : String
  port : Int
  remote_user : String?
  local_user : String?
  home : String?
  local_hostname : String?
  tags : Array[String]
} derive(Debug, Eq)

///|
pub fn ResolveContext::new(
  host : String,
  port? : Int = 22,
  remote_user? : String,
  local_user? : String,
  home? : String,
  local_hostname? : String,
  tags? : Array[String] = [],
) -> ResolveContext {
  {
    host,
    original_host: host,
    port,
    remote_user,
    local_user,
    home,
    local_hostname,
    tags,
  }
}

///|
pub(all) struct ResolvedValue {
  keyword : String
  value : String
  raw : String
  expanded : String
  source : @syntax.SourceLocation
  matched_patterns : ReadOnlyArray[String]
} derive(Debug, Eq)

///|
/// The result of applying one directive candidate. The trace is ordered by
/// source order and is suitable for explain and lint without re-running the
/// resolver.
pub(all) enum TraceDecision {
  Accepted
  Appended
  Shadowed
  Rejected
  Unsupported
} derive(Debug, Eq)

///|
pub(all) struct TraceEvent {
  keyword : String
  raw : String
  expanded : String?
  source : @syntax.SourceLocation
  matched_patterns : ReadOnlyArray[String]
  decision : TraceDecision
  message : String?
} derive(Debug, Eq)

///|
pub(all) struct ResolveOutcome {
  config : ResolvedConfig
  trace : ReadOnlyArray[TraceEvent]
} derive(Debug, Eq)

///|
pub struct ResolvedConfig {
  values : Map[String, ResolvedValue]
  ordered : Array[ResolvedValue]
} derive(Debug, Eq)

///|
pub fn ResolvedConfig::get(
  self : ResolvedConfig,
  keyword : String,
) -> ResolvedValue? {
  self.values.get(canonical_keyword(keyword))
}

///|
pub fn ResolvedConfig::entries(self : ResolvedConfig) -> Array[ResolvedValue] {
  self.ordered.copy()
}

///|
/// Return every final value for a directive. Append directives keep source
/// order; scalar directives return at most one value.
pub fn ResolvedConfig::get_all(
  self : ResolvedConfig,
  keyword : String,
) -> Array[ResolvedValue] {
  let result : Array[ResolvedValue] = []
  let normalized = canonical_keyword(keyword)
  for value in self.ordered {
    if value.keyword == normalized {
      result.push(value)
    }
  }
  result
}

///|
pub fn ResolveOutcome::events(self : ResolveOutcome) -> Array[TraceEvent] {
  [
    for event in self.trace => event
  ]
}

///|
/// Return whether evaluation recorded an invalid supported directive. Unknown
/// directives are intentionally `Unsupported`, not errors, so callers can
/// choose a compatibility or lint policy without losing the parsed config.
pub fn ResolveOutcome::has_rejections(self : ResolveOutcome) -> Bool {
  for event in self.trace {
    if event.decision == Rejected {
      return true
    }
  }
  false
}

///|
pub(all) suberror ResolveError {
  InvalidPort(value~ : String, source~ : @syntax.SourceLocation)
  UnsupportedToken(
    token~ : String,
    keyword~ : String,
    source~ : @syntax.SourceLocation
  )
  MissingTokenContext(
    token~ : String,
    keyword~ : String,
    source~ : @syntax.SourceLocation
  )
  InvalidDirectiveValue(
    keyword~ : String,
    value~ : String,
    message~ : String,
    source~ : @syntax.SourceLocation
  )
  UnsupportedMatchCondition(name~ : String, source~ : @syntax.SourceLocation)
} derive(Debug, Eq)