package task

import (
	"context"
	"maps"
	"time"

	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	crossplaneutil "github.com/tidbcloud/infra-provider/pkg/util/crossplane"
	route53v1beta1 "github.com/upbound/provider-aws/apis/route53/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const route53PauseRequeueAfter = 10 * time.Second

// reconcileRoute53DNS applies the Zone and Records until all of them are
// stable, then pauses them as a group. Paused objects are terminal: desired
// changes are ignored until rollback or deletion removes the pause annotation.
func reconcileRoute53DNS(ctx context.Context, cli client.Client, objs []client.Object) deploy.Result {
	stable := true
	toPause := make([]client.Object, 0, len(objs))

	for _, desired := range objs {
		current, err := copyDNSObject(desired)
		if err != nil {
			return deploy.Panic(err)
		}
		if err := cli.Get(ctx, client.ObjectKeyFromObject(desired), current); err != nil {
			if apierrors.IsNotFound(err) {
				current = nil
			} else {
				getErr := errors.Wrapf(err, "get Route53 object %T %s", desired, desired.GetName())
				return deploy.Error(getErr).WithRequeuAfter(time.Second * 5)
			}
		}

		if current != nil && k8s.IsCrossplaneCRPaused(current) {
			continue
		}

		toPause = append(toPause, desired)

		// Apply loads the live object into its argument. Keep the desired objects
		// clean so the later pause Apply does not own provider-managed fields.
		toApply, err := copyDNSObject(desired)
		if err != nil {
			return deploy.Panic(err)
		}
		result, err := k8s.ApplyWithResult(ctx, cli, toApply)
		if err != nil {
			return deploy.Panic(err).WithRequeuAfter(time.Second * 5)
		}
		if result != k8s.ApplyResultUnchanged {
			stable = false
			continue
		}
		if !route53ObjectReady(current) {
			stable = false
		}
	}

	if !stable {
		return deploy.Done().WithRequeuAfter(route53PauseRequeueAfter)
	}
	if err := pauseRoute53DNSObjects(ctx, cli, toPause); err != nil {
		return deploy.Error(err).WithRequeuAfter(time.Second * 5)
	}
	return deploy.Done()
}

func route53ObjectReady(obj client.Object) bool {
	switch current := obj.(type) {
	case *route53v1beta1.Zone:
		return meta.GetExternalName(current) != "" &&
			crossplaneutil.IsReadyAndSynced(current.Status.Conditions)
	case *route53v1beta1.Record:
		return crossplaneutil.IsReadyAndSynced(current.Status.Conditions)
	default:
		return false
	}
}

func copyDNSObject(obj client.Object) (client.Object, error) {
	if obj == nil {
		return nil, errors.New("cannot copy nil Route53 object")
	}
	copied, ok := obj.DeepCopyObject().(client.Object)
	if !ok {
		return nil, errors.Errorf("deep copy of Route53 object %T does not implement client.Object", obj)
	}
	return copied, nil
}

func hasRoute53Zone(objs []client.Object) bool {
	for _, obj := range objs {
		if _, ok := obj.(*route53v1beta1.Zone); ok {
			return true
		}
	}
	return false
}

func pauseRoute53DNSObjects(ctx context.Context, cli client.Client, objs []client.Object) error {
	for _, obj := range objs {
		paused, err := copyDNSObject(obj)
		if err != nil {
			return err
		}
		annotations := maps.Clone(paused.GetAnnotations())
		if annotations == nil {
			annotations = map[string]string{}
		}
		annotations[meta.AnnotationKeyReconciliationPaused] = "true"
		paused.SetAnnotations(annotations)
		if err := k8s.Apply(ctx, cli, paused); err != nil {
			return errors.Wrapf(err, "pause Route53 object %T %s", obj, obj.GetName())
		}
	}
	return nil
}
