package privatelink

import (
	"context"
	"strconv"

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

func (svc *service) ListPrivateLinkEndpoints(
	ctx context.Context,
	request *globaldedicatedv1.ListPrivateLinkEndpointsRequest,
) ([]*globaldedicatedv1.PrivateLinkEndpoint, bizerr.BizError) {
	if request.GetOrgIds() == nil || request.GetProjectIds() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "empty orgIDs or projectIDs")
	}

	clusters, err := svc.clusterRepo.Query(ctx, &clusterrepo.Filter{
		ExcludeStates:   []entity.State{globaldedicatedv1.Cluster_CREATING},
		OrganizationIds: request.GetOrgIds(),
		ProjectIds:      request.GetProjectIds(),
		ClusterIds:      request.GetClusterIds(),
		ShowDeleted:     false,
	})
	if err != nil {
		return nil, err
	}

	outs := make([]*globaldedicatedv1.PrivateLinkEndpoint, 0)

	for _, cluster := range clusters {
		tidbGroup, err := svc.tidbGroupRepo.LoadDefault(ctx, cluster.ClusterId)
		if err != nil {
			return nil, err
		}

		repoEndpoints, err := svc.privateLinkEndpointRepo.Query(ctx, &endpointrepo.PrivateLinkEndpointFilter{
			ClusterIDs:   []uint64{cluster.ClusterId},
			TidbGroupIDs: []uint64{tidbGroup.Id},
		})
		if err != nil {
			return nil, err
		}

		region, err := svc.regionRepo.SelectRegionById(ctx, cluster.RegionId, cluster.OrganizationId)
		if err != nil {
			return nil, err
		}
		if region.GetState() == globalcommonv1.Region_INACTIVE {
			tilog.Warn(ctx, "region is inactive", zap.String("regionID", region.Id.String()))
			continue
		}

		regionService, err := svc.regionServerCli.GetPrivateLinkService(
			ctx,
			region,
			&regionserver.GetPrivateLinkServiceRequest{
				ClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
				TidbGroupId: strconv.FormatUint(tidbGroup.Id, 10),
			},
		)
		if err != nil {
			if err.ErrCode() != bizerr.ErrCodeRecordNotFound {
				return nil, err
			}
			// If the tidb group is not ready, get private link service will return 404.
			tilog.Warn(
				ctx,
				"private link service not found",
				zap.Uint64("clusterID", cluster.ClusterId),
				zap.Uint64("tidbGroupID", tidbGroup.Id),
			)
		}

		res := buildResults(ctx, cluster, repoEndpoints, regionService)

		outs = append(outs, res...)
	}

	return outs, nil
}

func (svc *service) ListTidbNodeGroupPrivateLinkEndpoints(
	ctx context.Context,
	request *globaldedicatedv1.ListTidbNodeGroupPrivateLinkEndpointsRequest,
) (
	*globaldedicatedv1.PrivateLinkService,
	[]*globaldedicatedv1.PrivateLinkEndpoint,
	bizerr.BizError,
) {
	if request.TidbGroupId == 0 {
		return nil, nil, bizerr.New(bizerr.ErrCodeParams, "tidb group id is required")
	}

	tidbGroup, err := svc.tidbGroupRepo.Load(ctx, request.ClusterId, request.GetTidbGroupId())
	if err != nil {
		return nil, nil, err
	}

	repoEndpoints, err := svc.privateLinkEndpointRepo.Query(
		ctx,
		&endpointrepo.PrivateLinkEndpointFilter{
			TidbGroupIDs: []uint64{request.GetTidbGroupId()},
		})
	if err != nil {
		return nil, nil, err
	}

	cluster, err := svc.clusterRepo.Get(ctx, tidbGroup.ClusterId, false)
	if err != nil {
		return nil, nil, err
	}

	region, err := svc.regionRepo.SelectRegionById(ctx, cluster.RegionId, cluster.OrganizationId)
	if err != nil {
		return nil, nil, err
	}
	if region.GetState() == globalcommonv1.Region_INACTIVE {
		tilog.Warn(ctx, "region is inactive", zap.String("regionID", region.Id.String()))
		return nil, []*globaldedicatedv1.PrivateLinkEndpoint{}, nil
	}

	regionService, err := svc.regionServerCli.GetPrivateLinkService(
		ctx,
		region,
		&regionserver.GetPrivateLinkServiceRequest{
			ClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
			TidbGroupId: strconv.FormatUint(tidbGroup.Id, 10),
		},
	)
	if err != nil {
		if err.ErrCode() != bizerr.ErrCodeRecordNotFound {
			return nil, nil, err
		}
		// If the tidb group is not ready, get private link service will return 404.
		tilog.Warn(
			ctx,
			"private link service not found",
			zap.Uint64("clusterID", cluster.ClusterId),
			zap.Uint64("tidbGroupID", tidbGroup.Id),
		)
	}

	privateLinkEndpoints := buildResults(ctx, cluster, repoEndpoints, regionService)
	if len(request.ExcludeTypes) > 0 {
		privateLinkEndpoints = lo.Filter(
			privateLinkEndpoints,
			func(endpoint *globaldedicatedv1.PrivateLinkEndpoint, _ int) bool {
				return !lo.Contains(request.ExcludeTypes, endpoint.Type)
			},
		)
	}

	return privateLinkServiceRegionalToGlobal(regionService, cluster), privateLinkEndpoints, nil
}

func buildResults(
	ctx context.Context,
	cluster *entity.Cluster,
	repoEndpoints []*entity.PrivateLinkEndpoint,
	regionPrivateLinkSvc *regionaldedicatedv1.PrivateLinkService,
) []*globaldedicatedv1.PrivateLinkEndpoint {
	outs := make([]*globaldedicatedv1.PrivateLinkEndpoint, 0)

	// Join repo connections to region connections.
	for _, c := range regionPrivateLinkSvc.GetConnections() {
		var endpointName, host string
		switch cluster.GetCloudProviderId() {
		case globalcommonv1.CloudProvider_AWS:
			endpointName = c.GetAws().GetVpcEndpointId()
			host = regionPrivateLinkSvc.GetServiceDnsName()
		case globalcommonv1.CloudProvider_GCP:
			endpointName = c.GetGcp().GetPrivateServiceConnectEndpoint()
			host = c.GetGcp().GetHost()
		case globalcommonv1.CloudProvider_AZURE:
			endpointName = c.GetAzure().GetPrivateEndpointResourceId()
			host = c.GetAzure().GetHost()
		}

		out := &globaldedicatedv1.PrivateLinkEndpoint{
			EndpointName: endpointName,
			Status:       privateLinkEndpointStatusRegionalToGlobal(c.GetStatus()),
			IpAddressType: privateLinkEndpointIPAddressTypeRegionalToGlobal(
				c.GetIpAddressType(),
			),
			Message:          c.GetMessage(),
			PrivateIpAddress: c.GetAzure().GetPrivateIpAddress(),
			Host:             host,
		}

		if repoEndpoint, ok := lo.Find(repoEndpoints, func(endpoint *entity.PrivateLinkEndpoint) bool {
			return endpoint.EndpointID == endpointName
		}); !ok {
			// No endpoint found in the repository, and if the status is pending, the endpoint is discovered endpoint.
			// Currently, it mainly exists under the azure cloud provider. Once the user calls the CreatePrivateLinkEndpoint
			// API, the meta will be stored in the repo.
			if c.Status == regionaldedicatedv1.PrivateLinkService_Connection_PENDING {
				out.Status = globaldedicatedv1.PrivateLinkEndpoint_DISCOVERED
			}
		} else {
			out.Id = repoEndpoint.ID
			out.Type = repoEndpoint.Type
		}

		outs = append(outs, out)
	}

	regionConnEndpointNames := lo.Associate(outs, func(i *globaldedicatedv1.PrivateLinkEndpoint) (string, any) {
		return i.GetEndpointName(), nil
	})

	// Find connections exist in repo but not in regional.
	// These are connections still in workflow.
	for _, repoEndpoint := range repoEndpoints {
		if _, ok := regionConnEndpointNames[repoEndpoint.EndpointID]; ok {
			tilog.Info(ctx, "endpoint found in region", zap.String("endpointID", repoEndpoint.EndpointID))
			continue
		}

		// Endpoint not found in region, set status to PENDING.
		outs = append(outs, &globaldedicatedv1.PrivateLinkEndpoint{
			Id:               repoEndpoint.ID,
			EndpointName:     repoEndpoint.EndpointID,
			Status:           globaldedicatedv1.PrivateLinkEndpoint_PENDING,
			Type:             repoEndpoint.Type,
			PrivateIpAddress: repoEndpoint.PrivateIpAddress,
		})
	}

	return outs
}

func privateLinkEndpointIPAddressTypeRegionalToGlobal(
	addressType regionaldedicatedv1.PrivateLinkService_Connection_IPAddressType,
) globaldedicatedv1.PrivateLinkIPAddressType {
	switch addressType {
	case regionaldedicatedv1.PrivateLinkService_Connection_IP_ADDRESS_TYPE_IPV4:
		return globaldedicatedv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_IPV4
	case regionaldedicatedv1.PrivateLinkService_Connection_IP_ADDRESS_TYPE_IPV6:
		return globaldedicatedv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_IPV6
	case regionaldedicatedv1.PrivateLinkService_Connection_IP_ADDRESS_TYPE_DUALSTACK:
		return globaldedicatedv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_DUALSTACK
	default:
		return globaldedicatedv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_UNSPECIFIED
	}
}

func privateLinkEndpointStatusRegionalToGlobal(
	status regionaldedicatedv1.PrivateLinkService_Connection_Status,
) globaldedicatedv1.PrivateLinkEndpoint_Status {
	switch status {
	case regionaldedicatedv1.PrivateLinkService_Connection_PENDING:
		return globaldedicatedv1.PrivateLinkEndpoint_PENDING
	case regionaldedicatedv1.PrivateLinkService_Connection_ACTIVE:
		return globaldedicatedv1.PrivateLinkEndpoint_ACTIVE
	case regionaldedicatedv1.PrivateLinkService_Connection_DELETING:
		return globaldedicatedv1.PrivateLinkEndpoint_DELETING
	case regionaldedicatedv1.PrivateLinkService_Connection_FAILED:
		return globaldedicatedv1.PrivateLinkEndpoint_FAILED
	default:
		return globaldedicatedv1.PrivateLinkEndpoint_PENDING
	}
}
