package vpc

import (
	"context"
	"strconv"
	"sync"

	"github.com/samber/lo"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	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/adapter/regionserver"
	"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/region"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (s *serviceImpl) ListVpcs(
	ctx context.Context,
	req *globaldedicatedv1.ListVpcsRequest,
) (*globaldedicatedv1.ListVpcsResponse, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.listVpcs")
	defer span.End()

	// check project id and organization id
	if req.ProjectId == 0 && req.OrganizationId == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "project id or organization id is required")
	}

	if req.ProjectId != 0 {
		project, err := s.accountCli.GetProject(ctx, req.ProjectId)
		if err != nil {
			return nil, err
		}
		if req.OrganizationId != 0 && req.OrganizationId != project.TenantID {
			return nil, bizerr.New(
				bizerr.ErrCodeParams,
				"project %d does not belong to organization %d",
				req.ProjectId,
				req.OrganizationId,
			)
		}
		req.OrganizationId = project.TenantID
	}

	regions, err := s.getRequestedRegions(
		ctx,
		req.GetProjectId(),
		req.GetOrganizationId(),
		req.GetCloudProviderIds(),
		req.GetRegionIds(),
	)
	if err != nil {
		return nil, err
	}

	if len(regions) == 0 {
		tilog.Info(ctx, "no region found")
		return &globaldedicatedv1.ListVpcsResponse{
			Vpcs:  nil,
			Total: 0,
		}, nil
	}

	// call regional service to get vpcs
	regionalResultCh := regionserver.QueryMultiRegional(
		regions,
		func(region *entity.Region, _ *sync.WaitGroup, regionalResultCh chan<- regionserver.RegionalResult) {
			// only one call per region
			// no need to use wait group

			ctx, span := tracer.Start(ctx, "Domain.listVpcsInRegion")
			defer span.End()
			resp, err := s.regionServerCli.ListVpcs(ctx, region, &regionaldedicatedv1.ListVpcsRequest{
				ProjectId: strconv.FormatUint(req.GetProjectId(), 10),
				TenantId:  strconv.FormatUint(req.OrganizationId, 10),
			})
			if err != nil {
				regionalResultCh <- regionserver.RegionalResult{Err: err}
				return
			}
			var vpcs []*globaldedicatedv1.Vpc
			for _, regionalVpc := range resp.Vpcs {
				var vpc *globaldedicatedv1.Vpc
				vpc, err = vpcRegionToGlobal(regionalVpc, region)
				if err != nil {
					tilog.Warn(
						ctx,
						"failed to convert regional vpc to global vpc",
						zap.Any("regional_vpc", regionalVpc),
						zap.Error(err),
					)
					regionalResultCh <- regionserver.RegionalResult{Err: err}
					return
				}
				vpcs = append(vpcs, vpc)
			}
			regionalResultCh <- regionserver.RegionalResult{Items: lo.ToAnySlice(vpcs)}
		},
	)

	var vpcs []*globaldedicatedv1.Vpc
	for result := range regionalResultCh {
		if result.Err != nil {
			return nil, result.Err
		}
		if result.Items == nil {
			continue
		}

		for _, item := range result.Items {
			v, ok := item.(*globaldedicatedv1.Vpc)
			if !ok {
				tilog.Warn(ctx, "unexpected item type, not global vpc", zap.Any("item", item))
				continue
			}
			vpcs = append(vpcs, v)
		}
	}

	return &globaldedicatedv1.ListVpcsResponse{
		Vpcs:  vpcs,
		Total: uint32(len(vpcs)),
	}, nil
}

func (s *serviceImpl) getRequestedRegions(
	ctx context.Context,
	projectId, orgId uint64,
	cloudProviders []globalcommonv1.CloudProvider_Name,
	regions []globalcommonv1.Region_Name,
) ([]*entity.Region, bizerr.BizError) {
	// cidr in global service equals to vpc in regional service
	cidrFilter := &cidr.Filter{
		CloudProviderNames: cloudProviders,
		RegionNames:        regions,
	}

	if orgId != 0 {
		cidrFilter.OrganizationIds = []uint64{orgId}
	}
	if projectId != 0 {
		cidrFilter.ProjectIDs = []uint64{projectId}
	}

	cidrs, err := s.cidrRepo.Query(ctx, cidrFilter, nil)
	if err != nil {
		return nil, err
	}

	if len(cidrs) == 0 {
		tilog.Info(ctx, "no cidr found")
		return nil, nil
	}

	regionIds := lo.Union(lo.Map(cidrs, func(cidr *entity.CIDR, _ int) globalcommonv1.Region_Name {
		return cidr.BasicRegion.Name
	}))

	cloudProviderRegions, err := s.regionRepo.ListRegions(ctx, orgId, &region.Filter{
		RegionNames: regionIds,
	})
	if err != nil {
		return nil, err
	}

	cloudProviderRegions = lo.Filter(cloudProviderRegions, func(region *entity.Region, _ int) bool {
		return region.State == globalcommonv1.Region_ACTIVE
	})

	return cloudProviderRegions, nil
}

func vpcRegionToGlobal(
	regionalVpc *regionaldedicatedv1.Vpc,
	enRegion *entity.Region,
) (*globaldedicatedv1.Vpc, bizerr.BizError) {
	out := &globaldedicatedv1.Vpc{
		Id:                 regionalVpc.VpcId,
		ProjectId:          0, // fill later
		CloudProviderId:    enRegion.CloudProviderId,
		RegionId:           enRegion.Id,
		TidbCloudCidrBlock: regionalVpc.CidrBlock,
		Info:               nil, // fill later
		SecondaryCidrs:     regionalVpc.SecondaryCidrBlocks,
	}

	switch enRegion.CloudProviderId {
	case globalcommonv1.CloudProvider_AWS:
		info := regionalVpc.GetAws()
		out.Info = &globaldedicatedv1.Vpc_Aws{
			Aws: &globaldedicatedv1.Vpc_AwsVpcInfo{
				TidbCloudAccountId: info.GetAccountId(),
				TidbCloudVpcId:     info.GetVpcId(),
			},
		}
	case globalcommonv1.CloudProvider_GCP:
		info := regionalVpc.GetGcp()
		out.Info = &globaldedicatedv1.Vpc_Gcp{
			Gcp: &globaldedicatedv1.Vpc_GcpVpcInfo{
				TidbCloudProjectId:   info.GetProjectId(),
				TidbCloudNetworkName: info.GetNetworkName(),
			},
		}
	case globalcommonv1.CloudProvider_AZURE:
		info := regionalVpc.GetAzure()
		out.Info = &globaldedicatedv1.Vpc_Azure{
			Azure: &globaldedicatedv1.Vpc_AzureVpcInfo{
				TidbCloudVnetId: info.GetVnetId(),
			},
		}
	}

	if len(regionalVpc.ProjectIds) != 1 {
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"unexpected projectIds of regionalVpc %s in region %s",
			out.Id,
			enRegion.DisplayName,
		)
	}

	projectId, err := strconv.ParseUint(regionalVpc.ProjectIds[0], 10, 64)
	if err != nil {
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"failed to parse projectId of regionalVpc %s in region %s",
			out.Id,
			enRegion.DisplayName,
		)
	}
	out.ProjectId = projectId

	if len(regionalVpc.TenantIds) != 1 {
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"unexpected organizationIds of regionalVpc %s in region %s",
			out.Id,
			enRegion.DisplayName,
		)
	}

	organizationId, err := strconv.ParseUint(regionalVpc.TenantIds[0], 10, 64)
	if err != nil {
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"failed to parse organizationId of regionalVpc %s in region %s",
			out.Id,
			enRegion.DisplayName,
		)
	}
	out.OrganizationId = organizationId
	return out, nil
}
