package auth

import (
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apiserver/pkg/authentication/authenticator"
	"k8s.io/apiserver/pkg/authentication/token/cache"
	"k8s.io/apiserver/pkg/authentication/user"
	"k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
	"k8s.io/client-go/transport"
)

var (
	// Default value taken from DefaultAuthWebhookRetryBackoff
	WebhookBackoff = wait.Backoff{
		Duration: 500 * time.Millisecond,
		Factor:   1.5,
		Jitter:   0.2,
		Steps:    5,
	}
)

var ExistingContext = ToMiddleware(AuthenticatorFunc(func(req *http.Request) (user.Info, bool, error) {
	user, ok := request.UserFrom(req.Context())
	return user, ok, nil
}))

const CattleAuthFailed = "X-API-Cattle-Auth-Failed"

type Authenticator interface {
	Authenticate(req *http.Request) (user.Info, bool, error)
}

type AuthenticatorFunc func(req *http.Request) (user.Info, bool, error)

func (a AuthenticatorFunc) Authenticate(req *http.Request) (user.Info, bool, error) {
	return a(req)
}

type Middleware func(next http.Handler) http.Handler

func (m Middleware) Chain(middleware Middleware) Middleware {
	return func(next http.Handler) http.Handler {
		return m(middleware(next))
	}
}

func WebhookConfigForURL(url string) (string, error) {
	config := clientcmdapi.Config{
		Clusters: map[string]*clientcmdapi.Cluster{
			"local": {
				Server:                url,
				InsecureSkipTLSVerify: true,
			},
		},
		Contexts: map[string]*clientcmdapi.Context{
			"Default": {
				Cluster:   "local",
				AuthInfo:  "user",
				Namespace: "default",
			},
		},
		AuthInfos: map[string]*clientcmdapi.AuthInfo{
			"user": {},
		},
		CurrentContext: "Default",
	}

	tmpFile, err := ioutil.TempFile("", "webhook-config")
	if err != nil {
		return "", err
	}
	if err := tmpFile.Close(); err != nil {
		return "", err
	}

	return tmpFile.Name(), clientcmd.WriteToFile(config, tmpFile.Name())
}

func NewWebhookAuthenticator(cacheTTL time.Duration, kubeConfig *rest.Config) (Authenticator, error) {
	wh, err := webhook.New(kubeConfig, "v1", nil, WebhookBackoff)
	if err != nil {
		return nil, err
	}

	if cacheTTL > 0 {
		return &webhookAuth{
			auth: cache.New(wh, false, cacheTTL, cacheTTL),
		}, nil
	}

	return &webhookAuth{
		auth: wh,
	}, nil
}

func NewWebhookMiddleware(cacheTTL time.Duration, kubeConfig *rest.Config) (Middleware, error) {
	auth, err := NewWebhookAuthenticator(cacheTTL, kubeConfig)
	if err != nil {
		return nil, err
	}
	return ToMiddleware(auth), nil
}

type webhookAuth struct {
	auth authenticator.Token
}

func (w *webhookAuth) Authenticate(req *http.Request) (user.Info, bool, error) {
	token := req.Header.Get("Authorization")
	if strings.HasPrefix(token, "Bearer ") {
		token = strings.TrimPrefix(token, "Bearer ")
	} else {
		token = ""
	}

	if token == "" {
		cookie, err := req.Cookie("R_SESS")
		if err != nil && err != http.ErrNoCookie {
			return nil, false, err
		} else if err != http.ErrNoCookie && len(cookie.Value) > 0 {
			token = "cookie://" + cookie.Value
		}
	}

	if token == "" {
		return nil, false, nil
	}

	resp, ok, err := w.auth.AuthenticateToken(req.Context(), token)
	if resp == nil {
		return nil, ok, err
	}
	return resp.User, ok, err
}

func ToMiddleware(auth Authenticator) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
			info, ok, err := auth.Authenticate(req)
			ctx := req.Context()
			if err != nil {
				info = &user.DefaultInfo{
					Name: "system:cattle:error",
					UID:  "system:cattle:error",
					Groups: []string{
						"system:unauthenticated",
						"system:cattle:error",
					},
				}
				ctx = request.WithValue(ctx, CattleAuthFailed, "true")
			} else if !ok {
				info = &user.DefaultInfo{
					Name: "system:unauthenticated",
					UID:  "system:unauthenticated",
					Groups: []string{
						"system:unauthenticated",
					},
				}
			}
			ctx = request.WithUser(ctx, info)

			req = req.WithContext(ctx)
			next.ServeHTTP(rw, req)
		})
	}
}

func AlwaysAdmin(req *http.Request) (user.Info, bool, error) {
	return &user.DefaultInfo{
		Name: "admin",
		UID:  "admin",
		Groups: []string{
			"system:masters",
			"system:authenticated",
		},
	}, true, nil
}

func Impersonation(req *http.Request) (user.Info, bool, error) {
	userName := req.Header.Get(transport.ImpersonateUserHeader)
	if userName == "" {
		return nil, false, nil
	}

	result := user.DefaultInfo{
		Name:   userName,
		Groups: req.Header[transport.ImpersonateGroupHeader],
		Extra:  map[string][]string{},
	}

	for k, v := range req.Header {
		if strings.HasPrefix(k, transport.ImpersonateUserExtraHeaderPrefix) {
			result.Extra[k[len(transport.ImpersonateUserExtraHeaderPrefix):]] = v
		}
	}

	return &result, true, nil
}