package sqluser

import (
	"context"
	"fmt"
	"sync"

	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/svc/apikey"
	dbuser2 "github.com/tidbcloud/account/internel/svc/dbuser"

	"github.com/duke-git/lancet/v2/slice"
	"github.com/tidbcloud/account/internel/dao/entity"

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"

	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/account/thirdparty/infra"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/account/internel/dao"
	"gorm.io/gorm"
)

const (
	queryTypeUser  = "USER"
	queryTypeRole  = "ROLE"
	queryTypAll    = "ALL"
	cloudRoleAdmin = "role_admin"
)

var (
	IgnoreBuiltinRoles = []string{"root", "cloud_admin", "role_admin", "role_cdc_admin", "role_readwrite", "role_readonly"}
	// The list page needs to show root
	IgnoreBuiltinRolesInList = []string{"cloud_admin", "playground", "role_admin", "role_cdc_admin", "role_readwrite", "role_readonly"}
	CloudBuiltinRoles        = []string{"role_admin", "role_readwrite", "role_readonly"}
	CmCustomRoles            = []string{"role_readwrite", "role_readonly"}
	CmBuiltinRoles           = []string{"role_admin"}
	RootUser                 = "root"

	// query role sql: SELECT USER, PLUGIN,ACCOUNT_LOCKED,Password_expired FROM MYSQL.USER where  Password_expired='Y' AND ACCOUNT_LOCKED='Y' AND USER NOT IN('4Y7hgLqzKAqj1Nh.root');
	userFilter = core.ListDBUsersFilter{
		PasswordExpired: []string{"N"},
		AccountLocked:   []string{"N"},
		Plugin:          []string{"mysql_native_password"}}

	roleFilter = core.ListDBUsersFilter{
		PasswordExpired: []string{"Y"},
		AccountLocked:   []string{"Y"}}
)

type SqlUserSvcIface interface {
	CreateSqlUserToCm(ctx context.Context, req *central.CreateSqlUserReq, operatorRe dbuser2.GetCMOperatorResp) error
	ListSqlUserFormCM(ctx context.Context, clusterId uint64, cmOperatorResp dbuser2.GetCMOperatorResp) (*central.ListSqlUsersResp, error)
	QueryFormCM(ctx context.Context, req *central.QuerySqlUserReq, operator dbuser2.GetCMOperatorResp) (resp *central.QuerySqlUserResp, err error)
	UpdateSqlUserToCM(ctx context.Context, req *central.UpdateSqlUserReq, updateCustomRoleFlag bool, operatorRe dbuser2.GetCMOperatorResp) error
	DeleteSqlUserToCM(ctx context.Context, clusterId uint64, deleteUsername string, operatorResp dbuser2.GetCMOperatorResp) error
}

var (
	sqlUserSvcInitOnce sync.Once
	sqlUserSvcInstance SqlUserSvcIface
	sqlUserSvcInitErr  error
)

func GetSqlUserSvc() (SqlUserSvcIface, error) {
	sqlUserSvcInitOnce.Do(func() {
		sqlUserSvcInstance = NewSqlUserSvc(database.DBConn.DB, dao.GetDaoHub())
	})
	return sqlUserSvcInstance, sqlUserSvcInitErr
}

type SqlUserSvc struct {
	db  *gorm.DB
	hub *dao.DaoHub
}

func NewSqlUserSvc(db *gorm.DB, hub *dao.DaoHub) SqlUserSvcIface {
	return &SqlUserSvc{
		db:  db,
		hub: hub,
	}
}

func (s *SqlUserSvc) DeleteSqlUserToCM(ctx context.Context, clusterId uint64, deleteUsername string, operatorResp dbuser2.GetCMOperatorResp) error {
	if deleteUsername == "" {
		return errs.NewBadRequest("delete user must not null", nil)
	}
	if clusterId == 0 {
		return errs.NewBadRequest("cluster_id must not null", nil)
	}

	//operatorResp, err := s.dbUserSvc.GetCMOperator(ctx, getCMOperatorReq)
	rooUser := GetClusterUser(RootUser, operatorResp.Cluster.ServerlessToken)
	if deleteUsername == rooUser {
		msg := fmt.Sprintf("%s users are not allowed to delete", rooUser)
		return errs.NewBadRequest(msg, nil)
	}
	// query the sqluser

	filter := &central.QuerySqlUserFilter{
		//AuthTypes: req.AuthMethods,
		UserNames: []string{deleteUsername},
		QueryType: "USER",
	}
	param := &central.QuerySqlUserReq{
		ClusterId: clusterId,
		Filter:    filter,
	}

	querySqlUserResp, err := s.QueryFormCM(ctx, param, operatorResp)
	if err != nil {
		tilog.Info(ctx, "QueryFormCM err", zap.Error(err))
		//msg := fmt.Sprintf("CreateSqlUser [%s] fail, err %s", username, err.Error())
		return err
	}
	if len(querySqlUserResp.Users) == 0 {
		return errs.NewBadRequest(deleteUsername+" is not exist", nil)
	}
	return infra.GetHandler().DeleteSqlUser(ctx, operatorResp.DBUserOperator, deleteUsername, clusterId)
}
func (s *SqlUserSvc) CreateSqlUserToCm(ctx context.Context, req *central.CreateSqlUserReq, operatorResp dbuser2.GetCMOperatorResp) error {
	tilog.Info(ctx, "CreateSqlUserToCm req", zap.Any("CreateSqlUserReq", fmt.Sprintf("username=%s", req.UserName)))
	queryReq := &central.QuerySqlUserReq{
		OrgId:     req.OrgId,
		ProjectId: req.ProjectId,
		ClusterId: req.ClusterId,
		Filter: &central.QuerySqlUserFilter{
			UserNames: []string{req.UserName},
			QueryType: queryTypAll,
		},
	}
	resp, err := s.QueryFormCM(ctx, queryReq, operatorResp)
	if err != nil {
		return err
	}
	if len(resp.SqlUser) > 0 {
		msg := fmt.Sprintf("SQL user %s already exists.", req.UserName)
		tilog.Info(ctx, msg)
		return errs.NewBadRequest(msg, nil)
	}

	//operatorResp, err := s.dbUserSvc.GetCMOperator(ctx, getCMOperatorReq)
	if err != nil {
		return err
	}
	ignores := GetClusterUsers(IgnoreBuiltinRoles, operatorResp.Cluster.ServerlessToken)
	if slice.Contain(ignores, req.UserName) {

		msg := "User name conflicts with reserved name"
		tilog.Info(ctx, msg)
		return errs.NewBadRequest(msg, nil)
	}

	cmBuiltinRole, cmCustomRoles, _, err := CloudRoleToCM(ctx, req.BuiltinRole, req.CustomRole, operatorResp.Cluster.ServerlessToken)
	if err != nil {
		tilog.Info(ctx, "CloudRoleToCM err", zap.Error(err))
		return err
	}
	//password := getBase64Password(ctx, req.Password)
	email := ""
	createSqlUserReq := &core.CreateDBUserReq{
		ClusterId:   req.ClusterId,
		Username:    req.UserName,
		Email:       &email,
		Operator:    operatorResp.DBUserOperator,
		CustomRoles: cmCustomRoles,
		BuiltinRole: cmBuiltinRole,
		AuthMethod:  core.DBUser_AUTH_METHOD_PASSWORD,
		UserType:    core.DBUser_TYPE_USER_DEFINED,
		Settings:    &core.CreateDBUserReq_Settings{IgnoreExists: false},
		Credential:  &req.Password,
	}

	return infra.GetHandler().CreateGeneralDBUser(ctx, createSqlUserReq, req.BuiltinRole)
}

func (s *SqlUserSvc) ListSqlUserFormCM(ctx context.Context, clusterId uint64, operatorResp dbuser2.GetCMOperatorResp) (*central.ListSqlUsersResp, error) {

	ignores := GetClusterUsers(IgnoreBuiltinRolesInList, operatorResp.Cluster.ServerlessToken)

	//tilog.Info(ctx, "operatorResp for "+fmt.Sprint(cmOperator.OperatorType), zap.Any("DBUserOperator", operatorResp.DBUserOperator))
	tilog.Info(ctx, "getClusterUsers", zap.Any("ServerlessToken", operatorResp.Cluster.ServerlessToken))

	filter := getUserFilter()
	filter.IgnoreUsers = ignores
	// SELECT USER, PLUGIN,ACCOUNT_LOCKED,Password_expired FROM MYSQL.USER where  PLUGIN = 'mysql_native_password' AND Password_expired='N' AND ACCOUNT_LOCKED='N'  ;
	listDBUsersReq := &core.ListDBUsersReq{
		ClusterId: clusterId,
		Operator:  operatorResp.DBUserOperator,
		Filter:    filter,
	}
	cmResp, err := infra.GetHandler().ListSqlUser(ctx, listDBUsersReq)
	if err != nil {
		return nil, err
	}

	rooUser := GetClusterUser(RootUser, operatorResp.Cluster.ServerlessToken)

	resp := &central.ListSqlUsersResp{}
	var sqlUsers []*central.SqlUser
	for _, user := range cmResp.DbUsers {
		bRole, cRole := CmRoleToCloud(ctx, user.BuiltinRole, user.CustomRoles, operatorResp.Cluster.ServerlessToken)
		u := &central.SqlUser{
			UserName:    user.Username,
			AuthMethod:  user.Plugin,
			BuiltinRole: bRole,
			CustomRole:  cRole,
		}
		if user.Username == rooUser {
			u.BuiltinRole = cloudRoleAdmin
		}
		sqlUsers = append(sqlUsers, u)
	}
	resp.Items = sqlUsers

	return resp, nil
}

func (s *SqlUserSvc) QueryFormCM(ctx context.Context, req *central.QuerySqlUserReq, operatorResp dbuser2.GetCMOperatorResp) (resp *central.QuerySqlUserResp, err error) {

	ignoreRoles := GetClusterUsers(IgnoreBuiltinRoles, operatorResp.Cluster.ServerlessToken)
	tilog.Info(ctx, "QueryFormCM req", zap.Any("req", req))
	var filter *core.ListDBUsersFilter
	// user
	if req.Filter.QueryType == queryTypeUser {
		//	SELECT USER, PLUGIN,ACCOUNT_LOCKED,Password_expired FROM MYSQL.USER
		//	where  PLUGIN = 'mysql_native_password' AND Password_expired='N' AND ACCOUNT_LOCKED='N'  ;
		filter = getUserFilter()
		//  role
	} else if req.Filter.QueryType == queryTypeRole {
		// query role sql:
		// SELECT USER, PLUGIN,ACCOUNT_LOCKED,Password_expired FROM MYSQL.USER
		//where  Password_expired='Y' AND ACCOUNT_LOCKED='Y' AND USER NOT IN('4Y7hgLqzKAqj1Nh.root');
		filter = getRoleFilter()
		filter.IgnoreUsers = ignoreRoles

	} else if req.Filter.QueryType == queryTypAll {
		// query role sql:
		// SELECT USER, PLUGIN,ACCOUNT_LOCKED,Password_expired FROM MYSQL.USER
		//where  Password_expired='Y' AND ACCOUNT_LOCKED='Y' AND USER NOT IN('4Y7hgLqzKAqj1Nh.root');
		filter = &core.ListDBUsersFilter{}
	}

	filter.Users = req.Filter.UserNames
	filter.Plugin = req.Filter.AuthTypes

	listDBUsersReq := &core.ListDBUsersReq{
		ClusterId: req.ClusterId,
		Operator:  operatorResp.DBUserOperator,
		Filter:    filter,
	}
	cmResp, err := infra.GetHandler().ListSqlUser(ctx, listDBUsersReq)
	if err != nil {
		return nil, err

	}
	rooUser := GetClusterUser(RootUser, operatorResp.Cluster.ServerlessToken)
	//resp := &central.QuerySqlUserResp{}
	var sqlUsers []*central.SqlUser
	for _, user := range cmResp.DbUsers {
		bRole, cRole := CmRoleToCloud(ctx, user.BuiltinRole, user.CustomRoles, operatorResp.Cluster.ServerlessToken)
		u := &central.SqlUser{
			UserName:    user.Username,
			AuthMethod:  user.Plugin,
			BuiltinRole: bRole,
			CustomRole:  cRole,
		}
		if user.Username == rooUser {
			u.BuiltinRole = cloudRoleAdmin
		}
		sqlUsers = append(sqlUsers, u)
	}

	var users []string
	var roles []string
	resultMap := cmResp.ResultMap

	if len(resultMap) > 0 {
		userArr := resultMap["USER"].Values
		pluginArr := resultMap["PLUGIN"].Values
		alArr := resultMap["ACCOUNT_LOCKED"].Values
		peArr := resultMap["PASSWORD_EXPIRED"].Values
		for i := 0; i < len(userArr); i++ {
			// user

			//if pluginArr[i] == "mysql_native_password" && alArr[i] == "N" && peArr[i] == "N" {
			if slice.Contain(userFilter.Plugin, pluginArr[i]) && slice.Contain(userFilter.AccountLocked, alArr[i]) && slice.Contain(userFilter.PasswordExpired, peArr[i]) {
				users = append(users, userArr[i])
				//} else if alArr[i] == "Y" && peArr[i] == "Y" {
			} else if slice.Contain(roleFilter.AccountLocked, alArr[i]) && slice.Contain(roleFilter.PasswordExpired, peArr[i]) {
				roles = append(roles, userArr[i])
			}

		}
	}

	resp = &central.QuerySqlUserResp{
		Users:   users,
		Roles:   roles,
		SqlUser: sqlUsers,
	}
	return resp, err

}

/*
		request :  CustomRole=[]
	    gin   (req.CustomRole == nil)   =  false
	    grpc   (req.CustomRole == nil)  =  true

	    request :  do not set the parameter   CustomRole  , CustomRole=nil
	    gin   (req.CustomRole == nil)   =  true
	    grpc   (req.CustomRole == nil)  =  true
*/
func (s *SqlUserSvc) UpdateSqlUserToCM(ctx context.Context, req *central.UpdateSqlUserReq, updateCustomRoleFlag bool, operatorResp dbuser2.GetCMOperatorResp) error {
	tilog.Info(ctx, "UpdateSqlUserToCM req", zap.Any("req", req))

	filter := &central.QuerySqlUserFilter{
		//AuthTypes: req.AuthMethods,
		UserNames: []string{req.UserName},
		QueryType: "USER",
	}
	param := &central.QuerySqlUserReq{
		ClusterId: operatorResp.Cluster.Id,
		Filter:    filter,
	}

	querySqlUserResp, err := s.QueryFormCM(ctx, param, operatorResp)
	if err != nil {
		tilog.Info(ctx, "QueryFormCM err", zap.Error(err))
		//msg := fmt.Sprintf("CreateSqlUser [%s] fail, err %s", username, err.Error())
		return err
	}
	if len(querySqlUserResp.Users) == 0 {
		return errs.NewBadRequest(req.UserName+" is not exist", nil)
	}
	willUpdateUser := querySqlUserResp.SqlUser[0]
	rooUser := GetClusterUser(RootUser, operatorResp.Cluster.ServerlessToken)
	//tilog.Info(ctx, rooUser, zap.Any("rooUser", rooUser))
	if req.UserName == rooUser {
		if (req.BuiltinRole != "" && req.BuiltinRole != cloudRoleAdmin) || len(req.CustomRole) > 0 {
			return errs.NewBadRequest(req.UserName+"'s role is not allowed to update", nil)
		}
	}

	updateDBUserReq := &core.UpdateDBUserReq{
		ClusterId: operatorResp.Cluster.Id,
		Username:  req.UserName,
		Operator:  operatorResp.DBUserOperator,
	}
	checkCloudBuiltinRole := ""
	tilog.Info(ctx, "UpdateSqlUserToCM req.CustomRole ", zap.Any("req.CustomRole ", req.CustomRole), zap.Any("req.CustomRole == nil ", req.CustomRole == nil))
	// root user：Only password changes are allowed
	if req.UserName != rooUser {
		//  dont  update the BuiltinRole,  user the old BuiltinRole
		if req.BuiltinRole == "" {
			tilog.Info(ctx, "no need to update builtinRole, use old role", zap.Any("builtinRole", willUpdateUser.BuiltinRole))
			req.BuiltinRole = willUpdateUser.BuiltinRole
		}
		//  dont  update the CustomRole,  user the old CustomRole
		//if req.CustomRole == nil {
		if !updateCustomRoleFlag {
			tilog.Info(ctx, "no need to update customRole, use old role", zap.Any("CustomRoles", willUpdateUser.CustomRole))
			req.CustomRole = willUpdateUser.CustomRole
		}

		bRole, cRole, clearCmCustomRoles, err := CloudRoleToCM(ctx, req.BuiltinRole, req.CustomRole, operatorResp.Cluster.ServerlessToken)
		if err != nil {
			return err
		}
		updateDBUserReq.BuiltinRole = &bRole
		updateDBUserReq.CustomRoles = cRole
		updateDBUserReq.ClearCustomRoles = clearCmCustomRoles
		checkCloudBuiltinRole = req.BuiltinRole
	}
	if req.Password != "" {
		updateDBUserReq.Password = &req.Password
	}

	return infra.GetHandler().UpdateSqlUser(ctx, updateDBUserReq, checkCloudBuiltinRole)
}

func CmRoleToCloud(ctx context.Context, cmBuiltinRole core.DBUser_BuiltinRole, cmCustomRoles []string, serverlessToken *string) (cloudBuiltinRole string, cloudCustomRoles []string) {

	if cmBuiltinRole == core.DBUser_BUILTIN_ROLE_UNSPECIFIED {
		cloudBuiltinRolesInCluster := GetClusterUsers(CloudBuiltinRoles, serverlessToken)
		for _, role := range cmCustomRoles {
			if slice.Contain(cloudBuiltinRolesInCluster, role) {
				cloudBuiltinRole = role
			}
		}
	} else if cmBuiltinRole == core.DBUser_BUILTIN_ROLE_USER_ADMIN {
		cloudBuiltinRole = GetClusterUser(cloudRoleAdmin, serverlessToken)
	}
	cloudCustomRoles = slice.Difference(cmCustomRoles, GetClusterUsers(CloudBuiltinRoles, serverlessToken))
	//tilog.Info(ctx, "CmRoleToCloud ", zap.Any("cm builtinRole", cmBuiltinRole), zap.Any("customRoles", cmCustomRoles))
	//tilog.Info(ctx, "CmRoleToCloud ", zap.Any("cloudBuiltinRole", cloudBuiltinRole), zap.Any("cloudCustomRoles", cloudCustomRoles))
	return cloudBuiltinRole, cloudCustomRoles

}

func CloudRoleToCM(ctx context.Context, cloudBuiltinRole string, cloudCustomRoles []string, serverlessToken *string) (cmBuiltinRole core.DBUser_BuiltinRole, cmCustomRoles []string, clearCmCustomRoles bool, err error) {
	//CloudBuiltinRoles
	cmBuiltinRolesInCluster := GetClusterUsers(CmBuiltinRoles, serverlessToken)
	cmCustomRolesInCluster := GetClusterUsers(CmCustomRoles, serverlessToken)
	CloudBuiltinRolesArr := GetClusterUsers(CloudBuiltinRoles, serverlessToken)
	cmCustomRoles = []string{}
	if cloudBuiltinRole != "" && !slice.Contain(CloudBuiltinRolesArr, cloudBuiltinRole) {
		msg := "builtin role is illegal"
		tilog.Info(ctx, msg)
		return cmBuiltinRole, cmCustomRoles, clearCmCustomRoles, errs.NewBadRequest(msg, nil)
	}

	if slice.Contain(cmBuiltinRolesInCluster, cloudBuiltinRole) {
		// builtinRole=role_admin
		if cloudBuiltinRole == GetClusterUser(cloudRoleAdmin, serverlessToken) {

			cmBuiltinRole = core.DBUser_BUILTIN_ROLE_USER_ADMIN

			//cmCustomRoles = cloudCustomRoles
			cmCustomRoles = append(cmCustomRoles, cloudCustomRoles...)
		}
		//{"role_readwrite", "role_readonly"} to  add  customRoles  in cm
	} else if slice.Contain(cmCustomRolesInCluster, cloudBuiltinRole) {
		cmCustomRoles = append(cmCustomRoles, cloudBuiltinRole)
		cmCustomRoles = append(cmCustomRoles, cloudCustomRoles...)
	} else {
		cmBuiltinRole = core.DBUser_BUILTIN_ROLE_UNSPECIFIED
		cmCustomRoles = append(cmCustomRoles, cloudCustomRoles...)
	}
	tilog.Info(ctx, "CloudRoleToCM： cloudRole", zap.Any("cloudBuiltinRole", cloudBuiltinRole), zap.Any("cloudCustomRoles", cloudCustomRoles))
	tilog.Info(ctx, "CloudRoleToCM： CMRole ", zap.Any("cmBuiltinRole", cmBuiltinRole), zap.Any("cmCustomRoles", cmCustomRoles))
	clearCmCustomRoles = len(cmCustomRoles) == 0
	return cmBuiltinRole, cmCustomRoles, clearCmCustomRoles, nil
}

func GetClusterUsers(users []string, serverlessToken *string) (newUsers []string) {
	if serverlessToken != nil {
		for _, name := range users {
			var newName string
			// rol_admin has no token prefix
			if slice.Contain(CmBuiltinRoles, name) {
				newName = name
			} else {

				newName = fmt.Sprintf("%s.%s", *serverlessToken, name)
			}
			newUsers = append(newUsers, newName)
		}
		return newUsers
	}
	return users
}

func GetClusterUser(user string, serverlessToken *string) (newUsers string) {
	return GetClusterUsers([]string{user}, serverlessToken)[0]
}

func getUserFilter() *core.ListDBUsersFilter {
	userFilter := &core.ListDBUsersFilter{
		PasswordExpired: userFilter.PasswordExpired,
		AccountLocked:   userFilter.AccountLocked,
		Plugin:          userFilter.Plugin,
	}
	return userFilter
}

func getRoleFilter() *core.ListDBUsersFilter {
	userFilter := &core.ListDBUsersFilter{
		PasswordExpired: roleFilter.PasswordExpired,
		AccountLocked:   roleFilter.AccountLocked,
	}
	return userFilter
}

type ListSqlUsersReq struct {
	UserID       uint64
	Email        string
	OperatorType entity.TargetType
	ApiKey       *apikey.ApiKey
	ApiKeyID     uint64
	AccessKey    string
	ClusterID    uint64
	UserId       uint64

	//OrgId     uint64
	//ProjectId uint64

	PageSize uint32
	Page     uint32
}

type GetJwtCMOperatorResult struct {
	Cluster        *core.Cluster
	DBUserOperator *core.DBUserOperator
}
