//go:build linux || freebsd || netbsd || openbsd || solaris || (!windows && !js)

package csplugin

import (
	"context"
	"errors"
	"fmt"
	"io/fs"
	"math"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
)

func CheckCredential(uid int, gid int) *syscall.SysProcAttr {
	return &syscall.SysProcAttr{
		Credential: &syscall.Credential{
			Uid: uint32(uid),
			Gid: uint32(gid),
		},
	}
}

func (pb *PluginBroker) CreateCmd(ctx context.Context, binaryPath string) (*exec.Cmd, error) {
	var err error
	cmd := exec.CommandContext(ctx, binaryPath)
	if pb.pluginProcConfig.User != "" || pb.pluginProcConfig.Group != "" {
		if pb.pluginProcConfig.User == "" || pb.pluginProcConfig.Group == "" {
			return nil, errors.New("while getting process attributes: both plugin user and group must be set")
		}
		cmd.SysProcAttr, err = getProcessAttr(pb.pluginProcConfig.User, pb.pluginProcConfig.Group)
		if err != nil {
			return nil, fmt.Errorf("while getting process attributes: %w", err)
		}
		cmd.SysProcAttr.Credential.NoSetGroups = true
	}
	return cmd, err
}

func getUID(username string) (uint32, error) {
	u, err := user.Lookup(username)
	if err != nil {
		return 0, err
	}
	uid, err := strconv.ParseInt(u.Uid, 10, 32)
	if err != nil {
		return 0, err
	}
	if uid < 0 || uid > math.MaxInt32 {
		return 0, errors.New("out of bound uid")
	}
	return uint32(uid), nil
}

func getGID(groupname string) (uint32, error) {
	g, err := user.LookupGroup(groupname)
	if err != nil {
		return 0, err
	}
	gid, err := strconv.ParseInt(g.Gid, 10, 32)
	if err != nil {
		return 0, err
	}
	if gid < 0 || gid > math.MaxInt32 {
		return 0, errors.New("out of bound gid")
	}
	return uint32(gid), nil
}

func getPluginTypeAndSubtypeFromPath(path string) (string, string, error) {
	pluginFileName := filepath.Base(path)
	parts := strings.Split(pluginFileName, "-")
	if len(parts) < 2 {
		return "", "", fmt.Errorf("plugin name %s is invalid. Name should be like {type-name}", path)
	}
	return strings.Join(parts[:len(parts)-1], "-"), parts[len(parts)-1], nil
}

func getProcessAttr(username string, groupname string) (*syscall.SysProcAttr, error) {
	uid, err := getUID(username)
	if err != nil {
		return nil, err
	}
	gid, err := getGID(groupname)
	if err != nil {
		return nil, err
	}

	return &syscall.SysProcAttr{
		Credential: &syscall.Credential{
			Uid: uid,
			Gid: gid,
		},
	}, nil
}

func pluginIsValid(path string) error {
	var details fs.FileInfo
	var err error

	// check if it exists
	if details, err = os.Stat(path); err != nil {
		return fmt.Errorf("plugin at %s does not exist: %w", path, err)
	}

	// check if it is owned by current user
	currentUser, err := user.Current()
	if err != nil {
		return fmt.Errorf("while getting current user: %w", err)
	}
	currentUID, err := getUID(currentUser.Username)
	if err != nil {
		return fmt.Errorf("while looking up the current uid: %w", err)
	}
	stat := details.Sys().(*syscall.Stat_t)
	if stat.Uid != currentUID {
		return fmt.Errorf("plugin at %s is not owned by user '%s'", path, currentUser.Username)
	}

	mode := details.Mode()
	perm := uint32(mode)
	if (perm & 0o0002) != 0 {
		return fmt.Errorf("plugin at %s is world writable, world writable plugins are invalid", path)
	}
	if (perm & 0o0020) != 0 {
		return fmt.Errorf("plugin at %s is group writable, group writable plugins are invalid", path)
	}
	if (mode & os.ModeSetgid) != 0 {
		return fmt.Errorf("plugin at %s has setgid permission, which is not allowed", path)
	}
	return nil
}
