package groups

import (
	"context"
	"os"
	"regexp"
	"strconv"
	"strings"

	"nadir/internal/oscmd"

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

const tagGroups = "Groups"

var groupPath = "/etc/group"

// systemGIDMax mirrors the user convention: regular groups start at 1000.
const systemGIDMax = 1000

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

// groupNameRe matches valid group names (same rule as usernames). Starting with
// a letter/underscore also rejects leading-dash flag injection.
var groupNameRe = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}\$?$`)

// Group mirrors one /etc/group entry.
type Group struct {
	Name    string   `json:"name" example:"wheel" doc:"Group name"`
	GID     int      `json:"gid" example:"10" doc:"Group ID"`
	Members []string `json:"members" doc:"Supplementary members (primary-group members are not listed here)"`
	System  bool     `json:"system" doc:"True for system groups (gid < 1000)"`
}

type ListGroupsOutput struct {
	Body struct {
		Groups []Group `json:"groups" doc:"All groups from /etc/group"`
	}
}

type GetGroupOutput struct{ Body Group }

type GroupPath struct {
	Group string `path:"group" example:"wheel" doc:"Group name"`
}

type CreateGroupInput struct {
	Body struct {
		Name   string `json:"name" example:"developers" doc:"Group name"`
		GID    *int   `json:"gid,omitempty" example:"1500" doc:"Explicit GID (omit to auto-assign)"`
		System bool   `json:"system,omitempty" doc:"Create a system group (groupadd --system)"`
	}
}

func registerGroups(api huma.API) {
	huma.Register(api, huma.Operation{
		OperationID: "groups-list",
		Method:      "GET",
		Path:        "/api/groups",
		Summary:     "List groups",
		Description: "Returns every group in /etc/group, including system groups " +
			"(flagged via `system`).",
		Tags:     []string{tagGroups},
		Metadata: op("read"),
		Errors:   readErrors,
	}, func(ctx context.Context, _ *struct{}) (*ListGroupsOutput, error) {
		list, err := listGroups()
		if err != nil {
			return nil, huma.Error500InternalServerError("group lookup failed", err)
		}
		out := &ListGroupsOutput{}
		out.Body.Groups = list
		return out, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "groups-get",
		Method:      "GET",
		Path:        "/api/groups/{group}",
		Summary:     "Get a single group",
		Description: "Returns one group by name. 404 if it does not exist.",
		Tags:        []string{tagGroups},
		Metadata:    op("read"),
		Errors:      []int{400, 401, 403, 404, 500},
	}, func(ctx context.Context, in *GroupPath) (*GetGroupOutput, error) {
		if err := validateGroupName(in.Group); err != nil {
			return nil, err
		}
		g, ok, err := lookupGroup(in.Group)
		if err != nil {
			return nil, huma.Error500InternalServerError("group lookup failed", err)
		}
		if !ok {
			return nil, huma.Error404NotFound("group not found: " + in.Group)
		}
		return &GetGroupOutput{Body: g}, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "groups-create",
		Method:      "POST",
		Path:        "/api/groups",
		Summary:     "Create a group",
		Description: "Creates a group via groupadd. 409 if the group already exists.",
		Tags:        []string{tagGroups},
		Metadata:    op("write"),
		Errors:      writeErrors,
	}, func(ctx context.Context, in *CreateGroupInput) (*GetGroupOutput, error) {
		if err := validateGroupName(in.Body.Name); err != nil {
			return nil, err
		}
		if _, ok, err := lookupGroup(in.Body.Name); err != nil {
			return nil, huma.Error500InternalServerError("group lookup failed", err)
		} else if ok {
			return nil, huma.Error409Conflict("group already exists: " + in.Body.Name)
		}

		args := []string{}
		if in.Body.System {
			args = append(args, "--system")
		}
		if in.Body.GID != nil {
			args = append(args, "-g", strconv.Itoa(*in.Body.GID))
		}
		args = append(args, "--", in.Body.Name)
		if _, err := oscmd.Run("groupadd", args...); err != nil {
			return nil, huma.Error500InternalServerError("groupadd failed", err)
		}

		g, ok, err := lookupGroup(in.Body.Name)
		if err != nil || !ok {
			return nil, huma.Error500InternalServerError("group created but could not be read back", err)
		}
		return &GetGroupOutput{Body: g}, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "groups-delete",
		Method:      "DELETE",
		Path:        "/api/groups/{group}",
		Summary:     "Delete a group",
		Description: "Removes a group via groupdel. Returns 409 if it is the primary " +
			"group of an existing user (groupdel refuses), 404 if it does not exist.",
		Tags: []string{tagGroups},
		// Deleting a group is irreversible - gated behind root, not write.
		Metadata: op("root"),
		Errors:   []int{400, 401, 403, 404, 409, 500},
	}, func(ctx context.Context, in *GroupPath) (*oscmd.StatusOutput, error) {
		if err := validateGroupName(in.Group); err != nil {
			return nil, err
		}
		if _, ok, err := lookupGroup(in.Group); err != nil {
			return nil, huma.Error500InternalServerError("group lookup failed", err)
		} else if !ok {
			return nil, huma.Error404NotFound("group not found: " + in.Group)
		}
		if _, err := oscmd.Run("groupdel", "--", in.Group); err != nil {
			// groupdel refuses to remove a user's primary group; make that actionable.
			return nil, huma.Error409Conflict("groupdel failed (is it a user's primary group?)", err)
		}
		return oscmd.OK(), nil
	})
}

func validateGroupName(name string) error {
	if !groupNameRe.MatchString(name) {
		return huma.Error400BadRequest("invalid group name: " + name)
	}
	return nil
}

func listGroups() ([]Group, error) {
	data, err := os.ReadFile(groupPath)
	if err != nil {
		return nil, err
	}
	return parseGroup(data), nil
}

func lookupGroup(name string) (Group, bool, error) {
	list, err := listGroups()
	if err != nil {
		return Group{}, false, err
	}
	for _, g := range list {
		if g.Name == name {
			return g, true, nil
		}
	}
	return Group{}, false, nil
}

// parseGroup parses /etc/group content. Blank, commented, or malformed lines
// (fewer than 4 fields, non-numeric gid) are skipped.
func parseGroup(data []byte) []Group {
	var groups []Group
	for line := range strings.SplitSeq(string(data), "\n") {
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		f := strings.Split(line, ":")
		if len(f) < 4 {
			continue
		}
		gid, err := strconv.Atoi(f[2])
		if err != nil {
			continue
		}
		var members []string
		if f[3] != "" {
			members = strings.Split(f[3], ",")
		}
		groups = append(groups, Group{
			Name:    f[0],
			GID:     gid,
			Members: members,
			System:  gid < systemGIDMax,
		})
	}
	return groups
}
