package sqluser

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

	"braces.dev/errtrace"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.opentelemetry.io/otel/codes"
	"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"
)

func (s *sqlUserSvcImpl) List(
	ctx context.Context,
	req *globalserverlessv1.ListSqlUsersRequest,
) (_ map[string]*globalserverlessv1.SqlValues, _ []*globalserverlessv1.SqlUser, err error) {
	ctx, span := Tracer.Start(ctx, "List")
	defer func() {
		if err != nil {
			span.RecordError(err)
			span.SetStatus(codes.Error, err.Error())
		}
		span.End()
	}()

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

	if err := s.listCheck(ctx, req, cluster, regionalCluster); err != nil {
		return nil, nil, errtrace.Wrap(err)
	}

	cid, err := strconv.ParseUint(req.ClusterId, 10, 64)
	if err != nil {
		return nil, nil, bizerr.WrapErrorWithCode(err, 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 nil, nil, errtrace.Wrap(err)
	}

	resultMap, dbUsers, err := ListUser(ctx, req, proxy)
	if err != nil {
		return nil, nil, errtrace.Wrap(err)
	}

	return resultMap, dbUsers, nil
}

func (s *sqlUserSvcImpl) listCheck(
	_ context.Context,
	req *globalserverlessv1.ListSqlUsersRequest,
	en *entity.Cluster,
	cluster *regionalserverlessv1.ServerlessRegionalServiceClusterInfo,
) bizerr.BizError {
	if err := checkDBUserOperator(req.Operator); err != nil {
		return err
	}
	if err := checkManageUserRole(en, cluster); err != nil {
		return err
	}

	return nil
}

func ListUser(
	ctx context.Context,
	req *globalserverlessv1.ListSqlUsersRequest,
	proxy adapter.ClusterProxy,
) (map[string]*globalserverlessv1.SqlValues, []*globalserverlessv1.SqlUser, bizerr.BizError) {
	logger := tilog.WithContextS(ctx)

	resultMap, err := getMySqlUsers(ctx, req, proxy)
	if err != nil {
		logger.Infof("check user existence encouther an error: %s", err)
		if err.ErrCode() == bizerr.ErrCodeClusterProxy && strings.Contains(err.ErrMsg(), SqlErNonExistingGrant) {
			logger.Infof("dbuser is not existing")
			return nil, nil, err
		}
		return nil, nil, err
	}

	tilog.Info(ctx, "get mysql user", zap.Any("resultMap", resultMap))

	var dbUsers []*globalserverlessv1.SqlUser
	if resultMap != nil && resultMap["USER"] != nil && resultMap["PLUGIN"] != nil {
		users := resultMap["USER"].Values
		plugins := resultMap["PLUGIN"].Values
		userRoleMap, err := getBatchRoles(ctx, proxy, users)
		if err != nil {
			return nil, nil, err
		}

		for i, u := range users {
			out := &globalserverlessv1.SqlUser{
				Username: u,
				Plugin:   plugins[i],
			}
			roles := userRoleMap[u]
			if roles != nil {
				logger.Debugf("get user role : %s -> %s", u, roles)
				builtinRole, customRoles := ParseRoles(roles)
				out.BuiltinRole = builtinRole
				out.CustomRoles = customRoles
			}

			dbUsers = append(dbUsers, out)
		}
	}
	return resultMap, dbUsers, err
}

func getMySqlUsers(
	ctx context.Context,
	req *globalserverlessv1.ListSqlUsersRequest,
	proxy adapter.ClusterProxy,
) (map[string]*globalserverlessv1.SqlValues, bizerr.BizError) {
	sql := buildQueryUsersSql(ctx, req.Filter)
	tilog.Info(ctx, "query mysql.user", zap.String("sql", sql))
	result, err := proxy.QuerySQL(ctx, sql)
	if err != nil {
		return nil, err
	}

	tilog.Debug(ctx, "query mysql.user", zap.String("result", string(result)))
	sqlResultMap := map[string][]string{}
	if err := json.Unmarshal(result, &sqlResultMap); err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "query mysql.user result: %s", err)
	}
	responseMap := make(map[string]*globalserverlessv1.SqlValues)

	for k := range sqlResultMap {
		values, err := getKeyResult(ctx, sqlResultMap, k)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "base64 decode %s failed: %s", k, err)
		}
		listDBUsersValues := &globalserverlessv1.SqlValues{
			Values: values,
		}
		responseMap[k] = listDBUsersValues
	}
	return responseMap, nil
}

func getKeyResult(ctx context.Context, grants map[string][]string, key string) ([]string, bizerr.BizError) {
	var results []string
	for _, line := range grants[key] {
		value, err := base64.StdEncoding.DecodeString(line)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "base64 decode %s  failed: %s", key, err)
		}
		results = append(results, string(value))
	}
	tilog.Info(ctx, "getKeyResult", zap.String("key", key), zap.Strings("results", results))
	return results, nil
}
