// Copyright (c) 2025-2026 Tigera, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing persmissions and
// limitations under the License.

package stagednetworkpolicy

import (
	"context"

	"github.com/google/uuid"
	calico "github.com/projectcalico/api/pkg/apis/projectcalico/v3"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/watch"
	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/apiserver/pkg/registry/generic/registry"
	"k8s.io/apiserver/pkg/registry/rest"

	"github.com/projectcalico/calico/apiserver/pkg/rbac"
	"github.com/projectcalico/calico/apiserver/pkg/registry/projectcalico/authorizer"
	"github.com/projectcalico/calico/apiserver/pkg/registry/projectcalico/server"
	"github.com/projectcalico/calico/apiserver/pkg/registry/projectcalico/util"
	"github.com/projectcalico/calico/libcalico-go/lib/names"
)

// rest implements a RESTStorage for API services against etcd
type REST struct {
	*registry.Store
	rbac.CalicoResourceLister
	authorizer   authorizer.TierAuthorizer
	watchManager *util.WatchManager
	shortNames   []string
}

// EmptyObject returns an empty instance
func EmptyObject() runtime.Object {
	return &calico.StagedNetworkPolicy{}
}

// NewList returns a new shell of a binding list
func NewList() runtime.Object {
	return &calico.StagedNetworkPolicyList{}
}

// NewREST returns a RESTStorage object that will work against API services.
func NewREST(scheme *runtime.Scheme, opts server.Options, calicoResourceLister rbac.CalicoResourceLister, watchManager *util.WatchManager) (*REST, error) {
	strategy := NewStrategy(scheme)

	prefix := "/" + opts.ResourcePrefix()
	// We adapt the store's keyFunc so that we can use it with the StorageDecorator
	// without making any assumptions about where objects are stored in etcd
	keyFunc := func(obj runtime.Object) (string, error) {
		accessor, err := meta.Accessor(obj)
		if err != nil {
			return "", err
		}
		return registry.NamespaceKeyFunc(genericapirequest.WithNamespace(genericapirequest.NewContext(), accessor.GetNamespace()), prefix, accessor.GetName())
	}
	storageInterface, dFunc, err := opts.GetStorage(
		prefix,
		keyFunc,
		strategy,
		func() runtime.Object { return &calico.StagedNetworkPolicy{} },
		func() runtime.Object { return &calico.StagedNetworkPolicyList{} },
		GetAttrs,
		nil,
		nil,
	)
	if err != nil {
		return nil, err
	}
	store := &registry.Store{
		NewFunc:     func() runtime.Object { return &calico.StagedNetworkPolicy{} },
		NewListFunc: func() runtime.Object { return &calico.StagedNetworkPolicyList{} },
		KeyRootFunc: opts.KeyRootFunc(true),
		KeyFunc:     opts.KeyFunc(true),
		ObjectNameFunc: func(obj runtime.Object) (string, error) {
			return obj.(*calico.StagedNetworkPolicy).Name, nil
		},
		PredicateFunc:            MatchPolicy,
		DefaultQualifiedResource: calico.Resource("stagednetworkpolicies"),

		CreateStrategy:          strategy,
		UpdateStrategy:          strategy,
		DeleteStrategy:          strategy,
		EnableGarbageCollection: true,

		Storage:     storageInterface,
		DestroyFunc: dFunc,
	}

	return &REST{store, calicoResourceLister, authorizer.NewTierAuthorizer(opts.Authorizer), watchManager, opts.ShortNames}, nil
}

func (r *REST) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
	err := util.EnsureTierSelector(ctx, options, r.authorizer, r.CalicoResourceLister)
	if err != nil {
		return nil, err
	}

	return r.Store.List(ctx, options)
}

func (r *REST) Create(ctx context.Context, obj runtime.Object, val rest.ValidateObjectFunc, createOpt *metav1.CreateOptions) (runtime.Object, error) {
	policy := obj.(*calico.StagedNetworkPolicy)
	tierName := names.TierOrDefault(policy.Spec.Tier)
	err := r.authorizer.AuthorizeTierOperation(ctx, policy.Name, tierName)
	if err != nil {
		return nil, err
	}

	return r.Store.Create(ctx, obj, val, createOpt)
}

func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc,
	updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
	oldObj, err := r.Store.Get(ctx, name, &metav1.GetOptions{})
	switch {
	case err == nil:
		oldTier := names.TierOrDefault(oldObj.(*calico.StagedNetworkPolicy).Spec.Tier)
		if err := r.authorizer.AuthorizeTierOperation(ctx, name, oldTier); err != nil {
			return nil, false, err
		}

		// Also authorize the new tier, in case the update changes the policy's tier.
		newObj, err := objInfo.UpdatedObject(ctx, oldObj)
		if err != nil {
			return nil, false, err
		}
		newTier := names.TierOrDefault(newObj.(*calico.StagedNetworkPolicy).Spec.Tier)
		if newTier != oldTier {
			if err := r.authorizer.AuthorizeTierOperation(ctx, name, newTier); err != nil {
				return nil, false, err
			}
		}
	case k8serrors.IsNotFound(err) && forceAllowCreate:
		// A server-side apply of a policy that doesn't exist yet reaches Update
		// with forceAllowCreate set rather than going through Create. There's no
		// existing object to authorize against, so authorize the incoming object's
		// tier and let the store create it.
		newObj, err := objInfo.UpdatedObject(ctx, &calico.StagedNetworkPolicy{})
		if err != nil {
			return nil, false, err
		}
		newTier := names.TierOrDefault(newObj.(*calico.StagedNetworkPolicy).Spec.Tier)
		if err := r.authorizer.AuthorizeTierOperation(ctx, name, newTier); err != nil {
			return nil, false, err
		}
	default:
		return nil, false, err
	}

	return r.Store.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
}

// Get retrieves the item from storage.
func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
	obj, err := r.Store.Get(ctx, name, options)
	if err != nil {
		return nil, err
	}
	tierName := names.TierOrDefault(obj.(*calico.StagedNetworkPolicy).Spec.Tier)
	err = r.authorizer.AuthorizeTierOperation(ctx, name, tierName)
	if err != nil {
		return nil, err
	}

	return obj, nil
}

func (r *REST) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
	obj, err := r.Store.Get(ctx, name, &metav1.GetOptions{})
	if err != nil {
		return nil, false, err
	}
	tierName := names.TierOrDefault(obj.(*calico.StagedNetworkPolicy).Spec.Tier)
	err = r.authorizer.AuthorizeTierOperation(ctx, name, tierName)
	if err != nil {
		return nil, false, err
	}

	return r.Store.Delete(ctx, name, deleteValidation, options)
}

// Apply tiered RBAC to deletecollection requests.
func (r *REST) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
	if listOptions == nil {
		listOptions = &metainternalversion.ListOptions{}
	} else {
		listOptions = listOptions.DeepCopy()
	}
	if err := util.EnsureTierSelector(ctx, listOptions, r.authorizer, r.CalicoResourceLister); err != nil {
		return nil, err
	}
	return r.Store.DeleteCollection(ctx, deleteValidation, options, listOptions)
}

func (r *REST) Watch(ctx context.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
	err := util.EnsureTierSelector(ctx, options, r.authorizer, r.CalicoResourceLister)
	if err != nil {
		return nil, err
	}

	w, err := r.Store.Watch(ctx, options)
	if err != nil {
		return nil, err
	}

	record := util.WatchRecord{
		ID:    uuid.New().String(),
		Kind:  calico.KindStagedNetworkPolicy,
		Watch: w,
		Ctx:   ctx,
	}
	r.watchManager.AddWatch(record)

	return w, nil
}

func (r *REST) ShortNames() []string {
	return r.shortNames
}
