package region

import (
	"context"
	"slices"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/emptypb"

	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"
	regionrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/region"
	spurepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/spu"
	regionsvc "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/region"
	spusvc "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/spu"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	featgatesvc "github.com/tidbcloud/tidb-management-service/utilities/featuregate"
)

/*
ListRegions and ListCloudProviders are the two methods that are exposed to the client.
They need project ID to filter the regions and cloud providers that have available skus of TiDB and TiKV component.
*/

func NewServer(
	regionSvc regionsvc.Service,
	spuSvc spusvc.ServiceSpu,
	cloudProviderRepo regionrepo.Repo,
	spuRepo spurepo.RepoSpu,
	skuRepo spurepo.SkuRepo,
	featureGate featgatesvc.Service,
	skuSvc spusvc.SkuService,
) globaldedicatedv1.RegionServiceServer {
	return &server{
		regionSvc:         regionSvc,
		spuSvc:            spuSvc,
		cloudProviderRepo: cloudProviderRepo,
		spuRepo:           spuRepo,
		skuRepo:           skuRepo,
		featureGate:       featureGate,
		skuSvc:            skuSvc,
	}
}

type server struct {
	regionSvc         regionsvc.Service
	spuSvc            spusvc.ServiceSpu
	cloudProviderRepo regionrepo.Repo
	spuRepo           spurepo.RepoSpu
	skuRepo           spurepo.SkuRepo
	featureGate       featgatesvc.Service
	skuSvc            spusvc.SkuService
}

func (s *server) ListRegions(
	ctx context.Context,
	req *globaldedicatedv1.ListRegionsRequest,
) (*globaldedicatedv1.ListRegionsResponse, error) {
	rs, err := s.regionSvc.ListRegions(ctx, req)
	if err != nil {
		return nil, err
	}

	// internal call, return all regions
	if req.GetOrganizationId() == 0 {
		return &globaldedicatedv1.ListRegionsResponse{Regions: rs, Total: uint32(len(rs))}, nil
	}

	// filter regions
	filteredRegions, err := s.filterRegions(ctx, rs, req.GetOrganizationId(), req.GetProjectId(), !req.IgnoreSpuCheck)
	if err != nil {
		return nil, err
	}

	return &globaldedicatedv1.ListRegionsResponse{Regions: filteredRegions, Total: uint32(len(filteredRegions))}, nil
}

func (s *server) ListCloudProviders(
	ctx context.Context,
	req *globaldedicatedv1.ListCloudProvidersRequest,
) (*globaldedicatedv1.ListCloudProvidersResponse, error) {
	repoProviders, err := s.regionSvc.ListCloudProviders(ctx, req)
	if err != nil {
		return nil, err
	}

	// internal call, return all regions
	if req.GetOrganizationId() == 0 {
		return &globaldedicatedv1.ListCloudProvidersResponse{
			CloudProviders: repoProviders,
			Total:          uint32(len(repoProviders)),
		}, nil
	}

	globalProviders := make([]*globalcommonv1.CloudProvider, 0)

	for _, p := range repoProviders {
		if len(p.GetRegions()) == 0 {
			continue
		}

		if p.GetState() != globalcommonv1.CloudProvider_ACTIVE {
			tilog.Warn(ctx, "cloud provider is not active, skip it", zap.String("provider", p.GetId().String()))
			continue
		}

		filteredRegions, err := s.filterRegions(ctx, p.GetRegions(), req.GetOrganizationId(), req.GetProjectId(), !req.IgnoreSpuCheck)
		if err != nil {
			return nil, err
		}

		// build cloud provider response
		if len(filteredRegions) > 0 {
			p.Regions = filteredRegions
			globalProviders = append(globalProviders, p)
		}
	}

	return &globaldedicatedv1.ListCloudProvidersResponse{
		CloudProviders: globalProviders,
		Total:          uint32(len(globalProviders)),
	}, nil
}

func (s *server) filterRegions(
	ctx context.Context,
	regions []*globalcommonv1.Region,
	orgId, projectId uint64,
	checkSpu bool,
) ([]*globalcommonv1.Region, bizerr.BizError) {
	filteredRegions := make([]*globalcommonv1.Region, 0)
	for _, r := range regions {
		// filter each region

		// check if region is active
		if r.State != globalcommonv1.Region_ACTIVE {
			tilog.Warn(
				ctx,
				"region is not active, skip it",
				zap.String("region", r.Id.String()),
				zap.String("provider", r.GetCloudProviderId().String()),
			)
			continue
		}

		// check if region is open to tenant
		if !isRegionOpenToTenant(r, orgId) {
			tilog.Warn(
				ctx,
				"region is not open to tenant, skip it",
				zap.String("region", r.Id.String()),
				zap.String("provider", r.GetCloudProviderId().String()),
			)
			continue
		}

		if checkSpu {
			// if region has no tier, remove it from the list
			tiers, err := s.spuSvc.List(ctx, &globaldedicatedv1.ListSpusRequest{
				Request: &globaldedicatedv1.ListSpusRequest_WithSku{
					WithSku: &globaldedicatedv1.ListSpusRequest_FilterWithSku{
						CloudProvider: r.GetCloudProviderId(),
						Region:        r.GetId(),
						TenantId:      orgId,
						ProjectId:     projectId,
					},
				},
			})
			if err != nil {
				return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed check tier for region %+v, err: %w ", r, err)
			}

			// check if region has tier
			if len(tiers) == 0 {
				tilog.Warn(
					ctx,
					"region has no tier, skip it",
					zap.String("region", r.Id.String()),
					zap.String("provider", r.GetCloudProviderId().String()),
				)
				continue
			}
		}

		filteredRegions = append(filteredRegions, r)
	}

	return filteredRegions, nil
}

func isRegionOpenToTenant(r *entity.Region, tenantID uint64) bool {
	if r.IsPublic {
		return true
	}

	return slices.Contains(r.OpenedTenantIds, tenantID)
}

// internal call
func (s *server) GetRegionByLogicalName(
	ctx context.Context,
	req *globaldedicatedv1.GetRegionByLogicalNameRequest,
) (*globalcommonv1.Region, error) {
	r, err := s.regionSvc.GetRegionByLogicalName(ctx, req)
	if err != nil {
		return nil, err
	}

	return r, nil
}

// internal call
func (s *server) SelectRegionByOrgId(
	ctx context.Context,
	req *globaldedicatedv1.SelectRegionByOrgIdRequest,
) (*globalcommonv1.Region, error) {
	r, err := s.regionSvc.SelectRegionByOrgId(ctx, req)
	if err != nil {
		return nil, err
	}
	return r, nil
}

// internal call
func (s *server) RegisterCloudProvider(
	ctx context.Context,
	req *globaldedicatedv1.RegisterCloudProviderRequest,
) (*globalcommonv1.CloudProvider, error) {
	p, err := s.regionSvc.RegisterCloudProvider(ctx, req)
	if err != nil {
		return nil, err
	}

	return p, nil
}

// internal call
func (s *server) RegisterRegion(
	ctx context.Context,
	req *globaldedicatedv1.RegisterRegionRequest,
) (*globalcommonv1.Region, error) {
	r, err := s.regionSvc.RegisterRegion(ctx, req)
	if err != nil {
		return nil, err
	}

	return r, nil
}

// internal call
func (s *server) UpdateRegion(
	ctx context.Context,
	req *globaldedicatedv1.UpdateRegionRequest,
) (*globalcommonv1.Region, error) {
	r, err := s.regionSvc.UpdateRegion(ctx, req)
	if err != nil {
		return nil, err
	}

	return r, nil
}

// internal call
func (s *server) CheckRegionStatus(
	ctx context.Context,
	req *globaldedicatedv1.CheckRegionStatusRequest,
) (*emptypb.Empty, error) {
	resp, err := s.regionSvc.CheckRegionStatus(ctx, req)
	if err != nil {
		return nil, err
	}
	return resp, nil
}

// internal call
func (s *server) DeleteRegion(ctx context.Context, req *globaldedicatedv1.DeleteRegionRequest) (*emptypb.Empty, error) {
	resp, err := s.regionSvc.DeleteRegion(ctx, req)
	if err != nil {
		return nil, err
	}

	return resp, nil
}
