package compactbackup

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/record"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	"github.com/tidbcloud/eks-provider/controllers"
	"github.com/tidbcloud/eks-provider/controllers/compactbackup/task"
	"github.com/tidbcloud/eks-provider/controllers/utils/brguardian"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	metricsctrl "github.com/tidbcloud/infra-provider/pkg/metrics/controller"
	"github.com/tidbcloud/infra-provider/pkg/util"
	providerconfigUtil "github.com/tidbcloud/infra-provider/pkg/util/providerconfig"
)

// deletionRetentionPeriod defines how long to retain a CompactBackup CR after its deletion timestamp before final cleanup.
const deletionRetentionPeriod = 2 * 24 * time.Hour

//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=compactbackups,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=compactbackups/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=compactbackups/finalizers,verbs=update

// Reconciler is the compactbackup reconciler
type Reconciler struct {
	Logger             logr.Logger
	EventRecorder      record.EventRecorder
	Client             client.Client
	Cache              client.Reader
	CachedRemoteClient dataplane.EnvProviderManager
	Config             config.BRConfig
	RootConfig         config.RootConfig
}

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	start := time.Now()
	defer func() {
		metricsctrl.ReconcileDuration.WithLabelValues("compact_backup").Observe(time.Since(start).Seconds())
	}()

	logger := log.FromContext(ctx).WithValues("compactbackup", req.NamespacedName)

	compactBackup := &cloudv1.CompactBackup{}
	if err := r.Cache.Get(ctx, req.NamespacedName, compactBackup); err != nil {
		if apierrors.IsNotFound(err) {
			return ctrl.Result{}, nil
		}
		logger.Error(err, "unable to get compactbackup")
		return ctrl.Result{}, fmt.Errorf("unable to get compactbackup %s: %v", req.NamespacedName, err)
	}

	if ptr.Deref(compactBackup.Spec.Paused, false) {
		logger.Info("Skipping paused compactbackup", "compactbackup", req.NamespacedName)
		return ctrl.Result{}, nil
	}

	logger = logger.WithValues(
		"cluster", compactBackup.Spec.ClusterName,
		"phase", compactBackup.Status.Phase,
	)

	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      compactBackup.Spec.ClusterName,
			Namespace: compactBackup.Namespace,
		},
	}
	if err := r.Cache.Get(ctx, client.ObjectKeyFromObject(cluster), cluster); err != nil {
		if !apierrors.IsNotFound(err) {
			logger.Error(err, "unable to get cluster")
			return ctrl.Result{}, fmt.Errorf("unable to get cluster %s/%s: %v", cluster.Namespace, cluster.Name, err)
		}
		logger.Info("cluster not found, marking compactbackup as failed or removing finalizer", "cluster", cluster.Name)
		return r.handleClusterGone(ctx, compactBackup, logger)
	}

	remote, err := r.CachedRemoteClient.GetClient(ctx, cluster)
	if err != nil {
		logger.Error(err, "failed to get dataplane k8s client")
		return ctrl.Result{}, fmt.Errorf("failed to get dataplane k8s client: %v", err)
	}

	compactBackupStorageURL := ptr.Deref(compactBackup.Spec.Storage, "")
	if compactBackupStorageURL == "" {
		return ctrl.Result{}, fmt.Errorf("compactbackup storage is empty")
	}

	var cloud task.CloudProvider
	switch providerType := cluster.GetProviderType(); providerType {
	case consts.ProviderTypeEKS:
		region := compactBackup.GetRegion()
		assumeRoleARN := util.GetRoleARN(cluster.Spec.Kubernetes.Account, r.RootConfig.FullManage.AssumeRoleName)
		externalID := r.RootConfig.FullManage.AssumeRoleExternalID
		cloud, err = task.NewAWSProvider(compactBackupStorageURL, region, cluster.Spec.Kubernetes.Name, cluster.Spec.Kubernetes.Version, cluster.Status.NodeIdentity.AWS.RoleARN, cluster.Spec.Kubernetes.Account, assumeRoleARN, externalID, providerconfigUtil.GetClusterProviderConfigNameFromCluster(cluster), cluster.GetTenantID(), cluster.GetProjectID(), cluster.Name, *cluster.Spec.Kubernetes.Cloud.AWS, &r.RootConfig)
		if err != nil {
			return ctrl.Result{}, fmt.Errorf("failed to build AWS provider: %v", err)
		}
	case consts.ProviderTypeGKE:
		region := compactBackup.GetRegion()
		projectID := cluster.Spec.Kubernetes.Account
		nodeRole := cluster.Status.NodeIdentity.GCP.ServiceAccount
		cloud, err = task.NewGCPProvider(compactBackupStorageURL, region, projectID, cluster.Spec.Kubernetes.Name,
			cluster.Spec.Kubernetes.Version, nodeRole, providerconfigUtil.GetClusterProviderConfigNameFromCluster(cluster), cluster.GetTenantID(), cluster.GetProjectID())
		if err != nil {
			return ctrl.Result{}, fmt.Errorf("failed to build GCP provider: %v", err)
		}
	case consts.ProviderTypeAKS:
		tenantID := r.RootConfig.ClusterController.Azure.CrossplaneAzureProviderTenantID
		subscriptionID := r.RootConfig.ClusterController.Azure.CrossplaneAzureProviderSubscriptionID
		clientID := r.RootConfig.ClusterController.Azure.CrossplaneAzureProviderClientID

		azure := cluster.Spec.Kubernetes.Cloud.Azure
		if azure == nil {
			return ctrl.Result{}, fmt.Errorf("no aks info found in cluster %s/%s", cluster.Namespace, cluster.Name)
		}
		cloud, err = task.NewAzureProvider(tenantID, subscriptionID, clientID,
			cluster.Spec.Kubernetes.Account, cluster.Spec.Kubernetes.Name,
			cluster.Spec.Kubernetes.Version, *azure, compactBackupStorageURL, providerconfigUtil.GetClusterProviderConfigNameFromCluster(cluster), cluster.GetTenantID(), cluster.GetProjectID())
		if err != nil {
			return ctrl.Result{}, fmt.Errorf("failed to build Azure provider: %v", err)
		}
	default:
		return ctrl.Result{}, fmt.Errorf("unsupported provider type: %s", providerType)
	}

	// Create task context for CompactBackup
	c := task.NewContext(r.EventRecorder, logger, r.Client, remote, compactBackup, cluster, r.RootConfig.BR.Env, cloud, &r.RootConfig)

	// Build task chain: Guardian -> NodePool -> Compact
	tasks := []deploy.Task{
		task.NewGuardian(c),
		task.NewNodePool(c),
		task.NewCompact(c),
	}

	deployer := deploy.NewDeployer(tasks, nil, logger)
	deleted := !compactBackup.DeletionTimestamp.IsZero()

	var res ctrl.Result
	var errs error
	var retriableErrs error
	if deleted {
		elapsed := time.Since(compactBackup.DeletionTimestamp.Time)
		// skip deletion retention if the cluster is being deleted to avoid blocking cluster deletion
		if cluster.DeletionTimestamp.IsZero() && elapsed < deletionRetentionPeriod {
			remaining := deletionRetentionPeriod - elapsed
			logger.Info("compactbackup is within retention period, deferring cleanup",
				"deletionTimestamp", compactBackup.DeletionTimestamp,
				"elapsed", elapsed.Round(time.Second),
				"remaining", remaining.Round(time.Second),
			)
			return ctrl.Result{RequeueAfter: remaining}, nil
		}

		results := deployer.Destroy(ctx)
		res, errs, retriableErrs = brguardian.CheckpointDestroy(compactBackup, results, time.Now())
		if errs == nil && retriableErrs == nil {
			if err := r.Client.Update(ctx, compactBackup); err != nil {
				logger.Error(err, "failed to record destroy completion")
				return ctrl.Result{}, fmt.Errorf("failed to record destroy completion: %v", err)
			}
		} else {
			logger.Error(errs, "destroy failed", "retriableErrs", retriableErrs)
		}
	} else {
		results := deployer.Deploy(ctx)
		res, errs, retriableErrs = deploy.MinRequeue(results)
		if errs != nil {
			logger.Error(errs, "deploy failed", "retriableErrs", retriableErrs)
		}
	}

	return r.updateStatusAndEvents(ctx, compactBackup, res, errs)
}

// updateStatusAndEvents updates the CompactBackup status and records events
func (r *Reconciler) updateStatusAndEvents(ctx context.Context, compactBackup *cloudv1.CompactBackup, res ctrl.Result, errs error) (ctrl.Result, error) {
	// Record events based on current status (set by tasks) and errors
	if errs != nil {
		// Only record the reconcile error as an event, don't change the CR status
		// The actual CompactBackup status should be determined by the CompactTask based on real dataplane state
		r.EventRecorder.Event(compactBackup, corev1.EventTypeWarning, "ReconcileError", errs.Error())
	} else {
		// Record events based on the current phase (already set by tasks)
		switch compactBackup.Status.Phase {
		case cloudv1.CompactPhaseComplete:
			r.EventRecorder.Event(compactBackup, corev1.EventTypeNormal, "Completed", "CompactBackup completed successfully")
		case cloudv1.CompactPhaseFailed:
			r.EventRecorder.Event(compactBackup, corev1.EventTypeWarning, "Failed", compactBackup.Status.Message)
		case cloudv1.CompactPhaseRetry:
			r.EventRecorder.Event(compactBackup, corev1.EventTypeWarning, "Retrying", compactBackup.Status.Message)
			// No event needed for Running/Pending phases to avoid noise
		}
	}

	// Set start time if not already set and the task has started
	if compactBackup.Status.StartTime == nil && compactBackup.Status.Phase != cloudv1.CompactPhasePending {
		now := metav1.Now()
		compactBackup.Status.StartTime = &now
	}

	// Only save the status changes made by tasks to the API server
	if err := r.Client.Status().Update(ctx, compactBackup); err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to update compactbackup status: %v", err)
	}

	return res, errs
}

// handleClusterGone is called when the referenced cluster no longer exists (NotFound).
// If the CompactBackup itself is being deleted, removes the finalizer directly (dataplane resources are gone with the cluster).
// Otherwise marks CompactBackup as Failed unless it is already in a terminal state (Complete/Failed).
func (r *Reconciler) handleClusterGone(ctx context.Context, compactBackup *cloudv1.CompactBackup, logger logr.Logger) (ctrl.Result, error) {
	if !compactBackup.DeletionTimestamp.IsZero() {
		controllerutil.RemoveFinalizer(compactBackup, task.Finalizer)
		if err := r.Client.Update(ctx, compactBackup); err != nil {
			logger.Error(err, "failed to remove finalizer after cluster gone")
			return ctrl.Result{}, fmt.Errorf("failed to remove finalizer: %v", err)
		}
		return ctrl.Result{}, nil
	}

	if compactBackup.Status.Phase != cloudv1.CompactPhaseComplete && compactBackup.Status.Phase != cloudv1.CompactPhaseFailed {
		compactBackup.Status.Phase = cloudv1.CompactPhaseFailed
		compactBackup.Status.Message = fmt.Sprintf("cluster %s has been deleted or is being deleted", compactBackup.Spec.ClusterName)
		if err := r.Client.Status().Update(ctx, compactBackup); err != nil {
			logger.Error(err, "failed to update compactbackup status after cluster gone")
			return ctrl.Result{}, fmt.Errorf("failed to update compactbackup status: %v", err)
		}
	}
	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, pds ...predicate.Predicate) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&cloudv1.CompactBackup{}, builder.WithPredicates(pds...)).
		WithOptions(controller.Options{
			RateLimiter:             k8s.RetryLimiter,
			MaxConcurrentReconciles: controllers.GetMaxConcurrentReconciles(r.RootConfig, "compactbackup"),
		}).
		Complete(r)
}
