//go:build windows

package csplugin

import (
	"fmt"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"reflect"
	"strings"
	"syscall"
	"unsafe"

	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
	"golang.org/x/sys/windows"
)

var (
	advapi32 = syscall.NewLazyDLL("advapi32.dll")

	procGetAce = advapi32.NewProc("GetAce")
)

type AclSizeInformation struct {
	AceCount      uint32
	AclBytesInUse uint32
	AclBytesFree  uint32
}

type Acl struct {
	AclRevision uint8
	Sbz1        uint8
	AclSize     uint16
	AceCount    uint16
	Sbz2        uint16
}

type AccessAllowedAce struct {
	AceType    uint8
	AceFlags   uint8
	AceSize    uint16
	AccessMask uint32
	SidStart   uint32
}

const ACCESS_ALLOWED_ACE_TYPE = 0
const ACCESS_DENIED_ACE_TYPE = 1

func CheckPerms(path string) error {
	log.Debugf("checking permissions of %s\n", path)

	systemSid, err := windows.CreateWellKnownSid(windows.WELL_KNOWN_SID_TYPE(windows.WinLocalSystemSid))
	if err != nil {
		return errors.Wrap(err, "while creating SYSTEM well known sid")
	}

	adminSid, err := windows.CreateWellKnownSid(windows.WELL_KNOWN_SID_TYPE(windows.WinBuiltinAdministratorsSid))
	if err != nil {
		return errors.Wrap(err, "while creating built-in Administrators well known sid")
	}

	currentUser, err := user.Current()
	if err != nil {
		return errors.Wrap(err, "while getting current user")
	}

	currentUserSid, _, _, err := windows.LookupSID("", currentUser.Username)

	if err != nil {
		return errors.Wrap(err, "while looking up current user sid")
	}

	sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION)
	if err != nil {
		return errors.Wrap(err, "while getting owner security info")
	}
	if !sd.IsValid() {
		return errors.New("security descriptor is invalid")
	}
	owner, _, err := sd.Owner()
	if err != nil {
		return errors.Wrap(err, "while getting owner")
	}
	if !owner.IsValid() {
		return errors.New("owner is invalid")
	}

	if !owner.Equals(systemSid) && !owner.Equals(currentUserSid) && !owner.Equals(adminSid) {
		return fmt.Errorf("plugin at %s is not owned by SYSTEM, Administrators or by current user, but by %s", path, owner.String())
	}

	dacl, _, err := sd.DACL()
	if err != nil {
		return errors.Wrap(err, "while getting DACL")
	}

	if dacl == nil {
		return fmt.Errorf("no DACL found on plugin, meaning fully permissive access on plugin %s", path)
	}

	if err != nil {
		return errors.Wrap(err, "while looking up current user sid")
	}

	rs := reflect.ValueOf(dacl).Elem()

	/*
			For reference, the structure of the ACL type is:
			type ACL struct {
			aclRevision byte
			sbz1        byte
			aclSize     uint16
			aceCount    uint16
			sbz2        uint16
		}
		As the field are not exported, we have to use reflection to access them, this should not be an issue as the structure won't (probably) change any time soon.
	*/
	aceCount := rs.Field(3).Uint()

	for i := uint64(0); i < aceCount; i++ {
		ace := &AccessAllowedAce{}
		ret, _, _ := procGetAce.Call(uintptr(unsafe.Pointer(dacl)), uintptr(i), uintptr(unsafe.Pointer(&ace)))
		if ret == 0 {
			return errors.Wrap(windows.GetLastError(), "while getting ACE")
		}
		log.Debugf("ACE %d: %+v\n", i, ace)

		if ace.AceType == ACCESS_DENIED_ACE_TYPE {
			continue
		}
		aceSid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))

		if aceSid.Equals(systemSid) || aceSid.Equals(adminSid) {
			log.Debugf("Not checking permission for well-known SID %s", aceSid.String())
			continue
		}

		if aceSid.Equals(currentUserSid) {
			log.Debugf("Not checking permission for current user %s", currentUser.Username)
			continue
		}

		log.Debugf("Checking permission for SID %s", aceSid.String())
		denyMask := ^(windows.FILE_GENERIC_READ | windows.FILE_GENERIC_EXECUTE)
		if ace.AccessMask&uint32(denyMask) != 0 {
			return fmt.Errorf("only SYSTEM, Administrators or the user currently running crowdsec can have more than read/execute on plugin %s", path)
		}
	}

	return nil
}

func getProcessAtr() (*syscall.SysProcAttr, error) {
	var procToken, token windows.Token

	proc := windows.CurrentProcess()
	defer windows.CloseHandle(proc)

	err := windows.OpenProcessToken(proc, windows.TOKEN_DUPLICATE|windows.TOKEN_ADJUST_DEFAULT|
		windows.TOKEN_QUERY|windows.TOKEN_ASSIGN_PRIMARY|windows.TOKEN_ADJUST_GROUPS|windows.TOKEN_ADJUST_PRIVILEGES, &procToken)
	if err != nil {
		return nil, errors.Wrapf(err, "while opening process token")
	}
	defer procToken.Close()

	err = windows.DuplicateTokenEx(procToken, 0, nil, windows.SecurityImpersonation,
		windows.TokenPrimary, &token)
	if err != nil {
		return nil, errors.Wrapf(err, "while duplicating token")
	}

	//Remove all privileges from the token

	err = windows.AdjustTokenPrivileges(token, true, nil, 0, nil, nil)

	if err != nil {
		return nil, errors.Wrapf(err, "while adjusting token privileges")
	}

	//Run the plugin as a medium integrity level process
	//For some reasons, low level integrity don't work, the plugin and crowdsec cannot communicate over the TCP socket
	sid, err := windows.CreateWellKnownSid(windows.WELL_KNOWN_SID_TYPE(windows.WinMediumLabelSid))
	if err != nil {
		return nil, err
	}

	tml := &windows.Tokenmandatorylabel{}
	tml.Label.Attributes = windows.SE_GROUP_INTEGRITY
	tml.Label.Sid = sid

	err = windows.SetTokenInformation(token, windows.TokenIntegrityLevel,
		(*byte)(unsafe.Pointer(tml)), tml.Size())
	if err != nil {
		token.Close()
		return nil, errors.Wrapf(err, "while setting token information")
	}

	return &windows.SysProcAttr{
		CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
		Token:         syscall.Token(token),
	}, nil
}

func (pb *PluginBroker) CreateCmd(binaryPath string) (*exec.Cmd, error) {
	var err error
	cmd := exec.Command(binaryPath)
	cmd.SysProcAttr, err = getProcessAtr()
	if err != nil {
		return nil, errors.Wrap(err, "while getting process attributes")
	}
	return cmd, err
}

func getPluginTypeAndSubtypeFromPath(path string) (string, string, error) {
	pluginFileName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(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 pluginIsValid(path string) error {
	var err error

	// check if it exists
	if _, err = os.Stat(path); err != nil {
		return errors.Wrap(err, fmt.Sprintf("plugin at %s does not exist", path))
	}

	// check if it is owned by root
	err = CheckPerms(path)
	if err != nil {
		return err
	}

	return nil
}
