package convert

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/samber/lo"
	coresvcmodel "github.com/tidbcloud/core-svc/model"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
)

func ToDedicateGlobalProviders(providers []coresvcmodel.Provider) []*schema.Provider {
	return lo.Map(providers, func(item coresvcmodel.Provider, index int) *schema.Provider {
		return toDedicateGlobalProvider(&item)
	})
}

func toDedicateGlobalProvider(provider *coresvcmodel.Provider) *schema.Provider {
	return &schema.Provider{
		Model: gorm.Model{
			ID:        uint(provider.ID.Uint64()),
			CreatedAt: provider.CreatedAt,
		},
		Name:   provider.Name.String(),
		Status: string(provider.Status),
	}
}

func ToDedicateGlobalRegions(regions []coresvcmodel.Region) []*schema.Region {
	return lo.Map(regions, func(item coresvcmodel.Region, index int) *schema.Region {
		return toDedicateGlobalRegion(&item)
	})
}

func toDedicateGlobalRegion(region *coresvcmodel.Region) *schema.Region {
	schemaRegion := schema.Region{
		Model: gorm.Model{
			ID:        uint(region.ID.Uint64()),
			CreatedAt: region.CreatedAt,
		},
		Name:              region.Name,
		RegionName:        region.Name,
		DisplayName:       region.DisplayName,
		ProviderId:        region.ProviderID.Uint64(),
		CountryCode:       region.CountryCode,
		Area:              region.Area,
		Priority:          region.Priority,
		Status:            string(region.Status),
		DefaultBackupTime: region.DefaultBackupTime,
	}
	if region.IsPublic != nil {
		schemaRegion.IsPublic = region.IsPublic
	}

	return &schemaRegion
}

type tierUnitInfoFromTierUnitMapping struct {
	Key        string
	ProviderId uint64
	Component  string
	TierId     uint64
}

type TierTableContainers struct {
	Tiers                []schema.ComponentTier
	TierUnits            []schema.ComponentTierUnit
	InstanceTypes        []schema.InstanceType
	StorageTemplates     []schema.StorageTemplate
	Conditions           []schema.TierUnitCondition
	Tier2TierUnitMapping []schema.TierTierUnit
}

func ToTierTableContainers(
	tiers []coresvcmodel.Tier,
	tierUnits []coresvcmodel.TierUnit,
	infraComputes []coresvcmodel.InfraCompute,
	infraStorages []coresvcmodel.InfraStorage,
	providers []coresvcmodel.Provider,
) (*TierTableContainers, error) {
	awsProvider, ok := lo.Find(providers, func(item coresvcmodel.Provider) bool {
		return item.Name.String() == "aws"
	})
	if !ok {
		return nil, errors.New("not found aws provider")
	}
	if awsProvider.ID.Uint64() <= 0 {
		return nil, errors.New("not found aws provider")
	}

	gcpProvider, ok := lo.Find(providers, func(item coresvcmodel.Provider) bool {
		return item.Name.String() == "gcp"
	})
	if !ok {
		return nil, errors.New("not found gcp provider")
	}
	if gcpProvider.ID.Uint64() <= 0 {
		return nil, errors.New("not found gcp provider")
	}

	azureProviderMayBeNil, _ := lo.Find(providers, func(item coresvcmodel.Provider) bool {
		return item.Name.String() == "azure"
	})

	out := &TierTableContainers{
		Tiers:                make([]schema.ComponentTier, 0),
		TierUnits:            make([]schema.ComponentTierUnit, 0),
		InstanceTypes:        make([]schema.InstanceType, 0),
		StorageTemplates:     make([]schema.StorageTemplate, 0),
		Conditions:           make([]schema.TierUnitCondition, 0),
		Tier2TierUnitMapping: make([]schema.TierTierUnit, 0),
	}

	tierUnitInfo := make(map[string]*tierUnitInfoFromTierUnitMapping)
	for i := range tiers {
		globalTier, globalTierUnitMapping, err := toTier(
			&tiers[i],
			awsProvider.ID.Uint64(),
			gcpProvider.ID.Uint64(),
			azureProviderMayBeNil.ID.Uint64(),
			tierUnitInfo,
		)
		if err != nil {
			if !errors.Is(err, ErrToSpuEmpty) && !errors.Is(err, ErrDeployTypeShared) &&
				!errors.Is(err, ErrToSpuSpecialSpu) {
				return nil, err
			}
			tilog.PlainWarn(err.Error())
			continue
		}
		out.Tiers = append(out.Tiers, *globalTier)
		out.Tier2TierUnitMapping = append(out.Tier2TierUnitMapping, *globalTierUnitMapping)
	}

	// convert tier units,instance types, storage templates and conditions
	outs := make([]*GlobalTierUnitContainer, 0)

	pdTierUnit := make(map[string]uint64)
	pd2TikvTierUnit := make(map[string]map[string]any)
	pdCount := make(map[string]int)

	for i := range tierUnits {
		tierUnit := &tierUnits[i]
		info, ok := tierUnitInfo[tierUnit.TierUnitKey]
		if !ok {
			tilog.PlainInfo("unused tier unit", zap.String("tier unit", tierUnit.TierUnitKey))
			continue
		}

		container, err := toTierUnit(tierUnit, info, infraStorages, infraComputes, pd2TikvTierUnit, pdCount)
		if err != nil {
			if !errors.Is(err, ErrDeployTypeShared) && !errors.Is(err, ErrToSkuLocalStorage) {
				return nil, err
			}
			tilog.PlainWarn(err.Error())
			continue
		}

		if tierUnit.ComponentType == "pd" {
			pdTierUnit[tierUnit.TierUnitKey] = tierUnit.ID.Uint64()
		}

		outs = append(outs, container)
	}

	for _, v := range lo.SliceToMap(outs, func(item *GlobalTierUnitContainer) (uint, *schema.StorageTemplate) {
		return item.storageTemplate.ID, item.storageTemplate
	}) {
		out.StorageTemplates = append(out.StorageTemplates, *v)
	}

	for _, v := range lo.SliceToMap(outs, func(item *GlobalTierUnitContainer) (uint, *schema.InstanceType) {
		return item.instanceType.ID, item.instanceType
	}) {
		out.InstanceTypes = append(out.InstanceTypes, *v)
	}

	lo.ForEach(outs, func(item *GlobalTierUnitContainer, index int) {
		if item.tierUnit.ComponentType == "pd" {
			count, ok := pdCount[item.tierUnit.Key]
			if ok {
				item.tierUnit.DefaultNodeNum = uint32(count)
			} else {
				if !isSpecialSku(item.tierUnit.Key) {
					tilog.PlainWarn("not found pd tier unit", zap.Any("pdCount", pdCount))
				}

				item.tierUnit.DefaultNodeNum = 3
			}
		}
		out.TierUnits = append(out.TierUnits, *item.tierUnit)

		out.Conditions = append(out.Conditions, item.conditions...)
	})

	// append tikv tier id to condition of pd tier unit
	for key, tikvTiers := range pd2TikvTierUnit {
		pdTierId, ok := pdTierUnit[key]
		if !ok {
			return nil, fmt.Errorf("not found pd tierunit %s", key)
		}

		condition := schema.TierUnitCondition{
			Key:      globaldedicatedv1.Sku_Condition_ATTACH_TIERUNIT.String(),
			Operator: globaldedicatedv1.Sku_Condition_IN.String(),
			Values: getConditionValue(lo.MapToSlice(tikvTiers, func(key string, value any) string {
				return key
			})),
			ComponentTierUnitId: pdTierId,
		}

		out.Conditions = append(out.Conditions, condition)
	}

	return out, nil
}

func toTier(
	tier *coresvcmodel.Tier,
	awsProviderId, gcpProviderId, azureProviderId uint64,
	tierUnitInfos map[string]*tierUnitInfoFromTierUnitMapping,
) (*schema.ComponentTier, *schema.TierTierUnit, error) {
	items := tier.TierUnitMapping.Items
	if len(items) == 0 {
		return nil, nil, fmt.Errorf(
			"found empty tier unit mapping, tier id :%d, err: %w",
			tier.ID.Uint64(),
			ErrToSpuEmpty,
		)
	}
	if tier.DeployType == "shared" {
		return nil, nil, fmt.Errorf("deploy type is shared, err: %w", ErrDeployTypeShared)
	}

	if isSpecialSpu(tier) {
		return nil, nil, fmt.Errorf(
			"pay attention !!! skip special tier, tier key :%s, err: %w",
			tier.TierKey,
			ErrToSpuSpecialSpu,
		)
	}

	globalTierUnitMapping := schema.TierUnitMapping{
		Items: make([]*schema.TierUnitMappingItem, 0),
	}

	for i := range items {
		item := items[i]

		if item.TierUnitKey == "" {
			tilog.PlainWarn("found empty tier unit key", zap.Uint64("tier id", tier.ID.Uint64()))
			return nil, nil, fmt.Errorf("found empty tier unit key, tier id :%d", tier.ID.Uint64())
		}

		providerId, err := getCloudProviderId(item.ProviderName, awsProviderId, gcpProviderId, azureProviderId)
		if err != nil {
			return nil, nil, err
		}

		globalTierUnitMapping.Items = append(globalTierUnitMapping.Items, &schema.TierUnitMappingItem{
			ProviderName: item.ProviderName,
			RegionNames:  item.RegionNames,
			TenantIDs:    item.TenantIDs,
			SkuKey:       item.TierUnitKey,
		})

		existedTU, ok := tierUnitInfos[item.TierUnitKey]
		if ok {
			if existedTU.ProviderId != providerId || existedTU.TierId != tier.ID.Uint64() ||
				existedTU.Component != string(tier.ComponentType) {
				tilog.PlainError(
					"unexpect tier unit value",
					zap.Uint64("new provider id", providerId),
					zap.Uint64("new tier id", tier.ID.Uint64()),
					zap.String("new component", string(tier.ComponentType)),
					zap.Any("existed", existedTU),
				)
				return nil, nil, errors.New("unexpect tier unit value")
			}
		} else {
			tierUnitInfos[item.TierUnitKey] = &tierUnitInfoFromTierUnitMapping{
				Key:        item.TierUnitKey,
				ProviderId: providerId,
				Component:  string(tier.ComponentType),
				TierId:     tier.ID.Uint64(),
			}
		}
	}

	return &schema.ComponentTier{
			Model: gorm.Model{
				ID:        uint(tier.ID.Uint64()),
				CreatedAt: tier.CreatedAt,
			},
			Key:           tier.TierKey,
			ComponentType: string(tier.ComponentType),
			DisplayName:   tier.DisplayName,
			CPUNum:        uint32(tier.CPUNum),
			MemorySizeGi:  uint32(tier.MemorySizeGi),
			Default:       tier.Default,
			Name:          tier.Name,
		}, &schema.TierTierUnit{
			Model: gorm.Model{
				ID:        uint(tier.ID.Uint64()),
				CreatedAt: time.Now(),
			},
			TierUnitMapping: globalTierUnitMapping,
			ComponentTierId: tier.ID.Uint64(),
		}, nil
}

func getCloudProviderId(name string, awsProviderId, gcpProviderId, azureProviderId uint64) (uint64, error) {
	switch name {
	case "aws":
		return awsProviderId, nil
	case "gcp":
		return gcpProviderId, nil
	case "azure":
		return azureProviderId, nil
	}
	return 0, fmt.Errorf("unknown cloud provider %s", name)
}

func ToRegionTenants(regionTenants []coresvcmodel.RegionTenant, regions []coresvcmodel.Region) []*schema.RegionTenant {
	regionIdMap := lo.SliceToMap(regions, func(item coresvcmodel.Region) (uint64, any) {
		return item.ID.Uint64(), struct{}{}
	})
	// return lo.Map(regionTenants, func(item coresvcmodel.RegionTenant, index int) *schema.RegionTenant {
	//	return toRegionTenant(item)
	// })
	out := make([]*schema.RegionTenant, 0)
	for _, rt := range regionTenants {
		if _, ok := regionIdMap[rt.RegionID.Uint64()]; ok {
			out = append(out, toRegionTenant(rt))
		}
	}

	return out
}

func toRegionTenant(regionTenant coresvcmodel.RegionTenant) *schema.RegionTenant {
	return &schema.RegionTenant{
		RegionId: regionTenant.RegionID.Uint64(),
		TenantId: regionTenant.TenantID.Uint64(),
	}
}

type GlobalTierUnitContainer struct {
	instanceType    *schema.InstanceType
	storageTemplate *schema.StorageTemplate
	tierUnit        *schema.ComponentTierUnit
	conditions      []schema.TierUnitCondition
}

func toTierUnit(
	tierUnit *coresvcmodel.TierUnit,
	tierUnitInfo *tierUnitInfoFromTierUnitMapping,
	storages []coresvcmodel.InfraStorage,
	computes []coresvcmodel.InfraCompute,
	pd2TikvTierUnit map[string]map[string]any,
	pdCount map[string]int,
) (*GlobalTierUnitContainer, error) {
	if tierUnit.DeployType == "shared" {
		return nil, fmt.Errorf("deploy type is shared, err: %w", ErrDeployTypeShared)
	}

	if tierUnitInfo == nil {
		return nil, fmt.Errorf("unused tierunit %s", tierUnit.TierUnitKey)
	}

	coreSvcStorage, ok := lo.Find(storages, func(item coresvcmodel.InfraStorage) bool {
		return item.ID.Uint64() == tierUnit.InfraStorageID.Uint64()
	})
	if !ok {
		return nil, fmt.Errorf("tierunit %s not found the infra storage", tierUnit.TierUnitKey)
	}

	if coreSvcStorage.InstanceType == "local-storage" &&
		(tierUnit.ComponentType == coresvcmodel.TiFlash || tierUnit.ComponentType == coresvcmodel.TiKV || tierUnit.ComponentType == coresvcmodel.PD) {
		tilog.PlainWarn(
			"skip local storage",
			zap.Uint64("storage in coresvc", coreSvcStorage.ID.Uint64()),
			zap.String("tierunit", tierUnit.TierUnitKey),
		)
		return nil, fmt.Errorf(
			"local storage, tierunit %s, storage id: %d, err: %w",
			tierUnit.TierUnitKey,
			coreSvcStorage.ID.Uint64(),
			ErrToSkuLocalStorage,
		)
	}

	coreSvcCompute, ok := lo.Find(computes, func(item coresvcmodel.InfraCompute) bool {
		return item.ID.Uint64() == tierUnit.InfraComputeID.Uint64()
	})
	if !ok {
		return nil, fmt.Errorf("tierunit %s not found the infra compute", tierUnit.TierUnitKey)
	}

	globalStorage := &schema.StorageTemplate{
		Model: gorm.Model{
			ID:        uint(coreSvcStorage.ID.Uint64()),
			CreatedAt: coreSvcStorage.CreatedAt,
		},
		ProviderId: tierUnit.ProviderID.Uint64(),
		VolumeType: coreSvcStorage.InstanceType,
	}

	globalCompute := &schema.InstanceType{
		Model: gorm.Model{
			ID:        uint(coreSvcCompute.ID.Uint64()),
			CreatedAt: coreSvcCompute.CreatedAt,
		},
		InstanceType: coreSvcCompute.InstanceType,
		ProviderId:   tierUnit.ProviderID.Uint64(),
		CPUNum:       coreSvcCompute.CPUNum,
		MemorySizeGi: coreSvcCompute.MemorySizeGi,
		CpuArch:      string(coreSvcCompute.CpuArch),
	}

	globalTierUnit := &schema.ComponentTierUnit{
		Model: gorm.Model{
			ID:        uint(tierUnit.ID.Uint64()),
			CreatedAt: tierUnit.CreatedAt,
		},
		Key:             tierUnit.TierUnitKey,
		ComponentType:   tierUnitInfo.Component,
		ProviderId:      tierUnitInfo.ProviderId,
		ComponentTierId: tierUnitInfo.TierId,

		InstanceTypeId:    coreSvcCompute.ID.Uint64(),
		StorageTemplateId: lo.ToPtr(coreSvcStorage.ID.Uint64()),

		DefaultNodeNum: uint32(tierUnit.DefaultNodeNum),

		MinStorageGi:      uint32(tierUnit.MinStorageGi),
		MaxStorageGi:      uint32(tierUnit.MaxStorageGi),
		DefaultStorageGi:  uint32(tierUnit.DefaultStorageGi),
		DefaultIops:       coreSvcStorage.IOPS,
		DefaultThroughPut: lo.EmptyableToPtr(coreSvcStorage.ThroughputMi),
	}

	if tierUnit.PD != nil && !lo.IsEmpty(*tierUnit.PD) {
		if tierUnit.ComponentType != "tikv" {
			return nil, fmt.Errorf("pd attach not tikv tierunit %s", tierUnit.TierUnitKey)
		}

		count := pdCount[tierUnit.PD.TierUnitKey]
		if tierUnit.PD.NodeNum > count {
			pdCount[tierUnit.PD.TierUnitKey] = tierUnit.PD.NodeNum
		}

		tikvTierUnits, ok := pd2TikvTierUnit[tierUnit.PD.TierUnitKey]
		if !ok {
			tikvTierUnits = make(map[string]any)
		}
		tikvTierUnits[tierUnit.TierUnitKey] = struct{}{}
		pd2TikvTierUnit[tierUnit.PD.TierUnitKey] = tikvTierUnits
	}

	return &GlobalTierUnitContainer{
		instanceType:    globalCompute,
		storageTemplate: globalStorage,
		tierUnit:        globalTierUnit,
	}, nil
}

func getConditionValue(arr []string) string {
	data, err := json.Marshal(arr)
	if err != nil {
		tilog.PlainError("marshal data failed", zap.Any("arr", arr), zap.Error(err))
		panic(err)
	}

	return string(data)
}
