package testtoken

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"net/http"
	"strings"
	"time"

	"github.com/pingcap/dbaas/central/db/tenant"
	"github.com/pingcap/dbaas/pkg/auth/auth0"
	"github.com/pingcap/dbaas/pkg/auth/util"
	"github.com/pingcap/dbaas/pkg/secret/encrypt"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"go.uber.org/zap"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// Model is created with NewModel
type Model struct {
	log                         *zap.Logger
	auth0Domain                 string
	auth0ManagementClientId     string
	auth0ManagementClientSecret string
	aesCrypt                    *encrypt.AESCrypt
	tokenExtractors             []auth0.TokenExtractor
	K8sClient                   client.Client
}

func NewModel(log *zap.Logger, auth0Domain, auth0ManagementClientId, auth0ManagementClientSecret string, aesCrypt *encrypt.AESCrypt, tokenExtractors []auth0.TokenExtractor, k8sClient client.Client) Model {
	return Model{log, auth0Domain, auth0ManagementClientId, auth0ManagementClientSecret, aesCrypt, tokenExtractors, k8sClient}
}

type Token struct {
	Token      string
	ExpireTime int64
}

const tokenPreFix = "CLOUD"

type Claims struct {
	Email      string
	ExpireTime int64
}

func (m Model) GenerateTestToken(auth0Domain, auth0ManagementClientId, auth0ManagementClientSecret, email string) (*Token, error) {
	if auth0Domain != m.auth0Domain || auth0ManagementClientId != m.auth0ManagementClientId || auth0ManagementClientSecret != m.auth0ManagementClientSecret {
		return nil, errcode.NewForbiddenErr(errors.New("forbidden"))
	}
	expireTime := time.Now().Unix() + 24*3600
	claim := Claims{
		Email:      email,
		ExpireTime: expireTime,
	}
	data, err := json.Marshal(&claim)
	if err != nil {
		return nil, errcode.NewInternalErr(errors.Annotate(err, "failed to marshal token"))
	}
	return &Token{
		Token:      tokenPreFix + base64.URLEncoding.EncodeToString(m.aesCrypt.Encrypt(data)),
		ExpireTime: expireTime,
	}, nil
}

func (m Model) TryGetTestUserFromToken(rawToken string) *tenant.User {
	email := m.TryGetEmailFromTestToken(rawToken)
	if email == "" {
		return nil
	}
	user, found := tenant.GetUser(email)
	if found {
		return user
	}
	m.log.Info("find user failed", zap.String("email", email))
	return nil
}

func (m Model) TryGetEmailFromTestToken(rawToken string) string {
	if strings.HasPrefix(rawToken, tokenPreFix) {
		newToken := strings.TrimPrefix(rawToken, tokenPreFix)
		claims := Claims{}
		newTokenData, err := base64.URLEncoding.DecodeString(newToken)
		if err != nil {
			m.log.Error("decode token failed", zap.String("token", rawToken), zap.Error(err))
			return ""
		}
		if err := json.Unmarshal(m.aesCrypt.Decrypt(newTokenData), &claims); err != nil {
			m.log.Error("parse token failed", zap.String("token", rawToken), zap.Error(err))
			return ""
		}
		if claims.ExpireTime < time.Now().Unix() {
			m.log.Info("token is expired", zap.Int64("time", claims.ExpireTime))
			return ""
		}
		return claims.Email
	}
	return ""
}

func (m Model) TryAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Don't authenticate an OPTIONS request
		if r.Method == http.MethodOptions {
			next.ServeHTTP(w, r)
			return
		}

		if r.Context().Value(util.AuthProcessedByKey) != nil {
			// Processed by other auth middlewares, skip.
			next.ServeHTTP(w, r)
			return
		}

		rawToken, err := auth0.TokenFromFirst(m.tokenExtractors...)(r)
		if err != nil {
			next.ServeHTTP(w, r)
			return
		}
		user := m.TryGetTestUserFromToken(rawToken)
		if user == nil {
			next.ServeHTTP(w, r)
			return
		}
		newCtx := context.WithValue(r.Context(), auth0.AuthHttpMethod, r.Method)
		newCtx = context.WithValue(newCtx, auth0.AuthValidatorIDKey, auth0.AuthValidatorIDCentral)
		newCtx = context.WithValue(newCtx, auth0.RawTokenKey, rawToken)
		newCtx = context.WithValue(newCtx, auth0.AuthTokenClaimKey, user)
		newCtx = context.WithValue(newCtx, util.AuthProcessedByKey, auth0.AuthByID)

		newRequest := r.WithContext(newCtx)
		*r = *newRequest
		next.ServeHTTP(w, r)
	})
}
