func checkNextAssignee(chore *chModel.Chore, choresHistory []*chModel.ChoreHistory, performerID int, circleUsers []*circle.UserCircleDetail) (*int, error) {
	// copy the history to avoid modifying the original:
	history := make([]*chModel.ChoreHistory, len(choresHistory))
	copy(history, choresHistory)

	// "Anyone" chores carry no explicit assignees, so rotate across the whole circle rather
	// than dropping the assignment: a nil result is persisted verbatim by CompleteChore/
	// ApproveChore/SkipChore, and the notification planner only emits for an assigned user,
	// so dropping it would silence the chore. no_assignee is excluded because nil is its
	// intended result. Mirrors the create/edit validation, which treats an empty assignee
	// list as the whole circle.
	assignees := chore.Assignees
	if len(assignees) == 0 && chore.AssignStrategy != chModel.AssignmentStrategyNoAssignee {
		for _, circleUser := range circleUsers {
			assignees = append(assignees, chModel.ChoreAssignees{
				ChoreID: chore.ID,
				UserID:  circleUser.UserID,
			})
		}
	}

	assigneesMap := map[int]bool{}
	for _, assignee := range assignees {
		assigneesMap[assignee.UserID] = true
	}
	var nextAssignee *int
	if len(history) == 0 {
		// if there is no history, just assume the current operation as the first
		history = append(history, &chModel.ChoreHistory{
			AssignedTo: &performerID,
		})
	}

	switch chore.AssignStrategy {
	case chModel.AssignmentStrategyLeastAssigned:
		// find the assignee with the least number of chores
		assigneeChores := map[int]int{}
		for _, performer := range assignees {
			assigneeChores[performer.UserID] = 0
		}
		for _, history := range history {
			if history.AssignedTo != nil {
				if ok := assigneesMap[*history.AssignedTo]; ok {
					// calculate the number of chores assigned to each assignee
					assigneeChores[*history.AssignedTo]++
				}
			}
		}

		var minChores int64 = math.MaxInt64
		var bestAssignee int
		for assignee, numChores := range assigneeChores {
			// if this is the first assignee or if the number of
			// chores assigned to this assignee is less than the current minimum
			if int64(numChores) < minChores {
				minChores = int64(numChores)
				// set the next assignee to this assignee
				bestAssignee = assignee
			}
		}
		if len(assigneeChores) > 0 {
			nextAssignee = &bestAssignee
		}
	case chModel.AssignmentStrategyLeastCompleted:
		// find the assignee who has completed the least number of chores
		assigneeChores := map[int]int{}
		for _, performer := range assignees {
			assigneeChores[performer.UserID] = 0
		}
		for _, history := range history {
			// only count completions by users who are current assignees
			if _, ok := assigneesMap[history.CompletedBy]; ok {
				assigneeChores[history.CompletedBy]++
			}
		}

		// max Int value
		var minChores int64 = math.MaxInt64
		var bestAssignee int

		for assignee, numChores := range assigneeChores {
			// if this is the first assignee or if the number of
			// chores completed by this assignee is less than the current minimum
			if int64(numChores) < minChores {
				minChores = int64(numChores)
				// set the next assignee to this assignee
				bestAssignee = assignee
			}
		}
		if len(assigneeChores) > 0 {
			nextAssignee = &bestAssignee
		}
	case chModel.AssignmentStrategyRandom:
		if len(assignees) > 0 {
			assigneeID := assignees[rand.Intn(len(assignees))].UserID
			nextAssignee = &assigneeID
		}
	case chModel.AssignmentStrategyNoAssignee:
		nextAssignee = nil
	case chModel.AssignmentStrategyKeepLastAssigned:
		// keep the last assignee
		nextAssignee = chore.AssignedTo
	case chModel.AssignmentStrategyRandomExceptLastAssigned:
		var lastAssigned *int = chore.AssignedTo
		AssigneesCopy := make([]chModel.ChoreAssignees, len(assignees))
		copy(AssigneesCopy, assignees)
		var removeLastAssigned []chModel.ChoreAssignees
		if lastAssigned != nil {
			removeLastAssigned = remove(AssigneesCopy, *lastAssigned)
		} else {
			removeLastAssigned = AssigneesCopy
		}
		if len(removeLastAssigned) > 0 {
			assigneeID := removeLastAssigned[rand.Intn(len(removeLastAssigned))].UserID
			nextAssignee = &assigneeID
		} else {
			// only candidate is the last assignee (e.g. a single-member circle), so keep
			// them rather than dropping the assignment
			nextAssignee = chore.AssignedTo
		}
	case chModel.AssignmentStrategyRoundRobin:
		if len(assignees) == 0 {
			return chore.AssignedTo, fmt.Errorf("no assignees available")
		}

		// Find current assignee index
		currentIndex := -1
		for i, assignee := range assignees {
			if chore.AssignedTo != nil && assignee.UserID == *chore.AssignedTo {
				currentIndex = i
				break
			}
		}

		// If current assignee is not found, start from the beginning
		var assigneeID int
		if currentIndex == -1 {
			assigneeID = assignees[0].UserID
		} else {
			nextIndex := (currentIndex + 1) % len(assignees)
			assigneeID = assignees[nextIndex].UserID
		}
		nextAssignee = &assigneeID
	default:
		return chore.AssignedTo, fmt.Errorf("invalid assign strategy")
	}
	return nextAssignee, nil
}

func remove(s []chModel.ChoreAssignees, i int) []chModel.ChoreAssignees {
	var targetIndex = indexOf(s, i)
	if targetIndex == -1 {
		return s
	}
	s[targetIndex] = s[len(s)-1]
	return s[:len(s)-1]
}

func indexOf(arr []chModel.ChoreAssignees, value int) int {
	for i, v := range arr {
		if v.UserID == value {
			return i
		}
	}
	return -1
}

// region: request models
type NudgeRequest struct {
