package dbjwt

import (
	"context"
	"time"

	"github.com/tidbcloud/account/internel/svc/token"

	"github.com/tidbcloud/pkgv2/clusterproxy"

	apikey2 "github.com/tidbcloud/account/internel/svc/apikey"
	"github.com/tidbcloud/account/internel/svc/authcenter"
	"github.com/tidbcloud/account/internel/svc/dbuser"
	"github.com/tidbcloud/account/internel/svc/user"

	accountErr "github.com/tidbcloud/account/commons/errors"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/thirdparty/infra"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

var GlobalDBJwtSvc = NewDbJwtSvc()

type DbJwtSvcIface interface {
	GetJwtByToken(ctx context.Context, req *GetJwtByTokenParam) (*DbJwtInfo, error)
	GetJwtByUser(ctx context.Context, req *GetJwtByUserParam) (*DbJwtInfo, error)
	GetJwtByApiKey(ctx context.Context, req *GetJwtByApiKeyParam) (*DbJwtInfo, error)
	GetCloudAdminPasswd(ctx context.Context, req *GetCloudAdminPasswdParam) (string, error)
	//GetJwtCMOperator(ctx context.Context, req GetJwtCMOperatorParam) (*GetJwtCMOperatorResult, error)
	GetJwtCMOperator(ctx context.Context, req GetJwtCMOperatorParam) (*core.DBUserOperator, *core.Cluster, error)
}

type DbJwtSvc struct {
}

func NewDbJwtSvc() DbJwtSvcIface {
	return &DbJwtSvc{}
}

func (s *DbJwtSvc) GetJwtByToken(ctx context.Context, req *GetJwtByTokenParam) (*DbJwtInfo, error) {
	tilog.Info(ctx, "GetJwtByToken start")

	if req.Auth0Token == "" {
		tilog.Info(ctx, "Auth0Token is required", zap.Any("req", req))
		return nil, errs.NewBadRequest("Auth0Token is required", nil)
	}
	if req.ClusterID == 0 {
		tilog.Info(ctx, "ClusterId is required", zap.Any("req", req))
		return nil, errs.NewBadRequest("ClusterId is required", nil)
	}

	tokenSvc, _ := token.GetTokenSvc()
	email, err := tokenSvc.GetUserEmailByToken(ctx, req.Auth0Token)
	if err != nil {
		tilog.Info(ctx, "failed to get user ", zap.Error(err))
		return nil, err
	}

	userSvc, _ := user.GetUserSvc()
	user, err := userSvc.GetUserByEmail(ctx, email)
	if err != nil || (user != nil && user.Status != entity.UserStatusActive) {
		tilog.Info(ctx, "failed to get user by email", zap.Any("req", req), zap.Error(err))
		return nil, err
	}

	dbUser, jwt, _, err := getDBUserAndJwt(ctx, req.ClusterID, user, req.TokenExpiredDuration)
	if err != nil {
		return nil, err
	}

	response := &DbJwtInfo{
		Jwt:    *jwt,
		DbUser: *dbUser,
	}
	return response, nil
}

func getDBUserAndJwt(ctx context.Context, clusterId uint64, user *entity.User, tokenExpiredDuration time.Duration) (*string, *string, *core.Cluster, error) {
	if clusterId == 0 {
		tilog.Info(ctx, "cluster_id is required", zap.Any("clusterId", clusterId))
		return nil, nil, nil, errs.NewBadRequest("cluster_id is required", nil)
	}

	cluster, err := infra.GetHandler().GetClusterById(ctx, clusterId, true)
	if err != nil {
		tilog.Info(ctx, "call cm GetClusterById error", zap.Error(err))
		return nil, nil, nil, err
	}
	if cluster == nil {
		return nil, nil, nil, errs.NewBadRequest("clusterId is  unavailable", nil)
	}
	flag := infra.IsSupportDBUser(ctx, cluster)
	if !flag {
		return nil, nil, nil, accountErr.NewErrClusterNotSupportDbUserError("the cluster not support dbuser", nil)
	}
	isDedicated := !cluster.IsDevTier
	getDBUserReq := utils.GetDBUserReq{
		Email:           user.Email,
		ClusterId:       &clusterId,
		IsDedicated:     &isDedicated,
		ServerlessToken: cluster.ServerlessToken,
	}

	dbUser, err := utils.GetDBUser(ctx, getDBUserReq)
	if err != nil {
		return nil, nil, nil, err
	}
	dbUserSvc, _ := dbuser.GetDBUserSvc()
	jwt, err := dbUserSvc.GetJwtToken(ctx, cluster.TenantId, user.Email, *dbUser, clusterId, isDedicated, tokenExpiredDuration)
	if err != nil {
		return nil, nil, nil, err
	}
	return dbUser, &jwt, cluster, nil
}

func (s *DbJwtSvc) GetJwtByUser(ctx context.Context, req *GetJwtByUserParam) (*DbJwtInfo, error) {
	user, err := getUser(ctx, GetUserParam{req.UserID, req.Email})
	if err != nil {
		return nil, err
	}

	dbUser, jwt, _, err := getDBUserAndJwt(ctx, req.ClusterID, user, req.TokenExpiredDuration)
	if err != nil {
		return nil, err
	}

	jwtInfo := &DbJwtInfo{
		Jwt:    *jwt,
		DbUser: *dbUser,
	}

	return jwtInfo, nil
}

func getUser(ctx context.Context, req GetUserParam) (*entity.User, error) {
	if req.UserID == 0 && req.Email == "" {
		tilog.Info(ctx, "user_id and email cannot be both empty", zap.Any("req", req))
		return nil, errs.NewBadRequest("user_id and email cannot be both empty", nil)
	}

	userSvc, _ := user.GetUserSvc()
	var err error
	var user *entity.User
	if req.Email != "" {
		user, err = userSvc.GetUserByEmail(ctx, req.Email)
		if err != nil || (user != nil && user.Status != entity.UserStatusActive) {
			tilog.Info(ctx, "failed to get user by email", zap.Error(err))
			return nil, errs.NewBadRequest("failed to get user , please check the email", nil)
		}
	} else if req.UserID != 0 {
		user, err = userSvc.GetUserByID(ctx, req.UserID)
		if err != nil || (user != nil && user.Status != entity.UserStatusActive) {
			tilog.Info(ctx, "failed to get user by userID", zap.Error(err))
			return nil, errs.NewRecordExist("failed to get user , please check the user_id", err)
		}
	}
	if user == nil {
		tilog.Info(ctx, "failed to get user e, the user is not exit", zap.Error(err))
		return nil, errs.NewRecordExist("failed to get user e, the user is not exit", err)
	}
	return user, err
}

func (s *DbJwtSvc) GetJwtByApiKey(ctx context.Context, req *GetJwtByApiKeyParam) (*DbJwtInfo, error) {
	if req.ClusterID == 0 {
		tilog.Info(ctx, "cluster_id is required", zap.Any("req", req))
		return nil, errs.NewBadRequest("cluster_id is required", nil)
	}
	param := GetApiKeyParam{req.ApiKeyID, req.AccessKey}
	keyEntity, err := getApikey(ctx, param)
	if err != nil {
		return nil, err
	}
	//dbUser, jwt, err := getApiKeyDBUserAndJwt(ctx, req.ClusterId, keyEntity, a.dbUserSvc)
	resp, _, err := getApiKeyDBUserAndJwt(ctx, req.ClusterID, keyEntity, req.TokenExpiredDuration)

	if err != nil {
		return nil, err
	}
	//  check dbuser
	go checkAPIKeyDBuser(context.Background(), req.AccessKey, keyEntity.Id, req.ClusterID, *resp)

	response := &DbJwtInfo{
		Jwt:    *resp.jwt,
		DbUser: *resp.dbUser,
	}
	return response, nil
}

func getApikey(ctx context.Context, req GetApiKeyParam) (*apikey2.ApiKey, error) {
	tilog.Info(ctx, "GetJwtByApiKey start")
	if req.AccessKey == "" && req.ApiKeyID == 0 {
		tilog.Info(ctx, "access_key and api_key_id cannot be both empty", zap.Any("req", req))
		return nil, errs.NewBadRequest("access_key cannot be empty", nil)
	}
	//if req.ClusterID == 0 {
	//	tilog.Info(ctx, "cluster_id is required", zap.Any("req", req))
	//	return nil, errs.NewBadRequest("cluster_id is required", nil)
	//}
	apiKeySvc, _ := apikey2.GetApiKeySvc()
	var keyEntity *apikey2.ApiKey
	if req.AccessKey != "" {
		apiKey, err := apiKeySvc.GetApiKeyByAccessKey(ctx, req.AccessKey)
		if err != nil {
			tilog.Error(ctx, "failed to get api key by ak", zap.Error(err))
			return nil, errs.NewRecordExist("failed to get key , please check the ak", err)
		}
		keyEntity = apiKey
	} else if req.ApiKeyID != 0 {
		apiKey, err := apiKeySvc.GetApiKeyByID(ctx, req.ApiKeyID)
		if err != nil {
			tilog.Error(ctx, "failed to get api key by id", zap.Error(err))
			return nil, errs.NewRecordExist("failed to get user , please check the id", err)
		}
		keyEntity = apiKey
	}
	if keyEntity == nil {
		tilog.Info(ctx, "api key not exist")
		return nil, errs.NewRecordExist("api key not exist", nil)
	}
	if keyEntity.ExpiredAt != nil && time.Now().UTC().After(keyEntity.ExpiredAt.UTC()) {
		tilog.Info(ctx, "api key has expired")
		return nil, errs.NewRecordExist("api key has expired", nil)
	}
	return keyEntity, nil
}

func (s *DbJwtSvc) GetCloudAdminPasswd(ctx context.Context, req *GetCloudAdminPasswdParam) (string, error) {
	tilog.Info(ctx, "GetCloudAdminPasswd start", zap.Any("req", req))
	if req.ClusterID == 0 {
		tilog.Info(ctx, "cluster_id is required", zap.Any("req", req))
		return "", errs.NewBadRequest("cluster_id is required", nil)
	}

	//operator, err := infra.GetClusterCloudAdmin(ctx, cluster)
	svc, _ := authcenter.GetClusterUserSvc()
	operator, err := svc.GetClusterCloudAdmin(ctx, req.ClusterID, 0)
	if err != nil {
		return "", err
	}

	return operator.Credential, nil
}

// GetJwtCMOperator
//
//	@Description: get operator to call cm  dbsuer api
//	@Author wangletian 2024-02-27 14:21:47
//	@receiver s
//	@param ctx
//	@param req
//	@return *GetCMOperatorResp
//	@return error
func (s *DbJwtSvc) GetJwtCMOperator(ctx context.Context, req GetJwtCMOperatorParam) (*core.DBUserOperator, *core.Cluster, error) {
	if req.ClusterID == 0 {
		tilog.Info(ctx, "cluster_id is required", zap.Any("req", req))
		return nil, nil, errs.NewBadRequest("cluster_id is required", nil)
	}

	var dbUser *string
	var jwt *string
	//var cmOperator *core.DBUserOperator
	var cluster *core.Cluster
	// user
	if req.OperatorType == entity.RoleBindingTargetUser {

		user, err := getUser(ctx, GetUserParam{req.UserID, req.Email})
		if err != nil {
			return nil, nil, err
		}

		dbUser, jwt, cluster, err = getDBUserAndJwt(ctx, req.ClusterID, user, req.TokenExpiredDuration)
		if err != nil {
			return nil, nil, err
		}
		//  api key
	} else if req.OperatorType == entity.RoleBindingTargetAPIKey && req.ApiKey == nil {

		param := GetApiKeyParam{req.ApiKeyID, req.AccessKey}
		keyEntity, err := getApikey(ctx, param)
		if err != nil {
			return nil, nil, err
		}
		//dbUser, jwt, err := getApiKeyDBUserAndJwt(ctx, req.ClusterId, keyEntity, a.dbUserSvc)
		resp, cmCluster, err := getApiKeyDBUserAndJwt(ctx, req.ClusterID, keyEntity, req.TokenExpiredDuration)

		if err != nil {
			return nil, nil, err
		}
		dbUser = resp.dbUser
		jwt = resp.jwt
		cluster = cmCluster

	} else if req.OperatorType == entity.RoleBindingTargetAPIKey && req.ApiKey != nil {

		resp, cmCluster, err := getApiKeyDBUserAndJwt(ctx, req.ClusterID, req.ApiKey, req.TokenExpiredDuration)

		if err != nil {
			return nil, nil, err
		}
		dbUser = resp.dbUser
		jwt = resp.jwt
		cluster = cmCluster

	}
	cmOperator := &core.DBUserOperator{
		Username:   *dbUser,
		AuthMethod: core.DBUser_AUTH_METHOD_TOKEN,
		Credential: *jwt,
	}
	//resp := &GetJwtCMOperatorResult{
	//	DBUserOperator: cmOperator,
	//	Cluster:        cluster,
	//}
	//return resp, nil
	return cmOperator, cluster, nil

}

func getApiKeyDBUserAndJwt(ctx context.Context, clusterId uint64, apiKey *apikey2.ApiKey, tokenExpiredDuration time.Duration) (*GetApiKeyDBUserAndJwtResult, *core.Cluster, error) {
	cluster, err := infra.GetHandler().GetClusterById(ctx, clusterId, true, infra.SupportDBUserStatus...)
	if err != nil {
		tilog.Info(ctx, "call cm GetClusterById error", zap.Error(err))
		return nil, nil, err
	}
	if cluster == nil {
		return nil, nil, errs.NewBadRequest("clusterId is  unavailable", nil)
	}
	flag := infra.IsSupportDBUser(ctx, cluster)
	if !flag {
		return nil, nil, errs.NewBadRequest("the cluster not support dbuser", nil)
	}
	tilog.Info(ctx, "the api key  have cluster permissions", zap.Any("access_key", apiKey.AccessKey), zap.Any("clusterId", clusterId))
	isDedicated := !cluster.IsDevTier
	getDBUserReq := utils.GetDBUserReq{
		Email:           apiKey.AccessKey,
		ClusterId:       &clusterId,
		IsDedicated:     &isDedicated,
		ServerlessToken: cluster.ServerlessToken,
		TargetType:      entity.RoleBindingTargetAPIKey.String(),
	}

	dbUser, err := utils.GetDBUser(ctx, getDBUserReq)
	if err != nil {
		return nil, nil, err
	}
	dbUserSvc, _ := dbuser.GetDBUserSvc()
	jwt, err := dbUserSvc.GetJwtToken(ctx, cluster.TenantId, "", *dbUser, clusterId, isDedicated, tokenExpiredDuration)
	if err != nil {
		return nil, nil, err
	}
	resp := GetApiKeyDBUserAndJwtResult{
		dbUser:          dbUser,
		jwt:             &jwt,
		isDedicated:     &isDedicated,
		serverlessToken: cluster.ServerlessToken,
		orgId:           &cluster.TenantId,
	}

	return &resp, cluster, nil
}

func checkAPIKeyDBuser(ctx context.Context, apikey string, apiKeyId, clusterId uint64, resp GetApiKeyDBUserAndJwtResult) {
	cluster, err := infra.GetHandler().GetClusterById(ctx, clusterId, true, infra.SupportDBUserStatus...)
	if err != nil {
		tilog.Info(ctx, "checkAPIKeyDBuser call cm GetClusterById error", zap.Error(err))
		return
	}
	if cluster == nil {
		return
	}
	flag := infra.IsSupportDBUser(ctx, cluster)
	if !flag {
		return
	}

	svc, _ := authcenter.GetClusterUserSvc()
	operator, err := svc.GetExistDBUser(ctx, clusterId)
	if err != nil {
		tilog.Warn(ctx, "checkAPIKeyDBuser GetOrgOwnerDbUser error", zap.Error(err))
		dbUserSvc, _ := dbuser.GetDBUserSvc()
		_ = dbUserSvc.CreateCommandDBUserCluster(ctx, clusterId, cluster.TenantId, apiKeyId, apiKeyId, *resp.dbUser,
			entity.CommandStatusWaiting, entity.CommandActionCheck, entity.ClusterUserTargetApiKey, entity.DbRoleAdmin, entity.ClusterUserTargetApiKey)
		return
	}
	status := infra.GetHandler().CreateDBUser(ctx, clusterId, operator.Username, clusterproxy.AuthMethodToken, operator.Credential, "", *resp.dbUser, true, nil, entity.DbRoleAdmin)
	tilog.Info(ctx, "checkAPIKeyDBuser  result", zap.Any("status", status), zap.Any("ClusterID", clusterId), zap.Any("apikey", apikey))

}
