/*
   Copyright The containerd Authors.

   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 permissions and
   limitations under the License.
*/

package server

import (
	"context"
	"fmt"
	"time"

	"github.com/containerd/containerd/v2/core/leases"
	"github.com/containerd/containerd/v2/pkg/tracing"
	"github.com/containerd/errdefs"
	"github.com/containerd/log"

	runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
)

// RemovePodSandbox removes the sandbox. If there are running containers in the
// sandbox, they should be forcibly removed.
func (c *criService) RemovePodSandbox(ctx context.Context, r *runtime.RemovePodSandboxRequest) (*runtime.RemovePodSandboxResponse, error) {
	span := tracing.SpanFromContext(ctx)
	start := time.Now()
	sandbox, err := c.sandboxStore.Get(r.GetPodSandboxId())
	if err != nil {
		if !errdefs.IsNotFound(err) {
			return nil, fmt.Errorf("an error occurred when try to find sandbox %q: %w",
				r.GetPodSandboxId(), err)
		}
		// Do not return error if the id doesn't exist.
		log.G(ctx).Tracef("RemovePodSandbox called for sandbox %q that does not exist",
			r.GetPodSandboxId())
		return &runtime.RemovePodSandboxResponse{}, nil
	}

	defer c.nri.BlockPluginSync().Unblock()

	// Use the full sandbox id.
	id := sandbox.ID
	span.SetAttributes(tracing.Attribute("sandbox.id", id))

	// If the sandbox is still running, not ready, or in an unknown state, forcibly stop it.
	// Even if it's in a NotReady state, this will close its network namespace, if open.
	// This can happen if the task process associated with the Pod died or it was killed.
	log.G(ctx).Infof("Forcibly stopping sandbox %q", id)
	if err := c.stopPodSandbox(ctx, sandbox); err != nil {
		return nil, fmt.Errorf("failed to forcibly stop sandbox %q: %w", id, err)
	}

	if err := c.client.LeasesService().Delete(ctx, leases.Lease{ID: id}); err != nil {
		if !errdefs.IsNotFound(err) {
			return nil, fmt.Errorf("failed to delete lease for sandbox %q: %w", id, err)
		}
	}

	// Return error if sandbox network namespace is not closed yet.
	if sandbox.NetNS != nil {
		nsPath := sandbox.NetNS.GetPath()
		if closed, err := sandbox.NetNS.Closed(); err != nil {
			return nil, fmt.Errorf("failed to check sandbox network namespace %q closed: %w", nsPath, err)
		} else if !closed {
			return nil, fmt.Errorf("sandbox network namespace %q is not fully closed", nsPath)
		}
	}

	// Remove all containers inside the sandbox.
	// NOTE(random-liu): container could still be created after this point, Kubelet should
	// not rely on this behavior.
	// TODO(random-liu): Introduce an intermediate state to avoid container creation after
	// this point.
	cntrs := c.containerStore.List()
	for _, cntr := range cntrs {
		if cntr.SandboxID != id {
			continue
		}
		_, err = c.RemoveContainer(ctx, &runtime.RemoveContainerRequest{ContainerId: cntr.ID})
		if err != nil {
			return nil, fmt.Errorf("failed to remove container %q: %w", cntr.ID, err)
		}
	}

	if err := c.sandboxService.ShutdownSandbox(ctx, sandbox.Sandboxer, id); err != nil && !errdefs.IsNotFound(err) {
		return nil, fmt.Errorf("failed to delete sandbox %q: %w", id, err)
	}

	// Send CONTAINER_DELETED event with ContainerId equal to SandboxId.
	c.generateAndSendContainerEvent(ctx, id, id, runtime.ContainerEventType_CONTAINER_DELETED_EVENT)

	err = c.nri.RemovePodSandbox(ctx, &sandbox)
	if err != nil {
		log.G(ctx).WithError(err).Errorf("NRI pod removal notification failed")
	}

	// Remove sandbox from sandbox store. Note that once the sandbox is successfully
	// deleted:
	// 1) ListPodSandbox will not include this sandbox.
	// 2) PodSandboxStatus and StopPodSandbox will return error.
	// 3) On-going operations which have held the reference will not be affected.
	c.sandboxStore.Delete(id)

	if err := c.client.SandboxStore().Delete(ctx, id); err != nil {
		if !errdefs.IsNotFound(err) {
			return nil, fmt.Errorf("failed to remove sandbox metadata from store: %w", err)
		}
		log.G(ctx).WithError(err).Warnf("failed to delete sandbox metadata from store: %q maybe recovered from v1.x release", id)
	}

	// Release the sandbox name reserved for the sandbox.
	c.sandboxNameIndex.ReleaseByKey(id)

	sandboxRemoveTimer.WithValues(sandbox.RuntimeHandler).UpdateSince(start)

	span.AddEvent("pod sandbox removed",
		tracing.Attribute("sandbox.remove.duration", time.Since(start).String()),
	)

	return &runtime.RemovePodSandboxResponse{}, nil
}
