package region

import (
	"context"

	"github.com/samber/lo"
	"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"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cidr"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cluster"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (s *service) RegisterRegion(
	ctx context.Context,
	req *globaldedicatedv1.RegisterRegionRequest,
) (*entity.Region, bizerr.BizError) {
	if req.GetRegion() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region is required")
	}

	r := req.GetRegion()
	normalizeLegacyRegionFromWhiteLists(r)

	if r.GetId() == globalcommonv1.Region_NAME_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region id is required")
	}

	// check if cloud provider exists
	cp, err := s.repo.GetCloudProviderById(ctx, r.GetCloudProviderId(), 0)
	if err != nil {
		return nil, err
	}

	if err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		// check if region already exists
		rs, err := s.repo.ListRegions(ctx, 0, &region.Filter{
			RegionNames:        []entity.RegionName{r.GetId()},
			CloudProviderNames: []entity.CloudProviderName{r.CloudProviderId},
		})
		if err != nil {
			return err
		}

		// set logical name if not set
		// use region id as logical name
		if r.LogicalName == "" {
			r.LogicalName = r.GetId().String()
		}

		_, exists := lo.Find(rs, func(each *entity.Region) bool {
			return each.GetLogicalName() == r.GetLogicalName()
		})

		if exists {
			return bizerr.New(bizerr.ErrCodeRecordExist, "region logical %s already exists", r.GetLogicalName())
		}

		// check if region is public
		if r.IsPublic && r.LogicalName != r.GetId().String() {
			return bizerr.New(bizerr.ErrCodeParams, "private logical region %s must be not public", r.GetLogicalName())
		}

		// check if region is in the cloud provider
		if cp.State != globalcommonv1.CloudProvider_ACTIVE {
			return bizerr.New(bizerr.ErrCodeParams, "inactive cloud provider %s", cp.GetId())
		}

		if cp.Id != r.CloudProviderId {
			return bizerr.New(bizerr.ErrCodeParams, "region %s is not in cloud provider %s", r.GetId(), cp.GetId())
		}

		// chekc if region is open to tenant
		if len(r.OpenedTenantIds) > 0 {
			err = s.checkRegionOpenTenant(ctx, r.GetId(), r.OpenedTenantIds, r)
			if err != nil {
				return err
			}

			// check if region already used
			err = s.checkProviderAlreadyUsed(ctx, r.CloudProviderId, r.OpenedTenantIds, r)
			if err != nil {
				return err
			}
		}

		// save region
		err = s.repo.SaveRegion(ctx, r)
		if err != nil {
			return err
		}
		return nil
	}); err != nil {
		return nil, bizerr.WrapError(err, "register region failed")
	}

	return r, nil
}

func (s *service) GetRegionByLogicalName(
	ctx context.Context,
	req *globaldedicatedv1.GetRegionByLogicalNameRequest,
) (*entity.Region, bizerr.BizError) {
	r, err := s.repo.GetRegionByLogicalName(ctx, req.GetRegionLogicalName())
	if err != nil {
		return nil, err
	}

	if req.GetCloudProviderId() != 0 && r.CloudProviderId != req.GetCloudProviderId() {
		tilog.Warn(
			ctx,
			"region provider not match with request cloud provider",
			zap.String("region", req.GetRegionLogicalName()),
			zap.String("request provicedr", req.GetCloudProviderId().String()),
		)
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "region not found")
	}

	return r, nil
}

func (s *service) SelectRegionByOrgId(
	ctx context.Context,
	req *globaldedicatedv1.SelectRegionByOrgIdRequest,
) (*entity.Region, bizerr.BizError) {
	if req.GetOrganizationId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "organization id is required")
	}

	r, err := s.repo.SelectRegionById(ctx, req.GetRegionId(), req.GetOrganizationId())
	if err != nil {
		return nil, err
	}
	return r, nil
}

func (s *service) ListRegions(
	ctx context.Context,
	req *globaldedicatedv1.ListRegionsRequest,
) ([]*entity.Region, bizerr.BizError) {
	ps, err := s.repo.ListAllCloudProviders(ctx, req.GetOrganizationId())
	if err != nil {
		return nil, err
	}

	if len(ps) == 0 {
		return []*entity.Region{}, nil
	}

	rs := make([]*entity.Region, 0)
	for _, p := range ps {
		// not in requested cloud providers
		if len(req.GetCloudProviderIds()) > 0 && !lo.Contains(req.GetCloudProviderIds(), p.GetId()) {
			continue
		}

		if req.GetOrganizationId() != 0 {
			// if provider is not public, check if the region is open to the tenant
			for _, r := range p.GetRegions() {
				if entity.IsRegionOpenToTenant(r, req.GetOrganizationId()) {
					rs = append(rs, r)
				} else {
					tilog.Warn(ctx, "region is not open to tenant", zap.String("region", r.GetId().String()))
				}
			}
		} else {
			// if organization is not set, return all regions
			rs = append(rs, p.GetRegions()...)
		}
	}

	return rs, nil
}

func (s *service) UpdateRegion(
	ctx context.Context,
	req *globaldedicatedv1.UpdateRegionRequest,
) (*entity.Region, bizerr.BizError) {
	if req.GetRegion() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region is required")
	}

	if req.GetRegion().GetLogicalName() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region logical name is required")
	}
	normalizeLegacyRegionFromWhiteLists(req.GetRegion())

	if err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		// check if cloud provider exists
		cp, err := s.repo.GetCloudProviderById(ctx, req.GetRegion().GetCloudProviderId(), 0)
		if err != nil {
			return err
		}

		// check if region exists
		re, err := s.repo.GetRegionByLogicalName(ctx, req.GetRegion().GetLogicalName())
		if err != nil {
			return err
		}

		// check if region is in the cloud provider
		if re.CloudProviderId != cp.GetId() {
			return bizerr.New(bizerr.ErrCodeParams, "region %s is not in cloud provider %s", re.GetId(), cp.GetId())
		}

		// check if region is open to tenant
		if len(req.GetRegion().OpenedTenantIds) > 0 {
			if err := s.checkRegionOpenTenant(ctx, re.GetId(), req.GetRegion().OpenedTenantIds, re); err != nil {
				return err
			}

			// check if provider already used
			if err := s.checkProviderAlreadyUsed(ctx, re.CloudProviderId, req.GetRegion().OpenedTenantIds, re); err != nil {
				return err
			}
		}

		// check region public
		if req.GetRegion().IsPublic && req.GetRegion().GetLogicalName() != re.GetId().String() {
			return bizerr.New(bizerr.ErrCodeParams, "private logical region %s must be not public", req.GetRegion().GetLogicalName())
		}

		// save region
		err = s.repo.SaveRegion(ctx, req.GetRegion())
		if err != nil {
			return err
		}

		return nil
	}); err != nil {
		return nil, bizerr.WrapError(err, "register region failed")
	}

	return req.GetRegion(), nil
}

func (s *service) CheckRegionStatus(
	ctx context.Context,
	req *globaldedicatedv1.CheckRegionStatusRequest,
) (*emptypb.Empty, bizerr.BizError) {
	// check if region exists
	r, err := s.repo.GetRegionByLogicalName(ctx, req.GetRegionLogicalName())
	if err != nil {
		return nil, err
	}

	if req.GetCloudProviderId() != 0 && r.CloudProviderId != req.GetCloudProviderId() {
		tilog.Warn(
			ctx,
			"region provider not match with request cloud provider",
			zap.String("region", req.GetRegionLogicalName()),
			zap.String("request provicedr", req.GetCloudProviderId().String()),
		)
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "region not found")
	}

	// call region server
	_, err = s.regionServerCli.Ping(ctx, r, &emptypb.Empty{})
	if err != nil {
		return nil, err
	}

	return &emptypb.Empty{}, nil
}

func (s *service) DeleteRegion(
	ctx context.Context,
	req *globaldedicatedv1.DeleteRegionRequest,
) (*emptypb.Empty, bizerr.BizError) {
	// check if region exists
	r, err := s.repo.GetRegionByLogicalName(ctx, req.GetRegionLogicalName())
	if err != nil {
		return nil, err
	}

	if req.GetCloudProviderId() != 0 && r.CloudProviderId != req.GetCloudProviderId() {
		tilog.Warn(
			ctx,
			"region provider not match with request cloud provider",
			zap.String("region", req.GetRegionLogicalName()),
			zap.String("request provicedr", req.GetCloudProviderId().String()),
		)
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "region not found")
	}

	if r.IsPublic {
		return nil, bizerr.New(bizerr.ErrCodeParams, "public region %s can not be deleted", req.GetRegionLogicalName())
	}

	// check if region is used
	clusters, err := s.clusterRepo.Query(ctx, &cluster.Filter{
		CloudProviderNames: []entity.CloudProviderName{r.CloudProviderId},
		RegionNames:        []entity.RegionName{r.GetId()},
		LogicalRegionNames: []string{r.GetLogicalName()},
		ShowDeleted:        false,
	})
	if err != nil {
		return nil, err
	}

	for _, c := range clusters {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"region %s is used by cluster %d",
			req.GetRegionLogicalName(),
			c.ClusterId,
		)
	}

	// delete region
	err = s.repo.DeleteRegionByLogicalName(ctx, r.GetLogicalName())
	if err != nil {
		return nil, err
	}

	return &emptypb.Empty{}, nil
}

// checkRegionOpenTenant check the org is oepen other region
func (s *service) checkRegionOpenTenant(
	ctx context.Context,
	regionId globalcommonv1.Region_Name,
	orgIds []uint64,
	skipRegion *entity.Region,
) bizerr.BizError {
	// list all regions by region id
	// check if org is open to other region
	rs, err := s.repo.ListRegions(ctx, 0, &region.Filter{
		RegionNames: []entity.RegionName{regionId},
	})
	if err != nil {
		return err
	}

	// find region open to tenant
	for _, r := range rs {
		// public region skip
		if r.IsPublic {
			continue
		}

		orgId, open := lo.Find(orgIds, func(orgId uint64) bool {
			return entity.IsRegionOpenToTenant(r, orgId) &&
				r.GetLogicalName() != skipRegion.GetLogicalName() // skip specified region
		})

		// org is open to other region
		if open {
			return bizerr.New(bizerr.ErrCodeParams, "organization %d is aleady in region %s", orgId, r.GetLogicalName())
		}
	}

	return nil
}

// checkProviderAlreadyUsed check if the orgs already used public region or private region
func (s *service) checkProviderAlreadyUsed(
	ctx context.Context,
	providerName globalcommonv1.CloudProvider_Name,
	orgIds []uint64,
	skipRegion *entity.Region,
) bizerr.BizError {
	rs, err := s.repo.ListRegions(ctx, 0, &region.Filter{
		CloudProviderNames: []entity.CloudProviderName{providerName},
	})
	if err != nil {
		return err
	}

	// if logical name is same with id, it's public region
	isPublic := skipRegion.LogicalName == skipRegion.GetId().String()

	for _, r := range rs {
		// check cidr
		cidrs, err := s.cidrRepo.Query(ctx, &cidr.Filter{
			OrganizationIds: orgIds,
			RegionNames:     []entity.RegionName{r.GetId()},
		}, nil)

		if err != nil {
			return err
		}
		for _, c := range cidrs {
			if isPublic {
				// if already used other private region
				if c.BasicRegion.Name.String() != c.BasicRegion.LogicalName {
					return bizerr.New(
						bizerr.ErrCodeParams,
						"organization %d already create cidr %s in private region %s",
						c.TenantID,
						c.CIDR,
						c.BasicRegion.LogicalName,
					)
				}
			} else {
				// if already used other public region
				if c.BasicRegion.Name.String() == c.BasicRegion.LogicalName {
					return bizerr.New(
						bizerr.ErrCodeParams,
						"organization %d already create cidr %s in public region %s",
						c.TenantID,
						c.CIDR,
						c.BasicRegion.LogicalName,
					)
				}

				// if already used other private region with same physical region
				if c.BasicRegion.LogicalName != skipRegion.GetLogicalName() && c.BasicRegion.Name == skipRegion.GetId() {
					return bizerr.New(
						bizerr.ErrCodeParams,
						"organization %d already create cidr %s in region %s",
						c.TenantID,
						c.CIDR,
						c.BasicRegion.LogicalName,
					)
				}
			}
		}
	}

	return nil
}

func normalizeLegacyRegionFromWhiteLists(r *globalcommonv1.Region) {
	if r == nil || len(r.GetRegionWhiteLists()) == 0 {
		return
	}
	for _, wl := range r.GetRegionWhiteLists() {
		if r.GetServicePlan() != globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED &&
			wl.GetServicePlan() != r.GetServicePlan() {
			continue
		}
		r.ServicePlan = wl.GetServicePlan()
		r.IsPublic = wl.GetIsPublic()
		r.OpenedTenantIds = append([]uint64(nil), wl.GetOpenedTenantIds()...)
		return
	}
	wl := r.GetRegionWhiteLists()[0]
	r.ServicePlan = wl.GetServicePlan()
	r.IsPublic = wl.GetIsPublic()
	r.OpenedTenantIds = append([]uint64(nil), wl.GetOpenedTenantIds()...)
}
