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

package template

import (
	"context"
	"fmt"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	k8smeta "k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/projectcapsule/capsule/pkg/api/runtime"
	"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)

// Reference
// +kubebuilder:object:generate=true
type ResourceReference struct {
	runtime.VersionKind `json:",inline"`

	// Name of the values referent. This is useful
	// when you traying to get a specific resource
	// +kubebuilder:validation:MinLength=1
	// +kubebuilder:validation:MaxLength=253
	// +optional
	Name string `json:"name,omitempty"`
	// Namespace of the values referent.
	// +optional
	Namespace string `json:"namespace,omitempty"`
	// Selector which allows to get any amount of these resources based on labels
	// +optional
	Selector *metav1.LabelSelector `json:"selector,omitempty"`
	// Only relevant if name is set. If an item is not optional, there will be an error thrown when it does not exist
	// +kubebuilder:default:=true
	Optional bool `json:"optional,omitempty"`
}

func (t ResourceReference) RequiresTemplating() bool {
	if RequiresFastTemplate(t.Name) {
		return true
	}

	if RequiresFastTemplate(t.Namespace) {
		return true
	}

	if SelectorRequiresTemplating(t.Selector) {
		return true
	}

	return false
}

func (t ResourceReference) LoadTemplated(templateContext map[string]string) (ResourceReference, error) {
	if !t.RequiresTemplating() || templateContext == nil {
		return t, nil
	}

	out := t

	// Name + Namespace
	if out.Name != "" {
		out.Name = FastTemplate(out.Name, templateContext)
	}

	if out.Namespace != "" {
		out.Namespace = FastTemplate(out.Namespace, templateContext)
	}

	// Selector
	if out.Selector != nil {
		selCopy, err := FastTemplateLabelSelector(out.Selector, templateContext)
		if err != nil {
			return ResourceReference{}, err
		}

		out.Selector = selCopy
	}

	return out, nil
}

func (t ResourceReference) LoadResources(
	ctx context.Context,
	kubeClient client.Client,
	restMapper k8smeta.RESTMapper,
	namespace string,
	additionSelectors []labels.Selector,
	templateContext map[string]string,
	allowClusterScoped bool,
	validateNamespace NamespaceValidator,
) ([]*unstructured.Unstructured, error) {
	isNamespaced, err := t.IsNamespacedGVK(restMapper)
	if err != nil {
		return nil, err
	}

	if !allowClusterScoped && !isNamespaced {
		return nil, fmt.Errorf("cluster-scoped kind %s/%s is not allowed", t.APIVersion, t.Kind)
	}

	ref, err := t.LoadTemplated(templateContext)
	if err != nil {
		return nil, err
	}

	ns := ref.Namespace
	if ns == "" && namespace != "" {
		ns = namespace
	}

	if validateNamespace != nil && ns != "" {
		if err := validateNamespace(ns); err != nil {
			return nil, err
		}
	}

	return ref.loadResources(ctx, kubeClient, ns, additionSelectors)
}

func (t ResourceReference) IsNamespacedGVK(
	restMapper k8smeta.RESTMapper,
) (bool, error) {
	return IsNamespacedGVK(restMapper, t.APIVersion, t.Kind)
}

func IsNamespacedGVK(
	restMapper k8smeta.RESTMapper,
	apiVersion string,
	kind string,
) (bool, error) {
	gv, err := schema.ParseGroupVersion(apiVersion)
	if err != nil {
		return false, fmt.Errorf("invalid apiVersion %q: %w", apiVersion, err)
	}

	gvk := gv.WithKind(kind)

	mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
	if err != nil {
		return false, fmt.Errorf("failed to resolve GVK %s: %w", gvk.String(), err)
	}

	return mapping.Scope.Name() == k8smeta.RESTScopeNameNamespace, nil
}

func (t ResourceReference) loadResources(
	ctx context.Context,
	kubeClient client.Client,
	namespace string,
	additionSelectors []labels.Selector,
) ([]*unstructured.Unstructured, error) {
	log := log.FromContext(ctx)

	log.V(5).Info("gathering resources",
		"apiVersion", t.APIVersion,
		"kind", t.Kind,
		"name", t.Name,
		"namespace", namespace,
	)

	// GET path (single object)
	if t.Name != "" {
		obj := &unstructured.Unstructured{}
		obj.SetAPIVersion(t.APIVersion)
		obj.SetKind(t.Kind)

		key := client.ObjectKey{
			Name:      t.Name,
			Namespace: namespace,
		}

		if err := kubeClient.Get(ctx, key, obj); err != nil {
			if apierrors.IsNotFound(err) && t.Optional {
				return nil, nil
			}

			return nil, fmt.Errorf("failed to get %s/%s: %w", t.Kind, t.Name, err)
		}

		return []*unstructured.Unstructured{obj}, nil
	}

	list := &unstructured.UnstructuredList{}
	list.SetAPIVersion(t.APIVersion)
	list.SetKind(t.Kind + "List")

	var opts []client.ListOption
	if namespace != "" {
		opts = append(opts, client.InNamespace(namespace))
	}

	var tenantSel labels.Selector

	if t.Selector != nil {
		s, err := metav1.LabelSelectorAsSelector(t.Selector)
		if err != nil {
			return nil, fmt.Errorf("invalid label selector: %w", err)
		}

		tenantSel = s
	}

	all := make([]labels.Selector, 0, len(additionSelectors)+1)

	for _, s := range additionSelectors {
		if s != nil {
			all = append(all, s)
		}
	}

	if tenantSel != nil {
		all = append(all, tenantSel)
	}

	if len(all) > 0 {
		combined := selectors.CombineSelectors(all...)

		selectorStrings := make([]string, 0, len(all))

		for _, s := range all {
			if s != nil {
				selectorStrings = append(selectorStrings, s.String())
			}
		}

		log.V(5).Info("applying combined label selector",
			"namespace", namespace,
			"selectors", selectorStrings,
			"combinedSelector", combined.String(),
		)

		opts = append(opts, client.MatchingLabelsSelector{Selector: combined})
	} else {
		log.V(5).Info("listing without label selector",
			"namespace", namespace,
		)
	}

	if err := kubeClient.List(ctx, list, opts...); err != nil {
		return nil, fmt.Errorf("failed to list %s: %w", t.Kind, err)
	}

	results := make([]*unstructured.Unstructured, 0, len(list.Items))
	for i := range list.Items {
		results = append(results, list.Items[i].DeepCopy())
	}

	log.V(5).Info("gathered resources",
		"namespace", namespace,
		"count", len(results),
	)

	return results, nil
}
