package cluster

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"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"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	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"
	utilsclusterproxy "github.com/tidbcloud/tidb-management-service/utilities/clusterproxy"
)

const (
	showGlobalVariablesSQL = "SHOW GLOBAL VARIABLES"
	setSystemVariableSQL   = "SET GLOBAL `%s` = '%s'"
)

type systemVariablesQueryResult struct {
	Value        []string `json:"Value"`
	VariableName []string `json:"Variable_name"`
}

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

	if req.GetSkip() {
		tilog.Info(ctx, "skip syncing system variables by request")
		return nil
	}

	// Validate source cluster
	sourceCluster, err := s.clusterRepo.Get(ctx, req.GetSourceClusterId(), false)
	if err != nil {
		return bizerr.WrapError(err, "failed to get source cluster")
	}
	if sourceCluster.State != globaldedicatedv1.Cluster_AVAILABLE {
		return bizerr.New(bizerr.ErrCodePermission, "source cluster is not available")
	}

	// Validate target cluster
	targetCluster, err := s.clusterRepo.Get(ctx, req.GetTargetClusterId(), false)
	if err != nil {
		return bizerr.WrapError(err, "failed to get target cluster")
	}
	if targetCluster.State != globaldedicatedv1.Cluster_AVAILABLE {
		return bizerr.New(bizerr.ErrCodePermission, "target cluster is not available")
	}

	if err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		// Get system variables from source cluster or backup
		sourceSystemVariablesMap, found, err := s.getSourceSystemVariables(ctx, sourceCluster, req.GetAllowFallbackToBackup())
		if err != nil {
			return err
		}
		if !found {
			tilog.Warn(ctx, "skip syncing system variables because neither live config nor backup is available",
				zap.Uint64("source_cluster_id", sourceCluster.ClusterId))
			return nil
		}

		// Get system variables from target cluster
		targetSystemVariablesMap, targetProxy, err := s.getClusterSystemVariablesWithProxy(ctx, targetCluster, "target")
		if err != nil {
			return err
		}

		// Find variables that differ between source and target
		diffSystemVariables := lo.PickBy(sourceSystemVariablesMap, func(name, sourceValue string) bool {
			targetValue, exists := targetSystemVariablesMap[name]
			return exists && sourceValue != targetValue
		})

		// Convert to SQL statements with proper escaping
		sqlStatements := lo.MapToSlice(diffSystemVariables, func(name, value string) string {
			escapedValue := strings.ReplaceAll(value, "'", "''")
			return fmt.Sprintf(setSystemVariableSQL, name, escapedValue)
		})

		// Execute all SQL statements
		if len(sqlStatements) > 0 {
			for _, sql := range sqlStatements {
				if err := targetProxy.ExecuteSQL(ctx, sql); err != nil {
					errMsg := err.Error()
					switch {
					case strings.Contains(errMsg, "1238"):
						// 1238: Variable 'xxx' is a read only variable
						tilog.Info(ctx, "skip read only variable", zap.String("sql", sql))
						continue
					case strings.Contains(errMsg, "1227"), strings.Contains(errMsg, "1045"):
						// 1227: ER_SPECIFIC_ACCESS_DENIED_ERROR
						// 1045: ER_ACCESS_DENIED_ERROR
						// skip lack of privileges
						tilog.Info(ctx, "skip lack of privileges", zap.String("sql", sql))
						continue
					default:
						return err
					}
				}
			}
		}
		return nil
	}); err != nil {
		return bizerr.WrapError(err, "failed to sync system variables")
	}
	return nil
}

// getClusterSystemVariables retrieves system variables from a cluster
func (s *serviceImpl) getClusterSystemVariables(ctx context.Context, cluster *entity.Cluster, clusterType string) (map[string]string, error) {
	variables, _, err := s.getClusterSystemVariablesWithProxy(ctx, cluster, clusterType)
	return variables, err
}

// getClusterSystemVariablesWithProxy retrieves system variables and returns the proxy for reuse
func (s *serviceImpl) getClusterSystemVariablesWithProxy(
	ctx context.Context,
	cluster *entity.Cluster,
	clusterType string,
) (map[string]string, utilsclusterproxy.ClusterProxy, error) {
	// Get and validate region
	regionEn, err := s.regionRepo.SelectRegionById(ctx, cluster.RegionId, cluster.OrganizationId)
	if err != nil {
		return nil, nil, bizerr.WrapError(err, "failed to select cluster region")
	}
	if regionEn.State != globalcommonv1.Region_ACTIVE {
		return nil, nil, bizerr.New(bizerr.ErrCodePermission, "cluster region is not active")
	}

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

	// Decrypt admin password
	cloudAdminPwd, err := s.encryptor.DecryptBase64(ctx, cluster.EncryptedCloudAdminPassword)
	if err != nil {
		return nil, nil, bizerr.WrapError(err, "failed to decrypt cloud admin password")
	}

	// Create cluster proxy
	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 nil, nil, bizerr.WrapError(err, "failed to get cluster proxy")
	}

	// Query system variables
	systemVariablesJSON, err := proxy.QuerySQL(ctx, showGlobalVariablesSQL)
	if err != nil {
		return nil, nil, bizerr.WrapError(err, "failed to query system variables")
	}

	// Parse and decode system variables
	systemVariablesMap, parseErr := parseSystemVariables(systemVariablesJSON, clusterType)
	if parseErr != nil {
		return nil, nil, bizerr.WrapError(parseErr, "failed to parse system variables")
	}

	return systemVariablesMap, proxy, nil
}

// parseSystemVariables decodes base64-encoded system variables from JSON
func parseSystemVariables(jsonData []byte, clusterType string) (map[string]string, error) {
	var result systemVariablesQueryResult
	if err := json.Unmarshal(jsonData, &result); err != nil {
		return nil, fmt.Errorf("failed to unmarshal %s system variables: %w", clusterType, err)
	}

	variablesMap := make(map[string]string, len(result.VariableName))
	for idx := range result.VariableName {
		variableNameBytes, err := base64.StdEncoding.DecodeString(result.VariableName[idx])
		if err != nil {
			return nil, fmt.Errorf("failed to decode %s system variable name at index %d: %w", clusterType, idx, err)
		}

		variableValueBytes, err := base64.StdEncoding.DecodeString(result.Value[idx])
		if err != nil {
			return nil, fmt.Errorf("failed to decode %s system variable value at index %d: %w", clusterType, idx, err)
		}

		variablesMap[string(variableNameBytes)] = string(variableValueBytes)
	}

	return variablesMap, nil
}

func (s *serviceImpl) getSourceSystemVariables(
	ctx context.Context,
	sourceCluster *entity.Cluster,
	allowFallbackToBackup bool,
) (map[string]string, bool, error) {
	systemVariables, err := s.getClusterSystemVariables(ctx, sourceCluster, "source")
	if err == nil {
		return systemVariables, true, nil
	}

	if !allowFallbackToBackup {
		tilog.Info(ctx, "failed to get source system variables from live cluster and backup fallback is disabled",
			zap.Uint64("source_cluster_id", sourceCluster.ClusterId),
			zap.Error(err),
		)
		return nil, false, err
	}

	tilog.Warn(ctx, "failed to get source system variables from live cluster, fallback to backup",
		zap.Uint64("source_cluster_id", sourceCluster.ClusterId),
		zap.Error(err),
	)

	backup, backupErr := s.configBackupRepo.GetByClusterID(ctx, sourceCluster.ClusterId)
	if backupErr != nil {
		if backupErr.ErrCode() == bizerr.ErrCodeRecordNotFound {
			return nil, false, nil
		}
		return nil, false, backupErr
	}
	if len(backup.SystemVariables) == 0 {
		return nil, false, nil
	}
	return backup.SystemVariables, true, nil
}
