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

func (s *serviceImpl) ListVpcPeerings(
	ctx context.Context,
	req *globaldedicatedv1.ListVpcPeeringsRequest,
) (*globaldedicatedv1.ListVpcPeeringsResponse, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "ListVpcPeerings")
	defer span.End()

	repoFilter := &endpoint.VpcPeeringFilter{
		ProjectIds:         req.GetProjectIds(),
		CloudProviderNames: req.GetCloudProviders(),
		RegionNames:        req.GetRegions(),
		Types:              req.GetTypes(),
		IDs:                req.GetIds(),
	}
	if req.GetOrganizationId() != 0 {
		repoFilter.OrganizationIds = []uint64{req.GetOrganizationId()}
	}

	// Get vpc peerings from repo first.
	repoPeerings, err := s.vpcPeeringRepo.Query(
		ctx,
		repoFilter,
		req.GetPager(),
		req.GetSorter(),
	)
	if err != nil {
		return nil, err.Chain("failed to query vpc peerings")
	}

	regionalPeerings := make([]*regionaldedicatedv1.VpcPeering, 0, len(repoPeerings))

	// Group by project id.
	vpcs := make([]*globaldedicatedv1.Vpc, 0)
	for _, vs := range lo.GroupBy(repoPeerings, func(each *entity.VpcPeering) uint64 {
		return each.GetProjectId()
	}) {
		v := vs[0]
		vpcResp, err := s.ListVpcs(ctx, &globaldedicatedv1.ListVpcsRequest{
			OrganizationId:   v.GetOrganizationId(),
			ProjectId:        v.GetProjectId(),
			CloudProviderIds: req.GetCloudProviders(),
			RegionIds:        req.GetRegions(),
		})
		if err != nil {
			return nil, err
		}
		// Get related vpcs from regional service.
		vpcs = append(vpcs, vpcResp.Vpcs...)
	}

	// group by org id to get regions
	for orgId, vpcPeerings := range lo.GroupBy(repoPeerings, func(each *entity.VpcPeering) uint64 {
		return each.GetOrganizationId()
	}) {
		// Get active regions and filter out vpc peering belonging to inactive regions.
		regionNames := make(map[entity.RegionName]any)
		for _, vpcPeering := range vpcPeerings {
			n, err := enum.FromString[globalcommonv1.Region_Name](vpcPeering.GetTidbCloudRegion())
			if err != nil {
				return nil, bizerr.WrapError(err, "invalid region")
			}
			regionNames[entity.RegionName(n)] = nil
		}
		regions, err := s.regionRepo.ListRegions(
			ctx,
			orgId,
			&region.Filter{
				RegionNames: lo.Keys(regionNames),
				OnlyActive:  true,
			},
		)
		if err != nil {
			return nil, err
		}
		if len(regionNames) != len(regions) {
			tilog.Warn(ctx, "some regions of vpc peering entities are not found (or inactive) in repo",
				zap.Any("regionNames", regionNames), zap.Any("regions", regions), zap.Uint64("orgId", orgId))
		}
		vpcPeerings = lo.Filter(vpcPeerings, func(each *entity.VpcPeering, _ int) bool {
			return lo.ContainsBy(regions, func(region *entity.Region) bool {
				return region.Id.String() == each.GetTidbCloudRegion()
			})
		})

		// Get vpc peerings from regional service.
		regionalPeerings = append(regionalPeerings, s.queryRegionalVpcPeerings(ctx, vpcPeerings, regions)...)
	}

	// Convert to global vpc peerings.
	vpcPeeringsResp := make([]*globaldedicatedv1.VpcPeering, 0, len(repoPeerings))
	for _, vpcPeering := range repoPeerings {
		regionalPeering, ok := lo.Find(regionalPeerings, func(each *regionaldedicatedv1.VpcPeering) bool {
			return strconv.FormatUint(vpcPeering.GetId(), 10) == each.GetId()
		})
		if !ok {
			tilog.Warn(
				ctx,
				"regional vpc peering is not found",
				zap.Uint64("vpcPeeringID", vpcPeering.GetId()),
				zap.String("region", vpcPeering.GetTidbCloudRegion()),
			)
		}
		vpc, ok := lo.Find(vpcs, func(each *globaldedicatedv1.Vpc) bool {
			return each.OrganizationId == vpcPeering.OrganizationId && each.ProjectId == vpcPeering.ProjectId &&
				each.GetRegionId().String() == vpcPeering.GetTidbCloudRegion()
		})
		if !ok {
			return nil, bizerr.New(
				bizerr.ErrCodeUnknown,
				"vpc is not found: vpcPeeringID=%d, tenantID=%d, projectID=%d, region=%s",
				vpcPeering.GetId(),
				vpcPeering.GetOrganizationId(),
				vpcPeering.GetProjectId(),
				vpcPeering.GetTidbCloudRegion(),
			)
		}
		globalVpcPeering, err := cvtVpcPeering(vpcPeering, regionalPeering, vpc)
		if err != nil {
			return nil, err
		}

		// Filter out status.
		if len(req.GetStates()) > 0 && !lo.Contains(req.GetStates(), globalVpcPeering.GetState()) {
			tilog.Info(
				ctx,
				"vpc peering is filtered out by state",
				zap.Uint64("vpcPeeringID", vpcPeering.GetId()),
				zap.String("state", globalVpcPeering.GetState().String()),
			)
			continue
		}

		vpcPeeringsResp = append(vpcPeeringsResp, globalVpcPeering)
	}

	return &globaldedicatedv1.ListVpcPeeringsResponse{
		VpcPeerings: vpcPeeringsResp,
	}, nil
}

func (s *serviceImpl) GetVpcPeering(
	ctx context.Context,
	cloudProvider globalcommonv1.CloudProvider_Name,
	peeringID uint64,
) (*entity.VpcPeering, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "GetVpcPeering")
	defer span.End()

	// Get vpc peerings from repo first.
	repoPeering, err := s.vpcPeeringRepo.Load(ctx, cloudProvider, peeringID)
	if err != nil {
		return nil, err
	}

	// Get region.
	regionName, err_ := enum.FromString[globalcommonv1.Region_Name](repoPeering.GetTidbCloudRegion())
	if err_ != nil {
		return nil, bizerr.WrapError(err_, "invalid region")
	}
	regionEn, err := s.regionRepo.SelectRegionById(
		ctx,
		globalcommonv1.Region_Name(regionName),
		repoPeering.GetOrganizationId(),
	)
	if err != nil {
		return nil, err
	}

	vpcResp, err := s.regionServerCli.ListVpcs(ctx, regionEn, &regionaldedicatedv1.ListVpcsRequest{
		TenantId:  strconv.FormatUint(repoPeering.GetOrganizationId(), 10),
		ProjectId: strconv.FormatUint(repoPeering.GetProjectId(), 10),
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to list vpcs")
	}
	if len(vpcResp.Vpcs) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "vpc not found")
	}
	regionalVpc := vpcResp.GetVpcs()[0]

	vpc, err := vpcRegionToGlobal(regionalVpc, regionEn)
	if err != nil {
		return nil, err
	}

	// Get regional vpc peering.
	regionalPeeringOrNil, err := s.regionServerCli.GetVpcPeering(
		ctx,
		regionEn,
		&regionaldedicatedv1.GetVpcPeeringRequest{
			VpcPeeringId: strconv.FormatUint(peeringID, 10),
		},
	)
	if err != nil && err.ErrCode() != bizerr.ErrCodeRecordNotFound {
		return nil, bizerr.WrapError(err, "failed to get vpc peering")
	}

	// Convert to global vpc peering.
	globalVpcPeering, err := cvtVpcPeering(repoPeering, regionalPeeringOrNil, vpc)
	if err != nil {
		return nil, err
	}

	return globalVpcPeering, nil
}

func (s *serviceImpl) queryRegionalVpcPeerings(
	ctx context.Context,
	repoPeerings []*entity.VpcPeering,
	regions []*entity.Region,
) []*regionaldedicatedv1.VpcPeering {
	// Get vpc peerings from regional service.
	regionalVpcPeeringResultCh := regionserver.QueryMultiRegional(
		regions,
		func(region *entity.Region, wg *sync.WaitGroup, regionalResultCh chan<- regionserver.RegionalResult) {
			ctx, span := tracer.Start(ctx, "GetVpcPeering")
			defer span.End()

			for _, each := range repoPeerings {
				if each.GetTidbCloudRegion() != region.Id.String() {
					continue
				}
				wg.Add(1)
				go func(each *entity.VpcPeering) {
					defer wg.Done()
					vpcPeering, err := s.regionServerCli.GetVpcPeering(
						ctx,
						region,
						&regionaldedicatedv1.GetVpcPeeringRequest{
							VpcPeeringId:   strconv.FormatUint(each.GetId(), 10),
							ShowStatusOnly: false,
						},
					)
					if err != nil {
						regionalResultCh <- regionserver.RegionalResult{Err: err}
						return
					}
					regionalResultCh <- regionserver.RegionalResult{
						Items: []any{vpcPeering},
						Err:   nil,
					}
				}(each)
			}
		},
	)

	var regionalPeerings []*regionaldedicatedv1.VpcPeering
	for result := range regionalVpcPeeringResultCh {
		if result.Err != nil {
			tilog.Error(ctx, "failed to get vpc peering from regional", zap.Error(result.Err))
			continue
		}
		if len(result.Items) == 0 {
			continue
		}
		for _, item := range result.Items {
			vpcPeering, ok := item.(*regionaldedicatedv1.VpcPeering)
			if !ok {
				tilog.Error(ctx, "invalid vpc peering type", zap.Any("item", item))
				continue
			}
			regionalPeerings = append(regionalPeerings, vpcPeering)
		}
	}

	return regionalPeerings
}

func cvtVpcPeering(
	repoPeering *entity.VpcPeering,
	regionalPeeringOrNil *regionaldedicatedv1.VpcPeering,
	vpc *globaldedicatedv1.Vpc,
) (*globaldedicatedv1.VpcPeering, bizerr.BizError) {
	repoPeering.State = cvtVpcPeeringStatus(regionalPeeringOrNil.GetStatus())
	repoPeering.TidbCloudCidrBlock = vpc.GetTidbCloudCidrBlock()

	switch repoPeering.CloudProvider {
	case globalcommonv1.CloudProvider_AWS:
		repoPeering.Aws.TidbCloudAccountId = vpc.GetAws().GetTidbCloudAccountId()
		repoPeering.Aws.TidbCloudVpcId = vpc.GetAws().GetTidbCloudVpcId()
		repoPeering.Aws.VpcPeeringConnectionId = regionalPeeringOrNil.GetAws().GetVpcPeeringConnectionId()
	case globalcommonv1.CloudProvider_GCP:
		repoPeering.Gcp.TidbCloudProjectId = vpc.GetGcp().GetTidbCloudProjectId()
		repoPeering.Gcp.TidbCloudNetworkName = vpc.GetGcp().GetTidbCloudNetworkName()
	case globalcommonv1.CloudProvider_AZURE:
		// TODO(@yifan): Fill Azure info.
	default:
		return nil, bizerr.New(
			bizerr.ErrCodeUnknown,
			"invalid vpc peering cloud provider: vpcPeeringID=%d, cloudProvider=%s",
			repoPeering.GetId(),
			repoPeering.CloudProvider.String(),
		)
	}

	return repoPeering, nil
}

func cvtVpcPeeringStatus(status regionaldedicatedv1.VpcPeering_Status) globaldedicatedv1.VpcPeering_State {
	switch status {
	case regionaldedicatedv1.VpcPeering_PENDING:
		return globaldedicatedv1.VpcPeering_PENDING
	case regionaldedicatedv1.VpcPeering_ACTIVE:
		return globaldedicatedv1.VpcPeering_ACTIVE
	case regionaldedicatedv1.VpcPeering_FAILED:
		return globaldedicatedv1.VpcPeering_FAILED
	default:
		return globaldedicatedv1.VpcPeering_PENDING
	}
}
