package brguardian

import (
	"context"
	"fmt"
	"time"

	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
)

// Guardian protects a BR child resource and its parent Cluster from being
// removed before the child's destroy tasks have completed.
type Guardian struct {
	local            client.Client
	child            client.Object
	cluster          *cloudv1.Cluster
	childFinalizer   string
	clusterFinalizer string
}

func New(
	local client.Client,
	child client.Object,
	cluster *cloudv1.Cluster,
	childFinalizer string,
	clusterFinalizer string,
) *Guardian {
	return &Guardian{
		local:            local,
		child:            child,
		cluster:          cluster,
		childFinalizer:   childFinalizer,
		clusterFinalizer: clusterFinalizer,
	}
}

func (g *Guardian) Name() string {
	return "Guardian"
}

func (g *Guardian) Category() deploy.TaskCategory {
	return deploy.BRTask
}

func (g *Guardian) MetricsStartTime() *time.Time {
	return nil
}

func (g *Guardian) Create(ctx context.Context) deploy.Result {
	if g.cluster == nil {
		return deploy.Panic(fmt.Errorf("parent cluster is not found"))
	}
	if !g.cluster.DeletionTimestamp.IsZero() {
		return deploy.Panic(fmt.Errorf("cluster %s/%s is being deleted", g.cluster.Namespace, g.cluster.Name))
	}

	// Persist the child finalizer before protecting the Cluster. If adding the
	// Cluster finalizer loses a race with Cluster deletion, the child remains
	// protected and can use the cluster-gone escape path later.
	if !controllerutil.ContainsFinalizer(g.child, g.childFinalizer) {
		controllerutil.AddFinalizer(g.child, g.childFinalizer)
		if err := g.local.Update(ctx, g.child); err != nil {
			return deploy.Panic(fmt.Errorf("add child finalizer %q: %w", g.childFinalizer, err))
		}
	}

	if err := g.ensureClusterFinalizer(ctx); err != nil {
		return deploy.Panic(err)
	}
	return deploy.Done()
}

func (g *Guardian) PostCreate(context.Context) deploy.Result {
	return deploy.Done()
}

func (g *Guardian) Delete(ctx context.Context) deploy.Result {
	if !DestroyFinished(g.child) {
		return deploy.Done()
	}

	if err := g.removeClusterFinalizer(ctx); err != nil {
		return deploy.Panic(err)
	}
	controllerutil.RemoveFinalizer(g.child, g.childFinalizer)
	return deploy.Done()
}

func (g *Guardian) PostDelete(context.Context) deploy.Result {
	return deploy.Done()
}

func (g *Guardian) ensureClusterFinalizer(ctx context.Context) error {
	if !controllerutil.ContainsFinalizer(g.cluster, g.clusterFinalizer) {
		controllerutil.AddFinalizer(g.cluster, g.clusterFinalizer)
		if err := g.local.Update(ctx, g.cluster); err != nil {
			return fmt.Errorf("add cluster finalizer %q: %w", g.clusterFinalizer, err)
		}
	}
	return nil
}

func (g *Guardian) removeClusterFinalizer(ctx context.Context) error {
	if g.cluster == nil {
		return nil
	}

	if controllerutil.ContainsFinalizer(g.cluster, g.clusterFinalizer) {
		controllerutil.RemoveFinalizer(g.cluster, g.clusterFinalizer)
		if err := g.local.Update(ctx, g.cluster); err != nil {
			return fmt.Errorf("remove cluster finalizer %q: %w", g.clusterFinalizer, err)
		}
	}
	return nil
}

func DestroyFinished(child client.Object) bool {
	_, ok := child.GetAnnotations()[consts.DestroyFinishedAtAnnotation]
	return ok
}

// MarkDestroyFinished adds the destroy checkpoint once and reports whether the
// object changed.
func MarkDestroyFinished(child client.Object, now time.Time) bool {
	if DestroyFinished(child) {
		return false
	}
	annotations := child.GetAnnotations()
	if annotations == nil {
		annotations = make(map[string]string)
	}
	annotations[consts.DestroyFinishedAtAnnotation] = now.UTC().Format(time.RFC3339)
	child.SetAnnotations(annotations)
	return true
}

// CheckpointDestroy records a successful full destroy pass. Errors and
// retriable errors intentionally leave the child without a checkpoint.
func CheckpointDestroy(child client.Object, results []deploy.Result, now time.Time) (ctrl.Result, error, error) {
	res, errs, retriableErrs := deploy.MinRequeue(results)
	if errs == nil && retriableErrs == nil && MarkDestroyFinished(child, now) {
		res = ctrl.Result{Requeue: true}
	}
	return res, errs, retriableErrs
}
