package ngoperation

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

	clusterng "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/clients/mgmthttpcli"
	"github.com/tidbcloud/devops/pkg/infra/errs"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
)

var (
	ActionSetCassandra opent.ActionMetadata = opent.ActionMetadata{
		Name: "set_cassandra",
		Type: opent.ActionTypeAuto,
	}
)

var cassandraProxyComponentType = clusterng.ComponentType_name[int32(clusterng.ComponentType_CASSANDRA_PROXY)]

const (
	cassandraScaleModeParam opent.ParamName = "cassandra_scale_mode"
	cassandraScaleModeNoop  string          = "noop"
	cassandraScaleModeScale string          = "scale"
)

type cassandraOperationMode string

const (
	cassandraOperationModeScale  cassandraOperationMode = "scale"
	cassandraOperationModeEnable cassandraOperationMode = "enable"

	operationScaleCassandra  = "ScaleCassandra"
	operationEnableCassandra = "EnableCassandra"
)

type setCassandra struct {
	actions.BaseAction
	comps *actions.Components
}

func NewSetCassandra(comps *actions.Components) actions.AutoAction {
	return &setCassandra{comps: comps}
}

func (a *setCassandra) Metadata() opent.ActionMetadata {
	return ActionSetCassandra
}

func (a *setCassandra) Advisory(ctx context.Context, instance *opent.Instance) ([]opent.Advisory, error) {
	req, cid, sharedPoolID, err := a.getParam(instance)
	if err != nil {
		return nil, err
	}

	target := actions.ScaleTarget{ComponentType: "Cassandra Proxy"}
	if req.GetNodeQuota() != nil {
		target.TargetNodeCount = req.GetNodeQuota().GetNodeCount()
		if req.GetNodeQuota().GetNodeSpec() != nil {
			target.TargetCPUCores = req.GetNodeQuota().GetNodeSpec().GetCpu()
			target.TargetMemoryGi = req.GetNodeQuota().GetNodeSpec().GetMemoryGi()
		}
	}

	sp, err := a.getSharedPool(ctx, cid, sharedPoolID)
	if err == nil {
		if comp := findCassandraProxyComponent(sp, cid); comp != nil && comp.NodeQuota != nil {
			target.CurrentNodeCount = comp.NodeQuota.NodeCount
			target.TargetNodeCount = cassandraAdvisoryTargetNodeCount(comp.NodeQuota.NodeCount, req.GetNodeQuota())
			if comp.NodeQuota.NodeSpec != nil {
				target.CurrentCPUCores = comp.NodeQuota.NodeSpec.Cpu
				target.CurrentMemoryGi = comp.NodeQuota.NodeSpec.MemoryGi
			}
		}
	}

	return actions.ClusterScaleAdvisory(ctx, log.FromContextOrDef(ctx, a.comps.Logger), a.comps.ClinicCli, a.comps.GeminiCli, cid, target)
}

func (a *setCassandra) PreCheck(ctx context.Context, instance *opent.Instance) error {
	req, cid, sharedPoolID, err := a.getParam(instance)
	if err != nil {
		return err
	}

	mode, err := getCassandraOperationMode(instance)
	if err != nil {
		return err
	}

	switch mode {
	case cassandraOperationModeScale:
		status, err := a.requireScalableCassandraProxy(ctx, cid, sharedPoolID, req.GetForce())
		if err != nil {
			return err
		}
		scaleMode, scaleDirection, err := a.resolveScaleMode(ctx, cid, sharedPoolID, req.GetNodeQuota(), req.Image != nil)
		if err != nil {
			return err
		}
		if status.GetPhase() == clusterng.Component_UNAVAILABLE {
			scaleMode = cassandraScaleModeScale
		}
		setCassandraDerivedParams(instance, scaleMode, scaleDirection)
		return nil
	case cassandraOperationModeEnable:
		return a.requireMissingCassandraProxy(ctx, cid, sharedPoolID)
	default:
		return fmt.Errorf("unsupported cassandra operation mode %s", mode)
	}
}

func (a *setCassandra) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	req, cid, sharedPoolID, err := a.getParam(instance)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	mode, err := getCassandraOperationMode(instance)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	switch mode {
	case cassandraOperationModeScale:
		status, err := a.requireScalableCassandraProxy(ctx, cid, sharedPoolID, req.GetForce())
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		scaleMode, scaleDirection, err := a.resolveScaleMode(ctx, cid, sharedPoolID, req.GetNodeQuota(), req.Image != nil)
		if err != nil {
			return actions.FailedActionResult(err), nil
		}
		if status.GetPhase() == clusterng.Component_UNAVAILABLE {
			scaleMode = cassandraScaleModeScale
		}
		setCassandraDerivedParams(instance, scaleMode, scaleDirection)
		if scaleMode == cassandraScaleModeNoop {
			return actions.NewActionResult(opent.ActionStatusSucceeded, "cassandra proxy already matches desired quota", nil), nil
		}
	case cassandraOperationModeEnable:
		if err = a.requireMissingCassandraProxy(ctx, cid, sharedPoolID); err != nil {
			return actions.FailedActionResult(err), nil
		}
	default:
		return actions.FailedActionResult(fmt.Errorf("unsupported cassandra operation mode %s", mode)), nil
	}

	resp, err := a.setCassandraProxy(ctx, cid, sharedPoolID, req)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	return actions.RunningActionResult(fmt.Sprintf("sent request to set cassandra proxy, phase is '%s'", resp.GetPhase()), nil), nil
}

func (a *setCassandra) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	cid, err := actions.GetClusterIDStr(instance)
	if err != nil {
		return nil, err
	}
	sharedPoolID, err := actions.GetResourcePoolID(instance)
	if err != nil {
		return nil, err
	}

	resp, err := a.getCassandraProxy(ctx, cid, sharedPoolID)
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}

	switch resp.GetPhase() {
	case clusterng.Component_AVAILABLE:
		return actions.NewActionResult(
			opent.ActionStatusSucceeded,
			fmt.Sprintf("cassandra proxy is AVAILABLE, internal endpoint: %s, privatelink DNS: %s", resp.GetInternalEndpoint(), resp.GetPrivatelinkDnsName()),
			nil,
		), nil
	case clusterng.Component_CREATING, clusterng.Component_MODIFYING:
		return actions.RunningActionResult(fmt.Sprintf("cassandra proxy phase is '%s'", resp.GetPhase()), nil), nil
	case clusterng.Component_UNAVAILABLE, clusterng.Component_DELETING:
		return actions.NewActionResult(opent.ActionStatusFailed, fmt.Sprintf("cassandra proxy phase is '%s'", resp.GetPhase()), nil), nil
	default:
		return actions.NewActionResult(opent.ActionStatusUnknown, fmt.Sprintf("unsupported cassandra proxy phase '%s'", resp.GetPhase()), nil), nil
	}
}

func (a *setCassandra) getParam(instance *opent.Instance) (*clusterng.SetCassandraProxyRequest, string, string, error) {
	cid, err := actions.GetClusterIDStr(instance)
	if err != nil {
		return nil, "", "", err
	}
	sharedPoolID, err := actions.GetResourcePoolID(instance)
	if err != nil {
		return nil, "", "", err
	}

	minNodeCount, err := getRequiredUint32Param(instance, opent.ParamMinNodeCount)
	if err != nil {
		return nil, "", "", err
	}
	maxNodeCount, err := getRequiredUint32Param(instance, opent.ParamMaxNodeCount)
	if err != nil {
		return nil, "", "", err
	}
	cpuCores, err := getRequiredUint32Param(instance, opent.ParamCpuCores)
	if err != nil {
		return nil, "", "", err
	}
	memoryGi, err := getRequiredUint32Param(instance, opent.ParamMemoryGi)
	if err != nil {
		return nil, "", "", err
	}

	if minNodeCount > maxNodeCount {
		return nil, "", "", fmt.Errorf("min_node_count must be less than or equal to max_node_count")
	}
	if cpuCores < 2 {
		return nil, "", "", fmt.Errorf("cpu_cores must be at least 2")
	}
	if memoryGi < 4 {
		return nil, "", "", fmt.Errorf("memory_gi must be at least 4")
	}

	req := &clusterng.SetCassandraProxyRequest{
		ClusterId: cid,
		Force:     instance.Operation == operationScaleCassandra && instance.Params[opent.ParamForceScale] == "true",
		NodeQuota: &clusterng.NodeQuota{
			NodeSpec: &clusterng.NodeSpec{
				Cpu:      cpuCores,
				MemoryGi: memoryGi,
			},
			NodeCount:    minNodeCount,
			MinNodeCount: minNodeCount,
			MaxNodeCount: maxNodeCount,
		},
	}

	if image := strings.TrimSpace(instance.Params[opent.ParamImage]); image != "" {
		req.Image = &image
	}

	return req, cid, sharedPoolID, nil
}

func (a *setCassandra) cassandraRegionServer(ctx context.Context, sharedPoolID string) (mgmthttpcli.NextGenRegionServer, error) {
	if a.comps == nil || a.comps.MgmtHTTP == nil {
		return nil, fmt.Errorf("management HTTP client is required for cassandra proxy operations")
	}
	if a.comps.CoreSvc == nil || a.comps.CoreSvc.Mgmt == nil {
		return nil, fmt.Errorf("resource pool metadata client is required for cassandra proxy operations")
	}

	rp, err := a.comps.CoreSvc.Mgmt.GetResourcePoolString(ctx, sharedPoolID)
	if err != nil {
		return nil, err
	}
	region := rp.GetRegion()
	if region == nil {
		return nil, fmt.Errorf("resource pool %s has no region metadata", sharedPoolID)
	}
	if region.GetLogicalName() == "" {
		return nil, fmt.Errorf("resource pool %s has no logical region name", sharedPoolID)
	}
	if region.GetCloudProviderId() == commonv1.CloudProvider_NAME_UNSPECIFIED {
		return nil, fmt.Errorf("resource pool %s has no cloud provider", sharedPoolID)
	}
	return a.comps.MgmtHTTP.NextGenRegionServer(region.GetLogicalName(), region.GetCloudProviderId().String()), nil
}

func (a *setCassandra) getSharedPool(ctx context.Context, clusterID, sharedPoolID string) (*mgmthttpcli.SharedPool, error) {
	regional, err := a.cassandraRegionServer(ctx, sharedPoolID)
	if err != nil {
		return nil, err
	}
	return regional.GetSharedPool(ctx, sharedPoolID)
}

func (a *setCassandra) getCassandraProxy(ctx context.Context, clusterID, sharedPoolID string) (*clusterng.GetCassandraProxyResponse, error) {
	regional, err := a.cassandraRegionServer(ctx, sharedPoolID)
	if err != nil {
		return nil, err
	}
	return regional.GetCassandraProxy(ctx, clusterID)
}

func (a *setCassandra) setCassandraProxy(ctx context.Context, clusterID, sharedPoolID string, req *clusterng.SetCassandraProxyRequest) (*clusterng.SetCassandraProxyResponse, error) {
	regional, err := a.cassandraRegionServer(ctx, sharedPoolID)
	if err != nil {
		return nil, err
	}
	return regional.SetCassandraProxy(ctx, clusterID, req)
}

func (a *setCassandra) requireScalableCassandraProxy(ctx context.Context, clusterID, sharedPoolID string, force bool) (*clusterng.GetCassandraProxyResponse, error) {
	status, err := a.getCassandraProxy(ctx, clusterID, sharedPoolID)
	if err != nil {
		if errs.IsNotFound(err) {
			return nil, fmt.Errorf("cassandra proxy is not enabled; use EnableCassandra first")
		}
		return nil, err
	}
	phase := status.GetPhase()
	if phase == clusterng.Component_AVAILABLE || force && phase == clusterng.Component_UNAVAILABLE {
		return status, nil
	}
	return nil, fmt.Errorf("cassandra proxy phase is %s, retry ScaleCassandra after AVAILABLE", phase)
}

func (a *setCassandra) requireMissingCassandraProxy(ctx context.Context, clusterID, sharedPoolID string) error {
	status, err := a.getCassandraProxy(ctx, clusterID, sharedPoolID)
	if err != nil {
		if errs.IsNotFound(err) {
			return nil
		}
		return err
	}
	return fmt.Errorf("cassandra proxy already exists in phase %s; use ScaleCassandra for quota changes", status.GetPhase())
}

func getCassandraOperationMode(instance *opent.Instance) (cassandraOperationMode, error) {
	switch instance.Operation {
	case operationScaleCassandra:
		return cassandraOperationModeScale, nil
	case operationEnableCassandra:
		return cassandraOperationModeEnable, nil
	default:
		return "", fmt.Errorf("set_cassandra action cannot be used by operation %q", instance.Operation)
	}
}

func (a *setCassandra) resolveScaleMode(ctx context.Context, clusterID, sharedPoolID string, desiredQuota *clusterng.NodeQuota, imageOverride bool) (string, string, error) {
	sp, err := a.getSharedPool(ctx, clusterID, sharedPoolID)
	if err != nil {
		return "", "", err
	}

	desired := toMgmtNodeQuota(desiredQuota)
	current := findCassandraProxyComponent(sp, clusterID)
	if current == nil || current.NodeQuota == nil {
		return "", "", fmt.Errorf("cassandra proxy component is missing from shared pool")
	}

	if desiredQuota != nil {
		desiredQuota.NodeCount = current.NodeQuota.NodeCount
		desired.NodeCount = current.NodeQuota.NodeCount
	}
	quotaEqual := cassandraProxyNodeQuotaEqual(current.NodeQuota, desired)
	if quotaEqual {
		if imageOverride {
			return cassandraScaleModeScale, "up", nil
		}
		return cassandraScaleModeNoop, "up", nil
	}
	return cassandraScaleModeScale, computeCassandraScaleDirection(current.NodeQuota, desired), nil
}

func getRequiredUint32Param(instance *opent.Instance, param opent.ParamName) (uint32, error) {
	value, ok := instance.GetParam(param)
	if !ok || value == "" {
		return 0, fmt.Errorf("param %s is required", param)
	}

	parsed, err := strconv.ParseUint(value, 10, 32)
	if err != nil {
		return 0, fmt.Errorf("invalid %s: %s", param, value)
	}
	if parsed == 0 {
		return 0, fmt.Errorf("param %s must be greater than 0", param)
	}

	return uint32(parsed), nil
}

func setCassandraDerivedParams(instance *opent.Instance, scaleMode string, scaleDirection string) {
	if instance.Params == nil {
		instance.Params = map[opent.ParamName]string{}
	}
	instance.Params[cassandraScaleModeParam] = scaleMode
	instance.Params[opent.ParamScaleDirection] = scaleDirection
}

func findCassandraProxyComponent(sp *mgmthttpcli.SharedPool, clusterID string) *mgmthttpcli.Component {
	if sp == nil {
		return nil
	}
	for _, comp := range sp.ComponentGroups {
		if comp.ComponentType == cassandraProxyComponentType && comp.Id == clusterID {
			return comp
		}
	}
	return nil
}

func toMgmtNodeQuota(quota *clusterng.NodeQuota) *mgmthttpcli.NodeQuota {
	if quota == nil {
		return nil
	}
	result := &mgmthttpcli.NodeQuota{
		NodeCount:    quota.GetNodeCount(),
		MinNodeCount: quota.GetMinNodeCount(),
		MaxNodeCount: quota.GetMaxNodeCount(),
	}
	if quota.GetNodeSpec() != nil {
		result.NodeSpec = &mgmthttpcli.NodeSpec{
			Cpu:      quota.GetNodeSpec().GetCpu(),
			MemoryGi: quota.GetNodeSpec().GetMemoryGi(),
		}
	}
	return result
}

func cassandraProxyNodeQuotaEqual(a, b *mgmthttpcli.NodeQuota) bool {
	if a == nil || b == nil {
		return a == nil && b == nil
	}
	if a.NodeCount != b.NodeCount ||
		a.MinNodeCount != b.MinNodeCount ||
		a.MaxNodeCount != b.MaxNodeCount {
		return false
	}
	aSpec, bSpec := a.NodeSpec, b.NodeSpec
	if aSpec == nil || bSpec == nil {
		return aSpec == nil && bSpec == nil
	}
	return aSpec.Cpu == bSpec.Cpu && aSpec.MemoryGi == bSpec.MemoryGi
}

func computeCassandraScaleDirection(current, desired *mgmthttpcli.NodeQuota) string {
	if current == nil || desired == nil {
		return "up"
	}

	hasDown := desired.MaxNodeCount < current.MaxNodeCount || desired.MinNodeCount < current.MinNodeCount
	hasUp := desired.MaxNodeCount > current.MaxNodeCount || desired.MinNodeCount > current.MinNodeCount

	currentSpec, desiredSpec := current.NodeSpec, desired.NodeSpec
	if currentSpec != nil && desiredSpec != nil {
		if desiredSpec.Cpu < currentSpec.Cpu || desiredSpec.MemoryGi < currentSpec.MemoryGi {
			hasDown = true
		}
		if desiredSpec.Cpu > currentSpec.Cpu || desiredSpec.MemoryGi > currentSpec.MemoryGi {
			hasUp = true
		}
	}

	if hasDown && hasUp {
		return "mixed"
	}
	if hasDown {
		return "down"
	}
	return "up"
}

func cassandraAdvisoryTargetNodeCount(current uint32, desired *clusterng.NodeQuota) uint32 {
	if desired == nil {
		return current
	}
	if minNodeCount := desired.GetMinNodeCount(); minNodeCount > 0 && current < minNodeCount {
		return minNodeCount
	}
	if maxNodeCount := desired.GetMaxNodeCount(); maxNodeCount > 0 && current > maxNodeCount {
		return maxNodeCount
	}
	return current
}
