//go:build linux
// +build linux

package hcsv2

import (
	"context"
	"fmt"
	"os/exec"
	"sync"
	"syscall"

	"github.com/Microsoft/hcsshim/internal/guest/gcserr"
	"github.com/Microsoft/hcsshim/internal/guest/runtime"
	"github.com/Microsoft/hcsshim/internal/guest/stdio"
	"github.com/Microsoft/hcsshim/internal/log"
	"github.com/Microsoft/hcsshim/internal/logfields"
	"github.com/Microsoft/hcsshim/internal/oc"
	oci "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"go.opencensus.io/trace"
)

type Process interface {
	// Kill sends `signal` to the process.
	//
	// If the process has already exited returns `gcserr.HrErrNotFound` by contract.
	Kill(ctx context.Context, signal syscall.Signal) error
	// Pid returns the process id of the process.
	Pid() int
	// ResizeConsole resizes the tty to `height`x`width` for the process.
	ResizeConsole(ctx context.Context, height, width uint16) error
	// Wait returns a channel that can be used to wait for the process to exit
	// and gather the exit code. The second channel must be signaled from the
	// caller when the caller has completed its use of this call to Wait.
	Wait() (<-chan int, chan<- bool)
}

// Process is a struct that defines the lifetime and operations associated with
// an oci.Process.
type containerProcess struct {
	// c is the owning container
	c    *Container
	spec *oci.Process
	// cid is the container id that owns this process.
	cid string

	process runtime.Process
	pid     uint32
	// init is `true` if this is the container process itself
	init bool

	// This is only valid post the exitWg
	exitCode int
	// exitWg is marked as done as soon as the underlying
	// (runtime.Process).Wait() call returns, and exitCode has been updated.
	exitWg sync.WaitGroup

	// Used to allow addition/removal to the writersWg after an initial wait has
	// already been issued. It is not safe to call Add/Done without holding this
	// lock.
	writersSyncRoot sync.Mutex
	// Used to track the number of writers that need to finish
	// before the process can be marked for cleanup.
	writersWg sync.WaitGroup
	// Used to track the 1st caller to the writersWg that successfully
	// acknowledges it wrote the exit response.
	writersCalled bool
}

var _ Process = &containerProcess{}

// newProcess returns a containerProcess struct that has been initialized with
// an outstanding wait for process exit, and post exit an outstanding wait for
// process cleanup to release all resources once at least 1 waiter has
// successfully written the exit response.
func newProcess(c *Container, spec *oci.Process, process runtime.Process, pid uint32, init bool) *containerProcess {
	p := &containerProcess{
		c:       c,
		spec:    spec,
		process: process,
		init:    init,
		cid:     c.id,
		pid:     pid,
	}
	p.exitWg.Add(1)
	p.writersWg.Add(1)
	go func() {
		ctx, span := oc.StartSpan(context.Background(), "newProcess::waitBackground")
		defer span.End()
		span.AddAttributes(
			trace.StringAttribute(logfields.ContainerID, p.cid),
			trace.Int64Attribute(logfields.ProcessID, int64(p.pid)))

		// Wait for the process to exit
		exitCode, err := p.process.Wait()
		if err != nil {
			log.G(ctx).WithError(err).Error("failed to wait for runc process")
		}
		p.exitCode = exitCode
		log.G(ctx).WithField("exitCode", p.exitCode).Debug("process exited")

		// Free any process waiters
		p.exitWg.Done()

		// Schedule the removal of this process object from the map once at
		// least one waiter has read the result
		go func() {
			p.writersWg.Wait()
			// cleanup the process state
			if derr := p.process.Delete(); derr != nil {
				log.G(ctx).WithFields(logrus.Fields{
					"cid": p.cid,
					"pid": p.pid,
				}).Debugf("process cleanup error: %s", derr)
			}
			c.processesMutex.Lock()

			_, span := oc.StartSpan(context.Background(), "newProcess::waitBackground::waitAllWaiters")
			defer span.End()
			span.AddAttributes(
				trace.StringAttribute("cid", p.cid),
				trace.Int64Attribute("pid", int64(p.pid)))

			delete(c.processes, p.pid)
			c.processesMutex.Unlock()
		}()
	}()
	return p
}

// Kill sends 'signal' to the process.
//
// If the process has already exited returns `gcserr.HrErrNotFound` by contract.
func (p *containerProcess) Kill(_ context.Context, signal syscall.Signal) error {
	if err := syscall.Kill(int(p.pid), signal); err != nil {
		if errors.Is(err, syscall.ESRCH) {
			return gcserr.NewHresultError(gcserr.HrErrNotFound)
		}
		return err
	}

	if p.init {
		p.c.setExitType(signal)
	}

	return nil
}

func (p *containerProcess) Pid() int {
	return int(p.pid)
}

// ResizeConsole resizes the tty to `height`x`width` for the process.
func (p *containerProcess) ResizeConsole(_ context.Context, height, width uint16) error {
	tty := p.process.Tty()
	if tty == nil {
		return fmt.Errorf("pid: %d, is not a tty and cannot be resized", p.pid)
	}
	return tty.ResizeConsole(height, width)
}

// Wait returns a channel that can be used to wait for the process to exit and
// gather the exit code. The second channel must be signaled from the caller
// when the caller has completed its use of this call to Wait.
func (p *containerProcess) Wait() (<-chan int, chan<- bool) {
	ctx, span := oc.StartSpan(context.Background(), "opengcs::containerProcess::Wait")
	span.AddAttributes(
		trace.StringAttribute("cid", p.cid),
		trace.Int64Attribute("pid", int64(p.pid)))

	exitCodeChan := make(chan int, 1)
	doneChan := make(chan bool)

	// Increment our waiters for this waiter
	p.writersSyncRoot.Lock()
	p.writersWg.Add(1)
	p.writersSyncRoot.Unlock()

	go func() {
		bgExitCodeChan := make(chan int, 1)
		go func() {
			p.exitWg.Wait()
			bgExitCodeChan <- p.exitCode
		}()

		// Wait for the exit code or the caller to stop waiting.
		select {
		case exitCode := <-bgExitCodeChan:
			exitCodeChan <- exitCode

			// The caller got the exit code. Wait for them to tell us they have
			// issued the write
			<-doneChan
			p.writersSyncRoot.Lock()
			// Decrement this waiter
			log.G(ctx).Debug("wait completed, releasing wait count")

			p.writersWg.Done()
			if !p.writersCalled {
				// We have at least 1 response for the exit code for this
				// process. Decrement the release waiter that will free the
				// process resources when the writersWg hits 0
				log.G(ctx).Debug("first wait completed, releasing first wait count")

				p.writersCalled = true
				p.writersWg.Done()
			}
			p.writersSyncRoot.Unlock()
			span.End()

		case <-doneChan:
			// In this case the caller timed out before the process exited. Just
			// decrement the waiter but since no exit code we just deal with our
			// waiter.
			p.writersSyncRoot.Lock()
			log.G(ctx).Debug("wait canceled before exit, releasing wait count")

			p.writersWg.Done()
			p.writersSyncRoot.Unlock()
			span.End()
		}
	}()
	return exitCodeChan, doneChan
}

func newExternalProcess(ctx context.Context, cmd *exec.Cmd, tty *stdio.TtyRelay, onRemove func(pid int)) (*externalProcess, error) {
	ep := &externalProcess{
		cmd:       cmd,
		tty:       tty,
		waitBlock: make(chan struct{}),
		remove:    onRemove,
	}
	if err := cmd.Start(); err != nil {
		return nil, errors.Wrap(err, "failed to call Start for external process")
	}
	if tty != nil {
		tty.Start()
	}
	go func() {
		_ = cmd.Wait()
		ep.exitCode = cmd.ProcessState.ExitCode()
		log.G(ctx).WithFields(logrus.Fields{
			"pid":      cmd.Process.Pid,
			"exitCode": ep.exitCode,
		}).Debug("external process exited")
		if ep.tty != nil {
			ep.tty.Wait()
		}
		close(ep.waitBlock)
	}()
	return ep, nil
}

type externalProcess struct {
	cmd *exec.Cmd
	tty *stdio.TtyRelay

	waitBlock chan struct{}
	exitCode  int

	removeOnce sync.Once
	remove     func(pid int)
}

var _ Process = &externalProcess{}

func (ep *externalProcess) Kill(_ context.Context, signal syscall.Signal) error {
	if err := syscall.Kill(ep.cmd.Process.Pid, signal); err != nil {
		if errors.Is(err, syscall.ESRCH) {
			return gcserr.NewHresultError(gcserr.HrErrNotFound)
		}
		return err
	}
	return nil
}

func (ep *externalProcess) Pid() int {
	return ep.cmd.Process.Pid
}

func (ep *externalProcess) ResizeConsole(_ context.Context, height, width uint16) error {
	if ep.tty == nil {
		return fmt.Errorf("pid: %d, is not a tty and cannot be resized", ep.cmd.Process.Pid)
	}
	return ep.tty.ResizeConsole(height, width)
}

func (ep *externalProcess) Wait() (<-chan int, chan<- bool) {
	_, span := oc.StartSpan(context.Background(), "opengcs::externalProcess::Wait")
	span.AddAttributes(trace.Int64Attribute("pid", int64(ep.cmd.Process.Pid)))

	exitCodeChan := make(chan int, 1)
	doneChan := make(chan bool)

	go func() {
		defer close(exitCodeChan)

		// Wait for the exit code or the caller to stop waiting.
		select {
		case <-ep.waitBlock:
			// Process exited send the exit code and wait for caller to close.
			exitCodeChan <- ep.exitCode
			<-doneChan
			// At least one waiter was successful, remove this external process.
			ep.removeOnce.Do(func() {
				ep.remove(ep.cmd.Process.Pid)
			})
		case <-doneChan:
			// Caller closed early, do nothing.
		}
	}()
	return exitCodeChan, doneChan
}
