// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.

package resource

import (
	"context"
	"fmt"
	"gopkg.in/yaml.v3"
_	"log/slog"
	"net/url"
_	"os"
	"os/exec"
	"os/user"
	"io"
	"strings"
	"encoding/json"
	"errors"
	"gitea.rosskeen.house/rosskeen.house/machine"
)

type decodeUser User

type UserType string

const (
	UserTypeAddUser = "adduser"
	UserTypeUserAdd = "useradd"
)

type User struct {
	Name       string   `json:"name" yaml:"name"`
	UID        string   `json:"uid,omitempty" yaml:"uid,omitempty"`
	Group      string   `json:"group,omitempty" yaml:"group,omitempty"`
	Groups     []string `json:"groups,omitempty" yaml:"groups,omitempty"`
	Gecos      string   `json:"gecos,omitempty" yaml:"gecos,omitempty"`
	Home       string   `json:"home" yaml:"home"`
	CreateHome bool     `json:"createhome,omitempty" yaml:"createhome,omitempty"`
	Shell      string   `json:"shell,omitempty" yaml:"shell,omitempty"`
	UserType   UserType `json:"-" yaml:"-"`

	CreateCommand *Command `json:"-" yaml:"-"`
	ReadCommand *Command   `json:"-" yaml:"-"`
	UpdateCommand *Command `json:"-" yaml:"-"`
	DeleteCommand *Command `json:"-" yaml:"-"`
	State string           `json:"state,omitempty" yaml:"state,omitempty"`
}

func NewUser() *User {
	return &User{ CreateHome: true }
}

func init() {
	ResourceTypes.Register("user", func(u *url.URL) Resource {
		user := NewUser()
		user.Name = u.Hostname()
		user.UID = LookupUIDString(u.Hostname())
		if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil {
			user.UserType = UserTypeAddUser
		}
		if _, pathErr := exec.LookPath("useradd"); pathErr == nil {
			user.UserType = UserTypeUserAdd
		}
		user.CreateCommand, user.ReadCommand, user.UpdateCommand, user.DeleteCommand = user.UserType.NewCRUD()
		return user
	})
}

func (u *User) Clone() Resource {
	newu := &User {
		Name: u.Name,
		UID: u.UID,
		Group: u.Group,
		Groups: u.Groups,
		Gecos: u.Gecos,
		Home: u.Home,
		CreateHome: u.CreateHome,
		Shell: u.Shell,
		State: u.State,
		UserType: u.UserType,
	}
	newu.CreateCommand, newu.ReadCommand, newu.UpdateCommand, newu.DeleteCommand = u.UserType.NewCRUD()
	return newu
}

func (u *User) StateMachine() machine.Stater {
	return StorageMachine()
}

func (u *User) SetURI(uri string) error {
	resourceUri, e := url.Parse(uri)
	if e == nil {
		if resourceUri.Scheme == "user" {
			u.Name = resourceUri.Hostname()
		} else {
			e = fmt.Errorf("%w: %s is not a user", ErrInvalidResourceURI, uri)
		}
	}
	return e
}

func (u *User) URI() string {
	return fmt.Sprintf("user://%s", u.Name)
}

func (u *User) ResolveId(ctx context.Context) string {
	return LookupUIDString(u.Name)
}

func (u *User) Validate() error {
	return fmt.Errorf("failed")
}

func (u *User) Apply() error {
	switch u.State {
	case "present":
		_, NoUserExists := LookupUID(u.Name)
		if NoUserExists != nil {
			cmdErr := u.Create(context.Background())
			return cmdErr
		}
	case "absent":
		cmdErr := u.Delete()
		return cmdErr
	}
	return nil
}

func (u *User) Load(r io.Reader) error {
	c := NewYAMLDecoder(r)
	return c.Decode(u)
}

func (u *User) LoadDecl(yamlResourceDeclaration string) error {
	c := NewYAMLStringDecoder(yamlResourceDeclaration)
	return c.Decode(u)
}

func (u *User) AddUserCommand(args *[]string) error {
	*args = append(*args, "-D")
	if u.Group != "" {
		*args = append(*args, "-G", u.Group)
	}
	if u.Home != "" {
		*args = append(*args, "-h", u.Home)
	}
	return nil
}

func (u *User) UserAddCommand(args *[]string) error {
	if u.Group != "" {
		*args = append(*args, "-g", u.Group)
	}
	if len(u.Groups) > 0 {
		*args = append(*args, "-G", strings.Join(u.Groups, ","))
	}
	if u.Home != "" {
		*args = append(*args, "-d", u.Home)
	}
	if u.CreateHome {
		*args = append(*args, "-m")
	}
	return nil
}

func (u *User) Type() string { return "user" }

func (u *User) Create(ctx context.Context) (error) {
	_, err := u.CreateCommand.Execute(u)
	if err != nil {
		return err
	}

	_,e := u.Read(ctx)
	return e
}

func (u *User) Read(ctx context.Context) ([]byte, error) {
	exErr := u.ReadCommand.Extractor(nil, u)
	if exErr != nil {
		u.State = "absent"
	}
	if yaml, yamlErr := yaml.Marshal(u); yamlErr != nil {
		return yaml, yamlErr
	} else {
		return yaml, exErr
	}
}

func (u *User) Delete() (error) {
	_, err := u.DeleteCommand.Execute(u)
	if err != nil {
		return err
	}

	return err
}

func (u *User) UnmarshalJSON(data []byte) error {
	if unmarshalErr := json.Unmarshal(data, (*decodeUser)(u)); unmarshalErr != nil {
		return unmarshalErr
	}
	u.CreateCommand, u.ReadCommand, u.UpdateCommand, u.DeleteCommand = u.UserType.NewCRUD()
	return nil
}

func (u *User) UnmarshalYAML(value *yaml.Node) error {
	if unmarshalErr := value.Decode((*decodeUser)(u)); unmarshalErr != nil {
		return unmarshalErr
	}
	u.CreateCommand, u.ReadCommand, u.UpdateCommand, u.DeleteCommand = u.UserType.NewCRUD()
	return nil
}

func (u *UserType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
	switch *u {
	case UserTypeUserAdd:
		return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand()
	case UserTypeAddUser:
		return NewAddUserCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewDelUserDeleteCommand()
	default:
		if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil {
			*u = UserTypeAddUser
			return NewAddUserCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewDelUserDeleteCommand()
		}
		if _, pathErr := exec.LookPath("useradd"); pathErr == nil {
			*u = UserTypeUserAdd
			return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand()
		}
		return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand()
	}
}

func (u *UserType) UnmarshalValue(value string) error {
	switch value {
	case string(UserTypeUserAdd), string(UserTypeAddUser):
		*u = UserType(value)
	return nil
	default:
		return errors.New("invalid UserType value")
	}
}

func (u *UserType) UnmarshalJSON(data []byte) error {
	var s string
	if unmarshalUserTypeErr := json.Unmarshal(data, &s); unmarshalUserTypeErr != nil {
		return unmarshalUserTypeErr
	}
	return u.UnmarshalValue(s)
}

func (u *UserType) UnmarshalYAML(value *yaml.Node) error {
	var s string
	if err := value.Decode(&s); err != nil {
		return err
	}
	return u.UnmarshalValue(s)
}

func NewUserAddCreateCommand() *Command {
	c := NewCommand()
	c.Path = "useradd"
	c.Args = []CommandArg{
		CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
		CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"),
		CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"),
		CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"),
		CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"),
		CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"),
		CommandArg("{{ .Name }}"),
	}
	c.Extractor = func(out []byte, target any) error {
		return nil
/*
		for _,line := range strings.Split(string(out), "\n") {
			if line == "iptables: Chain already exists." {
				return nil
			}
		}
		return fmt.Errorf(string(out))
*/
	}
	return c
}

func NewAddUserCreateCommand() *Command {
	c := NewCommand()
	c.Path = "adduser"
	c.Args = []CommandArg{
		CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
		CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
		CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
		CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"),
		CommandArg("{{ if not .CreateHome }}-H{{ end }}"),
		CommandArg("-D"),
		CommandArg("{{ .Name }}"),
	}
	c.Extractor = func(out []byte, target any) error {
		return nil
/*
		for _,line := range strings.Split(string(out), "\n") {
			if line == "iptables: Chain already exists." {
				return nil
			}
		}
		return fmt.Errorf(string(out))
*/
	}
	return c
}

func NewUserReadCommand() *Command {
	c := NewCommand()
	c.Extractor = func(out []byte, target any) error {
		u := target.(*User)
		u.State = "absent"
		var readUser *user.User
		var e error
		if u.Name != "" {
			readUser, e = user.Lookup(u.Name)
		} else {
			if u.UID != "" {
				readUser, e = user.LookupId(u.UID)
			}
		}

		if e == nil {
			u.Name = readUser.Username
			u.UID = readUser.Uid
			u.Home = readUser.HomeDir
			u.Gecos = readUser.Name
			if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil {
				u.Group = readGroup.Name
			} else {
				return groupErr
			}
			if u.UID != "" {
				u.State = "present"
			}
		}
		return e
	}
	return c
}

func NewUserUpdateCommand() *Command {
	return nil
}

func NewUserDelDeleteCommand() *Command {
	c := NewCommand()
	c.Path = "userdel"
	c.Args = []CommandArg{
		CommandArg("{{ .Name }}"),
	}
	c.Extractor = func(out []byte, target any) error {
		return nil
	}
	return c
}

func NewDelUserDeleteCommand() *Command {
	c := NewCommand()
	c.Path = "deluser"
	c.Args = []CommandArg{
		CommandArg("{{ .Name }}"),
	}
	c.Extractor = func(out []byte, target any) error {
		return nil
	}
	return c
}
