// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0

package pod

import (
	"context"
	"fmt"

	"github.com/go-logr/logr"
	"github.com/pkg/errors"
	corev1 "k8s.io/api/core/v1"
	apierr "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	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/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	controllerutils "github.com/projectcapsule/capsule/internal/controllers/utils"
	caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
	"github.com/projectcapsule/capsule/pkg/runtime/predicates"
	"github.com/projectcapsule/capsule/pkg/tenant"
	"github.com/projectcapsule/capsule/pkg/utils"
)

type MetadataReconciler struct {
	Client client.Client
	Log    logr.Logger
}

func (m *MetadataReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlConfig controllerutils.ControllerOptions) error {
	return ctrl.NewControllerManagedBy(mgr).
		Named("capsule/pod").
		For(&corev1.Pod{}, m.forOptionPerInstanceName(ctx)).
		Watches(
			&capsulev1beta2.Tenant{},
			handler.EnqueueRequestsFromMapFunc(m.podsForTenant),
			builder.WithPredicates(predicates.TenantPodOptionsChangedPredicate{}),
		).
		WithOptions(ctrlConfig.Runtime.ToControllerOptions()).
		Complete(m)
}

//nolint:staticcheck
func (m *MetadataReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
	var pod corev1.Pod

	logger := log.FromContext(ctx)

	tenant, err := m.getTenant(ctx, request.NamespacedName, m.Client)
	if err != nil {
		noTenantObjError := &caperrors.NonTenantObjectError{}

		noPodMetaError := &caperrors.NoPodMetadataError{}
		if errors.As(err, &noTenantObjError) || errors.As(err, &noPodMetaError) {
			return reconcile.Result{}, nil
		}

		logger.Error(err, fmt.Sprintf("Cannot get tenant corev1.Pod %s/%s", request.Namespace, request.Name))

		return reconcile.Result{}, err
	}

	err = m.Client.Get(ctx, request.NamespacedName, &pod)
	if err != nil {
		if apierr.IsNotFound(err) {
			return reconcile.Result{}, nil
		}

		return reconcile.Result{}, err
	}

	_, err = controllerutil.CreateOrUpdate(ctx, m.Client, &pod, func() (err error) {
		pod.SetLabels(m.sync(pod.GetLabels(), tenant.Spec.PodOptions.AdditionalMetadata.Labels))
		pod.SetAnnotations(m.sync(pod.GetAnnotations(), tenant.Spec.PodOptions.AdditionalMetadata.Annotations))

		return nil
	})

	return reconcile.Result{}, err
}

func (m *MetadataReconciler) podsForTenant(ctx context.Context, obj client.Object) []reconcile.Request {
	tnt, ok := obj.(*capsulev1beta2.Tenant)
	if !ok {
		return nil
	}

	requests := make([]reconcile.Request, 0)

	for _, namespace := range tnt.Status.Namespaces {
		pods := &corev1.PodList{}
		if err := m.Client.List(ctx, pods, client.InNamespace(namespace)); err != nil {
			m.Log.Error(err, "failed listing Pods for Tenant metadata update", "tenant", tnt.Name, "namespace", namespace)

			continue
		}

		for i := range pods.Items {
			requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&pods.Items[i])})
		}
	}

	return requests
}

//nolint:staticcheck
func (m *MetadataReconciler) getTenant(ctx context.Context, namespacedName types.NamespacedName, client client.Client) (*capsulev1beta2.Tenant, error) {
	ns := &corev1.Namespace{}
	tenant := &capsulev1beta2.Tenant{}

	if err := client.Get(ctx, types.NamespacedName{Name: namespacedName.Namespace}, ns); err != nil {
		return nil, err
	}

	capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
	if _, ok := ns.GetLabels()[capsuleLabel]; !ok {
		return nil, caperrors.NewNonTenantObject(namespacedName.Name)
	}

	if err := client.Get(ctx, types.NamespacedName{Name: ns.Labels[capsuleLabel]}, tenant); err != nil {
		return nil, err
	}

	if tenant.Spec.PodOptions == nil || tenant.Spec.PodOptions.AdditionalMetadata == nil {
		return nil, caperrors.NewNoPodMetadata(namespacedName.Name)
	}

	return tenant, nil
}

func (m *MetadataReconciler) sync(available map[string]string, tenantSpec map[string]string) map[string]string {
	if tenantSpec != nil {
		if available == nil {
			return tenantSpec
		}

		for key, value := range tenantSpec {
			if available[key] != value {
				available[key] = value
			}
		}
	}

	return available
}

func (m *MetadataReconciler) forOptionPerInstanceName(ctx context.Context) builder.ForOption {
	return builder.WithPredicates(predicate.And(
		predicates.ObjectMetadataChangedPredicate{},
		predicate.NewPredicateFuncs(func(object client.Object) bool {
			status, err := tenant.IsNamespaceInTenant(ctx, m.Client, object.GetNamespace())
			if err != nil {
				m.Log.Error(err, "failed resolving instances")
			}

			return status
		}),
	))
}
