// SPDX-FileCopyrightText: 2020 Luke Granger-Brown <depot@lukegb.com>
//
// SPDX-License-Identifier: Apache-2.0
package openshiftauth

import (
	"context"
	"encoding/gob"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"

	"github.com/dghubble/gologin/v2"
	"github.com/dghubble/gologin/v2/oauth2"
	"github.com/dgrijalva/jwt-go"
	"github.com/gorilla/mux"
	"github.com/gorilla/securecookie"
	"github.com/gorilla/sessions"
	xoauth2 "golang.org/x/oauth2"
)

type SecretGetter func(ctx context.Context) (string, error)

func TokenDirSecretGetter(ctx context.Context) (string, error) {
	v, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
	if err != nil {
		return "", fmt.Errorf("couldn't read serviceaccount secret: %w", err)
	}
	return string(v), nil
}

type BaseHandler struct {
	Config       xoauth2.Config
	SecretGetter SecretGetter
}

func (h *BaseHandler) config(ctx context.Context) (*xoauth2.Config, error) {
	s, err := h.SecretGetter(ctx)
	if err != nil {
		return nil, err
	}
	cfg := h.Config
	cfg.ClientSecret = s
	return &cfg, nil
}

func (h *BaseHandler) wrapWithConfig(f func(cfg *xoauth2.Config) http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		cfg, err := h.config(r.Context())
		if err != nil {
			http.Error(rw, err.Error(), http.StatusInternalServerError)
			return
		}
		f(cfg).ServeHTTP(rw, r)
	})
}

func (h *BaseHandler) CallbackHandler(success, failure http.Handler) http.Handler {
	return h.wrapWithConfig(func(cfg *xoauth2.Config) http.Handler {
		return oauth2.CallbackHandler(cfg, success, failure)
	})
}

func (h *BaseHandler) LoginHandler(failure http.Handler) http.Handler {
	return h.wrapWithConfig(func(cfg *xoauth2.Config) http.Handler {
		return oauth2.LoginHandler(cfg, failure)
	})
}

type contextKey string

const (
	sessionContextKey contextKey = "session"
)

func AttachSessionMiddleware(sess sessions.Store) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			s, err := sess.Get(r, "openshiftauth")
			if err != nil {
				log.Printf("decoding session: %v", err)
			}
			ctx := context.WithValue(r.Context(), sessionContextKey, s)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

func Session(ctx context.Context) *sessions.Session {
	return ctx.Value(sessionContextKey).(*sessions.Session)
}

func UserFromContext(ctx context.Context) *User {
	s := Session(ctx)
	if s.Values["user"] != nil {
		return s.Values["user"].(*User)
	}
	return nil
}

func secretFromEnv(name string, length int) []byte {
	var bs []byte
	if bsHex, ok := os.LookupEnv(name); ok {
		newBS, err := hex.DecodeString(bsHex)
		if err != nil {
			log.Printf("failed to decode %v as hex: %v", name, err)
		} else {
			bs = newBS
		}
	}
	if bs == nil {
		log.Printf("generating a random %v: this is bad", name)
		bs = securecookie.GenerateRandomKey(length)
	}
	return bs
}

type User struct {
	Metadata struct {
		Name string `json:"name"`
	} `json:"metadata"`
	FullName   string   `json:"fullName"`
	Identities []string `json:"identities"`
	Groups     []string `json:"groups"`
}

func init() {
	gob.Register(&User{})
}

func fetchUserForToken(ctx context.Context, token *xoauth2.Token) (*User, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", "https://api.k8s.lukegb.tech:6443/apis/user.openshift.io/v1/users/~", nil)
	if err != nil {
		return nil, fmt.Errorf("http.NewRequestWithContext for users/~ failed")
	}
	token.SetAuthHeader(req)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("executing request for users/~ failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("openshift returned non-OK status %d %v", resp.StatusCode, resp.Status)
	}

	var u User
	if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
		return nil, fmt.Errorf("decoding response from openshift: %v", err)
	}

	return &u, nil
}

func RequireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		u := UserFromContext(r.Context())
		if u == nil {
			redirURL := r.URL.ResolveReference(r.URL)
			redirURL.Path = "/.login/start"
			redirURL.RawQuery = url.Values{
				"next": []string{r.URL.String()},
			}.Encode()
			http.Redirect(rw, r, redirURL.String(), http.StatusSeeOther)
			return
		}
		next.ServeHTTP(rw, r)
	})
}

type serviceAccountDetails struct {
	Namespace string `json:"kubernetes.io/serviceaccount/namespace"`
	Name      string `json:"kubernetes.io/serviceaccount/service-account.name"`
	FullName  string `json:"sub"`
}

func parseServiceAccount() (*serviceAccountDetails, error) {
	token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
	if err != nil {
		return nil, fmt.Errorf("reading service account JWT: %w", err)
	}

	// ParseUnverified is safe here because we're just using it to extract information about ourselves.
	p := &jwt.Parser{}
	claims := make(jwt.MapClaims)
	if _, _, err := p.ParseUnverified(string(token), claims); err != nil {
		return nil, fmt.Errorf("parsing JWT: %w", err)
	}
	val := func(s string) string {
		v, ok := claims[s]
		if !ok {
			return ""
		}
		return v.(string)
	}
	sad := &serviceAccountDetails{
		Namespace: val("kubernetes.io/serviceaccount/namespace"),
		Name:      val("kubernetes.io/serviceaccount/service-account.name"),
		FullName:  val("sub"),
	}
	return sad, nil
}

func NewRouter(r *mux.Router) (*mux.Router, error) {
	sess := sessions.NewCookieStore(
		secretFromEnv("OPENSHIFT_AUTH_SESSION_SIGN_SECRET", 32),
		secretFromEnv("OPENSHIFT_AUTH_SESSION_ENC_SECRET", 32))
	r.Use(AttachSessionMiddleware(sess))

	sad, err := parseServiceAccount()
	if err != nil {
		return nil, fmt.Errorf("loading service account information: %w", err)
	}
	log.Printf("using service account token for %v", sad.FullName)

	bh := &BaseHandler{
		Config: xoauth2.Config{
			ClientID: sad.FullName,
			Endpoint: xoauth2.Endpoint{
				AuthURL:  "https://oauth-openshift.apps.k8s.lukegb.tech/oauth/authorize",
				TokenURL: "https://oauth-openshift.apps.k8s.lukegb.tech/oauth/token",
			},
			RedirectURL: fmt.Sprintf("%s/.login/callback", os.Getenv("OPENSHIFT_AUTH_BASE")),
			Scopes:      []string{"user:info"},
		},
		SecretGetter: TokenDirSecretGetter,
	}
	baseURL, err := url.Parse(os.Getenv("OPENSHIFT_AUTH_BASE"))
	if err != nil {
		return nil, fmt.Errorf("failed to parse OPENSHIFT_AUTH_BASE as URL: %v", err)
	}
	stateConfig := gologin.DefaultCookieConfig
	r.HandleFunc("/.login/start", func(rw http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if nextURL, err := url.Parse(r.URL.Query().Get("next")); err == nil {
			nextURL = baseURL.ResolveReference(nextURL)
			if nextURL.Scheme != baseURL.Scheme || nextURL.User != nil || nextURL.Host != baseURL.Host {
				nextURL = nil
			}
			if nextURL != nil {
				s := Session(ctx)
				s.AddFlash(nextURL.RequestURI(), "_openshiftauth_next")
				sessions.Save(r, rw)
			}
		}
		oauth2.StateHandler(stateConfig, bh.LoginHandler(nil)).ServeHTTP(rw, r)
	})
	r.Handle("/.login/callback", oauth2.StateHandler(stateConfig, bh.CallbackHandler(
		http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			ctx := r.Context()
			token, err := oauth2.TokenFromContext(ctx)
			if err != nil {
				http.Error(rw, "oauth2 TokenFromContext failed", http.StatusBadRequest)
				return
			}

			u, err := fetchUserForToken(ctx, token)
			if err != nil {
				http.Error(rw, fmt.Sprintf("fetchUserForToken: %v", err), http.StatusInternalServerError)
				return
			}

			s := Session(ctx)
			s.Values["user"] = *u
			sessions.Save(r, rw)

			next := "/"
			nexts := s.Flashes("_openshiftauth_next")
			if len(nexts) >= 1 {
				next = nexts[0].(string)
			}
			http.Redirect(rw, r, next, http.StatusFound)
		}),
		nil)))

	authR := r.PathPrefix("/").Subrouter()
	authR.Use(RequireAuth)

	return authR, nil
}