package infra

import (
	"context"
	"fmt"

	"github.com/pingcap/tidb/parser/ast"
	"github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"go.uber.org/zap"
)

type OperateRoleParams struct {
	Cluster  *core.Cluster
	Operator *clusterproxy.ClusterOperator

	RoleName   string
	Grants     string
	Revocation string
}

func CreateRole(ctx context.Context, params *OperateRoleParams) error {
	if err := checkOperateRoleParams(params); err != nil {
		tilog.Warn(ctx, "InfraProxy CreateRole checkOperateRoleParams error", zap.Any("err", err))
		return err
	}

	tilog.Info(ctx, "InfraProxy CreateRole param", zap.Any("clusterId", params.Cluster.Id), zap.Any("operator", params.Operator.Username),
		zap.Any("roleName", params.RoleName), zap.Any("grants", params.Grants), zap.Any("revocation", params.Revocation))

	proxy, err := GetClusterProxy(ctx, params.Cluster)
	if err != nil {
		tilog.Warn(ctx, "InfraProxy CreateRole GetClusterProxy error", zap.Any("clusterId", params.Cluster.Id))
		return err
	}

	sqls := []string{
		buildCreateRoleSql(params.RoleName),
	}
	sqls = append(sqls, parseGrantAndRevokeSql(params.Grants, params.Revocation)...)

	err = proxy.ExecuteMultiSQLV2(ctx, params.Operator, sqls)
	tilog.Info(ctx, "InfraProxy CreateRole 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 checkOperateRoleParams(params *OperateRoleParams) 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.RoleName == "" {
		return errs.NewBadRequest("roleName is blank", nil)
	}
	if err := checkCustomRoles([]string{params.RoleName}); err != nil {
		return err
	}

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

func checkGrants(target string, grants string) error {
	astNodes, err := parseSql(grants)
	if err != nil {
		return errs.NewBadRequest(fmt.Sprintf("invalid grants. grants=%s", grants), err)
	}

	for _, astNode := range astNodes {
		stmt, ok := astNode.(*ast.GrantStmt)
		if !ok {
			return errs.NewBadRequest(fmt.Sprintf("invalid grants. grants=%s", astNode.Text()), nil)
		}
		if len(stmt.Users) != 1 {
			return errs.NewBadRequest(fmt.Sprintf("too many/few grantee, len=%d", len(stmt.Users)), nil)
		} else if stmt.Users[0].User.Username != target {
			return errs.NewBadRequest(fmt.Sprintf("invalid grantee. users=%s", stmt.Users[0].User.Username), nil)
		}
	}
	return nil
}

func checkRevocation(target string, revocation string) error {
	astNodes, err := parseSql(revocation)
	if err != nil {
		return errs.NewBadRequest(fmt.Sprintf("invalid revocation. revocation=%s", revocation), err)
	}

	for _, astNode := range astNodes {
		stmt, ok := astNode.(*ast.RevokeStmt)
		if !ok {
			return errs.NewBadRequest(fmt.Sprintf("invalid revoke. revoke=%s", astNode.Text()), nil)
		}
		if len(stmt.Users) != 1 {
			return errs.NewBadRequest(fmt.Sprintf("too many/few grantee, len=%d", len(stmt.Users)), nil)
		} else if stmt.Users[0].User.Username != target {
			return errs.NewBadRequest(fmt.Sprintf("invalid grantee. users=%s", stmt.Users[0].User.Username), nil)
		}
	}

	return nil
}
