// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package ptls

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net/http"
	"sync"

	"k8s.io/apiserver/pkg/admission"
	genericapiserver "k8s.io/apiserver/pkg/server"
	"k8s.io/apiserver/pkg/server/options"
	k8sinformers "k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/transport"
)

// defaultServingOptionsMinTLSVersion is the minimum tls version in the format
// expected by SecureServingOptions.MinTLSVersion from
// k8s.io/apiserver/pkg/server/options.
const defaultServingOptionsMinTLSVersion = "VersionTLS12"

type ConfigFunc func(*x509.CertPool) *tls.Config

func Merge(tlsConfigFunc ConfigFunc, tlsConfig *tls.Config) {
	secureTLSConfig := tlsConfigFunc(nil)

	// override the core security knobs of the TLS config
	// note that these have to be kept in sync with Default / Secure above
	tlsConfig.MinVersion = secureTLSConfig.MinVersion
	tlsConfig.CipherSuites = secureTLSConfig.CipherSuites

	// if the TLS config already states what protocols it wants to use, honor that instead of overriding
	if len(tlsConfig.NextProtos) == 0 {
		tlsConfig.NextProtos = secureTLSConfig.NextProtos
	}
}

// RestConfigFunc allows this package to not depend on the kubeclient package.
type RestConfigFunc func(*rest.Config) (kubernetes.Interface, *rest.Config, error)

// PrepareServerConfigFunc is a function that can prepare a RecommendedConfig before the use of RecommendedOptions.ApplyTo().
type PrepareServerConfigFunc func(c *genericapiserver.RecommendedConfig)

// DefaultRecommendedOptions configures the RecommendedOptions for a server to use the appropriate cipher suites,
// min TLS version, and client configuration options for servers that need to accept incoming connections from
// arbitrary clients (like the impersonation proxy).
// It returns a PrepareServerConfigFunc which must be used on a RecommendedConfig before passing it to RecommendedOptions.ApplyTo().
func DefaultRecommendedOptions(opts *options.RecommendedOptions, f RestConfigFunc) (PrepareServerConfigFunc, error) {
	defaultServing(opts.SecureServing)
	return secureClient(opts, f)
}

// SecureRecommendedOptions configures the RecommendedOptions for a server to use the appropriate cipher suites,
// min TLS version, and client configuration options for servers that only need to accept incoming connections from
// certain well known clients which we expect will always use modern TLS settings (like the Kube API server).
// It returns a PrepareServerConfigFunc which must be used on a RecommendedConfig before passing it to RecommendedOptions.ApplyTo().
func SecureRecommendedOptions(opts *options.RecommendedOptions, f RestConfigFunc) (PrepareServerConfigFunc, error) {
	SecureServing(opts.SecureServing)
	return secureClient(opts, f)
}

func defaultServing(opts *options.SecureServingOptionsWithLoopback) {
	c := Default(nil)
	cipherSuites := make([]string, 0, len(c.CipherSuites))
	for _, id := range c.CipherSuites {
		cipherSuites = append(cipherSuites, tls.CipherSuiteName(id))
	}
	opts.CipherSuites = cipherSuites

	opts.MinTLSVersion = defaultServingOptionsMinTLSVersion
}

func secureClient(opts *options.RecommendedOptions, f RestConfigFunc) (PrepareServerConfigFunc, error) {
	inClusterClient, inClusterConfig, err := f(nil)
	if err != nil {
		return nil, fmt.Errorf("failed to build in cluster client: %w", err)
	}

	if n, z := opts.Authentication.RemoteKubeConfigFile, opts.Authorization.RemoteKubeConfigFile; len(n) > 0 || len(z) > 0 {
		return nil, fmt.Errorf("delgating auth is not using in-cluster config:\nauthentication=%s\nauthorization=%s", n, z)
	}

	// delegated authn and authz provide easy hooks for us to set the TLS config.
	// however, the underlying clients use client-go's global TLS cache with an
	// in-cluster config.  to make this safe, we simply do the mutation once.
	wrapperFunc := wrapTransportOnce(inClusterConfig.WrapTransport)
	opts.Authentication.CustomRoundTripperFn = wrapperFunc
	opts.Authorization.CustomRoundTripperFn = wrapperFunc

	// Set this to nil to because it would normally set up c.ClientConfig and c.SharedInformerFactory, but we want to
	// do that ourselves instead by calling the func returned below before we call RecommendedOptions.ApplyTo().
	opts.CoreAPI = nil

	baseExtraAdmissionInitializers := opts.ExtraAdmissionInitializers
	opts.ExtraAdmissionInitializers = func(c *genericapiserver.RecommendedConfig) ([]admission.PluginInitializer, error) {
		// Abuse this closure to rewrite our loopback config. This is mostly future proofing for post start hooks.
		// Note that c.LoopbackClientConfig has already been set up inside RecommendedOptions.ApplyTo() before this
		// ExtraAdmissionInitializers function is invoked, so it is okay to use it here.
		_, loopbackConfig, err := f(c.LoopbackClientConfig)
		if err != nil {
			return nil, fmt.Errorf("failed to build loopback config: %w", err)
		}
		c.LoopbackClientConfig = loopbackConfig
		return baseExtraAdmissionInitializers(c)
	}

	// This returned function is intended to be called before RecommendedOptions.ApplyTo(). Is is intended
	// that the above setting of opts.CoreAPI to nil will make the below function the only thing that sets
	// the c.ClientConfig and c.SharedInformerFactory, although this is highly dependent on the implementation
	// details ofRecommendedOptions.ApplyTo() and all its helpers that it invokes.
	return func(c *genericapiserver.RecommendedConfig) {
		c.ClientConfig = inClusterConfig
		c.SharedInformerFactory = k8sinformers.NewSharedInformerFactory(inClusterClient, 0)
	}, nil
}

func wrapTransportOnce(f transport.WrapperFunc) transport.WrapperFunc {
	var once sync.Once
	return func(rt http.RoundTripper) http.RoundTripper {
		once.Do(func() {
			_ = f(rt) // assume in-place mutation
		})
		return rt
	}
}
