// Copy from cluster-controller/controllers/networkpeer/task/gke/peer.go
package gcp

import (
	"context"
	"fmt"
	"path"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	_ "github.com/crossplane/crossplane-runtime/pkg/meta"
	gcpvpcpeeringv1beta1 "github.com/crossplane/provider-gcp/apis/vpcpeering/v1beta1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/pkg/applier"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/vpcpeering/task"
)

const (
	defaultWaitDuration = time.Second * 30
)

type Peering struct {
	*task.BaseTask
}

func (t *Peering) Name() string {
	return "Peering"
}

func NewPeering(ctx *task.Context) *Peering {
	return &Peering{
		BaseTask: &task.BaseTask{
			Context: ctx,
		},
	}
}

func (t *Peering) Create(ctx context.Context) deploy.Result {
	// we already check the VPCPeering.Spec.Target.GCP in Default(), thus VPCPeering.Sepc.Target.GCP cannot be nil here.
	peerProject := t.VPCPeering.Spec.Target.GCP.ProjectID
	peerNetwork := t.VPCPeering.Spec.Target.GCP.PeerNetwork

	peer := t.NewVPCPeering(peerProject, peerNetwork)
	mutateFn := mutateGCPNetworkPeering
	// if the vpcpeering is migrated from infra-apiserver do not mutate anything.
	if islegacy := t.VPCPeering.Labels[v1alpha1.IsLegacyKey]; islegacy == "true" {
		mutateFn = mutateNothing
	}
	peer, err := applier.NewApplier(t.Local, mutateFn).
		Expected(peer).
		Apply(ctx)
	if err != nil {
		return deploy.Error(err)
	}
	gk := peer.GetObjectKind().GroupVersionKind().GroupKind().String()
	t.VPCPeering.Status.CrossplaneResourceName = path.Join(gk, peer.Name)

	// N.B. the tidbcloud provider-gcp's implement is not correct,
	// the real external name is `.Spec.ForProvider.Name`.
	// peering's extarnal annotation just points to the cr name.
	if externalName := peer.Spec.ForProvider.Name; externalName != "" {
		t.VPCPeering.Labels[v1alpha1.LabelKeyExternalName] = externalName
		t.VPCPeering.Status.ExternalName = externalName
	}
	if t.VPCPeering.Annotations[v1alpha1.AnnotationKeySourceVPCID] == "" {
		t.VPCPeering.Annotations[v1alpha1.AnnotationKeySourceVPCID] = t.VPC.Status.VPC.VPCID
	}

	ready, synced := utils.GetConditions(peer.Status.Conditions)
	if ready && synced {
		t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhaseActive
		t.VPCPeering.Status.Reason = "Peering is synced and ready"
	} else if !ready && synced {
		t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhasePending
		t.VPCPeering.Status.Reason = "Peering is synced but not ready"
	}
	// always requeue
	return deploy.Done().WithRequeuAfter(defaultWaitDuration)
}

func (t *Peering) Delete(ctx context.Context) deploy.Result {
	var name string
	// CrossplaneResourceName has fomat `King.Group/Name`
	if resourceName := t.VPCPeering.Status.CrossplaneResourceName; resourceName != "" {
		name = path.Base(resourceName)
	} else {
		name = genPeeringName(t.VPCPeering)
	}
	obj := applier.NewClusterObject[*gcpvpcpeeringv1beta1.Peering](name)
	if err := t.Local.Delete(ctx, obj); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *Peering) PostDelete(ctx context.Context) deploy.Result {
	var name string
	// CrossplaneResourceName has fomat `King.Group/Name`
	if resourceName := t.VPCPeering.Status.CrossplaneResourceName; resourceName != "" {
		name = path.Base(resourceName)
	} else {
		name = genPeeringName(t.VPCPeering)
	}
	obj := applier.NewClusterObject[*gcpvpcpeeringv1beta1.Peering](name)
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
		if errors.IsNotFound(err) {
			return deploy.Done()
		}
		return deploy.Error(err)
	}
	return deploy.Error(fmt.Errorf("peering cr is not deleted"))
}

func (t *Peering) NewVPCPeering(
	peerProject string,
	peerNetwork string,
) *gcpvpcpeeringv1beta1.Peering {
	// providerConfigName := namer.UniqueIdentifier(t.VPC.ObjectMeta)
	providerConfigName := t.ProviderConfigName()
	name := genPeeringName(t.VPCPeering)
	project := t.VPC.Spec.AccountRef.AccountID
	// for gcp, we store the network name in vpc.status.vpcId
	// we already check Status.VPCID in Gardian(), thus VPCID cannot be empty here.
	network := t.VPC.Status.VPC.VPCID
	peerNetworkURI := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/networks/%s", peerProject, peerNetwork)
	return &gcpvpcpeeringv1beta1.Peering{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "vpcpeering.gcp.crossplane.io/v1beta1",
			Kind:       "Peering",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: gcpvpcpeeringv1beta1.PeeringSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: providerConfigName,
				},
			},
			// the provider-gcp(tidbcloud) use ForProvider.Name to create peering on gcp
			// gcp has limitation for name Must be a match of regex '(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)'
			// so we use the cr name instead of the Vpcpeering.Name(which equals to the bizID generated by global-service and bizID is number)
			ForProvider: gcpvpcpeeringv1beta1.PeeringParameters{
				Project:            project,
				Network:            network,
				Name:               name,
				PeerNetwork:        peerNetworkURI,
				ImportCustomRoutes: true,
			},
		},
	}
}

func genPeeringName(peering *v1alpha1.VPCPeering) string {
	if v, ok := peering.Annotations[eksv1alpha1.OwnCrossplaneNameAddKey]; ok {
		return v
	}
	return namer.UniqueIdentifier(peering.ObjectMeta, namer.WithLimit(40))
}

func mutateGCPNetworkPeering(current, expected *gcpvpcpeeringv1beta1.Peering) error {
	// current.Labels = expected.Labels
	// current.Annotations = expected.Annotations
	current.Spec.ResourceSpec = expected.Spec.ResourceSpec
	current.Spec.ForProvider = expected.Spec.ForProvider
	return nil
}

func mutateNothing(current, expected *gcpvpcpeeringv1beta1.Peering) error {
	return nil
}
