package grpcli

import (
	"context"
	"sync"

	"github.com/elliotchance/pie/v2"
	service_config "github.com/tidbcloud/dataflow-service/internal/config"
	infraapi "github.com/tidbcloud/dataflow-service/pkg/infra-api"
	"github.com/tidbcloud/pkgv2/bizerr"
	globalv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
)

type AWSNetworkSpec struct {
	VpcID                string
	NodeRoleARN          string
	ExternalID           string
	AccountID            string
	NodeSubnets          []string
	SecondaryNodeSubnets []string
	EKSClusterName       string
}

type GCPNetworkSpec struct {
	ServiceAccountEmail string
	ProjectID           string
	VpcID               string
	Subnets             []string
	NodeZones           []string
}

type AzureNetworkSpec struct {
	VNetID string
}

type RegionalNetwork struct {
	AWS              *AWSNetworkSpec
	GCP              *GCPNetworkSpec
	Azure            *AzureNetworkSpec
	DataflowAgentUrl string
	Cidr             string
	ExternalIPs      []string
}

type ProviderVPCInfo struct {
	Network          string
	CloudAccountID   string
	AWSNodeSubnetIDs []string
	GCPZones         []string
}

func (n *RegionalNetwork) AWSExternalID() string {
	if n != nil && n.AWS != nil {
		return n.AWS.ExternalID
	}
	return ""
}

func (n *RegionalNetwork) AWSAccountID() string {
	if n != nil && n.AWS != nil {
		return n.AWS.AccountID
	}
	return ""
}

func (n *RegionalNetwork) GCPServiceAccountEmail() string {
	if n != nil && n.GCP != nil {
		return n.GCP.ServiceAccountEmail
	}
	return ""
}

func (n *RegionalNetwork) GetExternalIPs() []string {
	if n != nil {
		return n.ExternalIPs
	}
	return nil
}

func (n *RegionalNetwork) GetCidr() string {
	if n != nil {
		return n.Cidr
	}
	return ""
}

func (n *RegionalNetwork) GetDataflowAgentUrl() string {
	if n != nil {
		return n.DataflowAgentUrl
	}
	return ""
}

type RegionalNetworkReq struct {
	ProjectID    uint64
	ProviderType string
	Region       string
}

type ProviderVPCInfoReq struct {
	ProjectID    uint64
	ClusterID    uint64
	ProviderType string
	Region       string
}

// TiDBClusterInfraClient is used to query infrastructure information of tidb cluster deployment
type TiDBClusterInfraClient interface {
	GetNetwork(ctx context.Context, req *RegionalNetworkReq) (*RegionalNetwork, bizerr.BizError)
	GetProviderVPCInfo(ctx context.Context, req *ProviderVPCInfoReq) (*ProviderVPCInfo, bizerr.BizError)
	GetNetworkNodeRole(ctx context.Context, req *RegionalNetworkReq) (string, bizerr.BizError)
	GetCloudAccountID(ctx context.Context, req *RegionalNetworkReq) (string, bizerr.BizError)
	GetClusterNodeRole(ctx context.Context, clusterID uint64) (string, bizerr.BizError)
}

var (
	infraClient TiDBClusterInfraClient
	once        sync.Once
)

func InfraClient() TiDBClusterInfraClient {
	if infraClient == nil {
		panic("infraClient should be initialized first")
	}
	return infraClient
}

func MustNewTiDBClusterInfraClient() {
	once.Do(func() {
		infraClient = NewTiDBClusterInfraClient(
			context.Background(),
			service_config.Global().GrpcConfig.DedicatedGlobalSvcAddr,
		)
	})
}

func NewTiDBClusterInfraClient(
	ctx context.Context, dedicatedGlobalAddr string,
) TiDBClusterInfraClient {
	compatClient := newCompatibleCrGrpcClientImpl(ctx, dedicatedGlobalAddr)
	return &tidbClusterInfraClientImpl{
		compatClient: compatClient,
	}
}

// tidbClusterInfraClientImpl is a temporary implementation
// It is used to be compatible with network query during dedicated migration
// After migration, network query to infra api server will be removed
// and dedicated service should provide new interfaces.
type tidbClusterInfraClientImpl struct {
	compatClient CompatibleCrGrpcClient
}

func (c *tidbClusterInfraClientImpl) GetNetwork(
	ctx context.Context, req *RegionalNetworkReq,
) (*RegionalNetwork, bizerr.BizError) {
	return c.getNetworkFromCompatAPI(ctx, req)
}

func (c *tidbClusterInfraClientImpl) GetNetworkNodeRole(
	ctx context.Context, req *RegionalNetworkReq,
) (string, bizerr.BizError) {
	network, err := c.GetNetwork(ctx, req)
	if err != nil {
		return "", err
	}

	if network.AWS != nil {
		return network.AWS.NodeRoleARN, nil
	} else if network.GCP != nil {
		return network.GCP.ServiceAccountEmail, nil
	} else {
		return "", bizerr.New(bizerr.ErrCodeUnknown, "network is empty")
	}
}

func (c *tidbClusterInfraClientImpl) GetCloudAccountID(
	ctx context.Context, req *RegionalNetworkReq,
) (string, bizerr.BizError) {
	network, err := c.GetNetwork(ctx, req)
	if err != nil {
		return "", err
	}

	if network.AWS != nil {
		return network.AWS.AccountID, nil
	} else if network.GCP != nil {
		return network.GCP.ProjectID, nil
	} else {
		return "", bizerr.New(bizerr.ErrCodeUnknown, "network is empty")
	}
}

func (c *tidbClusterInfraClientImpl) GetClusterNodeRole(
	ctx context.Context, clusterID uint64,
) (string, bizerr.BizError) {
	req := &globalv1.GetCompatClusterRequest{
		ClusterId: clusterID,
	}
	cluster, err := c.compatClient.GetCluster(ctx, req)
	if err != nil {
		return "", err
	}

	nodeRole := cluster.GetNodeRole().GetCloudIamIdentity()
	return nodeRole, nil
}

func (c *tidbClusterInfraClientImpl) getNetworkFromCompatAPI(
	ctx context.Context, req *RegionalNetworkReq,
) (*RegionalNetwork, bizerr.BizError) {
	if req.ProviderType != infraapi.ProviderTypeEKS && req.ProviderType != infraapi.ProviderTypeGKE {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "unsupported provider type: %s", req.ProviderType)
	}
	compatNetwork, bizErr := c.compatClient.GetNetwork(ctx, &globalv1.GetCompatNetworkRequest{
		ProjectId:    req.ProjectID,
		ProviderName: req.ProviderType,
		RegionName:   req.Region,
	})
	if bizErr != nil {
		return nil, bizErr
	}

	var network *RegionalNetwork
	if req.ProviderType == infraapi.ProviderTypeEKS {
		network, bizErr = c.parseCompatAWSNetwork(compatNetwork)
	} else {
		network, bizErr = c.parseCompatGCPNetwork(compatNetwork)
	}
	if bizErr != nil {
		return nil, bizErr
	}

	network.Cidr = compatNetwork.GetCidr()
	network.ExternalIPs = compatNetwork.GetExternalIps()
	network.DataflowAgentUrl = compatNetwork.GetDataflowAgentUrl()
	return network, nil
}

func (c *tidbClusterInfraClientImpl) parseCompatAWSNetwork(
	compatNetwork *globalv1.CompatNetwork,
) (*RegionalNetwork, bizerr.BizError) {
	awsInfo := compatNetwork.GetAws()
	if awsInfo == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "aws info is empty")
	}

	awsNetworkSpec := &AWSNetworkSpec{
		VpcID:          compatNetwork.VpcId,
		NodeRoleARN:    compatNetwork.NodeRole,
		AccountID:      compatNetwork.GetIam().GetAccountId(),
		ExternalID:     compatNetwork.GetIam().GetExternalId(),
		EKSClusterName: compatNetwork.K8SName,
		NodeSubnets: pie.Map(awsInfo.NodeSubnets, func(subnet *globalv1.CompatNetwork_Aws_NodeSubnet) string {
			return subnet.SubnetId
		}),
		SecondaryNodeSubnets: pie.Map(awsInfo.SecondaryNodeSubnets,
			func(subnet *globalv1.CompatNetwork_Aws_NodeSubnet) string {
				return subnet.SubnetId
			}),
	}
	return &RegionalNetwork{
		AWS: awsNetworkSpec,
	}, nil
}

func (c *tidbClusterInfraClientImpl) parseCompatGCPNetwork(
	compatNetwork *globalv1.CompatNetwork,
) (*RegionalNetwork, bizerr.BizError) {
	gcpInfo := compatNetwork.GetGcp()
	if gcpInfo == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "gcp info is empty")
	}
	gcpNetworkSpec := &GCPNetworkSpec{
		ServiceAccountEmail: compatNetwork.NodeRole,
		ProjectID:           compatNetwork.GetIam().GetAccountId(),
		VpcID:               compatNetwork.GetVpcId(),
		Subnets:             gcpInfo.NodeSubnets,
		NodeZones:           gcpInfo.NodeZones,
	}
	return &RegionalNetwork{
		GCP: gcpNetworkSpec,
	}, nil
}

func (c *tidbClusterInfraClientImpl) GetProviderVPCInfo(
	ctx context.Context, req *ProviderVPCInfoReq,
) (*ProviderVPCInfo, bizerr.BizError) {
	regionalNetwork, err := c.getNetworkFromCompatAPI(ctx, &RegionalNetworkReq{
		ProjectID:    req.ProjectID,
		ProviderType: req.ProviderType,
		Region:       req.Region,
	})
	if err == nil {
		if req.ProviderType == infraapi.ProviderTypeEKS {
			return &ProviderVPCInfo{
				CloudAccountID:   regionalNetwork.AWSAccountID(),
				Network:          regionalNetwork.AWS.VpcID,
				AWSNodeSubnetIDs: regionalNetwork.AWS.NodeSubnets,
			}, nil
		}
		if req.ProviderType == infraapi.ProviderTypeGKE {
			return &ProviderVPCInfo{
				Network:        regionalNetwork.GCP.VpcID,
				GCPZones:       regionalNetwork.GCP.NodeZones,
				CloudAccountID: regionalNetwork.GCP.ProjectID,
			}, nil
		}
	}
	return nil, bizerr.WrapError(err, "get provider vpc info")
}
