package sqluser

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"

	"braces.dev/errtrace"
	"github.com/pingcap/tidb/parser"
	"github.com/pingcap/tidb/parser/ast"
	_ "github.com/pingcap/tidb/parser/test_driver"
	"github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/otel"
	"go.uber.org/zap"

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

const (
	// SqlErNonExistingGrant represents there is no such grant defined for user '%s' on host '%s'
	SqlErNonExistingGrant = "1141"
)

var Tracer = otel.Tracer("sqluser")

func BuildClusterOperator(operator *globalserverlessv1.SqlUserOperator) *clusterproxy.ClusterOperator {
	authMethodMap := map[globalserverlessv1.SqlUser_AuthMethod]clusterproxy.AuthMethod{
		globalserverlessv1.SqlUser_AUTH_METHOD_TOKEN:    clusterproxy.AuthMethodToken,
		globalserverlessv1.SqlUser_AUTH_METHOD_PASSWORD: clusterproxy.AuthMethodPassword,
	}

	return &clusterproxy.ClusterOperator{
		Username:   operator.Username,
		AuthMethod: authMethodMap[operator.AuthMethod],
		Credential: operator.Credential,
	}
}

func StringifyOperator(o *globalserverlessv1.SqlUserOperator) string {
	if o == nil {
		return "<nil>"
	}

	return fmt.Sprintf("name=%s,authMethod=%v,len(credential)=%d", o.Username, o.AuthMethod, len(o.Credential))
}

func checkDBUserOperator(operator *globalserverlessv1.SqlUserOperator) bizerr.BizError {
	if operator == nil {
		return bizerr.New(bizerr.ErrCodeParams, "operator is required")
	}

	// NOTE: we have to decrypt the credential if the auth_method is PASSWORD
	if operator.AuthMethod != globalserverlessv1.SqlUser_AUTH_METHOD_TOKEN {
		return bizerr.New(bizerr.ErrCodeParams, "invalid auth_method: %s", operator.AuthMethod)
	}

	return nil
}

const (
	BuiltinRoleNameAdmin    = "role_admin"
	BuiltinRoleNameCDCAdmin = "role_cdc_admin"
)

var (
	BuiltinRoleNameMap = map[globalserverlessv1.SqlUser_BuiltinRole]string{
		globalserverlessv1.SqlUser_BUILTIN_ROLE_USER_ADMIN: BuiltinRoleNameAdmin,
		globalserverlessv1.SqlUser_BUILTIN_ROLE_CDC_ADMIN:  BuiltinRoleNameCDCAdmin,
	}
	ReverseBuiltinRoleNameMap = map[string]globalserverlessv1.SqlUser_BuiltinRole{}
	BuiltinRoleNameSet        = map[string]bool{}
)

func init() {
	for roleType, name := range BuiltinRoleNameMap {
		BuiltinRoleNameSet[name] = true
		ReverseBuiltinRoleNameMap[name] = roleType
	}
}

type RoleGetter func(ctx context.Context, proxy adapter.ClusterProxy, username string) ([]string, bizerr.BizError)

func GetRoles(ctx context.Context, proxy adapter.ClusterProxy, username string) ([]string, bizerr.BizError) {
	result, err := proxy.QuerySQL(ctx, BuildShowRolesSql(username))
	if err != nil {
		return nil, err
	}

	tilog.Debug(ctx, "show roles result", zap.String("result", string(result)))
	grants := map[string][]string{}
	if err := json.Unmarshal(result, &grants); err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "unmarshal show grants result: %s", err)
	}
	key := fmt.Sprintf("Grants for %s@%%", username)

	var roles []string
	for _, line := range grants[key] {
		grant, err := base64.StdEncoding.DecodeString(line)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "base64 decode grant failed: %s", err)
		}

		astNodes, err := parseSql(string(grant))
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeDB, "parse result, error: %s", err)
		}

		for _, astNode := range astNodes {
			if stmt, ok := astNode.(*ast.GrantRoleStmt); ok {
				for _, each := range stmt.Roles {
					roles = append(roles, each.Username)
				}
			}
		}
	}
	return roles, nil
}

func parseSql(sql string) ([]ast.StmtNode, error) {
	p := parser.New()

	stmtNodes, _, err := p.Parse(sql, "", "")
	if err != nil {
		return nil, errtrace.Wrap(err)
	}

	return stmtNodes, nil
}

func CheckGrants(target, grants string) bizerr.BizError {
	astNodes, err := parseSql(grants)
	if err != nil {
		return bizerr.New(bizerr.ErrCodeParams, "invalid grants. grants=%s,err:%s", grants, err)
	}

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

func CheckRevocation(target, revocation string) bizerr.BizError {
	astNodes, err := parseSql(revocation)
	if err != nil {
		return bizerr.New(bizerr.ErrCodeParams, "invalid revocation. revocation=%s,err:%s", revocation, err)
	}

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

	return nil
}

func GetBuiltinRoles(roles []string) []string {
	var builtinRoles []string
	for _, role := range roles {
		if _, ok := BuiltinRoleNameSet[role]; ok {
			builtinRoles = append(builtinRoles, role)
		}
	}

	return builtinRoles
}

func GetCustomRoles(roles []string) []string {
	var customRoles []string
	for _, role := range roles {
		if _, ok := BuiltinRoleNameSet[role]; !ok {
			customRoles = append(customRoles, role)
		}
	}

	return customRoles
}

func ParseGrantAndRevokeSql(grants, revocation string) []string {
	var out []string
	for each := range strings.SplitSeq(grants, ";") {
		each = strings.TrimSpace(each)
		if each != "" {
			out = append(out, each)
		}
	}
	for each := range strings.SplitSeq(revocation, ";") {
		each = strings.TrimSpace(each)
		if each != "" {
			out = append(out, each)
		}
	}
	return out
}

func ParseRoles(roles []string) (builtinRole globalserverlessv1.SqlUser_BuiltinRole, customRoles []string) {
	for _, each := range roles {
		if role, ok := ReverseBuiltinRoleNameMap[each]; ok {
			builtinRole = role
			continue
		}

		customRoles = append(customRoles, each)
	}
	return builtinRole, customRoles
}

func getBatchRoles(
	ctx context.Context,
	proxy adapter.ClusterProxy,
	usernames []string,
) (map[string][]string, bizerr.BizError) {
	logger := tilog.WithContextS(ctx)
	userRoleMap := map[string][]string{}
	if len(usernames) == 0 {
		return userRoleMap, nil
	}

	sql := buildBatchShowRolesSql(usernames)
	logger.Debugf("get batch roles sql: %s", sql)

	result, err := proxy.QuerySQL(ctx, sql)
	if err != nil {
		return nil, err
	}
	logger.Debugf("get batch roles result: %s", result)
	sqlResultMap := map[string][]string{}
	if err := json.Unmarshal(result, &sqlResultMap); err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "unmarshal get batch roles result: %s", err)
	}

	userNamesArr := sqlResultMap["TO_USER"]
	rolesArr := sqlResultMap["FROM_USER"]

	for i, user := range userNamesArr {
		role := rolesArr[i]
		roleByte, err := base64.StdEncoding.DecodeString(role)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "base64 decode %s  failed: %s", role, err)
		}
		role = string(roleByte)
		userByte, err := base64.StdEncoding.DecodeString(user)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "base64 decode %s  failed: %s", user, err)
		}
		user = string(userByte)

		/*			if values, ok := userRoleMap[user]; ok {
						*values = append(*values, role)
					} else {
						arr := []string{role}
						userRoleMap[user] = &arr
					}
		*/
		userRoleMap[user] = append(userRoleMap[user], role)
	}

	logger.Debugf("get batch roles result: %s", userRoleMap)

	return userRoleMap, nil
}

type CreateUserInfo struct {
	UserType globalserverlessv1.SqlUser_Type
	UserName string
	// UserEmail is the full email address of this user
	// 1. optional
	// 2. is required if auth_method is token and empty string is acceptable
	UserEmail   string
	AuthMethod  globalserverlessv1.SqlUser_AuthMethod
	Credential  string // optional, required if auth_method is password
	BuiltinRole globalserverlessv1.SqlUser_BuiltinRole
	CustomRoles []string
}

type BatchCreateUserParams struct {
	Operator          *globalserverlessv1.SqlUserOperator
	ClusterAttributes entity.ClusterAttributes
	Cluster           *regionalserverlessv1.ServerlessRegionalServiceClusterInfo

	Users []*CreateUserInfo
}

type CreateUserParams struct {
	*CreateUserInfo

	Operator          *globalserverlessv1.SqlUserOperator
	Cluster           *regionalserverlessv1.ServerlessRegionalServiceClusterInfo
	ClusterAttributes entity.ClusterAttributes
	Settings          *globalserverlessv1.SqlUserServiceCreateSqlUserRequest_Settings
}

type UpdateUserPasswordParams struct {
	Operator   *globalserverlessv1.SqlUserOperator
	Credential string
	Cluster    *regionalserverlessv1.ServerlessRegionalServiceClusterInfo
}

type UpdateUserParams struct {
	Operator *globalserverlessv1.SqlUserOperator
	Cluster  *regionalserverlessv1.ServerlessRegionalServiceClusterInfo
	Username string

	BuiltinRole      *globalserverlessv1.SqlUser_BuiltinRole
	CustomRoles      []string
	Password         *string
	ClearCustomRoles bool
}

type DeleteUserParams struct {
	Operator *globalserverlessv1.SqlUserOperator
	Cluster  *regionalserverlessv1.ServerlessRegionalServiceClusterInfo
	Username string
}

type CreateRoleParams struct {
	Operator *globalserverlessv1.SqlUserOperator
	Cluster  *regionalserverlessv1.ServerlessRegionalServiceClusterInfo

	RoleName   string
	Grants     string
	Revocation string
}

type UpdateRoleParams struct {
	Operator *globalserverlessv1.SqlUserOperator
	Cluster  *regionalserverlessv1.ServerlessRegionalServiceClusterInfo

	RoleName   string
	Grants     string
	Revocation string
}
