///|
/// The context required to evaluate OpenSSH `Match` conditions without a
/// dependency on the resolver's own context type.
pub(all) struct MatchContext {
  host : String
  original_host : String
  remote_user : String?
  local_user : String?
  tags : Array[String]
} derive(Debug, Eq)

///|
/// Construct a Match context. `original_host` defaults to `host` because the
/// common non-canonicalized path preserves the original user input.
pub fn MatchContext::new(
  host : String,
  original_host? : String = host,
  remote_user? : String,
  local_user? : String,
  tags? : Array[String] = [],
) -> MatchContext {
  { host, original_host, remote_user, local_user, tags }
}

///|
/// P0 Match predicates. Unsupported OpenSSH conditions are rejected by the
/// parser instead of being represented here, so this enum is always safe to
/// evaluate without shelling out or canonicalizing a hostname.
pub(all) enum MatchPredicate {
  All
  Not(MatchPredicate)
  Host(PatternList)
  OriginalHost(PatternList)
  User(PatternList)
  LocalUser(PatternList)
  Tagged(PatternList)
} derive(Debug, Eq)

///|
fn predicate_list(
  condition : String,
  arguments : Array[String],
) -> PatternList raise PatternError {
  if arguments.length() == 0 {
    raise MissingMatchArgument(condition~)
  }
  compile_pattern_list(arguments)
}

///|
/// Parse one normalized Match condition and its pattern-list arguments.
/// `exec`, `canonical`, `final`, and unknown conditions deliberately produce
/// `UnsupportedMatchCondition`: P0 never executes a command or guesses an
/// OpenSSH canonicalization phase.
pub fn parse_match_predicate(
  condition : String,
  arguments : Array[String],
) -> MatchPredicate raise PatternError {
  let normalized = lower_ascii(condition)
  let (name, negated) = if normalized.has_prefix("!") {
    (normalized[1:].to_owned(), true)
  } else {
    (normalized, false)
  }
  let predicate = match name {
    "all" => {
      if !arguments.is_empty() {
        raise InvalidAllCondition
      }
      All
    }
    "host" => Host(predicate_list(name, arguments))
    "originalhost" => OriginalHost(predicate_list(name, arguments))
    "user" => User(predicate_list(name, arguments))
    "localuser" => LocalUser(predicate_list(name, arguments))
    "tagged" => Tagged(predicate_list(name, arguments))
    _ => raise UnsupportedMatchCondition(name=condition)
  }
  if negated {
    Not(predicate)
  } else {
    predicate
  }
}

///|
fn split_comma_patterns(value : String) -> Array[String] {
  let patterns : Array[String] = []
  for part in value.split(",") {
    patterns.push(part.to_owned())
  }
  patterns
}

///|
/// Parse interleaved Match tokens such as
/// `["host", "*.corp", "user", "deploy,admin"]`. Every condition other
/// than `all` consumes its following comma-separated pattern-list token.
pub fn parse_match_predicates(
  tokens : Array[String],
) -> Array[MatchPredicate] raise PatternError {
  let predicates : Array[MatchPredicate] = []
  let mut index = 0
  while index < tokens.length() {
    let condition = tokens[index]
    let normalized = lower_ascii(condition)
    let name = if normalized.has_prefix("!") {
      normalized[1:].to_owned()
    } else {
      normalized
    }
    if name == "all" {
      if tokens.length() != 1 {
        raise InvalidAllCondition
      }
      predicates.push(parse_match_predicate(condition, []))
      index = index + 1
    } else if index + 1 >= tokens.length() {
      raise MissingMatchArgument(condition~)
    } else {
      predicates.push(
        parse_match_predicate(
          condition,
          split_comma_patterns(tokens[index + 1]),
        ),
      )
      index = index + 2
    }
  }
  predicates
}

///|
fn optional_decision(
  patterns : PatternList,
  value : String?,
) -> PatternDecision {
  match value {
    Some(actual) => patterns.decide(actual)
    None => { matched: false, positive_index: None, negative_index: None }
  }
}

///|
/// Evaluate a Match predicate. `Tagged` succeeds if any tag matches its
/// pattern-list; it does not require every tag to match.
pub fn MatchPredicate::decide(
  self : MatchPredicate,
  context : MatchContext,
) -> PatternDecision {
  match self {
    All => { matched: true, positive_index: None, negative_index: None }
    Not(predicate) => {
      let decision = predicate.decide(context)
      { ..decision, matched: !decision.matched }
    }
    Host(patterns) => patterns.decide(context.host)
    OriginalHost(patterns) => patterns.decide(context.original_host)
    User(patterns) => optional_decision(patterns, context.remote_user)
    LocalUser(patterns) => optional_decision(patterns, context.local_user)
    Tagged(patterns) => {
      let mut first_positive : Int? = None
      let mut first_negative : Int? = None
      for tag in context.tags {
        let decision = patterns.decide(tag)
        if decision.negative_index is Some(index) && first_negative is None {
          first_negative = Some(index)
        }
        if decision.positive_index is Some(index) && first_positive is None {
          first_positive = Some(index)
        }
      }
      {
        matched: first_positive is Some(_) && first_negative is None,
        positive_index: first_positive,
        negative_index: first_negative,
      }
    }
  }
}

///|
/// Evaluate a Match predicate without recording explain information.
pub fn MatchPredicate::matches(
  self : MatchPredicate,
  context : MatchContext,
) -> Bool {
  self.decide(context).matched
}