package checkpoint

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"os"

	metadata "github.com/checkpoint-restore/checkpointctl/lib"
	"github.com/containers/common/libimage"
	"github.com/containers/common/pkg/config"
	"github.com/containers/podman/v4/libpod"
	ann "github.com/containers/podman/v4/pkg/annotations"
	"github.com/containers/podman/v4/pkg/checkpoint/crutils"
	"github.com/containers/podman/v4/pkg/criu"
	"github.com/containers/podman/v4/pkg/domain/entities"
	"github.com/containers/podman/v4/pkg/specgen/generate"
	"github.com/containers/podman/v4/pkg/specgenutil"
	spec "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/sirupsen/logrus"
)

// Prefixing the checkpoint/restore related functions with 'cr'

func CRImportCheckpointTar(ctx context.Context, runtime *libpod.Runtime, restoreOptions entities.RestoreOptions) ([]*libpod.Container, error) {
	// First get the container definition from the
	// tarball to a temporary directory
	dir, err := ioutil.TempDir("", "checkpoint")
	if err != nil {
		return nil, err
	}
	defer func() {
		if err := os.RemoveAll(dir); err != nil {
			logrus.Errorf("Could not recursively remove %s: %q", dir, err)
		}
	}()
	if err := crutils.CRImportCheckpointConfigOnly(dir, restoreOptions.Import); err != nil {
		return nil, err
	}
	return CRImportCheckpoint(ctx, runtime, restoreOptions, dir)
}

// CRImportCheckpoint it the function which imports the information
// from checkpoint tarball and re-creates the container from that information
func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOptions entities.RestoreOptions, dir string) ([]*libpod.Container, error) {
	// Load spec.dump from temporary directory
	dumpSpec := new(spec.Spec)
	if _, err := metadata.ReadJSONFile(dumpSpec, dir, metadata.SpecDumpFile); err != nil {
		return nil, err
	}

	// Load config.dump from temporary directory
	ctrConfig := new(libpod.ContainerConfig)
	if _, err := metadata.ReadJSONFile(ctrConfig, dir, metadata.ConfigDumpFile); err != nil {
		return nil, err
	}

	if ctrConfig.Pod != "" && restoreOptions.Pod == "" {
		return nil, errors.New("cannot restore pod container without --pod")
	}

	if ctrConfig.Pod == "" && restoreOptions.Pod != "" {
		return nil, errors.New("cannot restore non pod container into pod")
	}

	// This should not happen as checkpoints with these options are not exported.
	if len(ctrConfig.Dependencies) > 0 {
		return nil, errors.New("cannot import checkpoints of containers with dependencies")
	}

	// Volumes included in the checkpoint should not exist
	if !restoreOptions.IgnoreVolumes {
		for _, vol := range ctrConfig.NamedVolumes {
			exists, err := runtime.HasVolume(vol.Name)
			if err != nil {
				return nil, err
			}
			if exists {
				return nil, fmt.Errorf("volume with name %s already exists. Use --ignore-volumes to not restore content of volumes", vol.Name)
			}
		}
	}

	if restoreOptions.IgnoreStaticIP || restoreOptions.IgnoreStaticMAC {
		for net, opts := range ctrConfig.Networks {
			if restoreOptions.IgnoreStaticIP {
				opts.StaticIPs = nil
			}
			if restoreOptions.IgnoreStaticMAC {
				opts.StaticMAC = nil
			}
			ctrConfig.Networks[net] = opts
		}
	}

	ctrID := ctrConfig.ID
	newName := false

	// Check if the restored container gets a new name
	if restoreOptions.Name != "" {
		ctrConfig.ID = ""
		ctrConfig.Name = restoreOptions.Name
		newName = true
	}

	if restoreOptions.Pod != "" {
		// Restoring into a Pod requires much newer versions of CRIU
		if !criu.CheckForCriu(criu.PodCriuVersion) {
			return nil, fmt.Errorf("restoring containers into pods requires at least CRIU %d", criu.PodCriuVersion)
		}
		// The runtime also has to support it
		if !crutils.CRRuntimeSupportsPodCheckpointRestore(runtime.GetOCIRuntimePath()) {
			return nil, fmt.Errorf("runtime %s does not support pod restore", runtime.GetOCIRuntimePath())
		}
		// Restoring into an existing Pod
		ctrConfig.Pod = restoreOptions.Pod

		// According to podman pod create a pod can share the following namespaces:
		// cgroup, ipc, net, pid, uts
		// Let's make sure we a restoring into a pod with the same shared namespaces.
		pod, err := runtime.LookupPod(ctrConfig.Pod)
		if err != nil {
			return nil, fmt.Errorf("pod %q cannot be retrieved: %w", ctrConfig.Pod, err)
		}

		infraContainer, err := pod.InfraContainer()
		if err != nil {
			return nil, fmt.Errorf("cannot retrieve infra container from pod %q: %w", ctrConfig.Pod, err)
		}

		// If a namespaces was shared (!= "") it needs to be set to the new infrastructure container
		// If the infrastructure container does not share the same namespaces as the to be restored
		// container we abort.
		if ctrConfig.IPCNsCtr != "" {
			if !pod.SharesIPC() {
				return nil, fmt.Errorf("pod %s does not share the IPC namespace", ctrConfig.Pod)
			}
			ctrConfig.IPCNsCtr = infraContainer.ID()
		}

		if ctrConfig.NetNsCtr != "" {
			if !pod.SharesNet() {
				return nil, fmt.Errorf("pod %s does not share the network namespace", ctrConfig.Pod)
			}
			ctrConfig.NetNsCtr = infraContainer.ID()
			for net, opts := range ctrConfig.Networks {
				opts.StaticIPs = nil
				opts.StaticMAC = nil
				ctrConfig.Networks[net] = opts
			}
			ctrConfig.StaticIP = nil
			ctrConfig.StaticMAC = nil
		}

		if ctrConfig.PIDNsCtr != "" {
			if !pod.SharesPID() {
				return nil, fmt.Errorf("pod %s does not share the PID namespace", ctrConfig.Pod)
			}
			ctrConfig.PIDNsCtr = infraContainer.ID()
		}

		if ctrConfig.UTSNsCtr != "" {
			if !pod.SharesUTS() {
				return nil, fmt.Errorf("pod %s does not share the UTS namespace", ctrConfig.Pod)
			}
			ctrConfig.UTSNsCtr = infraContainer.ID()
		}

		if ctrConfig.CgroupNsCtr != "" {
			if !pod.SharesCgroup() {
				return nil, fmt.Errorf("pod %s does not share the cgroup namespace", ctrConfig.Pod)
			}
			ctrConfig.CgroupNsCtr = infraContainer.ID()
		}

		// Change SELinux labels to infrastructure container labels
		ctrConfig.MountLabel = infraContainer.MountLabel()
		ctrConfig.ProcessLabel = infraContainer.ProcessLabel()

		// Fix parent cgroup
		cgroupPath, err := pod.CgroupPath()
		if err != nil {
			return nil, fmt.Errorf("cannot retrieve cgroup path from pod %q: %w", ctrConfig.Pod, err)
		}
		ctrConfig.CgroupParent = cgroupPath

		oldPodID := dumpSpec.Annotations[ann.SandboxID]
		// Fix up SandboxID in the annotations
		dumpSpec.Annotations[ann.SandboxID] = ctrConfig.Pod
		// Fix up CreateCommand
		for i, c := range ctrConfig.CreateCommand {
			if c == oldPodID {
				ctrConfig.CreateCommand[i] = ctrConfig.Pod
			}
		}
	}

	if len(restoreOptions.PublishPorts) > 0 {
		pubPorts, err := specgenutil.CreatePortBindings(restoreOptions.PublishPorts)
		if err != nil {
			return nil, err
		}

		ports, err := generate.ParsePortMapping(pubPorts, nil)
		if err != nil {
			return nil, err
		}
		ctrConfig.PortMappings = ports
	}

	pullOptions := &libimage.PullOptions{}
	pullOptions.Writer = os.Stderr
	if _, err := runtime.LibimageRuntime().Pull(ctx, ctrConfig.RootfsImageName, config.PullPolicyMissing, pullOptions); err != nil {
		return nil, err
	}

	// Now create a new container from the just loaded information
	container, err := runtime.RestoreContainer(ctx, dumpSpec, ctrConfig)
	if err != nil {
		return nil, err
	}

	var containers []*libpod.Container
	if container == nil {
		return nil, nil
	}

	containerConfig := container.Config()
	ctrName := ctrConfig.Name
	if containerConfig.Name != ctrName {
		return nil, fmt.Errorf("name of restored container (%s) does not match requested name (%s)", containerConfig.Name, ctrName)
	}

	if !newName {
		// Only check ID for a restore with the same name.
		// Using -n to request a new name for the restored container, will also create a new ID
		if containerConfig.ID != ctrID {
			return nil, fmt.Errorf("ID of restored container (%s) does not match requested ID (%s)", containerConfig.ID, ctrID)
		}
	}

	containers = append(containers, container)
	return containers, nil
}
