package token

import (
	"context"
	"strings"
	"sync"

	"github.com/form3tech-oss/jwt-go"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/commons/config"

	"go.uber.org/zap"

	"github.com/tidbcloud/tiinf/monitor/tilog"

	"github.com/tidbcloud/account/pkg/auth/token"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/tiinf/errs"
)

var _ TokenSvcIface = (*TokenSvc)(nil)

type TokenSvcIface interface {
	GetUserEmailByToken(ctx context.Context, token string) (string, error)
	GetTokenType(ctx context.Context, token string) (string, error)
	GetClaimsFromToken(ctx context.Context, token string) (jwt.MapClaims, error)
	GetEmailTokenTypeByToken(ctx context.Context, token string) (string, string, error)
}

var (
	tokenSvcInitOnce sync.Once
	tokenSvcInstance TokenSvcIface
	tokenSvcInitErr  error
)

func GetTokenSvc() (TokenSvcIface, error) {
	tokenSvcInitOnce.Do(func() {
		var err error
		tokenSvcInstance, err = NewTokenSvc()
		if err != nil {
			tokenSvcInitErr = err
		}

	})
	return tokenSvcInstance, tokenSvcInitErr
}

type TokenSvc struct {
	testTokenModel TestTokenSvc
	auth0Validator *token.Validator
}

func NewTokenSvc() (TokenSvcIface, error) {
	centralConfig := config.Global()
	auth0Config := centralConfig.Auth0Config

	centralAuth0 := auth0.Conf{
		Audience: auth0Config.Auth0Audience,
		Secret:   []byte(auth0Config.Auth0Secret),
		Domain:   auth0Config.Auth0Domain,
	}
	portalAuth0 := auth0.Conf{
		Audience:                 auth0Config.ManagementPortalAuth0Audience,
		Secret:                   []byte(auth0Config.ManagementPortalAuth0Secret),
		Domain:                   auth0Config.ManagementPortalAuth0Domain,
		ClientCredentialAudience: auth0Config.ManagementPortalAuth0ClientAudience,
	}

	jwks, err := auth0.GetJWKS(centralAuth0.Domain)
	if err != nil {
		return nil, errors.Trace(err)
	}
	managementPortalJWKs, err := auth0.GetJWKS(portalAuth0.Domain)
	if err != nil {
		return nil, errors.Trace(err)
	}

	tokenValidator := auth0.NewRawTokenValidator().
		RegisterAuth(auth0.AuthValidatorIDCentral, centralAuth0, jwks).
		RegisterAuth(auth0.AuthValidatorIDManagement, portalAuth0, managementPortalJWKs)

	auth0Validator := token.NewValidator(tokenValidator)

	testTokenSvc, _ := GetTestTokenSvc()

	return &TokenSvc{
		testTokenModel: testTokenSvc,
		auth0Validator: auth0Validator,
	}, nil
}

func (s *TokenSvc) GetUserEmailByToken(ctx context.Context, token string) (string, error) {
	email := s.testTokenModel.TryGetEmailFromTestToken(token)
	if email != "" {
		return email, nil
	}

	claims, err := s.auth0Validator.RawTokenValidator.ValidateFor(auth0.AuthValidatorIDCentral, token)
	if err != nil {
		return "", errs.NewUnauthorized("invalid token", err)
	}

	email, err = auth0.GetEmailClaim(claims)
	if err != nil {
		return "", errs.NewUnauthorized("failed to get email from token", err)
	}
	tilog.Info(ctx, "get email form token", zap.Any("email", email))
	return email, nil
}

func (s *TokenSvc) GetClaimsFromToken(ctx context.Context, token string) (jwt.MapClaims, error) {
	return s.auth0Validator.RawTokenValidator.ValidateFor(auth0.AuthValidatorIDCentral, token)
}

func (s *TokenSvc) GetTokenType(ctx context.Context, token string) (string, error) {
	email := s.testTokenModel.TryGetEmailFromTestToken(token)
	if email != "" {
		return TokenTypeTest, nil
	}

	claims, err := s.auth0Validator.RawTokenValidator.ValidateFor(auth0.AuthValidatorIDCentral, token)
	if err != nil {
		return "", err
	}

	sub, err := auth0.GetSubjectClaim(claims)
	if err != nil {
		return "", err
	}

	items := strings.Split(sub, "|")
	if len(items) == 0 {
		return "", errs.NewBadRequest("bad token, no sub found", nil)
	}

	return items[0], nil
}

func (s *TokenSvc) GetEmailTokenTypeByToken(ctx context.Context, token string) (string, string, error) {
	email := s.testTokenModel.TryGetEmailFromTestToken(token)
	if email != "" {
		return email, TokenTypeTest, nil
	}

	claims, err := s.auth0Validator.RawTokenValidator.ValidateFor(auth0.AuthValidatorIDCentral, token)
	if err != nil {
		return "", "", errs.NewUnauthorized("invalid token", err)
	}
	email, err = auth0.GetEmailClaim(claims)
	if err != nil {
		return "", "", errs.NewUnauthorized("failed to get email from token", err)
	}
	sub, err := auth0.GetSubjectClaim(claims)
	if err != nil {
		return "", "", errs.NewUnauthorized("invalid token", err)
	}
	items := strings.Split(sub, "|")
	if len(items) == 0 {
		return "", "", errs.NewBadRequest("bad token, no sub found", nil)
	}
	return email, items[0], nil
}
