package infraapi

import (
	"context"
	"strconv"
	"time"

	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/errors"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/wait"
)

const (
	peerDeleteTimeout = 5 * time.Second
	peerCheckInterval = 50 * time.Millisecond
)

func (a *API) GetNetworkPeer(ctx context.Context, providerType string, projectID uint64, region string, customerVpcId string) (*cloudv1.NetworkPeer, error) {
	var (
		networkpeer     cloudv1.NetworkPeer
		ns              = Namespace(a.projectName, projectID)
		networkpeerName = NetworkPeerName(providerType, region, customerVpcId)
	)
	err := a.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: networkpeerName}, &networkpeer)
	if err != nil {
		if apierrors.IsNotFound(err) {
			networkpeerName = NetworkPeerNameOld(providerType, region)
			err = a.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: networkpeerName}, &networkpeer)
			if err != nil {
				return nil, errors.Annotatef(err, "get networkpeer CR %q in namespace %q failed", networkpeerName, ns)
			}
			return &networkpeer, nil
		}
		return nil, errors.Annotatef(err, "get networkpeer CR %q in namespace %q failed", networkpeerName, ns)
	}
	// peer is being finalized in background
	if !networkpeer.DeletionTimestamp.IsZero() {
		return nil, errors.Errorf("networkpeer %q has been deleted", networkpeerName)
	}
	return &networkpeer, nil
}

func (a *API) CreateAwsNetworkPeer(
	ctx context.Context,
	tenantID uint64,
	projectID uint64,
	providerType string,
	shootRegion string,
	customRegion string,
	peerAccountId string,
	customerVpcId string,
	customerVpcCidr string,
	peeringType entity.PeeringType,
) error {
	tenantIDStr := strconv.FormatUint(tenantID, 10)
	projectIDStr := strconv.FormatUint(projectID, 10)
	ns := Namespace(a.projectName, projectID)
	networkpeer := &cloudv1.NetworkPeer{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      NetworkPeerName(providerType, shootRegion, customerVpcId),
			Labels: map[string]string{
				tenantLabel:      tenantIDStr,
				projectLabel:     projectIDStr,
				projectNameLabel: a.projectName,
				serviceTypeLabel: dedicated,
			},
		},
		Spec: cloudv1.NetworkPeerSpec{
			Source: NetworkName(providerType, shootRegion),
		},
	}
	if peeringType == entity.PeeringTypeCustomer {
		networkpeer.Spec.Target = cloudv1.Target{
			AWS: &cloudv1.AWS{
				AccountID:  peerAccountId,
				VpcID:      customerVpcId,
				PeerCIDR:   customerVpcCidr,
				PeerRegion: customRegion,
			},
		}
	} else {
		networkpeer.Spec.Source = NetworkName(providerType, customRegion)
		networkpeer.Spec.Target = cloudv1.Target{
			TargetNetwork: &cloudv1.TargetNetwork{
				NetworkName: NetworkName(providerType, shootRegion),
			},
		}
	}
	err := a.client.Create(ctx, networkpeer)
	if err != nil && !apierrors.IsAlreadyExists(err) {
		return errors.Annotatef(err, "create networkpeer CR %q in namespace %q failed", networkpeer.Name, networkpeer.Namespace)
	}
	return nil
}

func (a *API) CreateGcpVpcPeer(
	ctx context.Context,
	providerType string,
	projectID uint64,
	tenantID uint64,
	shootRegion string,
	customerProjectId string,
	customerVpcNetwork string,
	customRegion string,
	peeringType entity.PeeringType,
) error {
	tenantIDStr := strconv.FormatUint(tenantID, 10)
	projectIDStr := strconv.FormatUint(projectID, 10)
	ns := Namespace(a.projectName, projectID)
	networkpeer := &cloudv1.NetworkPeer{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      NetworkPeerName(providerType, shootRegion, customerVpcNetwork),
			Labels: map[string]string{
				tenantLabel:      tenantIDStr,
				projectLabel:     projectIDStr,
				projectNameLabel: a.projectName,
				serviceTypeLabel: dedicated,
			},
		},
		Spec: cloudv1.NetworkPeerSpec{
			Source: NetworkName(providerType, shootRegion),
		},
	}
	if peeringType == entity.PeeringTypeCustomer {
		networkpeer.Spec.Target = cloudv1.Target{
			GCP: &cloudv1.GCP{
				ProjectID:   customerProjectId,
				PeerNetwork: customerVpcNetwork,
			},
		}
	} else {
		networkpeer.Spec.Source = NetworkName(providerType, customRegion)
		networkpeer.Spec.Target = cloudv1.Target{
			TargetNetwork: &cloudv1.TargetNetwork{
				NetworkName: NetworkName(providerType, shootRegion),
			},
		}
	}
	err := a.client.Create(ctx, networkpeer)
	if err != nil && !apierrors.IsAlreadyExists(err) {
		return errors.Annotatef(err, "create networkpeer CR %q in namespace %q failed", networkpeer.Name, networkpeer.Namespace)
	}
	return nil
}

func NetworkPeerName(providerType string, region string, customerVpcID string) string {
	return NetworkName(providerType, region) + "-" + customerVpcID + "-" + "networkpeer"
}

// this function is expired, but for compatible with old logic, this should use for the old  vpc peerings are all deleted
func NetworkPeerNameOld(providerType string, region string) string {
	return NetworkName(providerType, region) + "-" + "networkpeer"
}

func (a *API) DeleteNetworkPeerIgnoreNotFound(ctx context.Context, providerType string, projectID uint64, region string, customerVpcId string) error {
	networkpeer := &cloudv1.NetworkPeer{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: Namespace(a.projectName, projectID),
			Name:      NetworkPeerName(providerType, region, customerVpcId),
		},
	}
	err := a.client.Delete(ctx, networkpeer)
	if err != nil && !apierrors.IsNotFound(err) {
		return errors.Annotatef(err, "delete networkpeer CR %q in namespace %q failed", networkpeer.Name, networkpeer.Namespace)
	} else if err != nil && apierrors.IsNotFound(err) {
		// adoptable with old logic
		networkpeer = &cloudv1.NetworkPeer{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: Namespace(a.projectName, projectID),
				Name:      NetworkPeerNameOld(providerType, region),
			},
		}
		err = a.client.Delete(ctx, networkpeer)
		if err != nil && !apierrors.IsNotFound(err) {
			return errors.Annotatef(err, "delete networkpeer CR %q in namespace %q failed", networkpeer.Name, networkpeer.Namespace)
		}
	}
	// wait the deletion operation synced in local cache
	return wait.PollImmediate(peerCheckInterval, peerDeleteTimeout, func() (bool, error) {
		peer, _ := a.GetNetworkPeer(context.TODO(), providerType, projectID, region, customerVpcId)
		return peer == nil, nil
	})
}
