package workflow

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/samber/lo"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

// GenerateNodeInstanceGroupName generate node group name with the following rule:
// Example: 1234567890-tidb-2222222-t4g-medium-us-west-2a
// It's easy to get component, instanceType, zone and current node version
//

func GenStorageClassName(sharedPool *entity.SharedPool, storage *entity.Storage) string {
	if storage.GetType() == entity.LocalSSD {
		return entity.LocalSSD
	}

	return entity.CloudSSD
}

func GenVolumeAttributesClassName(storage *entity.Storage) string {
	name := fmt.Sprintf("%s-%d-%d", storage.GetType(), storage.GetIops(), storage.GetThroughputMbs())
	// io2 only has iops
	if storage.GetType() == entity.IO2 {
		name = fmt.Sprintf("%s-%d", storage.GetType(), storage.GetIops())
	}

	// cloud auto only has iops
	if storage.GetType() == entity.CloudAuto {
		name = fmt.Sprintf("%s-%d", storage.GetType(), storage.GetIops())
	}

	return strings.ToLower(name)
}

// BuildServerLabelsYAML generates YAML formatted server labels.
// Returns empty string when labels is empty or nil.
// The template renderer will auto-compensate indentation based on placeholder position.
func BuildServerLabelsYAML(labels map[string]string) string {
	if len(labels) == 0 {
		return ""
	}

	// Sort keys for stable output
	keys := make([]string, 0, len(labels))
	for k := range labels {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var lines []string
	lines = append(lines, "labels:")
	for _, k := range keys {
		lines = append(lines, fmt.Sprintf("  %q: %q", k, labels[k]))
	}
	return strings.Join(lines, "\n")
}

// func GetKmsId(sharedPool *entity.SharedPool) string {
// 	var kmsId string
// 	switch entity.GetK8sProviderType(sharedPool.GetProvider()) {
// 	case apiv1.ProviderType_AWS:
// 		kmsId = sharedPool.GetSecurityConfiguration().GetAwsMasterKeyArn()
// 	case apiv1.ProviderType_GCP:
// 		kmsId = sharedPool.GetSecurityConfiguration().GetGcpMasterKeyName()
// 	case apiv1.ProviderType_AZURE:
// 		// kmsId = clusterBlock.GetSecurityConfiguration().GetAzureKeyVaultKey()
// 	default:
// 		kmsId = ""
// 	}
// 	return kmsId
// }

// no target component id and type, all component will be executed operation
func IsAllComponent(args actions.ActionArgs) bool {
	compIds, _ := args.GetString(ComponentIdsKey)
	if compIds != "" {
		return false
	}

	compTypes, _ := args.GetString(ComponentTypesKey)
	return compTypes == ""
}

// check if the component is target component
func CheckIsTargetComponent(ctx context.Context, comp *entity.Component, args actions.ActionArgs) bool {
	var targetCompIds []string
	var targetCompTypes []string

	// apply to multiple component ids
	compIds, err := args.GetString(ComponentIdsKey)
	if err != nil {
		tilog.Warn(ctx, "not get component id, will allow all component id to execute operation")
	} else if compIds != "" {
		targetCompIds = strings.Split(compIds, ",")
	}

	// apply to multiple component types
	compTypes, err := args.GetString(ComponentTypesKey)
	if err != nil {
		tilog.Warn(ctx, "not get component type, will allow all component type to execute operation")
	} else if compTypes != "" {
		targetCompTypes = strings.Split(compTypes, ",")
	}

	// no target component id and type, allow all
	if len(targetCompIds) == 0 && len(targetCompTypes) == 0 {
		return true
	}

	// check component id
	if len(targetCompIds) > 0 && !lo.Contains(targetCompIds, comp.GetId()) {
		return false
	}

	// check component type
	if len(targetCompTypes) > 0 && !lo.Contains(targetCompTypes, entity.GetComponentTypeName(comp)) {
		return false
	}

	return true
}

// CheckIsAffectedComponent checks if the component is affected by the running DAG.
// It prioritizes AffectedComponentTypesKey over ComponentTypesKey for overlay filtering.
// This is used by running-DAG overlay to expose both TiKV and COPROCESSOR_WORKER as in-flight
// while keeping action mutation filter (CheckIsTargetComponent) scoped to mutation target only.
func CheckIsAffectedComponent(ctx context.Context, comp *entity.Component, args actions.ActionArgs) bool {
	// prioritize affected component types over component types for overlay
	affectedCompTypes, err := args.GetString(AffectedComponentTypesKey)
	if err == nil && affectedCompTypes != "" {
		return lo.Contains(strings.Split(affectedCompTypes, ","), entity.GetComponentTypeName(comp))
	}

	// Older DAGs do not carry overlay scope, so preserve the complete mutation
	// target semantics, including the ComponentIdsKey intersection.
	return CheckIsTargetComponent(ctx, comp, args)
}

// because the provided instance info is the total resource of the instance
// but the k8s node resource is less than the total resource
// cloud provider will reserve some resource for the system
// so we need to reduce the resource to avoid the resource overcommit
func SetResourceReady(comp *entity.Component, templArgs map[string]string) error {
	nodeSpec := comp.GetNodeQuota().GetNodeSpec()

	if nodeSpec.GetAvailableMemoryMi() == 0 {
		return fmt.Errorf("available memory is not set in node spec")
	}

	sidecarCPUMilli, sidecarMemInMiB := tioperator.SidecarResourcesFor(comp.GetComponentType())
	limitMem := tioperator.EffectiveMaxMemoryMi(nodeSpec)
	requestMem := tioperator.ComputeRequestMemory(uint64(nodeSpec.GetAvailableMemoryMi()), sidecarMemInMiB)
	templArgs["RequestMemory"] = strconv.FormatUint(requestMem, 10)
	templArgs["Memory"] = strconv.FormatUint(limitMem, 10)

	availCPUMilli := tioperator.EffectiveAvailableCPUMilli(nodeSpec)
	limitCPUMilli := tioperator.EffectiveMaxCPUMilli(nodeSpec)
	requestCPUMilli := tioperator.ComputeRequestCPU(availCPUMilli, sidecarCPUMilli)
	// CPU is the request Quantity so old templates (both sides {{!CPU}})
	// do not put Max on request. LimitCPU is consumed by exist-wrapped
	// limits.cpu in the current templates; old executors omit the key and
	// the limit CPU line is stripped. Unit lives in the arg; do not
	// suffix `m` in the template.
	templArgs["CPU"] = strconv.FormatUint(requestCPUMilli, 10) + "m"
	templArgs["LimitCPU"] = strconv.FormatUint(limitCPUMilli, 10) + "m"

	if comp.GetComponentType() == apiv1.ComponentType_TIDB || comp.GetComponentType() == apiv1.ComponentType_TIDB_WORKER {
		templArgs["GOMEMLIMIT"] = strconv.FormatUint(uint64(float64(limitMem)*tioperator.GoMemlimitRate), 10)
	}

	templArgs["SetResource"] = "true"
	return nil
}

func BuildTopologies(compType apiv1.ComponentType, zones []string, templArgs map[string]string, pgEnabled bool) error {
	if pgEnabled {
		const partitionCount = 3
		topologies := make([]string, 0, partitionCount)
		for i := 0; i < partitionCount; i++ {
			topologies = append(topologies, fmt.Sprintf("- topology:\n    tidbcloud.com/pg-partition: \"%d\"", i))
		}
		templArgs["Topologies"] = strings.Join(topologies, "\n")
		return nil
	}

	if len(zones) == 0 {
		return fmt.Errorf("no available zones")
	}

	// sort zones to make sure the order is the same
	// e.g. us-west-2z, us-west-2a, us-west-2c -> us-west-2a, us-west-2c, us-west-2z
	sort.Strings(zones)

	topologies := make([]string, 0, len(zones))

	if compType == apiv1.ComponentType_TIFLASH_COMPUTE || compType == apiv1.ComponentType_TIFLASH_WRITE {
		// tiflash only support 2 zone
		topologies = append(topologies, fmt.Sprintf("- topology:\n    topology.kubernetes.io/zone: %s", zones[0]))
		if len(zones) > 1 {
			topologies = append(topologies, fmt.Sprintf("- topology:\n    topology.kubernetes.io/zone: %s", zones[1]))
		}
	} else {
		for _, zone := range zones {
			topologies = append(topologies, fmt.Sprintf("- topology:\n    topology.kubernetes.io/zone: %s", zone))
		}
	}

	templArgs["Topologies"] = strings.Join(topologies, "\n")

	return nil
}

func FullComponentConfigVersion(
	ctx context.Context,
	clusterConfigClient clusterconfig.Client,
	meteringConfig *config.MeteringConfig,
	sharedPool *entity.SharedPool,
	comp *entity.Component,
	tidbClusterId string,
) error {
	if comp.GetPhase() == apiv1.Component_DELETING && comp.GetConfiguration().GetImageTag() == "" {
		setDefultConfig(sharedPool, comp)
		return nil
	}

	switch comp.GetComponentType() {
	case apiv1.ComponentType_TIDB:
		if tidbClusterId == "" {
			return fmt.Errorf("tidbClusterId is required for tidb component")
		}
		version, configuration, err := clusterConfigClient.GetTiDBClusterVersionAndConfig(ctx, &entity.TiDBGroup{
			TiDBGroup: apiv1.TiDBGroup{
				Id:           comp.GetId(),
				ClusterId:    tidbClusterId,
				SharedPoolId: sharedPool.GetId(),
			},
		}, comp.GetComponentType())
		if err != nil {
			tilog.Error(ctx, "failed to get component version and config",
				zap.String("componentType", comp.GetComponentType().String()),
				zap.String("componentId", comp.GetId()),
				zap.String("tidbClusterId", tidbClusterId))
			return fmt.Errorf("failed to get component version and config: %w", err)
		}
		comp.Version = version
		comp.Configuration = configuration

		if err := entity.ComputeTiDBConfig(sharedPool, comp); err != nil {
			tilog.Error(ctx, "failed to compute tidb config",
				zap.String("tidbGroupId", comp.GetId()),
				zap.String("tidbClusterId", tidbClusterId),
				zap.Error(err))
			return fmt.Errorf("failed to compute tidb config: %w", err)
		}
	case apiv1.ComponentType_TIPROXY:
		var version string
		var configuration *entity.ComponentConfiguration
		if entity.HasSharedTiProxy(sharedPool) {
			var err error
			version, configuration, err = clusterConfigClient.GetComponentVersionAndConfig(ctx, sharedPool, comp)
			if err != nil {
				tilog.Error(ctx, "failed to get tiproxy version and config with multi-tenant shared pool",
					zap.String("componentType", comp.GetComponentType().String()),
					zap.String("componentId", comp.GetId()))
				return fmt.Errorf("failed to get tiproxy version and config: %w", err)
			}
			tilog.Info(ctx, "get tiproxy version and config with multi-tenant shared pool",
				zap.String("version", version),
				zap.String("config", configuration.GetConfigurationFileContent()))
		} else {
			var err error
			version, configuration, err = clusterConfigClient.GetTiDBClusterVersionAndConfig(ctx, &entity.TiDBGroup{
				TiDBGroup: apiv1.TiDBGroup{
					ClusterId:    comp.GetId(),
					SharedPoolId: sharedPool.GetId(),
				},
			}, comp.GetComponentType())
			if err != nil {
				tilog.Error(ctx, "failed to get tiproxy version and config",
					zap.String("componentType", comp.GetComponentType().String()),
					zap.String("componentId", comp.GetId()))
				return fmt.Errorf("failed to get tiproxy version and config: %w", err)
			}
		}
		comp.Version = version
		comp.Configuration = configuration

		if err := entity.ComputeTiProxyConfig(sharedPool, comp); err != nil {
			tilog.Error(ctx, "failed to compute tiproxy config",
				zap.String("componentId", comp.GetId()),
				zap.Error(err))
			return fmt.Errorf("failed to compute tiproxy config: %w", err)
		}
	default:
		// full version and config
		version, configuration, err := clusterConfigClient.GetComponentVersionAndConfig(ctx, sharedPool, comp)
		if err != nil {
			tilog.Error(ctx, "failed to get component version and config",
				zap.String("componentType", comp.GetComponentType().String()),
				zap.String("componentId", comp.GetId()),
				zap.String("sharedPoolId", sharedPool.GetId()))
			return fmt.Errorf("failed to get component version and config: %w", err)
		}
		comp.Version = version
		comp.Configuration = configuration

		if err := computeComponentConfig(sharedPool, comp); err != nil {
			tilog.Error(ctx, "failed to compute component config",
				zap.String("componentType", comp.GetComponentType().String()),
				zap.String("componentId", comp.GetId()),
				zap.Error(err))
			return fmt.Errorf("failed to compute component config: %w", err)
		}
	}

	// patch metering config if enabled
	if meteringConfig.Enable {
		meteringSDKConf := meteringConfig.ToMeteringSDKConfig()
		meteringSDKConf.SharedPoolID = sharedPool.GetId()
		switch comp.GetComponentType() {
		case apiv1.ComponentType_TIDB:
			if err := entity.PatchTiDBConfigWithMetering(comp, meteringSDKConf); err != nil {
				tilog.Error(ctx, "failed to patch tidb config with metering",
					zap.String("tidbGroupId", comp.GetId()),
					zap.String("tidbClusterId", tidbClusterId),
					zap.Error(err))
				return fmt.Errorf("failed to patch tidb config with metering: %w", err)
			}
		case apiv1.ComponentType_TIDB_WORKER:
			if err := entity.PatchTiDBConfigWithMetering(comp, meteringSDKConf); err != nil {
				tilog.Error(ctx, "failed to patch tidb worker config with metering",
					zap.String("componentType", comp.GetComponentType().String()),
					zap.String("componentId", comp.GetId()),
					zap.Error(err))
				return fmt.Errorf("failed to patch tidb worker config with metering: %w", err)
			}
		case apiv1.ComponentType_PD:
			if err := entity.PatchPDConfigWithMetering(comp, meteringSDKConf); err != nil {
				tilog.Error(ctx, "failed to patch pd config with metering",
					zap.String("componentType", comp.GetComponentType().String()),
					zap.String("componentId", comp.GetId()),
					zap.Error(err))
				return fmt.Errorf("failed to patch pd config with metering: %w", err)
			}
		case apiv1.ComponentType_TIPROXY:
			if err := entity.PatchTiProxyConfigWithMetering(comp, meteringSDKConf); err != nil {
				tilog.Error(ctx, "failed to patch tiproxy config with metering",
					zap.String("componentId", comp.GetId()),
					zap.Error(err))
				return fmt.Errorf("failed to patch tiproxy config with metering: %w", err)
			}
		}
	}

	return nil
}

func AddAzureDfsArgs(sharedPool *entity.SharedPool, tplArgs map[string]string) error {
	if sharedPool.GetProvider().GetType() != apiv1.ProviderType_AZURE {
		return nil
	}
	account, container, err := sharedPool.GetAzureStorageAccountAndContainer()
	if err != nil {
		return err
	}
	tplArgs["DfsBackend"] = "azure"
	tplArgs["DfsAzureAccountName"] = account
	tplArgs["DfsAzureContainer"] = container
	if clientID := sharedPool.GetProvider().GetNodeClientId(); clientID != "" {
		tplArgs["AZURE_CLIENT_ID"] = clientID
	}
	return nil
}

func computeComponentConfig(sharedPool *entity.SharedPool, component *entity.Component) error {
	// kmsId := workflow.GetKmsId(sharedPool)

	kmsId := ""
	switch component.GetComponentType() {
	case apiv1.ComponentType_PD,
		apiv1.ComponentType_PD_TSO,
		apiv1.ComponentType_PD_SCHEDULING:
		err := entity.ComputePDConfig(sharedPool, component)
		if err != nil {
			return err
		}
	case apiv1.ComponentType_TIDB_WORKER:
		err := entity.ComputeTiDBConfig(sharedPool, component)
		if err != nil {
			return err
		}
	case apiv1.ComponentType_TIKV:
		err := entity.ComputeTiKVConfig(sharedPool, component, kmsId)
		if err != nil {
			return err
		}
	case apiv1.ComponentType_SCHEMA_MANAGER:
		err := entity.ComputeSchemaManagerConfig(sharedPool, component)
		if err != nil {
			return err
		}
	case apiv1.ComponentType_TIFLASH_LEARNER,
		apiv1.ComponentType_TIFLASH_COMPUTE_LEARNER:
		err := entity.ComputeTiflashLearnerConfig(sharedPool, component, kmsId)
		if err != nil {
			return err
		}
	case apiv1.ComponentType_TIFLASH_COMPUTE:
		err := entity.ComputeTiflashComputeConfig(sharedPool, component, kmsId)
		if err != nil {
			return err
		}
	case apiv1.ComponentType_TIFLASH_WRITE:
		err := entity.ComputeTiflashWriteConfig(sharedPool, component, kmsId)
		if err != nil {
			return err
		}
	}
	return nil
}

func setDefultConfig(sharedPool *entity.SharedPool, component *entity.Component) {
	if component.GetVersion() == "" {
		component.Version = sharedPool.GetVersion()
	}

	// set default config
	defConf := &entity.ComponentConfiguration{
		ImageRepo:                entity.GetComponentTypeName(component),
		ImageTag:                 sharedPool.GetVersion(),
		ConfigurationFileContent: "",
	}

	if component.GetConfiguration() == nil {
		component.Configuration = defConf
		return
	}

	if component.GetConfiguration().GetImageRepo() == "" {
		component.Configuration.ImageRepo = defConf.GetImageRepo()
	}

	if component.GetConfiguration().GetImageTag() == "" {
		component.Configuration.ImageTag = defConf.GetImageTag()
	}

	if component.GetConfiguration().GetConfigurationFileContent() == "" {
		component.Configuration.ConfigurationFileContent = defConf.GetConfigurationFileContent()
	}
}
