// Copyright 2010 Jonas mg
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package user

import (
	"fmt"
	"os"
	"path"
	"reflect"
	"strconv"
	"strings"
)

type userField int

// Field names for user database.
const (
	U_NAME userField = 1 << iota
	U_PASSWD
	U_UID
	U_GID
	U_GECOS
	U_DIR
	U_SHELL

	U_ALL // To get lines without searching into a field.
)

func (f userField) String() string {
	switch f {
	case U_NAME:
		return "Name"
	case U_PASSWD:
		return "Passwd"
	case U_UID:
		return "UID"
	case U_GID:
		return "GID"
	case U_GECOS:
		return "GECOS"
	case U_DIR:
		return "Dir"
	case U_SHELL:
		return "Shell"
	}
	return "ALL"
}

// An User represents an user account.
type User struct {
	// Login name. (Unique)
	Name string

	// Optional hashed password
	//
	// The hashed password field may be blank, in which case no password is
	// required to authenticate as the specified login name. However, some
	// applications which read the '/etc/passwd' file may decide not to permit
	// any access at all if the password field is blank. If the password field
	// is a lower-case "x", then the encrypted password is actually stored in
	// the "shadow(5)" file instead; there must be a corresponding line in the
	// '/etc/shadow' file, or else the user account is invalid. If the password
	// field is any other string, then it will be treated as an hashed password,
	// as specified by "crypt(3)".
	password string

	// Numerical user ID. (Unique)
	UID int

	// Numerical group ID
	GID int

	// User name or comment field
	//
	// The comment field is used by various system utilities, such as "finger(1)".
	Gecos string

	// User home directory
	//
	// The home directory field provides the name of the initial working
	// directory. The login program uses this information to set the value of
	// the $HOME environmental variable.
	Dir string

	// Optional user command interpreter
	//
	// The command interpreter field provides the name of the user's command
	// language interpreter, or the name of the initial program to execute.
	// The login program uses this information to set the value of the "$SHELL"
	// environmental variable. If this field is empty, it defaults to the value
	// "/bin/sh".
	Shell string

	addSystemUser bool
}

// NewUser returns a new User with both fields "Dir" and "Shell" got from
// the system configuration.
func NewUser(name string, gid int) *User {
	loadConfig()

	return &User{
		Name:  name,
		Dir:   path.Join(config.useradd.HOME, name),
		Shell: config.useradd.SHELL,
		UID:   -1,
		GID:   gid,
	}
}

// NewSystemUser returns a new system user.
func NewSystemUser(name, homeDir string, gid int) *User {
	return &User{
		Name:  name,
		Dir:   homeDir,
		Shell: "/bin/false",
		UID:   -1,
		GID:   gid,

		addSystemUser: true,
	}
}

func (u *User) filename() string { return fileUser }

// IsOfSystem indicates whether it is a system user.
func (u *User) IsOfSystem() bool {
	//loadConfig()

	if u.UID > config.login.SYS_UID_MIN && u.UID < config.login.SYS_UID_MAX {
		return true
	}
	return false
}

func (u *User) String() string {
	return fmt.Sprintf("%s:%s:%d:%d:%s:%s:%s\n",
		u.Name, u.password, u.UID, u.GID, u.Gecos, u.Dir, u.Shell)
}

// parseUser parses the row of an user.
func parseUser(row string) (*User, error) {
	fields := strings.Split(row, ":")
	if len(fields) != 7 {
		return nil, rowError{fileUser, row}
	}

	uid, err := strconv.Atoi(fields[2])
	if err != nil {
		return nil, atoiError{fileUser, row, "UID"}
	}
	gid, err := strconv.Atoi(fields[3])
	if err != nil {
		return nil, atoiError{fileUser, row, "GID"}
	}

	return &User{
		Name:     fields[0],
		password: fields[1],
		UID:      uid,
		GID:      gid,
		Gecos:    fields[4],
		Dir:      fields[5],
		Shell:    fields[6],
	}, nil
}

// == Lookup
//

// lookUp parses the user line searching a value into the field.
// Returns nil if is not found.
func (*User) lookUp(line string, f field, value interface{}) interface{} {
	_field := f.(userField)
	allField := strings.Split(line, ":")
	intField := make(map[int]int)

	// Check integers
	var err error
	if intField[2], err = strconv.Atoi(allField[2]); err != nil {
		panic(atoiError{fileUser, line, "UID"})
	}
	if intField[3], err = strconv.Atoi(allField[3]); err != nil {
		panic(atoiError{fileUser, line, "GID"})
	}

	// Check fields
	var isField bool
	if U_NAME&_field != 0 && allField[0] == value.(string) {
		isField = true
	} else if U_PASSWD&_field != 0 && allField[1] == value.(string) {
		isField = true
	} else if U_UID&_field != 0 && intField[2] == value.(int) {
		isField = true
	} else if U_GID&_field != 0 && intField[3] == value.(int) {
		isField = true
	} else if U_GECOS&_field != 0 && allField[4] == value.(string) {
		isField = true
	} else if U_DIR&_field != 0 && allField[5] == value.(string) {
		isField = true
	} else if U_SHELL&_field != 0 && allField[6] == value.(string) {
		isField = true
	} else if U_ALL&_field != 0 {
		isField = true
	}

	if isField {
		return &User{
			Name:     allField[0],
			password: allField[1],
			UID:      intField[2],
			GID:      intField[3],
			Gecos:    allField[4],
			Dir:      allField[5],
			Shell:    allField[6],
		}
	}
	return nil
}

// LookupUID looks up an user by user ID.
func LookupUID(uid int) (*User, error) {
	entries, err := LookupInUser(U_UID, uid, 1)
	if err != nil {
		return nil, err
	}

	return entries[0], err
}

// LookupUser looks up an user by name.
func LookupUser(name string) (*User, error) {
	entries, err := LookupInUser(U_NAME, name, 1)
	if err != nil {
		return nil, err
	}

	return entries[0], err
}

// LookupInUser looks up an user by the given values.
//
// The count determines the number of fields to return:
//   n > 0: at most n fields
//   n == 0: the result is nil (zero fields)
//   n < 0: all fields
func LookupInUser(field userField, value interface{}, n int) ([]*User, error) {
	iEntries, err := lookUp(&User{}, field, value, n)
	if err != nil {
		return nil, err
	}

	// == Convert to type user
	valueSlice := reflect.ValueOf(iEntries)
	entries := make([]*User, valueSlice.Len())

	for i := 0; i < valueSlice.Len(); i++ {
		entries[i] = valueSlice.Index(i).Interface().(*User)
	}

	return entries, err
}

// GetUsername returns the user name from the password database for the actual
// process.
// It panics whther there is an error at searching the UID.
func GetUsername() string {
	entry, err := LookupUID(os.Getuid())
	if err != nil {
		panic(err)
	}
	return entry.Name
}

// GetUsernameFromEnv returns the user name from the environment variable
// for the actual process.
func GetUsernameFromEnv() string {
	userEnv := []string{"USER", "USERNAME", "LOGNAME", "LNAME"}

	for _, val := range userEnv {
		name := os.Getenv(val)
		if name != "" {
			return name
		}
	}
	return ""
}

// == Editing
//

// AddUser adds an user to both user and shadow files.
func AddUser(name string, gid int) (uid int, err error) {
	s := NewShadow(name)
	if err = s.Add(nil); err != nil {
		return
	}

	return NewUser(name, gid).Add()
}

// AddSystemUser adds a system user to both user and shadow files.
func AddSystemUser(name, homeDir string, gid int) (uid int, err error) {
	s := NewShadow(name)
	if err = s.Add(nil); err != nil {
		return
	}

	return NewSystemUser(name, homeDir, gid).Add()
}

// Add adds a new user.
// Whether UID is < 0, it will choose the first id available in the range set
// in the system configuration.
func (u *User) Add() (uid int, err error) {
	loadConfig()

	user, err := LookupUser(u.Name)
	if err != nil {
		if _, ok := err.(NoFoundError); !ok {
			return
		}
	}
	if user != nil {
		return 0, ErrUserExist
	}

	if u.Name == "" {
		return 0, RequiredError("Name")
	}
	if u.Dir == "" {
		return 0, RequiredError("Dir")
	}
	if u.Dir == config.useradd.HOME {
		return 0, HomeError(config.useradd.HOME)
	}
	if u.Shell == "" {
		return 0, RequiredError("Shell")
	}

	var db *dbfile
	if u.UID < 0 {
		db, uid, err = nextUID(u.addSystemUser)
		if err != nil {
			db.close()
			return 0, err
		}
		u.UID = uid
	} else {
		db, err = openDBFile(fileUser, os.O_WRONLY|os.O_APPEND)
		if err != nil {
			return 0, err
		}

		// Check if Id is unique.
		_, err = LookupUID(u.UID)
		if err == nil {
			return 0, IdUsedError(u.UID)
		} else if _, ok := err.(NoFoundError); !ok {
			return 0, err
		}
	}

	u.password = "x"

	_, err = db.file.WriteString(u.String())
	err2 := db.close()
	if err2 != nil && err == nil {
		err = err2
	}
	return
}

// DelUser removes an user from the system.
func DelUser(name string) (err error) {
	err = del(name, &User{})
	if err == nil {
		err = del(name, &Shadow{})
	}
	return
}

// == Errors
//

// A HomeError reports an error at adding an account with invalid home directory.
type HomeError string

func (e HomeError) Error() string {
	return "invalid directory for the home directory of an account: " + string(e)
}
