//go:build linux
// +build linux

package runc

import (
	"encoding/json"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"

	"github.com/opencontainers/runc/libcontainer"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"

	"github.com/Microsoft/hcsshim/internal/guest/runtime"
)

// readPidFile reads the integer pid stored in the given file.
func (r *runcRuntime) readPidFile(pidFile string) (pid int, err error) {
	data, err := os.ReadFile(pidFile)
	if err != nil {
		return -1, errors.Wrap(err, "failed reading from pid file")
	}
	pid, err = strconv.Atoi(string(data))
	if err != nil {
		return -1, errors.Wrapf(err, "failed converting pid text %q to integer form", data)
	}
	return pid, nil
}

// cleanupContainer cleans up any state left behind by the container.
func (r *runcRuntime) cleanupContainer(id string) error {
	containerDir := r.getContainerDir(id)
	if err := os.RemoveAll(containerDir); err != nil {
		return errors.Wrapf(err, "failed removing the container directory for container %s", id)
	}
	return nil
}

// cleanupProcess cleans up any state left behind by the process.
func (r *runcRuntime) cleanupProcess(id string, pid int) error {
	processDir := r.getProcessDir(id, pid)
	if err := os.RemoveAll(processDir); err != nil {
		return errors.Wrapf(err, "failed removing the process directory for process %d in container %s", pid, id)
	}
	return nil
}

// getProcessDir returns the path to the state directory of the given process.
func (r *runcRuntime) getProcessDir(id string, pid int) string {
	containerDir := r.getContainerDir(id)
	return filepath.Join(containerDir, strconv.Itoa(pid))
}

// getContainerDir returns the path to the state directory of the given
// container.
func (*runcRuntime) getContainerDir(id string) string {
	return filepath.Join(containerFilesDir, id)
}

// makeContainerDir creates the state directory for the given container.
func (r *runcRuntime) makeContainerDir(id string) error {
	dir := r.getContainerDir(id)
	if err := os.MkdirAll(dir, os.ModeDir); err != nil {
		return errors.Wrapf(err, "failed making container directory for container %s", id)
	}
	return nil
}

// makeLogDir ensures the runc log directory exists.
func makeLogDir(bundlePath string) error {
	if err := os.MkdirAll(bundlePath, os.ModeDir); err != nil {
		return errors.Wrapf(err, "failed making runc log directory at %s", bundlePath)
	}
	return nil
}

// getLogPath returns the path to the runc.log file for a container.
func getLogPath(bundlePath string) string {
	return filepath.Join(bundlePath, "runc.log")
}

// processExists returns true if the given process exists in /proc, false if
// not.
// It should be noted that processes which have exited, but have not yet been
// waited on (i.e. zombies) are still considered to exist by this function.
func (*runcRuntime) processExists(pid int) bool {
	_, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid)))
	return !os.IsNotExist(err)
}

// todo (helsaawy): create `type runcCmd struct` that wraps an exec.Command, runs it and parses error from
// log file or std err (similar to containerd/libcni)

type standardLogEntry struct {
	Level   logrus.Level `json:"level"`
	Message string       `json:"msg"`
	Err     error        `json:"error,omitempty"`
}

func (l *standardLogEntry) asError() (err error) {
	err = parseRuncError(l.Message)
	if l.Err != nil {
		err = errors.Wrap(err, l.Err.Error())
	}
	return
}

func parseRuncError(s string) (err error) {
	if strings.HasPrefix(s, "container") && strings.HasSuffix(s, "does not exist") {
		// match "container %q does not exist" and [libcontainer.ErrNotExist]
		err = runtime.ErrContainerDoesNotExist
	} else if strings.Contains(s, "container with id exists") || strings.Contains(s, libcontainer.ErrExist.Error()) {
		err = runtime.ErrContainerAlreadyExists
	} else if strings.Contains(s, "invalid id format") || strings.Contains(s, libcontainer.ErrInvalidID.Error()) {
		err = runtime.ErrInvalidContainerID
	} else if strings.Contains(s, "container") && strings.Contains(s, "that is not stopped") {
		err = runtime.ErrContainerNotStopped
	} else if strings.Contains(s, libcontainer.ErrRunning.Error()) {
		err = runtime.ErrContainerStillRunning
	} else if strings.Contains(s, libcontainer.ErrNotRunning.Error()) {
		err = runtime.ErrContainerNotRunning
	} else {
		err = errors.New(s)
	}
	return err
}

func getRuncLogError(logPath string) error {
	reader, err := os.OpenFile(logPath, syscall.O_RDONLY, 0644)
	if err != nil {
		return nil
	}
	defer reader.Close()

	var lastErr error
	dec := json.NewDecoder(reader)
	for {
		entry := &standardLogEntry{}
		if err := dec.Decode(entry); err != nil {
			break
		}
		if entry.Level <= logrus.ErrorLevel {
			lastErr = entry.asError()
		}
	}
	return lastErr
}

func runcCommandLog(logPath string, args ...string) *exec.Cmd {
	args = append([]string{"--log", logPath, "--log-format", "json"}, args...)
	return runcCommand(args...)
}

func runcCommand(args ...string) *exec.Cmd {
	return exec.Command("runc", args...)
}
