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

var (
	ActionModifyResourcePoolVolume opent.ActionMetadata = opent.ActionMetadata{
		Name: "ModifyNextGenResourcePoolVolume",
		Type: opent.ActionTypeAuto,
	}
)

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

func NewModifyResourcePoolVolume(comps *actions.Components) actions.AutoAction {
	return &modifyResourcePoolVolume{comps: comps}
}

func (a *modifyResourcePoolVolume) Metadata() opent.ActionMetadata {
	return ActionModifyResourcePoolVolume
}

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

	_, _, err = GetResourcePoolAndSharedPool(ctx, sharedPoolID, a.comps.CoreSvc, a.comps.MgmtHTTP)
	if err != nil {
		return err
	}

	return nil
}

func (a *modifyResourcePoolVolume) 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, sp, err := GetResourcePoolAndSharedPoolWithRetry(ctx, spid, a.comps.CoreSvc, a.comps.MgmtHTTP, 3)
	if err != nil {
		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
	}

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

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

func (a *modifyResourcePoolVolume) 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")
	}

	// 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 comp.ComponentType == componentType {
			if comp.Phase == "AVAILABLE" {
				// send event
				displayEv, err := assembleResourcePoolEvent(string(eventent.ModifyResourcePoolVolumeEvent),
					evbus.EventStatusSucceeded,
					instance.Applicant, rp, sharedPool,
					string(eventent.ModifyResourcePoolVolumeSucceededSummary),
					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("component %s phase is '%s'", componentType, comp.Phase), nil), nil
		}
	}

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

func (a *modifyResourcePoolVolume) 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")
	}
	volumeName, _ := instance.GetParam(opent.ParamVolumeName)
	if volumeName == "" {
		return nil, "", fmt.Errorf("param volume_name is required")
	}

	req := &mgmthttpcli.ScaleSharedPoolRequest{
		Component: &mgmthttpcli.Component{
			ComponentType: componentType,
			Storages: []*mgmthttpcli.Component_Storage{
				{
					Name: volumeName,
				},
			},
		},
		Force: instance.Params[opent.ParamForceModify] == "true",
	}

	storageSizeStr, _ := instance.GetParam(opent.ParamVolumeSizeGi)
	if storageSizeStr != "" {
		storageSize, err := strconv.ParseUint(storageSizeStr, 10, 64)
		if err != nil {
			return nil, "", fmt.Errorf("parse storage size failed, error: %v", err)
		}
		req.Component.Storages[0].SizeGi = uint32(storageSize)
	}

	iopsStr, _ := instance.GetParam(opent.ParamVolumeIops)
	if iopsStr != "" {
		iops, err := strconv.ParseUint(iopsStr, 10, 64)
		if err != nil {
			return nil, "", fmt.Errorf("parse storage iops failed, error: %v", err)
		}
		req.Component.Storages[0].Iops = uint32(iops)
	}

	throughputStr, _ := instance.GetParam(opent.ParamVolumeThroughputMbs)
	if throughputStr != "" {
		throughput, err := strconv.ParseUint(throughputStr, 10, 64)
		if err != nil {
			return nil, "", fmt.Errorf("parse storage throughput failed, error: %v", err)
		}
		req.Component.Storages[0].ThroughputMbs = uint32(throughput)
	}

	volumeTypeStr, _ := instance.GetParam(opent.ParamVolumeType)
	if volumeTypeStr != "" {
		req.Component.Storages[0].Type = volumeTypeStr
	}

	if volumeTypeStr == "io2" && iopsStr == "" {
		return nil, "", fmt.Errorf("must modify iops when modifying to 'io2' volume type")
	}
	if volumeTypeStr == "gp3" {
		if iopsStr == "" || throughputStr == "" {
			return nil, "", fmt.Errorf("must modify iops and throughput when modifying to 'gp3' volume type")
		}
	}

	return req, sharedPoolID, nil
}

func assembleResourcePoolEvent(
	eventType string,
	status evbus.EventStatus,
	author string,
	rp *nextgenv1.ResourcePoolProfile,
	sharedPool *mgmthttpcli.SharedPool,
	summaryFmt string,
	args ...string,
) (*evbus.DisplayEvent, error) {
	if rp == nil || sharedPool == nil {
		return nil, fmt.Errorf("invalid resource pool or shared pool")
	}

	anyArgs := make([]any, len(args))
	for i, v := range args {
		anyArgs[i] = v
	}
	tenantID, ok := resourcePoolDisplayEventTenantIDFromResourcePool(rp)
	if !ok {
		return nil, fmt.Errorf("resource pool tenant attribution is unavailable")
	}

	ev := &evbus.DisplayEvent{
		DisplayEventType: eventType,
		DisplayStatus:    status,
		Severity:         evbus.EventSeverityInfo,
		Summary:          fmt.Sprintf(summaryFmt, anyArgs...),
		HappenedAtMs:     uint64(time.Now().UnixMilli()),
		Author:           author,
		TenantID:         tenantID,
		ProjectID:        0,
		ClusterID:        rp.ResourcePool.Id,
	}
	return ev, nil
}

func resourcePoolDisplayEventTenantIDFromResourcePool(rp *nextgenv1.ResourcePoolProfile) (uint64, bool) {
	if rp == nil || rp.GetResourcePool() == nil {
		return 0, false
	}
	rpp := rp.GetResourcePool()
	if rpp.GetOwnerType() == nextgenv1.ResourcePool_ORGANIZATION {
		return rpp.GetOwnerId(), true
	}
	if rpp.GetOwnerType() == nextgenv1.ResourcePool_SHARED {
		return 0, true
	}
	return 0, false
}

func resourcePoolDisplayEventTenantIDFromParams(instance *opent.Instance) (uint64, bool) {
	if instance == nil {
		return 0, false
	}
	tenantIDRaw := instance.Params[opent.ParamTenantID]
	if tenantIDRaw == "" {
		return 0, false
	}
	tenantID, err := strconv.ParseUint(tenantIDRaw, 10, 64)
	if err != nil {
		return 0, false
	}
	return tenantID, true
}
