package watch_graph //nolint:staticcheck

import (
	"context"
	"fmt"
	"time"

	"github.com/google/cel-go/common/types"
	corev1 "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/kubernetes_api/agent/watch_graph/vendored/k8s.io/api/core/v1"
	k8errors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/cache"
)

const (
	namespacePollPeriod = 10 * time.Second
)

var (
	nsGVR = corev1.GroupVersion.WithResource("namespaces")
)

type wgNamespaces struct {
	namespaces       Namespaces
	client           dynamic.Interface
	setStatus        func(nsWithStatus) // must be idempotent as it can be called more than once for the same ns
	objectToEvalVars func(*unstructured.Unstructured, schema.GroupVersionResource) map[string]any
	onError          func(Error)
	onWarn           func(Warning)
}

func (n *wgNamespaces) Run(ctx context.Context) {
	if n.namespaces.isFixedList() {
		n.pollNamespaces(ctx)
	} else {
		n.watchNamespaces(ctx)
	}
}

// pollNamespaces polls the specified namespaces.
// There is no watch for a single object, and we don't want to establish a cluster-wide namespace watch as that
// would require watch permissions on all namespaces.
func (n *wgNamespaces) pollNamespaces(ctx context.Context) {
	nsClient := n.client.Resource(nsGVR)
	n.doPollNamespaces(ctx, nsClient)
	t := time.NewTicker(namespacePollPeriod)
	defer t.Stop()
	done := ctx.Done()
	for {
		select {
		case <-done:
			return
		case <-t.C:
			n.doPollNamespaces(ctx, nsClient)
		}
	}
}

func (n *wgNamespaces) doPollNamespaces(ctx context.Context, nsClient dynamic.NamespaceableResourceInterface) {
	for ns := range n.namespaces.Names {
		res, err := nsClient.Get(ctx, ns, metav1.GetOptions{})
		switch {
		case err == nil:
			n.maybeSendNs(ctx, res)
		case k8errors.IsNotFound(err):
			n.setStatus(nsWithStatus{name: ns, status: nsDeleted})
		default:
			n.onWarn(NewNamespaceListFailedWarning(ns, err))
		}
	}
}

func (n *wgNamespaces) watchNamespaces(ctx context.Context) {
	nsInf := newInformer(n.client, nsGVR, metav1.NamespaceNone, n.namespaces.LabelSelector, n.namespaces.FieldSelector)
	_, err := nsInf.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj any) {
			n.maybeSendNs(ctx, obj.(*unstructured.Unstructured))
		},
		UpdateFunc: func(oldObj, newObj any) {
			n.maybeSendNs(ctx, newObj.(*unstructured.Unstructured))
		},
		DeleteFunc: func(obj any) {
			var nsUnstr *unstructured.Unstructured
			switch ns := obj.(type) {
			case *unstructured.Unstructured:
				nsUnstr = ns
			case cache.DeletedFinalStateUnknown:
				nsUnstr = ns.Obj.(*unstructured.Unstructured)
			default:
				n.onError(Error{
					Message: fmt.Sprintf("unexpected object type: %T", obj),
					Code:    InternalError,
				})
				return
			}
			n.setStatus(nsWithStatus{name: nsUnstr.GetName(), status: nsDeleted})
		},
	})
	if err != nil { // cannot happen
		n.onError(Error{
			Message: fmt.Sprintf("nsInf.AddEventHandler: %v", err),
			Code:    InternalError,
		})
		return
	}
	//nsInf.HasSynced() // TODO handle stuck sync
	nsInf.RunWithContext(ctx)
}

func (n *wgNamespaces) maybeSendNs(ctx context.Context, ns *unstructured.Unstructured) {
	selectedByExpression := true // empty/missing expression matches everything
	if n.namespaces.ObjectSelectorExpression != nil {
		val, _, err := n.namespaces.ObjectSelectorExpression.ContextEval(ctx, n.objectToEvalVars(ns, nsGVR))
		if err != nil {
			n.onError(Error{
				Message: fmt.Sprintf("namespaces: object selector expression: %v", err),
				Code:    InvalidArgument,
			})
			return
		}
		selectedByExpression = bool(val.(types.Bool))
	}

	name := ns.GetName()

	if selectedByExpression {
		n.setStatus(nsWithStatus{name: name, status: nsSelected})
	} else {
		n.setStatus(nsWithStatus{name: name, status: nsFilteredOut})
	}
}
