//go:build darwin || linux

package workspace

import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"

	"golang.org/x/sys/unix"
)

const (
	commandRootFD   = 3
	commandRecordFD = 4
	commandStatusFD = 5
	commandShimExit = 125
)

func startCommandProcess(record commandLaunchRecord, directory *os.File, output *commandOutput) (*commandProcess, error) {
	encoded, err := json.Marshal(record)
	if err != nil || len(encoded) == 0 || len(encoded) > commandLaunchRecordLimit || directory == nil {
		return nil, errCommandLaunchInvalid
	}
	rootFD, err := unix.Dup(int(directory.Fd()))
	if err != nil {
		return nil, err
	}
	root := os.NewFile(uintptr(rootFD), "workspace-root")
	recordReader, recordWriter, err := os.Pipe()
	if err != nil {
		_ = root.Close()
		return nil, err
	}
	statusReader, statusWriter, err := os.Pipe()
	if err != nil {
		_ = root.Close()
		_ = recordReader.Close()
		_ = recordWriter.Close()
		return nil, err
	}
	closeAll := func() {
		_ = root.Close()
		_ = recordReader.Close()
		_ = recordWriter.Close()
		_ = statusReader.Close()
		_ = statusWriter.Close()
	}

	currentExecutable, err := os.Executable()
	if err != nil {
		closeAll()
		return nil, err
	}
	if !filepath.IsAbs(currentExecutable) {
		closeAll()
		return nil, errCommandLaunchInvalid
	}
	cmd := exec.Command(currentExecutable, commandShimArgument)
	cmd.Env = []string{commandShimEnvironment + "=1"}
	cmd.ExtraFiles = []*os.File{root, recordReader, statusWriter}
	cmd.Stdout = output.writer(false)
	cmd.Stderr = output.writer(true)
	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	if err := cmd.Start(); err != nil {
		closeAll()
		return nil, err
	}
	_ = root.Close()
	_ = recordReader.Close()
	_ = statusWriter.Close()

	go func() {
		_, _ = io.Copy(recordWriter, bytes.NewReader(encoded))
		_ = recordWriter.Close()
	}()
	launch := make(chan commandLaunchStatus, 1)
	go func() {
		data, readErr := io.ReadAll(io.LimitReader(statusReader, 2))
		_ = statusReader.Close()
		launch <- commandLaunchStatus{started: readErr == nil && len(data) == 0}
	}()
	wait := make(chan error, 1)
	go func() {
		wait <- cmd.Wait()
	}()
	return &commandProcess{
		wait: wait, launch: launch, pid: cmd.Process.Pid,
		exitCode: func() int32 {
			if cmd.ProcessState == nil {
				return -1
			}
			return int32(cmd.ProcessState.ExitCode())
		},
	}, nil
}

func runCommandShim() int {
	status := os.NewFile(commandStatusFD, "workspace-command-status")
	fail := func() int {
		if status != nil {
			_, _ = status.Write([]byte{'F'})
			_ = status.Close()
		}
		return commandShimExit
	}
	if status == nil {
		return commandShimExit
	}
	unix.CloseOnExec(commandStatusFD)

	recordFile := os.NewFile(commandRecordFD, "workspace-command-record")
	root := os.NewFile(commandRootFD, "workspace-root")
	if recordFile == nil || root == nil {
		return fail()
	}
	defer recordFile.Close()
	defer root.Close()
	encoded, err := io.ReadAll(io.LimitReader(recordFile, commandLaunchRecordLimit+1))
	if err != nil || len(encoded) == 0 || len(encoded) > commandLaunchRecordLimit {
		return fail()
	}
	decoder := json.NewDecoder(bytes.NewReader(encoded))
	decoder.DisallowUnknownFields()
	var record commandLaunchRecord
	if err := decoder.Decode(&record); err != nil {
		return fail()
	}
	if err := ensureJSONEOF(decoder); err != nil || !validLaunchRecord(record) {
		return fail()
	}
	var stat unix.Stat_t
	if err := unix.Fstat(commandRootFD, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != record.Device || uint64(stat.Ino) != record.Inode {
		return fail()
	}
	if err := unix.Fchdir(commandRootFD); err != nil {
		return fail()
	}
	_ = recordFile.Close()
	_ = root.Close()
	argv := make([]string, 1, len(record.Args)+1)
	argv[0] = record.Executable
	argv = append(argv, record.Args...)
	if err := unix.Exec(record.Executable, argv, record.Environment); err != nil {
		return fail()
	}
	return commandShimExit
}

func ensureJSONEOF(decoder *json.Decoder) error {
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		if err == nil {
			return errors.New("workspace command record has trailing data")
		}
		return err
	}
	return nil
}

func validLaunchRecord(record commandLaunchRecord) bool {
	if record.Version != commandLaunchVersion || !filepath.IsAbs(record.Executable) || filepath.Clean(record.Executable) != record.Executable || strings.IndexByte(record.Executable, 0) >= 0 {
		return false
	}
	for _, arg := range record.Args {
		if strings.IndexByte(arg, 0) >= 0 {
			return false
		}
	}
	seen := make(map[string]struct{}, len(record.Environment))
	for _, item := range record.Environment {
		name, value, ok := strings.Cut(item, "=")
		if !ok || !validEnvironmentName(name) || name == commandShimEnvironment || strings.IndexByte(value, 0) >= 0 {
			return false
		}
		if _, duplicate := seen[name]; duplicate {
			return false
		}
		seen[name] = struct{}{}
	}
	return true
}

func terminateProcessGroup(pid int) {
	if pid <= 0 {
		return
	}
	_ = syscall.Kill(-pid, syscall.SIGTERM)
	_ = syscall.Kill(-pid, syscall.SIGKILL)
}
