package main

import (
	"context"
	"fmt"
	"reflect"
	"time"

	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	schedulingv1 "k8s.io/api/scheduling/v1"
	storagev1 "k8s.io/api/storage/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/util/retry"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/pkg/scheme"
)

func init() {
	ignoredCRDs := []string{
		"tidbclusterautoscalers.pingcap.com",
		"challenges.acme.cert-manager.io",
		"cninodes.vpcresources.k8s.aws",
		"eniconfigs.crd.k8s.amazonaws.com",
		"gatewayclasses.gateway.networking.k8s.io",
		"gateways.gateway.networking.k8s.io",
		"httproutes.gateway.networking.k8s.io",
		"referencegrants.gateway.networking.k8s.io",
		"ingressclassparameterses.configuration.konghq.com",
		"kongclusterplugins.configuration.konghq.com",
		"kongconsumergroups.configuration.konghq.com",
		"kongconsumers.configuration.konghq.com",
		"kongingresses.configuration.konghq.com",
		"konglicenses.configuration.konghq.com",
		"kongplugins.configuration.konghq.com",
		"kongupstreampolicies.configuration.konghq.com",
		"kongvaults.configuration.konghq.com",
		"tcpingresses.configuration.konghq.com",
		"udpingresses.configuration.konghq.com",
		"ingressclassparams.elbv2.k8s.aws",
		"orders.acme.cert-manager.io",
		"policyendpoints.networking.k8s.aws",
		"securitygrouppolicies.vpcresources.k8s.aws",
		"targetgroupbindings.elbv2.k8s.aws",
		"gcpdatasources.datalayer.gke.io",
		"gcpresourceallowlists.node.gke.io",
		"workloadallowlists.auto.gke.io",
		"allowlistedv2workloads.auto.gke.io",
		"allowlistedworkloads.auto.gke.io",
		"audits.warden.gke.io",
		"capacityrequests.internal.autoscaling.gke.io",
		"frontendconfigs.networking.gke.io",
		"gkenetworkparamsets.networking.gke.io",
		"managedcertificates.networking.gke.io",
		"memberships.hub.gke.io",
		"networks.networking.gke.io",
		"serviceattachments.networking.gke.io",
		"servicenetworkendpointgroups.networking.gke.io",
		"updateinfos.nodemanagement.gke.io",
		"backendconfigs.cloud.google.com",
		"computeclasses.cloud.google.com",
		"clusternodemonitorings.monitoring.googleapis.com",
		"clusterpodmonitorings.monitoring.googleapis.com",
		"clusterrules.monitoring.googleapis.com",
		"globalrules.monitoring.googleapis.com",
		"operatorconfigs.monitoring.googleapis.com",
		"podmonitorings.monitoring.googleapis.com",
		"rules.monitoring.googleapis.com",
		"provisioningrequests.autoscaling.x-k8s.io",
		"volumesnapshotclasses.snapshot.storage.k8s.io",
		"volumesnapshotcontents.snapshot.storage.k8s.io",
		"volumesnapshots.snapshot.storage.k8s.io",
		"awschaos.chaos-mesh.org",
		"azurechaos.chaos-mesh.org",
		"blockchaos.chaos-mesh.org",
		"dnschaos.chaos-mesh.org",
		"gcpchaos.chaos-mesh.org",
		"httpchaos.chaos-mesh.org",
		"iochaos.chaos-mesh.org",
		"jvmchaos.chaos-mesh.org",
		"kernelchaos.chaos-mesh.org",
		"networkchaos.chaos-mesh.org",
		"physicalmachinechaos.chaos-mesh.org",
		"physicalmachines.chaos-mesh.org",
		"podchaos.chaos-mesh.org",
		"podhttpchaos.chaos-mesh.org",
		"podiochaos.chaos-mesh.org",
		"podnetworkchaos.chaos-mesh.org",
		"schedules.chaos-mesh.org",
		"statuschecks.chaos-mesh.org",
		"stresschaos.chaos-mesh.org",
		"timechaos.chaos-mesh.org",
		"workflownodes.chaos-mesh.org",
		"workflows.chaos-mesh.org",
	}

	for _, crd := range ignoredCRDs {
		ignoredCRDMap[crd] = struct{}{}
	}
}

var ignoredCRDMap = map[string]struct{}{}

func isIgnored(name string) bool {
	_, ok := ignoredCRDMap[name]
	return ok
}

type transfer struct {
	from client.Client
	to   client.Client

	ns string
}

type TransferHandler interface {
	Transfer(ctx context.Context) error
}

func NewTransfer(ns string, from, to client.Client) TransferHandler {
	return &transfer{
		ns:   ns,
		from: from,
		to:   to,
	}
}

func (f *transfer) Transfer(ctx context.Context) error {
	ns := corev1.Namespace{}
	if err := f.from.Get(ctx, client.ObjectKey{
		Name: f.ns,
	}, &ns); err != nil {
		return err
	}

	if err := f.transfer(ctx, &ns, true); err != nil {
		return err
	}

	if err := f.from.Get(ctx, client.ObjectKey{
		Name: "tidb-admin",
	}, &ns); err != nil {
		return err
	}

	if err := f.transfer(ctx, &ns, true); err != nil {
		return err
	}

	crdList := apiextensionsv1.CustomResourceDefinitionList{}
	if err := f.from.List(ctx, &crdList); err != nil {
		return err
	}

	crds := []*apiextensionsv1.CustomResourceDefinition{}
	for i := range crdList.Items {
		crd := &crdList.Items[i]
		if isIgnored(crd.Name) {
			fmt.Println("ignore crd", crd.Name)
			continue
		}
		fmt.Println("transfer crd", crd.Name)
		if err := f.transfer(ctx, crd, true); err != nil {
			return err
		}
		crds = append(crds, crd)
	}

	for _, crd := range crds {
		if err := f.transferCustomResource(ctx, crd); err != nil {
			return err
		}
	}

	if err := f.transferResource(ctx, &schedulingv1.PriorityClassList{}, "", false); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &corev1.NodeList{}, "", true); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &corev1.SecretList{}, f.ns, false); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &corev1.SecretList{}, "tidb-admin", false); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &corev1.ConfigMapList{}, f.ns, false); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &rbacv1.RoleList{}, f.ns, false); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &rbacv1.RoleBindingList{}, f.ns, false); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &corev1.ServiceAccountList{}, f.ns, false); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &corev1.ServiceList{}, f.ns, true); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &corev1.ServiceList{}, "tidb-admin", true); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &corev1.PodList{}, f.ns, true); err != nil {
		return err
	}
	if err := f.transferResource(ctx, &storagev1.StorageClassList{}, "", false); err != nil {
		return err
	}

	return nil
}

func (f *transfer) transferResource(ctx context.Context, list client.ObjectList, ns string, hasStatus bool) error {
	opts := []client.ListOption{}
	if ns != "" {
		opts = append(opts, client.InNamespace(ns))
	}

	if err := f.from.List(ctx, list, opts...); err != nil {
		return err
	}

	objs, err := meta.ExtractList(list)
	if err != nil {
		return err
	}

	for _, obj := range objs {
		switch o := obj.(type) {
		case *corev1.Service:
			// reset to avoid validation error
			o.Spec.ClusterIP = ""
			o.Spec.ClusterIPs = nil
			if err := f.transfer(ctx, o, hasStatus); err != nil {
				return err
			}
		case *corev1.Pod:
			owners := o.GetOwnerReferences()
			// ignore backup job
			if len(owners) == 1 && owners[0].Kind == "Job" {
				continue
			}
			if err := f.transfer(ctx, o, hasStatus); err != nil {
				return err
			}

		case client.Object:
			if err := f.transfer(ctx, o, hasStatus); err != nil {
				return err
			}
		}
	}

	return nil
}

func (f *transfer) transferCustomResource(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition) error {
	gvk := schema.GroupVersionKind{
		Group: crd.Spec.Group,
		Kind:  crd.Spec.Names.ListKind,
	}

	if err := wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (done bool, err error) {
		if err := f.to.Get(ctx, client.ObjectKeyFromObject(crd), crd); err != nil {
			return false, err
		}
		for _, cond := range crd.Status.Conditions {
			if cond.Status != apiextensionsv1.ConditionTrue {
				return false, nil
			}
		}
		m := f.to.RESTMapper()
		if _, err := m.RESTMapping(gvk.GroupKind()); err != nil {
			return false, nil
		}

		return true, nil
	}); err != nil {
		return err
	}

	hasStatus := false
	for _, v := range crd.Spec.Versions {
		if v.Served && v.Storage {
			gvk.Version = v.Name
			if v.Subresources != nil && v.Subresources.Status != nil {
				hasStatus = true
			}
			break
		}
	}

	obj, err := scheme.Scheme.New(gvk)
	if err != nil {
		return err
	}

	list, ok := obj.(client.ObjectList)
	if !ok {
		return fmt.Errorf("not obj list: %T", obj)
	}

	isNamespaced := false
	switch crd.Spec.Scope {
	case apiextensionsv1.ClusterScoped:
	case apiextensionsv1.NamespaceScoped:
		isNamespaced = true
	}

	fmt.Println("transfer list", crd.Name, isNamespaced, hasStatus)
	return f.transferResource(ctx, list, f.ns, hasStatus)
}

func (f *transfer) transfer(ctx context.Context, obj client.Object, hasStatus bool) error {
	fmt.Println("transfer obj", reflect.TypeOf(obj), obj.GetName(), obj.GetNamespace(), hasStatus)
	if hasStatus {
		newObj := obj.DeepCopyObject().(client.Object)
		if err := f.transfer(ctx, newObj, false); err != nil {
			return err
		}

		if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
			if err := f.to.Get(ctx, client.ObjectKeyFromObject(newObj), newObj); err != nil {
				return fmt.Errorf("cannot get newest obj %v: %w", client.ObjectKeyFromObject(newObj), err)
			}

			obj.SetResourceVersion(newObj.GetResourceVersion())
			obj.SetUID(newObj.GetUID())

			// NOTE: status update may update the owner references
			// NOTE: it seems a bug
			obj.SetOwnerReferences(newObj.GetOwnerReferences())

			if err := f.to.Status().Update(ctx, obj); err != nil {
				return fmt.Errorf("cannot update obj status %v: %w", client.ObjectKeyFromObject(newObj), err)
			}

			return nil
		}); err != nil {
			return err
		}

		return nil
	}

	obj.SetResourceVersion("")
	owners := obj.GetOwnerReferences()
	if len(owners) != 0 {
		for i, ref := range owners {
			gv, err := schema.ParseGroupVersion(ref.APIVersion)
			if err != nil {
				return err
			}
			gvk := gv.WithKind(ref.Kind)
			ownerObj, err := scheme.Scheme.New(gvk)
			if err != nil {
				return err
			}
			owner := ownerObj.(client.Object)
			owner.SetNamespace(obj.GetNamespace())
			owner.SetName(ref.Name)
			if err := f.from.Get(ctx, client.ObjectKeyFromObject(owner), owner); err != nil {
				return err
			}
			if err := f.transfer(ctx, owner, false); err != nil {
				return err
			}
			fmt.Println("owner uid", ref.UID, owner.GetUID())
			owners[i].UID = owner.GetUID()
		}
		obj.SetOwnerReferences(owners)
	}
	if err := f.to.Create(ctx, obj); err != nil {
		if !errors.IsAlreadyExists(err) {
			return fmt.Errorf("cannot create obj %v, %#v: %w", client.ObjectKeyFromObject(obj), obj, err)
		}

		curObj := obj.DeepCopyObject().(client.Object)
		if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
			if err := f.to.Get(ctx, client.ObjectKeyFromObject(curObj), curObj); err != nil {
				return fmt.Errorf("cannot get newest obj %v: %w", client.ObjectKeyFromObject(curObj), err)
			}

			obj.SetResourceVersion(curObj.GetResourceVersion())
			obj.SetUID(curObj.GetUID())
			if err := f.to.Update(ctx, obj); err != nil {
				return fmt.Errorf("cannot update obj %v: %w", client.ObjectKeyFromObject(obj), err)
			}

			return nil
		}); err != nil {
			return err
		}
	}

	return nil
}
