package plugins

import (
	"fmt"
	"os"
	osuser "os/user"
	"sort"
	"strconv"

	"github.com/mauromorales/xpasswd/pkg/users"

	"github.com/hashicorp/go-multierror"
	"github.com/joho/godotenv"
	entities "github.com/mudler/entities/pkg/entities"
	"github.com/twpayne/go-vfs/v4"

	"github.com/rancher/yip/pkg/logger"
	"github.com/rancher/yip/pkg/schema"
)

func createUser(fs vfs.FS, u schema.User, console Console) error {
	pass := u.PasswordHash
	if u.LockPasswd {
		pass = "!"
	}

	userShadow := &entities.Shadow{
		Username:    u.Name,
		Password:    pass,
		LastChanged: "now",
	}

	etcgroup, err := fs.RawPath("/etc/group")
	if err != nil {
		return fmt.Errorf("error getting rawpath for /etc/group: %s", err.Error())
	}

	etcshadow, err := fs.RawPath("/etc/shadow")
	if err != nil {
		return fmt.Errorf("error getting rawpath for /etc/shadow: %s", err.Error())
	}

	etcpasswd, err := fs.RawPath("/etc/passwd")
	if err != nil {
		return fmt.Errorf("error getting rawpath for /etc/passwd: %s", err.Error())
	}

	useradd, err := fs.RawPath("/etc/default/useradd")
	if err != nil {
		return fmt.Errorf("error getting rawpath for /etc/default/useradd: %s", err.Error())
	}

	usrDefaults := map[string]string{}

	// Load default home and shell from `/etc/default/useradd`
	if _, err = os.Stat(useradd); err == nil {
		usrDefaults, err = godotenv.Read(useradd)
		if err != nil {
			return fmt.Errorf("failed parsing '%s'", useradd)
		}
	}

	// Set default home and shell if they are empty
	if usrDefaults["SHELL"] == "" {
		usrDefaults["SHELL"] = "/bin/sh"
	}
	if usrDefaults["HOME"] == "" {
		usrDefaults["HOME"] = "/home"
	}

	primaryGroup := u.Name

	gid := -1 // -1 instructs entities to find the next free id and assign it
	if u.PrimaryGroup != "" {
		gr, err := osuser.LookupGroup(u.PrimaryGroup)
		if err != nil {
			return fmt.Errorf("could not resolve primary group of user: %s", err.Error())
		}
		gid, _ = strconv.Atoi(gr.Gid)
		primaryGroup = u.PrimaryGroup
	}

	updateGroup := entities.Group{
		Name:     primaryGroup,
		Password: "x",
		Gid:      &gid,
		Users:    u.Name,
	}
	err = updateGroup.Apply(etcgroup, false)
	if err != nil {
		return fmt.Errorf("creating the user's group: %v", err)
	}

	// reload the group to get the generated GID
	groups, _ := entities.ParseGroup(etcgroup)
	for name, group := range groups {
		if name == updateGroup.Name {
			updateGroup = group
			gid = *group.Gid
			break
		}
	}

	uid := -1
	if u.UID != "" {
		// User defined-uid
		uid, err = strconv.Atoi(u.UID)
		if err != nil {
			return fmt.Errorf("failed parsing uid: %s", err.Error())
		}
	} else {
		list := users.NewUserList()
		list.SetPath(etcpasswd)
		list.Load()

		user := list.Get(u.Name)

		if user != nil {
			uid, err = user.UID()
			if err != nil {
				return fmt.Errorf("could not get user id: %v", err)
			}
		} else {
			// https://systemd.io/UIDS-GIDS/#special-distribution-uid-ranges
			uid, err = list.GenerateUIDInRange(entities.HumanIDMin, entities.HumanIDMax)
			if err != nil {
				return fmt.Errorf("no available uid: %v", err)
			}
		}
	}
	if uid == -1 {
		return fmt.Errorf("could not set uid for user")
	}

	if u.Homedir == "" {
		u.Homedir = fmt.Sprintf("%s/%s", usrDefaults["HOME"], u.Name)
	}

	if u.Shell == "" {
		u.Shell = usrDefaults["SHELL"]
	}

	userInfo := &entities.UserPasswd{
		Username: u.Name,
		Password: "x",
		Info:     u.GECOS,
		Homedir:  u.Homedir,
		Gid:      gid,
		Shell:    u.Shell,
		Uid:      uid,
	}

	if err := userInfo.Apply(etcpasswd, false); err != nil {
		return err
	}

	if err := userShadow.Apply(etcshadow, false); err != nil {
		return err
	}

	if !u.NoCreateHome {
		homedir, err := fs.RawPath(u.Homedir)
		if err != nil {
			return fmt.Errorf("error getting rawpath for homedir: %s", err.Error())
		}
		os.MkdirAll(homedir, 0755)
		os.Chown(homedir, uid, gid)
	}

	groups, _ = entities.ParseGroup(etcgroup)
	for name, group := range groups {
		for _, w := range u.Groups {
			if w == name {
				group.Users = u.Name
				group.Apply(etcgroup, false)
			}
		}
	}

	return nil
}

func setUserPass(fs vfs.FS, username, password string) error {
	etcshadow, err := fs.RawPath("/etc/shadow")
	if err != nil {
		return fmt.Errorf("error getting rawpath for /etc/shadow: %s", err.Error())
	}
	userShadow := &entities.Shadow{
		Username:    username,
		Password:    password,
		LastChanged: "now",
	}
	if err := userShadow.Apply(etcshadow, false); err != nil {
		return err
	}
	return nil
}

func User(l logger.Interface, s schema.Stage, fs vfs.FS, console Console) error {
	var errs error

	// Order users so they get the same UID on each run
	users := make([]string, 0, len(s.Users))

	for k := range s.Users {
		users = append(users, k)
	}
	sort.Strings(users)

	for _, k := range users {
		r := s.Users[k]
		r.Name = k
		if !r.Exists() {
			if err := createUser(fs, r, console); err != nil {
				errs = multierror.Append(errs, err)
			}
		} else if r.PasswordHash != "" {
			if err := setUserPass(fs, r.Name, r.PasswordHash); err != nil {
				return err
			}
		}

		if len(s.Users[k].SSHAuthorizedKeys) > 0 {
			SSH(l, schema.Stage{SSHKeys: map[string][]string{r.Name: r.SSHAuthorizedKeys}}, fs, console)
		}

	}
	return errs
}
