package action

import (
	"context"
	"fmt"
	"sort"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	templateversion "github.com/tidbcloud/cluster-service-ng/pkg/template/version"
	"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"
	autoscalingv2 "k8s.io/api/autoscaling/v2"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type ApplyTiProxyGroupAction struct {
	dataplaneManager    dataplane.Manager
	sharedPoolRepo      repo.SharedPoolRepo
	tidbClusterRepo     repo.TiDBClusterRepo
	tidbGroupRepo       repo.TiDBGroupRepo
	templateVersionSvc  *templateversion.Service
	imageConfig         *config.ImageConfig
	clusterConfigClient clusterconfig.Client
	dataplaneClient     dplanemgrv1.DataPlaneServiceClient
	lbClient            dplanemgrv1.LoadBalancerServiceClient
	meteringConfig      *config.MeteringConfig
}

const ApplyTiProxyGroupActionName = "apply-tiproxy-group"

func NewApplyTiProxyGroupAction(
	sharedPoolRepo repo.SharedPoolRepo,
	tidbClusterRepo repo.TiDBClusterRepo,
	tidbGroupRepo repo.TiDBGroupRepo,
	dataplaneManager dataplane.Manager,
	clusterConfigClient clusterconfig.Client,
	dataplaneClient dplanemgrv1.DataPlaneServiceClient,
	lbClient dplanemgrv1.LoadBalancerServiceClient,
	templateVersionSvc *templateversion.Service,
	imageConfig *config.ImageConfig,
	meteringConfig *config.MeteringConfig,
) actions.Action {
	return &ApplyTiProxyGroupAction{
		sharedPoolRepo:      sharedPoolRepo,
		tidbClusterRepo:     tidbClusterRepo,
		tidbGroupRepo:       tidbGroupRepo,
		dataplaneManager:    dataplaneManager,
		dataplaneClient:     dataplaneClient,
		clusterConfigClient: clusterConfigClient,
		lbClient:            lbClient,
		templateVersionSvc:  templateVersionSvc,
		imageConfig:         imageConfig,
		meteringConfig:      meteringConfig,
	}
}

func (a *ApplyTiProxyGroupAction) Name() string {
	return ApplyTiProxyGroupActionName
}

func getTiProxyHPAName(comp *entity.Component) string {
	return "tiproxygroup-" + entity.GetComponentGroupName(comp)
}

func cleanupTiProxyHPA(ctx context.Context, remoteCli client.Client, sharedPoolId string, comp *entity.Component) error {
	hpa := &autoscalingv2.HorizontalPodAutoscaler{
		ObjectMeta: metav1.ObjectMeta{
			Name:      getTiProxyHPAName(comp),
			Namespace: entity.GetDataPlaneNamespace(sharedPoolId),
		},
	}
	if err := remoteCli.Delete(ctx, hpa); err != nil && !apierrors.IsNotFound(err) {
		return fmt.Errorf("failed to delete TiProxy HPA %s/%s: %w", hpa.Namespace, hpa.Name, err)
	}
	return nil
}

func (a *ApplyTiProxyGroupAction) Invoke(args actions.ActionArgs) error {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "apply tiproxy group action")

	_, err := args.GetString(workflow.ComponentIdsKey)
	if err != nil {
		return fmt.Errorf("target component id is required, but not found")
	}

	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	// load sharedPool
	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		return fmt.Errorf("failed to load sharedPool: %w", err)
	}

	sharedPoolStr, err := args.GetString(workflow.SharedPoolKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool: %w", err)
	}

	var tempSharedPool entity.SharedPool
	err = protojson.Unmarshal([]byte(sharedPoolStr), &tempSharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to unmarshal sharedPool", zap.Error(err))
		return fmt.Errorf("failed to unmarshal sharedPool: %w", err)
	}

	sharedPool.ComponentGroups = tempSharedPool.GetComponentGroups()
	if sharedPool.GetVersion() == "" {
		sharedPool.Version = tempSharedPool.GetVersion()
	}

	remoteCli, err := a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		tilog.Error(ctx, "failed to get remote client for shared pool", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
		return fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	var nodeVersionMap map[string]string

	for _, comp := range sharedPool.GetComponentGroups() {

		if comp.GetComponentType() != apiv1.ComponentType_TIPROXY {
			tilog.Info(ctx, "only applying tiproxy component group, skip other component types",
				zap.String("componentId", comp.GetId()),
				zap.String("componentType", entity.GetComponentTypeName(comp)),
			)
			continue
		}

		// check if the component is target component
		if !workflow.CheckIsTargetComponent(ctx, comp, args) {
			tilog.Info(ctx, "skip apply component", zap.String("componentId", comp.GetId()), zap.String("componentType", comp.GetComponentType().String()))
			continue
		}

		if skipShared, _ := args.GetString(workflow.SkipSharedTiProxyApplyKey); skipShared == workflow.SkipSharedTiProxyApplyValue && entity.HasSharedTiProxy(sharedPool) {
			tilog.Info(ctx, "skip shared tiproxy apply in create tidb cluster flow", zap.String("componentId", comp.GetId()))
			continue
		}

		if comp.GetPhase() == apiv1.Component_AVAILABLE {
			tilog.Info(ctx, "component is already available, skip applying", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)))
			continue
		}

		needDelete := comp.GetPhase() == apiv1.Component_DELETING
		if needDelete {
			ensureTiProxyDeleteDefaults(sharedPool, comp)
			if err := cleanupTiProxyHPA(ctx, remoteCli, sharedPool.GetId(), comp); err != nil {
				tilog.Error(ctx, "failed to cleanup tiproxy hpa", zap.String("componentId", comp.GetId()), zap.Error(err))
				return err
			}
		} else {
			// full component config from config center
			err = workflow.FullComponentConfigVersion(ctx, a.clusterConfigClient, a.meteringConfig, sharedPool, comp, "")
			if err != nil {
				tilog.Error(ctx, "failed to full component config", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
				return fmt.Errorf("failed to full component config: %w", err)
			}

			// full tiproxy public endpoint config
			err = setTiProxyPublicEndpointConfig(ctx, sharedPool, comp, a.tidbGroupRepo, a.lbClient, a.dataplaneClient)
			if err != nil {
				tilog.Error(ctx, "failed to set tiproxy public endpoint config", zap.String("componentId", comp.GetId()), zap.Error(err))
				return fmt.Errorf("failed to set tiproxy public endpoint config: %w", err)
			}

			if nodeVersionMap == nil {
				nodeVersionMap, err = getNodeVersions(ctx, a.dataplaneClient, sharedPool.GetDataplaneName())
				if err != nil {
					tilog.Error(ctx, "failed to get node versions for shared pool", zap.String("sharedPoolId", sharedPool.GetId()), zap.Error(err))
					return fmt.Errorf("failed to get node versions for shared pool: %w", err)
				}
			}
		}

		var tidbCluster *entity.TiDBCluster
		if !entity.HasSharedTiProxy(sharedPool) {
			if !needDelete {
				// get tidbcluster
				tidbCluster, err = a.tidbClusterRepo.Load(ctx, comp.GetId(), false)
				if err != nil {
					tilog.Error(ctx, "failed to load tidb cluster", zap.String("tidbClusterId", comp.GetId()), zap.Error(err))
					return fmt.Errorf("failed to load tidb cluster: %w", err)
				}
			}
			if comp.Labels == nil {
				comp.Labels = make(map[string]string)
			}
			comp.Labels[tioperator.LabelKeyKeyspace] = comp.GetId()
		}

		// build template args
		templArgs, err := buildCompTempArgs(a.imageConfig, comp, sharedPool, nodeVersionMap)
		if err != nil {
			tilog.Error(ctx, "failed to build component template args", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to build component template args: %w", err)
		}

		if err := workflow.AddAzureDfsArgs(sharedPool, templArgs); err != nil {
			tilog.Error(ctx, "failed to add azure args", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to add azure args: %w", err)
		}

		switch {
		case tidbCluster != nil:
			templArgs["UserClusterId"] = tidbCluster.GetId()
			templArgs["UserTenantId"] = tidbCluster.GetTenantId()
		case entity.HasSharedTiProxy(sharedPool):
			templArgs["UserClusterId"] = schemav1.MultiTenantTag
			templArgs["UserTenantId"] = schemav1.MultiTenantTag
		default:
			templArgs["UserClusterId"] = comp.GetId()
		}

		templateName, err := entity.GetComponentTemplateName(comp)
		if err != nil {
			tilog.Error(ctx, "failed to get component template name", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to get component template name: %w", err)
		}

		templateVersion := comp.GetTemplateVersion()
		if needDelete {
			templateVersion = ""
		}

		err = templateExecuteWithVersionResolver(
			ctx, remoteCli, templateName, templateVersion, templArgs,
			needDelete, a.templateVersionSvc,
		)
		if err != nil {
			tilog.Error(ctx, "failed to execute template", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to execute template: %w", err)
		}
	}

	return nil
}

func ensureTiProxyDeleteDefaults(sharedPool *entity.SharedPool, comp *entity.Component) {
	if comp.GetVersion() == "" {
		comp.Version = sharedPool.GetVersion()
	}
	if comp.NodeQuota == nil {
		comp.NodeQuota = &entity.NodeQuota{}
	}
	if comp.Configuration == nil {
		comp.Configuration = &entity.ComponentConfiguration{}
	}
	if comp.GetConfiguration().GetImageRepo() == "" {
		comp.Configuration.ImageRepo = entity.GetComponentTypeName(comp)
	}
	if comp.GetConfiguration().GetImageTag() == "" {
		comp.Configuration.ImageTag = sharedPool.GetVersion()
	}
}

func setTiProxyPublicEndpointConfig(ctx context.Context, sharedPool *entity.SharedPool, comp *entity.Component, tidbGroupRepo repo.TiDBGroupRepo, lbClient dplanemgrv1.LoadBalancerServiceClient, dataplaneClient dplanemgrv1.DataPlaneServiceClient) error {
	publicLbCidrs := make([]string, 0)

	if entity.HasSharedTiProxy(sharedPool) && comp.GetId() == entity.ResolveTiProxyID(sharedPool, "") {
		dp, err := dataplaneClient.GetDataPlane(ctx, &dplanemgrv1.GetDataPlaneRequest{
			DataPlaneId: sharedPool.GetDataplaneName(),
		})
		if err != nil {
			tilog.Error(ctx, "failed to get dataplane for public subnet cidrs", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
			return fmt.Errorf("failed to get dataplane for public subnet cidrs: %w", err)
		}
		publicLbCidrs = dp.GetVpc().GetPublicSubnetCidrs()
		sort.Strings(publicLbCidrs)
	} else {
		tidbGroups, err := tidbGroupRepo.List(ctx, nil, nil, []string{comp.GetId()}, nil, false)
		if err != nil {
			tilog.Error(ctx, "failed to list tidb groups for tiproxy", zap.String("componentId", comp.GetId()), zap.Error(err))
			return fmt.Errorf("failed to list tidb groups for tiproxy: %w", err)
		}

		for _, tg := range tidbGroups {
			lbcidrs, err := getTiDBCidrs(ctx, lbClient, tg, "public")
			if err != nil {
				tilog.Error(ctx, "failed to get load balancer cidrs for tiproxy", zap.String("tidbGroupId", tg.GetId()), zap.Error(err))
				return fmt.Errorf("failed to get load balancer cidrs for tiproxy: %w", err)
			}
			publicLbCidrs = append(publicLbCidrs, lbcidrs...)
		}
		sort.Strings(publicLbCidrs)
	}

	conf := schemav1.NewTomlConfig(map[string]any{})
	if err := conf.UnmarshalTOML([]byte(comp.GetConfiguration().GetConfigurationFileContent())); err != nil {
		return status.Errorf(codes.InvalidArgument, "failed to unmarshal tiproxy-%s configuration: %s", comp.GetId(), err)
	}
	conf.Set("proxy.public-endpoints", publicLbCidrs)
	updatedConfigContent, err := conf.MarshalTOML()
	if err != nil {
		return fmt.Errorf("failed to marshal updated tiproxy-%s configuration: %w", comp.GetId(), err)
	}
	comp.GetConfiguration().ConfigurationFileContent = string(updatedConfigContent)
	return nil
}
