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

package db

import (
	"context"

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

const countComments = `-- name: CountComments :one
SELECT count(*) FROM comment
WHERE issue_id = $1 AND workspace_id = $2
`

type CountCommentsParams struct {
	IssueID     pgtype.UUID `json:"issue_id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
}

func (q *Queries) CountComments(ctx context.Context, arg CountCommentsParams) (int64, error) {
	row := q.db.QueryRow(ctx, countComments, arg.IssueID, arg.WorkspaceID)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const countNewCommentsSince = `-- name: CountNewCommentsSince :one
SELECT count(*) FROM comment
WHERE issue_id = $1
  AND workspace_id = $2
  AND created_at > $3
  AND id <> $4
  AND NOT (author_type = 'agent' AND author_id = $5)
`

type CountNewCommentsSinceParams struct {
	IssueID     pgtype.UUID        `json:"issue_id"`
	WorkspaceID pgtype.UUID        `json:"workspace_id"`
	Since       pgtype.Timestamptz `json:"since"`
	AnchorID    pgtype.UUID        `json:"anchor_id"`
	AuthorID    pgtype.UUID        `json:"author_id"`
}

// Counts comments on an issue created strictly after @since, ACROSS THE WHOLE
// ISSUE (every thread, not just the triggering one). Excludes the triggering
// comment itself (@anchor_id — its body is already injected into the prompt)
// and any authored by the given agent (@author_id), so a chatty agent does not
// inflate its own new-comment count. The agent is steered to read the
// triggering thread first (see BuildNewCommentsHint), but the count is
// issue-wide so it knows the full catch-up volume. Feeds the daemon claim
// response without shipping comment bodies.
func (q *Queries) CountNewCommentsSince(ctx context.Context, arg CountNewCommentsSinceParams) (int64, error) {
	row := q.db.QueryRow(ctx, countNewCommentsSince,
		arg.IssueID,
		arg.WorkspaceID,
		arg.Since,
		arg.AnchorID,
		arg.AuthorID,
	)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const createComment = `-- name: CreateComment :one
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content, type, parent_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
`

type CreateCommentParams struct {
	IssueID     pgtype.UUID `json:"issue_id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
	AuthorType  string      `json:"author_type"`
	AuthorID    pgtype.UUID `json:"author_id"`
	Content     string      `json:"content"`
	Type        string      `json:"type"`
	ParentID    pgtype.UUID `json:"parent_id"`
}

func (q *Queries) CreateComment(ctx context.Context, arg CreateCommentParams) (Comment, error) {
	row := q.db.QueryRow(ctx, createComment,
		arg.IssueID,
		arg.WorkspaceID,
		arg.AuthorType,
		arg.AuthorID,
		arg.Content,
		arg.Type,
		arg.ParentID,
	)
	var i Comment
	err := row.Scan(
		&i.ID,
		&i.IssueID,
		&i.AuthorType,
		&i.AuthorID,
		&i.Content,
		&i.Type,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentID,
		&i.WorkspaceID,
		&i.ResolvedAt,
		&i.ResolvedByType,
		&i.ResolvedByID,
	)
	return i, err
}

const deleteComment = `-- name: DeleteComment :exec
DELETE FROM comment WHERE id = $1 AND workspace_id = $2
`

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

// Defense-in-depth: workspace_id is a SQL-layer tenant guard. See DeleteIssue.
func (q *Queries) DeleteComment(ctx context.Context, arg DeleteCommentParams) error {
	_, err := q.db.Exec(ctx, deleteComment, arg.ID, arg.WorkspaceID)
	return err
}

const getComment = `-- name: GetComment :one
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id FROM comment
WHERE id = $1
`

func (q *Queries) GetComment(ctx context.Context, id pgtype.UUID) (Comment, error) {
	row := q.db.QueryRow(ctx, getComment, id)
	var i Comment
	err := row.Scan(
		&i.ID,
		&i.IssueID,
		&i.AuthorType,
		&i.AuthorID,
		&i.Content,
		&i.Type,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentID,
		&i.WorkspaceID,
		&i.ResolvedAt,
		&i.ResolvedByType,
		&i.ResolvedByID,
	)
	return i, err
}

const getCommentInWorkspace = `-- name: GetCommentInWorkspace :one
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id FROM comment
WHERE id = $1 AND workspace_id = $2
`

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

func (q *Queries) GetCommentInWorkspace(ctx context.Context, arg GetCommentInWorkspaceParams) (Comment, error) {
	row := q.db.QueryRow(ctx, getCommentInWorkspace, arg.ID, arg.WorkspaceID)
	var i Comment
	err := row.Scan(
		&i.ID,
		&i.IssueID,
		&i.AuthorType,
		&i.AuthorID,
		&i.Content,
		&i.Type,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentID,
		&i.WorkspaceID,
		&i.ResolvedAt,
		&i.ResolvedByType,
		&i.ResolvedByID,
	)
	return i, err
}

const getThreadRoot = `-- name: GetThreadRoot :one
WITH RECURSIVE root_of AS (
    SELECT c.id, c.parent_id
    FROM comment c
    WHERE c.id = $1 AND c.workspace_id = $2
    UNION ALL
    SELECT p.id, p.parent_id
    FROM comment p
    JOIN root_of r ON p.id = r.parent_id
)
SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type, c.created_at, c.updated_at, c.parent_id, c.workspace_id, c.resolved_at, c.resolved_by_type, c.resolved_by_id FROM comment c
WHERE c.id = (SELECT id FROM root_of WHERE parent_id IS NULL LIMIT 1)
`

type GetThreadRootParams struct {
	CommentID   pgtype.UUID `json:"comment_id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
}

// Returns the thread-root comment for @comment_id by walking parent_id up to
// the row whose parent_id IS NULL. For a root comment it returns that comment
// itself. Used when callers need thread-level behavior while parent_id remains
// the exact direct parent of a reply. Cycle-safe under the PK constraint (a
// comment cannot be its own ancestor).
func (q *Queries) GetThreadRoot(ctx context.Context, arg GetThreadRootParams) (Comment, error) {
	row := q.db.QueryRow(ctx, getThreadRoot, arg.CommentID, arg.WorkspaceID)
	var i Comment
	err := row.Scan(
		&i.ID,
		&i.IssueID,
		&i.AuthorType,
		&i.AuthorID,
		&i.Content,
		&i.Type,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentID,
		&i.WorkspaceID,
		&i.ResolvedAt,
		&i.ResolvedByType,
		&i.ResolvedByID,
	)
	return i, err
}

const hasAgentCommentedSince = `-- name: HasAgentCommentedSince :one
SELECT EXISTS (
    SELECT 1 FROM comment
    WHERE issue_id = $1
      AND author_type = 'agent'
      AND author_id = $2
      AND created_at >= $3
) AS commented
`

type HasAgentCommentedSinceParams struct {
	IssueID  pgtype.UUID        `json:"issue_id"`
	AuthorID pgtype.UUID        `json:"author_id"`
	Since    pgtype.Timestamptz `json:"since"`
}

func (q *Queries) HasAgentCommentedSince(ctx context.Context, arg HasAgentCommentedSinceParams) (bool, error) {
	row := q.db.QueryRow(ctx, hasAgentCommentedSince, arg.IssueID, arg.AuthorID, arg.Since)
	var commented bool
	err := row.Scan(&commented)
	return commented, err
}

const hasAgentRepliedInThread = `-- name: HasAgentRepliedInThread :one
SELECT count(*) > 0 AS has_replied FROM comment
WHERE parent_id = $1 AND author_type = 'agent' AND author_id = $2
`

type HasAgentRepliedInThreadParams struct {
	ParentID pgtype.UUID `json:"parent_id"`
	AgentID  pgtype.UUID `json:"agent_id"`
}

// Returns true if the given agent has posted a reply in the thread rooted at
// the specified parent comment. Used to detect agent participation in a
// member-started thread so that follow-up member replies still trigger the agent.
func (q *Queries) HasAgentRepliedInThread(ctx context.Context, arg HasAgentRepliedInThreadParams) (bool, error) {
	row := q.db.QueryRow(ctx, hasAgentRepliedInThread, arg.ParentID, arg.AgentID)
	var has_replied bool
	err := row.Scan(&has_replied)
	return has_replied, err
}

const listCommentsForIssue = `-- name: ListCommentsForIssue :many
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id FROM comment
WHERE issue_id = $1 AND workspace_id = $2
ORDER BY created_at ASC, id ASC
LIMIT $3
`

type ListCommentsForIssueParams struct {
	IssueID     pgtype.UUID `json:"issue_id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
	Limit       int32       `json:"limit"`
}

// All comments for an issue in chronological order, capped at $3 (DB safety
// net). Issue p99 is ~30 comments, max ever observed in prod is ~1.1k, so
// the handler-side cap of 2000 is purely defensive.
func (q *Queries) ListCommentsForIssue(ctx context.Context, arg ListCommentsForIssueParams) ([]Comment, error) {
	rows, err := q.db.Query(ctx, listCommentsForIssue, arg.IssueID, arg.WorkspaceID, arg.Limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Comment{}
	for rows.Next() {
		var i Comment
		if err := rows.Scan(
			&i.ID,
			&i.IssueID,
			&i.AuthorType,
			&i.AuthorID,
			&i.Content,
			&i.Type,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentID,
			&i.WorkspaceID,
			&i.ResolvedAt,
			&i.ResolvedByType,
			&i.ResolvedByID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listCommentsSinceForIssue = `-- name: ListCommentsSinceForIssue :many
SELECT id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id FROM comment
WHERE issue_id = $1 AND workspace_id = $2 AND created_at > $3
ORDER BY created_at ASC, id ASC
LIMIT $4
`

type ListCommentsSinceForIssueParams struct {
	IssueID     pgtype.UUID        `json:"issue_id"`
	WorkspaceID pgtype.UUID        `json:"workspace_id"`
	CreatedAt   pgtype.Timestamptz `json:"created_at"`
	Limit       int32              `json:"limit"`
}

// Comments created strictly after $3 in chronological order, capped at $4.
// Powers the CLI's `--since` agent-polling flow.
func (q *Queries) ListCommentsSinceForIssue(ctx context.Context, arg ListCommentsSinceForIssueParams) ([]Comment, error) {
	rows, err := q.db.Query(ctx, listCommentsSinceForIssue,
		arg.IssueID,
		arg.WorkspaceID,
		arg.CreatedAt,
		arg.Limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Comment{}
	for rows.Next() {
		var i Comment
		if err := rows.Scan(
			&i.ID,
			&i.IssueID,
			&i.AuthorType,
			&i.AuthorID,
			&i.Content,
			&i.Type,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentID,
			&i.WorkspaceID,
			&i.ResolvedAt,
			&i.ResolvedByType,
			&i.ResolvedByID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listRecentThreadCommentsForIssue = `-- name: ListRecentThreadCommentsForIssue :many
WITH RECURSIVE membership(id, root_id, comment_created_at) AS (
    -- Each root maps to itself.
    SELECT c.id, c.id AS root_id, c.created_at
    FROM comment c
    WHERE c.issue_id = $1
      AND c.workspace_id = $2
      AND c.parent_id IS NULL
    UNION ALL
    -- Each descendant inherits its parent's root_id.
    SELECT c.id, m.root_id, c.created_at
    FROM comment c
    JOIN membership m ON c.parent_id = m.id
    WHERE c.issue_id = $1
      AND c.workspace_id = $2
),
thread_stats AS (
    SELECT root_id, MAX(comment_created_at)::timestamptz AS last_activity_at
    FROM membership
    GROUP BY root_id
),
picked AS (
    SELECT ts.root_id, ts.last_activity_at
    FROM thread_stats ts
    WHERE (
        $3::boolean = FALSE
        OR (ts.last_activity_at, ts.root_id) < ($4::timestamptz, $5::uuid)
    )
    ORDER BY ts.last_activity_at DESC, ts.root_id DESC
    LIMIT $6
)
SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type,
       c.created_at, c.updated_at, c.parent_id, c.workspace_id,
       c.resolved_at, c.resolved_by_type, c.resolved_by_id,
       p.root_id AS thread_root_id,
       p.last_activity_at AS thread_last_activity_at
FROM picked p
JOIN membership m ON m.root_id = p.root_id
JOIN comment c ON c.id = m.id
ORDER BY p.last_activity_at ASC, p.root_id ASC, c.created_at ASC, c.id ASC
`

type ListRecentThreadCommentsForIssueParams struct {
	IssueID     pgtype.UUID        `json:"issue_id"`
	WorkspaceID pgtype.UUID        `json:"workspace_id"`
	HasCursor   bool               `json:"has_cursor"`
	BeforeAt    pgtype.Timestamptz `json:"before_at"`
	BeforeID    pgtype.UUID        `json:"before_id"`
	ThreadLimit int32              `json:"thread_limit"`
}

type ListRecentThreadCommentsForIssueRow struct {
	ID                   pgtype.UUID        `json:"id"`
	IssueID              pgtype.UUID        `json:"issue_id"`
	AuthorType           string             `json:"author_type"`
	AuthorID             pgtype.UUID        `json:"author_id"`
	Content              string             `json:"content"`
	Type                 string             `json:"type"`
	CreatedAt            pgtype.Timestamptz `json:"created_at"`
	UpdatedAt            pgtype.Timestamptz `json:"updated_at"`
	ParentID             pgtype.UUID        `json:"parent_id"`
	WorkspaceID          pgtype.UUID        `json:"workspace_id"`
	ResolvedAt           pgtype.Timestamptz `json:"resolved_at"`
	ResolvedByType       pgtype.Text        `json:"resolved_by_type"`
	ResolvedByID         pgtype.UUID        `json:"resolved_by_id"`
	ThreadRootID         pgtype.UUID        `json:"thread_root_id"`
	ThreadLastActivityAt pgtype.Timestamptz `json:"thread_last_activity_at"`
}

// Returns the N most recently active threads (root + every descendant) rather
// than the N most recent rows. A thread's "last activity" is MAX(created_at)
// over its whole subtree; threads are ranked by (last_activity_at DESC,
// root_id DESC) and the top N are expanded.
//
// Why thread-grouped instead of row-recent: with row-recent the newest 20
// comments can come from 8 different threads — the agent sees 8 unrelated
// tails. With thread-grouped the agent sees N complete conversational arcs,
// which matches how a human reads an issue (#2340).
//
// Response ordering:
//
//	threads:     (thread_last_activity_at ASC, root_id ASC)
//	in-thread:   (created_at ASC, id ASC)
//
// So the oldest-active thread appears first and the most recently-active
// thread is at the tail, closest to "now" in an agent prompt.
//
// Cursor scrolls back through threads. When @has_cursor=TRUE only threads
// with (last_activity_at, root_id) < (@before_at, @before_id) are eligible.
// The cursor is a THREAD cursor — both values identify a thread (its last
// activity timestamp and its root comment id), not a single row.
//
// The recursive `membership` CTE labels each comment with its thread root by
// walking down from every root. It does not assume any maximum nesting depth,
// which preserves correctness even if the schema ever allows reply-of-reply
// (the agent path in TaskService.createAgentComment collapses to root today,
// but the user-facing CreateComment handler does not enforce it).
func (q *Queries) ListRecentThreadCommentsForIssue(ctx context.Context, arg ListRecentThreadCommentsForIssueParams) ([]ListRecentThreadCommentsForIssueRow, error) {
	rows, err := q.db.Query(ctx, listRecentThreadCommentsForIssue,
		arg.IssueID,
		arg.WorkspaceID,
		arg.HasCursor,
		arg.BeforeAt,
		arg.BeforeID,
		arg.ThreadLimit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ListRecentThreadCommentsForIssueRow{}
	for rows.Next() {
		var i ListRecentThreadCommentsForIssueRow
		if err := rows.Scan(
			&i.ID,
			&i.IssueID,
			&i.AuthorType,
			&i.AuthorID,
			&i.Content,
			&i.Type,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentID,
			&i.WorkspaceID,
			&i.ResolvedAt,
			&i.ResolvedByType,
			&i.ResolvedByID,
			&i.ThreadRootID,
			&i.ThreadLastActivityAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listRootCommentsForIssue = `-- name: ListRootCommentsForIssue :many
WITH RECURSIVE selected_roots AS (
    SELECT c.id, c.created_at
    FROM comment c
    WHERE c.issue_id = $1
      AND c.workspace_id = $2
      AND c.parent_id IS NULL
    ORDER BY c.created_at ASC, c.id ASC
    LIMIT $3
),
membership(id, root_id, comment_created_at) AS (
    SELECT sr.id, sr.id AS root_id, sr.created_at
    FROM selected_roots sr
    UNION ALL
    SELECT c.id, m.root_id, c.created_at
    FROM comment c
    JOIN membership m ON c.parent_id = m.id
    WHERE c.issue_id = $1
      AND c.workspace_id = $2
),
thread_stats AS (
    SELECT root_id,
           (COUNT(*) - 1)::int AS reply_count,
           MAX(comment_created_at)::timestamptz AS last_activity_at
    FROM membership
    GROUP BY root_id
)
SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type,
       c.created_at, c.updated_at, c.parent_id, c.workspace_id,
       c.resolved_at, c.resolved_by_type, c.resolved_by_id,
       ts.reply_count AS reply_count,
       ts.last_activity_at AS last_activity_at
FROM selected_roots sr
JOIN comment c ON c.id = sr.id
JOIN thread_stats ts ON ts.root_id = sr.id
ORDER BY c.created_at ASC, c.id ASC
`

type ListRootCommentsForIssueParams struct {
	IssueID     pgtype.UUID `json:"issue_id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
	RowLimit    int32       `json:"row_limit"`
}

type ListRootCommentsForIssueRow struct {
	ID             pgtype.UUID        `json:"id"`
	IssueID        pgtype.UUID        `json:"issue_id"`
	AuthorType     string             `json:"author_type"`
	AuthorID       pgtype.UUID        `json:"author_id"`
	Content        string             `json:"content"`
	Type           string             `json:"type"`
	CreatedAt      pgtype.Timestamptz `json:"created_at"`
	UpdatedAt      pgtype.Timestamptz `json:"updated_at"`
	ParentID       pgtype.UUID        `json:"parent_id"`
	WorkspaceID    pgtype.UUID        `json:"workspace_id"`
	ResolvedAt     pgtype.Timestamptz `json:"resolved_at"`
	ResolvedByType pgtype.Text        `json:"resolved_by_type"`
	ResolvedByID   pgtype.UUID        `json:"resolved_by_id"`
	ReplyCount     int32              `json:"reply_count"`
	LastActivityAt pgtype.Timestamptz `json:"last_activity_at"`
}

// Top-level comments only, in issue chronological order, each annotated with
// per-thread orientation stats: reply_count (number of descendants) and
// last_activity_at (MAX(created_at) over the whole subtree). This powers
// `comment list --roots-only` so agents can not only orient around the global
// discussion but also triage which thread to drill into (biggest / most
// recently active) before fetching any specific reply thread.
//
// `selected_roots` picks the roots we will actually return first (the chrono
// page of size @row_limit), so the recursive `membership` walk only expands
// those threads' subtrees instead of every thread in the issue. membership
// labels each comment with its thread root by walking down from the selected
// roots, so the counts stay correct even if the schema ever allows
// reply-of-reply (the write path collapses to root today, but does not enforce
// it). Mirrors ListRecentThreadCommentsForIssue's stats CTE.
func (q *Queries) ListRootCommentsForIssue(ctx context.Context, arg ListRootCommentsForIssueParams) ([]ListRootCommentsForIssueRow, error) {
	rows, err := q.db.Query(ctx, listRootCommentsForIssue, arg.IssueID, arg.WorkspaceID, arg.RowLimit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ListRootCommentsForIssueRow{}
	for rows.Next() {
		var i ListRootCommentsForIssueRow
		if err := rows.Scan(
			&i.ID,
			&i.IssueID,
			&i.AuthorType,
			&i.AuthorID,
			&i.Content,
			&i.Type,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentID,
			&i.WorkspaceID,
			&i.ResolvedAt,
			&i.ResolvedByType,
			&i.ResolvedByID,
			&i.ReplyCount,
			&i.LastActivityAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listRootCommentsSinceForIssue = `-- name: ListRootCommentsSinceForIssue :many
WITH RECURSIVE selected_roots AS (
    SELECT c.id, c.created_at
    FROM comment c
    WHERE c.issue_id = $1
      AND c.workspace_id = $2
      AND c.parent_id IS NULL
      AND c.created_at > $3
    ORDER BY c.created_at ASC, c.id ASC
    LIMIT $4
),
membership(id, root_id, comment_created_at) AS (
    SELECT sr.id, sr.id AS root_id, sr.created_at
    FROM selected_roots sr
    UNION ALL
    SELECT c.id, m.root_id, c.created_at
    FROM comment c
    JOIN membership m ON c.parent_id = m.id
    WHERE c.issue_id = $1
      AND c.workspace_id = $2
),
thread_stats AS (
    SELECT root_id,
           (COUNT(*) - 1)::int AS reply_count,
           MAX(comment_created_at)::timestamptz AS last_activity_at
    FROM membership
    GROUP BY root_id
)
SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type,
       c.created_at, c.updated_at, c.parent_id, c.workspace_id,
       c.resolved_at, c.resolved_by_type, c.resolved_by_id,
       ts.reply_count AS reply_count,
       ts.last_activity_at AS last_activity_at
FROM selected_roots sr
JOIN comment c ON c.id = sr.id
JOIN thread_stats ts ON ts.root_id = sr.id
ORDER BY c.created_at ASC, c.id ASC
`

type ListRootCommentsSinceForIssueParams struct {
	IssueID     pgtype.UUID        `json:"issue_id"`
	WorkspaceID pgtype.UUID        `json:"workspace_id"`
	Since       pgtype.Timestamptz `json:"since"`
	RowLimit    int32              `json:"row_limit"`
}

type ListRootCommentsSinceForIssueRow struct {
	ID             pgtype.UUID        `json:"id"`
	IssueID        pgtype.UUID        `json:"issue_id"`
	AuthorType     string             `json:"author_type"`
	AuthorID       pgtype.UUID        `json:"author_id"`
	Content        string             `json:"content"`
	Type           string             `json:"type"`
	CreatedAt      pgtype.Timestamptz `json:"created_at"`
	UpdatedAt      pgtype.Timestamptz `json:"updated_at"`
	ParentID       pgtype.UUID        `json:"parent_id"`
	WorkspaceID    pgtype.UUID        `json:"workspace_id"`
	ResolvedAt     pgtype.Timestamptz `json:"resolved_at"`
	ResolvedByType pgtype.Text        `json:"resolved_by_type"`
	ResolvedByID   pgtype.UUID        `json:"resolved_by_id"`
	ReplyCount     int32              `json:"reply_count"`
	LastActivityAt pgtype.Timestamptz `json:"last_activity_at"`
}

// Top-level comments created strictly after @since, each annotated with the
// same reply_count / last_activity_at stats as ListRootCommentsForIssue. The
// @since filter narrows which roots are returned; the stats are still computed
// over each selected thread's full subtree (so a freshly created root with no
// replies reports reply_count 0 and last_activity_at = its own created_at).
// selected_roots applies the @since + @row_limit cut up front so the recursive
// membership walk only touches the subtrees of the roots we actually return.
func (q *Queries) ListRootCommentsSinceForIssue(ctx context.Context, arg ListRootCommentsSinceForIssueParams) ([]ListRootCommentsSinceForIssueRow, error) {
	rows, err := q.db.Query(ctx, listRootCommentsSinceForIssue,
		arg.IssueID,
		arg.WorkspaceID,
		arg.Since,
		arg.RowLimit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ListRootCommentsSinceForIssueRow{}
	for rows.Next() {
		var i ListRootCommentsSinceForIssueRow
		if err := rows.Scan(
			&i.ID,
			&i.IssueID,
			&i.AuthorType,
			&i.AuthorID,
			&i.Content,
			&i.Type,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentID,
			&i.WorkspaceID,
			&i.ResolvedAt,
			&i.ResolvedByType,
			&i.ResolvedByID,
			&i.ReplyCount,
			&i.LastActivityAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listThreadCommentsForIssue = `-- name: ListThreadCommentsForIssue :many
WITH RECURSIVE root_of AS (
    -- Walk up from the anchor until parent_id IS NULL.
    SELECT c.id, c.parent_id
    FROM comment c
    WHERE c.id = $2 AND c.issue_id = $3 AND c.workspace_id = $4
    UNION ALL
    SELECT p.id, p.parent_id
    FROM comment p
    JOIN root_of r ON p.id = r.parent_id
),
thread_root AS (
    SELECT id FROM root_of WHERE parent_id IS NULL LIMIT 1
),
descendants AS (
    -- Start from the root, then keep adding any comment whose parent is
    -- already in the set. Cycle-safe under PK constraint (a comment cannot
    -- be its own ancestor).
    SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type,
           c.created_at, c.updated_at, c.parent_id, c.workspace_id,
           c.resolved_at, c.resolved_by_type, c.resolved_by_id
    FROM comment c
    JOIN thread_root tr ON c.id = tr.id
    UNION
    SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type,
           c.created_at, c.updated_at, c.parent_id, c.workspace_id,
           c.resolved_at, c.resolved_by_type, c.resolved_by_id
    FROM comment c
    JOIN descendants d ON c.parent_id = d.id
    WHERE c.issue_id = $3 AND c.workspace_id = $4
)
SELECT id, issue_id, author_type, author_id, content, type,
       created_at, updated_at, parent_id, workspace_id,
       resolved_at, resolved_by_type, resolved_by_id
FROM descendants
ORDER BY created_at ASC, id ASC
LIMIT $1
`

type ListThreadCommentsForIssueParams struct {
	RowLimit    int32       `json:"row_limit"`
	AnchorID    pgtype.UUID `json:"anchor_id"`
	IssueID     pgtype.UUID `json:"issue_id"`
	WorkspaceID pgtype.UUID `json:"workspace_id"`
}

type ListThreadCommentsForIssueRow struct {
	ID             pgtype.UUID        `json:"id"`
	IssueID        pgtype.UUID        `json:"issue_id"`
	AuthorType     string             `json:"author_type"`
	AuthorID       pgtype.UUID        `json:"author_id"`
	Content        string             `json:"content"`
	Type           string             `json:"type"`
	CreatedAt      pgtype.Timestamptz `json:"created_at"`
	UpdatedAt      pgtype.Timestamptz `json:"updated_at"`
	ParentID       pgtype.UUID        `json:"parent_id"`
	WorkspaceID    pgtype.UUID        `json:"workspace_id"`
	ResolvedAt     pgtype.Timestamptz `json:"resolved_at"`
	ResolvedByType pgtype.Text        `json:"resolved_by_type"`
	ResolvedByID   pgtype.UUID        `json:"resolved_by_id"`
}

// Returns the root of the thread containing @anchor_id plus every descendant
// (recursive — supports real reply-to-reply nesting). @anchor_id may itself be
// a root or any reply in the thread. Output is chronological so it can be fed
// straight to the agent.
func (q *Queries) ListThreadCommentsForIssue(ctx context.Context, arg ListThreadCommentsForIssueParams) ([]ListThreadCommentsForIssueRow, error) {
	rows, err := q.db.Query(ctx, listThreadCommentsForIssue,
		arg.RowLimit,
		arg.AnchorID,
		arg.IssueID,
		arg.WorkspaceID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ListThreadCommentsForIssueRow{}
	for rows.Next() {
		var i ListThreadCommentsForIssueRow
		if err := rows.Scan(
			&i.ID,
			&i.IssueID,
			&i.AuthorType,
			&i.AuthorID,
			&i.Content,
			&i.Type,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentID,
			&i.WorkspaceID,
			&i.ResolvedAt,
			&i.ResolvedByType,
			&i.ResolvedByID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listThreadCommentsForIssuePaged = `-- name: ListThreadCommentsForIssuePaged :many
WITH RECURSIVE root_of AS (
    SELECT c.id, c.parent_id
    FROM comment c
    WHERE c.id = $1 AND c.issue_id = $2 AND c.workspace_id = $3
    UNION ALL
    SELECT p.id, p.parent_id
    FROM comment p
    JOIN root_of r ON p.id = r.parent_id
),
thread_root AS (
    SELECT id FROM root_of WHERE parent_id IS NULL LIMIT 1
),
descendants AS (
    SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type,
           c.created_at, c.updated_at, c.parent_id, c.workspace_id,
           c.resolved_at, c.resolved_by_type, c.resolved_by_id
    FROM comment c
    JOIN thread_root tr ON c.id = tr.id
    UNION
    SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type,
           c.created_at, c.updated_at, c.parent_id, c.workspace_id,
           c.resolved_at, c.resolved_by_type, c.resolved_by_id
    FROM comment c
    JOIN descendants d ON c.parent_id = d.id
    WHERE c.issue_id = $2 AND c.workspace_id = $3
),
reply_page AS (
    SELECT d.id, d.issue_id, d.author_type, d.author_id, d.content, d.type,
           d.created_at, d.updated_at, d.parent_id, d.workspace_id,
           d.resolved_at, d.resolved_by_type, d.resolved_by_id
    FROM descendants d
    WHERE d.id NOT IN (SELECT id FROM thread_root)
      AND (
          $4::boolean = FALSE
          OR (d.created_at, d.id) < ($5::timestamptz, $6::uuid)
      )
    ORDER BY d.created_at DESC, d.id DESC
    LIMIT $7
)
SELECT id, issue_id, author_type, author_id, content, type,
       created_at, updated_at, parent_id, workspace_id,
       resolved_at, resolved_by_type, resolved_by_id
FROM (
    SELECT d.id, d.issue_id, d.author_type, d.author_id, d.content, d.type,
           d.created_at, d.updated_at, d.parent_id, d.workspace_id,
           d.resolved_at, d.resolved_by_type, d.resolved_by_id
    FROM descendants d
    JOIN thread_root tr ON d.id = tr.id
    UNION ALL
    SELECT id, issue_id, author_type, author_id, content, type,
           created_at, updated_at, parent_id, workspace_id,
           resolved_at, resolved_by_type, resolved_by_id
    FROM reply_page
) combined
ORDER BY created_at ASC, id ASC
`

type ListThreadCommentsForIssuePagedParams struct {
	AnchorID    pgtype.UUID        `json:"anchor_id"`
	IssueID     pgtype.UUID        `json:"issue_id"`
	WorkspaceID pgtype.UUID        `json:"workspace_id"`
	HasCursor   bool               `json:"has_cursor"`
	BeforeAt    pgtype.Timestamptz `json:"before_at"`
	BeforeID    pgtype.UUID        `json:"before_id"`
	ReplyLimit  int32              `json:"reply_limit"`
}

type ListThreadCommentsForIssuePagedRow struct {
	ID             pgtype.UUID        `json:"id"`
	IssueID        pgtype.UUID        `json:"issue_id"`
	AuthorType     string             `json:"author_type"`
	AuthorID       pgtype.UUID        `json:"author_id"`
	Content        string             `json:"content"`
	Type           string             `json:"type"`
	CreatedAt      pgtype.Timestamptz `json:"created_at"`
	UpdatedAt      pgtype.Timestamptz `json:"updated_at"`
	ParentID       pgtype.UUID        `json:"parent_id"`
	WorkspaceID    pgtype.UUID        `json:"workspace_id"`
	ResolvedAt     pgtype.Timestamptz `json:"resolved_at"`
	ResolvedByType pgtype.Text        `json:"resolved_by_type"`
	ResolvedByID   pgtype.UUID        `json:"resolved_by_id"`
}

// Same root-walk + descendants expansion as ListThreadCommentsForIssue, but
// returns root + only the @reply_limit most recent replies (per the
// (created_at, id) composite key). When @has_cursor=TRUE only replies with
// (created_at, id) < (@before_at, @before_id) are eligible — that is the
// cursor for scrolling *within* a thread.
//
// Root is unconditional: it is included regardless of @reply_limit (even 0)
// and regardless of the cursor. A reader landing on a long thread needs the
// root for the "what is this thread about" context, even if every reply has
// been paginated past.
//
// Reply selection happens DESC (newest replies first) so the cursor walks
// toward older replies; the outer SELECT then re-sorts the combined output
// ASC so the body stays chronological (oldest → newest), matching every
// other comment list path.
func (q *Queries) ListThreadCommentsForIssuePaged(ctx context.Context, arg ListThreadCommentsForIssuePagedParams) ([]ListThreadCommentsForIssuePagedRow, error) {
	rows, err := q.db.Query(ctx, listThreadCommentsForIssuePaged,
		arg.AnchorID,
		arg.IssueID,
		arg.WorkspaceID,
		arg.HasCursor,
		arg.BeforeAt,
		arg.BeforeID,
		arg.ReplyLimit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []ListThreadCommentsForIssuePagedRow{}
	for rows.Next() {
		var i ListThreadCommentsForIssuePagedRow
		if err := rows.Scan(
			&i.ID,
			&i.IssueID,
			&i.AuthorType,
			&i.AuthorID,
			&i.Content,
			&i.Type,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentID,
			&i.WorkspaceID,
			&i.ResolvedAt,
			&i.ResolvedByType,
			&i.ResolvedByID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const resolveComment = `-- name: ResolveComment :one
UPDATE comment SET
    resolved_at = COALESCE(resolved_at, now()),
    resolved_by_type = COALESCE(resolved_by_type, $2),
    resolved_by_id = COALESCE(resolved_by_id, $3),
    updated_at = CASE WHEN resolved_at IS NULL THEN now() ELSE updated_at END
WHERE id = $1
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
`

type ResolveCommentParams struct {
	ID             pgtype.UUID `json:"id"`
	ResolvedByType pgtype.Text `json:"resolved_by_type"`
	ResolvedByID   pgtype.UUID `json:"resolved_by_id"`
}

// Idempotent: re-resolving keeps the original resolved_at + resolver. Always
// returns the row so the handler can surface the canonical state.
func (q *Queries) ResolveComment(ctx context.Context, arg ResolveCommentParams) (Comment, error) {
	row := q.db.QueryRow(ctx, resolveComment, arg.ID, arg.ResolvedByType, arg.ResolvedByID)
	var i Comment
	err := row.Scan(
		&i.ID,
		&i.IssueID,
		&i.AuthorType,
		&i.AuthorID,
		&i.Content,
		&i.Type,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentID,
		&i.WorkspaceID,
		&i.ResolvedAt,
		&i.ResolvedByType,
		&i.ResolvedByID,
	)
	return i, err
}

const unresolveComment = `-- name: UnresolveComment :one
UPDATE comment SET
    resolved_at = NULL,
    resolved_by_type = NULL,
    resolved_by_id = NULL,
    updated_at = CASE WHEN resolved_at IS NOT NULL THEN now() ELSE updated_at END
WHERE id = $1
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
`

// Idempotent: a no-op clear (already unresolved) just returns the row.
func (q *Queries) UnresolveComment(ctx context.Context, id pgtype.UUID) (Comment, error) {
	row := q.db.QueryRow(ctx, unresolveComment, id)
	var i Comment
	err := row.Scan(
		&i.ID,
		&i.IssueID,
		&i.AuthorType,
		&i.AuthorID,
		&i.Content,
		&i.Type,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentID,
		&i.WorkspaceID,
		&i.ResolvedAt,
		&i.ResolvedByType,
		&i.ResolvedByID,
	)
	return i, err
}

const updateComment = `-- name: UpdateComment :one
UPDATE comment SET
    content = $2,
    updated_at = now()
WHERE id = $1
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id
`

type UpdateCommentParams struct {
	ID      pgtype.UUID `json:"id"`
	Content string      `json:"content"`
}

func (q *Queries) UpdateComment(ctx context.Context, arg UpdateCommentParams) (Comment, error) {
	row := q.db.QueryRow(ctx, updateComment, arg.ID, arg.Content)
	var i Comment
	err := row.Scan(
		&i.ID,
		&i.IssueID,
		&i.AuthorType,
		&i.AuthorID,
		&i.Content,
		&i.Type,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentID,
		&i.WorkspaceID,
		&i.ResolvedAt,
		&i.ResolvedByType,
		&i.ResolvedByID,
	)
	return i, err
}
