package infra

import (
	"context"

	"github.com/ahmetb/go-linq/v3"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

type UpdateUserParams struct {
	Cluster  *core.Cluster
	Operator *clusterproxy.ClusterOperator
	Username string

	BuiltinRole      *string // nil means not to change builtin role, blank means to delete builtin role
	CustomRoles      []string
	Password         *string
	ClearCustomRoles bool
}

func UpdateUser(ctx context.Context, params *UpdateUserParams) error {
	if err := checkUpdateUserParams(params); err != nil {
		return err
	}

	return updateRolesAndPassword(ctx, params)
}

func checkUpdateUserParams(params *UpdateUserParams) error {
	if params == nil {
		return errs.NewBadRequest("params is nil", nil)
	}
	if err := checkClusterOperatorUsername(params.Cluster, params.Operator, params.Username); err != nil {
		return err
	}

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

	return checkCustomRoles(params.CustomRoles)
}

func updateRolesAndPassword(ctx context.Context, params *UpdateUserParams) error {
	tilog.Info(ctx, "InfraProxy updateRolesAndPassword param", zap.Any("clusterId", params.Cluster.Id), zap.Any("operator", params.Operator.Username),
		zap.Any("username", params.Username), zap.Any("builtinRole", params.BuiltinRole), zap.Any("customRoles", params.CustomRoles), zap.Any("clearCustomRoles", params.ClearCustomRoles))

	currentRoles, err := getUserGrantRoles(ctx, params.Cluster, params.Operator, params.Username)
	if err != nil {
		return err
	}
	desiredRoles := getUpdateDesiredRoles(params, currentRoles)

	tilog.Info(ctx, "InfraProxy updateRolesAndPassword get current and desired roles", zap.Any("currentRoles", currentRoles), zap.Any("desiredRoles", desiredRoles))

	var toDeleted, toAdded []string
	linq.From(currentRoles).Except(linq.From(desiredRoles)).ToSlice(&toDeleted)
	linq.From(desiredRoles).Except(linq.From(currentRoles)).ToSlice(&toAdded)

	tilog.Info(ctx, "InfraProxy updateRolesAndPassword, to operate roles", zap.Any("toAddedRoles", toAdded), zap.Any("toDeletedRoles", toDeleted))

	var toExecSqls []string

	updateFlag := false
	updateRoleFlag := false
	if len(toAdded) != 0 {
		toExecSqls = append(toExecSqls, buildGrantUserRolesSql(params.Username, toAdded)...)
		updateFlag = true
		updateRoleFlag = true
	}
	if len(toDeleted) != 0 {
		toExecSqls = append(toExecSqls, buildRevokeUserRolesSql(params.Username, toDeleted)...)
		updateFlag = true
		updateRoleFlag = true
	}
	if updateRoleFlag {
		toExecSqls = append(toExecSqls, buildEnableAllRoleSql(params.Username))
	}

	// change password
	if params.Password != nil {
		toExecSqls = append(toExecSqls, buildChangePasswordUserSql(params.Username, *params.Password))
		updateFlag = true
	}

	tilog.Info(ctx, "InfraProxy updateRolesAndPassword, buildSqls", zap.Any("sqls", toExecSqls))

	if !updateFlag {
		tilog.Info(ctx, "InfraProxy updateRolesAndPassword, password and roles are unchanged and do not need to be updated", 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 updateRolesAndPassword GetClusterProxy error", zap.Any("clusterId", params.Cluster.Id))
		return err
	}
	err = proxy.ExecuteMultiSQLV2(ctx, params.Operator, toExecSqls)
	tilog.Info(ctx, "InfraProxy updateRolesAndPassword ExecuteMultiSQLV2 result", zap.Any("clusterId", params.Cluster.Id), zap.Any("operator", params.Operator.Username), zap.Any("sqls", toExecSqls), zap.Any("err", err))

	return err
}

func getUpdateDesiredRoles(params *UpdateUserParams, currentRoles []string) []string {
	currentBuiltinRoles := getBuiltinRoles(currentRoles)
	currentCustomRoles := getCustomRoles(currentRoles)

	var desiredRoles []string

	if params.BuiltinRole != nil {
		if *params.BuiltinRole != "" {
			desiredRoles = append(desiredRoles, *params.BuiltinRole)
		}
	} else {
		desiredRoles = append(desiredRoles, currentBuiltinRoles...)
	}

	// ClearCustomRoles highest priority
	if !params.ClearCustomRoles {
		if len(params.CustomRoles) > 0 {
			desiredRoles = append(desiredRoles, params.CustomRoles...)
		} else {
			desiredRoles = append(desiredRoles, currentCustomRoles...)
		}
	}

	return desiredRoles
}
