//go:build windows

package jobcontainers

import (
	"context"
	"fmt"
	"strings"
	"unsafe"

	"github.com/Microsoft/go-winio/pkg/guid"
	"github.com/Microsoft/hcsshim/internal/log"
	"github.com/Microsoft/hcsshim/internal/winapi"
	"github.com/pkg/errors"
	"golang.org/x/sys/windows"
)

func randomPswd() (*uint16, error) {
	g, err := guid.NewV4()
	if err != nil {
		return nil, err
	}
	return windows.UTF16PtrFromString(g.String())
}

func groupExists(groupName string) bool {
	var p *byte
	if err := winapi.NetLocalGroupGetInfo(
		"",
		groupName,
		1,
		&p,
	); err != nil {
		return false
	}
	defer func() {
		_ = windows.NetApiBufferFree(p)
	}()
	return true
}

// makeLocalAccount creates a local account with the passed in username and a randomly generated password.
// The user specified by `user` will added to the `groupName`. This function does not check if groupName exists, that must be handled
// the caller.
func makeLocalAccount(ctx context.Context, user, groupName string) (_ *uint16, err error) {
	// Create a local account with a random password
	pswd, err := randomPswd()
	if err != nil {
		return nil, fmt.Errorf("failed to generate random password: %w", err)
	}

	userUTF16, err := windows.UTF16PtrFromString(user)
	if err != nil {
		return nil, fmt.Errorf("failed to encode username to UTF16: %w", err)
	}

	usr1 := &winapi.UserInfo1{
		Name:     userUTF16,
		Password: pswd,
		Priv:     winapi.USER_PRIV_USER,
		Flags:    winapi.UF_NORMAL_ACCOUNT | winapi.UF_DONT_EXPIRE_PASSWD,
	}
	if err := winapi.NetUserAdd(
		"",
		1,
		(*byte)(unsafe.Pointer(usr1)),
		nil,
	); err != nil {
		return nil, fmt.Errorf("failed to create user %s: %w", user, err)
	}
	defer func() {
		if err != nil {
			_ = winapi.NetUserDel("", user)
		}
	}()

	log.G(ctx).WithField("username", user).Debug("Created local user account for job container")

	sid, _, _, err := windows.LookupSID("", user)
	if err != nil {
		return nil, fmt.Errorf("failed to lookup SID for user %q: %w", user, err)
	}

	sids := []winapi.LocalGroupMembersInfo0{{Sid: sid}}
	if err := winapi.NetLocalGroupAddMembers(
		"",
		groupName,
		0,
		(*byte)(unsafe.Pointer(&sids[0])),
		1,
	); err != nil {
		return nil, fmt.Errorf("failed to add user %q to the %q group: %w", user, groupName, err)
	}

	return pswd, nil
}

// processToken verifies first whether userOrGroup is a username or group name. If it's a valid group name,
// a temporary local user account will be created and added to the group and then the token for the user will
// be returned. If it is not a group name then the user will logged into and the token will be returned.
func (c *JobContainer) processToken(ctx context.Context, userOrGroup string) (windows.Token, error) {
	var (
		domain   string
		userName string
		token    windows.Token
	)

	if userOrGroup == "" {
		return 0, errors.New("empty username or group name passed")
	}

	if groupExists(userOrGroup) {
		userName = c.id[:winapi.UserNameCharLimit]
		pswd, err := makeLocalAccount(ctx, userName, userOrGroup)
		if err != nil {
			return 0, fmt.Errorf("failed to create local account for container: %w", err)
		}
		if err := winapi.LogonUser(
			windows.StringToUTF16Ptr(userName),
			nil,
			pswd,
			winapi.LOGON32_LOGON_INTERACTIVE,
			winapi.LOGON32_PROVIDER_DEFAULT,
			&token,
		); err != nil {
			return 0, fmt.Errorf("failed to logon user: %w", err)
		}
		c.localUserAccount = userName
		return token, nil
	}

	// Must be a user string, split it by domain and username
	split := strings.Split(userOrGroup, "\\")
	if len(split) == 2 {
		domain = split[0]
		userName = split[1]
	} else if len(split) == 1 {
		userName = split[0]
	} else {
		return 0, fmt.Errorf("invalid user string `%s`", userOrGroup)
	}

	logonType := winapi.LOGON32_LOGON_INTERACTIVE
	// User asking to run as a local system account (NETWORK SERVICE, LOCAL SERVICE, SYSTEM)
	if domain == "NT AUTHORITY" {
		logonType = winapi.LOGON32_LOGON_SERVICE
	}

	if err := winapi.LogonUser(
		windows.StringToUTF16Ptr(userName),
		windows.StringToUTF16Ptr(domain),
		nil,
		logonType,
		winapi.LOGON32_PROVIDER_DEFAULT,
		&token,
	); err != nil {
		return 0, fmt.Errorf("failed to logon user: %w", err)
	}
	return token, nil
}

func openCurrentProcessToken() (windows.Token, error) {
	var token windows.Token
	if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_ALL_ACCESS, &token); err != nil {
		return 0, errors.Wrap(err, "failed to open current process token")
	}
	return token, nil
}
