package privatelink

import (
	"context"
	"strconv"

	"github.com/samber/lo"
	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/privatelink"
	tidbgrouprepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/tidbgroup"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

func (s *service) ListPrivateLinkConnections(
	ctx context.Context,
	req *globalngv1.ListPrivateLinkConnectionsRequest,
) ([]*entity.PrivateLinkConnection, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "privatelink.ListPrivateLinkConnections")
	defer span.End()

	if req.GetClusterId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster_id is required")
	}
	cluster, err := s.clusterRepo.Get(ctx, req.GetClusterId(), false)
	if err != nil {
		return nil, err
	}
	if cluster == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "cluster not found")
	}

	var tidbgroups []*entity.TidbGroup
	if req.GetTidbGroupId() != 0 {
		tidbgroups, err = s.tidbGroupRepo.Query(ctx, &tidbgrouprepo.Filter{
			ClusterIds: []uint64{req.GetClusterId()},
			Ids:        []uint64{req.GetTidbGroupId()},
		})
		if err != nil {
			return nil, err
		}
	} else {
		tidbgroups, err = s.tidbGroupRepo.Query(ctx, &tidbgrouprepo.Filter{
			ClusterIds: []uint64{req.GetClusterId()},
		})
		if err != nil {
			return nil, err
		}
	}

	region, err := s.regionRepo.Get(ctx, cluster.RegionId)
	if err != nil {
		return nil, err
	}

	connections := make([]*entity.PrivateLinkConnection, 0, len(tidbgroups))
	for _, tidbgroup := range tidbgroups {
		repoConnctions, err := s.privatelinkRepo.ListPrivateLinkConnections(ctx, &privatelink.Filter{
			ClusterID:   cluster.ClusterId,
			TidbGroupID: tidbgroup.Id,
		})
		if err != nil {
			return nil, err
		}
		if len(repoConnctions) == 0 {
			continue
		}
		privatelinkServiceEntity, err := s.privatelinkRepo.GetPrivateLinkService(
			ctx,
			repoConnctions[0].PrivateLinkServiceID,
		)
		if err != nil {
			return nil, err
		}
		regionService, err := s.regionalCli.GetPrivateLinkService(
			ctx,
			region,
			&clusterngv1beta.GetPrivateLinkServiceRequest{
				ClusterId:            strconv.FormatUint(cluster.ClusterId, 10),
				TidbGroupId:          strconv.FormatUint(tidbgroup.Id, 10),
				PrivateLinkServiceId: strconv.FormatUint(repoConnctions[0].PrivateLinkServiceID, 10),
			},
		)
		if err != nil {
			return nil, err
		}
		connections = append(
			connections,
			buildResults(ctx, cluster, tidbgroup, repoConnctions, regionService, privatelinkServiceEntity)...)
	}

	return connections, nil
}

func (s *service) GetPrivateLinkConnection(
	ctx context.Context,
	req *globalngv1.GetPrivateLinkConnectionRequest,
) (*entity.PrivateLinkConnection, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "privatelink.GetPrivateLinkConnection")
	defer span.End()

	if req.GetPrivateLinkConnectionId() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "private_link_connection_id is required")
	}

	privateLinkConnectionIDUint64, parseErr := strconv.ParseUint(req.GetPrivateLinkConnectionId(), 10, 64)
	if parseErr != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid private_link_connection_id")
	}

	repoConnection, err := s.privatelinkRepo.GetPrivateLinkConnection(ctx, privateLinkConnectionIDUint64)
	if err != nil {
		return nil, err
	}

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

	tidbgroup, err := s.tidbGroupRepo.Load(ctx, cluster.ClusterId, repoConnection.TidbGroupId)
	if err != nil {
		return nil, err
	}

	region, err := s.regionRepo.Get(ctx, cluster.RegionId)
	if err != nil {
		return nil, err
	}

	privatelinkServiceEntity, err := s.privatelinkRepo.GetPrivateLinkService(ctx, repoConnection.PrivateLinkServiceID)
	if err != nil {
		return nil, err
	}

	regionPrivateLinkSvc, err := s.regionalCli.GetPrivateLinkService(
		ctx,
		region,
		&clusterngv1beta.GetPrivateLinkServiceRequest{
			ClusterId:            strconv.FormatUint(cluster.ClusterId, 10),
			TidbGroupId:          strconv.FormatUint(repoConnection.TidbGroupId, 10),
			PrivateLinkServiceId: strconv.FormatUint(repoConnection.PrivateLinkServiceID, 10),
		},
	)
	if err != nil {
		return nil, err
	}

	outs := buildResults(
		ctx,
		cluster,
		tidbgroup,
		[]*entity.PrivateLinkConnection{repoConnection},
		regionPrivateLinkSvc,
		privatelinkServiceEntity,
	)
	if len(outs) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "private link connection not found")
	}

	if len(outs) > 1 {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "multiple private link connections found")
	}

	return outs[0], nil
}

func buildResults(
	ctx context.Context,
	cluster *entity.Cluster,
	tidbgroup *entity.TidbGroup,
	repoConnections []*entity.PrivateLinkConnection,
	regionPrivateLinkSvc *clusterngv1beta.PrivateLinkService,
	privatelinkServiceEntity *entity.PrivateLinkService,
) []*entity.PrivateLinkConnection {
	outs := make([]*entity.PrivateLinkConnection, 0)

	// Join repo connections to region connections.
	for _, c := range regionPrivateLinkSvc.GetConnections() {
		out := &entity.PrivateLinkConnection{
			PrivateLinkConnection: &globalngv1.PrivateLinkConnection{
				ClusterId:         cluster.ClusterId,
				TidbGroupId:       tidbgroup.Id,
				RegionId:          cluster.GetRegionId(),
				RegionDisplayName: cluster.GetRegionDisplayName(),
				CloudProviderId:   cluster.GetCloudProviderId(),
				// EndpointId:  fill later
				Message: c.GetMessage(),
				// PrivateLinkService: fill later
				PrivateLinkServiceState: coverToGlobalPrivateLinkServiceState(regionPrivateLinkSvc.GetStatus()),
				State:                   coverToGlobalPrivateLinkConnectionState(c.GetStatus()),
				// Host: fill later
				Port: strconv.FormatUint(uint64(privatelinkServiceEntity.GetServicePort()), 10),
			},
		}

		switch cluster.GetCloudProviderId() {
		case globalcommonv1.CloudProvider_AWS:
			out.EndpointId = c.GetAws().GetVpcEndpointId()
			out.Host = regionPrivateLinkSvc.GetServiceDnsName()
			out.PrivateLinkService = regionPrivateLinkSvc.GetAws().GetServiceName()
		case globalcommonv1.CloudProvider_GCP:
			out.EndpointId = c.GetGcp().GetPrivateServiceConnectEndpoint()
			out.Host = c.GetGcp().GetHost()
			out.PrivateLinkService = regionPrivateLinkSvc.GetGcp().GetServiceAttachment()
		case globalcommonv1.CloudProvider_AZURE:
			out.EndpointId = c.GetAzure().GetPrivateEndpointResourceId()
			out.PrivateIpAddress = c.GetAzure().GetPrivateIpAddress()
			out.Host = c.GetAzure().GetHost()
			out.PrivateLinkService = regionPrivateLinkSvc.GetAzure().GetServiceResourceId()
		case globalcommonv1.CloudProvider_ALICLOUD:
			out.EndpointId = c.GetAliCloud().GetVpcEndpointId()
			out.Host = c.GetAliCloud().GetHost()
			out.PrivateLinkService = regionPrivateLinkSvc.GetAliCloud().GetServiceName()
			out.PrivateDomainName = c.GetAliCloud().GetDefaultDomainName()
		default:
			tilog.Error(ctx, "unknown cloud provider", zap.String("cloud_provider", cluster.GetCloudProviderId().String()))
			out.EndpointId = "unknown"
		}

		if repoConnection, ok := lo.Find(repoConnections, func(connection *entity.PrivateLinkConnection) bool {
			return connection.EndpointId == out.EndpointId
		}); !ok {
			if c.GetStatus() != clusterngv1beta.PrivateLinkService_Connection_PENDING {
				// If the endpoint is not found in the repository, and the status is not pending, it is a deleted endpoint.
				tilog.Warn(
					ctx,
					"endpoint not found in repository",
					zap.String("endpoint_id", out.EndpointId),
					zap.String("status", c.GetStatus().String()),
				)
				continue
			}
			out.State = globalngv1.PrivateLinkConnection_DISCOVERED
		} else {
			out.EndpointType = repoConnection.GetEndpointType()
			// out.PrivateLinkService = repoConnection.GetPrivateLinkService()
			out.PrivateLinkConnectionId = repoConnection.PrivateLinkConnectionId
		}

		outs = append(outs, out)
	}

	endpointNames := lo.Associate(outs, func(i *entity.PrivateLinkConnection) (string, any) {
		return i.EndpointId, nil
	})

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

		// Endpoint not found in region, set status to PENDING.
		outs = append(outs, &entity.PrivateLinkConnection{
			PrivateLinkConnection: &globalngv1.PrivateLinkConnection{
				ClusterId:               cluster.ClusterId,
				TidbGroupId:             tidbgroup.Id,
				RegionId:                cluster.GetRegionId(),
				RegionDisplayName:       cluster.GetRegionDisplayName(),
				CloudProviderId:         cluster.GetCloudProviderId(),
				EndpointId:              repoConnection.EndpointId,
				PrivateIpAddress:        repoConnection.GetPrivateIpAddress(),
				State:                   globalngv1.PrivateLinkConnection_PENDING,
				EndpointType:            repoConnection.EndpointType,
				PrivateLinkService:      repoConnection.GetPrivateLinkService(),
				PrivateLinkServiceState: coverToGlobalPrivateLinkServiceState(regionPrivateLinkSvc.GetStatus()),
				PrivateLinkConnectionId: repoConnection.PrivateLinkConnectionId,
				Port:                    strconv.FormatUint(uint64(privatelinkServiceEntity.GetServicePort()), 10),
			},
		})
	}

	return outs
}

func coverToGlobalPrivateLinkConnectionState(
	state clusterngv1beta.PrivateLinkService_Connection_Status,
) globalngv1.PrivateLinkConnection_State {
	switch state {
	case clusterngv1beta.PrivateLinkService_Connection_PENDING:
		return globalngv1.PrivateLinkConnection_PENDING
	case clusterngv1beta.PrivateLinkService_Connection_ACTIVE:
		return globalngv1.PrivateLinkConnection_ACTIVE
	case clusterngv1beta.PrivateLinkService_Connection_FAILED:
		return globalngv1.PrivateLinkConnection_FAILED
	case clusterngv1beta.PrivateLinkService_Connection_DELETING:
		return globalngv1.PrivateLinkConnection_DELETING
	default:
		return globalngv1.PrivateLinkConnection_PENDING
	}
}
