package infra

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

	"github.com/pingcap/tidb/parser/ast"
	"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"
)

const (
	SqlErrAccessDeniedForUser = 1045
	SqlErrNonExistingGrant    = 1141
	SqlErrClusterRestoring    = 1105
)

const (
	ClusterRestoringMsg = "cluster is restoring"
)

func IsUserExistGrantRole(ctx context.Context, cluster *core.Cluster, operator *clusterproxy.ClusterOperator, username string) (bool, error) {
	if err := checkClusterOperatorUsername(cluster, operator, username); err != nil {
		return false, err
	}

	tilog.Info(ctx, "InfraProxy IsUserExistGrantRole param", zap.Any("clusterId", cluster.Id), zap.Any("operator", operator.Username), zap.Any("username", username))

	roles, err := getUserGrantRoles(ctx, cluster, operator, username)
	if err != nil {
		tilog.Warn(ctx, "InfraProxy IsUserExistGrantRole getUserGrantRoles error", zap.Any("err", err))
		return false, err
	}
	if len(roles) == 0 {
		tilog.Info(ctx, "InfraProxy IsUserExistGrantRole getUserGrantRoles empty", zap.Any("clusterId", cluster.Id), zap.Any("username", username))
		return false, nil
	}
	tilog.Info(ctx, "InfraProxy IsUserExistGrantRole userGrantRoLe is existing", zap.Any("clusterId", cluster.Id), zap.Any("username", username), zap.Any("roles", roles))
	return true, nil
}

func IsUserExisting(ctx context.Context, cluster *core.Cluster, operator *clusterproxy.ClusterOperator, username string) (bool, error) {
	if err := checkClusterOperatorUsername(cluster, operator, username); err != nil {
		return false, err
	}

	tilog.Info(ctx, "InfraProxy IsUserExisting param", zap.Any("clusterId", cluster.Id), zap.Any("operator", operator.Username), zap.Any("username", username))

	filter := &core.ListDBUsersFilter{
		Users: []string{username},
	}
	resultMap, err := listMySqlUsers(ctx, filter, cluster, operator)
	if err != nil {
		return false, err
	}
	if resultMap != nil && resultMap["USER"] != nil && len(resultMap["USER"].Values) > 0 {
		tilog.Info(ctx, "InfraProxy IsUserExisting user is existing", zap.Any("clusterId", cluster.Id), zap.Any("username", username))
		return true, nil
	}

	tilog.Info(ctx, "InfraProxy IsUserExisting user is not existing", zap.Any("clusterId", cluster.Id), zap.Any("username", username))
	return false, nil
}

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

	Filter *core.ListDBUsersFilter
}

func ListUsers(ctx context.Context, params *ListUsersParams) (map[string]*core.SqlValues, []DBUser, error) {
	if params == nil {
		return nil, nil, errs.NewBadRequest("params is nil", nil)
	}
	if params.Cluster == nil || params.Operator == nil {
		return nil, nil, errs.NewBadRequest("cluster or operator is nil", nil)
	}

	tilog.Info(ctx, "InfraProxy ListUsers param", zap.Any("clusterId", params.Cluster.Id), zap.Any("operator", params.Operator.Username), zap.Any("filter", params.Filter))

	resultMap, err := listMySqlUsers(ctx, params.Filter, params.Cluster, params.Operator)
	if err != nil {
		return nil, nil, err
	}

	var dbUsers []DBUser
	if resultMap != nil && resultMap["USER"] != nil && resultMap["PLUGIN"] != nil {
		users := resultMap["USER"].Values
		plugins := resultMap["PLUGIN"].Values

		userRoleMap, err := batchGetUserGrantRoles(ctx, params.Cluster, params.Operator, users)
		if err != nil {
			return nil, nil, err
		}

		for i, u := range users {
			//roles, err := getRoles(ctx, proxy, u)
			out := DBUser{
				Username: u,
				Plugin:   plugins[i],
			}

			if userRoleMap != nil {
				roles := userRoleMap[u]
				if roles != nil {
					builtinRole, customRoles := ParseRoles(roles)
					out.BuiltinRole = builtinRole
					out.CustomRoles = customRoles
				}
			}

			dbUsers = append(dbUsers, out)
		}
	}

	tilog.Info(ctx, "InfraProxy ListUsers result", zap.Any("resultMap", resultMap), zap.Any("dbUsers", dbUsers), zap.Any("err", err))
	return resultMap, dbUsers, err
}

func listMySqlUsers(ctx context.Context, filter *core.ListDBUsersFilter, cluster *core.Cluster, operator *clusterproxy.ClusterOperator) (map[string]*core.SqlValues, error) {
	proxy, err := GetClusterProxy(ctx, cluster)
	if err != nil {
		return nil, err
	}
	sql := buildQueryUsersSql(ctx, filter)
	rsp, err := proxy.QuerySQLWithSqlError(ctx, operator, sql)
	tilog.Info(ctx, "InfraProxy listMySqlUsers QuerySQLWithSqlError result", zap.Any("clusterID", cluster.Id), zap.Any("operator", operator.Username), zap.Any("sql", sql), zap.Any("rsp", rsp), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	if rsp.ErrNumber != 0 {
		if rsp.ErrNumber == SqlErrNonExistingGrant {
			return nil, nil
		}
		if rsp.ErrNumber == SqlErrAccessDeniedForUser {
			return nil, errs.NewForbidden(fmt.Sprintf("access denied, errNumber: %d, errMessage: %s", rsp.ErrNumber, rsp.ErrMessage), nil)
		}
		if rsp.ErrNumber == SqlErrClusterRestoring {
			return nil, errs.NewInternalDB(ClusterRestoringMsg, nil)
		}
		return nil, errs.NewInternalUnknown(fmt.Sprintf("query sql err, errNumber: %d, errMessage: %s", rsp.ErrNumber, rsp.ErrMessage), nil)
	}

	sqlResultMap := map[string][]string{}
	if err := json.Unmarshal(rsp.Result, &sqlResultMap); err != nil {
		return nil, errs.NewInternalUnknown("unmarshal sql result error", err)
	}

	responseMap := make(map[string]*core.SqlValues)
	for k := range sqlResultMap {
		values, err := getKeyResult(ctx, sqlResultMap, k)
		if err != nil {
			return nil, err
		}
		listDBUsersValues := &core.SqlValues{
			Values: values,
		}
		responseMap[k] = listDBUsersValues
	}

	tilog.Info(ctx, "InfraProxy listMySqlUsers get responseMap", zap.Any("responseMap", responseMap))
	return responseMap, nil
}

func getKeyResult(ctx context.Context, grants map[string][]string, key string) ([]string, error) {
	var results []string
	for _, line := range grants[key] {
		value, err := base64.StdEncoding.DecodeString(line)
		if err != nil {
			return nil, errs.NewInternalUnknown(fmt.Sprintf("base64 decode key: %s relate line: %s  failed", key, line), err)
		}
		results = append(results, string(value))

	}
	return results, nil
}

func batchGetUserGrantRoles(ctx context.Context, cluster *core.Cluster, operator *clusterproxy.ClusterOperator, usernames []string) (map[string][]string, error) {
	userRoleMap := map[string][]string{}
	if len(usernames) == 0 {
		return userRoleMap, nil
	}

	proxy, err := GetClusterProxy(ctx, cluster)
	if err != nil {
		return nil, err
	}
	sql := buildBatchShowRolesSql(usernames)
	rsp, err := proxy.QuerySQLWithSqlError(ctx, operator, sql)
	tilog.Info(ctx, "InfraProxy batchGetUserGrantRoles QuerySQLV2 result", zap.Any("clusterId", cluster.Id), zap.Any("operator", operator.Username), zap.Any("sql", sql), zap.Any("rsp", rsp), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	if rsp.ErrNumber != 0 {
		if rsp.ErrNumber == SqlErrNonExistingGrant {
			return nil, nil
		}
		if rsp.ErrNumber == SqlErrAccessDeniedForUser {
			return nil, errs.NewForbidden(fmt.Sprintf("access denied, errNumber: %d, errMessage: %s", rsp.ErrNumber, rsp.ErrMessage), nil)
		}
		if rsp.ErrNumber == SqlErrClusterRestoring {
			return nil, errs.NewInternalDB(ClusterRestoringMsg, nil)
		}
		return nil, errs.NewInternalUnknown(fmt.Sprintf("query sql err, errNumber: %d, errMessage: %s", rsp.ErrNumber, rsp.ErrMessage), nil)
	}

	sqlResultMap := map[string][]string{}
	if err = json.Unmarshal(rsp.Result, &sqlResultMap); err != nil {
		return nil, errs.NewInternalUnknown("unmarshal show grants result error", 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, errs.NewInternalUnknown(fmt.Sprintf("base64 decode user: %s relate role: %s error", user, role), err)
		}
		role = string(roleByte)
		userByte, err := base64.StdEncoding.DecodeString(user)
		if err != nil {
			return nil, errs.NewInternalUnknown(fmt.Sprintf("base64 decode user: %s error", user), err)
		}
		user = string(userByte)

		userRoleMap[user] = append(userRoleMap[user], role)
	}

	tilog.Info(ctx, "InfraProxy batchGetUserGrantRoles get userRoleMap", zap.Any("userRoleMap", userRoleMap))
	return userRoleMap, nil
}

func getUserGrantRoles(ctx context.Context, cluster *core.Cluster, operator *clusterproxy.ClusterOperator, username string) ([]string, error) {
	proxy, err := GetClusterProxy(ctx, cluster)
	if err != nil {
		return nil, err
	}
	sql := buildShowRolesSql(username)
	rsp, err := proxy.QuerySQLWithSqlError(ctx, operator, sql)
	tilog.Info(ctx, "InfraProxy getUserGrantRoles QuerySQLWithSqlError result", zap.Any("clusterID", cluster.Id), zap.Any("operator", operator.Username), zap.Any("sql", sql), zap.Any("rsp", rsp), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	if rsp.ErrNumber != 0 {
		if rsp.ErrNumber == SqlErrNonExistingGrant {
			return nil, nil
		}
		if rsp.ErrNumber == SqlErrAccessDeniedForUser {
			return nil, errs.NewForbidden(fmt.Sprintf("access denied, errNumber: %d, errMessage: %s", rsp.ErrNumber, rsp.ErrMessage), nil)
		}
		if rsp.ErrNumber == SqlErrClusterRestoring {
			return nil, errs.NewInternalDB(ClusterRestoringMsg, nil)
		}
		return nil, errs.NewInternalUnknown(fmt.Sprintf("query sql err, errNumber: %d, errMessage: %s", rsp.ErrNumber, rsp.ErrMessage), nil)
	}

	grants := map[string][]string{}
	if err = json.Unmarshal(rsp.Result, &grants); err != nil {
		return nil, errs.NewInternalUnknown("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, errs.NewInternalUnknown("base64 decode grant failed: %s", err)
		}

		grantStr := string(grant)
		astNodes, err := parseSql(grantStr)
		if err != nil {
			return nil, errs.NewInternalUnknown(fmt.Sprintf("parse sql error: %s", grantStr), err)
		}

		for _, astNode := range astNodes {
			if stmt, ok := astNode.(*ast.GrantRoleStmt); ok {
				for _, each := range stmt.Roles {
					roles = append(roles, each.Username)
				}
			}
		}
	}
	tilog.Info(ctx, "InfraProxy getUserGrantRoles result", zap.Any("clusterId", cluster.Id), zap.Any("username", username), zap.Any("roles", roles))

	return roles, nil
}
