package tcp

import (
	"context"
	"errors"
	"fmt"
	"math"
	"net/http"
	"strings"

	"github.com/rs/zerolog/log"
	"github.com/traefik/traefik/v3/pkg/config/runtime"
	"github.com/traefik/traefik/v3/pkg/muxer"
	httpmuxer "github.com/traefik/traefik/v3/pkg/muxer/http"
	tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp"
	"github.com/traefik/traefik/v3/pkg/observability/logs"
	"github.com/traefik/traefik/v3/pkg/server/provider"
	tcpservice "github.com/traefik/traefik/v3/pkg/server/service/tcp"
	"github.com/traefik/traefik/v3/pkg/tcp"
	traefiktls "github.com/traefik/traefik/v3/pkg/tls"
)

const maxUserPriority = math.MaxInt - 1000

type middlewareBuilder interface {
	BuildChain(ctx context.Context, names []string) *tcp.Chain
}

// Manager is a route/router manager.
type Manager struct {
	serviceManager      *tcpservice.Manager
	middlewaresBuilder  middlewareBuilder
	httpHandlers        map[string]http.Handler
	httpsHandlers       map[string]http.Handler
	tlsManager          *traefiktls.Manager
	conf                *runtime.Configuration
	providersPrecedence []string
}

// NewManager Creates a new Manager.
func NewManager(conf *runtime.Configuration,
	serviceManager *tcpservice.Manager,
	middlewaresBuilder middlewareBuilder,
	httpHandlers map[string]http.Handler,
	httpsHandlers map[string]http.Handler,
	tlsManager *traefiktls.Manager,
	providersPrecedence []string,
) *Manager {
	return &Manager{
		serviceManager:      serviceManager,
		middlewaresBuilder:  middlewaresBuilder,
		httpHandlers:        httpHandlers,
		httpsHandlers:       httpsHandlers,
		tlsManager:          tlsManager,
		conf:                conf,
		providersPrecedence: providersPrecedence,
	}
}

// BuildHandlers builds the handlers for the given entrypoints.
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]*Router {
	entryPointsRouters := m.getTCPRouters(rootCtx, entryPoints)
	entryPointsRoutersHTTP := m.getHTTPRouters(rootCtx, entryPoints, true)

	entryPointHandlers := make(map[string]*Router)
	for _, entryPointName := range entryPoints {
		routers := entryPointsRouters[entryPointName]

		logger := log.Ctx(rootCtx).With().Str(logs.EntryPointName, entryPointName).Logger()
		ctx := logger.WithContext(rootCtx)

		handler, err := m.buildEntryPointHandler(ctx, routers, entryPointsRoutersHTTP[entryPointName], m.httpHandlers[entryPointName], m.httpsHandlers[entryPointName])
		if err != nil {
			logger.Error().Err(err).Send()
			continue
		}
		entryPointHandlers[entryPointName] = handler
	}
	return entryPointHandlers
}

func (m *Manager) getTCPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.TCPRouterInfo {
	if m.conf != nil {
		return m.conf.GetTCPRoutersByEntryPoints(ctx, entryPoints)
	}

	return make(map[string]map[string]*runtime.TCPRouterInfo)
}

func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo {
	if m.conf != nil {
		return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls)
	}

	return make(map[string]map[string]*runtime.RouterInfo)
}

func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*Router, error) {
	// Build a new Router.
	router, err := NewRouter(m.providersPrecedence)
	if err != nil {
		return nil, err
	}

	router.SetHTTPHandler(handlerHTTP)

	// Even though the error is seemingly ignored (aside from logging it),
	// we actually rely later on the fact that a tls config is nil (which happens when an error is returned) to take special steps
	// when assigning a handler to a route.
	defaultTLSConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, traefiktls.DefaultTLSConfigName)
	if err != nil {
		log.Ctx(ctx).Error().Err(err).Msg("Error during the build of the default TLS configuration")
	}

	for routerHTTPName, routerHTTPConfig := range configsHTTP {
		if routerHTTPConfig.TLS == nil {
			continue
		}

		logger := log.Ctx(ctx).With().Str(logs.RouterName, routerHTTPName).Logger()
		ctxRouter := logger.WithContext(provider.AddInContext(ctx, routerHTTPName))

		// Even if the TLS options mismatch between the configured and the resolved one is handled in the aggregator
		// we also have to handle it here to be able to mark the router in error.
		tlsOptionsName := traefiktls.DefaultTLSConfigName
		if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
			tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
		}

		domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
		if err != nil {
			routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err)
			routerHTTPConfig.AddError(routerErr, true)
			logger.Error().Err(routerErr).Send()
			continue
		}

		if len(domains) == 0 {
			// Extra Host(*) rule, for HTTPS routers with no Host rule,
			// and for requests for which the SNI does not match _any_ of the other existing routers Host.
			// This is only about choosing the TLS configuration.
			// The actual routing will be done further on by the HTTPS handler.
			// See examples below.
			router.AddHTTPTLSConfig("*", defaultTLSConf, traefiktls.DefaultTLSConfigName)

			// The server name (from a Host(SNI) rule) is the only parameter (available in HTTP routing rules) on which we can map a TLS config,
			// because it is the only one accessible before decryption (we obtain it during the ClientHello).
			// Therefore, when a router has no Host rule, it does not make any sense to specify some TLS options.
			// Consequently, when it comes to deciding what TLS config will be used,
			// for a request that will match an HTTPS router with no Host rule,
			// the result will depend on the _others_ existing routers (their Host rule, to be precise), and the TLS options associated with them,
			// even though they don't match the incoming request. Consider the following examples:

			//	# conf1
			//	httpRouter1:
			//		rule: PathPrefix("/foo")
			//	# Wherever the request comes from, the TLS config used will be the default one, because of the Host(*) fallback.

			//	# conf2
			//	httpRouter1:
			//		rule: PathPrefix("/foo")
			//
			//	httpRouter2:
			//		rule: Host("foo.com") && PathPrefix("/bar")
			//		tlsoptions: myTLSOptions
			//	# When a request for "/foo" comes, even though it won't be routed by httpRouter2,
			//	# if its SNI is set to foo.com, myTLSOptions will be used for the TLS connection.
			//	# Otherwise, it will fallback to the default TLS config.
			if tlsOptionsName != traefiktls.DefaultTLSConfigName {
				logger.Warn().Msgf("No domain found in rule %v, the TLS option %s cannot be applied", routerHTTPConfig.Rule, tlsOptionsName)
				routerHTTPConfig.AddError(fmt.Errorf("no domain found in rule %v, the TLS option %s cannot be applied", routerHTTPConfig.Rule, tlsOptionsName), false)
			}
		}

		if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
			logger.Warn().Msg("Found different TLS options for routers on the same host, so using the default TLS options instead.")
			routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false)
		}

		// Even though the error is seemingly ignored (aside from logging it),
		// we actually rely later on the fact that a tls config is nil (which happens when an error is returned) to take special steps
		// when assigning a handler to a route.
		tlsConf, tlsConfErr := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, routerHTTPConfig.TLS.ResolvedOptions)
		if tlsConfErr != nil {
			// Note: we do not call AddError here because we already did so when buildRouterHandler errored for the same reason.
			logger.Error().Err(tlsConfErr).Send()
		}

		for _, domain := range domains {
			if tlsConf == nil {
				// we use nil config as a signal to insert a handler
				// that enforces that TLS connection attempts to the corresponding (broken) router should fail.
				logger.Debug().Msgf("Adding special closing route for %s because of a broken TLS options %s", domain, routerHTTPConfig.TLS.ResolvedOptions)
				router.AddHTTPTLSConfig(domain, nil, "")
				continue
			}

			logger.Debug().Msgf("Adding route for %s with TLS options %s", domain, routerHTTPConfig.TLS.ResolvedOptions)
			router.AddHTTPTLSConfig(domain, tlsConf, routerHTTPConfig.TLS.ResolvedOptions)
		}
	}

	// Keep in mind that defaultTLSConf might be nil here.
	router.SetHTTPSHandler(handlerHTTPS, defaultTLSConf)

	m.addTCPHandlers(ctx, configs, router)

	return router, nil
}

// addTCPHandlers creates the TCP handlers defined in configs, and adds them to router.
func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, router *Router) {
	for routerName, routerConfig := range configs {
		logger := log.Ctx(ctx).With().Str(logs.RouterName, routerName).Logger()
		ctxRouter := logger.WithContext(provider.AddInContext(ctx, routerName))

		if routerConfig.Priority == 0 {
			routerConfig.Priority = tcpmuxer.GetRulePriority(routerConfig.Rule)
		}

		if routerConfig.Service == "" {
			err := errors.New("the service is missing on the router")
			routerConfig.AddError(err, true)
			logger.Error().Err(err).Send()
			continue
		}

		if routerConfig.Rule == "" {
			err := errors.New("router has no rule")
			routerConfig.AddError(err, true)
			logger.Error().Err(err).Send()
			continue
		}

		domains, err := tcpmuxer.ParseHostSNI(routerConfig.Rule)
		if err != nil {
			routerErr := fmt.Errorf("invalid rule: %q , %w", routerConfig.Rule, err)
			routerConfig.AddError(routerErr, true)
			logger.Error().Err(routerErr).Send()
			continue
		}

		// HostSNI Rule, but TLS not set on the router, which is an error.
		// However, we allow the HostSNI(*) exception.
		if len(domains) > 0 && routerConfig.TLS == nil && domains[0] != "*" {
			routerErr := fmt.Errorf("invalid rule: %q , has HostSNI matcher, but no TLS on router", routerConfig.Rule)
			routerConfig.AddError(routerErr, true)
			logger.Error().Err(routerErr).Send()
			continue
		}

		if routerConfig.Priority > maxUserPriority && !strings.HasSuffix(routerName, "@internal") {
			routerErr := fmt.Errorf("the router priority %d exceeds the max user-defined priority %d", routerConfig.Priority, maxUserPriority)
			routerConfig.AddError(routerErr, true)
			logger.Error().Err(routerErr).Send()
			continue
		}

		var handler tcp.Handler
		if routerConfig.TLS == nil || routerConfig.TLS.Passthrough {
			handler, err = m.buildTCPHandler(ctxRouter, routerConfig)
			if err != nil {
				routerConfig.AddError(err, true)
				logger.Error().Err(err).Send()
				continue
			}
		}

		if routerConfig.TLS == nil {
			logger.Debug().Msgf("Adding route for %q", routerConfig.Rule)

			if err := router.muxerTCP.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
				routerConfig.AddError(err, true)
				logger.Error().Err(err).Send()
			}
			continue
		}

		if routerConfig.TLS.Passthrough {
			logger.Debug().Msgf("Adding Passthrough route for %q", routerConfig.Rule)

			if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
				routerConfig.AddError(err, true)
				logger.Error().Err(err).Send()
			}
			continue
		}

		for _, domain := range domains {
			if muxer.IsASCII(domain) {
				continue
			}

			asciiError := fmt.Errorf("invalid domain name value %q, non-ASCII characters are not allowed", domain)
			routerConfig.AddError(asciiError, true)
			logger.Error().Err(asciiError).Send()
		}

		tlsOptionsName := routerConfig.TLS.Options

		if len(tlsOptionsName) == 0 {
			tlsOptionsName = traefiktls.DefaultTLSConfigName
		}

		if tlsOptionsName != traefiktls.DefaultTLSConfigName {
			tlsOptionsName = provider.GetQualifiedName(ctxRouter, tlsOptionsName)
		}

		tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
		if err != nil {
			routerConfig.AddError(err, true)
			logger.Error().Err(err).Send()

			logger.Debug().Msgf("Adding special TLS closing route for %q because broken TLS options %s", routerConfig.Rule, tlsOptionsName)

			if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), &brokenTLSRouter{}); err != nil {
				routerConfig.AddError(err, true)
				logger.Error().Err(err).Send()
			}

			continue
		}

		// Now that the Rule is not just about the Host, we could theoretically have a config like:
		//	router1:
		//		rule: HostSNI(foo.com) && ClientIP(IP1)
		//		tlsOption: tlsOne
		//	router2:
		//		rule: HostSNI(foo.com) && ClientIP(IP2)
		//		tlsOption: tlsTwo
		// i.e. same HostSNI but different tlsOptions
		// This is only applicable if the muxer can decide about the routing _before_ telling the client about the tlsConf (i.e. before the TLS HandShake).
		// This seems to be the case so far with the existing matchers (HostSNI, and ClientIP), so it's all good.
		// Otherwise, we would have to do as for HTTPS, i.e. disallow different TLS configs for the same HostSNIs.

		handler, err = m.buildTCPHandler(ctxRouter, routerConfig)
		if err != nil {
			routerConfig.AddError(err, true)
			logger.Error().Err(err).Send()
			continue
		}

		handler = &tcp.TLSHandler{
			Next:           handler,
			Config:         tlsConf,
			TLSOptionsName: tlsOptionsName,
		}

		logger.Debug().Msgf("Adding TLS route for %q", routerConfig.Rule)

		if err := router.muxerTCPTLS.AddRoute(routerConfig.Rule, routerConfig.RuleSyntax, routerConfig.Priority, providerName(routerName), handler); err != nil {
			routerConfig.AddError(err, true)
			logger.Error().Err(err).Send()
			continue
		}
	}
}

func (m *Manager) buildTCPHandler(ctx context.Context, router *runtime.TCPRouterInfo) (tcp.Handler, error) {
	var qualifiedNames []string
	for _, name := range router.Middlewares {
		qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
	}
	router.Middlewares = qualifiedNames

	if router.Service == "" {
		return nil, errors.New("the service is missing on the router")
	}

	sHandler, err := m.serviceManager.BuildTCP(ctx, router.Service)
	if err != nil {
		return nil, err
	}

	mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)

	return tcp.NewChain().Extend(*mHandler).Then(sHandler)
}

func providerName(routerName string) string {
	parts := strings.Split(routerName, "@")
	if len(parts) == 2 {
		return parts[1]
	}
	return ""
}
