package vpc

import (
	"context"
	"fmt"
	"strconv"
	"sync"

	"github.com/samber/lo"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"

	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"
	cidrrepo "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"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
)

func (s *serviceImpl) QueryCidrs(
	ctx context.Context,
	request *globaldedicatedv1.QueryCidrsRequest,
) ([]*globaldedicatedv1.Cidr, bizerr.BizError) {
	if len(request.GetFilter().GetIds()) == 0 && len(request.GetFilter().GetProjectIds()) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "ids or project_ids filter is required")
	}

	cidrFilter := &cidrrepo.Filter{
		IDs:                request.Filter.Ids,
		OrganizationIds:    nil, // leave it empty
		ProjectIDs:         request.Filter.ProjectIds,
		RegionNames:        nil,                                   // leave it empty
		CloudProviderNames: []globalcommonv1.CloudProvider_Name{}, // fill later
	}
	if request.Filter.CloudProviderName != "" {
		cloudProviderName, rawErr := enum.FromString[globalcommonv1.CloudProvider_Name](
			request.Filter.CloudProviderName,
		)
		if rawErr != nil {
			return nil, bizerr.New(bizerr.ErrCodeParams, "invalid cloud provider")
		}
		cidrFilter.CloudProviderNames = append(
			cidrFilter.CloudProviderNames,
			globalcommonv1.CloudProvider_Name(cloudProviderName),
		)
	}

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

	out := make([]*globaldedicatedv1.Cidr, 0, len(cidrs))

	// group by project id
	projectToCidrs := lo.GroupBy(cidrs, func(cidr *entity.CIDR) uint64 {
		return cidr.ProjectID
	})

	for projectId, projectCidrs := range projectToCidrs {
		// query project
		project, err := s.accountCli.GetProject(ctx, projectId)
		if err != nil {
			return nil, err.Chain("failed to get project: project=%d, err=%w", projectId, err)
		}

		regionToCidrs := lo.GroupBy(projectCidrs, func(projectCidrs *entity.CIDR) globalcommonv1.Region_Name {
			return projectCidrs.BasicRegion.Name
		})
		regions, err := s.regionRepo.ListRegions(
			ctx,
			project.TenantID,
			&region.Filter{RegionNames: lo.Keys(regionToCidrs)},
		)
		if err != nil {
			return nil, err
		}
		regionNameToRegion := lo.SliceToMap(regions, func(each *entity.Region) (entity.RegionName, *entity.Region) {
			return each.Id, each
		})

		var outCidrList []*entity.CIDR
		for _, regionName := range lo.Keys(regionToCidrs) {
			enRegion, ok := regionNameToRegion[regionName]
			if !ok {
				return nil, bizerr.New(bizerr.ErrCodeUnknown, "region %s is not found", regionName.String())
			}

			if enRegion.State != globalcommonv1.Region_ACTIVE {
				delete(regionToCidrs, regionName)
				continue
			}

			outCidrList = append(outCidrList, regionToCidrs[regionName]...)
		}

		cidrIdToVpc := map[uint64]*regionaldedicatedv1.Vpc{}
		if !request.GetGlobalMetaOnly() {
			// return error if failed to query regional vpc, as if cidr state is inactive, user could delete cidr on Console.
			var err bizerr.BizError
			cidrIdToVpc, err = s.queryRegionalVpcs(ctx, regionToCidrs, regionNameToRegion)
			if err != nil {
				return nil, err
			}
		}

		regionalStateKnown := !request.GetGlobalMetaOnly()
		for _, cidr := range outCidrList {
			out = append(out, cidrEnToPb(cidr, cidrIdToVpc, regionalStateKnown))
		}
	}

	return out, nil
}

func (s *serviceImpl) GetDefaultCidr(
	ctx context.Context,
	request *globaldedicatedv1.GetDefaultCidrRequest,
) (*globaldedicatedv1.GetDefaultCidrResponse_DefaultCidr, bizerr.BizError) {
	project, err := s.accountCli.GetProject(ctx, request.GetProjectId())
	if err != nil {
		return nil, err
	}

	cloudProviderName, rawErr := enum.FromString[globalcommonv1.CloudProvider_Name](request.CloudProviderName)
	if rawErr != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid cloud provider")
	}

	regionName, rawErr := enum.FromString[globalcommonv1.Region_Name](request.RegionName)
	if rawErr != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid region")
	}
	cloudProvider, err := s.regionRepo.GetCloudProviderById(
		ctx,
		globalcommonv1.CloudProvider_Name(cloudProviderName),
		project.TenantID,
	)
	if err != nil {
		return nil, err
	}
	enRegion, err := s.regionRepo.SelectRegionById(ctx, globalcommonv1.Region_Name(regionName), project.TenantID)
	if err != nil {
		return nil, err
	}

	err = entity.CheckCloudProviderAndRegionAvailableForTenant(cloudProvider, enRegion, project.TenantID)
	if err != nil {
		return nil, err
	}

	regionList, err := s.getRegionList(
		ctx,
		globalcommonv1.CloudProvider_Name(cloudProviderName),
		globalcommonv1.Region_Name(regionName),
		request.GetProjectId(),
	)
	if err != nil {
		return nil, err
	}
	cidr, err := getRegionDefaultCIDR(
		globalcommonv1.CloudProvider_Name(cloudProviderName),
		globalcommonv1.Region_Name(regionName),
		regionList,
	)
	if err != nil {
		return nil, err
	}

	name := fmt.Sprintf("%s-%s-default", request.CloudProviderName, request.RegionName)
	defaultCIDR := &globaldedicatedv1.GetDefaultCidrResponse_DefaultCidr{
		Name:              name,
		CloudProviderName: request.CloudProviderName,
		RegionName:        request.RegionName,
		RegionDisplayName: enRegion.DisplayName,
		Cidr:              cidr,
		ProjectId:         request.GetProjectId(),
	}

	return defaultCIDR, nil
}

type CidrVpcMapping struct {
	CidrID uint64
	VPC    *regionaldedicatedv1.Vpc
}

func (s *serviceImpl) queryRegionalVpcs(
	ctx context.Context,
	regionToCidrs map[globalcommonv1.Region_Name][]*entity.CIDR,
	regionNameToRegion map[globalcommonv1.Region_Name]*entity.Region,
) (map[uint64]*regionaldedicatedv1.Vpc, bizerr.BizError) {
	// call regional service to get vpc
	regionalVpcResultCh := regionserver.QueryMultiRegional(
		lo.Values(regionNameToRegion),
		func(region *entity.Region, wg *sync.WaitGroup, regionalResultCh chan<- regionserver.RegionalResult) {
			regionCidrs := regionToCidrs[region.Id]

			for _, each := range regionCidrs {
				wg.Add(1)
				go func(each *entity.CIDR) {
					defer wg.Done()
					vpcs, err := s.regionServerCli.ListVpcs(ctx, region,
						&regionaldedicatedv1.ListVpcsRequest{
							TenantId:  strconv.FormatUint(each.TenantID, 10),
							ProjectId: strconv.FormatUint(each.ProjectID, 10),
							ClusterId: "", // leave it empty
							Statuses: []regionaldedicatedv1.Vpc_Status{
								regionaldedicatedv1.Vpc_ACTIVE,
								regionaldedicatedv1.Vpc_CREATING,
								regionaldedicatedv1.Vpc_FAILED,
							},
						},
					)
					if err != nil {
						regionalResultCh <- regionserver.RegionalResult{Err: err}
						return
					}

					if len(vpcs.GetVpcs()) == 0 {
						return
					}

					cidrVpcMapping := &CidrVpcMapping{
						CidrID: each.ID,
						VPC:    vpcs.Vpcs[0],
					}
					regionalResultCh <- regionserver.RegionalResult{
						Items: []any{cidrVpcMapping},
						Err:   nil,
					}
				}(each)
			}
		},
	)

	cidrIdToVpc := map[uint64]*regionaldedicatedv1.Vpc{}
	for result := range regionalVpcResultCh {
		if result.Err != nil {
			return nil, result.Err
		}
		if result.Items == nil {
			continue
		}

		for _, item := range result.Items {
			cidrVpcMapping, ok := item.(*CidrVpcMapping)
			if !ok {
				return nil, bizerr.New(
					bizerr.ErrCodeUnknown,
					"invalid type of QueryMultiRegional result, item: %v",
					item,
				)
			}
			cidrIdToVpc[cidrVpcMapping.CidrID] = cidrVpcMapping.VPC
		}
	}

	return cidrIdToVpc, nil
}

func cidrEnToPb(
	cidr *entity.CIDR,
	cidrIdToVpc map[uint64]*regionaldedicatedv1.Vpc,
	regionalStateKnown bool,
) *globaldedicatedv1.Cidr {
	out := &globaldedicatedv1.Cidr{
		Id:                cidr.ID,
		ProjectId:         cidr.ProjectID,
		Cidr:              cidr.CIDR,
		CloudProviderName: cidr.BasicRegion.CloudProviderName.String(),
		RegionName:        cidr.BasicRegion.Name.String(),
		RegionDisplayName: cidr.BasicRegion.DisplayName,
		VpcId:             "",
	}
	if !regionalStateKnown {
		out.State = globaldedicatedv1.Cidr_STATE_UNSPECIFIED
		return out
	}

	out.State = globaldedicatedv1.Cidr_STATE_INACTIVE
	if v, ok := cidrIdToVpc[cidr.ID]; ok {
		out.State = globaldedicatedv1.Cidr_STATE_ACTIVE
		out.VpcId = v.VpcId
	}

	return out
}

// getRegionList returns a list of region names, including the given region name. The region list is sorted by created_at and used to calculate the default cidr offset.
func (s *serviceImpl) getRegionList(
	ctx context.Context,
	cloudProviderName globalcommonv1.CloudProvider_Name,
	regionName globalcommonv1.Region_Name,
	projectID uint64,
) ([]string, bizerr.BizError) {
	cidrFilter := &cidrrepo.Filter{
		IDs:                nil,
		ProjectIDs:         []uint64{projectID},
		RegionNames:        nil,
		CloudProviderNames: []globalcommonv1.CloudProvider_Name{cloudProviderName},
	}
	sorter := &globaldedicatedv1.Sorter{
		OrderBy: "created_at",
		Sort:    globaldedicatedv1.Sorter_ASC,
	}
	sorters := []*globaldedicatedv1.Sorter{sorter}
	cidrs, err := s.cidrRepo.Query(ctx, cidrFilter, sorters)
	if err != nil {
		return nil, err
	}
	var regionList []string
	var found bool
	for _, item := range cidrs {
		name := item.BasicRegion.Name
		if name == regionName {
			found = true
		}
		regionList = append(regionList, name.String())
	}
	if !found {
		regionList = append(regionList, regionName.String())
	}
	return regionList, nil
}
