//go:build linux
// +build linux

package hcsv2

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"

	"github.com/Microsoft/hcsshim/internal/bridgeutils/gcserr"
	"github.com/Microsoft/hcsshim/internal/guest/cgroup"
	"github.com/Microsoft/hcsshim/internal/guest/prot"
	"github.com/Microsoft/hcsshim/internal/guest/runtime"
	specGuest "github.com/Microsoft/hcsshim/internal/guest/spec"
	"github.com/Microsoft/hcsshim/internal/guest/stdio"
	"github.com/Microsoft/hcsshim/internal/guest/storage"
	"github.com/Microsoft/hcsshim/internal/guest/transport"
	"github.com/Microsoft/hcsshim/internal/guestpath"
	"github.com/Microsoft/hcsshim/internal/log"
	"github.com/Microsoft/hcsshim/internal/logfields"
	"github.com/Microsoft/hcsshim/internal/ot"
	"github.com/Microsoft/hcsshim/internal/protocol/guestrequest"
	"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
	v1 "github.com/containerd/cgroups/v3/cgroup1/stats"
	oci "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/sirupsen/logrus"
	"go.opentelemetry.io/otel/attribute"
)

// containerStatus has been introduced to enable parallel container creation
type containerStatus uint32

const (
	// containerCreating is the default status set on a Container object, when
	// no underlying runtime container or init process has been assigned
	containerCreating containerStatus = iota
	// containerCreated is the status when a runtime container and init process
	// have been assigned, but runtime start command has not been issued yet
	containerCreated
	// containerRunning is the status when the init process has started and has
	// not yet exited.
	containerRunning
	// containerTerminated is the status when the init process has exited.
	containerTerminated
)

type Container struct {
	// id is the unique container identifier.
	id string
	// sandboxID is the pod this container belongs to. For sandbox containers, sandboxID == id.
	sandboxID string

	vsock   transport.Transport
	logPath string   // path to [logFile].
	logFile *os.File // file to redirect container's stdio to.

	spec          *oci.Spec
	ociBundlePath string
	isSandbox     bool

	container   runtime.Container
	initProcess *containerProcess

	etL      sync.Mutex
	exitType prot.NotificationType

	processesMutex sync.Mutex
	processes      map[uint32]*containerProcess

	// current container (creation) status.
	// Only access through [getStatus] and [setStatus].
	//
	// Note: its more ergonomic to store the uint32 and convert to/from [containerStatus]
	// then use [atomic.Value] and deal with unsafe conversions to/from [any], or use [atomic.Pointer]
	// and deal with the extra pointer dereferencing overhead.
	status atomic.Uint32

	// scratchDirPath represents the path inside the UVM where the scratch directory
	// of this container is located. Usually, this is either `/run/gcs/c/<containerID>` or
	// `/run/gcs/c/<UVMID>/container_<containerID>` if scratch is shared with UVM scratch.
	scratchDirPath string

	// sandboxRoot is the root directory of the pod within the guest.
	// Used during cleanup to unmount sandbox-specific paths.
	sandboxRoot string

	// stdioReconnectStopped, when set, stops the init process stdout/stderr
	// relay reconnect loop; set on teardown.
	stdioReconnectStopped atomic.Bool
}

func (c *Container) Start(ctx context.Context, conSettings stdio.ConnectionSettings) (_ int, err error) {
	entity := log.G(ctx).WithField(logfields.ContainerID, c.id)
	entity.Info("opengcs::Container::Start")

	// only use the logfile for the init process, since we don't want to tee stdio of execs
	t := c.vsock
	if c.logPath != "" {
		// don't use [os.Create] since that truncates an existing file, which is not desired
		if c.logFile, err = os.OpenFile(c.logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666); err != nil {
			return -1, fmt.Errorf("failed to open log file: %s: %w", c.logPath, err)
		}
		go func() {
			// initProcess is already created in [(*Host).CreateContainer], and is, therefore, "waitable"
			// wait in `writersWg`, which is closed after process is cleaned up (including io Relays)
			//
			// Note: [PipeRelay] and [TtyRelay] are not safe to call multiple times, so it is safer to wait
			// on the parent (init) process.
			c.initProcess.writersWg.Wait()

			if lfErr := c.logFile.Close(); lfErr != nil {
				entity.WithFields(logrus.Fields{
					logrus.ErrorKey: lfErr,
					logfields.Path:  c.logFile.Name(),
				}).Warn("failed to close log file")
			}
			c.logFile = nil
		}()

		t = transport.NewMultiWriter(c.vsock, c.logFile)
	}

	stdioSet, err := stdio.Connect(t, conSettings)
	if err != nil {
		return -1, err
	}

	// Keep the init process's stdout/stderr flowing across a host disconnect by
	// re-dialing the same vsock port; cancelled on container teardown.
	if stdioSet.Out != nil && conSettings.StdOut != nil {
		stdioSet.Out = transport.NewReconnectConnection(t, *conSettings.StdOut, stdioSet.Out, &c.stdioReconnectStopped)
	}
	if stdioSet.Err != nil && conSettings.StdErr != nil {
		stdioSet.Err = transport.NewReconnectConnection(t, *conSettings.StdErr, stdioSet.Err, &c.stdioReconnectStopped)
	}

	if c.initProcess.spec.Terminal {
		ttyr := c.container.Tty()
		ttyr.ReplaceConnectionSet(stdioSet)
		ttyr.Start()
	} else {
		pr := c.container.PipeRelay()
		pr.ReplaceConnectionSet(stdioSet)
		pr.CloseUnusedPipes()
		pr.Start()
	}
	err = c.container.Start()
	if err != nil {
		stdioSet.Close()
	} else {
		c.setStatus(containerRunning)
	}
	return int(c.initProcess.pid), err
}

func (c *Container) ExecProcess(ctx context.Context, process *oci.Process, conSettings stdio.ConnectionSettings) (int, error) {
	log.G(ctx).WithField(logfields.ContainerID, c.id).Info("opengcs::Container::ExecProcess")
	stdioSet, err := stdio.Connect(c.vsock, conSettings)
	if err != nil {
		return -1, err
	}

	// Add in the core rlimit specified on the container in case there was one set. This makes it so that execed processes can also generate
	// core dumps.
	process.Rlimits = c.spec.Process.Rlimits

	// If the client provided a user for the container to run as, we want to have the exec run as this user as well
	// unless the exec's spec was explicitly set to a different user. If the Username field is filled in on the containers
	// spec, at this point that means the work to find a uid:gid pairing for this username has already been done, so simply
	// assign the uid:gid from the container.
	if process.User.Username != "" {
		// The exec provided a user string of it's own. Grab the uid:gid pairing for the string (if one exists).
		if err := specGuest.SetUserStr(&oci.Spec{Root: c.spec.Root, Process: process}, process.User.Username); err != nil {
			return -1, err
		}
		// Runc doesn't care about this, and just to be safe clear it.
		process.User.Username = ""
	} else if c.spec.Process.User.Username != "" {
		process.User = c.spec.Process.User
	}

	p, err := c.container.ExecProcess(process, stdioSet)
	if err != nil {
		stdioSet.Close()
		return -1, err
	}

	pid := p.Pid()
	c.processesMutex.Lock()
	c.processes[uint32(pid)] = newProcess(c, process, p, uint32(pid), false)
	c.processesMutex.Unlock()
	return pid, nil
}

// InitProcess returns the container's init process
func (c *Container) InitProcess() Process {
	return c.initProcess
}

// GetProcess returns the Process with the matching 'pid'. If the 'pid' does
// not exit returns error.
func (c *Container) GetProcess(pid uint32) (Process, error) {
	//todo: thread a context to this function call
	logrus.WithFields(logrus.Fields{
		logfields.ContainerID: c.id,
		logfields.ProcessID:   pid,
	}).Info("opengcs::Container::GetProcess")
	if c.initProcess.pid == pid {
		return c.initProcess, nil
	}

	c.processesMutex.Lock()
	defer c.processesMutex.Unlock()

	p, ok := c.processes[pid]
	if !ok {
		return nil, gcserr.NewHresultError(gcserr.HrErrNotFound)
	}
	return p, nil
}

// GetAllProcessPids returns all process pids in the container namespace.
func (c *Container) GetAllProcessPids(ctx context.Context) ([]int, error) {
	log.G(ctx).WithField(logfields.ContainerID, c.id).Info("opengcs::Container::GetAllProcessPids")
	state, err := c.container.GetAllProcesses()
	if err != nil {
		return nil, err
	}
	pids := make([]int, len(state))
	for i, s := range state {
		pids[i] = s.Pid
	}
	return pids, nil
}

// Kill sends 'signal' to the container process.
func (c *Container) Kill(ctx context.Context, signal syscall.Signal) error {
	log.G(ctx).WithFields(logrus.Fields{
		logfields.ContainerID: c.id,
		"signal":              signal.String(),
	}).Info("opengcs::Container::Kill")
	err := c.container.Kill(signal)
	if err != nil {
		return err
	}
	c.setExitType(signal)
	c.stdioReconnectStopped.Store(true)
	return nil
}

func (c *Container) Delete(ctx context.Context) error {
	entity := log.G(ctx).WithField(logfields.ContainerID, c.id)
	entity.Info("opengcs::Container::Delete")
	c.stdioReconnectStopped.Store(true)
	if c.isSandbox && c.sandboxRoot != "" {
		// remove user mounts in sandbox container
		if err := storage.UnmountAllInPath(ctx, specGuest.SandboxMountsDirFromRoot(c.sandboxRoot), true); err != nil {
			entity.WithError(err).Error("failed to unmount sandbox mounts")
		}

		// remove tmpfs mounts in sandbox container
		if err := storage.UnmountAllInPath(ctx, specGuest.SandboxTmpfsMountsDirFromRoot(c.sandboxRoot), true); err != nil {
			entity.WithError(err).Error("failed to unmount tmpfs sandbox mounts")
		}

		// remove hugepages mounts in sandbox container
		if err := storage.UnmountAllInPath(ctx, specGuest.SandboxHugePagesMountsDirFromRoot(c.sandboxRoot), true); err != nil {
			entity.WithError(err).Error("failed to unmount hugepages mounts")
		}
	}

	var retErr error
	if err := c.container.Delete(); err != nil {
		retErr = err
	}

	// If the bundle was bind-mounted onto the scratch disk during create
	// (modifyCombinedLayers), undo that bind. The same path-based rule is used
	// here as at create time, so both shims are handled identically. Order
	// matters: this runs before removing the scratch/bundle dirs below (the
	// bind's source lives under scratchDirPath) and before the host unmaps the
	// scratch disk, so no mount still references that disk. removeTarget=false
	// detaches the bind only - the ociBundlePath directory itself remains and is
	// deleted by RemoveAll below.
	if bundleNeedsScratchBind(c.ociBundlePath, c.scratchDirPath) {
		if err := storage.UnmountPath(ctx, c.ociBundlePath, false); err != nil {
			entity.WithError(err).WithField("bundle", c.ociBundlePath).
				Warn("failed to unmount scratch-backed bundle bind")
		}
	}

	if err := os.RemoveAll(c.scratchDirPath); err != nil {
		if retErr != nil {
			retErr = fmt.Errorf("errors deleting container state: %w; %w", retErr, err)
		} else {
			retErr = err
		}
	}

	if err := os.RemoveAll(c.ociBundlePath); err != nil {
		if retErr != nil {
			retErr = fmt.Errorf("errors deleting container oci bundle dir: %w; %w", retErr, err)
		} else {
			retErr = err
		}
	}

	// For V2 sandbox containers, remove the now-empty pod directory
	// (parent of c.ociBundlePath, e.g. /run/gcs/pods/<podID>).
	if c.isSandbox && strings.HasPrefix(c.ociBundlePath, guestpath.LCOWV2RootPrefixInVM) {
		podDir := filepath.Dir(c.ociBundlePath)
		if err := os.Remove(podDir); err != nil && !os.IsNotExist(err) {
			entity.WithError(err).WithField("podDir", podDir).
				Warn("failed to remove pod directory after sandbox delete")
		}
	}

	return retErr
}

func (c *Container) Update(ctx context.Context, resources interface{}) error {
	log.G(ctx).WithField(logfields.ContainerID, c.id).Info("opengcs::Container::Update")
	return c.container.Update(resources)
}

// Wait waits for the container's init process to exit.
func (c *Container) Wait() prot.NotificationType {
	_, span := ot.StartSpan(context.Background(), "opengcs::Container::Wait")
	defer span.End()
	span.SetAttributes(attribute.String(logfields.ContainerID, c.id))

	c.initProcess.writersWg.Wait()
	c.etL.Lock()
	defer c.etL.Unlock()
	return c.exitType
}

// setExitType sets `c.exitType` to the appropriate value based on `signal` if
// `signal` will take down the container.
func (c *Container) setExitType(signal syscall.Signal) {
	c.etL.Lock()
	defer c.etL.Unlock()

	switch signal {
	case syscall.SIGTERM:
		c.exitType = prot.NtGracefulExit
	case syscall.SIGKILL:
		c.exitType = prot.NtForcedExit
	}
}

// GetStats returns the cgroup metrics for the container.
// Works with both cgroup v1 and v2 systems.
func (c *Container) GetStats(ctx context.Context) (*v1.Metrics, error) {
	_, span := ot.StartSpan(ctx, "opengcs::Container::GetStats")
	defer span.End()
	span.SetAttributes(attribute.String("cid", c.id))

	return cgroup.LoadAndStat(c.spec.Linux.CgroupsPath)
}

func (c *Container) modifyContainerConstraints(ctx context.Context, _ guestrequest.RequestType, cc *guestresource.LCOWContainerConstraints) (err error) {
	return c.Update(ctx, cc.Linux)
}

func (c *Container) getStatus() containerStatus {
	return containerStatus(c.status.Load())
}

func (c *Container) setStatus(st containerStatus) {
	c.status.Store(uint32(st))
}

func (c *Container) ID() string {
	return c.id
}
