// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT

//go:build linux
// +build linux

package user

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"os/user"
	"strconv"
	"strings"

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

type ExecUser struct {
	Uid  int
	Gid  int
	Home string
	Gids []int
}

func containsUser(users []string, match string) bool {
	for _, user := range users {
		if user == match {
			return true
		}
	}
	return false
}

func getGroupIds(user, filePath string) ([]int, error) {
	file, err := os.Open(filePath)
	if err != nil {
		log.Printf("E! Failed to open group file: %v", err)
		return nil, err
	}
	defer file.Close()
	reader := bufio.NewReader(file)
	groupIds := []int{}
	for {
		var wholeLine []byte
		for {
			line, isPrefix, err := reader.ReadLine()

			if err != nil {
				// EOF reached
				if err == io.EOF {
					return groupIds, nil
				}
				return nil, err
			}

			// Whole line was able to fit in single buffer
			if !isPrefix && len(wholeLine) == 0 {
				wholeLine = line
				break
			}
			wholeLine = append(wholeLine, line...)
			// Last fragment of line read
			if !isPrefix {
				break
			}
		}

		wholeLine = bytes.TrimSpace(wholeLine)
		// Not empty, not a comment, and has enough parts
		if len(wholeLine) == 0 || wholeLine[0] == '#' || bytes.Count(wholeLine, []byte{':'}) < 3 {
			continue
		}
		parts := strings.SplitN(string(wholeLine), ":", 4)
		users := strings.Split(parts[3], ",")
		if len(users) == 0 || !containsUser(users, user) {
			continue
		}
		groupId, err := strconv.Atoi(parts[2])
		if err != nil {
			return nil, err
		}
		groupIds = append(groupIds, groupId)
	}
}

func toExecUser(u *user.User) (*ExecUser, error) {
	uid, err := strconv.Atoi(u.Uid)
	if err != nil {
		log.Printf("E! Failed to convert uid to int: %v", err)
		return nil, err
	}
	gid, err := strconv.Atoi(u.Gid)
	if err != nil {
		log.Printf("E! Failed to convert gid to int: %v", err)
		return nil, err
	}
	gids, err := getGroupIds(u.Username, "/etc/group")
	if err != nil {
		log.Printf("E! Failed to get group IDs: %v", err)
		return nil, err
	}
	return &ExecUser{Uid: uid, Gid: gid, Home: u.HomeDir, Gids: gids}, nil
}

func switchUser(execUser *ExecUser) error {
	if err := unix.Setgroups(execUser.Gids); err != nil {
		log.Printf("E! Failed to set groups: %v", err)
		return err
	}

	if err := setGid(execUser.Gid); err != nil {
		log.Printf("E! Failed to set gid: %v", err)
		return err
	}

	if err := setUid(execUser.Uid); err != nil {
		log.Printf("E! Failed to set uid: %v", err)
		return err
	}

	if err := os.Setenv("HOME", execUser.Home); err != nil {
		log.Printf("E! Failed to set HOME: %v", err)
		return err
	}
	log.Printf("I! Set HOME: %v", execUser.Home)

	return nil
}

func getRunAsExecUser(runasuser string) (*ExecUser, error) {
	newUser, err := user.Lookup(runasuser)
	if err != nil {
		log.Printf("E! Failed to get newUser: %v", err)
		return nil, err
	}
	return toExecUser(newUser)
}

func ChangeUser(runAsUser string) (string, error) {
	if runAsUser == "" {
		runAsUser = "root"
	}

	execUser, err := getRunAsExecUser(runAsUser)
	if err != nil {
		log.Printf("E! Failed to getRunAsExecUser: %v", err)
		return runAsUser, err
	}

	if err := changeFileOwner(execUser.Uid, execUser.Gid); err != nil {
		return runAsUser, fmt.Errorf("error change ownership of dirs: %w", err)
	}

	if runAsUser == "root" {
		return "root", nil
	}

	if err := switchUser(execUser); err != nil {
		log.Printf("E! failed switching to %q: %v", runAsUser, err)
		return runAsUser, err
	}

	return runAsUser, nil
}
