package sqluser

import (
	"context"
	"strconv"

	"braces.dev/errtrace"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (s *sqlUserSvcImpl) CreateSqlUserRole(
	ctx context.Context,
	req *globalserverlessv1.SqlUserServiceCreateSqlRoleRequest,
) error {
	ctx, span := Tracer.Start(ctx, "CreateRole")
	defer span.End()

	if err := commonCheck(req.Operator); err != nil {
		return errtrace.Wrap(err)
	}

	tilog.Info(
		ctx,
		"try to create role",
		zap.String("name", req.RoleName),
		zap.String("operator", StringifyOperator(req.Operator)),
	)
	en, regionalCluster, err := s.getCluster(ctx, req.ClusterId)
	if err != nil {
		return errtrace.Wrap(err)
	}

	param := &CreateRoleParams{
		Operator:   req.Operator,
		Cluster:    regionalCluster,
		RoleName:   req.RoleName,
		Grants:     req.Grants,
		Revocation: req.Revocation,
	}

	if err := createRoleCheck(ctx, param, en); err != nil {
		return errtrace.Wrap(err)
	}

	cid, err := strconv.ParseUint(req.ClusterId, 10, 64)
	if err != nil {
		return bizerr.New(bizerr.ErrCodeParams, "invalid cluster id: %s", req.ClusterId)
	}
	proxy, err := s.clusterProxyProvider.GetClusterProxy(
		ctx,
		cid,
		regionalCluster.ProxyEndpoint,
		regionalCluster.Version,
		BuildClusterOperator(req.Operator),
	)
	if err != nil {
		return errtrace.Wrap(err)
	}

	sqls := []string{
		BuildCreateRoleSql(param.RoleName),
	}
	sqls = append(sqls, ParseGrantAndRevokeSql(param.Grants, param.Revocation)...)
	if err := proxy.ExecuteMultiSQL(ctx, sqls); err != nil {
		return errtrace.Wrap(err)
	}
	return nil
}

func createRoleCheck(_ context.Context, params *CreateRoleParams, en *entity.Cluster) bizerr.BizError {
	if params.Operator == nil {
		return bizerr.New(bizerr.ErrCodeParams, "operator is required")
	}

	if params.RoleName == "" {
		return bizerr.New(bizerr.ErrCodeParams, "invalid role_name: %s", params.RoleName)
	}
	if _, ok := BuiltinRoleNameSet[params.RoleName]; ok {
		return bizerr.New(bizerr.ErrCodeParams, "can't use builtin role name: %s", params.RoleName)
	}

	if err := CheckGrants(params.RoleName, params.Grants); err != nil {
		return err
	}
	if err := CheckRevocation(params.RoleName, params.Revocation); err != nil {
		return err
	}

	if err := checkManageUserRole(en, params.Cluster); err != nil {
		return err
	}
	return nil
}

func (s *sqlUserSvcImpl) UpdateSqlUserRole(
	ctx context.Context,
	req *globalserverlessv1.SqlUserServiceUpdateSqlRoleRequest,
) error {
	ctx, span := Tracer.Start(ctx, "UpdateRoleSvc")
	defer span.End()
	if err := commonCheck(req.Operator); err != nil {
		return errtrace.Wrap(err)
	}

	en, regionalCluster, err := s.getCluster(ctx, req.ClusterId)
	if err != nil {
		return errtrace.Wrap(err)
	}

	param := &UpdateRoleParams{
		Operator:   req.Operator,
		Cluster:    regionalCluster,
		RoleName:   req.RoleName,
		Grants:     req.GetGrants(),
		Revocation: req.GetRevocation(),
	}

	tilog.Info(
		ctx,
		"try to update role",
		zap.String("name", req.RoleName),
		zap.String("operator", StringifyOperator(req.Operator)),
	)

	if err := updateSqlRoleCheck(ctx, param, en); err != nil {
		return errtrace.Wrap(err)
	}

	cid, err := strconv.ParseUint(req.ClusterId, 10, 64)
	if err != nil {
		return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id: %s", req.ClusterId)
	}
	proxy, err := s.clusterProxyProvider.GetClusterProxy(
		ctx,
		cid,
		regionalCluster.ProxyEndpoint,
		regionalCluster.Version,
		BuildClusterOperator(param.Operator),
	)
	if err != nil {
		return errtrace.Wrap(err)
	}

	sqls := ParseGrantAndRevokeSql(param.Grants, param.Revocation)
	if err := proxy.ExecuteMultiSQL(ctx, sqls); err != nil {
		return errtrace.Wrap(err)
	}
	return nil
}

func updateSqlRoleCheck(_ context.Context, params *UpdateRoleParams, cluster *entity.Cluster) bizerr.BizError {
	if params.Operator == nil {
		return bizerr.New(bizerr.ErrCodeParams, "operator is required")
	}
	if params.RoleName == "" {
		return bizerr.New(bizerr.ErrCodeParams, "invalid role_name: %s", params.RoleName)
	}
	if _, ok := BuiltinRoleNameSet[params.RoleName]; ok {
		return bizerr.New(bizerr.ErrCodeParams, "can't change builtin role: %s", params.RoleName)
	}

	if err := CheckGrants(params.RoleName, params.Grants); err != nil {
		return err
	}
	if err := CheckRevocation(params.RoleName, params.Revocation); err != nil {
		return err
	}

	if err := checkManageUserRole(cluster, params.Cluster); err != nil {
		return err
	}
	return nil
}
