package https

import (
	"context"
	"net/http"
	"strconv"
	"sync"

	"github.com/gorilla/mux"
	"github.com/k3s-io/k3s/pkg/daemons/config"
	"github.com/k3s-io/api/pkg/generated/clientset/versioned/scheme"
	"github.com/k3s-io/k3s/pkg/util"
	"github.com/k3s-io/k3s/pkg/version"
	"k8s.io/apiserver/pkg/apis/apiserver"
	"k8s.io/apiserver/pkg/authentication/authenticator"
	"k8s.io/apiserver/pkg/authorization/authorizer"
	genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
	apirequest "k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/apiserver/pkg/server"
	"k8s.io/apiserver/pkg/server/options"
)

// RouterFunc provides a hook for components to register additional routes to a request router
type RouterFunc func(ctx context.Context, nodeConfig *config.Node) (*mux.Router, error)

var once sync.Once
var router *mux.Router
var err error

// Start returns a router with authn/authz filters applied.
// The first time it is called, the router is created and a new HTTPS listener is started if the handler is nil.
// Subsequent calls will return the same router.
func Start(ctx context.Context, nodeConfig *config.Node, runtime *config.ControlRuntime) (*mux.Router, error) {
	once.Do(func() {
		router = mux.NewRouter().SkipClean(true)
		config := server.Config{}

		if runtime == nil {
			// If we do not have an existing handler, set up a new listener
			tcp, lerr := util.ListenWithLoopback(ctx, nodeConfig.AgentConfig.ListenAddress, strconv.Itoa(nodeConfig.SupervisorPort))
			if lerr != nil {
				err = lerr
				return
			}

			serving := options.NewSecureServingOptions()
			serving.Listener = tcp
			serving.CipherSuites = nodeConfig.AgentConfig.CipherSuites
			serving.MinTLSVersion = nodeConfig.AgentConfig.MinTLSVersion
			serving.ServerCert = options.GeneratableKeyCert{
				CertKey: options.CertKey{
					CertFile: nodeConfig.AgentConfig.ServingKubeletCert,
					KeyFile:  nodeConfig.AgentConfig.ServingKubeletKey,
				},
			}
			if aerr := serving.ApplyTo(&config.SecureServing); aerr != nil {
				err = aerr
				return
			}
		} else {
			// If we have an existing handler, wrap it
			router.NotFoundHandler = runtime.Handler
			runtime.Handler = router
		}

		authn := options.NewDelegatingAuthenticationOptions()
		authn.Anonymous = &apiserver.AnonymousAuthConfig{
			Enabled: false,
		}
		authn.SkipInClusterLookup = true
		authn.ClientCert = options.ClientCertAuthenticationOptions{
			ClientCA: nodeConfig.AgentConfig.ClientCA,
		}
		authn.RemoteKubeConfigFile = nodeConfig.AgentConfig.KubeConfigKubelet
		if applyErr := authn.ApplyTo(&config.Authentication, config.SecureServing, nil); applyErr != nil {
			err = applyErr
			return
		}

		authz := options.NewDelegatingAuthorizationOptions()
		authz.AlwaysAllowPaths = []string{ // skip authz for paths that should not use SubjectAccessReview; basically everything that will use this router other than metrics
			"/v1-" + version.Program + "/p2p", // spegel libp2p peer discovery
			"/v2/*",                           // spegel registry mirror
			"/debug/pprof/*",                  // profiling
		}
		authz.RemoteKubeConfigFile = nodeConfig.AgentConfig.KubeConfigKubelet
		if applyErr := authz.ApplyTo(&config.Authorization); applyErr != nil {
			err = applyErr
			return
		}

		router.Use(filterChain(config.Authentication.Authenticator, config.Authorization.Authorizer))

		if config.SecureServing != nil {
			_, _, err = config.SecureServing.Serve(router, 0, ctx.Done())
		}
	})

	return router, err
}

// filterChain runs the kubernetes authn/authz filter chain using the mux middleware API
func filterChain(authn authenticator.Request, authz authorizer.Authorizer) mux.MiddlewareFunc {
	return func(handler http.Handler) http.Handler {
		requestInfoResolver := &apirequest.RequestInfoFactory{}
		failedHandler := genericapifilters.Unauthorized(scheme.Codecs)
		handler = genericapifilters.WithAuthorization(handler, authz, scheme.Codecs)
		handler = genericapifilters.WithAuthentication(handler, authn, failedHandler, nil, nil)
		handler = genericapifilters.WithRequestInfo(handler, requestInfoResolver)
		handler = genericapifilters.WithCacheControl(handler)
		return handler
	}
}
