// EXPERIMENTAL(#2213): The Work Items API is a work in progress and may introduce breaking changes even between minor versions.

package gitlab

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"text/template"
	"time"
)

type (
	WorkItemsServiceInterface interface {
		CreateWorkItem(fullPath string, workItemTypeID WorkItemTypeID, opt *CreateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error)
		GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error)
		ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error)
		UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error)
		DeleteWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*Response, error)
		ListWorkItemTypes(namespacePath string, opt *ListWorkItemTypesOptions, options ...RequestOptionFunc) ([]WorkItemType, *Response, error)
	}

	// WorkItemsService handles communication with the work item related methods
	// of the GitLab API.
	//
	// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitem
	//
	// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
	WorkItemsService struct {
		client *Client
	}
)

var _ WorkItemsServiceInterface = (*WorkItemsService)(nil)

// WorkItem represents a GitLab work item.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitem
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type WorkItem struct {
	ID          int64
	IID         int64
	Type        string
	State       string
	Status      *string
	Title       string
	Description string
	CreatedAt   *time.Time
	UpdatedAt   *time.Time
	ClosedAt    *time.Time
	WebURL      string
	Author      *BasicUser
	Assignees   []*BasicUser

	Color        *string
	Confidential bool
	DueDate      *ISOTime
	HealthStatus *string
	IterationID  *int64
	Labels       []LabelDetails
	LinkedItems  []LinkedWorkItem
	MilestoneID  *int64
	Parent       *WorkItemIID
	StartDate    *ISOTime
	Weight       *int64
}

func (wi WorkItem) GID() string {
	return gidGQL{
		Type:  "WorkItem",
		Int64: wi.ID,
	}.String()
}

// WorkItemIID identifies a work item by its namespace path and internal ID.
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type WorkItemIID struct {
	NamespacePath string
	IID           int64
}

// LinkedWorkItem represents a linked work item with its relationship type.
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type LinkedWorkItem struct {
	WorkItemIID

	// LinkType is the type of relationship between the work items.
	// Possible values: blocks, is_blocked_by, relates_to
	LinkType string
}

// WorkItemStateEvent represents a state change event for a work item.
type WorkItemStateEvent string

const (
	WorkItemStateEventClose  WorkItemStateEvent = "CLOSE"
	WorkItemStateEventReopen WorkItemStateEvent = "REOPEN"
)

// workItemTemplate defines the common fields for a work item in GraphQL queries.
// It's chained from userCoreBasicTemplate so nested templates work.
var workItemTemplate = template.Must(template.Must(userCoreBasicTemplate.Clone()).New("WorkItem").Parse(`
	id
	iid
	workItemType {
	  name
	}
	state
	title
	description
	confidential
	author {
	  {{ template "UserCoreBasic" }}
	}
	createdAt
	updatedAt
	closedAt
	webUrl
	features {
		assignees {
			assignees {
				nodes {
					{{ template "UserCoreBasic" }}
				}
			}
		}
		color {
			color
			textColor
		}
		healthStatus {
			healthStatus
		}
		hierarchy {
			hasParent
			parent {
				iid
				namespace {
					fullPath
				}
			}
		}
		iteration {
			iteration {
				id
			}
		}
		labels {
			labels {
				nodes {
					id
					title
					color
					description
					descriptionHtml
					textColor
				}
			}
		}
		linkedItems {
			linkedItems {
				nodes {
					workItem {
						iid
						namespace {
							fullPath
						}
					}
					linkType
				}
			}
		}
		milestone {
			milestone {
				id
			}
		}
		startAndDueDate {
			startDate
			dueDate
		}
		status {
			status {
				name
			}
		}
		weight {
			weight
		}
	}
`))

// getWorkItemTemplate is chained from workItemTemplate so it has access to both
// UserCoreBasic and WorkItem templates.
var getWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("GetWorkItem").Parse(`
	query GetWorkItem($fullPath: ID!, $iid: String!) {
		namespace(fullPath: $fullPath) {
			workItem(iid: $iid) {
				{{ template "WorkItem" }}
			}
		}
	}
`))

const (
	getWorkItemIDQuery = `
			query GetWorkItemID($fullPath: ID!, $iid: String!) {
				namespace(fullPath: $fullPath) {
					workItem(iid: $iid) {
						  id
					}
				}
			}
	`

	deleteWorkItemQuery = `
			mutation DeleteWorkItem($id: WorkItemID!) {
					workItemDelete(input: { id: $id }) {
							errors
					}
			}
	`
)

// GetWorkItem gets a single work item.
//
// fullPath is the full path to either a group or project.
// iid is the internal ID of the work item.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitem
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
func (s *WorkItemsService) GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error) {
	var queryBuilder strings.Builder
	if err := getWorkItemTemplate.Execute(&queryBuilder, nil); err != nil {
		return nil, nil, err
	}

	q := GraphQLQuery{
		Query: queryBuilder.String(),
		Variables: map[string]any{
			"fullPath": fullPath,
			"iid":      strconv.FormatInt(iid, 10),
		},
	}

	var result struct {
		Data struct {
			Namespace struct {
				WorkItem *workItemGQL `json:"workItem"`
			} `json:"namespace"`
		}
		GenericGraphQLErrors
	}

	resp, err := s.client.GraphQL.Do(q, &result, options...)
	if err != nil {
		return nil, resp, err
	}

	if len(result.Errors) != 0 {
		return nil, resp, &GraphQLResponseError{
			Err:    errors.New("GraphQL query failed"),
			Errors: result.GenericGraphQLErrors,
		}
	}

	wiQL := result.Data.Namespace.WorkItem
	if wiQL == nil {
		return nil, resp, ErrNotFound
	}

	return wiQL.unwrap(), resp, nil
}

// ListWorkItemsOptions represents the available ListWorkItems() options.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitems
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type ListWorkItemsOptions struct {
	AssigneeUsernames    []string
	AssigneeWildcardID   *string
	AuthorUsername       *string
	Confidential         *bool
	CRMContactID         *string
	CRMOrganizationID    *string
	HealthStatusFilter   *string
	IDs                  []string
	IIDs                 []string
	IncludeAncestors     *bool
	IncludeDescendants   *bool
	IterationCadenceID   []string
	IterationID          []string
	IterationWildcardID  *string
	LabelName            []string
	MilestoneTitle       []string
	MilestoneWildcardID  *string
	MyReactionEmoji      *string
	ParentIDs            []string
	ReleaseTag           []string
	ReleaseTagWildcardID *string
	State                *string
	Subscribed           *string
	Types                []string
	Weight               *string
	WeightWildcardID     *string

	// Time filters
	ClosedAfter   *time.Time
	ClosedBefore  *time.Time
	CreatedAfter  *time.Time
	CreatedBefore *time.Time
	DueAfter      *time.Time
	DueBefore     *time.Time
	UpdatedAfter  *time.Time
	UpdatedBefore *time.Time

	// Sorting
	Sort *string

	// Search
	Search *string
	In     []string

	// Pagination
	After  *string
	Before *string
	First  *int64
	Last   *int64
}

// listWorkItemsTemplate is chained from workItemTemplate so it has access to both
// UserCoreBasic and WorkItem templates.
var listWorkItemsTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("ListWorkItems").Parse(`
	query ListWorkItems(
		$fullPath: ID!
		$assigneeUsernames: [String!]
		$assigneeWildcardId: AssigneeWildcardId
		$authorUsername: String
		$confidential: Boolean
		$crmContactId: String
		$crmOrganizationId: String
		$healthStatusFilter: HealthStatusFilter
		$ids: [WorkItemID!]
		$iids: [String!]
		$includeAncestors: Boolean
		$includeDescendants: Boolean
		$iterationCadenceId: [IterationsCadenceID!]
		$iterationId: [ID]
		$iterationWildcardId: IterationWildcardId
		$labelName: [String!]
		$milestoneTitle: [String!]
		$milestoneWildcardId: MilestoneWildcardId
		$myReactionEmoji: String
		$parentIds: [WorkItemID!]
		$releaseTag: [String!]
		$releaseTagWildcardId: ReleaseTagWildcardId
		$state: IssuableState
		$subscribed: SubscriptionStatus
		$types: [IssueType!]
		$weight: String
		$weightWildcardId: WeightWildcardId
		$closedAfter: Time
		$closedBefore: Time
		$createdAfter: Time
		$createdBefore: Time
		$dueAfter: Time
		$dueBefore: Time
		$updatedAfter: Time
		$updatedBefore: Time
		$sort: WorkItemSort
		$search: String
		$in: [IssuableSearchableField!]
		$after: String
		$before: String
		$first: Int
		$last: Int
	) {
		namespace(fullPath: $fullPath) {
			workItems(
				assigneeUsernames: $assigneeUsernames
				assigneeWildcardId: $assigneeWildcardId
				authorUsername: $authorUsername
				confidential: $confidential
				crmContactId: $crmContactId
				crmOrganizationId: $crmOrganizationId
				healthStatusFilter: $healthStatusFilter
				ids: $ids
				iids: $iids
				includeAncestors: $includeAncestors
				includeDescendants: $includeDescendants
				iterationCadenceId: $iterationCadenceId
				iterationId: $iterationId
				iterationWildcardId: $iterationWildcardId
				labelName: $labelName
				milestoneTitle: $milestoneTitle
				milestoneWildcardId: $milestoneWildcardId
				myReactionEmoji: $myReactionEmoji
				parentIds: $parentIds
				releaseTag: $releaseTag
				releaseTagWildcardId: $releaseTagWildcardId
				state: $state
				subscribed: $subscribed
				types: $types
				weight: $weight
				weightWildcardId: $weightWildcardId
				closedAfter: $closedAfter
				closedBefore: $closedBefore
				createdAfter: $createdAfter
				createdBefore: $createdBefore
				dueAfter: $dueAfter
				dueBefore: $dueBefore
				updatedAfter: $updatedAfter
				updatedBefore: $updatedBefore
				sort: $sort
				search: $search
				in: $in
				after: $after
				before: $before
				first: $first
				last: $last
			) {
				nodes {
					{{ template "WorkItem" }}
				}
				pageInfo {
					endCursor
					hasNextPage
					startCursor
					hasPreviousPage
				}
			}
		}
	}
`))

// ListWorkItems lists workitems in a given namespace (group or project).
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitems
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
func (s *WorkItemsService) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) {
	var queryBuilder strings.Builder

	if err := listWorkItemsTemplate.Execute(&queryBuilder, nil); err != nil {
		return nil, nil, err
	}

	vars := map[string]any{
		"fullPath":             fullPath,
		"assigneeUsernames":    opt.AssigneeUsernames,
		"assigneeWildcardId":   opt.AssigneeWildcardID,
		"authorUsername":       opt.AuthorUsername,
		"confidential":         opt.Confidential,
		"crmContactId":         opt.CRMContactID,
		"crmOrganizationId":    opt.CRMOrganizationID,
		"healthStatusFilter":   opt.HealthStatusFilter,
		"ids":                  opt.IDs,
		"iids":                 opt.IIDs,
		"includeAncestors":     opt.IncludeAncestors,
		"includeDescendants":   opt.IncludeDescendants,
		"iterationCadenceId":   opt.IterationCadenceID,
		"iterationId":          opt.IterationID,
		"iterationWildcardId":  opt.IterationWildcardID,
		"labelName":            opt.LabelName,
		"milestoneTitle":       opt.MilestoneTitle,
		"milestoneWildcardId":  opt.MilestoneWildcardID,
		"myReactionEmoji":      opt.MyReactionEmoji,
		"parentIds":            opt.ParentIDs,
		"releaseTag":           opt.ReleaseTag,
		"releaseTagWildcardId": opt.ReleaseTagWildcardID,
		"state":                opt.State,
		"subscribed":           opt.Subscribed,
		"types":                opt.Types,
		"weight":               opt.Weight,
		"weightWildcardId":     opt.WeightWildcardID,
		"closedAfter":          opt.ClosedAfter,
		"closedBefore":         opt.ClosedBefore,
		"createdAfter":         opt.CreatedAfter,
		"createdBefore":        opt.CreatedBefore,
		"dueAfter":             opt.DueAfter,
		"dueBefore":            opt.DueBefore,
		"updatedAfter":         opt.UpdatedAfter,
		"updatedBefore":        opt.UpdatedBefore,
		"sort":                 opt.Sort,
		"search":               opt.Search,
		"in":                   opt.In,
		"after":                opt.After,
		"before":               opt.Before,
		"first":                opt.First,
		"last":                 opt.Last,
	}

	query := GraphQLQuery{
		Query:     queryBuilder.String(),
		Variables: vars,
	}

	var result struct {
		Data struct {
			Namespace struct {
				WorkItems connectionGQL[workItemGQL] `json:"workItems"`
			} `json:"namespace"`
		}
		GenericGraphQLErrors
	}

	resp, err := s.client.GraphQL.Do(query, &result, options...)
	if err != nil {
		return nil, resp, err
	}

	if len(result.Errors) != 0 {
		return nil, resp, &GraphQLResponseError{
			Err:    errors.New("GraphQL query failed"),
			Errors: result.GenericGraphQLErrors,
		}
	}

	var ret []*WorkItem

	for _, wi := range result.Data.Namespace.WorkItems.Nodes {
		ret = append(ret, wi.unwrap())
	}

	resp.PageInfo = &result.Data.Namespace.WorkItems.PageInfo

	return ret, resp, nil
}

var listWorkItemTypesTemplate = template.Must(template.New("listWorkItemTypes").Parse(`
query ListWorkItemTypes(
  $namespacePath: ID!,
  $name: IssueType,
  $onlyAvailable: Boolean,
  $after: String,
  $before: String,
  $first: Int,
  $last: Int
) {
  namespace(fullPath: $namespacePath) {
    workItemTypes(
      name: $name,
      onlyAvailable: $onlyAvailable,
      after: $after,
      before: $before,
      first: $first,
      last: $last
    ) {
      nodes {
        id
        name
        enabled
      }
      pageInfo {
        endCursor
        hasNextPage
        startCursor
        hasPreviousPage
      }
    }
  }
}
`))

// ListWorkItemTypes lists all work item types (system-defined and custom)
// for a given namespace.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitemtype
//
// Experimental: The Work Items API is a work in progress and may introduce
// breaking changes even between minor versions.
func (s *WorkItemsService) ListWorkItemTypes(
	namespacePath string,
	opt *ListWorkItemTypesOptions,
	options ...RequestOptionFunc,
) ([]WorkItemType, *Response, error) {
	var queryBuilder strings.Builder

	if opt == nil {
		opt = &ListWorkItemTypesOptions{}
	}
	if err := listWorkItemTypesTemplate.Execute(&queryBuilder, nil); err != nil {
		return nil, nil, err
	}

	vars := map[string]any{
		"namespacePath": namespacePath,
		"name":          opt.Name,
		"onlyAvailable": opt.OnlyAvailable,
		"after":         opt.After,
		"before":        opt.Before,
		"first":         opt.First,
		"last":          opt.Last,
	}

	query := GraphQLQuery{
		Query:     queryBuilder.String(),
		Variables: vars,
	}

	var result struct {
		Data struct {
			Namespace struct {
				WorkItemTypes connectionGQL[WorkItemType] `json:"workItemTypes"`
			} `json:"namespace"`
		}
		GenericGraphQLErrors
	}

	resp, err := s.client.GraphQL.Do(query, &result, options...)
	if err != nil {
		return nil, resp, err
	}

	if len(result.Errors) != 0 {
		return nil, resp, &GraphQLResponseError{
			Err:    errors.New("GraphQL query failed"),
			Errors: result.GenericGraphQLErrors,
		}
	}

	resp.PageInfo = &result.Data.Namespace.WorkItemTypes.PageInfo

	return result.Data.Namespace.WorkItemTypes.Nodes, resp, nil
}

// CreateWorkItemOptions represents the available CreateWorkItem() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/graphql/reference/#workitemcreateinput
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type CreateWorkItemOptions struct {
	// Title of the work item. Required.
	Title string

	// Description of the work item.
	Description *string

	// Sets the work item confidentiality.
	Confidential *bool

	// Global IDs of assignees.
	AssigneeIDs []int64

	// Global ID of the milestone to assign to the work item.
	MilestoneID *int64

	// Source which triggered the creation of the work item. Used only for tracking purposes.
	CreateSource *string

	// Timestamp when the work item was created. Available only for admins and project owners.
	CreatedAt *time.Time // admins and project owners only

	// CRM contact IDs to set.
	CRMContactIDs []int64

	// Global ID of the parent work item.
	ParentID *int64

	// Global IDs of labels to be added to the work item.
	LabelIDs []int64

	// Linked work items to be added to the work item.
	LinkedItems *CreateWorkItemOptionsLinkedItems

	// Start date for the work item.
	StartDate *ISOTime

	// Due date for the work item.
	DueDate *ISOTime

	// Weight of the work item.
	Weight *int64

	// Health status to be assigned to the work item. Possible values: onTrack, needsAttention, atRisk
	HealthStatus *string

	// Global ID of the iteration to assign to the work item.
	IterationID *int64

	// Color of the work item, represented as a hex code or named color. Example: "#fefefe"
	Color *string
}

// CreateWorkItemOptionsLinkedItems represents linked items to be added to a work item.
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type CreateWorkItemOptionsLinkedItems struct {
	LinkType    *string // enum: BLOCKED_BY, BLOCKS, RELATED
	WorkItemIDs []int64
}

// workItemCreateInputGQL represents the GraphQL input structure for creating a work item.
type workItemCreateInputGQL struct {
	// Required
	NamespacePath  string         `json:"namespacePath"`
	WorkItemTypeID WorkItemTypeID `json:"workItemTypeId"`
	Title          string         `json:"title"`

	// Optional
	AssigneesWidget       *workItemWidgetAssigneesInputGQL         `json:"assigneesWidget,omitempty"`
	Confidential          *bool                                    `json:"confidential,omitempty"`
	DescriptionWidget     *workItemWidgetDescriptionInputGQL       `json:"descriptionWidget,omitempty"`
	MilestoneWidget       *workItemWidgetMilestoneInputGQL         `json:"milestoneWidget,omitempty"`
	CreateSource          *string                                  `json:"createSource,omitempty"`
	CreatedAt             *time.Time                               `json:"createdAt,omitempty"`
	CRMContactsWidget     *workItemWidgetCRMContactsCreateInputGQL `json:"crmContactsWidget,omitempty"`
	HierarchyWidget       *workItemWidgetHierarchyCreateInputGQL   `json:"hierarchyWidget,omitempty"`
	LabelsWidget          *workItemWidgetLabelsCreateInputGQL      `json:"labelsWidget,omitempty"`
	LinkedItemsWidget     *workItemWidgetLinkedItemsCreateInputGQL `json:"linkedItemsWidget,omitempty"`
	StartAndDueDateWidget *workItemWidgetStartAndDueDateInputGQL   `json:"startAndDueDateWidget,omitempty"`
	WeightWidget          *workItemWidgetWeightInputGQL            `json:"weightWidget,omitempty"`
	HealthStatusWidget    *workItemWidgetHealthStatusInputGQL      `json:"healthStatusWidget,omitempty"`
	IterationWidget       *workItemWidgetIterationInputGQL         `json:"iterationWidget,omitempty"`
	ColorWidget           *workItemWidgetColorInputGQL             `json:"colorWidget,omitempty"`
}

// workItemWidgetAssigneesInputGQL represents the assignees widget input.
type workItemWidgetAssigneesInputGQL struct {
	AssigneeIDs []string `json:"assigneeIds,omitempty"`
}

// workItemWidgetDescriptionInputGQL represents the description widget input.
type workItemWidgetDescriptionInputGQL struct {
	Description *string `json:"description,omitempty"`
}

// workItemWidgetMilestoneInputGQL represents the milestone widget input.
type workItemWidgetMilestoneInputGQL struct {
	MilestoneID *string `json:"milestoneId,omitempty"`
}

// workItemWidgetCRMContactsCreateInputGQL represents the CRM contacts widget input.
type workItemWidgetCRMContactsCreateInputGQL struct {
	ContactIDs []string `json:"contactIds,omitempty"`
}

// workItemWidgetHierarchyCreateInputGQL represents the hierarchy widget input.
type workItemWidgetHierarchyCreateInputGQL struct {
	ParentID *string `json:"parentId,omitempty"`
}

// workItemWidgetLabelsCreateInputGQL represents the labels widget input.
type workItemWidgetLabelsCreateInputGQL struct {
	LabelIDs []string `json:"labelIds,omitempty"`
}

// workItemWidgetLinkedItemsCreateInputGQL represents the linked items widget input.
type workItemWidgetLinkedItemsCreateInputGQL struct {
	LinkType    *string  `json:"linkType,omitempty"`
	WorkItemIDs []string `json:"workItemsIds,omitempty"`
}

// workItemWidgetStartAndDueDateInputGQL represents the start and due date widget input.
type workItemWidgetStartAndDueDateInputGQL struct {
	StartDate *string `json:"startDate,omitempty"`
	DueDate   *string `json:"dueDate,omitempty"`
}

// workItemWidgetWeightInputGQL represents the weight widget input.
type workItemWidgetWeightInputGQL struct {
	Weight *int64 `json:"weight,omitempty"`
}

// workItemWidgetHealthStatusInputGQL represents the health status widget input.
type workItemWidgetHealthStatusInputGQL struct {
	HealthStatus *string `json:"healthStatus,omitempty"`
}

// workItemWidgetIterationInputGQL represents the iteration widget input.
type workItemWidgetIterationInputGQL struct {
	IterationID *string `json:"iterationId,omitempty"`
}

// workItemWidgetColorInputGQL represents the color widget input.
type workItemWidgetColorInputGQL struct {
	Color *string `json:"color,omitempty"`
}

// workItemWidgetCRMContactsUpdateInputGQL represents the CRM contacts widget input for updates.
type workItemWidgetCRMContactsUpdateInputGQL struct {
	ContactIDs    []string `json:"contactIds,omitempty"`
	OperationMode *string  `json:"operationMode,omitempty"`
}

// workItemWidgetHierarchyUpdateInputGQL represents the hierarchy widget input for updates.
type workItemWidgetHierarchyUpdateInputGQL struct {
	ParentID           *string  `json:"parentId,omitempty"`
	AdjacentWorkItemID *string  `json:"adjacentWorkItemId,omitempty"`
	ChildrenIDs        []string `json:"childrenIds,omitempty"`
	RelativePosition   *string  `json:"relativePosition,omitempty"`
}

// workItemWidgetLabelsUpdateInputGQL represents the labels widget input for updates.
type workItemWidgetLabelsUpdateInputGQL struct {
	AddLabelIDs    []string `json:"addLabelIds,omitempty"`
	RemoveLabelIDs []string `json:"removeLabelIds,omitempty"`
}

// workItemWidgetStartAndDueDateUpdateInputGQL represents the start and due date widget input for updates.
type workItemWidgetStartAndDueDateUpdateInputGQL struct {
	StartDate *string `json:"startDate,omitempty"`
	DueDate   *string `json:"dueDate,omitempty"`
}

// workItemWidgetStatusInputGQL represents the status widget input.
type workItemWidgetStatusInputGQL struct {
	Status *WorkItemStatusID `json:"status,omitempty"`
}

// newWorkItemCreateInput converts the user-facing CreateWorkItemOptions to the
// backend-facing GraphQL-aligned workItemCreateInputGQL struct.
func (opt *CreateWorkItemOptions) wrap(namespacePath string, workItemTypeID WorkItemTypeID) *workItemCreateInputGQL {
	if opt == nil {
		return &workItemCreateInputGQL{
			NamespacePath:  namespacePath,
			WorkItemTypeID: workItemTypeID,
		}
	}

	input := &workItemCreateInputGQL{
		NamespacePath:  namespacePath,
		WorkItemTypeID: workItemTypeID,

		Title:        opt.Title,
		Confidential: opt.Confidential,
		CreateSource: opt.CreateSource,
		CreatedAt:    opt.CreatedAt,
	}

	if opt.Description != nil {
		input.DescriptionWidget = &workItemWidgetDescriptionInputGQL{
			Description: opt.Description,
		}
	}

	if len(opt.AssigneeIDs) > 0 {
		input.AssigneesWidget = &workItemWidgetAssigneesInputGQL{
			AssigneeIDs: newGIDStrings("User", opt.AssigneeIDs...),
		}
	}

	if opt.MilestoneID != nil {
		input.MilestoneWidget = &workItemWidgetMilestoneInputGQL{
			MilestoneID: Ptr(gidGQL{"Milestone", *opt.MilestoneID}.String()),
		}
	}

	if len(opt.CRMContactIDs) > 0 {
		input.CRMContactsWidget = &workItemWidgetCRMContactsCreateInputGQL{
			ContactIDs: newGIDStrings("CustomerRelations::Contact", opt.CRMContactIDs...),
		}
	}

	if opt.ParentID != nil {
		input.HierarchyWidget = &workItemWidgetHierarchyCreateInputGQL{
			ParentID: Ptr(gidGQL{"WorkItem", *opt.ParentID}.String()),
		}
	}

	if len(opt.LabelIDs) > 0 {
		input.LabelsWidget = &workItemWidgetLabelsCreateInputGQL{
			LabelIDs: newGIDStrings("Label", opt.LabelIDs...),
		}
	}

	if opt.LinkedItems != nil && len(opt.LinkedItems.WorkItemIDs) > 0 {
		input.LinkedItemsWidget = &workItemWidgetLinkedItemsCreateInputGQL{
			LinkType:    opt.LinkedItems.LinkType,
			WorkItemIDs: newGIDStrings("WorkItem", opt.LinkedItems.WorkItemIDs...),
		}
	}

	if opt.StartDate != nil || opt.DueDate != nil {
		widget := &workItemWidgetStartAndDueDateInputGQL{}
		if opt.StartDate != nil {
			widget.StartDate = Ptr(opt.StartDate.String())
		}
		if opt.DueDate != nil {
			widget.DueDate = Ptr(opt.DueDate.String())
		}
		input.StartAndDueDateWidget = widget
	}

	if opt.Weight != nil {
		input.WeightWidget = &workItemWidgetWeightInputGQL{
			Weight: opt.Weight,
		}
	}

	if opt.HealthStatus != nil {
		input.HealthStatusWidget = &workItemWidgetHealthStatusInputGQL{
			HealthStatus: opt.HealthStatus,
		}
	}

	if opt.IterationID != nil {
		input.IterationWidget = &workItemWidgetIterationInputGQL{
			IterationID: Ptr(gidGQL{"Iteration", *opt.IterationID}.String()),
		}
	}

	if opt.Color != nil {
		input.ColorWidget = &workItemWidgetColorInputGQL{
			Color: opt.Color,
		}
	}

	return input
}

// updateWorkItemTemplate is chained from workItemTemplate so it has access to both
// UserCoreBasic and WorkItem templates.
var updateWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("UpdateWorkItem").Parse(`
	mutation UpdateWorkItem($input: WorkItemUpdateInput!) {
		workItemUpdate(input: $input) {
			workItem {
				{{ template "WorkItem" }}
			}
			errors
		}
	}
`))

// createWorkItemTemplate is chained from workItemTemplate so it has access to both
// UserCoreBasic and WorkItem templates.
var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("CreateWorkItem").Parse(`
	mutation CreateWorkItem($input: WorkItemCreateInput!) {
	  workItemCreate(input: $input) {
	    workItem {
	      {{ template "WorkItem" }}
	    }
	    errors
	  }
	}
`))

// CreateWorkItem creates a new work item.
//
// fullPath is the full path to either a group or project.
// workItemTypeID is the GraphQL ID of the work item type.
// opt contains the options for creating the work item.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/graphql/reference/#workitemcreateinput
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
func (s *WorkItemsService) CreateWorkItem(fullPath string, workItemTypeID WorkItemTypeID, opt *CreateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) {
	var queryBuilder strings.Builder
	if err := createWorkItemTemplate.Execute(&queryBuilder, nil); err != nil {
		return nil, nil, err
	}

	input := opt.wrap(fullPath, workItemTypeID)

	q := GraphQLQuery{
		Query: queryBuilder.String(),
		Variables: map[string]any{
			"input": input,
		},
	}

	var result struct {
		Data struct {
			WorkItemCreate struct {
				WorkItem *workItemGQL `json:"workItem"`
				Errors   []string     `json:"errors"`
			} `json:"workItemCreate"`
		}
		GenericGraphQLErrors
	}

	resp, err := s.client.GraphQL.Do(q, &result, options...)
	if err != nil {
		return nil, resp, err
	}

	if len(result.Errors) != 0 {
		return nil, resp, &GraphQLResponseError{
			Err:    errors.New("Mutation.workItemCreate failed"),
			Errors: result.GenericGraphQLErrors,
		}
	}

	if len(result.Data.WorkItemCreate.Errors) != 0 {
		err := &ErrorResponse{
			StatusCode: resp.Response.StatusCode,
			Message:    strings.Join(result.Data.WorkItemCreate.Errors, "; "),
			Response:   resp.Response,
		}

		return nil, resp, err
	}

	wiQL := result.Data.WorkItemCreate.WorkItem
	if wiQL == nil {
		return nil, resp, ErrEmptyResponse
	}

	return wiQL.unwrap(), resp, nil
}

// UpdateWorkItemOptions represents the available UpdateWorkItem() options.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemupdate
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type UpdateWorkItemOptions struct {
	// Title of the work item.
	Title *string

	// State event for the work item. Possible values: CLOSE, REOPEN
	StateEvent *WorkItemStateEvent

	// Description of the work item.
	Description *string

	// Global IDs of assignees. An empty (non-nil) slice can be used to remove all assignees.
	AssigneeIDs []int64

	// Global ID of the milestone to assign to the work item.
	MilestoneID *int64

	// CRM contact IDs to set. An empty (non-nil) slice can be used to remove all contacts.
	CRMContactIDs []int64

	// Global ID of the parent work item.
	ParentID *int64

	// Global IDs of labels to be added to the work item.
	AddLabelIDs []int64

	// Global IDs of labels to be removed from the work item.
	RemoveLabelIDs []int64

	// Start date for the work item.
	StartDate *ISOTime

	// Due date for the work item.
	DueDate *ISOTime

	// Weight of the work item.
	Weight *int64

	// Health status to be assigned to the work item. Possible values: onTrack, needsAttention, atRisk
	HealthStatus *string

	// Global ID of the iteration to assign to the work item.
	IterationID *int64

	// Color of the work item, represented as a hex code or named color. Example: "#fefefe"
	Color *string

	// Global ID of the work item status.
	Status *WorkItemStatusID
}

func (opt *UpdateWorkItemOptions) wrap(gid gidGQL) *workItemUpdateInputGQL {
	if opt == nil {
		return &workItemUpdateInputGQL{
			ID: gid.String(),
		}
	}

	input := &workItemUpdateInputGQL{
		ID:         gid.String(),
		Title:      opt.Title,
		StateEvent: opt.StateEvent,
	}

	if opt.Description != nil {
		input.DescriptionWidget = &workItemWidgetDescriptionInputGQL{
			Description: opt.Description,
		}
	}

	if len(opt.AssigneeIDs) > 0 {
		input.AssigneesWidget = &workItemWidgetAssigneesInputGQL{
			AssigneeIDs: newGIDStrings("User", opt.AssigneeIDs...),
		}
	}

	if opt.MilestoneID != nil {
		input.MilestoneWidget = &workItemWidgetMilestoneInputGQL{
			MilestoneID: Ptr(gidGQL{"Milestone", *opt.MilestoneID}.String()),
		}
	}

	if len(opt.CRMContactIDs) > 0 {
		input.CRMContactsWidget = &workItemWidgetCRMContactsUpdateInputGQL{
			ContactIDs:    newGIDStrings("CustomerRelations::Contact", opt.CRMContactIDs...),
			OperationMode: Ptr("REPLACE"),
		}
	}

	if opt.ParentID != nil {
		input.HierarchyWidget = &workItemWidgetHierarchyUpdateInputGQL{
			ParentID: Ptr(gidGQL{"WorkItem", *opt.ParentID}.String()),
		}
	}

	if len(opt.AddLabelIDs) > 0 || len(opt.RemoveLabelIDs) > 0 {
		widget := &workItemWidgetLabelsUpdateInputGQL{}
		if len(opt.AddLabelIDs) > 0 {
			widget.AddLabelIDs = newGIDStrings("Label", opt.AddLabelIDs...)
		}
		if len(opt.RemoveLabelIDs) > 0 {
			widget.RemoveLabelIDs = newGIDStrings("Label", opt.RemoveLabelIDs...)
		}
		input.LabelsWidget = widget
	}

	if opt.StartDate != nil || opt.DueDate != nil {
		widget := &workItemWidgetStartAndDueDateUpdateInputGQL{}
		if opt.StartDate != nil {
			widget.StartDate = Ptr(opt.StartDate.String())
		}
		if opt.DueDate != nil {
			widget.DueDate = Ptr(opt.DueDate.String())
		}
		input.StartAndDueDateWidget = widget
	}

	if opt.Weight != nil {
		input.WeightWidget = &workItemWidgetWeightInputGQL{
			Weight: opt.Weight,
		}
	}

	if opt.HealthStatus != nil {
		input.HealthStatusWidget = &workItemWidgetHealthStatusInputGQL{
			HealthStatus: opt.HealthStatus,
		}
	}

	if opt.IterationID != nil {
		input.IterationWidget = &workItemWidgetIterationInputGQL{
			IterationID: Ptr(gidGQL{"Iteration", *opt.IterationID}.String()),
		}
	}

	if opt.Color != nil {
		input.ColorWidget = &workItemWidgetColorInputGQL{
			Color: opt.Color,
		}
	}

	if opt.Status != nil {
		input.StatusWidget = &workItemWidgetStatusInputGQL{
			Status: opt.Status,
		}
	}

	return input
}

// UpdateWorkItem updates a work item by fullPath and iid.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemupdate
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
func (s *WorkItemsService) UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) {
	gid, resp, err := s.workItemGID(fullPath, iid, options...)
	if err != nil {
		return nil, resp, err
	}

	var queryBuilder strings.Builder
	if err := updateWorkItemTemplate.Execute(&queryBuilder, nil); err != nil {
		return nil, nil, err
	}

	q := GraphQLQuery{
		Query: queryBuilder.String(),
		Variables: map[string]any{
			"input": opt.wrap(gid),
		},
	}

	var result struct {
		Data struct {
			WorkItemUpdate struct {
				WorkItem *workItemGQL `json:"workItem"`
				Errors   []string     `json:"errors"`
			} `json:"workItemUpdate"`
		}
		GenericGraphQLErrors
	}

	resp, err = s.client.GraphQL.Do(q, &result, options...)
	if err != nil {
		return nil, resp, err
	}

	if len(result.Errors) != 0 {
		return nil, resp, &GraphQLResponseError{
			Err:    errors.New("GraphQL query failed"),
			Errors: result.GenericGraphQLErrors,
		}
	}

	if len(result.Data.WorkItemUpdate.Errors) != 0 {
		return nil, resp, errors.New(result.Data.WorkItemUpdate.Errors[0])
	}

	wiQL := result.Data.WorkItemUpdate.WorkItem
	if wiQL == nil {
		return nil, resp, ErrNotFound
	}

	return wiQL.unwrap(), resp, nil
}

// workItemUpdateInputGQL represents the GraphQL input structure for updating a work item.
type workItemUpdateInputGQL struct {
	ID                    string                                       `json:"id"`
	Title                 *string                                      `json:"title,omitempty"`
	StateEvent            *WorkItemStateEvent                          `json:"stateEvent,omitempty"`
	DescriptionWidget     *workItemWidgetDescriptionInputGQL           `json:"descriptionWidget,omitempty"`
	AssigneesWidget       *workItemWidgetAssigneesInputGQL             `json:"assigneesWidget,omitempty"`
	MilestoneWidget       *workItemWidgetMilestoneInputGQL             `json:"milestoneWidget,omitempty"`
	CRMContactsWidget     *workItemWidgetCRMContactsUpdateInputGQL     `json:"crmContactsWidget,omitempty"`
	HierarchyWidget       *workItemWidgetHierarchyUpdateInputGQL       `json:"hierarchyWidget,omitempty"`
	LabelsWidget          *workItemWidgetLabelsUpdateInputGQL          `json:"labelsWidget,omitempty"`
	StartAndDueDateWidget *workItemWidgetStartAndDueDateUpdateInputGQL `json:"startAndDueDateWidget,omitempty"`
	WeightWidget          *workItemWidgetWeightInputGQL                `json:"weightWidget,omitempty"`
	HealthStatusWidget    *workItemWidgetHealthStatusInputGQL          `json:"healthStatusWidget,omitempty"`
	IterationWidget       *workItemWidgetIterationInputGQL             `json:"iterationWidget,omitempty"`
	ColorWidget           *workItemWidgetColorInputGQL                 `json:"colorWidget,omitempty"`
	StatusWidget          *workItemWidgetStatusInputGQL                `json:"statusWidget,omitempty"`
}

// DeleteWorkItem deletes a single work item.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemdelete
func (s *WorkItemsService) DeleteWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*Response, error) {
	// get the global ID
	gid, resp, err := s.workItemGID(fullPath, iid, options...)
	if err != nil {
		if errors.Is(err, ErrEmptyResponse) {
			return resp, ErrNotFound
		}
		return resp, err
	}

	mutation := GraphQLQuery{
		Query: deleteWorkItemQuery,
		Variables: map[string]any{
			"id": gid.String(),
		},
	}

	var result struct {
		Data struct {
			WorkItemDelete struct {
				Errors []string `json:"errors"`
			} `json:"workItemDelete"`
		}
		GenericGraphQLErrors
	}

	resp, err = s.client.GraphQL.Do(mutation, &result, options...)
	if err != nil {
		return resp, err
	}

	if len(result.Errors) != 0 {
		return resp, &GraphQLResponseError{
			Err:    errors.New("Mutation.workItemDelete failed"),
			Errors: result.GenericGraphQLErrors,
		}
	}

	if len(result.Data.WorkItemDelete.Errors) != 0 {
		return resp, errors.New(strings.Join(result.Data.WorkItemDelete.Errors, ", "))
	}

	return resp, nil
}

// workItemGID queries for a work item's Global ID using fullPath and iid.
// Returns the Global ID string or ErrNotFound if the work item doesn't exist.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitem
func (s *WorkItemsService) workItemGID(fullPath string, iid int64, options ...RequestOptionFunc) (gidGQL, *Response, error) {
	q := GraphQLQuery{
		Query: getWorkItemIDQuery,
		Variables: map[string]any{
			"fullPath": fullPath,
			"iid":      strconv.FormatInt(iid, 10),
		},
	}

	var result struct {
		Data struct {
			Namespace struct {
				WorkItem struct {
					ID gidGQL `json:"id"`
				} `json:"workItem"`
			} `json:"namespace"`
		} `json:"data"`
		GenericGraphQLErrors
	}

	resp, err := s.client.GraphQL.Do(q, &result, options...)
	if err != nil {
		return gidGQL{}, resp, err
	}

	if len(result.Errors) != 0 {
		return gidGQL{}, resp, &GraphQLResponseError{
			Err:    fmt.Errorf("looking up global ID of %s#%d failed", fullPath, iid),
			Errors: result.GenericGraphQLErrors,
		}
	}

	id := result.Data.Namespace.WorkItem.ID
	if id.IsZero() {
		return gidGQL{}, resp, fmt.Errorf("looking up global ID of %s#%d failed: %w", fullPath, iid, ErrEmptyResponse)
	}

	return id, resp, nil
}

// workItemGQL represents the JSON structure returned by the GraphQL query.
// It is used to parse the response and convert it to the more user-friendly WorkItem type.
type workItemGQL struct {
	ID           gidGQL `json:"id"`
	IID          iidGQL `json:"iid"`
	WorkItemType struct {
		Name string `json:"name"`
	} `json:"workItemType"`
	State        string              `json:"state"`
	Title        string              `json:"title"`
	Description  string              `json:"description"`
	CreatedAt    *time.Time          `json:"createdAt"`
	UpdatedAt    *time.Time          `json:"updatedAt"`
	ClosedAt     *time.Time          `json:"closedAt"`
	Author       *userCoreBasicGQL   `json:"author"`
	Features     workItemFeaturesGQL `json:"features"`
	WebURL       string              `json:"webUrl"`
	Confidential bool                `json:"confidential"`
}

func (w workItemGQL) unwrap() *WorkItem {
	var assignees []*BasicUser

	wi := &WorkItem{
		ID:           w.ID.Int64,
		IID:          int64(w.IID),
		Type:         w.WorkItemType.Name,
		State:        w.State,
		Title:        w.Title,
		Description:  w.Description,
		CreatedAt:    w.CreatedAt,
		UpdatedAt:    w.UpdatedAt,
		ClosedAt:     w.ClosedAt,
		WebURL:       w.WebURL,
		Author:       w.Author.unwrap(),
		Assignees:    assignees,
		Confidential: w.Confidential,
	}

	w.Features.unwrap(wi)

	return wi
}

// workItemFeaturesGQL represents the optional features of the work item.
//
// While the "features" field in the "WorkItem" type is not nullable, each
// feature inside the struct is.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemfeatures
type workItemFeaturesGQL struct {
	Assignees       *workItemWidgetAssigneesGQL       `json:"assignees"`
	Color           *workItemWidgetColorGQL           `json:"color"`
	HealthStatus    *workItemWidgetHealthStatusGQL    `json:"healthStatus"`
	Hierarchy       *workItemWidgetHierarchyGQL       `json:"hierarchy"`
	Iteration       *workItemWidgetIterationGQL       `json:"iteration"`
	Labels          *workItemWidgetLabelsGQL          `json:"labels"`
	LinkedItems     *workItemWidgetLinkedItemsGQL     `json:"linkedItems"`
	Milestone       *workItemWidgetMilestoneGQL       `json:"milestone"`
	StartAndDueDate *workItemWidgetStartAndDueDateGQL `json:"startAndDueDate"`
	Status          *workItemWidgetStatusGQL          `json:"status"`
	Weight          *workItemWidgetWeightGQL          `json:"weight"`
}

func (f workItemFeaturesGQL) unwrap(wi *WorkItem) {
	wi.Assignees = f.Assignees.unwrap()
	wi.Color = f.Color.unwrap()
	wi.HealthStatus = f.HealthStatus.unwrap()
	wi.Parent = f.Hierarchy.unwrap()
	wi.IterationID = f.Iteration.unwrap()
	wi.Labels = f.Labels.unwrap()
	wi.LinkedItems = f.LinkedItems.unwrap()
	wi.MilestoneID = f.Milestone.unwrap()
	wi.StartDate, wi.DueDate = f.StartAndDueDate.unwrap()
	wi.Status = f.Status.unwrap()
	wi.Weight = f.Weight.unwrap()
}

// workItemWidgetAssigneesGQL represents the assignees widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetassignees
type workItemWidgetAssigneesGQL struct {
	Assignees *connectionGQL[userCoreBasicGQL] `json:"assignees"`
}

func (a *workItemWidgetAssigneesGQL) unwrap() []*BasicUser {
	if a == nil || a.Assignees == nil || len(a.Assignees.Nodes) == 0 {
		return nil
	}

	ret := make([]*BasicUser, 0, len(a.Assignees.Nodes))

	for _, assignee := range a.Assignees.Nodes {
		ret = append(ret, assignee.unwrap())
	}

	return ret
}

// workItemWidgetColorGQL represents a color widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetcolor
type workItemWidgetColorGQL struct {
	Color     *string `json:"color"`
	TextColor *string `json:"textColor"`
}

func (c *workItemWidgetColorGQL) unwrap() *string {
	if c == nil {
		return nil
	}

	return c.Color
}

// workItemWidgetHealthStatusGQL represents a health status widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgethealthstatus
type workItemWidgetHealthStatusGQL struct {
	HealthStatus *string `json:"healthStatus"`
}

func (h *workItemWidgetHealthStatusGQL) unwrap() *string {
	if h == nil {
		return nil
	}

	return h.HealthStatus
}

// workItemWidgetHierarchyGQL represents a hierarchy widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgethierarchy
type workItemWidgetHierarchyGQL struct {
	HasParent bool `json:"hasParent"`
	Parent    *struct {
		IID       string `json:"iid"`
		Namespace struct {
			FullPath string `json:"fullPath"`
		} `json:"namespace"`
	} `json:"parent"`
}

func (h *workItemWidgetHierarchyGQL) unwrap() *WorkItemIID {
	if h == nil || !h.HasParent || h.Parent == nil {
		return nil
	}

	iid, err := strconv.ParseInt(h.Parent.IID, 10, 64)
	if err != nil {
		return nil
	}

	return &WorkItemIID{
		NamespacePath: h.Parent.Namespace.FullPath,
		IID:           iid,
	}
}

// workItemWidgetIterationGQL represents a iteration widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetiteration
type workItemWidgetIterationGQL struct {
	Iteration *struct {
		ID gidGQL `json:"id"`
	} `json:"iteration"`
}

func (c *workItemWidgetIterationGQL) unwrap() *int64 {
	if c == nil || c.Iteration == nil {
		return nil
	}

	return Ptr(c.Iteration.ID.Int64)
}

// workItemWidgetLabelsGQL represents the labels widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetlabels
type workItemWidgetLabelsGQL struct {
	Labels *connectionGQL[labelGQL] `json:"labels"`
}

func (l *workItemWidgetLabelsGQL) unwrap() []LabelDetails {
	if l == nil || l.Labels == nil || len(l.Labels.Nodes) == 0 {
		return nil
	}

	ret := make([]LabelDetails, 0, len(l.Labels.Nodes))

	for _, label := range l.Labels.Nodes {
		ret = append(ret, label.unwrap())
	}

	return ret
}

// workItemWidgetLinkedItemsGQL represents the linked items widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetlinkeditems
type workItemWidgetLinkedItemsGQL struct {
	LinkedItems *connectionGQL[struct {
		WorkItem *struct {
			IID       string `json:"iid"`
			Namespace struct {
				FullPath string `json:"fullPath"`
			} `json:"namespace"`
		} `json:"workItem"`
		LinkType string `json:"linkType"`
	}] `json:"linkedItems"`
}

func (li *workItemWidgetLinkedItemsGQL) unwrap() []LinkedWorkItem {
	if li == nil || li.LinkedItems == nil {
		return nil
	}

	var ret []LinkedWorkItem

	for _, item := range li.LinkedItems.Nodes {
		if item.WorkItem == nil {
			continue
		}

		iid, err := strconv.ParseInt(item.WorkItem.IID, 10, 64)
		if err != nil {
			continue
		}

		ret = append(ret, LinkedWorkItem{
			WorkItemIID: WorkItemIID{
				NamespacePath: item.WorkItem.Namespace.FullPath,
				IID:           iid,
			},
			LinkType: item.LinkType,
		})
	}

	return ret
}

// workItemWidgetMilestoneGQL represents the milestone widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetmilestone
type workItemWidgetMilestoneGQL struct {
	Milestone *struct {
		ID gidGQL `json:"id"`
	} `json:"milestone"`
}

func (m *workItemWidgetMilestoneGQL) unwrap() *int64 {
	if m == nil || m.Milestone == nil {
		return nil
	}

	return Ptr(m.Milestone.ID.Int64)
}

// workItemWidgetStartAndDueDateGQL represents a start and due date widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetstartandduedate
type workItemWidgetStartAndDueDateGQL struct {
	DueDate   *ISOTime `json:"dueDate"`
	IsFixed   bool     `json:"isFixed"`
	StartDate *ISOTime `json:"startDate"`
}

func (du *workItemWidgetStartAndDueDateGQL) unwrap() (start, due *ISOTime) {
	if du == nil {
		return nil, nil
	}

	return du.StartDate, du.DueDate
}

// workItemWidgetStatusGQL represents the status widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetstatus
type workItemWidgetStatusGQL struct {
	Status *struct {
		Name *string `json:"name"`
	} `json:"status"`
}

func (s *workItemWidgetStatusGQL) unwrap() *string {
	if s == nil || s.Status == nil {
		return nil
	}

	return s.Status.Name
}

// workItemWidgetWeightGQL represents the weight widget.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#workitemwidgetweight
type workItemWidgetWeightGQL struct {
	Weight *int64 `json:"weight"`
}

func (w *workItemWidgetWeightGQL) unwrap() *int64 {
	if w == nil {
		return nil
	}

	return w.Weight
}

// WorkItemType represents a GitLab work item type.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitemtype
//
// Experimental: The Work Items API is a work in progress and may introduce
// breaking changes even between minor versions.
type WorkItemType struct {
	ID      WorkItemTypeID `json:"id"`
	Name    string         `json:"name"`
	Enabled bool           `json:"enabled"`
}

// ListWorkItemTypesOptions specifies the optional parameters to the
// WorkItemsService.ListWorkItemTypes method.
//
// Experimental: The Work Items API is a work in progress and may introduce
// breaking changes even between minor versions.
type ListWorkItemTypesOptions struct {
	Name          *string `json:"name,omitempty"`
	OnlyAvailable *bool   `json:"onlyAvailable,omitempty"`
	After         *string `json:"after,omitempty"`
	Before        *string `json:"before,omitempty"`
	First         *int64  `json:"first,omitempty"`
	Last          *int64  `json:"last,omitempty"`
}

// WorkItemTypeID represents the global ID of a work item type.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitemtype
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type WorkItemTypeID string

// WorkItemTypeID constants for the system-defined work item types.
const (
	WorkItemTypeIssue       WorkItemTypeID = `gid://gitlab/WorkItems::Type/1`
	WorkItemTypeIncident    WorkItemTypeID = `gid://gitlab/WorkItems::Type/2`
	WorkItemTypeTestCase    WorkItemTypeID = `gid://gitlab/WorkItems::Type/3`
	WorkItemTypeRequirement WorkItemTypeID = `gid://gitlab/WorkItems::Type/4`
	WorkItemTypeTask        WorkItemTypeID = `gid://gitlab/WorkItems::Type/5`
	WorkItemTypeObjective   WorkItemTypeID = `gid://gitlab/WorkItems::Type/6`
	WorkItemTypeKeyResult   WorkItemTypeID = `gid://gitlab/WorkItems::Type/7`
	WorkItemTypeEpic        WorkItemTypeID = `gid://gitlab/WorkItems::Type/8`
	WorkItemTypeTicket      WorkItemTypeID = `gid://gitlab/WorkItems::Type/9`
)

// WorkItemStatusID represents the global ID of a work item status.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitemsstatus
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type WorkItemStatusID string

// WorkItemStatusID constants for the system-defined work item statuses.
const (
	WorkItemStatusToDo       WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1`
	WorkItemStatusInProgress WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/2`
	WorkItemStatusDone       WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/3`
	WorkItemStatusWontDo     WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/4`
	WorkItemStatusDuplicate  WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/5`
)
