// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.31.1
// source: issue.sql

package db

import (
	"context"

	"github.com/jackc/pgx/v5/pgtype"
)

const childIssueProgress = `-- name: ChildIssueProgress :many
SELECT parent_issue_id,
       COUNT(*)::bigint AS total,
       COUNT(*) FILTER (WHERE status IN ('done', 'cancelled'))::bigint AS done
FROM issue
WHERE workspace_id = $1
  AND parent_issue_id IS NOT NULL
GROUP BY parent_issue_id
`

type ChildIssueProgressRow struct {
	ParentIssueID pgtype.UUID `json:"parent_issue_id"`
	Total         int64       `json:"total"`
	Done          int64       `json:"done"`
}

func (q *Queries) ChildIssueProgress(ctx context.Context, workspaceID pgtype.UUID) ([]ChildIssueProgressRow, error) {
	rows, err := q.db.Query(ctx, childIssueProgress, workspaceID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ChildIssueProgressRow{}
	for rows.Next() {
		var i ChildIssueProgressRow
		if err := rows.Scan(&i.ParentIssueID, &i.Total, &i.Done); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const countCreatedIssueAssignees = `-- name: CountCreatedIssueAssignees :many
SELECT
  assignee_type,
  assignee_id,
  COUNT(*)::bigint as frequency
FROM issue
WHERE workspace_id = $1
  AND creator_id = $2
  AND creator_type = 'member'
  AND assignee_type IS NOT NULL
  AND assignee_id IS NOT NULL
GROUP BY assignee_type, assignee_id
`

type CountCreatedIssueAssigneesParams struct {
	WorkspaceID pgtype.UUID `json:"workspace_id"`
	CreatorID   pgtype.UUID `json:"creator_id"`
}

type CountCreatedIssueAssigneesRow struct {
	AssigneeType pgtype.Text `json:"assignee_type"`
	AssigneeID   pgtype.UUID `json:"assignee_id"`
	Frequency    int64       `json:"frequency"`
}

// Count assignees on issues created by a specific user.
func (q *Queries) CountCreatedIssueAssignees(ctx context.Context, arg CountCreatedIssueAssigneesParams) ([]CountCreatedIssueAssigneesRow, error) {
	rows, err := q.db.Query(ctx, countCreatedIssueAssignees, arg.WorkspaceID, arg.CreatorID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []CountCreatedIssueAssigneesRow{}
	for rows.Next() {
		var i CountCreatedIssueAssigneesRow
		if err := rows.Scan(&i.AssigneeType, &i.AssigneeID, &i.Frequency); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const countIssues = `-- name: CountIssues :one
SELECT count(*) FROM issue i
WHERE i.workspace_id = $1
  AND ($2::text IS NULL OR i.status = $2)
  AND ($3::text IS NULL OR i.priority = $3)
  AND ($4::uuid IS NULL OR i.assignee_id = $4)
  AND ($5::uuid[] IS NULL OR i.assignee_id = ANY($5::uuid[]))
  AND ($6::uuid IS NULL OR i.creator_id = $6)
  AND ($7::uuid IS NULL OR i.project_id = $7)
  AND ($8::bool IS NULL OR (i.start_date IS NOT NULL OR i.due_date IS NOT NULL))
  AND ($9::jsonb IS NULL OR i.metadata @> $9::jsonb)
  AND (
    $10::uuid IS NULL
    OR (i.assignee_type = 'agent' AND i.assignee_id IN (
          SELECT a.id FROM agent a
           WHERE a.workspace_id = $1
             AND a.owner_id     = $10::uuid
    ))
    OR (i.assignee_type = 'squad' AND i.assignee_id IN (
          SELECT sm.squad_id
            FROM squad_member sm
            JOIN squad s ON s.id = sm.squad_id
           WHERE s.workspace_id = $1
             AND sm.member_type = 'member'
             AND sm.member_id   = $10::uuid
          UNION
          SELECT s.id
            FROM squad s
            JOIN agent a ON a.id = s.leader_id
           WHERE s.workspace_id = $1
             AND a.workspace_id = $1
             AND a.owner_id     = $10::uuid
          UNION
          SELECT sm.squad_id
            FROM squad_member sm
            JOIN squad s ON s.id = sm.squad_id
            JOIN agent a ON a.id = sm.member_id
           WHERE s.workspace_id = $1
             AND sm.member_type = 'agent'
             AND a.workspace_id = $1
             AND a.owner_id     = $10::uuid
    ))
  )
`

type CountIssuesParams struct {
	WorkspaceID    pgtype.UUID   `json:"workspace_id"`
	Status         pgtype.Text   `json:"status"`
	Priority       pgtype.Text   `json:"priority"`
	AssigneeID     pgtype.UUID   `json:"assignee_id"`
	AssigneeIds    []pgtype.UUID `json:"assignee_ids"`
	CreatorID      pgtype.UUID   `json:"creator_id"`
	ProjectID      pgtype.UUID   `json:"project_id"`
	Scheduled      pgtype.Bool   `json:"scheduled"`
	MetadataFilter []byte        `json:"metadata_filter"`
	InvolvesUserID pgtype.UUID   `json:"involves_user_id"`
}

// See ListIssues for the semantics of involves_user_id.
func (q *Queries) CountIssues(ctx context.Context, arg CountIssuesParams) (int64, error) {
	row := q.db.QueryRow(ctx, countIssues,
		arg.WorkspaceID,
		arg.Status,
		arg.Priority,
		arg.AssigneeID,
		arg.AssigneeIds,
		arg.CreatorID,
		arg.ProjectID,
		arg.Scheduled,
		arg.MetadataFilter,
		arg.InvolvesUserID,
	)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const createIssue = `-- name: CreateIssue :one
INSERT INTO issue (
    workspace_id, title, description, status, priority,
    assignee_type, assignee_id, creator_type, creator_id,
    parent_issue_id, position, start_date, due_date, number, project_id
) VALUES (
    $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata
`

type CreateIssueParams struct {
	WorkspaceID   pgtype.UUID `json:"workspace_id"`
	Title         string      `json:"title"`
	Description   pgtype.Text `json:"description"`
	Status        string      `json:"status"`
	Priority      string      `json:"priority"`
	AssigneeType  pgtype.Text `json:"assignee_type"`
	AssigneeID    pgtype.UUID `json:"assignee_id"`
	CreatorType   string      `json:"creator_type"`
	CreatorID     pgtype.UUID `json:"creator_id"`
	ParentIssueID pgtype.UUID `json:"parent_issue_id"`
	Position      float64     `json:"position"`
	StartDate     pgtype.Date `json:"start_date"`
	DueDate       pgtype.Date `json:"due_date"`
	Number        int32       `json:"number"`
	ProjectID     pgtype.UUID `json:"project_id"`
}

func (q *Queries) CreateIssue(ctx context.Context, arg CreateIssueParams) (Issue, error) {
	row := q.db.QueryRow(ctx, createIssue,
		arg.WorkspaceID,
		arg.Title,
		arg.Description,
		arg.Status,
		arg.Priority,
		arg.AssigneeType,
		arg.AssigneeID,
		arg.CreatorType,
		arg.CreatorID,
		arg.ParentIssueID,
		arg.Position,
		arg.StartDate,
		arg.DueDate,
		arg.Number,
		arg.ProjectID,
	)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const createIssueWithOrigin = `-- name: CreateIssueWithOrigin :one
INSERT INTO issue (
    workspace_id, title, description, status, priority,
    assignee_type, assignee_id, creator_type, creator_id,
    parent_issue_id, position, start_date, due_date, number, project_id,
    origin_type, origin_id
) VALUES (
    $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
    $16, $17
) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata
`

type CreateIssueWithOriginParams struct {
	WorkspaceID   pgtype.UUID `json:"workspace_id"`
	Title         string      `json:"title"`
	Description   pgtype.Text `json:"description"`
	Status        string      `json:"status"`
	Priority      string      `json:"priority"`
	AssigneeType  pgtype.Text `json:"assignee_type"`
	AssigneeID    pgtype.UUID `json:"assignee_id"`
	CreatorType   string      `json:"creator_type"`
	CreatorID     pgtype.UUID `json:"creator_id"`
	ParentIssueID pgtype.UUID `json:"parent_issue_id"`
	Position      float64     `json:"position"`
	StartDate     pgtype.Date `json:"start_date"`
	DueDate       pgtype.Date `json:"due_date"`
	Number        int32       `json:"number"`
	ProjectID     pgtype.UUID `json:"project_id"`
	OriginType    pgtype.Text `json:"origin_type"`
	OriginID      pgtype.UUID `json:"origin_id"`
}

func (q *Queries) CreateIssueWithOrigin(ctx context.Context, arg CreateIssueWithOriginParams) (Issue, error) {
	row := q.db.QueryRow(ctx, createIssueWithOrigin,
		arg.WorkspaceID,
		arg.Title,
		arg.Description,
		arg.Status,
		arg.Priority,
		arg.AssigneeType,
		arg.AssigneeID,
		arg.CreatorType,
		arg.CreatorID,
		arg.ParentIssueID,
		arg.Position,
		arg.StartDate,
		arg.DueDate,
		arg.Number,
		arg.ProjectID,
		arg.OriginType,
		arg.OriginID,
	)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const deleteIssue = `-- name: DeleteIssue :exec
DELETE FROM issue WHERE id = $1 AND workspace_id = $2
`

type DeleteIssueParams struct {
	ID          pgtype.UUID `json:"id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
}

// Defense-in-depth: the workspace_id predicate makes the tenant invariant a
// SQL-layer guarantee rather than a handler-layer one. Handler loaders
// (loadIssueForUser / GetIssueInWorkspace) already enforce membership today,
// but a future loader bypass or a new caller skipping the loader would be
// silently catastrophic without this guard. See incident #1661.
func (q *Queries) DeleteIssue(ctx context.Context, arg DeleteIssueParams) error {
	_, err := q.db.Exec(ctx, deleteIssue, arg.ID, arg.WorkspaceID)
	return err
}

const deleteIssueMetadataKey = `-- name: DeleteIssueMetadataKey :one
UPDATE issue SET
    metadata = metadata - $1::text,
    updated_at = now()
WHERE id = $2 AND workspace_id = $3
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata
`

type DeleteIssueMetadataKeyParams struct {
	Key         string      `json:"key"`
	ID          pgtype.UUID `json:"id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
}

// Atomically removes a single key from the issue's metadata JSONB.
// Deleting a missing key is a no-op (still returns the row).
func (q *Queries) DeleteIssueMetadataKey(ctx context.Context, arg DeleteIssueMetadataKeyParams) (Issue, error) {
	row := q.db.QueryRow(ctx, deleteIssueMetadataKey, arg.Key, arg.ID, arg.WorkspaceID)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const findActiveDuplicateIssue = `-- name: FindActiveDuplicateIssue :one
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue
WHERE workspace_id = $1
  AND status NOT IN ('done', 'cancelled')
  AND project_id IS NOT DISTINCT FROM $2::uuid
  AND parent_issue_id IS NOT DISTINCT FROM $3::uuid
  AND lower(btrim(regexp_replace(title, '[[:space:]]+', ' ', 'g'))) = $4
ORDER BY created_at ASC
LIMIT 1
`

type FindActiveDuplicateIssueParams struct {
	WorkspaceID     pgtype.UUID `json:"workspace_id"`
	ProjectID       pgtype.UUID `json:"project_id"`
	ParentIssueID   pgtype.UUID `json:"parent_issue_id"`
	NormalizedTitle string      `json:"normalized_title"`
}

func (q *Queries) FindActiveDuplicateIssue(ctx context.Context, arg FindActiveDuplicateIssueParams) (Issue, error) {
	row := q.db.QueryRow(ctx, findActiveDuplicateIssue,
		arg.WorkspaceID,
		arg.ProjectID,
		arg.ParentIssueID,
		arg.NormalizedTitle,
	)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const getIssue = `-- name: GetIssue :one
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue
WHERE id = $1
`

func (q *Queries) GetIssue(ctx context.Context, id pgtype.UUID) (Issue, error) {
	row := q.db.QueryRow(ctx, getIssue, id)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const getIssueByNumber = `-- name: GetIssueByNumber :one
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue
WHERE workspace_id = $1 AND number = $2
`

type GetIssueByNumberParams struct {
	WorkspaceID pgtype.UUID `json:"workspace_id"`
	Number      int32       `json:"number"`
}

func (q *Queries) GetIssueByNumber(ctx context.Context, arg GetIssueByNumberParams) (Issue, error) {
	row := q.db.QueryRow(ctx, getIssueByNumber, arg.WorkspaceID, arg.Number)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const getIssueByOrigin = `-- name: GetIssueByOrigin :one
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue
WHERE workspace_id = $1
  AND origin_type = $2
  AND origin_id = $3
LIMIT 1
`

type GetIssueByOriginParams struct {
	WorkspaceID pgtype.UUID `json:"workspace_id"`
	OriginType  pgtype.Text `json:"origin_type"`
	OriginID    pgtype.UUID `json:"origin_id"`
}

// Finds the issue stamped with a specific (origin_type, origin_id) pair.
// Used by quick-create completion to deterministically locate the issue
// produced by a given agent_task_queue.id — robust against concurrent
// issue creates by the same agent (assignment task + quick-create both
// running with max_concurrent_tasks > 1).
func (q *Queries) GetIssueByOrigin(ctx context.Context, arg GetIssueByOriginParams) (Issue, error) {
	row := q.db.QueryRow(ctx, getIssueByOrigin, arg.WorkspaceID, arg.OriginType, arg.OriginID)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const getIssueInWorkspace = `-- name: GetIssueInWorkspace :one
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue
WHERE id = $1 AND workspace_id = $2
`

type GetIssueInWorkspaceParams struct {
	ID          pgtype.UUID `json:"id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
}

func (q *Queries) GetIssueInWorkspace(ctx context.Context, arg GetIssueInWorkspaceParams) (Issue, error) {
	row := q.db.QueryRow(ctx, getIssueInWorkspace, arg.ID, arg.WorkspaceID)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const listChildIssues = `-- name: ListChildIssues :many
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue
WHERE parent_issue_id = $1
ORDER BY position ASC, created_at DESC
`

func (q *Queries) ListChildIssues(ctx context.Context, parentIssueID pgtype.UUID) ([]Issue, error) {
	rows, err := q.db.Query(ctx, listChildIssues, parentIssueID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Issue{}
	for rows.Next() {
		var i Issue
		if err := rows.Scan(
			&i.ID,
			&i.WorkspaceID,
			&i.Title,
			&i.Description,
			&i.Status,
			&i.Priority,
			&i.AssigneeType,
			&i.AssigneeID,
			&i.CreatorType,
			&i.CreatorID,
			&i.ParentIssueID,
			&i.AcceptanceCriteria,
			&i.ContextRefs,
			&i.Position,
			&i.DueDate,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Number,
			&i.ProjectID,
			&i.OriginType,
			&i.OriginID,
			&i.FirstExecutedAt,
			&i.StartDate,
			&i.Metadata,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listChildrenByParents = `-- name: ListChildrenByParents :many
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata FROM issue
WHERE workspace_id = $1
  AND parent_issue_id = ANY($2::uuid[])
ORDER BY parent_issue_id, position ASC, created_at DESC
`

type ListChildrenByParentsParams struct {
	WorkspaceID pgtype.UUID   `json:"workspace_id"`
	ParentIds   []pgtype.UUID `json:"parent_ids"`
}

// Batched variant of ListChildIssues: returns all children for the given
// parent set in one round trip. Used by Swimlane to avoid an N+1 fan-out
// (one request per visible parent lane). Result is grouped client-side by
// parent_issue_id; the workspace filter is also enforced so callers can't
// enumerate children of parents in workspaces they don't belong to.
func (q *Queries) ListChildrenByParents(ctx context.Context, arg ListChildrenByParentsParams) ([]Issue, error) {
	rows, err := q.db.Query(ctx, listChildrenByParents, arg.WorkspaceID, arg.ParentIds)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Issue{}
	for rows.Next() {
		var i Issue
		if err := rows.Scan(
			&i.ID,
			&i.WorkspaceID,
			&i.Title,
			&i.Description,
			&i.Status,
			&i.Priority,
			&i.AssigneeType,
			&i.AssigneeID,
			&i.CreatorType,
			&i.CreatorID,
			&i.ParentIssueID,
			&i.AcceptanceCriteria,
			&i.ContextRefs,
			&i.Position,
			&i.DueDate,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Number,
			&i.ProjectID,
			&i.OriginType,
			&i.OriginID,
			&i.FirstExecutedAt,
			&i.StartDate,
			&i.Metadata,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listIssues = `-- name: ListIssues :many
SELECT i.id, i.workspace_id, i.title, i.description, i.status, i.priority,
       i.assignee_type, i.assignee_id, i.creator_type, i.creator_id,
       i.parent_issue_id, i.position, i.start_date, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.metadata
FROM issue i
WHERE i.workspace_id = $1
  AND ($4::text IS NULL OR i.status = $4)
  AND ($5::text IS NULL OR i.priority = $5)
  AND ($6::uuid IS NULL OR i.assignee_id = $6)
  AND ($7::uuid[] IS NULL OR i.assignee_id = ANY($7::uuid[]))
  AND ($8::uuid IS NULL OR i.creator_id = $8)
  AND ($9::uuid IS NULL OR i.project_id = $9)
  AND ($10::bool IS NULL OR (i.start_date IS NOT NULL OR i.due_date IS NOT NULL))
  AND ($11::jsonb IS NULL OR i.metadata @> $11::jsonb)
  AND (
    $12::uuid IS NULL
    -- (1) assignee is an agent owned by the user
    OR (i.assignee_type = 'agent' AND i.assignee_id IN (
          SELECT a.id FROM agent a
           WHERE a.workspace_id = $1
             AND a.owner_id     = $12::uuid
    ))
    -- (2)(3)(4) assignee is a squad related to the user — three relations
    OR (i.assignee_type = 'squad' AND i.assignee_id IN (
          -- (2) the user is a human member of the squad
          SELECT sm.squad_id
            FROM squad_member sm
            JOIN squad s ON s.id = sm.squad_id
           WHERE s.workspace_id = $1
             AND sm.member_type = 'member'
             AND sm.member_id   = $12::uuid
          UNION
          -- (3) the squad's canonical leader is an agent owned by the user.
          -- We read squad.leader_id directly rather than relying on a
          -- squad_member row, because the leader copy in squad_member is
          -- best-effort (see squad.go AddSquadMember error handling).
          SELECT s.id
            FROM squad s
            JOIN agent a ON a.id = s.leader_id
           WHERE s.workspace_id = $1
             AND a.workspace_id = $1
             AND a.owner_id     = $12::uuid
          UNION
          -- (4) the squad has an agent member owned by the user
          SELECT sm.squad_id
            FROM squad_member sm
            JOIN squad s ON s.id = sm.squad_id
            JOIN agent a ON a.id = sm.member_id
           WHERE s.workspace_id = $1
             AND sm.member_type = 'agent'
             AND a.workspace_id = $1
             AND a.owner_id     = $12::uuid
    ))
  )
ORDER BY i.position ASC, i.created_at DESC
LIMIT $2 OFFSET $3
`

type ListIssuesParams struct {
	WorkspaceID    pgtype.UUID   `json:"workspace_id"`
	Limit          int32         `json:"limit"`
	Offset         int32         `json:"offset"`
	Status         pgtype.Text   `json:"status"`
	Priority       pgtype.Text   `json:"priority"`
	AssigneeID     pgtype.UUID   `json:"assignee_id"`
	AssigneeIds    []pgtype.UUID `json:"assignee_ids"`
	CreatorID      pgtype.UUID   `json:"creator_id"`
	ProjectID      pgtype.UUID   `json:"project_id"`
	Scheduled      pgtype.Bool   `json:"scheduled"`
	MetadataFilter []byte        `json:"metadata_filter"`
	InvolvesUserID pgtype.UUID   `json:"involves_user_id"`
}

type ListIssuesRow struct {
	ID            pgtype.UUID        `json:"id"`
	WorkspaceID   pgtype.UUID        `json:"workspace_id"`
	Title         string             `json:"title"`
	Description   pgtype.Text        `json:"description"`
	Status        string             `json:"status"`
	Priority      string             `json:"priority"`
	AssigneeType  pgtype.Text        `json:"assignee_type"`
	AssigneeID    pgtype.UUID        `json:"assignee_id"`
	CreatorType   string             `json:"creator_type"`
	CreatorID     pgtype.UUID        `json:"creator_id"`
	ParentIssueID pgtype.UUID        `json:"parent_issue_id"`
	Position      float64            `json:"position"`
	StartDate     pgtype.Date        `json:"start_date"`
	DueDate       pgtype.Date        `json:"due_date"`
	CreatedAt     pgtype.Timestamptz `json:"created_at"`
	UpdatedAt     pgtype.Timestamptz `json:"updated_at"`
	Number        int32              `json:"number"`
	ProjectID     pgtype.UUID        `json:"project_id"`
	Metadata      []byte             `json:"metadata"`
}

// involves_user_id widens the assignee filter to surface issues where the user
// is *indirectly* the assignee — via an owned agent or a squad they belong to /
// lead / have an agent inside. The semantics intentionally exclude direct
// member assignment (`assignee_type='member' AND assignee_id=involves_user_id`)
// because that is already the meaning of the `assignee_id` filter (tab 1
// "Assigned to me"), and the two filters must produce disjoint result sets.
func (q *Queries) ListIssues(ctx context.Context, arg ListIssuesParams) ([]ListIssuesRow, error) {
	rows, err := q.db.Query(ctx, listIssues,
		arg.WorkspaceID,
		arg.Limit,
		arg.Offset,
		arg.Status,
		arg.Priority,
		arg.AssigneeID,
		arg.AssigneeIds,
		arg.CreatorID,
		arg.ProjectID,
		arg.Scheduled,
		arg.MetadataFilter,
		arg.InvolvesUserID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ListIssuesRow{}
	for rows.Next() {
		var i ListIssuesRow
		if err := rows.Scan(
			&i.ID,
			&i.WorkspaceID,
			&i.Title,
			&i.Description,
			&i.Status,
			&i.Priority,
			&i.AssigneeType,
			&i.AssigneeID,
			&i.CreatorType,
			&i.CreatorID,
			&i.ParentIssueID,
			&i.Position,
			&i.StartDate,
			&i.DueDate,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Number,
			&i.ProjectID,
			&i.Metadata,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listOpenIssues = `-- name: ListOpenIssues :many
SELECT i.id, i.workspace_id, i.title, i.description, i.status, i.priority,
       i.assignee_type, i.assignee_id, i.creator_type, i.creator_id,
       i.parent_issue_id, i.position, i.start_date, i.due_date, i.created_at, i.updated_at, i.number, i.project_id, i.metadata
FROM issue i
WHERE i.workspace_id = $1
  AND i.status NOT IN ('done', 'cancelled')
  AND ($2::text IS NULL OR i.priority = $2)
  AND ($3::uuid IS NULL OR i.assignee_id = $3)
  AND ($4::uuid[] IS NULL OR i.assignee_id = ANY($4::uuid[]))
  AND ($5::uuid IS NULL OR i.creator_id = $5)
  AND ($6::uuid IS NULL OR i.project_id = $6)
  AND ($7::jsonb IS NULL OR i.metadata @> $7::jsonb)
  AND (
    $8::uuid IS NULL
    OR (i.assignee_type = 'agent' AND i.assignee_id IN (
          SELECT a.id FROM agent a
           WHERE a.workspace_id = $1
             AND a.owner_id     = $8::uuid
    ))
    OR (i.assignee_type = 'squad' AND i.assignee_id IN (
          SELECT sm.squad_id
            FROM squad_member sm
            JOIN squad s ON s.id = sm.squad_id
           WHERE s.workspace_id = $1
             AND sm.member_type = 'member'
             AND sm.member_id   = $8::uuid
          UNION
          SELECT s.id
            FROM squad s
            JOIN agent a ON a.id = s.leader_id
           WHERE s.workspace_id = $1
             AND a.workspace_id = $1
             AND a.owner_id     = $8::uuid
          UNION
          SELECT sm.squad_id
            FROM squad_member sm
            JOIN squad s ON s.id = sm.squad_id
            JOIN agent a ON a.id = sm.member_id
           WHERE s.workspace_id = $1
             AND sm.member_type = 'agent'
             AND a.workspace_id = $1
             AND a.owner_id     = $8::uuid
    ))
  )
ORDER BY i.position ASC, i.created_at DESC
`

type ListOpenIssuesParams struct {
	WorkspaceID    pgtype.UUID   `json:"workspace_id"`
	Priority       pgtype.Text   `json:"priority"`
	AssigneeID     pgtype.UUID   `json:"assignee_id"`
	AssigneeIds    []pgtype.UUID `json:"assignee_ids"`
	CreatorID      pgtype.UUID   `json:"creator_id"`
	ProjectID      pgtype.UUID   `json:"project_id"`
	MetadataFilter []byte        `json:"metadata_filter"`
	InvolvesUserID pgtype.UUID   `json:"involves_user_id"`
}

type ListOpenIssuesRow struct {
	ID            pgtype.UUID        `json:"id"`
	WorkspaceID   pgtype.UUID        `json:"workspace_id"`
	Title         string             `json:"title"`
	Description   pgtype.Text        `json:"description"`
	Status        string             `json:"status"`
	Priority      string             `json:"priority"`
	AssigneeType  pgtype.Text        `json:"assignee_type"`
	AssigneeID    pgtype.UUID        `json:"assignee_id"`
	CreatorType   string             `json:"creator_type"`
	CreatorID     pgtype.UUID        `json:"creator_id"`
	ParentIssueID pgtype.UUID        `json:"parent_issue_id"`
	Position      float64            `json:"position"`
	StartDate     pgtype.Date        `json:"start_date"`
	DueDate       pgtype.Date        `json:"due_date"`
	CreatedAt     pgtype.Timestamptz `json:"created_at"`
	UpdatedAt     pgtype.Timestamptz `json:"updated_at"`
	Number        int32              `json:"number"`
	ProjectID     pgtype.UUID        `json:"project_id"`
	Metadata      []byte             `json:"metadata"`
}

// See ListIssues for the semantics of involves_user_id (mirrors the 4-branch
// filter; member-direct assignment is intentionally excluded).
func (q *Queries) ListOpenIssues(ctx context.Context, arg ListOpenIssuesParams) ([]ListOpenIssuesRow, error) {
	rows, err := q.db.Query(ctx, listOpenIssues,
		arg.WorkspaceID,
		arg.Priority,
		arg.AssigneeID,
		arg.AssigneeIds,
		arg.CreatorID,
		arg.ProjectID,
		arg.MetadataFilter,
		arg.InvolvesUserID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ListOpenIssuesRow{}
	for rows.Next() {
		var i ListOpenIssuesRow
		if err := rows.Scan(
			&i.ID,
			&i.WorkspaceID,
			&i.Title,
			&i.Description,
			&i.Status,
			&i.Priority,
			&i.AssigneeType,
			&i.AssigneeID,
			&i.CreatorType,
			&i.CreatorID,
			&i.ParentIssueID,
			&i.Position,
			&i.StartDate,
			&i.DueDate,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Number,
			&i.ProjectID,
			&i.Metadata,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const lockIssueDuplicateKey = `-- name: LockIssueDuplicateKey :exec
SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))
`

func (q *Queries) LockIssueDuplicateKey(ctx context.Context, dollar_1 string) error {
	_, err := q.db.Exec(ctx, lockIssueDuplicateKey, dollar_1)
	return err
}

const markIssueFirstExecuted = `-- name: MarkIssueFirstExecuted :one
UPDATE issue
SET first_executed_at = now()
WHERE id = $1 AND first_executed_at IS NULL
RETURNING id, workspace_id, creator_type, creator_id, first_executed_at
`

type MarkIssueFirstExecutedRow struct {
	ID              pgtype.UUID        `json:"id"`
	WorkspaceID     pgtype.UUID        `json:"workspace_id"`
	CreatorType     string             `json:"creator_type"`
	CreatorID       pgtype.UUID        `json:"creator_id"`
	FirstExecutedAt pgtype.Timestamptz `json:"first_executed_at"`
}

// Flips first_executed_at from NULL to now() atomically. Returns the row if
// this was the first time the issue was executed; no rows otherwise. The
// analytics issue_executed event fires exactly when this returns a row —
// retries and re-assignments hit the WHERE clause and no-op.
func (q *Queries) MarkIssueFirstExecuted(ctx context.Context, id pgtype.UUID) (MarkIssueFirstExecutedRow, error) {
	row := q.db.QueryRow(ctx, markIssueFirstExecuted, id)
	var i MarkIssueFirstExecutedRow
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.CreatorType,
		&i.CreatorID,
		&i.FirstExecutedAt,
	)
	return i, err
}

const setIssueMetadataKey = `-- name: SetIssueMetadataKey :one

UPDATE issue SET
    metadata = jsonb_set(metadata, ARRAY[$1::text], $2::jsonb),
    updated_at = now()
WHERE id = $3 AND workspace_id = $4
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata
`

type SetIssueMetadataKeyParams struct {
	Key         string      `json:"key"`
	Value       []byte      `json:"value"`
	ID          pgtype.UUID `json:"id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
}

// SearchIssues: moved to handler (dynamic SQL for multi-word search support).
// Atomically sets a single key in the issue's metadata JSONB. The
// workspace_id filter is the authorization gate — handler resolves the
// issue first so this is also the tenant check.
func (q *Queries) SetIssueMetadataKey(ctx context.Context, arg SetIssueMetadataKeyParams) (Issue, error) {
	row := q.db.QueryRow(ctx, setIssueMetadataKey,
		arg.Key,
		arg.Value,
		arg.ID,
		arg.WorkspaceID,
	)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const updateIssue = `-- name: UpdateIssue :one
UPDATE issue SET
    title = COALESCE($2, title),
    description = COALESCE($3, description),
    status = COALESCE($4, status),
    priority = COALESCE($5, priority),
    assignee_type = $6,
    assignee_id = $7,
    position = COALESCE($8, position),
    start_date = $9,
    due_date = $10,
    parent_issue_id = $11,
    project_id = $12,
    updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata
`

type UpdateIssueParams struct {
	ID            pgtype.UUID   `json:"id"`
	Title         pgtype.Text   `json:"title"`
	Description   pgtype.Text   `json:"description"`
	Status        pgtype.Text   `json:"status"`
	Priority      pgtype.Text   `json:"priority"`
	AssigneeType  pgtype.Text   `json:"assignee_type"`
	AssigneeID    pgtype.UUID   `json:"assignee_id"`
	Position      pgtype.Float8 `json:"position"`
	StartDate     pgtype.Date   `json:"start_date"`
	DueDate       pgtype.Date   `json:"due_date"`
	ParentIssueID pgtype.UUID   `json:"parent_issue_id"`
	ProjectID     pgtype.UUID   `json:"project_id"`
}

func (q *Queries) UpdateIssue(ctx context.Context, arg UpdateIssueParams) (Issue, error) {
	row := q.db.QueryRow(ctx, updateIssue,
		arg.ID,
		arg.Title,
		arg.Description,
		arg.Status,
		arg.Priority,
		arg.AssigneeType,
		arg.AssigneeID,
		arg.Position,
		arg.StartDate,
		arg.DueDate,
		arg.ParentIssueID,
		arg.ProjectID,
	)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}

const updateIssueStatus = `-- name: UpdateIssueStatus :one
UPDATE issue SET
    status = $2,
    updated_at = now()
WHERE id = $1 AND workspace_id = $3
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at, number, project_id, origin_type, origin_id, first_executed_at, start_date, metadata
`

type UpdateIssueStatusParams struct {
	ID          pgtype.UUID `json:"id"`
	Status      string      `json:"status"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
}

// Workspace_id in the WHERE clause is a SQL-layer tenant guard; see DeleteIssue.
func (q *Queries) UpdateIssueStatus(ctx context.Context, arg UpdateIssueStatusParams) (Issue, error) {
	row := q.db.QueryRow(ctx, updateIssueStatus, arg.ID, arg.Status, arg.WorkspaceID)
	var i Issue
	err := row.Scan(
		&i.ID,
		&i.WorkspaceID,
		&i.Title,
		&i.Description,
		&i.Status,
		&i.Priority,
		&i.AssigneeType,
		&i.AssigneeID,
		&i.CreatorType,
		&i.CreatorID,
		&i.ParentIssueID,
		&i.AcceptanceCriteria,
		&i.ContextRefs,
		&i.Position,
		&i.DueDate,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Number,
		&i.ProjectID,
		&i.OriginType,
		&i.OriginID,
		&i.FirstExecutedAt,
		&i.StartDate,
		&i.Metadata,
	)
	return i, err
}
