package cluster

import (
	"database/sql"
	"encoding/json"
	"errors"
	"strconv"

	"github.com/samber/lo"
	"google.golang.org/protobuf/types/known/timestamppb"
	"gorm.io/gorm"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/common"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
	spucvt "github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/spu/cvt"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
)

func cvtClusterEntityToModel(
	en *entity.Cluster,
	providerId, regionId uint64,
	cmekId *uint64,
) (*schema.Cluster, bizerr.BizError) {
	if en == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster is nil")
	}

	m := &schema.Cluster{
		ID:                             en.ClusterId,
		TenantId:                       en.OrganizationId,
		ProjectId:                      en.ProjectId,
		ProviderId:                     providerId,
		RegionId:                       regionId,
		Port:                           en.Port,
		State:                          en.State.String(),
		OnNewArch:                      en.OnNewArch,
		CustomerManagedEncryptionKeyId: cmekId,
		EncryptedCloudAdminPassword:    en.EncryptedCloudAdminPassword,
		HasRootPassword:                en.HasRootPassword,
		ClusterVersion:                 en.Version,
		ClusterIdentifierHash:          en.ClusterIdentifierHash,
	}

	// full time
	if en.CreateTime.IsValid() {
		m.CreatedAt = en.CreateTime.AsTime()
	}

	if en.UpdateTime.IsValid() {
		m.UpdatedAt = en.UpdateTime.AsTime()
	}

	if en.DeleteTime.IsValid() {
		m.DeletedAt = gorm.DeletedAt{
			Valid: true,
			Time:  en.DeleteTime.AsTime(),
		}
	}

	if en.CreateFinishedTime.IsValid() {
		m.CreateFinishedAt = sql.NullTime{
			Valid: true,
			Time:  en.CreateFinishedTime.AsTime(),
		}
	}

	return m, nil
}

func cvtClusterModelToEntity(m *schema.Cluster) (*entity.Cluster, bizerr.BizError) {
	if m == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cluster is nil")
	}

	en := &entity.Cluster{
		Cluster: &globaldedicatedv1.Cluster{
			ClusterId:      m.ID,
			OrganizationId: m.TenantId,
			ProjectId:      m.ProjectId,
			CreateTime:     timestamppb.New(m.CreatedAt),
			UpdateTime:     timestamppb.New(m.UpdatedAt),
			Port:           m.Port,
			State: globaldedicatedv1.Cluster_State(
				globaldedicatedv1.Cluster_State_value[m.State],
			),
			Version:                     m.ClusterVersion,
			HasRootPassword:             m.HasRootPassword,
			ClusterComponents:           []*globaldedicatedv1.Cluster_ClusterComponent{},
			RegionDisplayName:           m.Region.DisplayName,
			EncryptedCloudAdminPassword: m.EncryptedCloudAdminPassword,
		},
		OnNewArch:             m.OnNewArch,
		ClusterIdentifierHash: m.ClusterIdentifierHash,
		CurrentComponentSkus:  make(map[string]*entity.Sku),
	}

	if m.DeletedAt.Valid {
		en.DeleteTime = timestamppb.New(m.DeletedAt.Time)
	} else {
		en.DeleteTime = nil
	}

	if m.CreateFinishedAt.Valid {
		en.CreateFinishedTime = timestamppb.New(m.CreateFinishedAt.Time)
	} else {
		en.CreateFinishedTime = nil
	}

	provider, err := enum.FromString[globalcommonv1.CloudProvider_Name](m.Provider.Name)
	if err != nil {
		return nil, bizerr.WrapError(err, "convert cloud provider")
	}
	en.CloudProviderId = globalcommonv1.CloudProvider_Name(provider)
	region, err := enum.FromString[globalcommonv1.Region_Name](m.Region.RegionName)
	if err != nil {
		return nil, bizerr.WrapError(err, "convert region")
	}
	en.RegionId = globalcommonv1.Region_Name(region)
	if m.CustomerManagedEncryptionKey != nil {
		en.CustomerManagedEncryptionKeyId = strconv.FormatUint(uint64(m.CustomerManagedEncryptionKey.ID), 10)
	} else {
		en.CustomerManagedEncryptionKeyId = ""
	}

	for i := range m.ClusterComponents {
		cmEn, err := cvtComponentModelToEntity(&m.ClusterComponents[i])
		if err != nil {
			return nil, err
		}

		en.ClusterComponents = append(en.ClusterComponents, cmEn)

		sku, err_ := spucvt.SkuModelToEn(&m.ClusterComponents[i].ComponentTierUnit)
		if err_ != nil {
			return nil, bizerr.WrapError(err_, "convert sku model to entity")
		}

		en.CurrentComponentSkus[cmEn.GetSkuKey()] = sku
	}

	return en, nil
}

func cvtComponentEntityToModel(en *globaldedicatedv1.Cluster_ClusterComponent) (*schema.ClusterComponent, error) {
	if en == nil {
		return nil, errors.New("cluster component is nil")
	}
	if en.GetSkuKey() == "" {
		return nil, errors.New("sku key of cluster component is empty")
	}
	if en.GetClusterId() == 0 {
		return nil, errors.New("cluster id of cluster component is empty")
	}

	m := &schema.ClusterComponent{
		Model: gorm.Model{
			ID: uint(en.Id),
		},
		WorkPoolId:           en.GetTidbGroupId(),
		ComponentType:        en.GetComponentType().String(),
		ClusterId:            en.GetClusterId(),
		ComponentTierKey:     en.GetSpuKey(),
		ComponentTierUnitKey: en.GetSkuKey(),
		Count:                en.GetCount(),
	}

	if en.GetCreateTime().IsValid() {
		m.CreatedAt = en.GetCreateTime().AsTime()
	}

	return m, nil
}

func cvtComponentModelToEntity(
	cm *schema.ClusterComponent,
) (*globaldedicatedv1.Cluster_ClusterComponent, bizerr.BizError) {
	if cm == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cluster component is nil")
	}

	cType, err := enum.FromString[globaldedicatedv1.ComponentType](cm.ComponentType)
	if err != nil {
		return nil, bizerr.WrapError(err, "convert component type")
	}
	cpuArch, err := enum.FromString[globaldedicatedv1.InstanceMu_CpuArch](cm.ComponentTierUnit.InstanceType.CpuArch)
	if err != nil {
		return nil, bizerr.WrapError(err, "convert cpu arch")
	}

	instanceCpuGeneration, err := common.GetInstanceCpuGeneration(&cm.ComponentTierUnit)
	if err != nil {
		return nil, bizerr.WrapError(err, "get instance cpu generation")
	}

	ce := &globaldedicatedv1.Cluster_ClusterComponent{
		Id:                    uint64(cm.ID),
		CreateTime:            timestamppb.New(cm.CreatedAt),
		DeleteTime:            nil, // fill later
		ClusterId:             cm.ClusterId,
		ComponentType:         globaldedicatedv1.ComponentType(cType),
		SpuName:               common.GetSpuName(cm.ComponentTierKey),
		SpuKey:                cm.ComponentTierUnit.ComponentTier.Key,
		SpuDisplayName:        cm.ComponentTierUnit.ComponentTier.DisplayName,
		SpuVersion:            cm.ComponentTierUnit.ComponentTier.Version,
		SkuKey:                cm.ComponentTierUnit.Key,
		Count:                 lo.EmptyableToPtr(cm.Count),
		Storages:              nil, // fill later
		TidbGroupId:           cm.WorkPoolId,
		IsDefaultTidbGroup:    cm.WorkPool.Default,
		TidbGroupDisplayName:  cm.WorkPool.DisplayName,
		InstanceType:          cm.ComponentTierUnit.InstanceType.InstanceType,
		CpuNum:                cm.ComponentTierUnit.InstanceType.CPUNum,
		MemorySizeGi:          cm.ComponentTierUnit.InstanceType.MemorySizeGi,
		InstanceCpuArch:       globaldedicatedv1.InstanceMu_CpuArch(cpuArch),
		InstanceCpuGeneration: instanceCpuGeneration,
		ModifyProgress:        nil,
		MeteringSkuKeys:       nil,
	}

	if cm.DeletedAt.Valid {
		ce.DeleteTime = timestamppb.New(cm.DeletedAt.Time)
	}

	// Return directly if component does not have storage
	if _, ok := entity.ComponentsHaveStorage[globaldedicatedv1.ComponentType(cType)]; !ok {
		return ce, nil
	}

	// fill Storages
	for _, storage := range cm.Storages {
		storageType, err := enum.FromString[globaldedicatedv1.Sku_DiskType](storage.StorageType)
		if err != nil {
			return nil, bizerr.WrapError(err, "convert storage type")
		}
		storageEn, err := cvtComponentStorageModelToEntity(storage, &cm.ComponentTierUnit)
		if err != nil {
			return nil, bizerr.WrapError(err, "convert storage")
		}
		if ce.GetStorages() == nil {
			ce.Storages = make(map[string]*globaldedicatedv1.Cluster_ClusterComponent_Storage)
		}
		ce.GetStorages()[globaldedicatedv1.Sku_DiskType(storageType).String()] = storageEn
	}

	return ce, nil
}

func cvtComponentStorageModelToEntity(
	storage *schema.ClusterComponentStorage,
	sku *schema.ComponentTierUnit,
) (*entity.ClusterComponentStorage, error) {
	if storage == nil {
		return nil, errors.New("cluster component storage is nil")
	}

	// Compatibility: Old component storage model doesn't have size, iops and throughput fields.
	if storage.StorageType == globaldedicatedv1.Sku_DATA.String() {
		if storage.SizeGi == 0 {
			storage.SizeGi = sku.DefaultStorageGi
		}
		if storage.VolumeType == nil || *storage.VolumeType == "" {
			storage.VolumeType = lo.ToPtr(sku.StorageTemplate.VolumeType)
		}
		if storage.Iops == nil || *storage.Iops == 0 {
			storage.Iops = &sku.DefaultIops
		}
		if storage.ThroughPutMibps == nil || *storage.ThroughPutMibps == 0 {
			storage.ThroughPutMibps = sku.DefaultThroughPut
		}
	}

	vt, err := enum.FromString[globaldedicatedv1.StorageMu_VolumeType](lo.FromPtr(storage.VolumeType))
	if err != nil {
		return nil, err
	}
	dt, err := enum.FromString[globaldedicatedv1.Sku_DiskType](storage.StorageType)
	if err != nil {
		return nil, err
	}

	return &entity.ClusterComponentStorage{
		StorageId:       uint64(storage.ID),
		ComponentId:     storage.ClusterComponentId,
		DiskType:        globaldedicatedv1.Sku_DiskType(dt),
		VolumeType:      lo.ToPtr(globaldedicatedv1.StorageMu_VolumeType(vt)),
		SizeGi:          storage.SizeGi,
		Iops:            lo.FromPtr(storage.Iops),
		ThroughputMibps: lo.FromPtr(storage.ThroughPutMibps),
		AdditionalIops:  lo.EmptyableToPtr(storage.AdditionalIops),
	}, nil
}

func cvtComponentStorageEntityToModel(
	storage *entity.ClusterComponentStorage,
) (*schema.ClusterComponentStorage, error) {
	if storage == nil {
		return nil, errors.New("cluster component storage is nil")
	}

	s := &schema.ClusterComponentStorage{
		Model: gorm.Model{
			ID: uint(storage.GetStorageId()),
		},
		ClusterComponentId: storage.GetComponentId(),
		StorageType:        storage.GetDiskType().String(),
		VolumeType:         lo.ToPtr(storage.VolumeType.String()),
		SizeGi:             storage.SizeGi,
		Iops:               lo.EmptyableToPtr(storage.Iops),
		ThroughPutMibps:    lo.EmptyableToPtr(storage.ThroughputMibps),
		AdditionalIops:     storage.GetAdditionalIops(),
	}

	return s, nil
}

func clusterOperationModelToEn(m *schema.ClusterOperation) (*entity.ClusterOperation, bizerr.BizError) {
	if m == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cluster operation is nil")
	}

	out := &entity.ClusterOperation{
		ID:          uint64(m.ID),
		OperatorID:  m.OperatorID,
		ClusterID:   m.ClusterID,
		Type:        0, // fill later
		CreatedAt:   m.CreatedAt,
		CompletedAt: m.CompletedAt,
		State:       0,   // fill later
		Operation:   nil, // fill later
	}

	// fill Type
	operationType, err := enum.FromString[globaldedicatedv1.ClusterOperationType](m.Type)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "operation type %s is not supported", m.Type)
	}
	out.Type = globaldedicatedv1.ClusterOperationType(operationType)

	// fill State
	operationState, err := enum.FromString[globaldedicatedv1.ClusterOperationState](m.Status)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "operation state %s is not supported", m.Status)
	}
	out.State = globaldedicatedv1.ClusterOperationState(operationState)

	// fill Operation
	if out.Type == globaldedicatedv1.ClusterOperationType_SCALE && m.Content != "" {
		var operation entity.ScaleOperation
		err = json.Unmarshal([]byte(m.Content), &operation)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to unmarshal scale operation: %s", err)
		}
		out.Operation = &operation
	}

	return out, nil
}

func clusterOperationEnToModel(en *entity.ClusterOperation) (*schema.ClusterOperation, bizerr.BizError) {
	if en == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cluster operation is nil")
	}

	out := &schema.ClusterOperation{
		Model: gorm.Model{
			ID: uint(en.ID),
		},
		ClusterID:   en.ClusterID,
		Cluster:     nil, // leave it empty
		OperatorID:  en.OperatorID,
		Type:        en.Type.String(),
		CompletedAt: en.CompletedAt,
		Status:      en.State.String(),
		Content:     "", // fill later
	}

	// fill Content
	if en.Operation != nil {
		content, err := json.Marshal(en.Operation)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to marshal operation: %s", err)
		}
		out.Content = string(content)
	}

	return out, nil
}

func clusterSnapshotModelToEn(snapshot *schema.ClusterSnapshot) (*entity.ClusterSnapshot, bizerr.BizError) {
	if snapshot == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cluster snapshot is nil")
	}
	if snapshot.Cluster == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cluster of cluster snapshot is nil")
	}

	c, err := cvtClusterModelToEntity(snapshot.Cluster)
	if err != nil {
		return nil, err
	}

	out := &entity.ClusterSnapshot{
		ClusterSnapshot: &globaldedicatedv1.ClusterSnapshot{
			Id:                uint64(snapshot.ID),
			TenantId:          snapshot.TenantID,
			ProjectId:         snapshot.ProjectID,
			CloudProviderName: snapshot.Cluster.Provider.Name,
			ClusterId:         snapshot.ClusterID,
		},
		Cluster: c,
	}

	// Build components. Different disk type is saved as different component state, and they are merged into one cluster component.
	comps := make(
		map[globaldedicatedv1.ComponentType]*globaldedicatedv1.Cluster_ClusterComponent,
		len(snapshot.ComponentStates.Components),
	)
	for _, each := range snapshot.ComponentStates.Components {
		componentType, err := enum.FromString[globaldedicatedv1.ComponentType](each.ComponentType)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unknown component type %s", each.ComponentType)
		}

		if _, ok := comps[globaldedicatedv1.ComponentType(componentType)]; !ok {
			comps[globaldedicatedv1.ComponentType(componentType)] = &globaldedicatedv1.Cluster_ClusterComponent{
				ClusterId:     snapshot.ClusterID,
				ComponentType: globaldedicatedv1.ComponentType(componentType),
				SpuName:       common.GetSpuName(each.SpuKey),
				SpuKey:        each.SpuKey,
				SkuKey:        each.SkuKey,
				Count:         lo.EmptyableToPtr(each.Replicas),
			}
		}

		if _, ok := entity.ComponentsHaveStorage[globaldedicatedv1.ComponentType(componentType)]; !ok {
			continue
		}

		// set default storage type
		if each.StorageDiskType == "" {
			each.StorageDiskType = globaldedicatedv1.Sku_DATA.String()
		}

		diskType, err := enum.FromString[globaldedicatedv1.Sku_DiskType](each.StorageDiskType)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "unknown storage type %s", each.StorageDiskType)
		}

		vt, err := enum.FromString[globaldedicatedv1.StorageMu_VolumeType](each.StorageVolumeType)
		if err != nil {
			return nil, bizerr.WrapError(err, "convert volume type")
		}

		comp := comps[globaldedicatedv1.ComponentType(componentType)]
		if comp.GetStorages() == nil {
			comp.Storages = make(map[string]*globaldedicatedv1.Cluster_ClusterComponent_Storage)
		}
		comp.GetStorages()[each.StorageDiskType] = &globaldedicatedv1.Cluster_ClusterComponent_Storage{
			SizeGi:          each.StorageSizeGi,
			Iops:            each.StorageIOPS,
			ThroughputMibps: each.StorageThroughputMibps,
			VolumeType:      lo.ToPtr(globaldedicatedv1.StorageMu_VolumeType(vt)),
			AdditionalIops:  lo.EmptyableToPtr(each.StorageAdditionalIops),
			DiskType:        globaldedicatedv1.Sku_DiskType(diskType),
		}
	}

	// Currently snapshot doesn't save multiple tidb groups, so tidb or tiproxy only have the default group.
	out.Components = lo.Values(comps)

	return out, nil
}

func clusterSnapshotEnToModel(snapshot *entity.ClusterSnapshot) *schema.ClusterSnapshot {
	out := &schema.ClusterSnapshot{
		Model: gorm.Model{
			ID: uint(snapshot.GetId()),
		},
		TenantID:        snapshot.GetTenantId(),
		ProjectID:       snapshot.GetProjectId(),
		ClusterID:       snapshot.GetClusterId(),
		ComponentStates: schema.ComponentStates{},
		Cluster:         nil, // leave it empty
	}

	for _, each := range snapshot.Components {
		if each.GetCount() == 0 {
			continue
		}

		if _, ok := entity.ComponentsHaveStorage[each.GetComponentType()]; !ok {
			// If component not have storage, just append state without storage info.
			out.ComponentStates.Components = append(out.ComponentStates.Components, schema.ComponentState{
				ComponentType: each.GetComponentType().String(),
				SpuKey:        each.GetSpuKey(),
				SkuKey:        each.GetSkuKey(),
				Replicas:      each.GetCount(),
			})
		} else {
			// If component have storage, for each storage append a state with storage info.
			for diskType, storage := range each.GetStorages() {
				if storage.GetVolumeType() == globaldedicatedv1.StorageMu_VOLUME_TYPE_UNSPECIFIED {
					// If volume type is unspecified, skip this storage.
					continue
				}
				out.ComponentStates.Components = append(out.ComponentStates.Components, schema.ComponentState{
					ComponentType: each.GetComponentType().String(),
					SpuKey:        each.GetSpuKey(),
					SkuKey:        each.GetSkuKey(),
					Replicas:      each.GetCount(),

					StorageSizeGi:          storage.GetSizeGi(),
					StorageIOPS:            storage.GetIops(),
					StorageDiskType:        diskType,
					StorageThroughputMibps: storage.GetThroughputMibps(),
					StorageAdditionalIops:  storage.GetAdditionalIops(),
					StorageVolumeType:      storage.GetVolumeType().String(),
				})
			}
		}
	}

	return out
}

func cvtClusterPausePlanEntityToModel(en *entity.ClusterPausePlan, clusterId uint64) (*schema.ClusterPausePlan, error) {
	if en == nil {
		return nil, errors.New("cluster pause plan is nil")
	}

	m := &schema.ClusterPausePlan{
		ClusterID: clusterId,
		PauseAt:   en.PauseTime.AsTime(),
		Plan:      en.PausePlan.String(),
	}

	if en.Id > 0 {
		m.ID = uint(en.Id)
	}

	return m, nil
}

func cvtClusterPausePlanModelToEntity(m *schema.ClusterPausePlan) (*entity.ClusterPausePlan, error) {
	enumPlan, err := enum.FromString[globaldedicatedv1.Cluster_PausePlan_Type](m.Plan)
	if err != nil {
		return nil, err
	}

	return &entity.ClusterPausePlan{
		Id: uint64(m.ID),
		Cluster_PausePlan: &globaldedicatedv1.Cluster_PausePlan{
			PauseTime: timestamppb.New(m.PauseAt),
			PausePlan: globaldedicatedv1.Cluster_PausePlan_Type(enumPlan),
		},
	}, nil
}
