package agentbuild

import (
	"context"
	"fmt"

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	eksv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	gkecontainerv1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/observability/agent-control-server/pkg/util"
)

// CheckDaemonSetReady checks if a DaemonSet resource is ready.
//
// A DaemonSet is considered ready when the number of ready pods equals the desired number of scheduled pods.
func CheckDaemonSetReady(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj appsv1.DaemonSet
	if err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj); err != nil {
		return fmt.Errorf("failed to get daemonset %s: %w", name, err)
	}
	if obj.Status.NumberReady != obj.Status.DesiredNumberScheduled {
		return fmt.Errorf("daemonset %s is not ready", name)
	}
	return nil
}

// CheckDaemonSetDeleted checks if a DaemonSet resource is deleted.
//
// It returns nil if the DaemonSet is not found or if the API server does not recognize the DaemonSet resource.
func CheckDaemonSetDeleted(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj appsv1.DaemonSet
	err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj)
	if err == nil {
		return fmt.Errorf("daemonset %s/%s is still present", namespace, name)
	}
	if meta.IsNoMatchError(err) {
		return nil
	}
	if client.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to get daemonset %s: %w", name, err)
	}
	return nil
}

// CheckDeploymentReady checks if a Deployment resource is ready.
//
// A Deployment is considered ready when it has an Available condition with status True.
func CheckDeploymentReady(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj appsv1.Deployment
	if err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj); err != nil {
		return fmt.Errorf("failed to get deployment %s: %w", name, err)
	}
	if !util.IsStatusConditionTrue(obj.Status.Conditions, appsv1.DeploymentAvailable) {
		return fmt.Errorf("deployment %s is not ready", name)
	}
	return nil
}

// CheckStatefulSetReady checks if a StatefulSet resource is ready.
//
// A StatefulSet is considered ready when the number of available replicas equals the desired number of replicas.
func CheckStatefulSetReady(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj appsv1.StatefulSet
	if err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj); err != nil {
		return fmt.Errorf("failed to get statefulset %s/%s: %w", namespace, name, err)
	}
	if obj.Spec.Replicas == nil || *obj.Spec.Replicas != obj.Status.AvailableReplicas {
		return fmt.Errorf("statefulset %s/%s is not ready", namespace, name)
	}
	return nil
}

// CheckStatefulSetReadyWithLabels checks if StatefulSet resources matching the selector are ready.
//
// Each StatefulSet is considered ready when the number of available replicas equals the desired number of replicas.
func CheckStatefulSetReadyWithLabels(ctx context.Context, cli client.Client, namespace string, labels map[string]string) error {
	var objs appsv1.StatefulSetList
	if err := cli.List(ctx, &objs, client.InNamespace(namespace), client.MatchingLabels(labels)); err != nil {
		return fmt.Errorf("failed to list statefulset %s/(%v): %w", namespace, labels, err)
	}

	for _, app := range objs.Items {
		if app.Spec.Replicas == nil || *app.Spec.Replicas != app.Status.AvailableReplicas {
			return fmt.Errorf("statefulset %s/%s is not ready", app.Namespace, app.Name)
		}
	}
	return nil
}

// CheckStatefulSetDeleted checks if a StatefulSet resource is deleted.
//
// It returns nil if the StatefulSet is not found or if the API server does not recognize the StatefulSet resource.
func CheckStatefulSetDeleted(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj appsv1.StatefulSet
	err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj)
	if err == nil {
		return fmt.Errorf("statefulset %s/%s is still present", namespace, name)
	}
	if client.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to get statefulset %s: %w", name, err)
	}
	return nil
}

// CheckStatefulSetDeletedWithLabels checks if StatefulSet resources matching the selector are deleted.
//
// It returns nil if no matching StatefulSet is found or if the API server does not recognize the StatefulSet resource.
func CheckStatefulSetDeletedWithLabels(ctx context.Context, cli client.Client, namespace string, labels map[string]string) error {
	var objs appsv1.StatefulSetList
	if err := cli.List(ctx, &objs, client.InNamespace(namespace), client.MatchingLabels(labels)); err != nil {
		return fmt.Errorf("failed to list statefulset %s/(%v): %w", namespace, labels, err)
	}

	if len(objs.Items) > 0 {
		return fmt.Errorf("statefulset %s/(%s) is still present", namespace, labels)
	}
	return nil
}

// CheckVMPodScrapeReady checks if a VMPodScrape resource is ready.
//
// A VMPodScrape is considered ready when its UpdateStatus is Operational.
func CheckVMPodScrapeReady(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj vmoperatorv1beta1.VMPodScrape
	if err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj); err != nil {
		return fmt.Errorf("failed to get vmpodscrape %s: %w", name, err)
	}
	if obj.Status.UpdateStatus != vmoperatorv1beta1.UpdateStatusOperational {
		return fmt.Errorf("vmpodscrape %s status is not operational", name)
	}
	return nil
}

// CheckVMPodScrapeDeleted checks if a VMPodScrape resource is deleted.
//
// It returns nil if the VMPodScrape is not found or if the API server does not recognize the VMPodScrape resource.
func CheckVMPodScrapeDeleted(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj vmoperatorv1beta1.VMPodScrape
	err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj)
	if err == nil {
		return fmt.Errorf("vmpodscrape %s/%s is still present", namespace, name)
	}
	if meta.IsNoMatchError(err) {
		return nil
	}
	if client.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to get vmpodscrape %s: %w", name, err)
	}
	return nil
}

// CheckVMServiceScrapeReady checks if a VMServiceScrape resource is ready.
//
// A VMServiceScrape is considered ready when its UpdateStatus is Operational.
func CheckVMServiceScrapeReady(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj vmoperatorv1beta1.VMServiceScrape
	if err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj); err != nil {
		return fmt.Errorf("failed to get vmservicescrape %s: %w", name, err)
	}
	if obj.Status.UpdateStatus != vmoperatorv1beta1.UpdateStatusOperational {
		return fmt.Errorf("vmservicescrape %s status is not operational", name)
	}
	return nil
}

// CheckVMServiceScrapeDeleted checks if a VMServiceScrape resource is deleted.
//
// It returns nil if the VMServiceScrape is not found or if the API server does not recognize the VMServiceScrape resource.
func CheckVMServiceScrapeDeleted(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj vmoperatorv1beta1.VMServiceScrape
	err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj)
	if err == nil {
		return fmt.Errorf("vmservicescrape %s/%s is still present", namespace, name)
	}
	if meta.IsNoMatchError(err) {
		return nil
	}
	if client.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to get vmservicescrape %s: %w", name, err)
	}
	return nil
}

// CheckNodeGroupReady checks if a NodeGroup resource is ready.
//
// A NodeGroup is considered ready when its status is Active.
func CheckNodeGroupReady(ctx context.Context, cli client.Client, name string) error {
	var obj eksv1alpha1.NodeGroup
	if err := cli.Get(ctx, types.NamespacedName{Name: name}, &obj); err != nil {
		return fmt.Errorf("failed to get nodegroup %s: %w", name, err)
	}
	if obj.Status.AtProvider.Status != eksv1alpha1.NodeGroupStatusActive {
		return fmt.Errorf("nodegroup %s is not ready", name)
	}
	return nil
}

// CheckNodePoolReady checks if a NodePool resource is ready.
//
// A NodePool is considered ready when its status is Running.
func CheckNodePoolReady(ctx context.Context, cli client.Client, name string) error {
	var obj gkecontainerv1beta1.NodePool
	if err := cli.Get(ctx, types.NamespacedName{Name: name}, &obj); err != nil {
		return fmt.Errorf("failed to get nodepool %s: %w", name, err)
	}
	if obj.Status.AtProvider.Status != gkecontainerv1beta1.NodePoolStateRunning {
		return fmt.Errorf("nodepool %s is not ready", name)
	}
	return nil
}

// CheckNodeGroupDeleted checks if a NodeGroup resource is deleted.
//
// It returns nil if the NodeGroup is not found or if the API server does not recognize the NodeGroup resource.
func CheckNodeGroupDeleted(ctx context.Context, cli client.Client, name string) error {
	var obj eksv1alpha1.NodeGroup
	err := cli.Get(ctx, types.NamespacedName{Name: name}, &obj)
	if err == nil {
		return fmt.Errorf("nodegroup %s is still present", name)
	}
	if meta.IsNoMatchError(err) {
		return nil
	}
	if client.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to get nodegroup %s: %w", name, err)
	}
	return nil
}

// CheckNodePoolDeleted checks if a NodePool resource is deleted.
//
// It returns nil if the NodePool is not found or if the API server does not recognize the NodePool resource.
func CheckNodePoolDeleted(ctx context.Context, cli client.Client, name string) error {
	var obj gkecontainerv1beta1.NodePool
	err := cli.Get(ctx, types.NamespacedName{Name: name}, &obj)
	if err == nil {
		return fmt.Errorf("nodepool %s is still present", name)
	}
	if meta.IsNoMatchError(err) {
		return nil
	}
	if client.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to get nodepool %s: %w", name, err)
	}
	return nil
}

// CheckAwsVPCEndpointDeleted checks if a VPCEndpoint resource is deleted.
//
// It returns nil if the VPCEndpoint is not found or if the API server does not recognize the VPCEndpoint resource.
func CheckAwsVPCEndpointDeleted(ctx context.Context, cli client.Client, name string) error {
	var obj ec2v1alpha1.VPCEndpoint
	err := cli.Get(ctx, types.NamespacedName{Name: name}, &obj)
	if err == nil {
		return fmt.Errorf("vpcendpoint %s is still present", name)
	}
	if meta.IsNoMatchError(err) {
		return nil
	}
	if client.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to get vpcendpoint %s: %w", name, err)
	}
	return nil
}

// CheckNodeGroupDeletedWithLabels checks if any NodeGroup resources with the specified labels are still present.
//
// It returns nil if no matching NodeGroup is found or if the API server does not recognize the NodeGroup resource.
func CheckNodeGroupDeletedWithLabels(ctx context.Context, cli client.Client, labels map[string]string) error {
	var objs eksv1alpha1.NodeGroupList
	if err := cli.List(ctx, &objs, client.MatchingLabels(labels)); err != nil {
		return err
	}
	if len(objs.Items) > 0 {
		return fmt.Errorf("nodegroups with labels %v are still present", labels)
	}
	return nil
}

// CheckNodePoolDeletedWithLabels checks if any NodePool resources with the specified labels are still present.
//
// It returns nil if no matching NodePool is found or if the API server does not recognize the NodePool resource.
func CheckNodePoolDeletedWithLabels(ctx context.Context, cli client.Client, labels map[string]string) error {
	var objs gkecontainerv1beta1.NodePoolList
	if err := cli.List(ctx, &objs, client.MatchingLabels(labels)); err != nil {
		return err
	}
	if len(objs.Items) > 0 {
		return fmt.Errorf("nodepools with labels %v are still present", labels)
	}
	return nil
}

// CheckVMAgentsDeleted checks if any VMAgent resources are still present.
//
// It returns nil if no VMAgent is found or if the API server does not recognize the VMAgent resource.
func CheckVMAgentsDeleted(ctx context.Context, cli client.Client, name, namespace string) error {
	var obj vmoperatorv1beta1.VMAgent
	err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &obj)
	if err == nil {
		return fmt.Errorf("vmagent %s is still present", name)
	}
	if meta.IsNoMatchError(err) {
		return nil
	}
	if client.IgnoreNotFound(err) != nil {
		return fmt.Errorf("failed to get vmagent %s: %w", name, err)
	}
	return nil
}

// CheckNamespaceExists checks if a namespace exists.
//
// It returns nil if the namespace exists, or an error if it does not exist or if there was an error checking.
func CheckNamespaceExists(ctx context.Context, cli client.Client, namespace string) error {
	obj := &corev1.Namespace{}
	obj.Name = namespace
	return cli.Get(ctx, client.ObjectKeyFromObject(obj), obj)
}

// CheckAndUpdateServiceAccountAnnotations removes a specific annotation from a ServiceAccount if it exists.
//
// Returns (true, nil) if the annotation was removed, (false, nil) if nothing was changed,
// or (false, error) if an error occurred.
func CheckAndUpdateServiceAccountAnnotations(ctx context.Context, cli client.Client, name, namespace, annotationKey string) (bool, error) {
	sa := &corev1.ServiceAccount{}
	if err := cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, sa); err != nil {
		// If not found, nothing to do
		if client.IgnoreNotFound(err) != nil {
			return false, fmt.Errorf("failed to get serviceaccount %s/%s: %w", namespace, name, err)
		}
		return false, nil
	}

	// Check if the annotation exists
	if sa.Annotations == nil {
		return false, nil
	}
	if _, exists := sa.Annotations[annotationKey]; !exists {
		return false, nil
	}

	// Remove the annotation
	delete(sa.Annotations, annotationKey)
	if err := cli.Update(ctx, sa); err != nil {
		return false, fmt.Errorf("failed to update serviceaccount %s/%s: %w", namespace, name, err)
	}
	return true, nil
}

// CheckAndUpdatePVCLabels checks if a PVC needs label updates and applies them if necessary.
//
// Returns true if labels were updated, false otherwise.
func CheckAndUpdatePVCLabels(ctx context.Context, cli client.Client, pvcName, namespace string, desiredLabels map[string]string) (bool, error) {
	pvc := &corev1.PersistentVolumeClaim{}
	if err := cli.Get(ctx, types.NamespacedName{Name: pvcName, Namespace: namespace}, pvc); err != nil {
		return false, fmt.Errorf("failed to get pvc %s: %w", pvcName, err)
	}

	// Check if labels need to be updated
	needsUpdate := false
	if pvc.Labels == nil {
		pvc.Labels = make(map[string]string)
		needsUpdate = true
	}
	for key, value := range desiredLabels {
		if existingValue, exists := pvc.Labels[key]; !exists || existingValue != value {
			pvc.Labels[key] = value
			needsUpdate = true
		}
	}

	if needsUpdate {
		if err := cli.Update(ctx, pvc); err != nil {
			return false, fmt.Errorf("failed to update pvc %s labels: %w", pvcName, err)
		}
		return true, nil
	}
	return false, nil
}
