//go:build windows

package csplugin

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

	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 fmt.Errorf("while creating SYSTEM well known sid: %w", err)
	}

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

	currentUser, err := user.Current()
	if err != nil {
		return fmt.Errorf("while getting current user: %w", err)
	}

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

	if err != nil {
		return fmt.Errorf("while looking up current user sid: %w", err)
	}

	sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION)
	if err != nil {
		return fmt.Errorf("while getting owner security info: %w", err)
	}
	if !sd.IsValid() {
		return errors.New("security descriptor is invalid")
	}
	owner, _, err := sd.Owner()
	if err != nil {
		return fmt.Errorf("while getting owner: %w", err)
	}
	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 fmt.Errorf("while getting DACL: %w", err)
	}

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

	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 := range aceCount {
		ace := &AccessAllowedAce{}
		ret, _, _ := procGetAce.Call(uintptr(unsafe.Pointer(dacl)), uintptr(i), uintptr(unsafe.Pointer(&ace)))
		if ret == 0 {
			return fmt.Errorf("while getting ACE: %w", windows.GetLastError())
		}
		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, fmt.Errorf("while opening process token: %w", err)
	}
	defer procToken.Close()

	err = windows.DuplicateTokenEx(procToken, 0, nil, windows.SecurityImpersonation,
		windows.TokenPrimary, &token)
	if err != nil {
		return nil, fmt.Errorf("while duplicating token: %w", err)
	}

	//Remove all privileges from the token

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

	if err != nil {
		return nil, fmt.Errorf("while adjusting token privileges: %w", err)
	}

	//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, fmt.Errorf("while setting token information: %w", err)
	}

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

func (pb *PluginBroker) CreateCmd(ctx context.Context, binaryPath string) (*exec.Cmd, error) {
	var err error
	cmd := exec.CommandContext(ctx, binaryPath)
	cmd.SysProcAttr, err = getProcessAtr()
	if err != nil {
		return nil, fmt.Errorf("while getting process attributes: %w", err)
	}
	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 fmt.Errorf("plugin at %s does not exist", path)
	}

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

	return nil
}
