package user

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
)

type User struct {
	Name  string
	Pass  string
	Uid   int
	Gid   int
	Gecos string
	Home  string
	Shell string
}

type Group struct {
	Name string
	Pass string
	Gid  int
	List []string
}

func parseLine(line string, v ...interface{}) {
	if line == "" {
		return
	}

	parts := strings.Split(line, ":")
	for i, p := range parts {
		if len(v) <= i {
			// if we have more "parts" than we have places to put them, bail for great "tolerance" of naughty configuration files
			break
		}

		switch e := v[i].(type) {
		case *string:
			// "root", "adm", "/bin/bash"
			*e = p
		case *int:
			// "0", "4", "1000"
			// ignore string to int conversion errors, for great "tolerance" of naughty configuration files
			*e, _ = strconv.Atoi(p)
		case *[]string:
			// "", "root", "root,adm,daemon"
			if p != "" {
				*e = strings.Split(p, ",")
			} else {
				*e = []string{}
			}
		default:
			// panic, because this is a programming/logic error, not a runtime one
			panic("parseLine expects only pointers!  argument " + strconv.Itoa(i) + " is not a pointer!")
		}
	}
}

func ParsePasswd() ([]*User, error) {
	return ParsePasswdFilter(nil)
}

func ParsePasswdFilter(filter func(*User) bool) ([]*User, error) {
	f, err := os.Open("/etc/passwd")
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return parsePasswdFile(f, filter)
}

func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
	var (
		s   = bufio.NewScanner(r)
		out = []*User{}
	)

	for s.Scan() {
		if err := s.Err(); err != nil {
			return nil, err
		}

		text := strings.TrimSpace(s.Text())
		if text == "" {
			continue
		}

		// see: man 5 passwd
		//  name:password:UID:GID:GECOS:directory:shell
		// Name:Pass:Uid:Gid:Gecos:Home:Shell
		//  root:x:0:0:root:/root:/bin/bash
		//  adm:x:3:4:adm:/var/adm:/bin/false
		p := &User{}
		parseLine(
			text,
			&p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell,
		)

		if filter == nil || filter(p) {
			out = append(out, p)
		}
	}

	return out, nil
}

func ParseGroup() ([]*Group, error) {
	return ParseGroupFilter(nil)
}

func ParseGroupFilter(filter func(*Group) bool) ([]*Group, error) {
	f, err := os.Open("/etc/group")
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return parseGroupFile(f, filter)
}

func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
	var (
		s   = bufio.NewScanner(r)
		out = []*Group{}
	)

	for s.Scan() {
		if err := s.Err(); err != nil {
			return nil, err
		}

		text := s.Text()
		if text == "" {
			continue
		}

		// see: man 5 group
		//  group_name:password:GID:user_list
		// Name:Pass:Gid:List
		//  root:x:0:root
		//  adm:x:4:root,adm,daemon
		p := &Group{}
		parseLine(
			text,
			&p.Name, &p.Pass, &p.Gid, &p.List,
		)

		if filter == nil || filter(p) {
			out = append(out, p)
		}
	}

	return out, nil
}

// Given a string like "user", "1000", "user:group", "1000:1000", returns the uid, gid, and list of supplementary group IDs, if possible.
func GetUserGroupSupplementary(userSpec string, defaultUid int, defaultGid int) (int, int, []int, error) {
	var (
		uid      = defaultUid
		gid      = defaultGid
		suppGids = []int{}

		userArg, groupArg string
	)

	// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
	parseLine(userSpec, &userArg, &groupArg)

	users, err := ParsePasswdFilter(func(u *User) bool {
		if userArg == "" {
			return u.Uid == uid
		}
		return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
	})
	if err != nil && !os.IsNotExist(err) {
		if userArg == "" {
			userArg = strconv.Itoa(uid)
		}
		return 0, 0, nil, fmt.Errorf("Unable to find user %v: %v", userArg, err)
	}

	haveUser := users != nil && len(users) > 0
	if haveUser {
		// if we found any user entries that matched our filter, let's take the first one as "correct"
		uid = users[0].Uid
		gid = users[0].Gid
	} else if userArg != "" {
		// we asked for a user but didn't find them...  let's check to see if we wanted a numeric user
		uid, err = strconv.Atoi(userArg)
		if err != nil {
			// not numeric - we have to bail
			return 0, 0, nil, fmt.Errorf("Unable to find user %v", userArg)
		}

		// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
	}

	if groupArg != "" || (haveUser && users[0].Name != "") {
		groups, err := ParseGroupFilter(func(g *Group) bool {
			if groupArg != "" {
				return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
			}
			for _, u := range g.List {
				if u == users[0].Name {
					return true
				}
			}
			return false
		})
		if err != nil && !os.IsNotExist(err) {
			return 0, 0, nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
		}

		haveGroup := groups != nil && len(groups) > 0
		if groupArg != "" {
			if haveGroup {
				// if we found any group entries that matched our filter, let's take the first one as "correct"
				gid = groups[0].Gid
			} else {
				// we asked for a group but didn't find id...  let's check to see if we wanted a numeric group
				gid, err = strconv.Atoi(groupArg)
				if err != nil {
					// not numeric - we have to bail
					return 0, 0, nil, fmt.Errorf("Unable to find group %v", groupArg)
				}

				// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
			}
		} else if haveGroup {
			suppGids = make([]int, len(groups))
			for i, group := range groups {
				suppGids[i] = group.Gid
			}
		}
	}

	return uid, gid, suppGids, nil
}
