package spu

import (
	"context"
	"errors"
	"strconv"

	"github.com/samber/lo"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"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/domain/repository/spu"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
	"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"
)

type skuRepoImpl struct {
	provider db.Provider
}

func NewSkuRepo(provider db.Provider) spu.SkuRepo {
	return &skuRepoImpl{
		provider: provider,
	}
}

func (tu *skuRepoImpl) Save(ctx context.Context, en *entity.Sku) (uint, bizerr.BizError) {
	wdb := tu.provider.WriteDB(ctx)

	// Query provider first to get their IDs.
	var providerModel schema.Provider
	if err := tu.provider.WriteDB(ctx).Take(&providerModel, "name = ?", en.CloudProvider.String()).Error; err != nil {
		return 0, bizerr.WrapGormError(err, "load provider failed")
	}

	skuModel := cvt.SkuEnToModel(en, uint64(providerModel.ID))

	var err error
	if skuModel.ID != 0 {
		err = wdb.Omit("created_at").Save(skuModel).Error
	} else {
		err = wdb.Save(skuModel).Error
	}
	if err != nil {
		return 0, bizerr.WrapGormError(err, "save sku failed")
	}

	// Save conditions.
	for _, condition := range en.Conditions {
		cond, err := cvt.SkuConditionEnToModel(condition, uint64(skuModel.ID))
		if err != nil {
			return 0, bizerr.WrapError(err, "convert condition failed")
		}
		existing := &schema.TierUnitCondition{}
		err = wdb.Where("`component_tier_unit_id` = ? AND `key` = ?", cond.ComponentTierUnitId, cond.Key).
			First(existing).
			Error
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, bizerr.WrapGormError(err, "query sku condition failed")
		}
		cond.ID = existing.ID
		cond.CreatedAt = existing.CreatedAt
		if err := wdb.Save(cond).Error; err != nil {
			return 0, bizerr.WrapError(err, "create sku condition failed")
		}
	}

	return skuModel.ID, nil
}

func (tu *skuRepoImpl) QuerySpuMappings(
	ctx context.Context,
	spuIds []uint64,
) (map[uint64][]*entity.SpuMapping, bizerr.BizError) {
	rdb := tu.provider.ReadDB(ctx)

	var spuMappings []*schema.TierTierUnit
	if err := rdb.Model(&schema.TierTierUnit{}).
		Where("`component_tier_id` IN ?", spuIds).
		Find(&spuMappings).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "query mapping failed")
	}

	spuMappingMap := lo.KeyBy(spuMappings, func(each *schema.TierTierUnit) uint64 { return each.ComponentTierId })

	out := make(map[uint64][]*entity.SpuMapping)

	for spuId, mapping := range spuMappingMap {
		mappingEns := make([]*entity.SpuMapping, 0)
		for i := 0; i < len(mapping.TierUnitMapping.Items); i++ {
			item := mapping.TierUnitMapping.Items[i]

			provider, err := enum.FromString[globalcommonv1.CloudProvider_Name](item.ProviderName)
			if err != nil {
				return nil, bizerr.WrapError(
					err,
					"failed to convert provider %s for mapping %d",
					item.ProviderName,
					spuId,
				)
			}
			if globalcommonv1.CloudProvider_Name(provider) == globalcommonv1.CloudProvider_NAME_UNSPECIFIED {
				return nil, bizerr.New(
					bizerr.ErrCodeParams,
					"invalid provider %s for mapping %d",
					item.ProviderName,
					spuId,
				)
			}

			regions := make(map[globalcommonv1.Region_Name]bool)
			for _, regionName := range item.RegionNames {
				region, err := enum.FromString[globalcommonv1.Region_Name](regionName)
				if err != nil {
					return nil, bizerr.WrapError(err, "failed to convert region %s for mapping %d", regionName, spuId)
				}
				// Allow "" or "NAME_UNSPECIFIED" region string here. "" or "NAME_UNSPECIFIED" region string will be unmarshalled to UNSPECIFIED, and means all regions.
				regions[globalcommonv1.Region_Name(region)] = true
			}

			tenantIDs := make(map[uint64]bool)
			for _, tenantIDStr := range item.TenantIDs {
				tid, err := strconv.ParseUint(tenantIDStr, 10, 64)
				if err != nil {
					return nil, bizerr.WrapError(err, "failed to convert tenant %s for mapping %d", tenantIDStr, spuId)
				}
				tenantIDs[tid] = true
			}

			mappingEns = append(mappingEns, &entity.SpuMapping{
				SpuId:     spuId,
				SkuKey:    item.SkuKey,
				Provider:  globalcommonv1.CloudProvider_Name(provider),
				Regions:   regions,
				TenantIDs: tenantIDs,
			})
		}

		out[spuId] = mappingEns
	}

	return out, nil
}

func (tu *skuRepoImpl) Query(ctx context.Context, filter *spu.SkuFilter) ([]*entity.Sku, bizerr.BizError) {
	rdb := tu.provider.ReadDB(ctx)

	if filter.Provider != globalcommonv1.CloudProvider_NAME_UNSPECIFIED {
		rdb = rdb.Where("`Provider`.`name` = ?", filter.Provider.String())
	}

	if filter.Component != globaldedicatedv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED {
		rdb = rdb.Where("`component_tier_units`.`component_type` = ?", filter.Component.String())
	}

	if filter.SpuId != nil {
		rdb = rdb.Where("`component_tier_units`.`component_tier_id` = ?", *filter.SpuId)
	}

	if len(filter.Providers) > 0 {
		providerNames := lo.Map(filter.Providers, func(p globalcommonv1.CloudProvider_Name, _ int) string {
			return p.String()
		})
		rdb = rdb.Where("`Provider`.`name` IN ?", providerNames)
	}

	if len(filter.Components) > 0 {
		componentTypes := lo.Map(filter.Components, func(c globaldedicatedv1.ComponentType, _ int) string {
			return c.String()
		})
		rdb = rdb.Where("`component_tier_units`.`component_type` IN ?", componentTypes)
	}

	var tierUnits []*schema.ComponentTierUnit
	if err := rdb.Model(&schema.ComponentTierUnit{}).
		InnerJoins("Provider").
		Joins("ComponentTier").
		Joins("InstanceType").
		Joins("StorageTemplate").
		Joins("RaftStorageTemplate").
		Preload("TierUnitConditions").
		Find(&tierUnits).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "query sku failed")
	}

	out := make([]*entity.Sku, 0)
	for _, tierUnit := range tierUnits {
		en, err := cvt.SkuModelToEn(tierUnit)
		if err != nil {
			return nil, bizerr.WrapError(err, "convert sku failed")
		}

		out = append(out, en)
	}

	return out, nil
}

func (tu *skuRepoImpl) LoadByKeys(ctx context.Context, keys []string) (map[string]*entity.Sku, bizerr.BizError) {
	keys = lo.Uniq(lo.Filter(keys, func(k string, _ int) bool { return k != "" }))

	if len(keys) == 0 {
		tilog.Warn(ctx, "empty keys")
		return make(map[string]*entity.Sku), nil
	}

	rdb := tu.provider.ReadDB(ctx)

	var skus []*schema.ComponentTierUnit
	if err := rdb.Model(&schema.ComponentTierUnit{}).
		Where("`component_tier_units`.`key` in ?", keys).
		InnerJoins("Provider").
		Joins("ComponentTier").
		Joins("InstanceType").
		Joins("StorageTemplate").
		Joins("RaftStorageTemplate").
		Preload("TierUnitConditions").
		Find(&skus).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "query sku failed")
	}

	out := make(map[string]*entity.Sku)
	for _, tierUnit := range skus {
		en, err := cvt.SkuModelToEn(tierUnit)
		if err != nil {
			return nil, bizerr.WrapError(err, "convert sku failed")
		}

		out[en.Key] = en
	}

	if len(out) != len(keys) {
		return nil, bizerr.New(bizerr.ErrCodeParams, "missing skus. names=%v", keys)
	}

	return out, nil
}

func (tu *skuRepoImpl) Delete(ctx context.Context, id uint64) bizerr.BizError {
	wdb := tu.provider.WriteDB(ctx)
	err := wdb.Transaction(func(tx *gorm.DB) error {
		if err := tx.Delete(&schema.TierTierUnit{}, "component_tier_unit_id = ?", id).Error; err != nil {
			return bizerr.WrapGormError(err, "delete tier unit mapping failed")
		}
		if err := tx.Delete(&schema.ComponentTierUnit{}, id).Error; err != nil {
			return bizerr.WrapGormError(err, "delete sku failed")
		}
		return nil
	})
	if err != nil {
		return bizerr.WrapGormError(err, "delete sku failed")
	}
	return nil
}

func (tu *skuRepoImpl) Get(ctx context.Context, id uint64) (*entity.Sku, bizerr.BizError) {
	rdb := tu.provider.ReadDB(ctx)

	var tierUnit *schema.ComponentTierUnit
	if err := rdb.Model(&schema.ComponentTierUnit{}).
		InnerJoins("Provider").
		Joins("ComponentTier").
		Joins("InstanceType").
		Joins("StorageTemplate").
		Joins("RaftStorageTemplate").
		Preload("TierUnitConditions").
		Where("`component_tier_units`.`id` = ?", id).
		First(&tierUnit).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "get sku failed")
	}

	en, err := cvt.SkuModelToEn(tierUnit)
	if err != nil {
		return nil, bizerr.WrapError(err, "convert sku failed")
	}
	return en, nil
}
