package middleware

import (
	"crypto/rsa"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/golang-jwt/jwt"
	"github.com/labstack/echo/v4"

	"github.com/pingcap/test-infra/common/errors"
	"github.com/pingcap/test-infra/services/rms/config"
	"github.com/pingcap/test-infra/services/rms/utils"
)

func Authenticate(next echo.HandlerFunc) echo.HandlerFunc {
	return func(ctx echo.Context) error {
		user := authenticate(ctx)
		if user != nil {
			utils.SetUser(ctx, user)
		}
		return next(ctx)
	}
}

func EnsureAuthenticated(next echo.HandlerFunc) echo.HandlerFunc {
	return func(ctx echo.Context) error {
		u := utils.GetUser(ctx)
		if u == nil {
			err := ctx.JSON(http.StatusUnauthorized, errors.NewUnauthorized())
			if err != nil {
				return err
			}
			return nil
		}
		return next(ctx)
	}
}

func authenticate(ctx echo.Context) *utils.User {
	header := ctx.Request().Header.Get(echo.HeaderAuthorization)
	if header == "" {
		return nil
	}
	authParts := strings.SplitN(header, " ", 2)
	if len(authParts) != 2 {
		ctx.Logger().Error("Invalid authorization header")
		return nil
	}
	typ, token := authParts[0], authParts[1]
	if typ == "Bearer" {
		if strings.HasPrefix(token, "rms_") {
			return jwtAuthenticate(token[4:])
		}
		return tcmsAuthenticate(ctx, token)
	}
	ctx.Logger().Errorf("Unsupported authentication type: %s", typ)

	return nil
}

func jwtAuthenticate(tokenString string) *utils.User {
	if defaultPublicKey == nil {
		return nil
	}
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodRSAPSS); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return defaultPublicKey, nil
		// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
	})
	if err != nil {
		return nil
	}
	if claim, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		user := &utils.User{Group: "user"}
		if name, ok := claim["sub"].(string); ok {
			user.Name = name
		}
		if grp, ok := claim["group"].(string); ok {
			user.Group = grp
		}
		return user
	}
	return nil
}

func tcmsAuthenticate(ctx echo.Context, token string) *utils.User {
	type TcmsUser struct {
		User *struct {
			ID    uint64 `json:"id"`
			Email string `json:"email"`
		} `json:"user"`
	}
	url := config.DefaultRMSConfiguration.TCMS.URL + "/api/v1/tokens/validate?user=1&token=" + token
	resp, err := http.Get(url)
	if err != nil {
		ctx.Logger().Error(err)
		return nil
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil
	}
	user := &TcmsUser{}
	err = json.Unmarshal(body, user)
	if err != nil {
		ctx.Logger().Error(err)
		return nil
	} else if user.User == nil {
		ctx.Logger().Infof(string(body))
		return nil
	}
	authed := &utils.User{
		Name: user.User.Email,
	}
	// FIXME(@codingpoet): remove the hardcode after tcms support user with group
	if user.User.ID == 0 {
		authed.Group = "admin"
	} else {
		authed.Group = "USER"
	}
	return authed
}

var defaultPublicKey *rsa.PublicKey

func AuthSetup() {
	pub, err := base64.StdEncoding.DecodeString(config.DefaultRMSConfiguration.JwtPubKey)
	if err != nil {
		return
	}
	defaultPublicKey, _ = jwt.ParseRSAPublicKeyFromPEM(pub)
}
