package action

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	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/cloudtags"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	mysqlschema "github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	"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 CreateDataplaneActionName = "create-dataplane"

type CreateDataplaneAction struct {
	client    dplanemgrv1.DataPlaneServiceClient
	bizConfig *config.BizConfig
	plan      string
}

func NewCreateDataplaneAction(client dplanemgrv1.DataPlaneServiceClient, bizConfig *config.BizConfig, plan string) actions.AsyncAction {
	return &CreateDataplaneAction{
		client:    client,
		bizConfig: bizConfig,
		plan:      plan,
	}
}

func (a *CreateDataplaneAction) Name() string {
	return CreateDataplaneActionName
}

func (a *CreateDataplaneAction) Start(args actions.ActionArgs) error {
	ctx := utils.GenerateContext(args)
	dataplaneId, err := args.GetString(workflow.DataplaneIdKey)
	if err != nil {
		return fmt.Errorf("failed to get dataplane id: %w", err)
	}

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

	serviceCidr, _ := args.GetString(workflow.DataplaneServiceCidrKey)
	if serviceCidr == "" {
		tilog.Warn(ctx, "dataplane service cidr is empty, use default value", zap.String("dataplaneId", dataplaneId))
	}

	accountId, err := args.GetString(workflow.DataplaneAccountIdKey)
	if err != nil {
		return fmt.Errorf("failed to get dataplane account id: %w", err)
	}

	var pca *dplanemgrv1.DataPlane_PCA
	if pcaStr, err := args.GetString(workflow.DataplanePCAKey); err == nil {
		tilog.Info(ctx, "dataplane pca", zap.String("pca", pcaStr))
		pca = new(dplanemgrv1.DataPlane_PCA)
		if err := protojson.Unmarshal([]byte(pcaStr), pca); err != nil {
			return fmt.Errorf("failed to unmarshal dataplane pca: %w", err)
		}
	}

	var nodeZones []string
	if nodeZonesStr, err := args.GetString(workflow.DataplaneNodeZonesKey); err == nil {
		if err := json.Unmarshal([]byte(nodeZonesStr), &nodeZones); err != nil {
			return fmt.Errorf("failed to unmarshal dataplane node zones: %w", err)
		}
	}
	haType := dplanemgrv1.DataPlane_HIGH_AVAILABILITY_TYPE_UNSPECIFIED
	if haTypeStr, err := args.GetString(workflow.DataplaneHATypeKey); err == nil {
		value, ok := dplanemgrv1.DataPlane_HighAvailabilityType_value[haTypeStr]
		if !ok {
			return fmt.Errorf("invalid dataplane high availability type: %s", haTypeStr)
		}
		haType = dplanemgrv1.DataPlane_HighAvailabilityType(value)
	}

	tilog.Info(ctx, "create dataplane action start", zap.String("dataplaneId", dataplaneId))
	{
		in := new(dplanemgrv1.GetDataPlaneRequest)
		in.DataPlaneId = dataplaneId
		if _, err := a.client.GetDataPlane(ctx, in); err == nil {
			tilog.Info(ctx, "dataplane already exists", zap.String("dataplaneId", dataplaneId))
			return nil
		}
	}
	{
		in := new(dplanemgrv1.CreateDataPlaneRequest)
		in.DataPlaneId = dataplaneId
		in.DataPlane = new(dplanemgrv1.DataPlane)
		in.DataPlane.Vpc = new(dplanemgrv1.DataPlane_Vpc)
		in.DataPlane.AccountId = accountId
		in.DataPlane.Vpc.Cidr = cidr
		in.DataPlane.Kubernetes = new(dplanemgrv1.DataPlane_Kubernetes)
		in.DataPlane.Kubernetes.ServiceCidr = serviceCidr
		in.DataPlane.NodeZones = nodeZones
		in.DataPlane.HighAvailabilityType = haType
		in.DataPlane.Pca = pca
		if enabled, _ := args.GetString(workflow.EnableCapacityPlannerKey); enabled != "" {
			in.EnableCapacityPlanner, err = strconv.ParseBool(enabled)
			if err != nil {
				return fmt.Errorf("invalid enable capacity planner value %q: %w", enabled, err)
			}
		}
		if a.bizConfig != nil {
			in.DataPlane.ResourceGroupName = strings.TrimSpace(a.bizConfig.AzureResourceGroupName)
		}
		// TODO(shenjun): set the tags
		in.DataPlane.Tags = make(map[string]string)
		if tenantId, _ := args.GetString(workflow.TenantIdKey); tenantId != "" {
			in.DataPlane.Tags[schemav1.LabelUserTenantKey] = tenantId
		} else {
			in.DataPlane.Tags[schemav1.LabelUserTenantKey] = schemav1.MultiTenantTag
		}
		if sharedPoolId, _ := args.GetString(workflow.SharedPoolIdKey); sharedPoolId != "" {
			tilog.Warn(ctx, "not get tenant id from args or shared pool")
			in.DataPlane.Tags[schemav1.LabelSharedPoolKey] = sharedPoolId
		}
		in.DataPlane.Tags[schemav1.LabelServiceTypeKey] = a.servicePlanFromArgs(args)
		if sharedPoolStr, err := args.GetString(workflow.SharedPoolKey); err == nil && sharedPoolStr != "" {
			var sharedPool entity.SharedPool
			if err := protojson.Unmarshal([]byte(sharedPoolStr), &sharedPool); err != nil {
				return fmt.Errorf("failed to unmarshal sharedpool: %w", err)
			}
			in.DataPlane.Tags = mergeStringMaps(in.DataPlane.Tags, cloudtags.EncodeForDataPlane(sharedPool.GetCloudResourceTags()))
		}
		if _, err := a.client.CreateDataPlane(ctx, in); err != nil {
			if status.Code(err) == codes.AlreadyExists {
				// if the dataplane already exists, we can ignore the error
				return nil
			}
			tilog.Error(ctx, "failed to create dataplane", zap.String("in", in.String()), zap.Error(err))
			return err
		}
	}

	return nil
}

func mergeStringMaps(maps ...map[string]string) map[string]string {
	out := map[string]string{}
	for _, m := range maps {
		for k, v := range m {
			out[k] = v
		}
	}
	return out
}

func (a *CreateDataplaneAction) servicePlanFromArgs(args actions.ActionArgs) string {
	if capacityModeStr, _ := args.GetString(workflow.CapacityModeKey); capacityModeStr == apiv1.CapacityMode_TOPOLOGY_DRIVEN.String() {
		return entity.ResolveServicePlan(nil, apiv1.CapacityMode_TOPOLOGY_DRIVEN, a.plan)
	}
	if a.plan == entity.PremiumServicePlan {
		if deployType, _ := args.GetString(workflow.SharedPoolDeployTypeKey); deployType == mysqlschema.SharedPoolDeployTypeSharedTiProxy {
			return entity.EssentialServicePlan
		}
	}
	return entity.ResolveServicePlan(nil, apiv1.CapacityMode_RCU_DRIVEN, a.plan)
}

func (a *CreateDataplaneAction) CheckFinish(args actions.ActionArgs) (bool, error) {
	ctx := utils.GenerateContext(args)
	dataplaneId, err := args.GetString(workflow.DataplaneIdKey)
	if err != nil {
		return false, fmt.Errorf("failed to get dataplane id: %w", err)
	}
	tilog.Info(ctx, "create dataplane check finish, wait dataplane to be ready", zap.String("dataplaneId", dataplaneId))
	in := new(dplanemgrv1.GetDataPlaneRequest)
	in.DataPlaneId = dataplaneId

	out, err := a.client.GetDataPlane(ctx, in)
	if err != nil {
		tilog.Error(ctx, "failed to get dataplane", zap.Error(err))
		return false, err
	}

	if dpstatus := out.GetStatus(); dpstatus == dplanemgrv1.DataPlane_Active {
		// this is a debug log to check the out has k8s name.
		// dataplane mgr team think dataplane mgr return the k8s name.
		tilog.Info(ctx, "dataplane is active",
			zap.String("id", out.GetId()),
			zap.String("status", out.GetStatus().String()),
			zap.String("out", out.String()))
		return true, nil
	} else if dpstatus == dplanemgrv1.DataPlane_Failed {
		tilog.Error(ctx, "dataplane is not active", zap.String("id", out.GetId()), zap.String("status", out.GetStatus().String()))
		return true, fmt.Errorf("dataplane %s has status %s", out.GetId(), dpstatus)
	}
	// just retry
	tilog.Info(ctx, "dataplane is not active", zap.String("id", out.GetId()), zap.String("status", out.GetStatus().String()))
	return false, nil
}
