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/service/operation/actions"
)

var (
	ActionCreateTiKVGroup = opent.ActionMetadata{
		Name: "CreateTiKVGroup",
		Type: opent.ActionTypeAuto,
	}
)

var (
	_ actions.Action = &createTiKVGroup{}
)

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

func NewCreateTiKVGroup(comps *actions.Components) actions.AutoAction {
	return &createTiKVGroup{comps: comps}
}

func (a *createTiKVGroup) Metadata() opent.ActionMetadata {
	return ActionCreateTiKVGroup
}

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

	return nil
}

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

	rp, err := a.comps.CoreSvc.Mgmt.GetResourcePoolString(ctx, req.GetSharedPoolId())
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	logicalRegion, provider := GetLogicalRegionAndProviderString(rp.GetRegion())
	resp, err := a.comps.MgmtHTTP.NextGenRegionServer(logicalRegion, provider).CreateTiKVGroup(ctx, req)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	return actions.RunningActionResult(fmt.Sprintf("sent request to create TiKV group: %s", resp.GetId()), nil), nil
}

func (a *createTiKVGroup) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	sharedPoolID, tikvGroupID, err := getCreateTiKVGroupRequiredParam(instance)
	if err != nil {
		return nil, err
	}

	rp, sp, err := GetResourcePoolAndSharedPoolWithRetry(ctx, sharedPoolID, a.comps.CoreSvc, a.comps.MgmtHTTP, 3)
	if err != nil {
		return actions.NewActionResult(opent.ActionStatusUnknown, err.Error(), nil), nil
	}
	_ = rp

	for _, comp := range sp.ComponentGroups {
		if comp.Id == tikvGroupID && componentTypeEqual(comp.ComponentType, clusterng.ComponentType_TIKV.String()) {
			if comp.Phase == "AVAILABLE" {
				return actions.SucceededActionResult(nil), nil
			}
			return actions.RunningActionResult(fmt.Sprintf("TiKV group phase is '%s'", comp.Phase), nil), nil
		}
	}

	return actions.RunningActionResult("TiKV group not found yet, waiting...", nil), nil
}

func (a *createTiKVGroup) getParam(ctx context.Context, instance *opent.Instance) (*clusterng.CreateTiKVGroupRequest, error) {
	sharedPoolID, tikvGroupID, err := getCreateTiKVGroupRequiredParam(instance)
	if err != nil {
		return nil, err
	}

	if err := validateCreateTiKVGroupNodeCount(instance); err != nil {
		return nil, err
	}

	var defaultTiKVGroup *mgmthttpcli.Component
	if needDefaultTiKVGroupParam(instance) {
		defaultTiKVGroup, err = a.getDefaultTiKVGroup(ctx, sharedPoolID)
		if err != nil {
			return nil, err
		}
	}

	return buildCreateTiKVGroupRequest(sharedPoolID, tikvGroupID, instance, defaultTiKVGroup)
}

func getCreateTiKVGroupRequiredParam(instance *opent.Instance) (string, string, error) {
	sharedPoolID, err := actions.GetResourcePoolID(instance)
	if err != nil {
		return "", "", err
	}

	tikvGroupID, ok := instance.GetParam(opent.ParamTiKVGroupID)
	if !ok || tikvGroupID == "" {
		return "", "", fmt.Errorf("param tikv_group_id is required")
	}

	return sharedPoolID, tikvGroupID, nil
}

func validateCreateTiKVGroupNodeCount(instance *opent.Instance) error {
	nodeCountStr, _ := instance.GetParam(opent.ParamNodeCount)
	_, err := resolveRequiredUint32Param(opent.ParamNodeCount, nodeCountStr, 0, false)
	return err
}

func (a *createTiKVGroup) getDefaultTiKVGroup(ctx context.Context, sharedPoolID string) (*mgmthttpcli.Component, error) {
	_, sp, err := GetResourcePoolAndSharedPool(ctx, sharedPoolID, a.comps.CoreSvc, a.comps.MgmtHTTP)
	if err != nil {
		return nil, fmt.Errorf("get shared pool for default TiKV group failed: %w", err)
	}

	return findDefaultTiKVGroup(sp)
}

func buildCreateTiKVGroupRequest(sharedPoolID, tikvGroupID string, instance *opent.Instance, defaultTiKVGroup *mgmthttpcli.Component) (*clusterng.CreateTiKVGroupRequest, error) {
	req := &clusterng.CreateTiKVGroupRequest{
		SharedPoolId: sharedPoolID,
		TikvGroupId:  tikvGroupID,
		NodeQuota:    &clusterng.NodeQuota{},
		Storages:     []*clusterng.Component_Storage{},
		Force:        instance.Params[opent.ParamForceCreate] == "true",
	}

	// node count
	nodeCountStr, _ := instance.GetParam(opent.ParamNodeCount)
	nodeCount, err := resolveRequiredUint32Param(opent.ParamNodeCount, nodeCountStr, 0, false)
	if err != nil {
		return nil, err
	}
	req.NodeQuota.NodeCount = nodeCount

	// mem & cpu
	memoryGiStr, _ := instance.GetParam(opent.ParamMemoryGi)
	cpuCoresStr, _ := instance.GetParam(opent.ParamCpuCores)
	defaultMemoryGi, hasDefaultMemoryGi := defaultTiKVGroupMemoryGi(defaultTiKVGroup)
	memoryGi, err := resolveRequiredUint32Param(opent.ParamMemoryGi, memoryGiStr, defaultMemoryGi, hasDefaultMemoryGi)
	if err != nil {
		return nil, err
	}
	defaultCPUCores, hasDefaultCPUCores := defaultTiKVGroupCPUCores(defaultTiKVGroup)
	cpuCores, err := resolveRequiredUint32Param(opent.ParamCpuCores, cpuCoresStr, defaultCPUCores, hasDefaultCPUCores)
	if err != nil {
		return nil, err
	}
	req.NodeQuota.NodeSpec = &clusterng.NodeSpec{
		MemoryGi: memoryGi,
		Cpu:      cpuCores,
	}

	// data storage
	dataStorage, err := buildCreateTiKVGroupStorage(instance, defaultTiKVGroup, clusterng.Component_Storage_DATA,
		opent.ParamName("data_storage_type"),
		opent.ParamName("data_storage_size_gi"),
		opent.ParamName("data_storage_iops"),
		opent.ParamName("data_storage_throughput_mbs"),
	)
	if err != nil {
		return nil, err
	}
	req.Storages = append(req.Storages, dataStorage)

	// raftlog storage
	raftlogStorage, err := buildCreateTiKVGroupStorage(instance, defaultTiKVGroup, clusterng.Component_Storage_RAFTLOG,
		opent.ParamName("raftlog_storage_type"),
		opent.ParamName("raftlog_storage_size_gi"),
		opent.ParamName("raftlog_storage_iops"),
		opent.ParamName("raftlog_storage_throughput_mbs"),
	)
	if err != nil {
		return nil, err
	}
	req.Storages = append(req.Storages, raftlogStorage)

	// optional labels
	labels, err := instance.GetMapParam(opent.ParamLabels)
	if err == nil && len(labels) > 0 {
		req.Labels = labels
	}

	if exclusiveStr, ok := instance.GetParam(opent.ParamExclusive); ok && exclusiveStr != "" {
		exclusive, err := strconv.ParseBool(exclusiveStr)
		if err != nil {
			return nil, fmt.Errorf("invalid exclusive: %s", exclusiveStr)
		}
		req.Exclusive = exclusive
	}

	return req, nil
}

func needDefaultTiKVGroupParam(instance *opent.Instance) bool {
	for _, name := range []opent.ParamName{
		opent.ParamMemoryGi,
		opent.ParamCpuCores,
		opent.ParamName("data_storage_type"),
		opent.ParamName("data_storage_size_gi"),
		opent.ParamName("data_storage_iops"),
		opent.ParamName("data_storage_throughput_mbs"),
		opent.ParamName("raftlog_storage_type"),
		opent.ParamName("raftlog_storage_size_gi"),
		opent.ParamName("raftlog_storage_iops"),
		opent.ParamName("raftlog_storage_throughput_mbs"),
	} {
		value, _ := instance.GetParam(name)
		if value == "" {
			return true
		}
	}
	return false
}

func findDefaultTiKVGroup(sp *mgmthttpcli.SharedPool) (*mgmthttpcli.Component, error) {
	if sp == nil {
		return nil, fmt.Errorf("shared pool is nil")
	}

	defaultComponentID := ""
	for _, comp := range sp.ComponentGroups {
		if componentTypeEqual(comp.ComponentType, clusterng.ComponentType_PD.String()) {
			// The default TiKV group uses the same component id as PD.
			defaultComponentID = comp.Id
			break
		}
	}
	if defaultComponentID == "" {
		return nil, fmt.Errorf("PD component is not found for default TiKV group")
	}

	for _, comp := range sp.ComponentGroups {
		if comp.Id == defaultComponentID && componentTypeEqual(comp.ComponentType, clusterng.ComponentType_TIKV.String()) {
			return comp, nil
		}
	}

	return nil, fmt.Errorf("default TiKV group with component id %s is not found", defaultComponentID)
}

func buildCreateTiKVGroupStorage(
	instance *opent.Instance,
	defaultTiKVGroup *mgmthttpcli.Component,
	storageName clusterng.Component_Storage_Name,
	typeParam opent.ParamName,
	sizeParam opent.ParamName,
	iopsParam opent.ParamName,
	throughputParam opent.ParamName,
) (*clusterng.Component_Storage, error) {
	defaultStorage := findDefaultTiKVGroupStorage(defaultTiKVGroup, storageName)

	storageTypeStr, _ := instance.GetParam(typeParam)
	defaultType, hasDefaultType := defaultStorageType(defaultStorage)
	storageType, err := resolveRequiredStringParam(typeParam, storageTypeStr, defaultType, hasDefaultType)
	if err != nil {
		return nil, err
	}

	storageSizeStr, _ := instance.GetParam(sizeParam)
	defaultSize, hasDefaultSize := defaultStorageSize(defaultStorage)
	storageSize, err := resolveRequiredUint32Param(sizeParam, storageSizeStr, defaultSize, hasDefaultSize)
	if err != nil {
		return nil, err
	}

	storageIopsStr, _ := instance.GetParam(iopsParam)
	defaultIops, hasDefaultIops := defaultStorageIops(defaultStorage)
	storageIops, err := resolveOptionalUint32Param(iopsParam, storageIopsStr, defaultIops, hasDefaultIops)
	if err != nil {
		return nil, err
	}

	storageThroughputStr, _ := instance.GetParam(throughputParam)
	defaultThroughput, hasDefaultThroughput := defaultStorageThroughput(defaultStorage)
	storageThroughput, err := resolveOptionalUint32Param(throughputParam, storageThroughputStr, defaultThroughput, hasDefaultThroughput)
	if err != nil {
		return nil, err
	}

	return &clusterng.Component_Storage{
		Name:          storageName,
		Type:          storageType,
		SizeGi:        storageSize,
		Iops:          storageIops,
		ThroughputMbs: storageThroughput,
	}, nil
}

func findDefaultTiKVGroupStorage(defaultTiKVGroup *mgmthttpcli.Component, storageName clusterng.Component_Storage_Name) *mgmthttpcli.Component_Storage {
	if defaultTiKVGroup == nil {
		return nil
	}
	for _, storage := range defaultTiKVGroup.Storages {
		if strings.EqualFold(storage.Name, storageName.String()) {
			return storage
		}
	}
	return nil
}

func resolveRequiredStringParam(name opent.ParamName, value string, fallbackValue string, fallbackOK bool) (string, error) {
	if value != "" {
		return value, nil
	}
	if fallbackOK {
		return fallbackValue, nil
	}
	return "", fmt.Errorf("param %s is required", name)
}

func resolveRequiredUint32Param(name opent.ParamName, value string, fallbackValue uint32, fallbackOK bool) (uint32, error) {
	if value != "" {
		return parseUint32Param(name, value)
	}
	if fallbackOK {
		return fallbackValue, nil
	}
	return 0, fmt.Errorf("param %s is required", name)
}

func resolveOptionalUint32Param(name opent.ParamName, value string, fallbackValue uint32, fallbackOK bool) (uint32, error) {
	if value != "" {
		return parseUint32Param(name, value)
	}
	if fallbackOK {
		return fallbackValue, nil
	}
	return 0, nil
}

func parseUint32Param(name opent.ParamName, value string) (uint32, error) {
	result, err := strconv.ParseUint(value, 10, 32)
	if err != nil {
		return 0, fmt.Errorf("invalid %s: %s", name, value)
	}
	return uint32(result), nil
}

func componentTypeEqual(actual string, expected string) bool {
	return strings.EqualFold(actual, expected)
}

func defaultTiKVGroupMemoryGi(defaultTiKVGroup *mgmthttpcli.Component) (uint32, bool) {
	if defaultTiKVGroup == nil || defaultTiKVGroup.NodeQuota == nil || defaultTiKVGroup.NodeQuota.NodeSpec == nil {
		return 0, false
	}
	return defaultTiKVGroup.NodeQuota.NodeSpec.MemoryGi, defaultTiKVGroup.NodeQuota.NodeSpec.MemoryGi > 0
}

func defaultTiKVGroupCPUCores(defaultTiKVGroup *mgmthttpcli.Component) (uint32, bool) {
	if defaultTiKVGroup == nil || defaultTiKVGroup.NodeQuota == nil || defaultTiKVGroup.NodeQuota.NodeSpec == nil {
		return 0, false
	}
	return defaultTiKVGroup.NodeQuota.NodeSpec.Cpu, defaultTiKVGroup.NodeQuota.NodeSpec.Cpu > 0
}

func defaultStorageType(storage *mgmthttpcli.Component_Storage) (string, bool) {
	if storage == nil {
		return "", false
	}
	return storage.Type, storage.Type != ""
}

func defaultStorageSize(storage *mgmthttpcli.Component_Storage) (uint32, bool) {
	if storage == nil {
		return 0, false
	}
	return storage.SizeGi, storage.SizeGi > 0
}

func defaultStorageIops(storage *mgmthttpcli.Component_Storage) (uint32, bool) {
	if storage == nil {
		return 0, false
	}
	return storage.Iops, true
}

func defaultStorageThroughput(storage *mgmthttpcli.Component_Storage) (uint32, bool) {
	if storage == nil {
		return 0, false
	}
	return storage.ThroughputMbs, true
}
