package cluster

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

	"github.com/samber/lo"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	pkgclusterproxy "github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/structpb"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/clusterproxy"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

const (
	GetNewCollationEnabledSQL    = "select VARIABLE_VALUE from mysql.tidb where variable_name = 'new_collation_enabled'"
	NewCollationEnabledConfigKey = "new_collations_enabled_on_first_bootstrap"
)

func (s *serviceImpl) GetClusterCustomParams(
	ctx context.Context,
	req *globaldedicatedv1.GetClusterCustomParamsRequest,
) (*globaldedicatedv1.GetClusterCustomParamsResponse, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.GetClusterCustomParams")
	defer span.End()

	if req.ClusterId == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster_id is required")
	}

	cluster, err := s.clusterRepo.Get(ctx, req.ClusterId, false)
	if err != nil {
		return nil, err
	}

	if cluster.State != globaldedicatedv1.Cluster_AVAILABLE {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"cannot get custom params for cluster %d because its state is %s; this operation is only allowed for clusters in AVAILABLE state",
			cluster.ClusterId,
			cluster.State.String(),
		)
	}

	newCollationEnabled, err := s.getNewCollationEnabledWithBackupFallback(ctx, cluster)
	if err != nil {
		return nil, err
	}

	clusterConfiguration, err := s.clusterConfigCli.GetCustomClusterConfiguration(ctx, cluster)
	if err != nil {
		return nil, err
	}

	for i, componentConfig := range clusterConfiguration.ComponentConfigurations {
		if componentConfig.ComponentType == globaldedicatedv1.ComponentType_TIDB {
			if componentConfig.CustomParams == nil {
				componentConfig.CustomParams = make(map[string]*globaldedicatedv1.ClusterConfiguration_ComponentConfiguration_ParamItem)
			}

			// Set new_collations_enabled_on_first_bootstrap parameter.
			// Note: This configuration takes effect only for the TiDB cluster that is first initialized. After the
			// initialization, you cannot use this configuration item to enable or disable the new collation support.
			componentConfig.CustomParams[NewCollationEnabledConfigKey] = &globaldedicatedv1.ClusterConfiguration_ComponentConfiguration_ParamItem{
				Value:   structpb.NewBoolValue(newCollationEnabled),
				Comment: "enables or disables the new collation support",
			}
			clusterConfiguration.ComponentConfigurations[i] = componentConfig
			break
		}
	}

	return &globaldedicatedv1.GetClusterCustomParamsResponse{
		ClusterConfiguration: clusterConfiguration,
	}, nil
}

func (s *serviceImpl) getNewCollationEnabledWithBackupFallback(
	ctx context.Context,
	cluster *entity.Cluster,
) (bool, bizerr.BizError) {
	newCollationEnabled, err := s.isNewCollationEnabled(ctx, cluster)
	if err == nil {
		return newCollationEnabled, nil
	}

	tilog.Warn(ctx, "failed to get new_collation_enabled from live cluster, fallback to config backup",
		zap.Uint64("cluster_id", cluster.ClusterId),
		zap.Error(err),
	)

	backupValue, backupErr := s.getBackedUpNewCollationEnabled(ctx, cluster.ClusterId)
	if backupErr != nil {
		return false, bizerr.WrapError(
			backupErr,
			"failed to get new_collation_enabled from backup after live query failure for cluster %d",
			cluster.ClusterId,
		)
	}

	tilog.Warn(ctx, "use backed up new_collation_enabled after live query failure",
		zap.Uint64("cluster_id", cluster.ClusterId),
		zap.Bool("new_collation_enabled", *backupValue),
	)
	return *backupValue, nil
}

func (s *serviceImpl) getBackedUpNewCollationEnabled(
	ctx context.Context,
	clusterID uint64,
) (*bool, bizerr.BizError) {
	backup, err := s.configBackupRepo.GetByClusterID(ctx, clusterID)
	if err != nil {
		if err.ErrCode() == bizerr.ErrCodeRecordNotFound {
			return nil, bizerr.New(
				bizerr.ErrCodeRecordNotFound,
				"config backup for cluster %d not found when reading new_collation_enabled",
				clusterID,
			)
		}
		return nil, bizerr.WrapError(err, "failed to get config backup for cluster %d", clusterID)
	}

	if backup.NewCollationEnabled == nil {
		return nil, bizerr.New(
			bizerr.ErrCodeRecordNotFound,
			"new_collation_enabled is unavailable in config backup for cluster %d",
			clusterID,
		)
	}

	return backup.NewCollationEnabled, nil
}

func (s *serviceImpl) isNewCollationEnabled(
	ctx context.Context,
	cluster *entity.Cluster,
) (bool, bizerr.BizError) {
	region, err := s.regionRepo.SelectRegionById(ctx, cluster.RegionId, cluster.OrganizationId)
	if err != nil {
		return false, err
	}

	regionCluster, err := s.regionServerCli.GetCluster(
		ctx,
		region,
		&regionaldedicatedv1.GetClusterRequest{
			ClusterId:      strconv.FormatUint(cluster.ClusterId, 10),
			ShowStatusOnly: false,
		},
	)
	if err != nil {
		return false, bizerr.WrapError(err, "failed to get region cluster")
	}

	cloudAdminPwd, err := s.encryptor.DecryptBase64(ctx, cluster.EncryptedCloudAdminPassword)
	if err != nil {
		return false, err
	}

	proxy, err := s.clusterProxyProvider.GetClusterProxy(
		ctx,
		cluster.ClusterId,
		regionCluster.ProxyBaseUrl,
		cluster.Version,
		&pkgclusterproxy.ClusterOperator{
			Username:   clusterproxy.CloudAdminUser,
			AuthMethod: pkgclusterproxy.AuthMethodPassword,
			Credential: cloudAdminPwd,
		},
	)
	if err != nil {
		return false, err
	}
	sqlResult, err := proxy.QuerySQL(ctx, GetNewCollationEnabledSQL)
	if err != nil {
		return false, err
	}

	type variableResult struct {
		VariableValue []string `json:"VARIABLE_VALUE"`
	}

	var result variableResult
	if err := json.Unmarshal(sqlResult, &result); err != nil {
		return false, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeJsonMarshal, "failed to unmarshal new_collation_enabled variable")
	}
	if len(result.VariableValue) != 1 {
		return false, bizerr.New(bizerr.ErrCodeParams, "failed to get new_collation_enabled variable")
	}

	valueBytes, rawErr := base64.StdEncoding.DecodeString(result.VariableValue[0])
	if rawErr != nil {
		return false, bizerr.WrapError(rawErr, "failed to decode base64 value: %s", result.VariableValue[0])
	}

	newCollationEnabled := lo.Ternary(
		strings.EqualFold(string(valueBytes), "false"),
		false,
		true,
	)
	return newCollationEnabled, nil
}
