package infra

import (
	"context"
	"encoding/json"

	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"github.com/tidbcloud/pkgv2/asserts"
	"github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

type CreateUserInfo struct {
	//UserType          DBUserType
	UserName          string
	UserAttribute     map[string]string // only add user attribute when auth method is token
	AuthMethod        clusterproxy.AuthMethod
	EncryptCredential string  // optional, required if auth_method is password
	BuiltinRole       *string // nil or blank means do not to set builtin role
	CustomRoles       []string
	Grants            string
}

type CreateUserParams struct {
	Cluster  *core.Cluster
	Operator *clusterproxy.ClusterOperator
	Settings *core.CreateDBUserReq_Settings // optional

	*CreateUserInfo
}

func CreateUser(ctx context.Context, params *CreateUserParams) error {
	if err := checkCreateUserParams(params); err != nil {
		tilog.Warn(ctx, "InfraProxy CreateUser checkCreateUserParams error", zap.Any("err", err))
		return err
	}
	tilog.Info(ctx, "InfraProxy CreateUser param", zap.Any("clusterId", params.Cluster.Id), zap.Any("operator", params.Operator.Username), zap.Any("createUserInfo", params.CreateUserInfo))

	if params.Settings != nil && params.Settings.IgnoreExists {
		existing, err := IsUserExisting(ctx, params.Cluster, params.Operator, params.UserName)
		if err != nil {
			tilog.Warn(ctx, "InfraProxy CreateUser IsUserExisting error", zap.Any("clusterId", params.Cluster.Id), zap.Any("operator", params.Operator.Username), zap.Any("username", params.UserName), zap.Any("err", err))
			return err
		}
		if existing {
			tilog.Info(ctx, "InfraProxy CreateUser IsUserExisting return true, ignore to create", zap.Any("username", params.UserName), zap.Any("clusterId", params.Cluster.Id))
			return nil
		}
	}

	proxy, err := GetClusterProxy(ctx, params.Cluster)
	if err != nil {
		tilog.Warn(ctx, "InfraProxy CreateUser GetClusterProxy error", zap.Any("clusterId", params.Cluster.Id))
		return err
	}
	sqls, err := buildCreateAndGrantSqls(ctx, params.CreateUserInfo)
	if err != nil {
		tilog.Warn(ctx, "InfraProxy CreateUser buildCreateAndGrantSqls error", zap.Any("createUserInfo", params.CreateUserInfo), zap.Any("err", err))
		return err
	}
	err = proxy.ExecuteMultiSQLV2(ctx, params.Operator, sqls)
	tilog.Info(ctx, "InfraProxy CreateUser ExecuteMultiSQLV2 result", zap.Any("clusterId", params.Cluster.Id), zap.Any("operator", params.Operator.Username), zap.Any("sqls", sqls), zap.Any("err", err))

	return err
}

func buildCreateAndGrantSqls(ctx context.Context, user *CreateUserInfo) ([]string, error) {
	var sqls []string

	createUserSQL, err := buildCreateUserSql(ctx, user)
	if err != nil {
		return nil, err
	}
	sqls = append(sqls, createUserSQL)
	desiredRoles := getCreateDesiredRoles(ctx, user)
	if len(desiredRoles) > 0 {
		sqls = append(sqls, buildGrantUserRolesSql(user.UserName, desiredRoles)...)
	}
	sqls = append(sqls, buildEnableAllRoleSql(user.UserName))

	if user.Grants != "" {
		grantSqls := parseGrantAndRevokeSql(user.Grants, "")
		if len(grantSqls) > 0 {
			sqls = append(sqls, grantSqls...)
		}
	}

	return sqls, nil
}

func buildCreateUserSql(ctx context.Context, user *CreateUserInfo) (string, error) {
	var createUserSql string
	switch user.AuthMethod {
	case clusterproxy.AuthMethodToken:
		createUserSql = buildCreateTokenBasedUserSql(user.UserName, buildTokenUserAttribute(user.UserAttribute), config.Iss)
	case clusterproxy.AuthMethodPassword:
		plaintextPassword, err := decryptCredential(ctx, user.EncryptCredential)
		if err != nil {
			return "", err
		}
		createUserSql = buildCreatePasswordBasedUserSql(user.UserName, plaintextPassword)
	default:
		asserts.BeTrue(false, "invalid authMethod: %s", user.AuthMethod)
	}
	return createUserSql, nil
}

func getCreateDesiredRoles(ctx context.Context, user *CreateUserInfo) []string {
	var desiredRoles []string

	if user.BuiltinRole != nil && *user.BuiltinRole != "" {
		desiredRoles = append(desiredRoles, *user.BuiltinRole)
	}
	if len(user.CustomRoles) > 0 {
		desiredRoles = append(desiredRoles, user.CustomRoles...)
	}

	return desiredRoles
}

func buildTokenUserAttribute(attribute map[string]string) string {
	out, err := json.Marshal(attribute)
	asserts.BeSuccess(err)
	return string(out)
}

//func buildDelegateUserAttribute(userEmail string) string {
//	attribute := map[string]string{
//		"email": userEmail,
//	}
//	out, err := json.Marshal(attribute)
//	asserts.BeSuccess(err)
//	return string(out)
//}

func checkCreateUserParams(params *CreateUserParams) error {
	if params == nil {
		return errs.NewBadRequest("params is nil", nil)
	}
	if params.Cluster == nil {
		return errs.NewBadRequest("cluster is nil", nil)
	}
	if params.Operator == nil {
		return errs.NewBadRequest("operator is nil", nil)
	}
	if params.CreateUserInfo == nil {
		return errs.NewBadRequest("user is nil", nil)
	}
	user := params.CreateUserInfo

	if params.UserName == "" {
		return errs.NewBadRequest("userName is blank", nil)
	}
	/*if err := checkDBUserType(user.UserType); err != nil {
		return err
	}*/

	if user.AuthMethod != clusterproxy.AuthMethodToken && user.AuthMethod != clusterproxy.AuthMethodPassword {
		return errs.NewBadRequest("invalid authMethod", nil)
	}
	if user.AuthMethod == clusterproxy.AuthMethodPassword && user.EncryptCredential == "" {
		return errs.NewBadRequest("credential is required if auth_method is password", nil)
	}
	if user.AuthMethod == clusterproxy.AuthMethodToken && len(user.UserAttribute) == 0 {
		return errs.NewBadRequest("token based user must set attribute", nil)
	}

	if err := checkBuiltinRole(user.BuiltinRole); err != nil {
		return err
	}

	return checkCustomRoles(user.CustomRoles)
}
