package repo

import (
	"context"
	"errors"

	"braces.dev/errtrace"
	"github.com/samber/lo"
	"github.com/tidbcloud/pkgv2/db"
	"gorm.io/gorm"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/infra/persistence/model"
	"github.com/tidbcloud/tidb-management-service/serverless-global-service/infra/persistence/repo/cvt"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

type regionRepoImpl struct {
	dbProvider db.TransProvider
}

func NewRegionRepoImpl(dbProvider db.TransProvider) repository.RegionRepo {
	return &regionRepoImpl{
		dbProvider: dbProvider,
	}
}

func (r *regionRepoImpl) LoadRegions(
	ctx context.Context,
	filter *repository.CloudRegionFilter,
) ([]*entity.Region, error) {
	var regions []*model.Region

	rdb := r.dbProvider.ReadDB(ctx).
		Model(&model.Region{}).
		InnerJoins("Provider").
		Preload("RegionTenants")

	if filter != nil && len(filter.CloudProviderNames) > 0 {
		rdb = rdb.Where(
			"`Provider`.`name` IN (?)",
			lo.Map(
				filter.CloudProviderNames,
				func(i globalcommonv1.CloudProvider_Name, _ int) string { return i.String() },
			),
		)
	}

	if filter != nil && len(filter.Names) > 0 {
		if len(filter.CloudProviderNames) == 0 {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"region names must be used with cloud provider names, to avoid region name conflict",
			)
		}
		rdb = rdb.Where(
			"regions.name in (?)",
			lo.Map(filter.Names, func(i globalcommonv1.Region_Name, _ int) string { return i.String() }),
		)
	}
	if filter != nil && len(filter.ServicePlans) > 0 {
		rdb = rdb.Where(
			"regions.service_plan in (?)",
			lo.Map(filter.ServicePlans, func(i globalcommonv1.ServicePlan, _ int) string { return i.String() }),
		)
	}
	if filter != nil && len(filter.EngineTypes) > 0 {
		rdb = rdb.Where("regions.engine_type in (?)", filter.EngineTypes)
	}

	if err := rdb.Find(&regions).Error; err != nil {
		return nil, errtrace.Wrap(bizerr.WrapGormError(err, "load regions"))
	}

	outs := make([]*entity.Region, 0, len(regions))
	for _, region := range regions {
		var err error
		re, err := cvt.RegionModel2Entity(region, region.Provider)
		if err != nil {
			return nil, errtrace.Wrap(err)
		}
		outs = append(outs, re)
	}

	return outs, nil
}

func (r *regionRepoImpl) Save(ctx context.Context, region *entity.Region) (*entity.Region, error) {
	if region == nil {
		return nil, errors.New("empty region")
	}

	ctx, span := Tracer.Start(ctx, "regionRepo.Save")
	defer span.End()

	regionModel := cvt.RegionEnToModel(region)
	rdb := r.dbProvider.ReadDB(ctx)
	// find region by id
	var re model.Region
	if err := rdb.Model(&model.Region{}).
		Joins("JOIN providers ON regions.provider_id = providers.id").
		Where("`regions`.`name` = ?", region.Id.String()).
		Where("`regions`.`service_plan` = ?", region.ServicePlan.String()).
		Where("`regions`.`engine_type` = ?", regionModel.EngineType).
		Where("providers.name=?", region.CloudProviderId.String()).
		Take(&re).Error; err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerr.WrapGormError(err, "find region %s", region.Id.String())
		}
	} else {
		// update region
		regionModel.ID = re.ID
		regionModel.CreatedAt = re.CreatedAt
	}

	// find provider by id
	var provider model.Provider
	if err := rdb.Model(&model.Provider{}).
		Where("`providers`.`name` = ?", region.CloudProviderId.String()).Take(&provider).Error; err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerr.WrapGormError(err, "find cloud provider %s", region.CloudProviderId.String())
		}
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"cloud provider %s is not supported",
			region.CloudProviderId.String(),
		)
	}
	regionModel.ProviderId = uint64(provider.ID)
	regionModel.Provider = &provider

	if err := r.dbProvider.Transaction(ctx, func(ctx context.Context) error {
		wdb := r.dbProvider.WriteDB(ctx)
		if err := wdb.Save(regionModel).Error; err != nil {
			return errtrace.Wrap(err)
		}

		if len(region.GetOpenedTenantIds()) > 0 {
			tenantRegions := make([]*model.RegionTenant, 0)
			for _, tenantId := range region.GetOpenedTenantIds() {
				count := int64(0)
				err := wdb.Model(&model.RegionTenant{}).Where("`region_id` = ? and `tenant_id` = ?", regionModel.ID, tenantId).Count(&count).Error
				if err != nil {
					return bizerr.WrapGormError(err, "count region %s tenant %d", region.Id.String(), tenantId)
				}
				if count > 0 {
					continue
				}
				tenantRegions = append(tenantRegions, &model.RegionTenant{
					TenantId: tenantId,
					RegionId: uint64(regionModel.ID),
				})
			}
			regionModel.RegionTenants = tenantRegions
			if len(tenantRegions) > 0 {
				if err := wdb.Save(tenantRegions).Error; err != nil {
					return errtrace.Wrap(err)
				}
			}
		}
		return nil
	}); err != nil {
		return nil, errtrace.Wrap(err)
	}

	// if region is public, remove all tenants
	if regionModel.IsPublic {
		wdb := r.dbProvider.WriteDB(ctx)
		if err := wdb.Model(&model.RegionTenant{}).
			Where("`region_id` = ?", regionModel.ID).
			Delete(&model.RegionTenant{}).Error; err != nil {
			return nil, bizerr.WrapGormError(err, "remove all tenants of region %s", region.Id.String())
		}
	}

	return cvt.RegionModel2Entity(regionModel, &provider)
}

func (r *regionRepoImpl) Delete(ctx context.Context, region *entity.Region) error {
	ctx, span := Tracer.Start(ctx, "regionRepo.Delete")
	defer span.End()

	if region == nil {
		return errors.New("empty region")
	}

	rdb := r.dbProvider.ReadDB(ctx)

	// find region by id
	var re model.Region
	if err := rdb.Model(&model.Region{}).
		Joins("JOIN providers ON regions.provider_id = providers.id").
		Where("`regions`.`name` = ?", region.Id.String()).
		Where("`regions`.`service_plan` = ?", region.ServicePlan.String()).
		Where("`regions`.`engine_type` = ?", region.EngineType).
		Where("providers.name=?", region.CloudProviderId.String()).
		Take(&re).Error; err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return bizerr.WrapGormError(err, "find region %s", region.Id.String())
		}
	}

	if err := r.dbProvider.Transaction(ctx, func(ctx context.Context) error {
		wdb := r.dbProvider.WriteDB(ctx)

		if err := wdb.Model(&model.RegionTenant{}).
			Where("`region_id` = ?", re.ID).
			Delete(&model.RegionTenant{}).Error; err != nil {
			return bizerr.WrapGormError(err, "remove all tenants of region %s", region.Id.String())
		}

		if err := wdb.Delete(&re).Error; err != nil {
			return errtrace.Wrap(err)
		}

		return nil
	}); err != nil {
		return errtrace.Wrap(err)
	}

	return nil
}
