package user

import (
	"fmt"
	"os"
	"os/exec"

	"github.com/go-ldap/ldap/v3"
	"github.com/spf13/cobra"
	"gitlab.c3sl.ufpr.br/tss24/useradm/internal/auth"
	"gitlab.c3sl.ufpr.br/tss24/useradm/internal/entities/opts"
	"gitlab.c3sl.ufpr.br/tss24/useradm/internal/entities/user"
	"gitlab.c3sl.ufpr.br/tss24/useradm/internal/manage"
	"gitlab.c3sl.ufpr.br/tss24/useradm/internal/validate"
	"gitlab.c3sl.ufpr.br/tss24/useradm/pkg/utils"
	"gopkg.in/yaml.v3"
)

type cfg struct {
	Name   string `yaml:"Name"`
	GRR    string `yaml:"GRR"`
	Group  string `yaml:"Group"`
	Status string `yaml:"Status"`
	Shell  string `yaml:"Shell"`
	Resp   string `yaml:"Resp"`
	Expiry string `yaml:"Expiry"`
}

var ModCmd = &cobra.Command{
	Use:   "mod [username]",
	Short: "Modify user information",
	Long: `Opens a file for editing the users config. 
Uses $EDITOR variable, if it is not set, use vim`,
	Args: cobra.ExactArgs(1),
	RunE: modUserCmd,
}

func init() {
	ModCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
}

func modUserCmd(cmd *cobra.Command, args []string) error {
	var opts opts.Opts

	l, err := auth.ConnLDAP()
	if err != nil {
		return err
	}
	defer l.Close()

	users, err := manage.GetAllUsers(l)
	if err != nil {
		return err
	}

	groups, err := auth.GetAllGroupsLDAP(l)
	if err != nil {
		return err
	}

	err = opts.RetrieveOpts(cmd)
	if err != nil {
		return err
	}

	curr, err := manage.Locate(l, args[0])
	if err != nil {
		return err
	}

	state := cfg{
		Name:   curr.Name,
		GRR:    curr.GRR,
		Group:  curr.GID,
		Status: curr.Status,
		Shell:  curr.Shell,
		Resp:   curr.Resp,
		Expiry: curr.Expiry,
	}

	changes, err := promptUserYaml(state)

	if changes.GRR != curr.GRR {
		err := validate.GRR(users, changes.GRR)
		if err != nil {
			return err
		}
	}

	err = validate.GroupUnique(groups, changes.Group)
	if err != nil {
		return err
	}

	err = validate.Status(changes.Status)
	if err != nil {
		return err
	}

	err = validate.Expiry(changes.Expiry)
	if err != nil {
		return err
	}

	oldGroup := curr.GID

	req, err := genRequest(groups, curr, changes)
	if err != nil {
		return err
	}

	fmt.Printf("%v\n\n", curr.ToString())
	utils.ConfirmationPrompt(opts.Confirm, "user update")

	if err := l.Modify(req); err != nil {
		return fmt.Errorf("Failed to update user attributes: %v", err)
	}

	if oldGroup != changes.Group {
		if err := auth.DelFromGroupLDAP(l, curr.UID, curr.GID); err != nil {
			return err
		}

		if err := auth.AddToGroupLDAP(l, curr.UID, changes.Group); err != nil {
			return err
		}
	}

	if err := utils.ClearCache(); err != nil {
		fmt.Printf(`Failed to reload cache!
all is ok but may take a while to apply the changes
Output: %v`, err)
	} else {
		fmt.Printf("Changes applied!\n")
	}

	return nil
}

func genRequest(groups map[string]string,
	curr *user.User, changes cfg) (*ldap.ModifyRequest, error) {

	req := ldap.NewModifyRequest(curr.DN, nil)

	// changed name
	if curr.Name != changes.Name {
		req.Replace("cn", []string{changes.Name})
	}

	// changed status
	if curr.Status != changes.Status {
		if changes.Status == "Active" && changes.Shell == "/bin/false" {
			req.Replace("loginShell", []string{"/bin/bash"})
		} else if changes.Status == "Active" {
			req.Replace("loginShell", []string{changes.Shell})
		} else {
			req.Replace("loginShell", []string{"/bin/false"})
		}
	} else {
		// only changed shell
		if curr.Shell != changes.Shell {
			req.Replace("loginShell", []string{changes.Shell})
		}
	}

	// changed base group
	if curr.GID != changes.Group {
		var gidNumber string
		for id, gid := range groups {
			if gid == changes.Group {
				gidNumber = id
				break
			}
		}

		req.Replace("gidNumber", []string{gidNumber})
	}

	gecos := curr.Gecos

	applyChangesToUser(curr, changes)

	curr.GenGecos()

	// changed gecos
	if curr.Gecos != gecos {
		req.Replace("gecos", []string{curr.Gecos})
	}

	return req, nil
}

func applyChangesToUser(c *user.User, n cfg) {
	c.GRR = n.GRR
	c.GID = n.Group
	c.Name = n.Name
	c.Resp = n.Resp
	c.Shell = n.Shell
	c.Status = n.Status
	c.Expiry = n.Expiry
}

// generates a yaml file of a config state for the user to edit
func promptUserYaml(state cfg) (cfg, error) {
	var newState cfg
	tmpFile, err := os.CreateTemp("", "config-*.yaml")
	if err != nil {
		err = fmt.Errorf("Error trying to create temp file: %v", err)
		return newState, err
	}
	defer os.Remove(tmpFile.Name())

	data, err := yaml.Marshal(state)
	if err != nil {
		return newState, fmt.Errorf("Error serializing the yaml: %v", err)
	}

	if err := os.WriteFile(tmpFile.Name(), data, 0644); err != nil {
		return newState, fmt.Errorf("Error writing to temp file: %v", err)
	}

	// abrir o editor
	editor := os.Getenv("EDITOR")
	if editor == "" {
		editor = "vim"
	}

	comd := exec.Command(editor, tmpFile.Name())
	comd.Stdin = os.Stdin
	comd.Stdout = os.Stdout
	comd.Stderr = os.Stderr

	if err := comd.Run(); err != nil {
		return newState, fmt.Errorf("Error opening the editor: %v", err)
	}

	editedData, err := os.ReadFile(tmpFile.Name())
	if err != nil {
		return newState, fmt.Errorf("Error reading the modified file: %v", err)
	}

	if err := yaml.Unmarshal(editedData, &newState); err != nil {
		return newState, fmt.Errorf("Error de-serializing the yaml: %v", err)
	}

	return newState, nil
}