package ngoperation

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"time"

	eventent "github.com/tidbcloud/devops/entity/event"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/infra/clients/mgmthttpcli"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
	evbus "github.com/tidbcloud/eventbus-datastruct/event"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
)

const resourcePoolTiProxyComponentType = "TIPROXY"

var (
	ActionScaleResourcePool opent.ActionMetadata = opent.ActionMetadata{
		Name: "ScaleNextGenResourcePool",
		Type: opent.ActionTypeAuto,
	}
)

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

func NewScaleScaleResourcePool(comps *actions.Components) actions.AutoAction {
	return &scaleResourcePool{comps: comps}
}

func (a *scaleResourcePool) Metadata() opent.ActionMetadata {
	return ActionScaleResourcePool
}

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

	_, sp, err := a.getResourcePoolAndSharedPoolForScale(ctx, sharedPoolID, req.Component.ComponentType)
	if err != nil {
		return err
	}

	componentType, _ := instance.GetParam(opent.ParamComponentType)
	direction := "up" // safe default: no approval needed if current state is unknown
	componentID := req.Component.Id
	comp := findSharedPoolComponent(sp, componentType, componentID)
	if componentID != "" && comp == nil {
		return fmt.Errorf("%s is not found in shared pool", sharedPoolComponentName(componentType, componentID))
	}
	if comp != nil && comp.NodeQuota != nil {
		direction = computeNodeScaleDirection(
			comp.NodeQuota.NodeCount, req.Component.NodeQuota.NodeCount,
			comp.NodeQuota.NodeSpec, req.Component.NodeQuota.NodeSpec,
		)
	}
	instance.Params[opent.ParamScaleDirection] = direction
	return nil
}

func (a *scaleResourcePool) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	req, spid, err := a.getParam(instance)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}
	rp, err := a.getResourcePoolProfileForScale(ctx, spid, req.Component.ComponentType)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}
	sp, err := a.getSharedPoolForResourcePoolWithRetry(ctx, spid, rp, 3)
	if err != nil {
		a.comps.Logger.Error(err, "get resource pool and shared pool failed for scale resource pool event")
		return actions.FailedActionResult(err), nil
	}
	logicalRegion, provider := GetLogicalRegionAndProviderString(rp.GetRegion())
	_, err = a.comps.MgmtHTTP.NextGenRegionServer(logicalRegion, provider).ScaleSharedPool(ctx, spid, req)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	// send event
	var beforeBytes []byte
	if sp != nil && sp.ComponentGroups != nil {
		for _, comp := range sp.ComponentGroups {
			if matchSharedPoolComponent(comp, req.Component.ComponentType, req.Component.Id) {
				if len(comp.Storages) > 0 {
					beforeBytes, _ = json.Marshal(comp.NodeQuota)
				}
			}
		}
	}
	before := string(beforeBytes)
	afterBytes, _ := json.Marshal(req.Component.NodeQuota)
	after := string(afterBytes)
	displayEv, err := assembleResourcePoolEvent(string(eventent.ScaleResourcePoolEvent),
		evbus.EventStatusStarted,
		instance.Applicant,
		rp,
		sp,
		string(eventent.ScaleResourcePoolCreatedSummary),
		strconv.FormatUint(rp.ResourcePool.Id, 10),
		req.Component.ComponentType,
		before,
		after,
	)
	if err != nil {
		a.comps.Logger.Error(err, "assemble scale resource pool event failed")
	} else {
		err = a.comps.EventBus.DBaaSDisplayEvent.Publish(displayEv)
		if err != nil {
			a.comps.Logger.Error(err, "publish scale resource pool event failed")
		}
	}

	return actions.RunningActionResult("sent request to scale shared pool", nil), nil
}

func (a *scaleResourcePool) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	spid, err := actions.GetResourcePoolID(instance)
	if err != nil {
		return nil, err
	}
	componentType, ok := instance.GetParam(opent.ParamComponentType)
	if !ok || componentType == "" {
		return nil, fmt.Errorf("param component_type is required")
	}
	componentID, _ := instance.GetParam(opent.ParamComponentID)

	// sometimes GetCluster will return error, so retries added
	rp, sharedPool, err := GetResourcePoolAndSharedPoolWithRetry(ctx, spid, a.comps.CoreSvc, a.comps.MgmtHTTP, 3)
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}

	for _, comp := range sharedPool.ComponentGroups {
		if matchSharedPoolComponent(comp, componentType, componentID) {
			if comp.Phase == "AVAILABLE" {
				// send event
				displayEv, err := assembleResourcePoolEvent(string(eventent.ScaleResourcePoolEvent),
					evbus.EventStatusSucceeded,
					instance.Applicant, rp, sharedPool,
					string(eventent.ScaleResourcePoolSSucceededSummary),
					strconv.FormatUint(rp.ResourcePool.Id, 10),
				)
				if err != nil {
					a.comps.Logger.Error(err, "assemble modify resource pool volume succeeded event failed")
				} else {
					err = a.comps.EventBus.DBaaSDisplayEvent.Publish(displayEv)
					if err != nil {
						a.comps.Logger.Error(err, "publish modify resource pool volume succeeded event failed")
					}
				}
				return actions.SucceededActionResult(nil), nil
			}
			return actions.RunningActionResult(fmt.Sprintf("%s phase is '%s'", sharedPoolComponentName(componentType, componentID), comp.Phase), nil), nil
		}
	}

	return actions.NewActionResult(opent.ActionStatusUnknown, fmt.Sprintf("%s is not found in shared pool", sharedPoolComponentName(componentType, componentID)), nil), nil
}

func (a *scaleResourcePool) getParam(instance *opent.Instance) (*mgmthttpcli.ScaleSharedPoolRequest, string, error) {
	sharedPoolID, err := actions.GetResourcePoolID(instance)
	if err != nil {
		return nil, "", err
	}
	componentType, ok := instance.GetParam(opent.ParamComponentType)
	if !ok || componentType == "" {
		return nil, "", fmt.Errorf("param component_type is required")
	}

	req := &mgmthttpcli.ScaleSharedPoolRequest{
		Component: &mgmthttpcli.Component{
			ComponentType: componentType,
			NodeQuota:     &mgmthttpcli.NodeQuota{},
		},
		Force: instance.Params[opent.ParamForceScale] == "true",
	}
	componentID, _ := instance.GetParam(opent.ParamComponentID)
	if componentID != "" {
		req.Component.Id = componentID
	}

	// node count
	nodeCountStr, _ := instance.GetParam(opent.ParamNodeCount)
	if nodeCountStr == "" {
		return nil, "", fmt.Errorf("param node_count is required")
	}
	nodeCount, err := strconv.Atoi(nodeCountStr)
	if err != nil {
		return nil, "", fmt.Errorf("invalid node_count: %s", nodeCountStr)
	}
	req.Component.NodeQuota.NodeCount = uint32(nodeCount)

	// mem & cpu
	memoryGiStr, _ := instance.GetParam(opent.ParamMemoryGi)
	cpuCoresStr, _ := instance.GetParam(opent.ParamCpuCores)
	if (memoryGiStr != "") != (cpuCoresStr != "") {
		return nil, "", fmt.Errorf("both memory_gi and cpu_cores must be provided or neither")
	}
	if memoryGiStr != "" || cpuCoresStr != "" {
		memoryGi, err := strconv.Atoi(memoryGiStr)
		if err != nil {
			return nil, "", fmt.Errorf("invalid memory_gi: %s", memoryGiStr)
		}
		cpuCores, err := strconv.Atoi(cpuCoresStr)
		if err != nil {
			return nil, "", fmt.Errorf("invalid cpu_cores: %s", cpuCoresStr)
		}
		req.Component.NodeQuota.NodeSpec = &mgmthttpcli.NodeSpec{
			MemoryGi: uint32(memoryGi),
			Cpu:      uint32(cpuCores),
		}
	}

	return req, sharedPoolID, nil
}

func findSharedPoolComponent(sharedPool *mgmthttpcli.SharedPool, componentType, componentID string) *mgmthttpcli.Component {
	if sharedPool == nil {
		return nil
	}
	for _, component := range sharedPool.ComponentGroups {
		if matchSharedPoolComponent(component, componentType, componentID) {
			return component
		}
	}
	return nil
}

func matchSharedPoolComponent(component *mgmthttpcli.Component, componentType, componentID string) bool {
	if component == nil || component.ComponentType != componentType {
		return false
	}
	return componentID == "" || component.Id == componentID
}

func sharedPoolComponentName(componentType, componentID string) string {
	if componentID == "" {
		return fmt.Sprintf("component %s", componentType)
	}
	return fmt.Sprintf("component %s of type %s", componentID, componentType)
}

func (a *scaleResourcePool) getResourcePoolAndSharedPoolForScale(ctx context.Context, sharedPoolID string, componentType string) (*nextgenv1.ResourcePoolProfile, *mgmthttpcli.SharedPool, error) {
	rp, err := a.getResourcePoolProfileForScale(ctx, sharedPoolID, componentType)
	if err != nil {
		return nil, nil, err
	}
	sp, err := a.getSharedPoolForResourcePool(ctx, sharedPoolID, rp)
	if err != nil {
		return nil, nil, err
	}
	return rp, sp, nil
}

func (a *scaleResourcePool) getResourcePoolProfileForScale(ctx context.Context, sharedPoolID string, componentType string) (*nextgenv1.ResourcePoolProfile, error) {
	rp, err := a.comps.CoreSvc.Mgmt.GetResourcePoolString(ctx, sharedPoolID)
	if err != nil {
		return nil, err
	}
	if err := validateScaleResourcePoolTarget(componentType, sharedPoolID, rp); err != nil {
		return nil, err
	}
	return rp, nil
}

func (a *scaleResourcePool) getSharedPoolForResourcePool(ctx context.Context, sharedPoolID string, rp *nextgenv1.ResourcePoolProfile) (*mgmthttpcli.SharedPool, error) {
	logicalRegion, provider := GetLogicalRegionAndProviderString(rp.GetRegion())
	return a.comps.MgmtHTTP.NextGenRegionServer(logicalRegion, provider).GetSharedPool(ctx, sharedPoolID)
}

func (a *scaleResourcePool) getSharedPoolForResourcePoolWithRetry(ctx context.Context, sharedPoolID string, rp *nextgenv1.ResourcePoolProfile, retries int) (*mgmthttpcli.SharedPool, error) {
	var sp *mgmthttpcli.SharedPool
	var err error
	for retries > 0 {
		sp, err = a.getSharedPoolForResourcePool(ctx, sharedPoolID, rp)
		if err == nil {
			return sp, nil
		}
		retries--
		if retries > 0 {
			time.Sleep(time.Second * 10)
		}
	}
	return nil, err
}

func validateScaleResourcePoolTarget(componentType string, sharedPoolID string, rp *nextgenv1.ResourcePoolProfile) error {
	if componentType != resourcePoolTiProxyComponentType {
		return nil
	}
	if rp == nil || rp.GetResourcePool() == nil {
		return fmt.Errorf("resource pool %s is not found", sharedPoolID)
	}
	if rp.GetResourcePool().GetOwnerType() != nextgenv1.ResourcePool_SHARED {
		return fmt.Errorf("%s can only be scaled at pool level for shared resource pools", resourcePoolTiProxyComponentType)
	}
	return nil
}
