package region

import (
	"context"
	"errors"
	"time"

	"github.com/samber/lo"
	"github.com/tidbcloud/core-svc/model"
	"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/dedicated-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
)

type regionRepoImpl struct {
	provider db.Provider
}

func NewRegionRepo(provider db.Provider) region.Repo {
	return &regionRepoImpl{
		provider: provider,
	}
}

func (p *regionRepoImpl) ListAllCloudProviders(
	ctx context.Context,
	orgId uint64,
) ([]*entity.CloudProvider, bizerr.BizError) {
	rdb := p.provider.ReadDB(ctx)
	var providers []*schema.Provider
	if err := rdb.Model(&schema.Provider{}).Preload("Regions").Preload("Regions.RegionTenants").
		Find(&providers).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "load all cloud providers")
	}
	return buildCloudProviders(providers, orgId)
}

func (p *regionRepoImpl) GetCloudProviderById(
	ctx context.Context,
	cloudProviderName entity.CloudProviderName,
	orgId uint64,
) (*entity.CloudProvider, bizerr.BizError) {
	rdb := p.provider.ReadDB(ctx)
	var provider schema.Provider
	if err := rdb.Model(&schema.Provider{}).Preload("Regions").Preload("Regions.RegionTenants").
		Where("`providers`.`name` = ?", cloudProviderName.String()).
		Take(&provider).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"cloud provider %s is not supported",
				cloudProviderName.String(),
			)
		}
		return nil, bizerr.WrapGormError(err, "load cloud provider %s", cloudProviderName.String())
	}
	return CloudProviderModelToEn(&provider, orgId)
}

func (p *regionRepoImpl) ListRegions(
	ctx context.Context,
	orgId uint64,
	filter *region.Filter,
) ([]*entity.Region, bizerr.BizError) {
	if filter == nil {
		filter = &region.Filter{}
	}

	rdb := p.provider.ReadDB(ctx).
		Model(&schema.Region{}).
		InnerJoins("Provider").
		Preload("RegionTenants")

	// Build queries.
	if 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 len(filter.RegionNames) != 0 {
		rdb = rdb.Where(
			"`regions`.`region_name` IN (?)",
			lo.Map(filter.RegionNames, func(i globalcommonv1.Region_Name, _ int) string { return i.String() }),
		)
	}
	if filter.OnlyActive {
		rdb = rdb.Where("`regions`.`status` = ?", globalcommonv1.Region_ACTIVE.String())
	}

	var regions []*schema.Region
	if err := rdb.
		Find(&regions).
		Error; err != nil {
		return nil, bizerr.WrapGormError(err, "load all regions of org %d, filter %v", orgId, filter)
	}
	if len(regions) == 0 {
		return nil, nil
	}

	// Build result.
	out := make([]*entity.Region, 0, len(regions))
	for _, r := range regions {
		rEn, err := CloudRegionModelToEn(r, &r.Provider)
		if err != nil {
			return nil, err
		}
		out = append(out, rEn)
	}
	return filterRegionsByOrgId(out, orgId), nil
}

func (p *regionRepoImpl) SelectRegionById(
	ctx context.Context,
	regionName entity.RegionName,
	orgId uint64,
) (*entity.Region, bizerr.BizError) {
	if orgId == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "organization id is unspecified")
	}

	if regionName == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region name is unspecified")
	}

	rdb := p.provider.ReadDB(ctx)

	// first region by region name to get provider
	var cloudRegion *schema.Region
	if err := rdb.Model(&schema.Region{}).InnerJoins("Provider").Preload("RegionTenants").
		Where("`regions`.`region_name` = ?", regionName.String()).
		First(&cloudRegion).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "load region %s with org %d", regionName.String(), orgId)
	}

	// fill Id
	cloudProviderId, err := enum.FromString[globalcommonv1.CloudProvider_Name](cloudRegion.Provider.Name)
	if err != nil {
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"cloud provider %s is not supported for region %s",
			cloudRegion.Provider.Name,
			regionName.String(),
		)
	}

	// list all regions by provider
	provider, err := p.GetCloudProviderById(ctx, entity.CloudProviderName(cloudProviderId), orgId)
	if err != nil {
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"load cloud provider %s for region %s",
			cloudRegion.Provider.Name,
			regionName.String(),
		)
	}

	for _, r := range provider.Regions {
		if r.GetId() == regionName {
			return r, nil
		}
	}

	return nil, bizerr.New(bizerr.ErrCodeParams, "region %s is not supported", regionName.String())
}

func (p *regionRepoImpl) GetRegionByLogicalName(
	ctx context.Context,
	logicalName string,
) (*entity.Region, bizerr.BizError) {
	rdb := p.provider.ReadDB(ctx)
	var cloudRegion schema.Region
	if err := rdb.Model(&schema.Region{}).InnerJoins("Provider").Preload("RegionTenants").
		Where("`regions`.`name` = ?", logicalName).
		First(&cloudRegion).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerr.New(bizerr.ErrCodeParams, "region logical name %s is not supported", logicalName)
		}
		return nil, bizerr.WrapGormError(err, "load region by logical name %s", logicalName)
	}
	return CloudRegionModelToEn(&cloudRegion, &cloudRegion.Provider)
}

func (p *regionRepoImpl) SaveCloudProvider(ctx context.Context, cloudProvider *entity.CloudProvider) bizerr.BizError {
	wdb := p.provider.WriteDB(ctx)
	provider, err := CloudProviderEnToModel(cloudProvider)
	if err != nil {
		return err
	}
	if err := wdb.Save(&provider).Error; err != nil {
		return bizerr.WrapGormError(err, "save cloud provider %s", cloudProvider.Id.String())
	}
	return nil
}

func (p *regionRepoImpl) SaveRegion(ctx context.Context, r *entity.Region) bizerr.BizError {
	wdb := p.provider.WriteDB(ctx)
	cloudRegion, err := CloudRegionEnToModel(r)
	if err != nil {
		return err
	}

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

	// check region logic name
	if r.GetLogicalName() == "" {
		r.LogicalName = r.Id.String() // use region id as logic name default
	}

	if r.GetLogicalName() != r.Id.String() && r.GetIsPublic() {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"private logical region %s is not allowed set public",
			r.GetLogicalName(),
		)
	}

	// find region by id
	var re schema.Region
	if err := wdb.Model(&schema.Region{}).
		Where("`regions`.`name` = ? and `regions`.`region_name` = ?", r.GetLogicalName(), r.Id.String()).Take(&re).Error; err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return bizerr.WrapGormError(err, "find region %s", r.Id.String())
		}
		// if not found, create new region
		// use generated id > 20000000
		cloudRegion.ID = uint(GenRegionModelId())
		cloudRegion.CreatedAt = time.Now()
	} else {
		// update region
		cloudRegion.ID = re.ID
		cloudRegion.CreatedAt = re.CreatedAt
	}

	// save region
	// set provider id
	cloudRegion.ProviderId = uint64(provider.ID)
	if err := wdb.Save(&cloudRegion).Error; err != nil {
		return bizerr.WrapGormError(err, "save region %s", r.Id.String())
	}

	// save region tenants
	if len(r.OpenedTenantIds) > 0 {
		tenantRegions := make([]*schema.RegionTenant, 0)
		for _, tenantId := range r.OpenedTenantIds {
			count := int64(0)
			err := wdb.Model(&schema.RegionTenant{}).
				Where("`region_id` = ? and `tenant_id` = ?", cloudRegion.ID, tenantId).
				Count(&count).
				Error
			if err != nil {
				return bizerr.WrapGormError(
					err,
					"count region %s(%s) tenant %d",
					r.GetLogicalName(),
					r.Id.String(),
					tenantId,
				)
			}
			if count > 0 {
				continue
			}
			tenantRegions = append(tenantRegions, &schema.RegionTenant{
				RegionId: uint64(cloudRegion.ID),
				TenantId: tenantId,
			})
		}
		if len(tenantRegions) > 0 {
			if err := wdb.Save(tenantRegions).Error; err != nil {
				return bizerr.WrapGormError(
					err,
					"save region %s(%s) tenants %v",
					r.GetLogicalName(),
					r.Id.String(),
					r.OpenedTenantIds,
				)
			}
		}
	}

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

func (p *regionRepoImpl) DeleteRegionByLogicalName(ctx context.Context, logicalName string) bizerr.BizError {
	// get region by logical name
	rdb := p.provider.ReadDB(ctx)
	var cloudRegion schema.Region
	if err := rdb.Model(&schema.Region{}).InnerJoins("Provider").
		Where("`regions`.`name` = ?", logicalName).
		First(&cloudRegion).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return bizerr.New(bizerr.ErrCodeParams, "region logical name %s is not supported", logicalName)
		}
		return bizerr.WrapGormError(err, "load region by logical name %s", logicalName)
	}

	// delete region
	wdb := p.provider.WriteDB(ctx)
	if err := wdb.Model(&model.RegionTenant{}).
		Where("`region_id` = ?", cloudRegion.ID).
		Delete(&model.RegionTenant{}).Error; err != nil {
		return bizerr.WrapGormError(err, "remove all tenants of region %s", logicalName)
	}

	if err := wdb.Delete(&cloudRegion).Error; err != nil {
		return bizerr.WrapGormError(err, "delete region %s", logicalName)
	}

	return nil
}
