package spu

import (
	"context"

	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/domain/repository/spu"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (s *skuServiceImpl) GetSku(
	ctx context.Context,
	req *globaldedicatedv1.GetSkuRequest,
) (*entity.Sku, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.GetSku")
	defer span.End()

	ctu, err := s.skuRepo.Get(ctx, req.GetId())
	if err != nil {
		return nil, err
	}
	return ctu, nil
}

func (s *skuServiceImpl) ListSkus(
	ctx context.Context,
	req *globaldedicatedv1.ListSkusRequest,
) (*globaldedicatedv1.ListSkusResponse, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.ListSkus")
	defer span.End()

	skus, err := s.skuRepo.Query(ctx, &spu.SkuFilter{
		Providers:  req.GetCloudProviders(),
		Components: req.GetComponentTypes(),
	})
	if err != nil {
		return nil, err
	}

	return &globaldedicatedv1.ListSkusResponse{
		Skus: skus,
	}, nil
}

func (s *skuServiceImpl) DeleteSku(ctx context.Context, id uint64) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "Domain.DeleteSku")
	defer span.End()

	return s.skuRepo.Delete(ctx, id)
}

func (s *skuServiceImpl) CreateSku(ctx context.Context, req *globaldedicatedv1.Sku) (*entity.Sku, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.CreateSku")
	defer span.End()

	// SPU fk check.
	_, err := s.spuRepo.Get(ctx, req.GetSpuId())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "spu not found")
	}

	// Required field check.
	if req.GetCloudProvider() == globalcommonv1.CloudProvider_NAME_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cloud provider is required")
	}
	if req.GetComponentType() == globaldedicatedv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "component type is required")
	}
	if req.GetDefaultNodeCount() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "default node count is required")
	}

	// Instance check.
	_, err = s.instanceMuRepo.Get(ctx, req.GetInstanceType())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "instance type not found")
	}

	// Storage check.
	if err := s.skuStorageCheck(ctx, req); err != nil {
		return nil, err
	}

	id, err := s.skuRepo.Save(ctx, req)
	if err != nil {
		return nil, err
	}
	req.Id = uint64(id)

	return req, nil
}

func (s *skuServiceImpl) skuStorageCheck(ctx context.Context, req *globaldedicatedv1.Sku) bizerr.BizError {
	if _, ok := entity.ComponentsHaveStorage[req.GetComponentType()]; !ok {
		// No need to check storage. Explicitly set to empty.
		req.DataStorage = nil
		req.RaftlogStorage = nil
	} else {
		// Check data storage.
		_, err := s.storageMuRepo.Get(ctx, req.GetDataStorage().GetStorageMuId())
		if err != nil {
			return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "storage not found")
		}
		if err := commonSkuStorageCheck(req.GetDataStorage()); err != nil {
			return bizerr.WrapError(err, "invalid storage")
		}
		// Check raft log storage. Raft log storage is optional.
		if req.GetRaftlogStorage().GetStorageMuId() != 0 {
			_, err := s.storageMuRepo.Get(ctx, req.GetRaftlogStorage().GetStorageMuId())
			if err != nil {
				return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "storage not found")
			}
			if err := commonSkuStorageCheck(req.GetRaftlogStorage()); err != nil {
				return bizerr.WrapError(err, "invalid storage")
			}
		} else {
			req.RaftlogStorage = nil
		}
	}

	return nil
}

func commonSkuStorageCheck(storage *globaldedicatedv1.Sku_Storage) bizerr.BizError {
	if storage.GetSizeGiDefault() == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "size is required")
	}
	if storage.GetSizeGiMax() == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "max size is required")
	}
	if storage.GetSizeGiMin() == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "min size is required")
	}

	// gcp does not support setting iops. Just ignore this check.
	if storage.GetVolumeType() != globaldedicatedv1.StorageMu_PD_SSD {
		switch i := storage.GetIops().(type) {
		case *globaldedicatedv1.Sku_Storage_IopsFixed_:
			if i.IopsFixed.GetIops() == 0 {
				return bizerr.New(bizerr.ErrCodeParams, "fixed iops is required")
			}
		case *globaldedicatedv1.Sku_Storage_IopsDynamic_:
			if i.IopsDynamic.GetIopsPerSizeGi() == 0 {
				return bizerr.New(bizerr.ErrCodeParams, "iops per size is required")
			}
		}
	}

	// aws io2, azure ultrassd, and gcp don't support setting throughput. Ignore this check for them.
	if storage.GetVolumeType() != globaldedicatedv1.StorageMu_IO2 &&
		storage.GetVolumeType() != globaldedicatedv1.StorageMu_ULTRASSD &&
		storage.GetVolumeType() != globaldedicatedv1.StorageMu_PD_SSD {
		switch t := storage.GetThroughput().(type) {
		case *globaldedicatedv1.Sku_Storage_ThroughputFixed_:
			if t.ThroughputFixed.GetThroughputMibps() == 0 {
				return bizerr.New(bizerr.ErrCodeParams, "fixed throughput is required")
			}
		case *globaldedicatedv1.Sku_Storage_ThroughputDynamic_:
			if t.ThroughputDynamic.GetThroughputMibpsPerSizeGi() == 0 {
				return bizerr.New(bizerr.ErrCodeParams, "throughput per size is required")
			}
			if t.ThroughputDynamic.GetThroughputMibpsPerIops() == 0 {
				return bizerr.New(bizerr.ErrCodeParams, "throughput per iops is required")
			}
		}
	}

	return nil
}

func (s *skuServiceImpl) UpdateSku(
	ctx context.Context,
	req *globaldedicatedv1.BatchUpsertSkusRequest_Update,
) (*entity.Sku, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.UpdateSku")
	defer span.End()

	existing, err := s.skuRepo.Get(ctx, req.GetId())
	if err != nil {
		return nil, err
	}

	if req.GetDefaultNodeCount() > 0 {
		existing.DefaultNodeCount = req.GetDefaultNodeCount()
	}
	if req.GetPriority() > 0 {
		existing.Priority = req.GetPriority()
	}
	if len(req.GetConditions()) > 0 {
		existing.Conditions = req.GetConditions()
	}

	commonUpdateSkuStorage(req.GetDataStorage(), existing.DataStorage)
	commonUpdateSkuStorage(req.GetRaftlogStorage(), existing.RaftlogStorage)

	if err := s.skuStorageCheck(ctx, existing); err != nil {
		return nil, err
	}

	id, err := s.skuRepo.Save(ctx, existing)
	if err != nil {
		return nil, err
	}
	existing.Id = uint64(id)

	return existing, nil
}

func commonUpdateSkuStorage(
	req *globaldedicatedv1.BatchUpsertSkusRequest_Update_Storage,
	exist *globaldedicatedv1.Sku_Storage,
) {
	if req.GetSizeGiDefault() > 0 {
		exist.SizeGiDefault = req.GetSizeGiDefault()
	}
	if req.GetSizeGiMax() > 0 {
		exist.SizeGiMax = req.GetSizeGiMax()
	}
	if req.GetSizeGiMin() > 0 {
		exist.SizeGiMin = req.GetSizeGiMin()
	}
	if req.GetIopsMax() > 0 {
		exist.IopsMax = req.GetIopsMax()
	}
	if req.GetIopsMin() > 0 {
		exist.IopsMin = req.GetIopsMin()
	}
	if req.GetThroughputMibpsMax() > 0 {
		exist.ThroughputMibpsMax = req.GetThroughputMibpsMax()
	}
	if req.GetThroughputMibpsMin() > 0 {
		exist.ThroughputMibpsMin = req.GetThroughputMibpsMin()
	}

	switch i := req.GetIops().(type) {
	case *globaldedicatedv1.BatchUpsertSkusRequest_Update_Storage_IopsFixed_:
		// If existing is not fixed iops, override it.
		if exist.GetIopsFixed() == nil {
			exist.Iops = &globaldedicatedv1.Sku_Storage_IopsFixed_{IopsFixed: &globaldedicatedv1.Sku_Storage_IopsFixed{}}
		}
		if i.IopsFixed.GetIops() > 0 {
			exist.GetIopsFixed().Iops = i.IopsFixed.GetIops()
		}
	case *globaldedicatedv1.BatchUpsertSkusRequest_Update_Storage_IopsDynamic_:
		// If existing is not dynamic iops, override it.
		if exist.GetIopsDynamic() == nil {
			exist.Iops = &globaldedicatedv1.Sku_Storage_IopsDynamic_{IopsDynamic: &globaldedicatedv1.Sku_Storage_IopsDynamic{}}
		}
		if i.IopsDynamic.GetIopsPerSizeGi() > 0 {
			exist.GetIopsDynamic().IopsPerSizeGi = i.IopsDynamic.GetIopsPerSizeGi()
		}
	}

	switch t := req.GetThroughput().(type) {
	case *globaldedicatedv1.BatchUpsertSkusRequest_Update_Storage_ThroughputFixed_:
		// If existing is not fixed throughput, override it.
		if exist.GetThroughputFixed() == nil {
			exist.Throughput = &globaldedicatedv1.Sku_Storage_ThroughputFixed_{ThroughputFixed: &globaldedicatedv1.Sku_Storage_ThroughputFixed{}}
		}
		if t.ThroughputFixed.GetThroughputMibps() > 0 {
			exist.GetThroughputFixed().ThroughputMibps = t.ThroughputFixed.GetThroughputMibps()
		}
	case *globaldedicatedv1.BatchUpsertSkusRequest_Update_Storage_ThroughputDynamic_:
		// If existing is not dynamic throughput, override it.
		if exist.GetThroughputDynamic() == nil {
			exist.Throughput = &globaldedicatedv1.Sku_Storage_ThroughputDynamic_{
				ThroughputDynamic: &globaldedicatedv1.Sku_Storage_ThroughputDynamic{},
			}
		}
		if t.ThroughputDynamic.GetThroughputMibpsPerSizeGi() > 0 {
			exist.GetThroughputDynamic().ThroughputMibpsPerSizeGi = t.ThroughputDynamic.GetThroughputMibpsPerSizeGi()
		}
		if t.ThroughputDynamic.GetThroughputMibpsPerIops() > 0 {
			exist.GetThroughputDynamic().ThroughputMibpsPerIops = t.ThroughputDynamic.GetThroughputMibpsPerIops()
		}
	}
}
