package main

import "strings"

// ---------------------------------------------------------------------------
// OpenSSH pattern matching (a port of match.c)
// ---------------------------------------------------------------------------
//
// These are NOT filepath.Match / path.Match. OpenSSH implements its own glob
// over bytes with exactly two metacharacters:
//
//	*  matches zero or more characters, INCLUDING '.' and '/'
//	?  matches exactly one character
//
// There are no character classes, no escaping, and no separator awareness.
// Matching is byte-by-byte and case sensitive; ssh lowercases the target host
// before matching but leaves the pattern from the config file untouched.

// matchPattern is a faithful port of OpenSSH match.c:match_pattern().
func matchPattern(s, pattern string) bool {
	for {
		// If at end of pattern, accept if also at end of string.
		if len(pattern) == 0 {
			return len(s) == 0
		}

		if pattern[0] == '*' {
			// Skip the asterisk.
			pattern = pattern[1:]

			// If at end of pattern, accept immediately.
			if len(pattern) == 0 {
				return true
			}

			// If next character in pattern is known, optimize.
			if pattern[0] != '?' && pattern[0] != '*' {
				// Look for instances of the next character in
				// pattern, and try to match starting from those.
				for i := 0; i < len(s); i++ {
					if s[i] == pattern[0] && matchPattern(s[i+1:], pattern[1:]) {
						return true
					}
				}
				return false
			}
			// Move ahead one character at a time and try to match
			// at each position.
			for i := 0; i < len(s); i++ {
				if matchPattern(s[i:], pattern) {
					return true
				}
			}
			return false
		}

		// There must be at least one more character in the string.
		if len(s) == 0 {
			return false
		}

		// Check if the next character of the string is acceptable.
		if pattern[0] != '?' && pattern[0] != s[0] {
			return false
		}

		s = s[1:]
		pattern = pattern[1:]
	}
}

// Result values of matchPatternList, mirroring OpenSSH's tri-state return.
const (
	patternNegated  = -1 // a negated subpattern matched: hard reject
	patternNoMatch  = 0  // nothing matched
	patternPositive = 1  // at least one positive subpattern matched
)

// matchPatternList is a port of OpenSSH match.c:match_pattern_list(). The
// pattern is a comma-separated list whose elements may be negated with '!'.
// A negated element that matches short-circuits the whole list.
func matchPatternList(s, patterns string, doLower bool) int {
	got := patternNoMatch
	for _, raw := range strings.Split(patterns, ",") {
		negated := false
		sub := raw
		if strings.HasPrefix(sub, "!") {
			negated = true
			sub = sub[1:]
		}
		if doLower {
			sub = strings.ToLower(sub)
		}
		if matchPattern(s, sub) {
			if negated {
				return patternNegated
			}
			got = patternPositive
		}
	}
	return got
}

// matchHostList applies the ssh_config Host-line rules to a whitespace
// separated list of patterns: each pattern is tried in order, a negated
// pattern that matches disables the whole block immediately (OpenSSH breaks
// out of the loop with the block inactive), and otherwise any positive match
// activates it.
//
// It returns whether the block is active and, when it is not, the pattern
// responsible for the negative decision (empty when nothing matched at all).
func matchHostList(host string, patterns []string) (active bool, negatedBy string) {
	for _, p := range patterns {
		if p == "" {
			continue
		}
		neg := false
		pat := p
		if strings.HasPrefix(pat, "!") {
			neg = true
			pat = pat[1:]
		}
		if matchPattern(host, pat) {
			if neg {
				// Negation short-circuits: the block is dead even if
				// an earlier pattern on the same line matched.
				return false, p
			}
			active = true
		}
	}
	return active, ""
}

// isWildcard reports whether a Host pattern contains glob metacharacters.
func isWildcard(p string) bool {
	return strings.ContainsAny(p, "*?")
}

// isConcreteHost reports whether a Host pattern names exactly one host and can
// therefore be resolved and listed.
func isConcreteHost(p string) bool {
	return p != "" && !isWildcard(p) && !strings.HasPrefix(p, "!")
}
