package users

import (
	"context"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	"nadir/internal/oscmd"

	"github.com/danielgtaylor/huma/v2"
)

const tagUsers = "Users"

var passwdPath = "/etc/passwd"

// systemUIDMax is the conventional upper bound for system (non-login) accounts;
// regular users start at 1000 on Debian/Fedora. Used only to flag accounts for
// the client, not to filter them out.
const systemUIDMax = 1000

var (
	readErrors  = []int{401, 403, 500}
	writeErrors = []int{400, 401, 403, 404, 409, 500}
)

// userNameRe matches valid Linux usernames (the useradd default NAME_REGEX).
// Starting with a letter/underscore also rejects leading-dash flag injection.
var userNameRe = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}\$?$`)

// User mirrors one /etc/passwd entry.
type User struct {
	Username string `json:"username" example:"alice" doc:"Login name"`
	UID      int    `json:"uid" example:"1000" doc:"User ID"`
	GID      int    `json:"gid" example:"1000" doc:"Primary group ID"`
	Comment  string `json:"comment" example:"Alice Smith" doc:"GECOS comment (often the full name)"`
	Home     string `json:"home" example:"/home/alice" doc:"Home directory"`
	Shell    string `json:"shell" example:"/bin/bash" doc:"Login shell"`
	System   bool   `json:"system" doc:"True for system accounts (uid < 1000)"`
}

type ListUsersOutput struct {
	Body struct {
		Users []User `json:"users" doc:"All accounts from /etc/passwd"`
	}
}

type GetUserOutput struct{ Body User }

type UserPath struct {
	Username string `path:"username" example:"alice" doc:"Login name"`
}

type CreateUserInput struct {
	Body struct {
		Username   string `json:"username" example:"alice" doc:"Login name"`
		Comment    string `json:"comment,omitempty" example:"Alice Smith" doc:"GECOS comment"`
		Shell      string `json:"shell,omitempty" example:"/bin/bash" doc:"Login shell"`
		Home       string `json:"home,omitempty" example:"/home/alice" doc:"Home directory (defaults to /home/<username>)"`
		CreateHome bool   `json:"create_home,omitempty" doc:"Create the home directory (useradd -m)"`
		System     bool   `json:"system,omitempty" doc:"Create a system account (useradd --system)"`
	}
}

type DeleteUserInput struct {
	Username   string `path:"username" example:"alice" doc:"Login name"`
	RemoveHome bool   `query:"remove_home" doc:"Also remove the home directory and mail spool (userdel -r)"`
}

type SetPasswordInput struct {
	Username string `path:"username" example:"alice" doc:"Login name"`
	Body     struct {
		Password string `json:"password" doc:"New password (sent to chpasswd over stdin, never argv)"`
	}
}

type SetGroupsInput struct {
	Username string `path:"username" example:"alice" doc:"Login name"`
	Body     struct {
		Groups []string `json:"groups" doc:"Supplementary groups; replaces the user's full supplementary set"`
	}
}

type UserGroupsOutput struct {
	Body struct {
		Username string   `json:"username" example:"alice"`
		Groups   []string `json:"groups" doc:"All groups the user belongs to (primary + supplementary)"`
	}
}

func registerUsers(api huma.API, sessions sessionStore) {
	huma.Register(api, huma.Operation{
		OperationID: "users-list",
		Method:      "GET",
		Path:        "/api/users",
		Summary:     "List user accounts",
		Description: "Returns every account in /etc/passwd, including system " +
			"accounts (flagged via `system`).",
		Tags:     []string{tagUsers},
		Metadata: op("read"),
		Errors:   readErrors,
	}, func(ctx context.Context, _ *struct{}) (*ListUsersOutput, error) {
		list, err := listUsers()
		if err != nil {
			return nil, huma.Error500InternalServerError("user lookup failed", err)
		}
		out := &ListUsersOutput{}
		out.Body.Users = list
		return out, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "users-get",
		Method:      "GET",
		Path:        "/api/users/{username}",
		Summary:     "Get a single user",
		Description: "Returns one account by login name. 404 if it does not exist.",
		Tags:        []string{tagUsers},
		Metadata:    op("read"),
		Errors:      []int{400, 401, 403, 404, 500},
	}, func(ctx context.Context, in *UserPath) (*GetUserOutput, error) {
		if err := validateUsername(in.Username); err != nil {
			return nil, err
		}
		u, ok, err := lookupUser(in.Username)
		if err != nil {
			return nil, huma.Error500InternalServerError("user lookup failed", err)
		}
		if !ok {
			return nil, huma.Error404NotFound("user not found: " + in.Username)
		}
		return &GetUserOutput{Body: u}, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "users-create",
		Method:      "POST",
		Path:        "/api/users",
		Summary:     "Create a user account",
		Description: "Creates an account via useradd. The new account has a locked " +
			"password until one is set via the password endpoint. 409 if the user " +
			"already exists.",
		Tags:     []string{tagUsers},
		Metadata: op("write"),
		Errors:   writeErrors,
	}, func(ctx context.Context, in *CreateUserInput) (*GetUserOutput, error) {
		if err := validateUsername(in.Body.Username); err != nil {
			return nil, err
		}
		// -c/-s/-d are option *arguments*, so the "--" separator doesn't shield
		// them. Validate at the boundary: a ':' or newline in the GECOS field
		// would corrupt /etc/passwd; shell/home must be absolute paths.
		if strings.ContainsAny(in.Body.Comment, ":\n") {
			return nil, huma.Error400BadRequest("comment may not contain ':' or newlines")
		}
		if in.Body.Shell != "" && !filepath.IsAbs(in.Body.Shell) {
			return nil, huma.Error400BadRequest("shell must be an absolute path")
		}
		if in.Body.Home != "" && !filepath.IsAbs(in.Body.Home) {
			return nil, huma.Error400BadRequest("home must be an absolute path")
		}
		if _, ok, err := lookupUser(in.Body.Username); err != nil {
			return nil, huma.Error500InternalServerError("user lookup failed", err)
		} else if ok {
			return nil, huma.Error409Conflict("user already exists: " + in.Body.Username)
		}

		args := []string{}
		if in.Body.System {
			args = append(args, "--system")
		}
		if in.Body.CreateHome {
			args = append(args, "-m")
		}
		if in.Body.Comment != "" {
			args = append(args, "-c", in.Body.Comment)
		}
		if in.Body.Shell != "" {
			args = append(args, "-s", in.Body.Shell)
		}
		if in.Body.Home != "" {
			args = append(args, "-d", in.Body.Home)
		}
		args = append(args, "--", in.Body.Username)
		if _, err := oscmd.Run("useradd", args...); err != nil {
			return nil, huma.Error500InternalServerError("useradd failed", err)
		}

		u, ok, err := lookupUser(in.Body.Username)
		if err != nil || !ok {
			return nil, huma.Error500InternalServerError("user created but could not be read back", err)
		}
		return &GetUserOutput{Body: u}, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "users-delete",
		Method:      "DELETE",
		Path:        "/api/users/{username}",
		Summary:     "Delete a user account",
		Description: "Removes an account via userdel. Pass ?remove_home=true to " +
			"also delete the home directory. 404 if the user does not exist.",
		Tags: []string{tagUsers},
		// Deleting an account is irreversible - gated behind root, not write.
		Metadata: op("root"),
		Errors:   []int{400, 401, 403, 404, 500},
	}, func(ctx context.Context, in *DeleteUserInput) (*oscmd.StatusOutput, error) {
		if err := validateUsername(in.Username); err != nil {
			return nil, err
		}
		if _, ok, err := lookupUser(in.Username); err != nil {
			return nil, huma.Error500InternalServerError("user lookup failed", err)
		} else if !ok {
			return nil, huma.Error404NotFound("user not found: " + in.Username)
		}

		args := []string{}
		if in.RemoveHome {
			args = append(args, "-r")
		}
		args = append(args, "--", in.Username)
		if _, err := oscmd.Run("userdel", args...); err != nil {
			return nil, huma.Error500InternalServerError("userdel failed", err)
		}
		return oscmd.OK(), nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "users-set-password",
		Method:      "POST",
		Path:        "/api/users/{username}/password",
		Summary:     "Set a user's password",
		Description: "Sets the password via chpasswd (fed over stdin, so the secret " +
			"never appears in the process list). 404 if the user does not exist. " +
			"Requires the `root` permission: resetting a privileged account's " +
			"password (e.g. root) is a full-system action, not a routine write.",
		Tags:     []string{tagUsers},
		Metadata: op("root"),
		Errors:   []int{400, 401, 403, 404, 500},
	}, func(ctx context.Context, in *SetPasswordInput) (*oscmd.StatusOutput, error) {
		if err := validateUsername(in.Username); err != nil {
			return nil, err
		}
		if in.Body.Password == "" {
			return nil, huma.Error400BadRequest("empty password")
		}
		// chpasswd reads one "name:password" line per stdin line, so a newline in
		// the password would inject a second line and set another account's password.
		if strings.ContainsAny(in.Body.Password, "\n\r") {
			return nil, huma.Error400BadRequest("password may not contain newlines")
		}
		if _, ok, err := lookupUser(in.Username); err != nil {
			return nil, huma.Error500InternalServerError("user lookup failed", err)
		} else if !ok {
			return nil, huma.Error404NotFound("user not found: " + in.Username)
		}
		// chpasswd reads "name:password" lines from stdin.
		if _, err := oscmd.RunStdin(in.Username+":"+in.Body.Password+"\n", "chpasswd"); err != nil {
			return nil, huma.Error500InternalServerError("chpasswd failed", err)
		}
		if sessions != nil {
			if err := sessions.DeleteByUsername(in.Username); err != nil {
				log.Printf("failed to invalidate sessions for %s: %v", in.Username, err)
			}
		}
		return oscmd.OK(), nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "users-set-groups",
		Method:      "PUT",
		Path:        "/api/users/{username}/groups",
		Summary:     "Set a user's supplementary groups",
		Description: "Replaces the user's full supplementary group set via " +
			"`usermod -G` (an empty list removes them from all supplementary " +
			"groups). Returns the resulting group membership. 404 if the user " +
			"does not exist; 400 if any named group is missing. Requires the " +
			"`root` permission: adding an account to wheel/sudo/docker is a " +
			"privilege grant, not a routine write.",
		Tags:     []string{tagUsers},
		Metadata: op("root"),
		Errors:   writeErrors,
	}, func(ctx context.Context, in *SetGroupsInput) (*UserGroupsOutput, error) {
		if err := validateUsername(in.Username); err != nil {
			return nil, err
		}
		for _, g := range in.Body.Groups {
			// Group names follow the same rule as usernames; this also blocks
			// flag injection and a stray comma turning one group into two.
			if !userNameRe.MatchString(g) {
				return nil, huma.Error400BadRequest("invalid group name: " + g)
			}
		}
		if _, ok, err := lookupUser(in.Username); err != nil {
			return nil, huma.Error500InternalServerError("user lookup failed", err)
		} else if !ok {
			return nil, huma.Error404NotFound("user not found: " + in.Username)
		}
		if _, err := oscmd.Run("usermod", "-G", strings.Join(in.Body.Groups, ","), "--", in.Username); err != nil {
			return nil, huma.Error400BadRequest("usermod failed (does every named group exist?)", err)
		}

		// id -nG lists all groups (primary + supplementary) the user now has.
		out, err := oscmd.Run("id", "-nG", in.Username)
		if err != nil {
			return nil, huma.Error500InternalServerError("groups set but read-back failed", err)
		}
		res := &UserGroupsOutput{}
		res.Body.Username = in.Username
		res.Body.Groups = strings.Fields(out)
		return res, nil
	})
}

// validateUsername rejects empty, flag-like, or malformed names before exec.
func validateUsername(name string) error {
	if !userNameRe.MatchString(name) {
		return huma.Error400BadRequest("invalid username: " + name)
	}
	return nil
}

func listUsers() ([]User, error) {
	data, err := os.ReadFile(passwdPath)
	if err != nil {
		return nil, err
	}
	return parsePasswd(data), nil
}

func lookupUser(name string) (User, bool, error) {
	list, err := listUsers()
	if err != nil {
		return User{}, false, err
	}
	for _, u := range list {
		if u.Username == name {
			return u, true, nil
		}
	}
	return User{}, false, nil
}

// parsePasswd parses /etc/passwd content. Lines that are blank, commented, or
// malformed (fewer than 7 fields, non-numeric ids) are skipped.
func parsePasswd(data []byte) []User {
	var users []User
	for line := range strings.SplitSeq(string(data), "\n") {
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		f := strings.Split(line, ":")
		if len(f) < 7 {
			continue
		}
		uid, err := strconv.Atoi(f[2])
		if err != nil {
			continue
		}
		gid, err := strconv.Atoi(f[3])
		if err != nil {
			continue
		}
		users = append(users, User{
			Username: f[0],
			UID:      uid,
			GID:      gid,
			Comment:  f[4],
			Home:     f[5],
			Shell:    f[6],
			System:   uid < systemUIDMax,
		})
	}
	return users
}
