package action

import (
	"context"
	"fmt"

	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/cert"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/k8s"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"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/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type ApplyTiCiAction struct {
	dataplaneManager   dataplane.Manager
	sharedPoolRepo     repo.SharedPoolRepo
	templateVersionSvc *templateversion.Service
	imageConfig        *config.ImageConfig
	dataplaneClient    dplanemgrv1.DataPlaneServiceClient
}

const ApplyTiCiActionName = "apply-tici"

func NewApplyTiCiAction(
	sharedPoolRepo repo.SharedPoolRepo,
	dataplaneManager dataplane.Manager,
	dataplaneClient dplanemgrv1.DataPlaneServiceClient,
	templateVersionSvc *templateversion.Service,
	imageConfig *config.ImageConfig,
) actions.Action {
	return &ApplyTiCiAction{
		sharedPoolRepo:     sharedPoolRepo,
		dataplaneManager:   dataplaneManager,
		dataplaneClient:    dataplaneClient,
		templateVersionSvc: templateVersionSvc,
		imageConfig:        imageConfig,
	}
}

func (a *ApplyTiCiAction) Name() string {
	return ApplyTiCiActionName
}

func (a *ApplyTiCiAction) Invoke(args actions.ActionArgs) error {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "apply tici action")

	enableTiCI, _ := args.GetString(workflow.EnableTiCIKey)
	if enableTiCI != workflow.EnableTiCIValue {
		tilog.Info(ctx, "tici is not enabled, skipping apply tici action")
		return nil
	}

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

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

	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)
	}

	nodeSpec, nodeVersion, err := a.getSmallestNodeSpec(ctx, sharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to get smallest node spec", zap.String("sharedPoolId", sharedPool.GetId()), zap.Error(err))
		return fmt.Errorf("failed to get smallest node spec: %w", err)
	}

	// Deploy TiCI Meta (using Deployment)
	if err := a.applyTiCiMeta(ctx, remoteCli, sharedPool, nodeSpec, nodeVersion); err != nil {
		return fmt.Errorf("failed to apply tici meta: %w", err)
	}

	// Deploy TiCI Worker (using StatefulSet)
	if err := a.applyTiCiWorker(ctx, remoteCli, sharedPool, nodeSpec, nodeVersion); err != nil {
		return fmt.Errorf("failed to apply tici worker: %w", err)
	}

	return nil
}

func (a *ApplyTiCiAction) getSmallestNodeSpec(ctx context.Context, sharedPool *entity.SharedPool) (*entity.NodeSpec, string, error) {
	nodePools, err := a.dataplaneClient.ListNodePools(ctx, &dplanemgrv1.ListNodePoolsRequest{
		DataPlaneId: sharedPool.GetDataplaneName(),
	})
	if err != nil {
		return nil, "", err
	}

	if len(nodePools.GetNodePools()) == 0 {
		return nil, "", fmt.Errorf("no node pools found in dataplane %s", sharedPool.GetDataplaneName())
	}

	var smallestPool *dplanemgrv1.NodePool
	for _, np := range nodePools.GetNodePools() {
		// Skip invalid node pools (cpu_core=0 or memory_gi=0)
		if np.GetCpuCore() == 0 || np.GetMemoryGi() == 0 {
			continue
		}

		// Skip node pools with local disk - TiCI doesn't support local disk
		if np.GetWithLocalDisk() {
			continue
		}

		// Skip node pools without available resources
		if np.GetAvailableCpuMilliCore() == 0 || np.GetAvailableMemoryMi() == 0 {
			tilog.Warn(ctx, "skip node pool without available resources",
				zap.String("instanceType", np.GetInstanceType()),
				zap.Uint32("cpuCore", np.GetCpuCore()),
				zap.Uint32("memoryGi", np.GetMemoryGi()))
			continue
		}

		if smallestPool == nil {
			smallestPool = np
			continue
		}
		if np.GetCpuCore() < smallestPool.GetCpuCore() ||
			(np.GetCpuCore() == smallestPool.GetCpuCore() && np.GetMemoryGi() < smallestPool.GetMemoryGi()) {
			smallestPool = np
		}
	}

	if smallestPool == nil {
		return nil, "", fmt.Errorf("no valid node pools found in dataplane %s (all pools either have local disk, zero resources, or missing available resources)", sharedPool.GetDataplaneName())
	}

	// Populate NodeSpec including available resources so resource calculation
	// (SetResourceReady) can compute requests/limits correctly.
	// TiCI does not use local disk, so WithLocalDisk and LocalDiskSizeGi are always false/0.
	nodeSpec := &entity.NodeSpec{
		Cpu:               smallestPool.GetCpuCore(),
		MemoryGi:          smallestPool.GetMemoryGi(),
		AvailableCpu:      smallestPool.GetAvailableCpuMilliCore() / 1000, // convert millicores -> cores
		AvailableMemoryMi: smallestPool.GetAvailableMemoryMi(),
		WithLocalDisk:     false,
		LocalDiskSizeGi:   0,
	}

	return nodeSpec, smallestPool.GetK8SVersion(), nil
}

func (a *ApplyTiCiAction) createDefaultTiCiComponent(sharedPool *entity.SharedPool, componentType apiv1.ComponentType, nodeSpec *entity.NodeSpec) (*entity.Component, error) {
	// Use the default component ID (PD component ID) from SharedPool
	componentId := sharedPool.GetDefaultComponenId()
	if componentId == "" {
		return nil, fmt.Errorf("failed to get default component ID from shared pool, PD component not found")
	}

	// Set different MaxNodeCount based on component type
	maxNodeCount := uint32(10) // Default for Worker
	if componentType == apiv1.ComponentType_TICI_META {
		maxNodeCount = 1 // Meta is a singleton service
	}

	return &entity.Component{
		Id:            componentId,
		Phase:         apiv1.Component_CREATING,
		ComponentType: componentType,
		NodeQuota: &entity.NodeQuota{
			NodeCount:    0,
			MinNodeCount: 0,
			MaxNodeCount: maxNodeCount,
			NodeSpec:     nodeSpec,
		},
	}, nil
}

func (a *ApplyTiCiAction) createTiCiCert(ctx context.Context, remoteCli client.Client, sharedPool *entity.SharedPool, comp *entity.Component, componentType apiv1.ComponentType) error {
	compTypeName := entity.GetComponentTypeName(comp)
	compCert := cert.ComponentCert(compTypeName, comp.GetId(), sharedPool.GetDataPlaneNamespace())
	if err := k8s.CreateIfNotExist(ctx, remoteCli, compCert); err != nil {
		tilog.Error(ctx, "failed to create tici cert", zap.String("componentType", componentType.String()), zap.Error(err))
		return fmt.Errorf("failed to create tici cert: %w", err)
	}
	tilog.Info(ctx, "tici cert created", zap.String("certName", compCert.Name), zap.String("namespace", compCert.Namespace))
	return nil
}

func (a *ApplyTiCiAction) applyTiCiMeta(ctx context.Context, remoteCli client.Client, sharedPool *entity.SharedPool, nodeSpec *entity.NodeSpec, nodeVersion string) error {
	comp, err := a.createDefaultTiCiComponent(sharedPool, apiv1.ComponentType_TICI_META, nodeSpec)
	if err != nil {
		tilog.Error(ctx, "failed to create default tici meta component", zap.Error(err))
		return fmt.Errorf("failed to create default tici meta component: %w", err)
	}

	err = a.createTiCiCert(ctx, remoteCli, sharedPool, comp, apiv1.ComponentType_TICI_META)
	if err != nil {
		tilog.Error(ctx, "failed to create cert for tici meta", zap.Error(err))
		return fmt.Errorf("failed to create cert for tici meta: %w", err)
	}

	nodeVersionMap := map[string]string{
		entity.GetComponentNodeSpec(nodeSpec): nodeVersion,
	}
	templArgs, err := buildCompTempArgs(a.imageConfig, comp, sharedPool, nodeVersionMap)
	if err != nil {
		tilog.Error(ctx, "failed to build component template args for tici meta", zap.Error(err))
		return fmt.Errorf("failed to build component template args for tici meta: %w", err)
	}

	// Set TiCI Meta specific parameters
	templArgs["Image"] = fmt.Sprintf("%s/%s/tici:master", a.imageConfig.Registry, a.imageConfig.RepoPrefix)
	templArgs["ServerPort"] = "8500"
	templArgs["StatusPort"] = "8501"
	templArgs["PDAddress"] = sharedPool.GetPdAddress()
	templArgs["DfsPrefix"] = sharedPool.GetTiCIPrefix()
	templArgs["DfsBucket"] = sharedPool.GetObjectStorage().GetBucket()
	templArgs["DfsRegion"] = sharedPool.GetObjectStorage().GetRegion()
	templArgs["VerifiedCN"] = utils.FnvHash(sharedPool.GetDataPlaneNamespace())
	templArgs["DfsEndpoint"] = entity.GetObjectStorageAddress(sharedPool)
	if err := workflow.AddAzureDfsArgs(sharedPool, templArgs); err != nil {
		return err
	}

	// Find TiDB Worker component to get its service name
	for _, workerComp := range sharedPool.GetComponentGroups() {
		if workerComp.GetComponentType() == apiv1.ComponentType_TIDB_WORKER {
			templArgs["TiDBWorkerAddress"] = fmt.Sprintf("%s.%s.svc.cluster.local",
				entity.GetComponentSvcName(sharedPool, workerComp),
				sharedPool.GetDataPlaneNamespace())
			break
		}
	}

	// Create ConfigMap
	err = templateExecuteWithVersionResolver(ctx, remoteCli, "tici_meta_configmap", "", templArgs, false, a.templateVersionSvc)
	if err != nil {
		tilog.Error(ctx, "failed to execute tici meta config map template", zap.Error(err))
		return fmt.Errorf("failed to execute tici meta config map template: %w", err)
	}

	// Create Service
	err = templateExecuteWithVersionResolver(ctx, remoteCli, "tici_svc", "", templArgs, false, a.templateVersionSvc)
	if err != nil {
		tilog.Error(ctx, "failed to execute tici meta service template", zap.Error(err))
		return fmt.Errorf("failed to execute tici meta service template: %w", err)
	}

	// Create Deployment
	err = templateExecuteWithVersionResolver(ctx, remoteCli, "tici_meta", "", templArgs, false, a.templateVersionSvc)
	if err != nil {
		tilog.Error(ctx, "failed to execute tici meta deployment template", zap.Error(err))
		return fmt.Errorf("failed to execute tici meta deployment template: %w", err)
	}

	tilog.Info(ctx, "successfully applied tici meta component")
	return nil
}

func (a *ApplyTiCiAction) applyTiCiWorker(ctx context.Context, remoteCli client.Client, sharedPool *entity.SharedPool, nodeSpec *entity.NodeSpec, nodeVersion string) error {
	comp, err := a.createDefaultTiCiComponent(sharedPool, apiv1.ComponentType_TICI_WORKER, nodeSpec)
	if err != nil {
		tilog.Error(ctx, "failed to create default tici worker component", zap.Error(err))
		return fmt.Errorf("failed to create default tici worker component: %w", err)
	}

	err = a.createTiCiCert(ctx, remoteCli, sharedPool, comp, apiv1.ComponentType_TICI_WORKER)
	if err != nil {
		tilog.Error(ctx, "failed to create cert for tici worker", zap.Error(err))
		return fmt.Errorf("failed to create cert for tici worker: %w", err)
	}

	// Create VAC for TiCI worker storage
	storage, err := a.getTiCiWorkerStorage(sharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to get tici worker storage config", zap.Error(err))
		return fmt.Errorf("failed to get tici worker storage config: %w", err)
	}

	if err := a.createTiCiVAC(ctx, remoteCli, sharedPool, storage); err != nil {
		tilog.Error(ctx, "failed to create VAC for tici worker", zap.Error(err))
		return fmt.Errorf("failed to create VAC for tici worker: %w", err)
	}

	nodeVersionMap := map[string]string{
		entity.GetComponentNodeSpec(nodeSpec): nodeVersion,
	}
	templArgs, err := buildCompTempArgs(a.imageConfig, comp, sharedPool, nodeVersionMap)
	if err != nil {
		tilog.Error(ctx, "failed to build component template args for tici worker", zap.Error(err))
		return fmt.Errorf("failed to build component template args for tici worker: %w", err)
	}

	// Set TiCI Worker specific parameters
	templArgs["Image"] = fmt.Sprintf("%s/%s/tici:master", a.imageConfig.Registry, a.imageConfig.RepoPrefix)
	templArgs["ServerPort"] = "8510"
	templArgs["StatusPort"] = "8511"
	templArgs["PDAddress"] = sharedPool.GetPdAddress()
	templArgs["DfsPrefix"] = sharedPool.GetTiCIPrefix()
	templArgs["DfsBucket"] = sharedPool.GetObjectStorage().GetBucket()
	templArgs["DfsRegion"] = sharedPool.GetObjectStorage().GetRegion()
	templArgs["VerifiedCN"] = utils.FnvHash(sharedPool.GetDataPlaneNamespace())
	templArgs["DfsEndpoint"] = entity.GetObjectStorageAddress(sharedPool)
	if err := workflow.AddAzureDfsArgs(sharedPool, templArgs); err != nil {
		return err
	}

	// Storage configuration for StatefulSet
	templArgs["StorageSize"] = "300"
	templArgs["StorageClassName"] = workflow.GenStorageClassName(sharedPool, storage)
	templArgs["VolumeAttributesClassName"] = workflow.GenVolumeAttributesClassName(storage)

	// Create ConfigMap
	err = templateExecuteWithVersionResolver(ctx, remoteCli, "tici_worker_configmap", "", templArgs, false, a.templateVersionSvc)
	if err != nil {
		tilog.Error(ctx, "failed to execute tici worker config map template", zap.Error(err))
		return fmt.Errorf("failed to execute tici worker config map template: %w", err)
	}

	// Create Service
	err = templateExecuteWithVersionResolver(ctx, remoteCli, "tici_svc", "", templArgs, false, a.templateVersionSvc)
	if err != nil {
		tilog.Error(ctx, "failed to execute tici worker service template", zap.Error(err))
		return fmt.Errorf("failed to execute tici worker service template: %w", err)
	}

	// Create StatefulSet
	err = templateExecuteWithVersionResolver(ctx, remoteCli, "tici_worker", "", templArgs, false, a.templateVersionSvc)
	if err != nil {
		tilog.Error(ctx, "failed to execute tici worker statefulset template", zap.Error(err))
		return fmt.Errorf("failed to execute tici worker statefulset template: %w", err)
	}

	tilog.Info(ctx, "successfully applied tici worker component")
	return nil
}

func (a *ApplyTiCiAction) getTiCiWorkerStorage(sharedPool *entity.SharedPool) (*entity.Storage, error) {
	switch sharedPool.GetProvider().GetType() {
	case apiv1.ProviderType_AWS:
		return &entity.Storage{
			Type:          entity.GP3,
			Iops:          5000,
			ThroughputMbs: 400,
		}, nil
	case apiv1.ProviderType_ALICLOUD:
		return &entity.Storage{
			Type:          entity.CloudAuto,
			Iops:          400,
			ThroughputMbs: 0,
		}, nil
	default:
		return nil, fmt.Errorf("unsupported provider type %s for creating TiCI storage", sharedPool.GetProvider().GetType().String())
	}
}

func (a *ApplyTiCiAction) createTiCiVAC(ctx context.Context, remoteCli client.Client, sharedPool *entity.SharedPool, storage *entity.Storage) error {
	vacName := workflow.GenVolumeAttributesClassName(storage)

	var driverName string
	switch sharedPool.GetProvider().GetType() {
	case apiv1.ProviderType_ALICLOUD:
		driverName = alicloudDriverName
		vac := buildVolumeAttributesClassV1(vacName, driverName, storage)
		if err := k8s.CreateIfNotExist(ctx, remoteCli, vac); err != nil {
			tilog.Error(ctx, "failed to create volume attributes class", zap.String("vac", vac.String()), zap.Error(err))
			return fmt.Errorf("failed to create volume attributes class: %w", err)
		}
	case apiv1.ProviderType_AWS:
		driverName = awsDriverName
		vac := buildVolumeAttributesClassV1Beta1(vacName, driverName, storage)
		if err := k8s.CreateIfNotExist(ctx, remoteCli, vac); err != nil {
			tilog.Error(ctx, "failed to create volume attributes class", zap.String("vac", vac.String()), zap.Error(err))
			return fmt.Errorf("failed to create volume attributes class: %w", err)
		}
	default:
		return fmt.Errorf("unsupported provider type %s for creating volume attributes class", sharedPool.GetProvider().GetType().String())
	}

	tilog.Info(ctx, "volume attributes class created", zap.String("vac", vacName))
	return nil
}
