package tcp

import (
	"fmt"
	"regexp"
	"strings"
	"unicode/utf8"

	"github.com/go-acme/lego/v4/challenge/tlsalpn01"
	"github.com/rs/zerolog/log"
	"github.com/traefik/traefik/v3/pkg/ip"
)

var tcpFuncs = map[string]func(*matchersTree, ...string) error{
	"ALPN":          expect1Parameter(alpn),
	"ClientIP":      expect1Parameter(clientIP),
	"HostSNI":       expect1Parameter(hostSNI),
	"HostSNIRegexp": expect1Parameter(hostSNIRegexp),
}

func expect1Parameter(fn func(*matchersTree, ...string) error) func(*matchersTree, ...string) error {
	return func(route *matchersTree, s ...string) error {
		if len(s) != 1 {
			return fmt.Errorf("unexpected number of parameters; got %d, expected 1", len(s))
		}

		return fn(route, s...)
	}
}

// alpn checks if any of the connection ALPN protocols matches one of the matcher protocols.
func alpn(tree *matchersTree, protos ...string) error {
	proto := protos[0]

	if proto == tlsalpn01.ACMETLS1Protocol {
		return fmt.Errorf("invalid protocol value for ALPN matcher, %q is not allowed", proto)
	}

	tree.matcher = func(meta ConnData) bool {
		for _, alpnProto := range meta.alpnProtos {
			if alpnProto == proto {
				return true
			}
		}

		return false
	}

	return nil
}

func clientIP(tree *matchersTree, clientIP ...string) error {
	checker, err := ip.NewChecker(clientIP)
	if err != nil {
		return fmt.Errorf("initializing IP checker for ClientIP matcher: %w", err)
	}

	tree.matcher = func(meta ConnData) bool {
		ok, err := checker.Contains(meta.remoteIP)
		if err != nil {
			log.Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
			return false
		}
		return ok
	}

	return nil
}

var hostOrIP = regexp.MustCompile(`^[[:alnum:]\.\-\:]+$`)

// hostSNI checks if the SNI Host of the connection match the matcher host.
func hostSNI(tree *matchersTree, hosts ...string) error {
	host := hosts[0]

	if host == "*" {
		// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
		// it allows matching with an empty serverName.
		tree.matcher = func(meta ConnData) bool { return true }
		return nil
	}

	if !hostOrIP.MatchString(host) {
		return fmt.Errorf("invalid value for HostSNI matcher, %q is not a valid hostname", host)
	}

	tree.matcher = func(meta ConnData) bool {
		if meta.serverName == "" {
			return false
		}

		if host == meta.serverName {
			return true
		}

		// trim trailing period in case of FQDN
		host = strings.TrimSuffix(host, ".")

		return host == meta.serverName
	}

	return nil
}

// hostSNIRegexp checks if the SNI Host of the connection matches the matcher host regexp.
func hostSNIRegexp(tree *matchersTree, templates ...string) error {
	template := templates[0]

	if !isASCII(template) {
		return fmt.Errorf("invalid value for HostSNIRegexp matcher, %q is not a valid hostname", template)
	}

	re, err := regexp.Compile(template)
	if err != nil {
		return fmt.Errorf("compiling HostSNIRegexp matcher: %w", err)
	}

	tree.matcher = func(meta ConnData) bool {
		return re.MatchString(meta.serverName)
	}

	return nil
}

// isASCII checks if the given string contains only ASCII characters.
func isASCII(s string) bool {
	for i := 0; i < len(s); i++ {
		if s[i] >= utf8.RuneSelf {
			return false
		}
	}

	return true
}
