package server

import (
	"context"
	"fmt"
	"regexp"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow/dag"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
)

const (
	// MaxLabelsCount is the maximum number of labels allowed per component.
	MaxLabelsCount = 20
	// MaxLabelKeyLength is the maximum length of a label key.
	MaxLabelKeyLength = 63
	// MaxLabelValueLength is the maximum length of a label value.
	MaxLabelValueLength = 256
)

// labelKeyPattern enforces the same pattern as defined in proto:
// ^[a-z0-9/_-]+$ - lowercase alphanumeric, slashes, underscores, and hyphens only.
var labelKeyPattern = regexp.MustCompile(`^[a-z0-9/_-]+$`)

// reservedLabelKeys are label keys that cannot be set by external API calls.
var reservedLabelKeys = []string{
	tioperator.LabelKeyKeyspace,
	tioperator.LabelKeyCidr,
	"host",
	"region",
	"zone",
}

// supportedLabelComponentTypes are component types that support setting labels.
var supportedLabelComponentTypes = map[apiv1.ComponentType]bool{
	apiv1.ComponentType_TIDB:            true,
	apiv1.ComponentType_TIKV:            true,
	apiv1.ComponentType_TIFLASH_WRITE:   true,
	apiv1.ComponentType_TIFLASH_COMPUTE: true,
	apiv1.ComponentType_TIPROXY:         true,
}

// isReservedLabelKey checks if the given key is a reserved label key.
func isReservedLabelKey(key string) bool {
	for _, reservedKey := range reservedLabelKeys {
		if key == reservedKey {
			return true
		}
	}
	return false
}

// validateLabelKey validates a label key format and length.
func validateLabelKey(key string) error {
	if key == "" {
		return fmt.Errorf("label key cannot be empty")
	}
	if len(key) > MaxLabelKeyLength {
		return fmt.Errorf("label key '%s' exceeds maximum length of %d characters", key, MaxLabelKeyLength)
	}
	if !labelKeyPattern.MatchString(key) {
		return fmt.Errorf("label key '%s' contains invalid characters, must match pattern ^[a-z0-9/_-]+$", key)
	}
	return nil
}

// validateLabelValue validates a label value length.
func validateLabelValue(key, value string) error {
	if len(value) > MaxLabelValueLength {
		return fmt.Errorf("label value for key '%s' exceeds maximum length of %d characters", key, MaxLabelValueLength)
	}
	return nil
}

// isSupportedLabelComponentType checks if the given component type supports labels.
func isSupportedLabelComponentType(componentType apiv1.ComponentType) bool {
	return supportedLabelComponentTypes[componentType]
}

func (s *serverImpl) SetSharedPoolComponentLabels(ctx context.Context, sharedPoolId string, targetComponent *entity.Component) (*entity.Component, error) {
	if sharedPoolId == "" {
		return nil, status.Errorf(codes.InvalidArgument, "sharedPool id is required")
	}

	if targetComponent == nil {
		return nil, status.Errorf(codes.InvalidArgument, "target component is empty")
	}

	if targetComponent.GetComponentType() == apiv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED {
		return nil, status.Errorf(codes.InvalidArgument, "target component type is required")
	}

	// Check if component type supports labels
	if !isSupportedLabelComponentType(targetComponent.GetComponentType()) {
		return nil, status.Errorf(codes.InvalidArgument, "component type '%s' does not support labels", targetComponent.GetComponentType().String())
	}

	labels := targetComponent.GetLabels()
	if len(labels) == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "labels cannot be empty")
	}

	// Validate label count
	if len(labels) > MaxLabelsCount {
		return nil, status.Errorf(codes.InvalidArgument, "too many labels: %d exceeds maximum of %d", len(labels), MaxLabelsCount)
	}

	// Validate each label key and value
	for key, value := range labels {
		// Check for reserved label keys
		if isReservedLabelKey(key) {
			return nil, status.Errorf(codes.InvalidArgument, "label key '%s' is reserved and cannot be set externally", key)
		}
		// Validate key format and length
		if err := validateLabelKey(key); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "invalid label key: %s", err.Error())
		}
		// Validate value length
		if err := validateLabelValue(key, value); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "invalid label value: %s", err.Error())
		}
	}

	var currComponent *entity.Component

	var dagExecId string
	var dagSubmitted bool
	if err := s.transactioner.Transaction(ctx, func(ctx context.Context) error {
		// check if the sharedPool is locked
		unlockFunc, err := s.lock(ctx, sharedPoolId)
		if err != nil {
			tilog.Error(ctx, "failed to lock sharedPool", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
			return err
		}
		defer func() {
			if err := unlockFunc(); err != nil {
				tilog.Error(ctx, "failed to unlock sharedPool", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
			}
		}()

		// get shared first
		sharedPool, err := s.sharedPoolRepo.Load(ctx, sharedPoolId, false)
		if err != nil {
			if status.Code(err) != codes.NotFound {
				return status.Errorf(codes.Internal, "failed to load sharedPool: %s", err)
			}
			return status.Errorf(codes.NotFound, "sharedPool %s not found", sharedPoolId)
		}
		if err := guardSharedPoolMutation(sharedPool, sharedPoolMutationBlockedWhilePaused); err != nil {
			return err
		}

		// merge sharedPool status from dag task
		err = s.mergeSharedPoolStatusFromDagTask(ctx, sharedPool)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to merge sharedPool status from dag task: %s", err)
		}

		if sharedPool.GetPhase() == apiv1.SharedPool_CREATING {
			return status.Errorf(codes.FailedPrecondition, "shared pool is creating, please try again later")
		}

		// Label updates re-render the target CR via PlanUpdateComponent, so a
		// TIKV label change must not race an in-flight tikv-remote-worker
		// switch (or unrepaired topology drift) - both render the same
		// TiKVGroup from frozen snapshots and the later writer would win.
		if isTiKVRemoteWorkerGuardedComponentType(targetComponent.GetComponentType()) {
			if err := s.guardTiKVRemoteWorkersTopologyStable(ctx, sharedPool); err != nil {
				return err
			}
		}

		if targetComponent.GetId() == "" {
			tilog.Warn(ctx, "target component id is empty, will use default id")
			targetComponent.Id = sharedPool.GetDefaultComponenId()
		}

		// Find the target component
		var found bool
		for _, comp := range sharedPool.GetComponentGroups() {
			if comp.GetId() != targetComponent.GetId() || comp.GetComponentType() != targetComponent.GetComponentType() {
				continue
			}

			found = true
			currComponent = comp

			if err := checkComponentDeleteFence(comp); err != nil {
				return err
			}

			// Merge labels: upsert semantics - add or override, don't delete
			if comp.Labels == nil {
				comp.Labels = make(map[string]string)
			}
			for key, value := range targetComponent.GetLabels() {
				comp.Labels[key] = value
			}

			comp.Phase = apiv1.Component_MODIFYING
			break
		}

		if !found {
			return status.Errorf(codes.NotFound, "component with id '%s' and type '%s' not found in sharedPool",
				targetComponent.GetId(), targetComponent.GetComponentType().String())
		}

		sharedPoolStr, err := protojson.Marshal(sharedPool)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to marshal sharedPool: %s", err)
		}

		// start update component dag
		args := actions.ActionArgs{
			workflow.SharedPoolIdKey:                      actions.ActionArgString(sharedPool.GetId()),
			workflow.SharedPoolKey:                        actions.ActionArgString(string(sharedPoolStr)),
			workflow.ComponentIdsKey:                      actions.ActionArgString(targetComponent.GetId()),
			workflow.ComponentTypesKey:                    actions.ActionArgString(entity.GetComponentTypeName(targetComponent)),
			entity.GetUpdateComponentKey(targetComponent): actions.ActionArgString(string(tioperator.OpsUpdateLabels)),
			utils.TraceIdKey:                              utils.GetTraceId(ctx),
		}

		dagExecId = utils.GeneratedDagExecId(ctx,
			fmt.Sprintf("set-component-labels-%s-%s-%s-%s",
				sharedPool.GetId(),
				entity.GetComponentTypeName(targetComponent),
				targetComponent.GetId(),
				utils.FnvHash(string(sharedPoolStr)),
			))
		err = s.dagExecutor.Submit(ctx, dag.PlanUpdateComponent, args, dagExecId)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to submit set-component-labels dag: %s", err)
		}
		dagSubmitted = true
		return nil
	}); err != nil {
		if dagSubmitted {
			if abortErr := s.dagExecutor.Abort(ctx, dagExecId); abortErr != nil {
				tilog.Error(ctx, "failed to abort dag after transaction failure",
					zap.String("dagExecId", dagExecId),
					zap.Error(abortErr))
			}
		}
		return nil, err
	}

	return currComponent, nil
}
