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

package database

import (
	"context"
	"database/sql"
	"encoding/json"
	"time"

	"github.com/google/uuid"
	"github.com/lib/pq"
	"github.com/sqlc-dev/pqtype"
)

const activityBumpWorkspace = `-- name: ActivityBumpWorkspace :exec
WITH latest AS (
	SELECT
		workspace_builds.id::uuid AS build_id,
		workspace_builds.deadline::timestamp with time zone AS build_deadline,
		workspace_builds.max_deadline::timestamp with time zone AS build_max_deadline,
		workspace_builds.transition AS build_transition,
		provisioner_jobs.completed_at::timestamp with time zone AS job_completed_at,
		templates.activity_bump AS activity_bump,
		(
			CASE
				-- If the extension would push us over the next_autostart
				-- interval, then extend the deadline by the full TTL (NOT
				-- activity bump) from the autostart time. This will essentially
				-- be as if the workspace auto started at the given time and the
				-- original TTL was applied.
				--
				-- Sadly we can't define 'activity_bump_interval' above since
				-- it won't be available for this CASE statement, so we have to
				-- copy the cast twice.
				WHEN NOW() + (templates.activity_bump / 1000 / 1000 / 1000 || ' seconds')::interval > $1 :: timestamptz
				    -- If the autostart is behind now(), then the
					-- autostart schedule is either the 0 time and not provided,
					-- or it was the autostart in the past, which is no longer
					-- relevant. If autostart is > 0 and in the past, then
					-- that is a mistake by the caller.
					AND $1 > NOW()
					THEN
					-- Extend to the autostart, then add the activity bump
					(($1 :: timestamptz) - NOW()) + CASE
						WHEN templates.allow_user_autostop
					    	THEN (workspaces.ttl / 1000 / 1000 / 1000 || ' seconds')::interval
							ELSE (templates.default_ttl / 1000 / 1000 / 1000 || ' seconds')::interval
					END

				-- Default to the activity bump duration.
				ELSE
					(templates.activity_bump / 1000 / 1000 / 1000 || ' seconds')::interval
			END
		) AS ttl_interval
	FROM workspace_builds
	JOIN provisioner_jobs
		ON provisioner_jobs.id = workspace_builds.job_id
	JOIN workspaces
		ON workspaces.id = workspace_builds.workspace_id
	JOIN templates
		ON templates.id = workspaces.template_id
	WHERE
		workspace_builds.workspace_id = $2::uuid
		-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
		-- are managed by the reconciliation loop and not subject to activity bumping
	  	AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
	ORDER BY workspace_builds.build_number DESC
	LIMIT 1
)
UPDATE
	workspace_builds wb
SET
	updated_at = NOW(),
	deadline = CASE
		WHEN l.build_max_deadline = '0001-01-01 00:00:00+00'
		-- Never reduce the deadline from activity.
		THEN GREATEST(wb.deadline, NOW() + l.ttl_interval)
		ELSE LEAST(GREATEST(wb.deadline, NOW() + l.ttl_interval), l.build_max_deadline)
	END
FROM latest l
WHERE wb.id = l.build_id
AND l.job_completed_at IS NOT NULL
AND l.activity_bump > 0
AND l.build_transition = 'start'
AND l.ttl_interval > '0 seconds'::interval
AND l.build_deadline != '0001-01-01 00:00:00+00'
AND l.build_deadline - (l.ttl_interval * 0.95) < NOW()
`

type ActivityBumpWorkspaceParams struct {
	NextAutostart time.Time `db:"next_autostart" json:"next_autostart"`
	WorkspaceID   uuid.UUID `db:"workspace_id" json:"workspace_id"`
}

// Bumps the workspace deadline by the template's configured "activity_bump"
// duration (default 1h). If the workspace bump will cross an autostart
// threshold, then the bump is autostart + TTL. This is the deadline behavior if
// the workspace was to autostart from a stopped state.
//
// Max deadline is respected, and the deadline will never be bumped past it.
// The deadline will never decrease.
// We only bump if the template has an activity bump duration set.
// We only bump if the raw interval is positive and non-zero.
// We only bump if workspace shutdown is manual.
// We only bump when 5% of the deadline has elapsed.
func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error {
	_, err := q.db.ExecContext(ctx, activityBumpWorkspace, arg.NextAutostart, arg.WorkspaceID)
	return err
}

const deleteAIProviderKey = `-- name: DeleteAIProviderKey :exec
DELETE FROM
    ai_provider_keys
WHERE
    id = $1::uuid
`

func (q *sqlQuerier) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteAIProviderKey, id)
	return err
}

const getAIProviderKeyByID = `-- name: GetAIProviderKeyByID :one
SELECT
    id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
    ai_provider_keys
WHERE
    id = $1::uuid
`

func (q *sqlQuerier) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (AIProviderKey, error) {
	row := q.db.QueryRowContext(ctx, getAIProviderKeyByID, id)
	var i AIProviderKey
	err := row.Scan(
		&i.ID,
		&i.ProviderID,
		&i.APIKey,
		&i.ApiKeyKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getAIProviderKeyPresence = `-- name: GetAIProviderKeyPresence :many
SELECT DISTINCT
    provider_id
FROM
    ai_provider_keys
WHERE
    provider_id = ANY($1::uuid[])
ORDER BY
    provider_id ASC
`

// Returns the provider IDs that have at least one provider-scoped key.
func (q *sqlQuerier) GetAIProviderKeyPresence(ctx context.Context, providerIds []uuid.UUID) ([]uuid.UUID, error) {
	rows, err := q.db.QueryContext(ctx, getAIProviderKeyPresence, pq.Array(providerIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []uuid.UUID
	for rows.Next() {
		var provider_id uuid.UUID
		if err := rows.Scan(&provider_id); err != nil {
			return nil, err
		}
		items = append(items, provider_id)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAIProviderKeys = `-- name: GetAIProviderKeys :many
SELECT
    ai_provider_keys.id, ai_provider_keys.provider_id, ai_provider_keys.api_key, ai_provider_keys.api_key_key_id, ai_provider_keys.created_at, ai_provider_keys.updated_at
FROM
    ai_provider_keys
    JOIN ai_providers ON ai_providers.id = ai_provider_keys.provider_id
WHERE
    $1::boolean OR NOT ai_providers.deleted
ORDER BY
    ai_provider_keys.provider_id ASC,
    ai_provider_keys.created_at ASC,
    ai_provider_keys.id ASC
`

// Returns AI provider key rows. By default, only rows whose parent
// provider is live (deleted = FALSE) are returned, so the API list
// handler can fetch every visible provider's keys in a single query.
// The dbcrypt key rotation utility passes include_deleted=TRUE to
// re-encrypt rows that belong to soft-deleted providers as well.
func (q *sqlQuerier) GetAIProviderKeys(ctx context.Context, includeDeleted bool) ([]AIProviderKey, error) {
	rows, err := q.db.QueryContext(ctx, getAIProviderKeys, includeDeleted)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIProviderKey
	for rows.Next() {
		var i AIProviderKey
		if err := rows.Scan(
			&i.ID,
			&i.ProviderID,
			&i.APIKey,
			&i.ApiKeyKeyID,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAIProviderKeysByProviderID = `-- name: GetAIProviderKeysByProviderID :many
SELECT
    id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
    ai_provider_keys
WHERE
    provider_id = $1::uuid
ORDER BY
    created_at ASC,
    id ASC
`

// Returns all keys for a provider, ordered by created_at ASC so the
// oldest key is returned first. AI Bridge currently uses the oldest
// key per provider; multiple keys are stored to support future
// failover and rotation flows.
func (q *sqlQuerier) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]AIProviderKey, error) {
	rows, err := q.db.QueryContext(ctx, getAIProviderKeysByProviderID, providerID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIProviderKey
	for rows.Next() {
		var i AIProviderKey
		if err := rows.Scan(
			&i.ID,
			&i.ProviderID,
			&i.APIKey,
			&i.ApiKeyKeyID,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAIProviderKeysByProviderIDs = `-- name: GetAIProviderKeysByProviderIDs :many
SELECT
    id, provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
    ai_provider_keys
WHERE
    provider_id = ANY($1::uuid[])
ORDER BY
    provider_id ASC,
    created_at ASC,
    id ASC
`

// Returns all keys for the requested providers, ordered by provider then created_at ASC
// so callers can select the oldest non-empty key per provider without issuing N queries.
func (q *sqlQuerier) GetAIProviderKeysByProviderIDs(ctx context.Context, providerIds []uuid.UUID) ([]AIProviderKey, error) {
	rows, err := q.db.QueryContext(ctx, getAIProviderKeysByProviderIDs, pq.Array(providerIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIProviderKey
	for rows.Next() {
		var i AIProviderKey
		if err := rows.Scan(
			&i.ID,
			&i.ProviderID,
			&i.APIKey,
			&i.ApiKeyKeyID,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertAIProviderKey = `-- name: InsertAIProviderKey :one
INSERT INTO ai_provider_keys (
    id,
    provider_id,
    api_key,
    api_key_key_id,
    created_at,
    updated_at
) VALUES (
    $1::uuid,
    $2::uuid,
    $3::text,
    $4::text,
    $5::timestamptz,
    $6::timestamptz
)
RETURNING
    id, provider_id, api_key, api_key_key_id, created_at, updated_at
`

type InsertAIProviderKeyParams struct {
	ID          uuid.UUID      `db:"id" json:"id"`
	ProviderID  uuid.UUID      `db:"provider_id" json:"provider_id"`
	APIKey      string         `db:"api_key" json:"api_key"`
	ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
	CreatedAt   time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt   time.Time      `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error) {
	row := q.db.QueryRowContext(ctx, insertAIProviderKey,
		arg.ID,
		arg.ProviderID,
		arg.APIKey,
		arg.ApiKeyKeyID,
		arg.CreatedAt,
		arg.UpdatedAt,
	)
	var i AIProviderKey
	err := row.Scan(
		&i.ID,
		&i.ProviderID,
		&i.APIKey,
		&i.ApiKeyKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const updateEncryptedAIProviderKey = `-- name: UpdateEncryptedAIProviderKey :one
UPDATE
    ai_provider_keys
SET
    api_key = $1::text,
    api_key_key_id = $2::text,
    updated_at = NOW()
WHERE
    id = $3::uuid
RETURNING
    id, provider_id, api_key, api_key_key_id, created_at, updated_at
`

type UpdateEncryptedAIProviderKeyParams struct {
	APIKey      string         `db:"api_key" json:"api_key"`
	ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
	ID          uuid.UUID      `db:"id" json:"id"`
}

// Updates only the encrypted columns (api_key, api_key_key_id) and
// the updated_at timestamp on a row. Used by the dbcrypt key
// rotation utility to re-encrypt or decrypt rows in place.
func (q *sqlQuerier) UpdateEncryptedAIProviderKey(ctx context.Context, arg UpdateEncryptedAIProviderKeyParams) (AIProviderKey, error) {
	row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderKey, arg.APIKey, arg.ApiKeyKeyID, arg.ID)
	var i AIProviderKey
	err := row.Scan(
		&i.ID,
		&i.ProviderID,
		&i.APIKey,
		&i.ApiKeyKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const deleteAIProviderByID = `-- name: DeleteAIProviderByID :exec
UPDATE
    ai_providers
SET
    deleted = TRUE,
    enabled = FALSE,
    updated_at = NOW()
WHERE
    id = $1::uuid AND deleted = FALSE
`

func (q *sqlQuerier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteAIProviderByID, id)
	return err
}

const getAIProviderByID = `-- name: GetAIProviderByID :one
SELECT
    id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
FROM
    ai_providers
WHERE
    id = $1::uuid AND deleted = FALSE
`

func (q *sqlQuerier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIProvider, error) {
	row := q.db.QueryRowContext(ctx, getAIProviderByID, id)
	var i AIProvider
	err := row.Scan(
		&i.ID,
		&i.Type,
		&i.Name,
		&i.DisplayName,
		&i.Enabled,
		&i.Deleted,
		&i.BaseUrl,
		&i.Settings,
		&i.SettingsKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getAIProviderByIDForReferenceLock = `-- name: GetAIProviderByIDForReferenceLock :one
SELECT
    id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
FROM
    ai_providers
WHERE
    id = $1::uuid AND deleted = FALSE
FOR SHARE
`

// Lock the provider row until the model-config write completes. The
// transaction alone does not stop a concurrent soft-delete or disable
// between validation and writing the model config reference.
func (q *sqlQuerier) GetAIProviderByIDForReferenceLock(ctx context.Context, id uuid.UUID) (AIProvider, error) {
	row := q.db.QueryRowContext(ctx, getAIProviderByIDForReferenceLock, id)
	var i AIProvider
	err := row.Scan(
		&i.ID,
		&i.Type,
		&i.Name,
		&i.DisplayName,
		&i.Enabled,
		&i.Deleted,
		&i.BaseUrl,
		&i.Settings,
		&i.SettingsKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getAIProviderByName = `-- name: GetAIProviderByName :one
SELECT
    id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
FROM
    ai_providers
WHERE
    name = $1::text AND deleted = FALSE
`

func (q *sqlQuerier) GetAIProviderByName(ctx context.Context, name string) (AIProvider, error) {
	row := q.db.QueryRowContext(ctx, getAIProviderByName, name)
	var i AIProvider
	err := row.Scan(
		&i.ID,
		&i.Type,
		&i.Name,
		&i.DisplayName,
		&i.Enabled,
		&i.Deleted,
		&i.BaseUrl,
		&i.Settings,
		&i.SettingsKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getAIProviders = `-- name: GetAIProviders :many
SELECT
    id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
FROM
    ai_providers
WHERE
    ($1::boolean OR NOT deleted)
    AND ($2::boolean OR enabled)
ORDER BY
    name ASC
`

type GetAIProvidersParams struct {
	IncludeDeleted  bool `db:"include_deleted" json:"include_deleted"`
	IncludeDisabled bool `db:"include_disabled" json:"include_disabled"`
}

// Returns AI provider rows. Soft-deleted and disabled rows are excluded
// unless include_deleted or include_disabled is set.
func (q *sqlQuerier) GetAIProviders(ctx context.Context, arg GetAIProvidersParams) ([]AIProvider, error) {
	rows, err := q.db.QueryContext(ctx, getAIProviders, arg.IncludeDeleted, arg.IncludeDisabled)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIProvider
	for rows.Next() {
		var i AIProvider
		if err := rows.Scan(
			&i.ID,
			&i.Type,
			&i.Name,
			&i.DisplayName,
			&i.Enabled,
			&i.Deleted,
			&i.BaseUrl,
			&i.Settings,
			&i.SettingsKeyID,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertAIProvider = `-- name: InsertAIProvider :one
INSERT INTO ai_providers (
    id,
    type,
    name,
    display_name,
    enabled,
    base_url,
    settings,
    settings_key_id
) VALUES (
    $1::uuid,
    $2::ai_provider_type,
    $3::text,
    $4::text,
    $5::boolean,
    $6::text,
    $7::text,
    $8::text
)
RETURNING
    id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
`

type InsertAIProviderParams struct {
	ID            uuid.UUID      `db:"id" json:"id"`
	Type          AIProviderType `db:"type" json:"type"`
	Name          string         `db:"name" json:"name"`
	DisplayName   sql.NullString `db:"display_name" json:"display_name"`
	Enabled       bool           `db:"enabled" json:"enabled"`
	BaseUrl       string         `db:"base_url" json:"base_url"`
	Settings      sql.NullString `db:"settings" json:"settings"`
	SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
}

func (q *sqlQuerier) InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error) {
	row := q.db.QueryRowContext(ctx, insertAIProvider,
		arg.ID,
		arg.Type,
		arg.Name,
		arg.DisplayName,
		arg.Enabled,
		arg.BaseUrl,
		arg.Settings,
		arg.SettingsKeyID,
	)
	var i AIProvider
	err := row.Scan(
		&i.ID,
		&i.Type,
		&i.Name,
		&i.DisplayName,
		&i.Enabled,
		&i.Deleted,
		&i.BaseUrl,
		&i.Settings,
		&i.SettingsKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const updateAIProvider = `-- name: UpdateAIProvider :one
UPDATE
    ai_providers
SET
    display_name = $1::text,
    enabled = $2::boolean,
    base_url = $3::text,
    settings = $4::text,
    settings_key_id = $5::text,
    updated_at = NOW()
WHERE
    id = $6::uuid AND deleted = FALSE
RETURNING
    id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
`

type UpdateAIProviderParams struct {
	DisplayName   sql.NullString `db:"display_name" json:"display_name"`
	Enabled       bool           `db:"enabled" json:"enabled"`
	BaseUrl       string         `db:"base_url" json:"base_url"`
	Settings      sql.NullString `db:"settings" json:"settings"`
	SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
	ID            uuid.UUID      `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error) {
	row := q.db.QueryRowContext(ctx, updateAIProvider,
		arg.DisplayName,
		arg.Enabled,
		arg.BaseUrl,
		arg.Settings,
		arg.SettingsKeyID,
		arg.ID,
	)
	var i AIProvider
	err := row.Scan(
		&i.ID,
		&i.Type,
		&i.Name,
		&i.DisplayName,
		&i.Enabled,
		&i.Deleted,
		&i.BaseUrl,
		&i.Settings,
		&i.SettingsKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const updateEncryptedAIProviderSettings = `-- name: UpdateEncryptedAIProviderSettings :one
UPDATE
    ai_providers
SET
    settings = $1::text,
    settings_key_id = $2::text,
    updated_at = NOW()
WHERE
    id = $3::uuid
RETURNING
    id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
`

type UpdateEncryptedAIProviderSettingsParams struct {
	Settings      sql.NullString `db:"settings" json:"settings"`
	SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
	ID            uuid.UUID      `db:"id" json:"id"`
}

// Updates only the encrypted columns (settings, settings_key_id) and
// the updated_at timestamp on a row, regardless of its deleted flag.
// Used by the dbcrypt key rotation utility to re-encrypt or decrypt
// rows in place.
func (q *sqlQuerier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg UpdateEncryptedAIProviderSettingsParams) (AIProvider, error) {
	row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderSettings, arg.Settings, arg.SettingsKeyID, arg.ID)
	var i AIProvider
	err := row.Scan(
		&i.ID,
		&i.Type,
		&i.Name,
		&i.DisplayName,
		&i.Enabled,
		&i.Deleted,
		&i.BaseUrl,
		&i.Settings,
		&i.SettingsKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const calculateAIBridgeInterceptionsTelemetrySummary = `-- name: CalculateAIBridgeInterceptionsTelemetrySummary :one
WITH interceptions_in_range AS (
    -- Get all matching interceptions in the given timeframe.
    SELECT
        id,
        initiator_id,
        (ended_at - started_at) AS duration
    FROM
        aibridge_interceptions
    WHERE
        provider = $1::text
        AND model = $2::text
        AND COALESCE(client, 'Unknown') = $3::text
        AND ended_at IS NOT NULL -- incomplete interceptions are not included in summaries
        AND ended_at >= $4::timestamptz
        AND ended_at < $5::timestamptz
),
interception_counts AS (
    SELECT
        COUNT(id) AS interception_count,
        COUNT(DISTINCT initiator_id) AS unique_initiator_count
    FROM
        interceptions_in_range
),
duration_percentiles AS (
    SELECT
        (COALESCE(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM duration)), 0) * 1000)::bigint AS interception_duration_p50_millis,
        (COALESCE(PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM duration)), 0) * 1000)::bigint AS interception_duration_p90_millis,
        (COALESCE(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM duration)), 0) * 1000)::bigint AS interception_duration_p95_millis,
        (COALESCE(PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM duration)), 0) * 1000)::bigint AS interception_duration_p99_millis
    FROM
        interceptions_in_range
),
token_aggregates AS (
    SELECT
        COALESCE(SUM(tu.input_tokens), 0) AS token_count_input,
        COALESCE(SUM(tu.output_tokens), 0) AS token_count_output,
        COALESCE(SUM(tu.cache_read_input_tokens), 0) AS token_count_cached_read,
        COALESCE(SUM(tu.cache_write_input_tokens), 0) AS token_count_cached_written,
        COUNT(tu.id) AS token_usages_count
    FROM
        interceptions_in_range i
    LEFT JOIN
        aibridge_token_usages tu ON i.id = tu.interception_id
),
prompt_aggregates AS (
    SELECT
        COUNT(up.id) AS user_prompts_count
    FROM
        interceptions_in_range i
    LEFT JOIN
        aibridge_user_prompts up ON i.id = up.interception_id
),
tool_aggregates AS (
    SELECT
        COUNT(tu.id) FILTER (WHERE tu.injected = true) AS tool_calls_count_injected,
        COUNT(tu.id) FILTER (WHERE tu.injected = false) AS tool_calls_count_non_injected,
        COUNT(tu.id) FILTER (WHERE tu.injected = true AND tu.invocation_error IS NOT NULL) AS injected_tool_call_error_count
    FROM
        interceptions_in_range i
    LEFT JOIN
        aibridge_tool_usages tu ON i.id = tu.interception_id
)
SELECT
    ic.interception_count::bigint AS interception_count,
    dp.interception_duration_p50_millis::bigint AS interception_duration_p50_millis,
    dp.interception_duration_p90_millis::bigint AS interception_duration_p90_millis,
    dp.interception_duration_p95_millis::bigint AS interception_duration_p95_millis,
    dp.interception_duration_p99_millis::bigint AS interception_duration_p99_millis,
    ic.unique_initiator_count::bigint AS unique_initiator_count,
    pa.user_prompts_count::bigint AS user_prompts_count,
    tok_agg.token_usages_count::bigint AS token_usages_count,
    tok_agg.token_count_input::bigint AS token_count_input,
    tok_agg.token_count_output::bigint AS token_count_output,
    tok_agg.token_count_cached_read::bigint AS token_count_cached_read,
    tok_agg.token_count_cached_written::bigint AS token_count_cached_written,
    tool_agg.tool_calls_count_injected::bigint AS tool_calls_count_injected,
    tool_agg.tool_calls_count_non_injected::bigint AS tool_calls_count_non_injected,
    tool_agg.injected_tool_call_error_count::bigint AS injected_tool_call_error_count
FROM
    interception_counts ic,
    duration_percentiles dp,
    token_aggregates tok_agg,
    prompt_aggregates pa,
    tool_aggregates tool_agg
`

type CalculateAIBridgeInterceptionsTelemetrySummaryParams struct {
	Provider      string    `db:"provider" json:"provider"`
	Model         string    `db:"model" json:"model"`
	Client        string    `db:"client" json:"client"`
	EndedAtAfter  time.Time `db:"ended_at_after" json:"ended_at_after"`
	EndedAtBefore time.Time `db:"ended_at_before" json:"ended_at_before"`
}

type CalculateAIBridgeInterceptionsTelemetrySummaryRow struct {
	InterceptionCount             int64 `db:"interception_count" json:"interception_count"`
	InterceptionDurationP50Millis int64 `db:"interception_duration_p50_millis" json:"interception_duration_p50_millis"`
	InterceptionDurationP90Millis int64 `db:"interception_duration_p90_millis" json:"interception_duration_p90_millis"`
	InterceptionDurationP95Millis int64 `db:"interception_duration_p95_millis" json:"interception_duration_p95_millis"`
	InterceptionDurationP99Millis int64 `db:"interception_duration_p99_millis" json:"interception_duration_p99_millis"`
	UniqueInitiatorCount          int64 `db:"unique_initiator_count" json:"unique_initiator_count"`
	UserPromptsCount              int64 `db:"user_prompts_count" json:"user_prompts_count"`
	TokenUsagesCount              int64 `db:"token_usages_count" json:"token_usages_count"`
	TokenCountInput               int64 `db:"token_count_input" json:"token_count_input"`
	TokenCountOutput              int64 `db:"token_count_output" json:"token_count_output"`
	TokenCountCachedRead          int64 `db:"token_count_cached_read" json:"token_count_cached_read"`
	TokenCountCachedWritten       int64 `db:"token_count_cached_written" json:"token_count_cached_written"`
	ToolCallsCountInjected        int64 `db:"tool_calls_count_injected" json:"tool_calls_count_injected"`
	ToolCallsCountNonInjected     int64 `db:"tool_calls_count_non_injected" json:"tool_calls_count_non_injected"`
	InjectedToolCallErrorCount    int64 `db:"injected_tool_call_error_count" json:"injected_tool_call_error_count"`
}

// Calculates the telemetry summary for a given provider, model, and client
// combination for telemetry reporting.
func (q *sqlQuerier) CalculateAIBridgeInterceptionsTelemetrySummary(ctx context.Context, arg CalculateAIBridgeInterceptionsTelemetrySummaryParams) (CalculateAIBridgeInterceptionsTelemetrySummaryRow, error) {
	row := q.db.QueryRowContext(ctx, calculateAIBridgeInterceptionsTelemetrySummary,
		arg.Provider,
		arg.Model,
		arg.Client,
		arg.EndedAtAfter,
		arg.EndedAtBefore,
	)
	var i CalculateAIBridgeInterceptionsTelemetrySummaryRow
	err := row.Scan(
		&i.InterceptionCount,
		&i.InterceptionDurationP50Millis,
		&i.InterceptionDurationP90Millis,
		&i.InterceptionDurationP95Millis,
		&i.InterceptionDurationP99Millis,
		&i.UniqueInitiatorCount,
		&i.UserPromptsCount,
		&i.TokenUsagesCount,
		&i.TokenCountInput,
		&i.TokenCountOutput,
		&i.TokenCountCachedRead,
		&i.TokenCountCachedWritten,
		&i.ToolCallsCountInjected,
		&i.ToolCallsCountNonInjected,
		&i.InjectedToolCallErrorCount,
	)
	return i, err
}

const countAIBridgeInterceptions = `-- name: CountAIBridgeInterceptions :one
SELECT
	COUNT(*)
FROM
	aibridge_interceptions
WHERE
	-- Remove inflight interceptions (ones which lack an ended_at value).
	aibridge_interceptions.ended_at IS NOT NULL
	-- Filter by time frame
	AND CASE
		WHEN $1::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= $1::timestamptz
		ELSE true
	END
	AND CASE
		WHEN $2::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at <= $2::timestamptz
		ELSE true
	END
	-- Filter initiator_id
	AND CASE
		WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN aibridge_interceptions.initiator_id = $3::uuid
		ELSE true
	END
	-- Filter provider
	AND CASE
		WHEN $4::text != '' THEN aibridge_interceptions.provider = $4::text
		ELSE true
	END
	-- Filter provider_name
	AND CASE
		WHEN $5::text != '' THEN aibridge_interceptions.provider_name = $5::text
		ELSE true
	END
	-- Filter model
	AND CASE
		WHEN $6::text != '' THEN aibridge_interceptions.model = $6::text
		ELSE true
	END
	-- Filter client
	AND CASE
		WHEN $7::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $7::text
		ELSE true
	END
	-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions
	-- @authorize_filter
`

type CountAIBridgeInterceptionsParams struct {
	StartedAfter  time.Time `db:"started_after" json:"started_after"`
	StartedBefore time.Time `db:"started_before" json:"started_before"`
	InitiatorID   uuid.UUID `db:"initiator_id" json:"initiator_id"`
	Provider      string    `db:"provider" json:"provider"`
	ProviderName  string    `db:"provider_name" json:"provider_name"`
	Model         string    `db:"model" json:"model"`
	Client        string    `db:"client" json:"client"`
}

func (q *sqlQuerier) CountAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, countAIBridgeInterceptions,
		arg.StartedAfter,
		arg.StartedBefore,
		arg.InitiatorID,
		arg.Provider,
		arg.ProviderName,
		arg.Model,
		arg.Client,
	)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const countAIBridgeSessions = `-- name: CountAIBridgeSessions :one
SELECT
	COUNT(DISTINCT (aibridge_interceptions.session_id, aibridge_interceptions.initiator_id))
FROM
	aibridge_interceptions
WHERE
	-- Remove inflight interceptions (ones which lack an ended_at value).
	aibridge_interceptions.ended_at IS NOT NULL
	-- Filter by time frame
	AND CASE
		WHEN $1::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= $1::timestamptz
		ELSE true
	END
	AND CASE
		WHEN $2::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at <= $2::timestamptz
		ELSE true
	END
	-- Filter initiator_id
	AND CASE
		WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN aibridge_interceptions.initiator_id = $3::uuid
		ELSE true
	END
	-- Filter provider
	AND CASE
		WHEN $4::text != '' THEN aibridge_interceptions.provider = $4::text
		ELSE true
	END
	-- Filter provider_name
	AND CASE
		WHEN $5::text != '' THEN aibridge_interceptions.provider_name = $5::text
		ELSE true
	END
	-- Filter model
	AND CASE
		WHEN $6::text != '' THEN aibridge_interceptions.model = $6::text
		ELSE true
	END
	-- Filter client
	AND CASE
		WHEN $7::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $7::text
		ELSE true
	END
	-- Filter session_id
	AND CASE
		WHEN $8::text != '' THEN aibridge_interceptions.session_id = $8::text
		ELSE true
	END
	-- Authorize Filter clause will be injected below in CountAuthorizedAIBridgeSessions
	-- @authorize_filter
`

type CountAIBridgeSessionsParams struct {
	StartedAfter  time.Time `db:"started_after" json:"started_after"`
	StartedBefore time.Time `db:"started_before" json:"started_before"`
	InitiatorID   uuid.UUID `db:"initiator_id" json:"initiator_id"`
	Provider      string    `db:"provider" json:"provider"`
	ProviderName  string    `db:"provider_name" json:"provider_name"`
	Model         string    `db:"model" json:"model"`
	Client        string    `db:"client" json:"client"`
	SessionID     string    `db:"session_id" json:"session_id"`
}

func (q *sqlQuerier) CountAIBridgeSessions(ctx context.Context, arg CountAIBridgeSessionsParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, countAIBridgeSessions,
		arg.StartedAfter,
		arg.StartedBefore,
		arg.InitiatorID,
		arg.Provider,
		arg.ProviderName,
		arg.Model,
		arg.Client,
		arg.SessionID,
	)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const deleteOldAIBridgeRecords = `-- name: DeleteOldAIBridgeRecords :one
WITH
  -- We don't have FK relationships between the dependent tables and aibridge_interceptions, so we can't rely on DELETE CASCADE.
  to_delete AS (
    SELECT id FROM aibridge_interceptions
    WHERE started_at < $1::timestamp with time zone
  ),
  -- CTEs are executed in order.
  model_thoughts AS (
    DELETE FROM aibridge_model_thoughts
    WHERE interception_id IN (SELECT id FROM to_delete)
    RETURNING 1
  ),
  tool_usages AS (
    DELETE FROM aibridge_tool_usages
    WHERE interception_id IN (SELECT id FROM to_delete)
    RETURNING 1
  ),
  token_usages AS (
    DELETE FROM aibridge_token_usages
    WHERE interception_id IN (SELECT id FROM to_delete)
    RETURNING 1
  ),
  user_prompts AS (
    DELETE FROM aibridge_user_prompts
    WHERE interception_id IN (SELECT id FROM to_delete)
    RETURNING 1
  ),
  interceptions AS (
    DELETE FROM aibridge_interceptions
    WHERE id IN (SELECT id FROM to_delete)
    RETURNING 1
  )
SELECT (
  (SELECT COUNT(*) FROM model_thoughts) +
  (SELECT COUNT(*) FROM tool_usages) +
  (SELECT COUNT(*) FROM token_usages) +
  (SELECT COUNT(*) FROM user_prompts) +
  (SELECT COUNT(*) FROM interceptions)
)::bigint as total_deleted
`

// Cumulative count.
func (q *sqlQuerier) DeleteOldAIBridgeRecords(ctx context.Context, beforeTime time.Time) (int64, error) {
	row := q.db.QueryRowContext(ctx, deleteOldAIBridgeRecords, beforeTime)
	var total_deleted int64
	err := row.Scan(&total_deleted)
	return total_deleted, err
}

const getAIBridgeInterceptionByID = `-- name: GetAIBridgeInterceptionByID :one
SELECT
	id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint
FROM
	aibridge_interceptions
WHERE
	id = $1::uuid
`

func (q *sqlQuerier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (AIBridgeInterception, error) {
	row := q.db.QueryRowContext(ctx, getAIBridgeInterceptionByID, id)
	var i AIBridgeInterception
	err := row.Scan(
		&i.ID,
		&i.InitiatorID,
		&i.Provider,
		&i.Model,
		&i.StartedAt,
		&i.Metadata,
		&i.EndedAt,
		&i.APIKeyID,
		&i.Client,
		&i.ThreadParentID,
		&i.ThreadRootID,
		&i.ClientSessionID,
		&i.SessionID,
		&i.ProviderName,
		&i.CredentialKind,
		&i.CredentialHint,
	)
	return i, err
}

const getAIBridgeInterceptionLineageByToolCallID = `-- name: GetAIBridgeInterceptionLineageByToolCallID :one
SELECT aibridge_interceptions.id AS thread_parent_id,
       COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id) AS thread_root_id
FROM aibridge_interceptions
WHERE aibridge_interceptions.id = (
  SELECT interception_id FROM aibridge_tool_usages
  WHERE provider_tool_call_id = $1::text
  ORDER BY created_at DESC
  LIMIT 1
)
`

type GetAIBridgeInterceptionLineageByToolCallIDRow struct {
	ThreadParentID uuid.UUID `db:"thread_parent_id" json:"thread_parent_id"`
	ThreadRootID   uuid.UUID `db:"thread_root_id" json:"thread_root_id"`
}

// Look up the parent interception and the root of the thread by finding
// which interception recorded a tool usage with the given tool call ID.
// COALESCE ensures that if the parent has no thread_root_id (i.e. it IS
// the root), we return its own ID as the root.
func (q *sqlQuerier) GetAIBridgeInterceptionLineageByToolCallID(ctx context.Context, toolCallID string) (GetAIBridgeInterceptionLineageByToolCallIDRow, error) {
	row := q.db.QueryRowContext(ctx, getAIBridgeInterceptionLineageByToolCallID, toolCallID)
	var i GetAIBridgeInterceptionLineageByToolCallIDRow
	err := row.Scan(&i.ThreadParentID, &i.ThreadRootID)
	return i, err
}

const getAIBridgeInterceptions = `-- name: GetAIBridgeInterceptions :many
SELECT
	id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint
FROM
	aibridge_interceptions
`

func (q *sqlQuerier) GetAIBridgeInterceptions(ctx context.Context) ([]AIBridgeInterception, error) {
	rows, err := q.db.QueryContext(ctx, getAIBridgeInterceptions)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIBridgeInterception
	for rows.Next() {
		var i AIBridgeInterception
		if err := rows.Scan(
			&i.ID,
			&i.InitiatorID,
			&i.Provider,
			&i.Model,
			&i.StartedAt,
			&i.Metadata,
			&i.EndedAt,
			&i.APIKeyID,
			&i.Client,
			&i.ThreadParentID,
			&i.ThreadRootID,
			&i.ClientSessionID,
			&i.SessionID,
			&i.ProviderName,
			&i.CredentialKind,
			&i.CredentialHint,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAIBridgeTokenUsagesByInterceptionID = `-- name: GetAIBridgeTokenUsagesByInterceptionID :many
SELECT
	id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at, cache_read_input_tokens, cache_write_input_tokens
FROM
	aibridge_token_usages WHERE interception_id = $1::uuid
ORDER BY
	created_at ASC,
	id ASC
`

func (q *sqlQuerier) GetAIBridgeTokenUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeTokenUsage, error) {
	rows, err := q.db.QueryContext(ctx, getAIBridgeTokenUsagesByInterceptionID, interceptionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIBridgeTokenUsage
	for rows.Next() {
		var i AIBridgeTokenUsage
		if err := rows.Scan(
			&i.ID,
			&i.InterceptionID,
			&i.ProviderResponseID,
			&i.InputTokens,
			&i.OutputTokens,
			&i.Metadata,
			&i.CreatedAt,
			&i.CacheReadInputTokens,
			&i.CacheWriteInputTokens,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAIBridgeToolUsagesByInterceptionID = `-- name: GetAIBridgeToolUsagesByInterceptionID :many
SELECT
	id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id
FROM
	aibridge_tool_usages
WHERE
	interception_id = $1::uuid
ORDER BY
	created_at ASC,
	id ASC
`

func (q *sqlQuerier) GetAIBridgeToolUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeToolUsage, error) {
	rows, err := q.db.QueryContext(ctx, getAIBridgeToolUsagesByInterceptionID, interceptionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIBridgeToolUsage
	for rows.Next() {
		var i AIBridgeToolUsage
		if err := rows.Scan(
			&i.ID,
			&i.InterceptionID,
			&i.ProviderResponseID,
			&i.ServerUrl,
			&i.Tool,
			&i.Input,
			&i.Injected,
			&i.InvocationError,
			&i.Metadata,
			&i.CreatedAt,
			&i.ProviderToolCallID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAIBridgeUserPromptsByInterceptionID = `-- name: GetAIBridgeUserPromptsByInterceptionID :many
SELECT
	id, interception_id, provider_response_id, prompt, metadata, created_at
FROM
	aibridge_user_prompts
WHERE
	interception_id = $1::uuid
ORDER BY
	created_at ASC,
	id ASC
`

func (q *sqlQuerier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeUserPrompt, error) {
	rows, err := q.db.QueryContext(ctx, getAIBridgeUserPromptsByInterceptionID, interceptionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIBridgeUserPrompt
	for rows.Next() {
		var i AIBridgeUserPrompt
		if err := rows.Scan(
			&i.ID,
			&i.InterceptionID,
			&i.ProviderResponseID,
			&i.Prompt,
			&i.Metadata,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertAIBridgeInterception = `-- name: InsertAIBridgeInterception :one
INSERT INTO aibridge_interceptions (
	id, api_key_id, initiator_id, provider, provider_name, model, metadata, started_at, client, client_session_id, thread_parent_id, thread_root_id, credential_kind, credential_hint
) VALUES (
	$1, $2, $3, $4, $5, $6, COALESCE($7::jsonb, '{}'::jsonb), $8, $9, $10, $11::uuid, $12::uuid, $13, $14
)
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint
`

type InsertAIBridgeInterceptionParams struct {
	ID                         uuid.UUID       `db:"id" json:"id"`
	APIKeyID                   sql.NullString  `db:"api_key_id" json:"api_key_id"`
	InitiatorID                uuid.UUID       `db:"initiator_id" json:"initiator_id"`
	Provider                   string          `db:"provider" json:"provider"`
	ProviderName               string          `db:"provider_name" json:"provider_name"`
	Model                      string          `db:"model" json:"model"`
	Metadata                   json.RawMessage `db:"metadata" json:"metadata"`
	StartedAt                  time.Time       `db:"started_at" json:"started_at"`
	Client                     sql.NullString  `db:"client" json:"client"`
	ClientSessionID            sql.NullString  `db:"client_session_id" json:"client_session_id"`
	ThreadParentInterceptionID uuid.NullUUID   `db:"thread_parent_interception_id" json:"thread_parent_interception_id"`
	ThreadRootInterceptionID   uuid.NullUUID   `db:"thread_root_interception_id" json:"thread_root_interception_id"`
	CredentialKind             CredentialKind  `db:"credential_kind" json:"credential_kind"`
	CredentialHint             string          `db:"credential_hint" json:"credential_hint"`
}

func (q *sqlQuerier) InsertAIBridgeInterception(ctx context.Context, arg InsertAIBridgeInterceptionParams) (AIBridgeInterception, error) {
	row := q.db.QueryRowContext(ctx, insertAIBridgeInterception,
		arg.ID,
		arg.APIKeyID,
		arg.InitiatorID,
		arg.Provider,
		arg.ProviderName,
		arg.Model,
		arg.Metadata,
		arg.StartedAt,
		arg.Client,
		arg.ClientSessionID,
		arg.ThreadParentInterceptionID,
		arg.ThreadRootInterceptionID,
		arg.CredentialKind,
		arg.CredentialHint,
	)
	var i AIBridgeInterception
	err := row.Scan(
		&i.ID,
		&i.InitiatorID,
		&i.Provider,
		&i.Model,
		&i.StartedAt,
		&i.Metadata,
		&i.EndedAt,
		&i.APIKeyID,
		&i.Client,
		&i.ThreadParentID,
		&i.ThreadRootID,
		&i.ClientSessionID,
		&i.SessionID,
		&i.ProviderName,
		&i.CredentialKind,
		&i.CredentialHint,
	)
	return i, err
}

const insertAIBridgeModelThought = `-- name: InsertAIBridgeModelThought :one
INSERT INTO aibridge_model_thoughts (
  interception_id, content, metadata, created_at
) VALUES (
  $1, $2, COALESCE($3::jsonb, '{}'::jsonb), $4
)
RETURNING interception_id, content, metadata, created_at
`

type InsertAIBridgeModelThoughtParams struct {
	InterceptionID uuid.UUID       `db:"interception_id" json:"interception_id"`
	Content        string          `db:"content" json:"content"`
	Metadata       json.RawMessage `db:"metadata" json:"metadata"`
	CreatedAt      time.Time       `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) InsertAIBridgeModelThought(ctx context.Context, arg InsertAIBridgeModelThoughtParams) (AIBridgeModelThought, error) {
	row := q.db.QueryRowContext(ctx, insertAIBridgeModelThought,
		arg.InterceptionID,
		arg.Content,
		arg.Metadata,
		arg.CreatedAt,
	)
	var i AIBridgeModelThought
	err := row.Scan(
		&i.InterceptionID,
		&i.Content,
		&i.Metadata,
		&i.CreatedAt,
	)
	return i, err
}

const insertAIBridgeTokenUsage = `-- name: InsertAIBridgeTokenUsage :one
INSERT INTO aibridge_token_usages (
  id, interception_id, provider_response_id, input_tokens, output_tokens, cache_read_input_tokens, cache_write_input_tokens, metadata, created_at
) VALUES (
  $1, $2, $3, $4, $5, $6, $7, COALESCE($8::jsonb, '{}'::jsonb), $9
)
RETURNING id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at, cache_read_input_tokens, cache_write_input_tokens
`

type InsertAIBridgeTokenUsageParams struct {
	ID                    uuid.UUID       `db:"id" json:"id"`
	InterceptionID        uuid.UUID       `db:"interception_id" json:"interception_id"`
	ProviderResponseID    string          `db:"provider_response_id" json:"provider_response_id"`
	InputTokens           int64           `db:"input_tokens" json:"input_tokens"`
	OutputTokens          int64           `db:"output_tokens" json:"output_tokens"`
	CacheReadInputTokens  int64           `db:"cache_read_input_tokens" json:"cache_read_input_tokens"`
	CacheWriteInputTokens int64           `db:"cache_write_input_tokens" json:"cache_write_input_tokens"`
	Metadata              json.RawMessage `db:"metadata" json:"metadata"`
	CreatedAt             time.Time       `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error) {
	row := q.db.QueryRowContext(ctx, insertAIBridgeTokenUsage,
		arg.ID,
		arg.InterceptionID,
		arg.ProviderResponseID,
		arg.InputTokens,
		arg.OutputTokens,
		arg.CacheReadInputTokens,
		arg.CacheWriteInputTokens,
		arg.Metadata,
		arg.CreatedAt,
	)
	var i AIBridgeTokenUsage
	err := row.Scan(
		&i.ID,
		&i.InterceptionID,
		&i.ProviderResponseID,
		&i.InputTokens,
		&i.OutputTokens,
		&i.Metadata,
		&i.CreatedAt,
		&i.CacheReadInputTokens,
		&i.CacheWriteInputTokens,
	)
	return i, err
}

const insertAIBridgeToolUsage = `-- name: InsertAIBridgeToolUsage :one
INSERT INTO aibridge_tool_usages (
  id, interception_id, provider_response_id, provider_tool_call_id, tool, server_url, input, injected, invocation_error, metadata, created_at
) VALUES (
  $1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10::jsonb, '{}'::jsonb), $11
)
RETURNING id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id
`

type InsertAIBridgeToolUsageParams struct {
	ID                 uuid.UUID       `db:"id" json:"id"`
	InterceptionID     uuid.UUID       `db:"interception_id" json:"interception_id"`
	ProviderResponseID string          `db:"provider_response_id" json:"provider_response_id"`
	ProviderToolCallID sql.NullString  `db:"provider_tool_call_id" json:"provider_tool_call_id"`
	Tool               string          `db:"tool" json:"tool"`
	ServerUrl          sql.NullString  `db:"server_url" json:"server_url"`
	Input              string          `db:"input" json:"input"`
	Injected           bool            `db:"injected" json:"injected"`
	InvocationError    sql.NullString  `db:"invocation_error" json:"invocation_error"`
	Metadata           json.RawMessage `db:"metadata" json:"metadata"`
	CreatedAt          time.Time       `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error) {
	row := q.db.QueryRowContext(ctx, insertAIBridgeToolUsage,
		arg.ID,
		arg.InterceptionID,
		arg.ProviderResponseID,
		arg.ProviderToolCallID,
		arg.Tool,
		arg.ServerUrl,
		arg.Input,
		arg.Injected,
		arg.InvocationError,
		arg.Metadata,
		arg.CreatedAt,
	)
	var i AIBridgeToolUsage
	err := row.Scan(
		&i.ID,
		&i.InterceptionID,
		&i.ProviderResponseID,
		&i.ServerUrl,
		&i.Tool,
		&i.Input,
		&i.Injected,
		&i.InvocationError,
		&i.Metadata,
		&i.CreatedAt,
		&i.ProviderToolCallID,
	)
	return i, err
}

const insertAIBridgeUserPrompt = `-- name: InsertAIBridgeUserPrompt :one
INSERT INTO aibridge_user_prompts (
  id, interception_id, provider_response_id, prompt, metadata, created_at
) VALUES (
  $1, $2, $3, $4, COALESCE($5::jsonb, '{}'::jsonb), $6
)
RETURNING id, interception_id, provider_response_id, prompt, metadata, created_at
`

type InsertAIBridgeUserPromptParams struct {
	ID                 uuid.UUID       `db:"id" json:"id"`
	InterceptionID     uuid.UUID       `db:"interception_id" json:"interception_id"`
	ProviderResponseID string          `db:"provider_response_id" json:"provider_response_id"`
	Prompt             string          `db:"prompt" json:"prompt"`
	Metadata           json.RawMessage `db:"metadata" json:"metadata"`
	CreatedAt          time.Time       `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) (AIBridgeUserPrompt, error) {
	row := q.db.QueryRowContext(ctx, insertAIBridgeUserPrompt,
		arg.ID,
		arg.InterceptionID,
		arg.ProviderResponseID,
		arg.Prompt,
		arg.Metadata,
		arg.CreatedAt,
	)
	var i AIBridgeUserPrompt
	err := row.Scan(
		&i.ID,
		&i.InterceptionID,
		&i.ProviderResponseID,
		&i.Prompt,
		&i.Metadata,
		&i.CreatedAt,
	)
	return i, err
}

const listAIBridgeClients = `-- name: ListAIBridgeClients :many
SELECT
	COALESCE(client, 'Unknown') AS client
FROM
	aibridge_interceptions
WHERE
	ended_at IS NOT NULL
	-- Filter client (prefix match to allow B-tree index usage).
	AND CASE
		WHEN $1::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') LIKE $1::text || '%'
		ELSE true
	END
	-- We use an ` + "`" + `@authorize_filter` + "`" + ` as we are attempting to list clients
	-- that are relevant to the user and what they are allowed to see.
	-- Authorize Filter clause will be injected below in
	-- ListAIBridgeClientsAuthorized.
	-- @authorize_filter
GROUP BY
	client
LIMIT COALESCE(NULLIF($3::integer, 0), 100)
OFFSET $2
`

type ListAIBridgeClientsParams struct {
	Client string `db:"client" json:"client"`
	Offset int32  `db:"offset_" json:"offset_"`
	Limit  int32  `db:"limit_" json:"limit_"`
}

func (q *sqlQuerier) ListAIBridgeClients(ctx context.Context, arg ListAIBridgeClientsParams) ([]string, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeClients, arg.Client, arg.Offset, arg.Limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []string
	for rows.Next() {
		var client string
		if err := rows.Scan(&client); err != nil {
			return nil, err
		}
		items = append(items, client)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeInterceptions = `-- name: ListAIBridgeInterceptions :many
SELECT
	aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id, aibridge_interceptions.client, aibridge_interceptions.thread_parent_id, aibridge_interceptions.thread_root_id, aibridge_interceptions.client_session_id, aibridge_interceptions.session_id, aibridge_interceptions.provider_name, aibridge_interceptions.credential_kind, aibridge_interceptions.credential_hint,
	visible_users.id, visible_users.username, visible_users.name, visible_users.avatar_url
FROM
	aibridge_interceptions
JOIN
	visible_users ON visible_users.id = aibridge_interceptions.initiator_id
WHERE
	-- Remove inflight interceptions (ones which lack an ended_at value).
	aibridge_interceptions.ended_at IS NOT NULL
	-- Filter by time frame
	AND CASE
		WHEN $1::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= $1::timestamptz
		ELSE true
	END
	AND CASE
		WHEN $2::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at <= $2::timestamptz
		ELSE true
	END
	-- Filter initiator_id
	AND CASE
		WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN aibridge_interceptions.initiator_id = $3::uuid
		ELSE true
	END
	-- Filter provider
	AND CASE
		WHEN $4::text != '' THEN aibridge_interceptions.provider = $4::text
		ELSE true
	END
	-- Filter provider_name
	AND CASE
		WHEN $5::text != '' THEN aibridge_interceptions.provider_name = $5::text
		ELSE true
	END
	-- Filter model
	AND CASE
		WHEN $6::text != '' THEN aibridge_interceptions.model = $6::text
		ELSE true
	END
	-- Filter client
	AND CASE
		WHEN $7::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = $7::text
		ELSE true
	END
	-- Cursor pagination
	AND CASE
		WHEN $8::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
			-- The pagination cursor is the last ID of the previous page.
			-- The query is ordered by the started_at field, so select all
			-- rows before the cursor and before the after_id UUID.
			-- This uses a less than operator because we're sorting DESC. The
			-- "after_id" terminology comes from our pagination parser in
			-- coderd.
			(aibridge_interceptions.started_at, aibridge_interceptions.id) < (
				(SELECT started_at FROM aibridge_interceptions WHERE id = $8),
				$8::uuid
			)
		)
		ELSE true
	END
	-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeInterceptions
	-- @authorize_filter
ORDER BY
	aibridge_interceptions.started_at DESC,
	aibridge_interceptions.id DESC
LIMIT COALESCE(NULLIF($10::integer, 0), 100)
OFFSET $9
`

type ListAIBridgeInterceptionsParams struct {
	StartedAfter  time.Time `db:"started_after" json:"started_after"`
	StartedBefore time.Time `db:"started_before" json:"started_before"`
	InitiatorID   uuid.UUID `db:"initiator_id" json:"initiator_id"`
	Provider      string    `db:"provider" json:"provider"`
	ProviderName  string    `db:"provider_name" json:"provider_name"`
	Model         string    `db:"model" json:"model"`
	Client        string    `db:"client" json:"client"`
	AfterID       uuid.UUID `db:"after_id" json:"after_id"`
	Offset        int32     `db:"offset_" json:"offset_"`
	Limit         int32     `db:"limit_" json:"limit_"`
}

type ListAIBridgeInterceptionsRow struct {
	AIBridgeInterception AIBridgeInterception `db:"aibridge_interception" json:"aibridge_interception"`
	VisibleUser          VisibleUser          `db:"visible_user" json:"visible_user"`
}

func (q *sqlQuerier) ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]ListAIBridgeInterceptionsRow, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeInterceptions,
		arg.StartedAfter,
		arg.StartedBefore,
		arg.InitiatorID,
		arg.Provider,
		arg.ProviderName,
		arg.Model,
		arg.Client,
		arg.AfterID,
		arg.Offset,
		arg.Limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListAIBridgeInterceptionsRow
	for rows.Next() {
		var i ListAIBridgeInterceptionsRow
		if err := rows.Scan(
			&i.AIBridgeInterception.ID,
			&i.AIBridgeInterception.InitiatorID,
			&i.AIBridgeInterception.Provider,
			&i.AIBridgeInterception.Model,
			&i.AIBridgeInterception.StartedAt,
			&i.AIBridgeInterception.Metadata,
			&i.AIBridgeInterception.EndedAt,
			&i.AIBridgeInterception.APIKeyID,
			&i.AIBridgeInterception.Client,
			&i.AIBridgeInterception.ThreadParentID,
			&i.AIBridgeInterception.ThreadRootID,
			&i.AIBridgeInterception.ClientSessionID,
			&i.AIBridgeInterception.SessionID,
			&i.AIBridgeInterception.ProviderName,
			&i.AIBridgeInterception.CredentialKind,
			&i.AIBridgeInterception.CredentialHint,
			&i.VisibleUser.ID,
			&i.VisibleUser.Username,
			&i.VisibleUser.Name,
			&i.VisibleUser.AvatarURL,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeInterceptionsTelemetrySummaries = `-- name: ListAIBridgeInterceptionsTelemetrySummaries :many
SELECT
    DISTINCT ON (provider, model, client)
    provider,
    model,
    COALESCE(client, 'Unknown') AS client
FROM
    aibridge_interceptions
WHERE
    ended_at IS NOT NULL -- incomplete interceptions are not included in summaries
    AND ended_at >= $1::timestamptz
    AND ended_at < $2::timestamptz
`

type ListAIBridgeInterceptionsTelemetrySummariesParams struct {
	EndedAtAfter  time.Time `db:"ended_at_after" json:"ended_at_after"`
	EndedAtBefore time.Time `db:"ended_at_before" json:"ended_at_before"`
}

type ListAIBridgeInterceptionsTelemetrySummariesRow struct {
	Provider string `db:"provider" json:"provider"`
	Model    string `db:"model" json:"model"`
	Client   string `db:"client" json:"client"`
}

// Finds all unique AI Bridge interception telemetry summaries combinations
// (provider, model, client) in the given timeframe for telemetry reporting.
func (q *sqlQuerier) ListAIBridgeInterceptionsTelemetrySummaries(ctx context.Context, arg ListAIBridgeInterceptionsTelemetrySummariesParams) ([]ListAIBridgeInterceptionsTelemetrySummariesRow, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeInterceptionsTelemetrySummaries, arg.EndedAtAfter, arg.EndedAtBefore)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListAIBridgeInterceptionsTelemetrySummariesRow
	for rows.Next() {
		var i ListAIBridgeInterceptionsTelemetrySummariesRow
		if err := rows.Scan(&i.Provider, &i.Model, &i.Client); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeModelThoughtsByInterceptionIDs = `-- name: ListAIBridgeModelThoughtsByInterceptionIDs :many
SELECT
	interception_id, content, metadata, created_at
FROM
	aibridge_model_thoughts
WHERE
	interception_id = ANY($1::uuid[])
ORDER BY
	created_at ASC
`

func (q *sqlQuerier) ListAIBridgeModelThoughtsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeModelThought, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeModelThoughtsByInterceptionIDs, pq.Array(interceptionIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIBridgeModelThought
	for rows.Next() {
		var i AIBridgeModelThought
		if err := rows.Scan(
			&i.InterceptionID,
			&i.Content,
			&i.Metadata,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeModels = `-- name: ListAIBridgeModels :many
SELECT
	model
FROM
	aibridge_interceptions
WHERE
	-- Remove inflight interceptions (ones which lack an ended_at value).
	aibridge_interceptions.ended_at IS NOT NULL
	-- Filter model
	AND CASE
		WHEN $1::text != '' THEN aibridge_interceptions.model LIKE $1::text || '%'
		ELSE true
	END
	-- We use an ` + "`" + `@authorize_filter` + "`" + ` as we are attempting to list models that are relevant
	-- to the user and what they are allowed to see.
	-- Authorize Filter clause will be injected below in ListAIBridgeModelsAuthorized
	-- @authorize_filter
GROUP BY
	model
ORDER BY
	model ASC
LIMIT COALESCE(NULLIF($3::integer, 0), 100)
OFFSET $2
`

type ListAIBridgeModelsParams struct {
	Model  string `db:"model" json:"model"`
	Offset int32  `db:"offset_" json:"offset_"`
	Limit  int32  `db:"limit_" json:"limit_"`
}

func (q *sqlQuerier) ListAIBridgeModels(ctx context.Context, arg ListAIBridgeModelsParams) ([]string, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeModels, arg.Model, arg.Offset, arg.Limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []string
	for rows.Next() {
		var model string
		if err := rows.Scan(&model); err != nil {
			return nil, err
		}
		items = append(items, model)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeSessionThreads = `-- name: ListAIBridgeSessionThreads :many
WITH paginated_threads AS (
	SELECT
		-- Find thread root interceptions (thread_root_id IS NULL), apply cursor
		-- pagination, and return the page.
		aibridge_interceptions.id AS thread_id,
		aibridge_interceptions.started_at
	FROM
		aibridge_interceptions
	WHERE
		aibridge_interceptions.session_id = $1::text
		AND aibridge_interceptions.ended_at IS NOT NULL
		AND aibridge_interceptions.thread_root_id IS NULL
		-- Pagination cursor.
		AND ($2::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR
			(aibridge_interceptions.started_at, aibridge_interceptions.id) > (
				(SELECT started_at FROM aibridge_interceptions ai2 WHERE ai2.id = $2),
				$2::uuid
			)
		)
		AND ($3::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR
			(aibridge_interceptions.started_at, aibridge_interceptions.id) < (
				(SELECT started_at FROM aibridge_interceptions ai2 WHERE ai2.id = $3),
				$3::uuid
			)
		)
		-- @authorize_filter
	ORDER BY
		aibridge_interceptions.started_at ASC,
		aibridge_interceptions.id ASC
	LIMIT COALESCE(NULLIF($4::integer, 0), 50)
)
SELECT
	COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id) AS thread_id,
	aibridge_interceptions.id, aibridge_interceptions.initiator_id, aibridge_interceptions.provider, aibridge_interceptions.model, aibridge_interceptions.started_at, aibridge_interceptions.metadata, aibridge_interceptions.ended_at, aibridge_interceptions.api_key_id, aibridge_interceptions.client, aibridge_interceptions.thread_parent_id, aibridge_interceptions.thread_root_id, aibridge_interceptions.client_session_id, aibridge_interceptions.session_id, aibridge_interceptions.provider_name, aibridge_interceptions.credential_kind, aibridge_interceptions.credential_hint
FROM
	aibridge_interceptions
JOIN
	paginated_threads pt
		ON pt.thread_id = COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id)
WHERE
	aibridge_interceptions.session_id = $1::text
	AND aibridge_interceptions.ended_at IS NOT NULL
	-- @authorize_filter
ORDER BY
	-- Ensure threads and their associated interceptions (agentic loops) are sorted chronologically.
	pt.started_at ASC,
	pt.thread_id ASC,
	aibridge_interceptions.started_at ASC,
	aibridge_interceptions.id ASC
`

type ListAIBridgeSessionThreadsParams struct {
	SessionID string    `db:"session_id" json:"session_id"`
	AfterID   uuid.UUID `db:"after_id" json:"after_id"`
	BeforeID  uuid.UUID `db:"before_id" json:"before_id"`
	Limit     int32     `db:"limit_" json:"limit_"`
}

type ListAIBridgeSessionThreadsRow struct {
	ThreadID             uuid.UUID            `db:"thread_id" json:"thread_id"`
	AIBridgeInterception AIBridgeInterception `db:"aibridge_interception" json:"aibridge_interception"`
}

// Returns all interceptions belonging to paginated threads within a session.
// Threads are paginated by (started_at, thread_id) cursor.
func (q *sqlQuerier) ListAIBridgeSessionThreads(ctx context.Context, arg ListAIBridgeSessionThreadsParams) ([]ListAIBridgeSessionThreadsRow, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeSessionThreads,
		arg.SessionID,
		arg.AfterID,
		arg.BeforeID,
		arg.Limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListAIBridgeSessionThreadsRow
	for rows.Next() {
		var i ListAIBridgeSessionThreadsRow
		if err := rows.Scan(
			&i.ThreadID,
			&i.AIBridgeInterception.ID,
			&i.AIBridgeInterception.InitiatorID,
			&i.AIBridgeInterception.Provider,
			&i.AIBridgeInterception.Model,
			&i.AIBridgeInterception.StartedAt,
			&i.AIBridgeInterception.Metadata,
			&i.AIBridgeInterception.EndedAt,
			&i.AIBridgeInterception.APIKeyID,
			&i.AIBridgeInterception.Client,
			&i.AIBridgeInterception.ThreadParentID,
			&i.AIBridgeInterception.ThreadRootID,
			&i.AIBridgeInterception.ClientSessionID,
			&i.AIBridgeInterception.SessionID,
			&i.AIBridgeInterception.ProviderName,
			&i.AIBridgeInterception.CredentialKind,
			&i.AIBridgeInterception.CredentialHint,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeSessions = `-- name: ListAIBridgeSessions :many
WITH cursor_pos AS (
	-- Resolve the cursor's last_active_at once, outside the HAVING clause,
	-- so the planner cannot accidentally re-evaluate it per group. Direct
	-- LEFT JOIN is safe here since we only use MAX/MIN aggregates (no COUNT
	-- affected by fan-out from multiple prompts per interception).
	-- COALESCE falls back to MIN(ai.started_at) so the cursor value is
	-- never NULL, which would silently drop rows from the HAVING comparison.
	SELECT COALESCE(MAX(up.created_at), MIN(ai.started_at)) AS last_active_at
	FROM aibridge_interceptions ai
	LEFT JOIN aibridge_user_prompts up ON up.interception_id = ai.id
	WHERE ai.session_id = $1 AND ai.ended_at IS NOT NULL
),
session_page AS (
	-- Paginate at the session level first; only cheap aggregates here.
	-- A lateral correlated subquery for prompts keeps the join one-to-one
	-- with aibridge_interceptions so COUNT(*) for thread tallies is not
	-- inflated. LIMIT 1 combined with the (interception_id, created_at DESC)
	-- index makes this an index-only lookup per interception row rather than
	-- a full-table-scan GROUP BY over all prompts.
	-- last_active_at is the latest prompt timestamp, falling back to
	-- MIN(started_at) for sessions with no prompts. The COALESCE ensures
	-- it is never NULL so the HAVING row-value cursor comparison is safe.
	SELECT
		ai.session_id,
		ai.initiator_id,
		MIN(ai.started_at) AS started_at,
		MAX(ai.ended_at) AS ended_at,
		COUNT(*) FILTER (WHERE ai.thread_root_id IS NULL) AS threads,
		COALESCE(MAX(latest_prompt.latest_prompt_at), MIN(ai.started_at))::timestamptz AS last_active_at
	FROM
		aibridge_interceptions ai
	LEFT JOIN LATERAL (
		SELECT created_at AS latest_prompt_at
		FROM aibridge_user_prompts
		WHERE interception_id = ai.id
		ORDER BY created_at DESC
		LIMIT 1
	) latest_prompt ON true
	WHERE
		-- Remove inflight interceptions (ones which lack an ended_at value).
		ai.ended_at IS NOT NULL
		-- Filter by time frame
		AND CASE
			WHEN $2::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN ai.started_at >= $2::timestamptz
			ELSE true
		END
		AND CASE
			WHEN $3::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN ai.started_at <= $3::timestamptz
			ELSE true
		END
		-- Filter initiator_id
		AND CASE
			WHEN $4::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN ai.initiator_id = $4::uuid
			ELSE true
		END
		-- Filter provider
		AND CASE
			WHEN $5::text != '' THEN ai.provider = $5::text
			ELSE true
		END
		-- Filter provider_name
		AND CASE
			WHEN $6::text != '' THEN ai.provider_name = $6::text
			ELSE true
		END
		-- Filter model
		AND CASE
			WHEN $7::text != '' THEN ai.model = $7::text
			ELSE true
		END
		-- Filter client
		AND CASE
			WHEN $8::text != '' THEN COALESCE(ai.client, 'Unknown') = $8::text
			ELSE true
		END
		-- Filter session_id
		AND CASE
			WHEN $9::text != '' THEN ai.session_id = $9::text
			ELSE true
		END
		-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeSessions
		-- @authorize_filter
	GROUP BY
		ai.session_id, ai.initiator_id
	HAVING
		-- Cursor pagination: uses a composite (last_active_at, session_id) cursor to
		-- support keyset pagination. The less-than comparison matches the DESC
		-- sort order so rows after the cursor come later in results. The cursor
		-- value comes from cursor_pos to guarantee single evaluation.
		CASE
			WHEN $1::text != '' THEN (
				(COALESCE(MAX(latest_prompt.latest_prompt_at), MIN(ai.started_at)), ai.session_id) < (
					(SELECT last_active_at FROM cursor_pos),
					$1::text
				)
			)
			ELSE true
		END
	ORDER BY
		last_active_at DESC,
		ai.session_id DESC
	LIMIT COALESCE(NULLIF($11::integer, 0), 100)
	OFFSET $10
)
SELECT
	sp.session_id,
	visible_users.id AS user_id,
	visible_users.username AS user_username,
	visible_users.name AS user_name,
	visible_users.avatar_url AS user_avatar_url,
	sr.providers::text[] AS providers,
	sr.models::text[] AS models,
	COALESCE(sr.client, '')::varchar(64) AS client,
	sr.metadata::jsonb AS metadata,
	sp.started_at::timestamptz AS started_at,
	sp.ended_at::timestamptz AS ended_at,
	sp.threads,
	COALESCE(st.input_tokens, 0)::bigint AS input_tokens,
	COALESCE(st.output_tokens, 0)::bigint AS output_tokens,
	COALESCE(st.cache_read_input_tokens, 0)::bigint AS cache_read_input_tokens,
	COALESCE(st.cache_write_input_tokens, 0)::bigint AS cache_write_input_tokens,
	COALESCE(slp.prompt, '') AS last_prompt,
	sp.last_active_at AS last_active_at
FROM
	session_page sp
JOIN
	visible_users ON visible_users.id = sp.initiator_id
LEFT JOIN LATERAL (
	SELECT
		(ARRAY_AGG(ai.client ORDER BY ai.started_at, ai.id))[1] AS client,
		(ARRAY_AGG(ai.metadata ORDER BY ai.started_at, ai.id))[1] AS metadata,
		ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers,
		ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models,
		ARRAY_AGG(ai.id) AS interception_ids
	FROM aibridge_interceptions ai
	WHERE ai.session_id = sp.session_id
		AND ai.initiator_id = sp.initiator_id
		AND ai.ended_at IS NOT NULL
) sr ON true
LEFT JOIN LATERAL (
	-- Aggregate tokens only for this session's interceptions.
	SELECT
		COALESCE(SUM(tu.input_tokens), 0)::bigint AS input_tokens,
		COALESCE(SUM(tu.output_tokens), 0)::bigint AS output_tokens,
		COALESCE(SUM(tu.cache_read_input_tokens), 0)::bigint AS cache_read_input_tokens,
		COALESCE(SUM(tu.cache_write_input_tokens), 0)::bigint AS cache_write_input_tokens
	FROM aibridge_token_usages tu
	WHERE tu.interception_id = ANY(sr.interception_ids)
) st ON true
LEFT JOIN LATERAL (
	-- Fetch only the most recent user prompt across all interceptions
	-- in the session.
	SELECT up.prompt
	FROM aibridge_user_prompts up
	WHERE up.interception_id = ANY(sr.interception_ids)
	ORDER BY up.created_at DESC, up.id DESC
	LIMIT 1
) slp ON true
ORDER BY
	sp.last_active_at DESC,
	sp.session_id DESC
`

type ListAIBridgeSessionsParams struct {
	AfterSessionID string    `db:"after_session_id" json:"after_session_id"`
	StartedAfter   time.Time `db:"started_after" json:"started_after"`
	StartedBefore  time.Time `db:"started_before" json:"started_before"`
	InitiatorID    uuid.UUID `db:"initiator_id" json:"initiator_id"`
	Provider       string    `db:"provider" json:"provider"`
	ProviderName   string    `db:"provider_name" json:"provider_name"`
	Model          string    `db:"model" json:"model"`
	Client         string    `db:"client" json:"client"`
	SessionID      string    `db:"session_id" json:"session_id"`
	Offset         int32     `db:"offset_" json:"offset_"`
	Limit          int32     `db:"limit_" json:"limit_"`
}

type ListAIBridgeSessionsRow struct {
	SessionID             string          `db:"session_id" json:"session_id"`
	UserID                uuid.UUID       `db:"user_id" json:"user_id"`
	UserUsername          string          `db:"user_username" json:"user_username"`
	UserName              string          `db:"user_name" json:"user_name"`
	UserAvatarUrl         string          `db:"user_avatar_url" json:"user_avatar_url"`
	Providers             []string        `db:"providers" json:"providers"`
	Models                []string        `db:"models" json:"models"`
	Client                string          `db:"client" json:"client"`
	Metadata              json.RawMessage `db:"metadata" json:"metadata"`
	StartedAt             time.Time       `db:"started_at" json:"started_at"`
	EndedAt               time.Time       `db:"ended_at" json:"ended_at"`
	Threads               int64           `db:"threads" json:"threads"`
	InputTokens           int64           `db:"input_tokens" json:"input_tokens"`
	OutputTokens          int64           `db:"output_tokens" json:"output_tokens"`
	CacheReadInputTokens  int64           `db:"cache_read_input_tokens" json:"cache_read_input_tokens"`
	CacheWriteInputTokens int64           `db:"cache_write_input_tokens" json:"cache_write_input_tokens"`
	LastPrompt            string          `db:"last_prompt" json:"last_prompt"`
	LastActiveAt          time.Time       `db:"last_active_at" json:"last_active_at"`
}

// Returns paginated sessions with aggregated metadata, token counts, and
// the most recent user prompt. A "session" is a logical grouping of
// interceptions that share the same session_id (set by the client).
//
// Pagination-first strategy: identify the page of sessions cheaply via a
// single GROUP BY scan, then do expensive lateral joins (tokens, prompts,
// first-interception metadata) only for the ~page-size result set.
func (q *sqlQuerier) ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams) ([]ListAIBridgeSessionsRow, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeSessions,
		arg.AfterSessionID,
		arg.StartedAfter,
		arg.StartedBefore,
		arg.InitiatorID,
		arg.Provider,
		arg.ProviderName,
		arg.Model,
		arg.Client,
		arg.SessionID,
		arg.Offset,
		arg.Limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListAIBridgeSessionsRow
	for rows.Next() {
		var i ListAIBridgeSessionsRow
		if err := rows.Scan(
			&i.SessionID,
			&i.UserID,
			&i.UserUsername,
			&i.UserName,
			&i.UserAvatarUrl,
			pq.Array(&i.Providers),
			pq.Array(&i.Models),
			&i.Client,
			&i.Metadata,
			&i.StartedAt,
			&i.EndedAt,
			&i.Threads,
			&i.InputTokens,
			&i.OutputTokens,
			&i.CacheReadInputTokens,
			&i.CacheWriteInputTokens,
			&i.LastPrompt,
			&i.LastActiveAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeTokenUsagesByInterceptionIDs = `-- name: ListAIBridgeTokenUsagesByInterceptionIDs :many
SELECT
	id, interception_id, provider_response_id, input_tokens, output_tokens, metadata, created_at, cache_read_input_tokens, cache_write_input_tokens
FROM
	aibridge_token_usages
WHERE
	interception_id = ANY($1::uuid[])
ORDER BY
	created_at ASC,
	id ASC
`

func (q *sqlQuerier) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeTokenUsagesByInterceptionIDs, pq.Array(interceptionIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIBridgeTokenUsage
	for rows.Next() {
		var i AIBridgeTokenUsage
		if err := rows.Scan(
			&i.ID,
			&i.InterceptionID,
			&i.ProviderResponseID,
			&i.InputTokens,
			&i.OutputTokens,
			&i.Metadata,
			&i.CreatedAt,
			&i.CacheReadInputTokens,
			&i.CacheWriteInputTokens,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeToolUsagesByInterceptionIDs = `-- name: ListAIBridgeToolUsagesByInterceptionIDs :many
SELECT
	id, interception_id, provider_response_id, server_url, tool, input, injected, invocation_error, metadata, created_at, provider_tool_call_id
FROM
	aibridge_tool_usages
WHERE
	interception_id = ANY($1::uuid[])
ORDER BY
	created_at ASC,
	id ASC
`

func (q *sqlQuerier) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeToolUsagesByInterceptionIDs, pq.Array(interceptionIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIBridgeToolUsage
	for rows.Next() {
		var i AIBridgeToolUsage
		if err := rows.Scan(
			&i.ID,
			&i.InterceptionID,
			&i.ProviderResponseID,
			&i.ServerUrl,
			&i.Tool,
			&i.Input,
			&i.Injected,
			&i.InvocationError,
			&i.Metadata,
			&i.CreatedAt,
			&i.ProviderToolCallID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAIBridgeUserPromptsByInterceptionIDs = `-- name: ListAIBridgeUserPromptsByInterceptionIDs :many
SELECT
	id, interception_id, provider_response_id, prompt, metadata, created_at
FROM
	aibridge_user_prompts
WHERE
	interception_id = ANY($1::uuid[])
ORDER BY
	created_at ASC,
	id ASC
`

func (q *sqlQuerier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeUserPrompt, error) {
	rows, err := q.db.QueryContext(ctx, listAIBridgeUserPromptsByInterceptionIDs, pq.Array(interceptionIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AIBridgeUserPrompt
	for rows.Next() {
		var i AIBridgeUserPrompt
		if err := rows.Scan(
			&i.ID,
			&i.InterceptionID,
			&i.ProviderResponseID,
			&i.Prompt,
			&i.Metadata,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateAIBridgeInterceptionEnded = `-- name: UpdateAIBridgeInterceptionEnded :one
UPDATE aibridge_interceptions
	SET ended_at = $1::timestamptz
WHERE
	id = $2::uuid
	AND ended_at IS NULL
RETURNING id, initiator_id, provider, model, started_at, metadata, ended_at, api_key_id, client, thread_parent_id, thread_root_id, client_session_id, session_id, provider_name, credential_kind, credential_hint
`

type UpdateAIBridgeInterceptionEndedParams struct {
	EndedAt time.Time `db:"ended_at" json:"ended_at"`
	ID      uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error) {
	row := q.db.QueryRowContext(ctx, updateAIBridgeInterceptionEnded, arg.EndedAt, arg.ID)
	var i AIBridgeInterception
	err := row.Scan(
		&i.ID,
		&i.InitiatorID,
		&i.Provider,
		&i.Model,
		&i.StartedAt,
		&i.Metadata,
		&i.EndedAt,
		&i.APIKeyID,
		&i.Client,
		&i.ThreadParentID,
		&i.ThreadRootID,
		&i.ClientSessionID,
		&i.SessionID,
		&i.ProviderName,
		&i.CredentialKind,
		&i.CredentialHint,
	)
	return i, err
}

const deleteGroupAIBudget = `-- name: DeleteGroupAIBudget :one
DELETE FROM group_ai_budgets WHERE group_id = $1 RETURNING group_id, spend_limit_micros, created_at, updated_at
`

func (q *sqlQuerier) DeleteGroupAIBudget(ctx context.Context, groupID uuid.UUID) (GroupAiBudget, error) {
	row := q.db.QueryRowContext(ctx, deleteGroupAIBudget, groupID)
	var i GroupAiBudget
	err := row.Scan(
		&i.GroupID,
		&i.SpendLimitMicros,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getAIModelPriceByProviderModel = `-- name: GetAIModelPriceByProviderModel :one
SELECT provider, model, input_price, output_price, cache_read_price, cache_write_price, created_at, updated_at
FROM ai_model_prices
WHERE provider = $1 AND model = $2
`

type GetAIModelPriceByProviderModelParams struct {
	Provider string `db:"provider" json:"provider"`
	Model    string `db:"model" json:"model"`
}

func (q *sqlQuerier) GetAIModelPriceByProviderModel(ctx context.Context, arg GetAIModelPriceByProviderModelParams) (AiModelPrice, error) {
	row := q.db.QueryRowContext(ctx, getAIModelPriceByProviderModel, arg.Provider, arg.Model)
	var i AiModelPrice
	err := row.Scan(
		&i.Provider,
		&i.Model,
		&i.InputPrice,
		&i.OutputPrice,
		&i.CacheReadPrice,
		&i.CacheWritePrice,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getGroupAIBudget = `-- name: GetGroupAIBudget :one
SELECT group_id, spend_limit_micros, created_at, updated_at
FROM group_ai_budgets
WHERE group_id = $1
`

func (q *sqlQuerier) GetGroupAIBudget(ctx context.Context, groupID uuid.UUID) (GroupAiBudget, error) {
	row := q.db.QueryRowContext(ctx, getGroupAIBudget, groupID)
	var i GroupAiBudget
	err := row.Scan(
		&i.GroupID,
		&i.SpendLimitMicros,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const upsertAIModelPrices = `-- name: UpsertAIModelPrices :exec
INSERT INTO ai_model_prices (
	provider, model, input_price, output_price, cache_read_price, cache_write_price
)
SELECT
	elem->>'provider',
	elem->>'model',
	(elem->>'input_price')::bigint,
	(elem->>'output_price')::bigint,
	(elem->>'cache_read_price')::bigint,
	(elem->>'cache_write_price')::bigint
FROM jsonb_array_elements($1::jsonb) AS elem
ON CONFLICT (provider, model) DO UPDATE SET
	input_price       = EXCLUDED.input_price,
	output_price      = EXCLUDED.output_price,
	cache_read_price  = EXCLUDED.cache_read_price,
	cache_write_price = EXCLUDED.cache_write_price,
	updated_at        = NOW()
`

// Upsert a batch of (provider, model) rows from a JSON array. Each element
// must have provider, model, and the four price fields; null prices are
// written as SQL NULL.
func (q *sqlQuerier) UpsertAIModelPrices(ctx context.Context, seed json.RawMessage) error {
	_, err := q.db.ExecContext(ctx, upsertAIModelPrices, seed)
	return err
}

const upsertGroupAIBudget = `-- name: UpsertGroupAIBudget :one
INSERT INTO group_ai_budgets (group_id, spend_limit_micros)
VALUES ($1, $2)
ON CONFLICT (group_id) DO UPDATE SET
	spend_limit_micros = EXCLUDED.spend_limit_micros,
	updated_at  = NOW()
RETURNING group_id, spend_limit_micros, created_at, updated_at
`

type UpsertGroupAIBudgetParams struct {
	GroupID          uuid.UUID `db:"group_id" json:"group_id"`
	SpendLimitMicros int64     `db:"spend_limit_micros" json:"spend_limit_micros"`
}

func (q *sqlQuerier) UpsertGroupAIBudget(ctx context.Context, arg UpsertGroupAIBudgetParams) (GroupAiBudget, error) {
	row := q.db.QueryRowContext(ctx, upsertGroupAIBudget, arg.GroupID, arg.SpendLimitMicros)
	var i GroupAiBudget
	err := row.Scan(
		&i.GroupID,
		&i.SpendLimitMicros,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getActiveAISeatCount = `-- name: GetActiveAISeatCount :one
SELECT
	COUNT(*)
FROM
	ai_seat_state ais
JOIN
	users u
ON
	ais.user_id = u.id
WHERE
	u.status = 'active'::user_status
	AND u.deleted = false
	AND u.is_system = false
`

func (q *sqlQuerier) GetActiveAISeatCount(ctx context.Context) (int64, error) {
	row := q.db.QueryRowContext(ctx, getActiveAISeatCount)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const upsertAISeatState = `-- name: UpsertAISeatState :one
INSERT INTO ai_seat_state (
	user_id,
	first_used_at,
	last_used_at,
	last_event_type,
	last_event_description,
	updated_at
)
VALUES
	($1, $2, $2, $3, $4, $2)
ON CONFLICT (user_id) DO UPDATE
SET
	last_used_at = EXCLUDED.last_used_at,
	last_event_type = EXCLUDED.last_event_type,
	last_event_description = EXCLUDED.last_event_description,
	updated_at = EXCLUDED.updated_at
RETURNING
	-- Postgres vodoo to know if a row was inserted.
	(xmax = 0)::boolean AS is_new
`

type UpsertAISeatStateParams struct {
	UserID               uuid.UUID         `db:"user_id" json:"user_id"`
	FirstUsedAt          time.Time         `db:"first_used_at" json:"first_used_at"`
	LastEventType        AiSeatUsageReason `db:"last_event_type" json:"last_event_type"`
	LastEventDescription string            `db:"last_event_description" json:"last_event_description"`
}

// Returns true if a new rows was inserted, false otherwise.
func (q *sqlQuerier) UpsertAISeatState(ctx context.Context, arg UpsertAISeatStateParams) (bool, error) {
	row := q.db.QueryRowContext(ctx, upsertAISeatState,
		arg.UserID,
		arg.FirstUsedAt,
		arg.LastEventType,
		arg.LastEventDescription,
	)
	var is_new bool
	err := row.Scan(&is_new)
	return is_new, err
}

const getUserAISeatStates = `-- name: GetUserAISeatStates :many
SELECT
	ais.user_id
FROM
	ai_seat_state ais
JOIN
	users u
ON
	ais.user_id = u.id
WHERE
	ais.user_id = ANY($1::uuid[])
	AND u.status = 'active'::user_status
	AND u.deleted = false
	AND u.is_system = false
`

// Returns user IDs from the provided list that are consuming an AI seat.
// Filters to active, non-deleted, non-system users to match the canonical
// seat count query (GetActiveAISeatCount).
func (q *sqlQuerier) GetUserAISeatStates(ctx context.Context, userIds []uuid.UUID) ([]uuid.UUID, error) {
	rows, err := q.db.QueryContext(ctx, getUserAISeatStates, pq.Array(userIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []uuid.UUID
	for rows.Next() {
		var user_id uuid.UUID
		if err := rows.Scan(&user_id); err != nil {
			return nil, err
		}
		items = append(items, user_id)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteAPIKeyByID = `-- name: DeleteAPIKeyByID :exec
DELETE FROM
	api_keys
WHERE
	id = $1
`

func (q *sqlQuerier) DeleteAPIKeyByID(ctx context.Context, id string) error {
	_, err := q.db.ExecContext(ctx, deleteAPIKeyByID, id)
	return err
}

const deleteAPIKeysByUserID = `-- name: DeleteAPIKeysByUserID :exec
DELETE FROM
	api_keys
WHERE
	user_id = $1
`

func (q *sqlQuerier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteAPIKeysByUserID, userID)
	return err
}

const deleteApplicationConnectAPIKeysByUserID = `-- name: DeleteApplicationConnectAPIKeysByUserID :exec
DELETE FROM
	api_keys
WHERE
	user_id = $1 AND
	'coder:application_connect'::api_key_scope = ANY(scopes)
`

func (q *sqlQuerier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteApplicationConnectAPIKeysByUserID, userID)
	return err
}

const deleteExpiredAPIKeys = `-- name: DeleteExpiredAPIKeys :execrows
WITH expired_keys AS (
	SELECT id
	FROM api_keys
	-- expired keys only
	WHERE expires_at < $1::timestamptz
	LIMIT $2
)
DELETE FROM
	api_keys
USING
	expired_keys
WHERE
	api_keys.id = expired_keys.id
`

type DeleteExpiredAPIKeysParams struct {
	Before     time.Time `db:"before" json:"before"`
	LimitCount int32     `db:"limit_count" json:"limit_count"`
}

func (q *sqlQuerier) DeleteExpiredAPIKeys(ctx context.Context, arg DeleteExpiredAPIKeysParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteExpiredAPIKeys, arg.Before, arg.LimitCount)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const expirePrebuildsAPIKeys = `-- name: ExpirePrebuildsAPIKeys :exec
WITH unexpired_prebuilds_workspace_session_tokens AS (
	SELECT id, SUBSTRING(token_name FROM 38 FOR 36)::uuid AS workspace_id
	FROM api_keys
	WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
	AND expires_at > $1::timestamptz
	AND token_name SIMILAR TO 'c42fdf75-3097-471c-8c33-fb52454d81c0_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}_session_token'
),
stale_prebuilds_workspace_session_tokens AS (
	SELECT upwst.id
	FROM unexpired_prebuilds_workspace_session_tokens upwst
	LEFT JOIN workspaces w
	ON w.id = upwst.workspace_id
	WHERE w.owner_id <> 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
),
unnamed_prebuilds_api_keys AS (
	SELECT id
	FROM api_keys
	WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
	AND token_name = ''
	AND expires_at > $1::timestamptz
)
UPDATE api_keys
SET expires_at = $1::timestamptz
WHERE id IN (
	SELECT id FROM stale_prebuilds_workspace_session_tokens
	UNION
	SELECT id FROM unnamed_prebuilds_api_keys
)
`

// Firstly, collect api_keys owned by the prebuilds user that correlate
// to workspaces no longer owned by the prebuilds user.
// Next, collect api_keys that belong to the prebuilds user but have no token name.
// These were most likely created via 'coder login' as the prebuilds user.
func (q *sqlQuerier) ExpirePrebuildsAPIKeys(ctx context.Context, now time.Time) error {
	_, err := q.db.ExecContext(ctx, expirePrebuildsAPIKeys, now)
	return err
}

const getAPIKeyByID = `-- name: GetAPIKeyByID :one
SELECT
	id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
FROM
	api_keys
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, error) {
	row := q.db.QueryRowContext(ctx, getAPIKeyByID, id)
	var i APIKey
	err := row.Scan(
		&i.ID,
		&i.HashedSecret,
		&i.UserID,
		&i.LastUsed,
		&i.ExpiresAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.LoginType,
		&i.LifetimeSeconds,
		&i.IPAddress,
		&i.TokenName,
		&i.Scopes,
		&i.AllowList,
	)
	return i, err
}

const getAPIKeyByName = `-- name: GetAPIKeyByName :one
SELECT
	id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
FROM
	api_keys
WHERE
	user_id = $1 AND
	token_name = $2 AND
	token_name != ''
LIMIT
	1
`

type GetAPIKeyByNameParams struct {
	UserID    uuid.UUID `db:"user_id" json:"user_id"`
	TokenName string    `db:"token_name" json:"token_name"`
}

// there is no unique constraint on empty token names
func (q *sqlQuerier) GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) {
	row := q.db.QueryRowContext(ctx, getAPIKeyByName, arg.UserID, arg.TokenName)
	var i APIKey
	err := row.Scan(
		&i.ID,
		&i.HashedSecret,
		&i.UserID,
		&i.LastUsed,
		&i.ExpiresAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.LoginType,
		&i.LifetimeSeconds,
		&i.IPAddress,
		&i.TokenName,
		&i.Scopes,
		&i.AllowList,
	)
	return i, err
}

const getAPIKeysByLoginType = `-- name: GetAPIKeysByLoginType :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE login_type = $1
AND ($2::bool OR expires_at > now())
`

type GetAPIKeysByLoginTypeParams struct {
	LoginType      LoginType `db:"login_type" json:"login_type"`
	IncludeExpired bool      `db:"include_expired" json:"include_expired"`
}

func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, arg GetAPIKeysByLoginTypeParams) ([]APIKey, error) {
	rows, err := q.db.QueryContext(ctx, getAPIKeysByLoginType, arg.LoginType, arg.IncludeExpired)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []APIKey
	for rows.Next() {
		var i APIKey
		if err := rows.Scan(
			&i.ID,
			&i.HashedSecret,
			&i.UserID,
			&i.LastUsed,
			&i.ExpiresAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.LoginType,
			&i.LifetimeSeconds,
			&i.IPAddress,
			&i.TokenName,
			&i.Scopes,
			&i.AllowList,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAPIKeysByUserID = `-- name: GetAPIKeysByUserID :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE login_type = $1 AND user_id = $2
AND ($3::bool OR expires_at > now())
`

type GetAPIKeysByUserIDParams struct {
	LoginType      LoginType `db:"login_type" json:"login_type"`
	UserID         uuid.UUID `db:"user_id" json:"user_id"`
	IncludeExpired bool      `db:"include_expired" json:"include_expired"`
}

func (q *sqlQuerier) GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error) {
	rows, err := q.db.QueryContext(ctx, getAPIKeysByUserID, arg.LoginType, arg.UserID, arg.IncludeExpired)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []APIKey
	for rows.Next() {
		var i APIKey
		if err := rows.Scan(
			&i.ID,
			&i.HashedSecret,
			&i.UserID,
			&i.LastUsed,
			&i.ExpiresAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.LoginType,
			&i.LifetimeSeconds,
			&i.IPAddress,
			&i.TokenName,
			&i.Scopes,
			&i.AllowList,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list FROM api_keys WHERE last_used > $1
`

func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) {
	rows, err := q.db.QueryContext(ctx, getAPIKeysLastUsedAfter, lastUsed)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []APIKey
	for rows.Next() {
		var i APIKey
		if err := rows.Scan(
			&i.ID,
			&i.HashedSecret,
			&i.UserID,
			&i.LastUsed,
			&i.ExpiresAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.LoginType,
			&i.LifetimeSeconds,
			&i.IPAddress,
			&i.TokenName,
			&i.Scopes,
			&i.AllowList,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertAPIKey = `-- name: InsertAPIKey :one
INSERT INTO
	api_keys (
		id,
		lifetime_seconds,
		hashed_secret,
		ip_address,
		user_id,
		last_used,
		expires_at,
		created_at,
		updated_at,
		login_type,
		scopes,
		allow_list,
		token_name
	)
VALUES
	($1,
	 -- If the lifetime is set to 0, default to 24hrs
	 CASE $2::bigint
	     WHEN 0 THEN 86400
		 ELSE $2::bigint
	 END
	 , $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, token_name, scopes, allow_list
`

type InsertAPIKeyParams struct {
	ID              string       `db:"id" json:"id"`
	LifetimeSeconds int64        `db:"lifetime_seconds" json:"lifetime_seconds"`
	HashedSecret    []byte       `db:"hashed_secret" json:"hashed_secret"`
	IPAddress       pqtype.Inet  `db:"ip_address" json:"ip_address"`
	UserID          uuid.UUID    `db:"user_id" json:"user_id"`
	LastUsed        time.Time    `db:"last_used" json:"last_used"`
	ExpiresAt       time.Time    `db:"expires_at" json:"expires_at"`
	CreatedAt       time.Time    `db:"created_at" json:"created_at"`
	UpdatedAt       time.Time    `db:"updated_at" json:"updated_at"`
	LoginType       LoginType    `db:"login_type" json:"login_type"`
	Scopes          APIKeyScopes `db:"scopes" json:"scopes"`
	AllowList       AllowList    `db:"allow_list" json:"allow_list"`
	TokenName       string       `db:"token_name" json:"token_name"`
}

func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) {
	row := q.db.QueryRowContext(ctx, insertAPIKey,
		arg.ID,
		arg.LifetimeSeconds,
		arg.HashedSecret,
		arg.IPAddress,
		arg.UserID,
		arg.LastUsed,
		arg.ExpiresAt,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.LoginType,
		arg.Scopes,
		arg.AllowList,
		arg.TokenName,
	)
	var i APIKey
	err := row.Scan(
		&i.ID,
		&i.HashedSecret,
		&i.UserID,
		&i.LastUsed,
		&i.ExpiresAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.LoginType,
		&i.LifetimeSeconds,
		&i.IPAddress,
		&i.TokenName,
		&i.Scopes,
		&i.AllowList,
	)
	return i, err
}

const updateAPIKeyByID = `-- name: UpdateAPIKeyByID :exec
UPDATE
	api_keys
SET
	last_used = $2,
	expires_at = $3,
	ip_address = $4
WHERE
	id = $1
`

type UpdateAPIKeyByIDParams struct {
	ID        string      `db:"id" json:"id"`
	LastUsed  time.Time   `db:"last_used" json:"last_used"`
	ExpiresAt time.Time   `db:"expires_at" json:"expires_at"`
	IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
}

func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateAPIKeyByID,
		arg.ID,
		arg.LastUsed,
		arg.ExpiresAt,
		arg.IPAddress,
	)
	return err
}

const countAuditLogs = `-- name: CountAuditLogs :one
SELECT COUNT(*) FROM (
	SELECT 1
	FROM audit_logs
		LEFT JOIN users ON audit_logs.user_id = users.id
		LEFT JOIN organizations ON audit_logs.organization_id = organizations.id
		-- First join on workspaces to get the initial workspace create
		-- to workspace build 1 id. This is because the first create is
		-- is a different audit log than subsequent starts.
		LEFT JOIN workspaces ON audit_logs.resource_type = 'workspace'
		AND audit_logs.resource_id = workspaces.id
		-- Get the reason from the build if the resource type
		-- is a workspace_build
		LEFT JOIN workspace_builds wb_build ON audit_logs.resource_type = 'workspace_build'
		AND audit_logs.resource_id = wb_build.id
		-- Get the reason from the build #1 if this is the first
		-- workspace create.
		LEFT JOIN workspace_builds wb_workspace ON audit_logs.resource_type = 'workspace'
		AND audit_logs.action = 'create'
		AND workspaces.id = wb_workspace.workspace_id
		AND wb_workspace.build_number = 1
	WHERE
		-- Filter resource_type
		CASE
			WHEN $1::text != '' THEN resource_type = $1::resource_type
			ELSE true
		END
		-- Filter resource_id
		AND CASE
			WHEN $2::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN resource_id = $2
			ELSE true
		END
		-- Filter organization_id
		AND CASE
			WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN audit_logs.organization_id = $3
			ELSE true
		END
		-- Filter by resource_target
		AND CASE
			WHEN $4::text != '' THEN resource_target = $4
			ELSE true
		END
		-- Filter action
		AND CASE
			WHEN $5::text != '' THEN action = $5::audit_action
			ELSE true
		END
		-- Filter by user_id
		AND CASE
			WHEN $6::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN user_id = $6
			ELSE true
		END
		-- Filter by username
		AND CASE
			WHEN $7::text != '' THEN user_id = (
				SELECT id
				FROM users
				WHERE lower(username) = lower($7)
					AND deleted = false
			)
			ELSE true
		END
		-- Filter by user_email
		AND CASE
			WHEN $8::text != '' THEN users.email = $8
			ELSE true
		END
		-- Filter by date_from
		AND CASE
			WHEN $9::timestamp with time zone != '0001-01-01 00:00:00Z' THEN "time" >= $9
			ELSE true
		END
		-- Filter by date_to
		AND CASE
			WHEN $10::timestamp with time zone != '0001-01-01 00:00:00Z' THEN "time" <= $10
			ELSE true
		END
		-- Filter by build_reason
		AND CASE
			WHEN $11::text != '' THEN COALESCE(wb_build.reason::text, wb_workspace.reason::text) = $11
			ELSE true
		END
		-- Filter request_id
		AND CASE
			WHEN $12::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN audit_logs.request_id = $12
			ELSE true
		END
		-- Authorize Filter clause will be injected below in CountAuthorizedAuditLogs
		-- @authorize_filter
	-- Avoid a slow scan on a large table with joins. The caller
	-- passes the count cap and we add 1 so the frontend can detect
	-- capping and show "... of N+". A cap of 0 means no limit (NULLIF
	-- -> NULL + 1 = NULL).
	-- NOTE: Parameterizing this so that we can easily change from,
	-- e.g., 2000 to 5000. However, use literal NULL (or no LIMIT)
	-- here if disabling the capping on a large table permanently.
	-- This way the PG planner can plan parallel execution for
	-- potential large wins.
	LIMIT NULLIF($13::int, 0) + 1
) AS limited_count
`

type CountAuditLogsParams struct {
	ResourceType   string    `db:"resource_type" json:"resource_type"`
	ResourceID     uuid.UUID `db:"resource_id" json:"resource_id"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	ResourceTarget string    `db:"resource_target" json:"resource_target"`
	Action         string    `db:"action" json:"action"`
	UserID         uuid.UUID `db:"user_id" json:"user_id"`
	Username       string    `db:"username" json:"username"`
	Email          string    `db:"email" json:"email"`
	DateFrom       time.Time `db:"date_from" json:"date_from"`
	DateTo         time.Time `db:"date_to" json:"date_to"`
	BuildReason    string    `db:"build_reason" json:"build_reason"`
	RequestID      uuid.UUID `db:"request_id" json:"request_id"`
	CountCap       int32     `db:"count_cap" json:"count_cap"`
}

func (q *sqlQuerier) CountAuditLogs(ctx context.Context, arg CountAuditLogsParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, countAuditLogs,
		arg.ResourceType,
		arg.ResourceID,
		arg.OrganizationID,
		arg.ResourceTarget,
		arg.Action,
		arg.UserID,
		arg.Username,
		arg.Email,
		arg.DateFrom,
		arg.DateTo,
		arg.BuildReason,
		arg.RequestID,
		arg.CountCap,
	)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const deleteOldAuditLogConnectionEvents = `-- name: DeleteOldAuditLogConnectionEvents :exec
DELETE FROM audit_logs
WHERE id IN (
    SELECT id FROM audit_logs
    WHERE
        (
            action = 'connect'
            OR action = 'disconnect'
            OR action = 'open'
            OR action = 'close'
        )
        AND "time" < $1::timestamp with time zone
    ORDER BY "time" ASC
    LIMIT $2
)
`

type DeleteOldAuditLogConnectionEventsParams struct {
	BeforeTime time.Time `db:"before_time" json:"before_time"`
	LimitCount int32     `db:"limit_count" json:"limit_count"`
}

func (q *sqlQuerier) DeleteOldAuditLogConnectionEvents(ctx context.Context, arg DeleteOldAuditLogConnectionEventsParams) error {
	_, err := q.db.ExecContext(ctx, deleteOldAuditLogConnectionEvents, arg.BeforeTime, arg.LimitCount)
	return err
}

const deleteOldAuditLogs = `-- name: DeleteOldAuditLogs :execrows
WITH old_logs AS (
    SELECT id
    FROM audit_logs
    WHERE
        "time" < $1::timestamp with time zone
        AND action NOT IN ('connect', 'disconnect', 'open', 'close')
    ORDER BY "time" ASC
    LIMIT $2
)
DELETE FROM audit_logs
USING old_logs
WHERE audit_logs.id = old_logs.id
`

type DeleteOldAuditLogsParams struct {
	BeforeTime time.Time `db:"before_time" json:"before_time"`
	LimitCount int32     `db:"limit_count" json:"limit_count"`
}

// Deletes old audit logs based on retention policy, excluding deprecated
// connection events (connect, disconnect, open, close) which are handled
// separately by DeleteOldAuditLogConnectionEvents.
func (q *sqlQuerier) DeleteOldAuditLogs(ctx context.Context, arg DeleteOldAuditLogsParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteOldAuditLogs, arg.BeforeTime, arg.LimitCount)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const getAuditLogsOffset = `-- name: GetAuditLogsOffset :many
SELECT audit_logs.id, audit_logs.time, audit_logs.user_id, audit_logs.organization_id, audit_logs.ip, audit_logs.user_agent, audit_logs.resource_type, audit_logs.resource_id, audit_logs.resource_target, audit_logs.action, audit_logs.diff, audit_logs.status_code, audit_logs.additional_fields, audit_logs.request_id, audit_logs.resource_icon,
	-- sqlc.embed(users) would be nice but it does not seem to play well with
	-- left joins.
	users.username AS user_username,
	users.name AS user_name,
	users.email AS user_email,
	users.created_at AS user_created_at,
	users.updated_at AS user_updated_at,
	users.last_seen_at AS user_last_seen_at,
	users.status AS user_status,
	users.login_type AS user_login_type,
	users.rbac_roles AS user_roles,
	users.avatar_url AS user_avatar_url,
	users.deleted AS user_deleted,
	users.quiet_hours_schedule AS user_quiet_hours_schedule,
	COALESCE(organizations.name, '') AS organization_name,
	COALESCE(organizations.display_name, '') AS organization_display_name,
	COALESCE(organizations.icon, '') AS organization_icon
FROM audit_logs
	LEFT JOIN users ON audit_logs.user_id = users.id
	LEFT JOIN organizations ON audit_logs.organization_id = organizations.id
	-- First join on workspaces to get the initial workspace create
	-- to workspace build 1 id. This is because the first create is
	-- is a different audit log than subsequent starts.
	LEFT JOIN workspaces ON audit_logs.resource_type = 'workspace'
	AND audit_logs.resource_id = workspaces.id
	-- Get the reason from the build if the resource type
	-- is a workspace_build
	LEFT JOIN workspace_builds wb_build ON audit_logs.resource_type = 'workspace_build'
	AND audit_logs.resource_id = wb_build.id
	-- Get the reason from the build #1 if this is the first
	-- workspace create.
	LEFT JOIN workspace_builds wb_workspace ON audit_logs.resource_type = 'workspace'
	AND audit_logs.action = 'create'
	AND workspaces.id = wb_workspace.workspace_id
	AND wb_workspace.build_number = 1
WHERE
	-- Filter resource_type
	CASE
		WHEN $1::text != '' THEN resource_type = $1::resource_type
		ELSE true
	END
	-- Filter resource_id
	AND CASE
		WHEN $2::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN resource_id = $2
		ELSE true
	END
	-- Filter organization_id
	AND CASE
		WHEN $3::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN audit_logs.organization_id = $3
		ELSE true
	END
	-- Filter by resource_target
	AND CASE
		WHEN $4::text != '' THEN resource_target = $4
		ELSE true
	END
	-- Filter action
	AND CASE
		WHEN $5::text != '' THEN action = $5::audit_action
		ELSE true
	END
	-- Filter by user_id
	AND CASE
		WHEN $6::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN user_id = $6
		ELSE true
	END
	-- Filter by username
	AND CASE
		WHEN $7::text != '' THEN user_id = (
			SELECT id
			FROM users
			WHERE lower(username) = lower($7)
				AND deleted = false
		)
		ELSE true
	END
	-- Filter by user_email
	AND CASE
		WHEN $8::text != '' THEN users.email = $8
		ELSE true
	END
	-- Filter by date_from
	AND CASE
		WHEN $9::timestamp with time zone != '0001-01-01 00:00:00Z' THEN "time" >= $9
		ELSE true
	END
	-- Filter by date_to
	AND CASE
		WHEN $10::timestamp with time zone != '0001-01-01 00:00:00Z' THEN "time" <= $10
		ELSE true
	END
	-- Filter by build_reason
	AND CASE
		WHEN $11::text != '' THEN COALESCE(wb_build.reason::text, wb_workspace.reason::text) = $11
		ELSE true
	END
	-- Filter request_id
	AND CASE
		WHEN $12::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN audit_logs.request_id = $12
		ELSE true
	END
	-- Authorize Filter clause will be injected below in GetAuthorizedAuditLogsOffset
	-- @authorize_filter
ORDER BY "time" DESC
LIMIT -- a limit of 0 means "no limit". The audit log table is unbounded
	-- in size, and is expected to be quite large. Implement a default
	-- limit of 100 to prevent accidental excessively large queries.
	COALESCE(NULLIF($14::int, 0), 100) OFFSET $13
`

type GetAuditLogsOffsetParams struct {
	ResourceType   string    `db:"resource_type" json:"resource_type"`
	ResourceID     uuid.UUID `db:"resource_id" json:"resource_id"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	ResourceTarget string    `db:"resource_target" json:"resource_target"`
	Action         string    `db:"action" json:"action"`
	UserID         uuid.UUID `db:"user_id" json:"user_id"`
	Username       string    `db:"username" json:"username"`
	Email          string    `db:"email" json:"email"`
	DateFrom       time.Time `db:"date_from" json:"date_from"`
	DateTo         time.Time `db:"date_to" json:"date_to"`
	BuildReason    string    `db:"build_reason" json:"build_reason"`
	RequestID      uuid.UUID `db:"request_id" json:"request_id"`
	OffsetOpt      int32     `db:"offset_opt" json:"offset_opt"`
	LimitOpt       int32     `db:"limit_opt" json:"limit_opt"`
}

type GetAuditLogsOffsetRow struct {
	AuditLog                AuditLog       `db:"audit_log" json:"audit_log"`
	UserUsername            sql.NullString `db:"user_username" json:"user_username"`
	UserName                sql.NullString `db:"user_name" json:"user_name"`
	UserEmail               sql.NullString `db:"user_email" json:"user_email"`
	UserCreatedAt           sql.NullTime   `db:"user_created_at" json:"user_created_at"`
	UserUpdatedAt           sql.NullTime   `db:"user_updated_at" json:"user_updated_at"`
	UserLastSeenAt          sql.NullTime   `db:"user_last_seen_at" json:"user_last_seen_at"`
	UserStatus              NullUserStatus `db:"user_status" json:"user_status"`
	UserLoginType           NullLoginType  `db:"user_login_type" json:"user_login_type"`
	UserRoles               pq.StringArray `db:"user_roles" json:"user_roles"`
	UserAvatarUrl           sql.NullString `db:"user_avatar_url" json:"user_avatar_url"`
	UserDeleted             sql.NullBool   `db:"user_deleted" json:"user_deleted"`
	UserQuietHoursSchedule  sql.NullString `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
	OrganizationName        string         `db:"organization_name" json:"organization_name"`
	OrganizationDisplayName string         `db:"organization_display_name" json:"organization_display_name"`
	OrganizationIcon        string         `db:"organization_icon" json:"organization_icon"`
}

// GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided
// ID.
func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOffsetParams) ([]GetAuditLogsOffsetRow, error) {
	rows, err := q.db.QueryContext(ctx, getAuditLogsOffset,
		arg.ResourceType,
		arg.ResourceID,
		arg.OrganizationID,
		arg.ResourceTarget,
		arg.Action,
		arg.UserID,
		arg.Username,
		arg.Email,
		arg.DateFrom,
		arg.DateTo,
		arg.BuildReason,
		arg.RequestID,
		arg.OffsetOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetAuditLogsOffsetRow
	for rows.Next() {
		var i GetAuditLogsOffsetRow
		if err := rows.Scan(
			&i.AuditLog.ID,
			&i.AuditLog.Time,
			&i.AuditLog.UserID,
			&i.AuditLog.OrganizationID,
			&i.AuditLog.Ip,
			&i.AuditLog.UserAgent,
			&i.AuditLog.ResourceType,
			&i.AuditLog.ResourceID,
			&i.AuditLog.ResourceTarget,
			&i.AuditLog.Action,
			&i.AuditLog.Diff,
			&i.AuditLog.StatusCode,
			&i.AuditLog.AdditionalFields,
			&i.AuditLog.RequestID,
			&i.AuditLog.ResourceIcon,
			&i.UserUsername,
			&i.UserName,
			&i.UserEmail,
			&i.UserCreatedAt,
			&i.UserUpdatedAt,
			&i.UserLastSeenAt,
			&i.UserStatus,
			&i.UserLoginType,
			&i.UserRoles,
			&i.UserAvatarUrl,
			&i.UserDeleted,
			&i.UserQuietHoursSchedule,
			&i.OrganizationName,
			&i.OrganizationDisplayName,
			&i.OrganizationIcon,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertAuditLog = `-- name: InsertAuditLog :one
INSERT INTO audit_logs (
		id,
		"time",
		user_id,
		organization_id,
		ip,
		user_agent,
		resource_type,
		resource_id,
		resource_target,
		action,
		diff,
		status_code,
		additional_fields,
		request_id,
		resource_icon
	)
VALUES (
		$1,
		$2,
		$3,
		$4,
		$5,
		$6,
		$7,
		$8,
		$9,
		$10,
		$11,
		$12,
		$13,
		$14,
		$15
	)
RETURNING id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code, additional_fields, request_id, resource_icon
`

type InsertAuditLogParams struct {
	ID               uuid.UUID       `db:"id" json:"id"`
	Time             time.Time       `db:"time" json:"time"`
	UserID           uuid.UUID       `db:"user_id" json:"user_id"`
	OrganizationID   uuid.UUID       `db:"organization_id" json:"organization_id"`
	Ip               pqtype.Inet     `db:"ip" json:"ip"`
	UserAgent        sql.NullString  `db:"user_agent" json:"user_agent"`
	ResourceType     ResourceType    `db:"resource_type" json:"resource_type"`
	ResourceID       uuid.UUID       `db:"resource_id" json:"resource_id"`
	ResourceTarget   string          `db:"resource_target" json:"resource_target"`
	Action           AuditAction     `db:"action" json:"action"`
	Diff             json.RawMessage `db:"diff" json:"diff"`
	StatusCode       int32           `db:"status_code" json:"status_code"`
	AdditionalFields json.RawMessage `db:"additional_fields" json:"additional_fields"`
	RequestID        uuid.UUID       `db:"request_id" json:"request_id"`
	ResourceIcon     string          `db:"resource_icon" json:"resource_icon"`
}

func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) {
	row := q.db.QueryRowContext(ctx, insertAuditLog,
		arg.ID,
		arg.Time,
		arg.UserID,
		arg.OrganizationID,
		arg.Ip,
		arg.UserAgent,
		arg.ResourceType,
		arg.ResourceID,
		arg.ResourceTarget,
		arg.Action,
		arg.Diff,
		arg.StatusCode,
		arg.AdditionalFields,
		arg.RequestID,
		arg.ResourceIcon,
	)
	var i AuditLog
	err := row.Scan(
		&i.ID,
		&i.Time,
		&i.UserID,
		&i.OrganizationID,
		&i.Ip,
		&i.UserAgent,
		&i.ResourceType,
		&i.ResourceID,
		&i.ResourceTarget,
		&i.Action,
		&i.Diff,
		&i.StatusCode,
		&i.AdditionalFields,
		&i.RequestID,
		&i.ResourceIcon,
	)
	return i, err
}

const deleteOldBoundaryLogs = `-- name: DeleteOldBoundaryLogs :execrows
WITH old_logs AS (
    SELECT id
    FROM boundary_logs
    WHERE captured_at < $1::timestamptz
    ORDER BY captured_at ASC
    LIMIT $2
)
DELETE FROM boundary_logs
USING old_logs
WHERE boundary_logs.id = old_logs.id
`

type DeleteOldBoundaryLogsParams struct {
	BeforeTime time.Time `db:"before_time" json:"before_time"`
	LimitCount int32     `db:"limit_count" json:"limit_count"`
}

// Deletes boundary logs older than the given time, bounded by a row limit
// to avoid long-running transactions.
func (q *sqlQuerier) DeleteOldBoundaryLogs(ctx context.Context, arg DeleteOldBoundaryLogsParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteOldBoundaryLogs, arg.BeforeTime, arg.LimitCount)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const getBoundaryLogByID = `-- name: GetBoundaryLogByID :one
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule FROM boundary_logs WHERE id = $1
`

func (q *sqlQuerier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (BoundaryLog, error) {
	row := q.db.QueryRowContext(ctx, getBoundaryLogByID, id)
	var i BoundaryLog
	err := row.Scan(
		&i.ID,
		&i.SessionID,
		&i.SequenceNumber,
		&i.CapturedAt,
		&i.CreatedAt,
		&i.Proto,
		&i.Method,
		&i.Detail,
		&i.MatchedRule,
	)
	return i, err
}

const getBoundarySessionByID = `-- name: GetBoundarySessionByID :one
SELECT id, workspace_agent_id, confined_process_name, started_at, updated_at FROM boundary_sessions WHERE id = $1
`

func (q *sqlQuerier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (BoundarySession, error) {
	row := q.db.QueryRowContext(ctx, getBoundarySessionByID, id)
	var i BoundarySession
	err := row.Scan(
		&i.ID,
		&i.WorkspaceAgentID,
		&i.ConfinedProcessName,
		&i.StartedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const insertBoundaryLog = `-- name: InsertBoundaryLog :one
INSERT INTO boundary_logs (
    id,
    session_id,
    sequence_number,
    captured_at,
    created_at,
    proto,
    method,
    detail,
    matched_rule
) VALUES (
    $1,
    $2,
    $3,
    $4,
    $5,
    $6,
    $7,
    $8,
    $9
) RETURNING id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
`

type InsertBoundaryLogParams struct {
	ID             uuid.UUID      `db:"id" json:"id"`
	SessionID      uuid.UUID      `db:"session_id" json:"session_id"`
	SequenceNumber int32          `db:"sequence_number" json:"sequence_number"`
	CapturedAt     time.Time      `db:"captured_at" json:"captured_at"`
	CreatedAt      time.Time      `db:"created_at" json:"created_at"`
	Proto          string         `db:"proto" json:"proto"`
	Method         string         `db:"method" json:"method"`
	Detail         string         `db:"detail" json:"detail"`
	MatchedRule    sql.NullString `db:"matched_rule" json:"matched_rule"`
}

func (q *sqlQuerier) InsertBoundaryLog(ctx context.Context, arg InsertBoundaryLogParams) (BoundaryLog, error) {
	row := q.db.QueryRowContext(ctx, insertBoundaryLog,
		arg.ID,
		arg.SessionID,
		arg.SequenceNumber,
		arg.CapturedAt,
		arg.CreatedAt,
		arg.Proto,
		arg.Method,
		arg.Detail,
		arg.MatchedRule,
	)
	var i BoundaryLog
	err := row.Scan(
		&i.ID,
		&i.SessionID,
		&i.SequenceNumber,
		&i.CapturedAt,
		&i.CreatedAt,
		&i.Proto,
		&i.Method,
		&i.Detail,
		&i.MatchedRule,
	)
	return i, err
}

const insertBoundarySession = `-- name: InsertBoundarySession :one
INSERT INTO boundary_sessions (
    id,
    workspace_agent_id,
    confined_process_name,
    started_at,
    updated_at
) VALUES (
    $1,
    $2,
    $3,
    $4,
    $5
) RETURNING id, workspace_agent_id, confined_process_name, started_at, updated_at
`

type InsertBoundarySessionParams struct {
	ID                  uuid.UUID `db:"id" json:"id"`
	WorkspaceAgentID    uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
	ConfinedProcessName string    `db:"confined_process_name" json:"confined_process_name"`
	StartedAt           time.Time `db:"started_at" json:"started_at"`
	UpdatedAt           time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) InsertBoundarySession(ctx context.Context, arg InsertBoundarySessionParams) (BoundarySession, error) {
	row := q.db.QueryRowContext(ctx, insertBoundarySession,
		arg.ID,
		arg.WorkspaceAgentID,
		arg.ConfinedProcessName,
		arg.StartedAt,
		arg.UpdatedAt,
	)
	var i BoundarySession
	err := row.Scan(
		&i.ID,
		&i.WorkspaceAgentID,
		&i.ConfinedProcessName,
		&i.StartedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const listBoundaryLogsBySessionID = `-- name: ListBoundaryLogsBySessionID :many
SELECT id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
FROM boundary_logs
WHERE
    session_id = $1
    AND CASE
        WHEN $2::int IS NOT NULL THEN sequence_number > $2
        ELSE true
    END
    AND CASE
        WHEN $3::int IS NOT NULL THEN sequence_number < $3
        ELSE true
    END
ORDER BY sequence_number ASC
LIMIT COALESCE(NULLIF($4::int, 0), 100)
`

type ListBoundaryLogsBySessionIDParams struct {
	SessionID uuid.UUID     `db:"session_id" json:"session_id"`
	SeqAfter  sql.NullInt32 `db:"seq_after" json:"seq_after"`
	SeqBefore sql.NullInt32 `db:"seq_before" json:"seq_before"`
	LimitOpt  int32         `db:"limit_opt" json:"limit_opt"`
}

// Lists boundary logs for a session, sorted by sequence number ascending.
// Supports optional exclusive sequence number bounds (seq_after, seq_before)
// for fetching events between two known interceptions.
func (q *sqlQuerier) ListBoundaryLogsBySessionID(ctx context.Context, arg ListBoundaryLogsBySessionIDParams) ([]BoundaryLog, error) {
	rows, err := q.db.QueryContext(ctx, listBoundaryLogsBySessionID,
		arg.SessionID,
		arg.SeqAfter,
		arg.SeqBefore,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []BoundaryLog
	for rows.Next() {
		var i BoundaryLog
		if err := rows.Scan(
			&i.ID,
			&i.SessionID,
			&i.SequenceNumber,
			&i.CapturedAt,
			&i.CreatedAt,
			&i.Proto,
			&i.Method,
			&i.Detail,
			&i.MatchedRule,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAndResetBoundaryUsageSummary = `-- name: GetAndResetBoundaryUsageSummary :one
WITH deleted AS (
    DELETE FROM boundary_usage_stats
    RETURNING replica_id, unique_workspaces_count, unique_users_count, allowed_requests, denied_requests, window_start, updated_at
)
SELECT
    COALESCE(SUM(unique_workspaces_count) FILTER (
        WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval
    ), 0)::bigint AS unique_workspaces,
    COALESCE(SUM(unique_users_count) FILTER (
        WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval
    ), 0)::bigint AS unique_users,
    COALESCE(SUM(allowed_requests) FILTER (
        WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval
    ), 0)::bigint AS allowed_requests,
    COALESCE(SUM(denied_requests) FILTER (
        WHERE window_start >= NOW() - ($1::bigint || ' ms')::interval
    ), 0)::bigint AS denied_requests
FROM deleted
`

type GetAndResetBoundaryUsageSummaryRow struct {
	UniqueWorkspaces int64 `db:"unique_workspaces" json:"unique_workspaces"`
	UniqueUsers      int64 `db:"unique_users" json:"unique_users"`
	AllowedRequests  int64 `db:"allowed_requests" json:"allowed_requests"`
	DeniedRequests   int64 `db:"denied_requests" json:"denied_requests"`
}

// Atomic read+delete prevents replicas that flush between a separate read and
// reset from having their data deleted before the next snapshot. Uses a common
// table expression with DELETE...RETURNING so the rows we sum are exactly the
// rows we delete. Stale rows are excluded from the sum but still deleted.
func (q *sqlQuerier) GetAndResetBoundaryUsageSummary(ctx context.Context, maxStalenessMs int64) (GetAndResetBoundaryUsageSummaryRow, error) {
	row := q.db.QueryRowContext(ctx, getAndResetBoundaryUsageSummary, maxStalenessMs)
	var i GetAndResetBoundaryUsageSummaryRow
	err := row.Scan(
		&i.UniqueWorkspaces,
		&i.UniqueUsers,
		&i.AllowedRequests,
		&i.DeniedRequests,
	)
	return i, err
}

const upsertBoundaryUsageStats = `-- name: UpsertBoundaryUsageStats :one
INSERT INTO boundary_usage_stats (
    replica_id,
    unique_workspaces_count,
    unique_users_count,
    allowed_requests,
    denied_requests,
    window_start,
    updated_at
) VALUES (
    $1,
    $2,
    $3,
    $4,
    $5,
    NOW(),
    NOW()
) ON CONFLICT (replica_id) DO UPDATE SET
    unique_workspaces_count = $6,
    unique_users_count = $7,
    allowed_requests = boundary_usage_stats.allowed_requests + EXCLUDED.allowed_requests,
    denied_requests = boundary_usage_stats.denied_requests + EXCLUDED.denied_requests,
    updated_at = NOW()
RETURNING (xmax = 0) AS new_period
`

type UpsertBoundaryUsageStatsParams struct {
	ReplicaID             uuid.UUID `db:"replica_id" json:"replica_id"`
	UniqueWorkspacesDelta int64     `db:"unique_workspaces_delta" json:"unique_workspaces_delta"`
	UniqueUsersDelta      int64     `db:"unique_users_delta" json:"unique_users_delta"`
	AllowedRequests       int64     `db:"allowed_requests" json:"allowed_requests"`
	DeniedRequests        int64     `db:"denied_requests" json:"denied_requests"`
	UniqueWorkspacesCount int64     `db:"unique_workspaces_count" json:"unique_workspaces_count"`
	UniqueUsersCount      int64     `db:"unique_users_count" json:"unique_users_count"`
}

// Upserts boundary usage statistics for a replica. On INSERT (new period), uses
// delta values for unique counts (only data since last flush). On UPDATE, uses
// cumulative values for unique counts (accurate period totals). Request counts
// are always deltas, accumulated in DB. Returns true if insert, false if update.
func (q *sqlQuerier) UpsertBoundaryUsageStats(ctx context.Context, arg UpsertBoundaryUsageStatsParams) (bool, error) {
	row := q.db.QueryRowContext(ctx, upsertBoundaryUsageStats,
		arg.ReplicaID,
		arg.UniqueWorkspacesDelta,
		arg.UniqueUsersDelta,
		arg.AllowedRequests,
		arg.DeniedRequests,
		arg.UniqueWorkspacesCount,
		arg.UniqueUsersCount,
	)
	var new_period bool
	err := row.Scan(&new_period)
	return new_period, err
}

const deleteChatDebugDataAfterMessageID = `-- name: DeleteChatDebugDataAfterMessageID :execrows
WITH affected_runs AS (
    SELECT DISTINCT run.id
    FROM chat_debug_runs run
    WHERE run.chat_id = $1::uuid
        AND run.started_at < $2::timestamptz
        AND (
            run.history_tip_message_id > $3::bigint
            OR run.trigger_message_id > $3::bigint
        )

    UNION

    SELECT DISTINCT step.run_id AS id
    FROM chat_debug_steps step
    JOIN chat_debug_runs run ON run.id = step.run_id
        AND run.chat_id = step.chat_id
    WHERE step.chat_id = $1::uuid
        AND run.started_at < $2::timestamptz
        AND (
            step.assistant_message_id > $3::bigint
            OR step.history_tip_message_id > $3::bigint
        )
)
DELETE FROM chat_debug_runs
WHERE chat_id = $1::uuid
    AND id IN (SELECT id FROM affected_runs)
`

type DeleteChatDebugDataAfterMessageIDParams struct {
	ChatID        uuid.UUID `db:"chat_id" json:"chat_id"`
	StartedBefore time.Time `db:"started_before" json:"started_before"`
	MessageID     int64     `db:"message_id" json:"message_id"`
}

// Deletes debug runs (and their cascaded steps) whose message IDs
// exceed the cutoff. The started_before bound prevents retried
// cleanup from deleting runs created by a replacement turn that
// raced ahead of the retry window.
func (q *sqlQuerier) DeleteChatDebugDataAfterMessageID(ctx context.Context, arg DeleteChatDebugDataAfterMessageIDParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteChatDebugDataAfterMessageID, arg.ChatID, arg.StartedBefore, arg.MessageID)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const deleteChatDebugDataByChatID = `-- name: DeleteChatDebugDataByChatID :execrows
DELETE FROM chat_debug_runs
WHERE chat_id = $1::uuid
    AND started_at < $2::timestamptz
`

type DeleteChatDebugDataByChatIDParams struct {
	ChatID        uuid.UUID `db:"chat_id" json:"chat_id"`
	StartedBefore time.Time `db:"started_before" json:"started_before"`
}

// The started_before bound prevents retried cleanup from deleting
// runs created by a replacement turn that races ahead of the retry
// window (for example, after an unarchive races with a pending
// archive-cleanup retry).
func (q *sqlQuerier) DeleteChatDebugDataByChatID(ctx context.Context, arg DeleteChatDebugDataByChatIDParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteChatDebugDataByChatID, arg.ChatID, arg.StartedBefore)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const deleteOldChatDebugRuns = `-- name: DeleteOldChatDebugRuns :execrows
WITH deletable AS (
    SELECT id, chat_id
    FROM chat_debug_runs
    WHERE updated_at < $1::timestamptz
    ORDER BY updated_at ASC
    LIMIT $2::int
)
DELETE FROM chat_debug_runs
USING deletable
WHERE chat_debug_runs.id = deletable.id
    AND chat_debug_runs.chat_id = deletable.chat_id
`

type DeleteOldChatDebugRunsParams struct {
	BeforeTime time.Time `db:"before_time" json:"before_time"`
	LimitCount int32     `db:"limit_count" json:"limit_count"`
}

// updated_at is the retention clock, so the window starts after the run
// stops being written to.
// Intentionally no finished_at IS NOT NULL guard: abandoned in-flight rows
// older than the cutoff are also purged.
func (q *sqlQuerier) DeleteOldChatDebugRuns(ctx context.Context, arg DeleteOldChatDebugRunsParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteOldChatDebugRuns, arg.BeforeTime, arg.LimitCount)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const finalizeStaleChatDebugRows = `-- name: FinalizeStaleChatDebugRows :one
WITH finalized_runs AS (
    UPDATE chat_debug_runs
    SET
        status = 'interrupted',
        updated_at = $1::timestamptz,
        finished_at = $1::timestamptz
    WHERE updated_at < $2::timestamptz
        AND finished_at IS NULL
        AND status NOT IN ('completed', 'error', 'interrupted')
    RETURNING id
), finalized_steps AS (
    UPDATE chat_debug_steps
    SET
        status = 'interrupted',
        updated_at = $1::timestamptz,
        finished_at = $1::timestamptz
    WHERE (
            updated_at < $2::timestamptz
            OR run_id IN (SELECT id FROM finalized_runs)
        )
        AND finished_at IS NULL
        AND status NOT IN ('completed', 'error', 'interrupted')
    RETURNING 1
)
SELECT
    (SELECT COUNT(*) FROM finalized_runs)::bigint AS runs_finalized,
    (SELECT COUNT(*) FROM finalized_steps)::bigint AS steps_finalized
`

type FinalizeStaleChatDebugRowsParams struct {
	Now           time.Time `db:"now" json:"now"`
	UpdatedBefore time.Time `db:"updated_before" json:"updated_before"`
}

type FinalizeStaleChatDebugRowsRow struct {
	RunsFinalized  int64 `db:"runs_finalized" json:"runs_finalized"`
	StepsFinalized int64 `db:"steps_finalized" json:"steps_finalized"`
}

// Marks orphaned in-progress rows as interrupted so they do not stay
// in a non-terminal state forever. The NOT IN list must match the
// terminal statuses defined by ChatDebugStatus in codersdk/chats.go.
//
// The steps CTE also catches steps whose parent run was just finalized
// (via run_id IN), because PostgreSQL data-modifying CTEs share the
// same snapshot and cannot see each other's row updates. Without this,
// a step with a recent updated_at would survive its run's finalization
// and remain in 'in_progress' state permanently.
//
// @now is the caller's clock timestamp so that mock-clock tests stay
// consistent with the @updated_before cutoff.
func (q *sqlQuerier) FinalizeStaleChatDebugRows(ctx context.Context, arg FinalizeStaleChatDebugRowsParams) (FinalizeStaleChatDebugRowsRow, error) {
	row := q.db.QueryRowContext(ctx, finalizeStaleChatDebugRows, arg.Now, arg.UpdatedBefore)
	var i FinalizeStaleChatDebugRowsRow
	err := row.Scan(&i.RunsFinalized, &i.StepsFinalized)
	return i, err
}

const getChatDebugRunByID = `-- name: GetChatDebugRunByID :one
SELECT id, chat_id, root_chat_id, parent_chat_id, model_config_id, trigger_message_id, history_tip_message_id, kind, status, provider, model, summary, started_at, updated_at, finished_at
FROM chat_debug_runs
WHERE id = $1::uuid
`

func (q *sqlQuerier) GetChatDebugRunByID(ctx context.Context, id uuid.UUID) (ChatDebugRun, error) {
	row := q.db.QueryRowContext(ctx, getChatDebugRunByID, id)
	var i ChatDebugRun
	err := row.Scan(
		&i.ID,
		&i.ChatID,
		&i.RootChatID,
		&i.ParentChatID,
		&i.ModelConfigID,
		&i.TriggerMessageID,
		&i.HistoryTipMessageID,
		&i.Kind,
		&i.Status,
		&i.Provider,
		&i.Model,
		&i.Summary,
		&i.StartedAt,
		&i.UpdatedAt,
		&i.FinishedAt,
	)
	return i, err
}

const getChatDebugRunsByChatID = `-- name: GetChatDebugRunsByChatID :many
SELECT id, chat_id, root_chat_id, parent_chat_id, model_config_id, trigger_message_id, history_tip_message_id, kind, status, provider, model, summary, started_at, updated_at, finished_at
FROM chat_debug_runs
WHERE chat_id = $1::uuid
ORDER BY started_at DESC, id DESC
LIMIT $2::int
`

type GetChatDebugRunsByChatIDParams struct {
	ChatID   uuid.UUID `db:"chat_id" json:"chat_id"`
	LimitVal int32     `db:"limit_val" json:"limit_val"`
}

// Returns the most recent debug runs for a chat, ordered newest-first.
// Callers must supply an explicit limit to avoid unbounded result sets.
func (q *sqlQuerier) GetChatDebugRunsByChatID(ctx context.Context, arg GetChatDebugRunsByChatIDParams) ([]ChatDebugRun, error) {
	rows, err := q.db.QueryContext(ctx, getChatDebugRunsByChatID, arg.ChatID, arg.LimitVal)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatDebugRun
	for rows.Next() {
		var i ChatDebugRun
		if err := rows.Scan(
			&i.ID,
			&i.ChatID,
			&i.RootChatID,
			&i.ParentChatID,
			&i.ModelConfigID,
			&i.TriggerMessageID,
			&i.HistoryTipMessageID,
			&i.Kind,
			&i.Status,
			&i.Provider,
			&i.Model,
			&i.Summary,
			&i.StartedAt,
			&i.UpdatedAt,
			&i.FinishedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatDebugStepsByRunID = `-- name: GetChatDebugStepsByRunID :many
SELECT id, run_id, chat_id, step_number, operation, status, history_tip_message_id, assistant_message_id, normalized_request, normalized_response, usage, attempts, error, metadata, started_at, updated_at, finished_at
FROM chat_debug_steps
WHERE run_id = $1::uuid
ORDER BY step_number ASC, started_at ASC
`

func (q *sqlQuerier) GetChatDebugStepsByRunID(ctx context.Context, runID uuid.UUID) ([]ChatDebugStep, error) {
	rows, err := q.db.QueryContext(ctx, getChatDebugStepsByRunID, runID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatDebugStep
	for rows.Next() {
		var i ChatDebugStep
		if err := rows.Scan(
			&i.ID,
			&i.RunID,
			&i.ChatID,
			&i.StepNumber,
			&i.Operation,
			&i.Status,
			&i.HistoryTipMessageID,
			&i.AssistantMessageID,
			&i.NormalizedRequest,
			&i.NormalizedResponse,
			&i.Usage,
			&i.Attempts,
			&i.Error,
			&i.Metadata,
			&i.StartedAt,
			&i.UpdatedAt,
			&i.FinishedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertChatDebugRun = `-- name: InsertChatDebugRun :one
INSERT INTO chat_debug_runs (
    chat_id,
    root_chat_id,
    parent_chat_id,
    model_config_id,
    trigger_message_id,
    history_tip_message_id,
    kind,
    status,
    provider,
    model,
    summary,
    started_at,
    updated_at,
    finished_at
)
VALUES (
    $1::uuid,
    $2::uuid,
    $3::uuid,
    $4::uuid,
    $5::bigint,
    $6::bigint,
    $7::text,
    $8::text,
    $9::text,
    $10::text,
    COALESCE($11::jsonb, '{}'::jsonb),
    COALESCE($12::timestamptz, NOW()),
    COALESCE($13::timestamptz, NOW()),
    $14::timestamptz
)
RETURNING id, chat_id, root_chat_id, parent_chat_id, model_config_id, trigger_message_id, history_tip_message_id, kind, status, provider, model, summary, started_at, updated_at, finished_at
`

type InsertChatDebugRunParams struct {
	ChatID              uuid.UUID             `db:"chat_id" json:"chat_id"`
	RootChatID          uuid.NullUUID         `db:"root_chat_id" json:"root_chat_id"`
	ParentChatID        uuid.NullUUID         `db:"parent_chat_id" json:"parent_chat_id"`
	ModelConfigID       uuid.NullUUID         `db:"model_config_id" json:"model_config_id"`
	TriggerMessageID    sql.NullInt64         `db:"trigger_message_id" json:"trigger_message_id"`
	HistoryTipMessageID sql.NullInt64         `db:"history_tip_message_id" json:"history_tip_message_id"`
	Kind                string                `db:"kind" json:"kind"`
	Status              string                `db:"status" json:"status"`
	Provider            sql.NullString        `db:"provider" json:"provider"`
	Model               sql.NullString        `db:"model" json:"model"`
	Summary             pqtype.NullRawMessage `db:"summary" json:"summary"`
	StartedAt           sql.NullTime          `db:"started_at" json:"started_at"`
	UpdatedAt           sql.NullTime          `db:"updated_at" json:"updated_at"`
	FinishedAt          sql.NullTime          `db:"finished_at" json:"finished_at"`
}

// updated_at is the retention clock used by DeleteOldChatDebugRuns.
// Set it on every write to keep retention semantics correct.
func (q *sqlQuerier) InsertChatDebugRun(ctx context.Context, arg InsertChatDebugRunParams) (ChatDebugRun, error) {
	row := q.db.QueryRowContext(ctx, insertChatDebugRun,
		arg.ChatID,
		arg.RootChatID,
		arg.ParentChatID,
		arg.ModelConfigID,
		arg.TriggerMessageID,
		arg.HistoryTipMessageID,
		arg.Kind,
		arg.Status,
		arg.Provider,
		arg.Model,
		arg.Summary,
		arg.StartedAt,
		arg.UpdatedAt,
		arg.FinishedAt,
	)
	var i ChatDebugRun
	err := row.Scan(
		&i.ID,
		&i.ChatID,
		&i.RootChatID,
		&i.ParentChatID,
		&i.ModelConfigID,
		&i.TriggerMessageID,
		&i.HistoryTipMessageID,
		&i.Kind,
		&i.Status,
		&i.Provider,
		&i.Model,
		&i.Summary,
		&i.StartedAt,
		&i.UpdatedAt,
		&i.FinishedAt,
	)
	return i, err
}

const insertChatDebugStep = `-- name: InsertChatDebugStep :one
WITH locked_run AS (
    UPDATE chat_debug_runs
    SET updated_at = COALESCE($14::timestamptz, NOW())
    WHERE id = $1::uuid
        AND chat_id = $16::uuid
        AND finished_at IS NULL
    RETURNING chat_id
)
INSERT INTO chat_debug_steps (
    run_id,
    chat_id,
    step_number,
    operation,
    status,
    history_tip_message_id,
    assistant_message_id,
    normalized_request,
    normalized_response,
    usage,
    attempts,
    error,
    metadata,
    started_at,
    updated_at,
    finished_at
)
SELECT
    $1::uuid,
    locked_run.chat_id,
    $2::int,
    $3::text,
    $4::text,
    $5::bigint,
    $6::bigint,
    COALESCE($7::jsonb, '{}'::jsonb),
    $8::jsonb,
    $9::jsonb,
    COALESCE($10::jsonb, '[]'::jsonb),
    $11::jsonb,
    COALESCE($12::jsonb, '{}'::jsonb),
    COALESCE($13::timestamptz, NOW()),
    COALESCE($14::timestamptz, NOW()),
    $15::timestamptz
FROM locked_run
RETURNING id, run_id, chat_id, step_number, operation, status, history_tip_message_id, assistant_message_id, normalized_request, normalized_response, usage, attempts, error, metadata, started_at, updated_at, finished_at
`

type InsertChatDebugStepParams struct {
	RunID               uuid.UUID             `db:"run_id" json:"run_id"`
	StepNumber          int32                 `db:"step_number" json:"step_number"`
	Operation           string                `db:"operation" json:"operation"`
	Status              string                `db:"status" json:"status"`
	HistoryTipMessageID sql.NullInt64         `db:"history_tip_message_id" json:"history_tip_message_id"`
	AssistantMessageID  sql.NullInt64         `db:"assistant_message_id" json:"assistant_message_id"`
	NormalizedRequest   pqtype.NullRawMessage `db:"normalized_request" json:"normalized_request"`
	NormalizedResponse  pqtype.NullRawMessage `db:"normalized_response" json:"normalized_response"`
	Usage               pqtype.NullRawMessage `db:"usage" json:"usage"`
	Attempts            pqtype.NullRawMessage `db:"attempts" json:"attempts"`
	Error               pqtype.NullRawMessage `db:"error" json:"error"`
	Metadata            pqtype.NullRawMessage `db:"metadata" json:"metadata"`
	StartedAt           sql.NullTime          `db:"started_at" json:"started_at"`
	UpdatedAt           sql.NullTime          `db:"updated_at" json:"updated_at"`
	FinishedAt          sql.NullTime          `db:"finished_at" json:"finished_at"`
	ChatID              uuid.UUID             `db:"chat_id" json:"chat_id"`
}

// The CTE atomically locks the parent run via UPDATE, bumps its
// updated_at (eliminating a separate TouchChatDebugRunUpdatedAt
// call), and enforces the finalization guard: if the run is already
// finished, the UPDATE returns zero rows, the INSERT gets no source
// rows, and sql.ErrNoRows is returned. The UPDATE also serializes
// with concurrent FinalizeStale under READ COMMITTED isolation.
func (q *sqlQuerier) InsertChatDebugStep(ctx context.Context, arg InsertChatDebugStepParams) (ChatDebugStep, error) {
	row := q.db.QueryRowContext(ctx, insertChatDebugStep,
		arg.RunID,
		arg.StepNumber,
		arg.Operation,
		arg.Status,
		arg.HistoryTipMessageID,
		arg.AssistantMessageID,
		arg.NormalizedRequest,
		arg.NormalizedResponse,
		arg.Usage,
		arg.Attempts,
		arg.Error,
		arg.Metadata,
		arg.StartedAt,
		arg.UpdatedAt,
		arg.FinishedAt,
		arg.ChatID,
	)
	var i ChatDebugStep
	err := row.Scan(
		&i.ID,
		&i.RunID,
		&i.ChatID,
		&i.StepNumber,
		&i.Operation,
		&i.Status,
		&i.HistoryTipMessageID,
		&i.AssistantMessageID,
		&i.NormalizedRequest,
		&i.NormalizedResponse,
		&i.Usage,
		&i.Attempts,
		&i.Error,
		&i.Metadata,
		&i.StartedAt,
		&i.UpdatedAt,
		&i.FinishedAt,
	)
	return i, err
}

const touchChatDebugRunUpdatedAt = `-- name: TouchChatDebugRunUpdatedAt :exec
UPDATE chat_debug_runs
SET updated_at = $1::timestamptz
WHERE id = $2::uuid
    AND chat_id = $3::uuid
`

type TouchChatDebugRunUpdatedAtParams struct {
	Now    time.Time `db:"now" json:"now"`
	ID     uuid.UUID `db:"id" json:"id"`
	ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}

// Overrides updated_at on the parent run without touching any
// other column. Used by tests that need to stamp a run with a
// specific timestamp after the InsertChatDebugStep CTE has
// already bumped it to NOW(), so stale-row finalization paths
// can be exercised deterministically. The chatdebug service
// itself does not call this: heartbeats go through
// TouchChatDebugStepAndRun, and step creation updates the parent
// run via the InsertChatDebugStep CTE.
func (q *sqlQuerier) TouchChatDebugRunUpdatedAt(ctx context.Context, arg TouchChatDebugRunUpdatedAtParams) error {
	_, err := q.db.ExecContext(ctx, touchChatDebugRunUpdatedAt, arg.Now, arg.ID, arg.ChatID)
	return err
}

const touchChatDebugStepAndRun = `-- name: TouchChatDebugStepAndRun :exec
WITH touched_run AS (
    UPDATE chat_debug_runs
    SET updated_at = $1::timestamptz
    WHERE id = $3::uuid
        AND chat_id = $4::uuid
    RETURNING id, chat_id
)
UPDATE chat_debug_steps
SET updated_at = $1::timestamptz
FROM touched_run
WHERE chat_debug_steps.id = $2::uuid
    AND chat_debug_steps.run_id = touched_run.id
    AND chat_debug_steps.chat_id = touched_run.chat_id
`

type TouchChatDebugStepAndRunParams struct {
	Now    time.Time `db:"now" json:"now"`
	StepID uuid.UUID `db:"step_id" json:"step_id"`
	RunID  uuid.UUID `db:"run_id" json:"run_id"`
	ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}

// Atomically bumps updated_at on both the step and its parent run
// in a single statement. This prevents FinalizeStale from
// interleaving between the two touches and finalizing a run whose
// step heartbeat was just written.
//
// The step UPDATE joins through touched_run (via FROM) and reads
// its RETURNING rows. Per the PostgreSQL WITH semantics, RETURNING
// is the only way to communicate values between a data-modifying
// CTE and the main query, and consuming those rows forces the run
// UPDATE to complete before the step UPDATE. That matches the
// lock order used by FinalizeStaleChatDebugRows and avoids a
// deadlock between concurrent heartbeats and stale sweeps. The
// join also constrains the step update to the specified run so a
// mismatched (run_id, step_id) pair cannot silently refresh an
// unrelated step.
func (q *sqlQuerier) TouchChatDebugStepAndRun(ctx context.Context, arg TouchChatDebugStepAndRunParams) error {
	_, err := q.db.ExecContext(ctx, touchChatDebugStepAndRun,
		arg.Now,
		arg.StepID,
		arg.RunID,
		arg.ChatID,
	)
	return err
}

const updateChatDebugRun = `-- name: UpdateChatDebugRun :one
UPDATE chat_debug_runs
SET
    root_chat_id = COALESCE($1::uuid, root_chat_id),
    parent_chat_id = COALESCE($2::uuid, parent_chat_id),
    model_config_id = COALESCE($3::uuid, model_config_id),
    trigger_message_id = COALESCE($4::bigint, trigger_message_id),
    history_tip_message_id = COALESCE($5::bigint, history_tip_message_id),
    status = COALESCE($6::text, status),
    provider = COALESCE($7::text, provider),
    model = COALESCE($8::text, model),
    summary = COALESCE($9::jsonb, summary),
    finished_at = COALESCE(finished_at, $10::timestamptz),
    updated_at = $11::timestamptz
WHERE id = $12::uuid
    AND chat_id = $13::uuid
RETURNING id, chat_id, root_chat_id, parent_chat_id, model_config_id, trigger_message_id, history_tip_message_id, kind, status, provider, model, summary, started_at, updated_at, finished_at
`

type UpdateChatDebugRunParams struct {
	RootChatID          uuid.NullUUID         `db:"root_chat_id" json:"root_chat_id"`
	ParentChatID        uuid.NullUUID         `db:"parent_chat_id" json:"parent_chat_id"`
	ModelConfigID       uuid.NullUUID         `db:"model_config_id" json:"model_config_id"`
	TriggerMessageID    sql.NullInt64         `db:"trigger_message_id" json:"trigger_message_id"`
	HistoryTipMessageID sql.NullInt64         `db:"history_tip_message_id" json:"history_tip_message_id"`
	Status              sql.NullString        `db:"status" json:"status"`
	Provider            sql.NullString        `db:"provider" json:"provider"`
	Model               sql.NullString        `db:"model" json:"model"`
	Summary             pqtype.NullRawMessage `db:"summary" json:"summary"`
	FinishedAt          sql.NullTime          `db:"finished_at" json:"finished_at"`
	Now                 time.Time             `db:"now" json:"now"`
	ID                  uuid.UUID             `db:"id" json:"id"`
	ChatID              uuid.UUID             `db:"chat_id" json:"chat_id"`
}

// Uses COALESCE so that passing NULL from Go means "keep the
// existing value." This is intentional: debug rows follow a
// write-once-finalize pattern where fields are set at creation
// or finalization and never cleared back to NULL. The @now
// parameter keeps updated_at under the caller's clock.
// updated_at is also the retention clock used by DeleteOldChatDebugRuns.
//
// finished_at is enforced as write-once at the SQL level: once
// populated it cannot be overwritten by a later call. Callers
// that issue a summary or status refresh after the run has
// already finalized therefore cannot corrupt the original
// completion timestamp, which keeps duration and ordering
// calculations stable regardless of how many times the row is
// updated.
func (q *sqlQuerier) UpdateChatDebugRun(ctx context.Context, arg UpdateChatDebugRunParams) (ChatDebugRun, error) {
	row := q.db.QueryRowContext(ctx, updateChatDebugRun,
		arg.RootChatID,
		arg.ParentChatID,
		arg.ModelConfigID,
		arg.TriggerMessageID,
		arg.HistoryTipMessageID,
		arg.Status,
		arg.Provider,
		arg.Model,
		arg.Summary,
		arg.FinishedAt,
		arg.Now,
		arg.ID,
		arg.ChatID,
	)
	var i ChatDebugRun
	err := row.Scan(
		&i.ID,
		&i.ChatID,
		&i.RootChatID,
		&i.ParentChatID,
		&i.ModelConfigID,
		&i.TriggerMessageID,
		&i.HistoryTipMessageID,
		&i.Kind,
		&i.Status,
		&i.Provider,
		&i.Model,
		&i.Summary,
		&i.StartedAt,
		&i.UpdatedAt,
		&i.FinishedAt,
	)
	return i, err
}

const updateChatDebugStep = `-- name: UpdateChatDebugStep :one
UPDATE chat_debug_steps
SET
    status = COALESCE($1::text, status),
    history_tip_message_id = COALESCE($2::bigint, history_tip_message_id),
    assistant_message_id = COALESCE($3::bigint, assistant_message_id),
    normalized_request = COALESCE($4::jsonb, normalized_request),
    normalized_response = COALESCE($5::jsonb, normalized_response),
    usage = COALESCE($6::jsonb, usage),
    attempts = COALESCE($7::jsonb, attempts),
    error = COALESCE($8::jsonb, error),
    metadata = COALESCE($9::jsonb, metadata),
    finished_at = COALESCE($10::timestamptz, finished_at),
    updated_at = $11::timestamptz
WHERE id = $12::uuid
    AND chat_id = $13::uuid
RETURNING id, run_id, chat_id, step_number, operation, status, history_tip_message_id, assistant_message_id, normalized_request, normalized_response, usage, attempts, error, metadata, started_at, updated_at, finished_at
`

type UpdateChatDebugStepParams struct {
	Status              sql.NullString        `db:"status" json:"status"`
	HistoryTipMessageID sql.NullInt64         `db:"history_tip_message_id" json:"history_tip_message_id"`
	AssistantMessageID  sql.NullInt64         `db:"assistant_message_id" json:"assistant_message_id"`
	NormalizedRequest   pqtype.NullRawMessage `db:"normalized_request" json:"normalized_request"`
	NormalizedResponse  pqtype.NullRawMessage `db:"normalized_response" json:"normalized_response"`
	Usage               pqtype.NullRawMessage `db:"usage" json:"usage"`
	Attempts            pqtype.NullRawMessage `db:"attempts" json:"attempts"`
	Error               pqtype.NullRawMessage `db:"error" json:"error"`
	Metadata            pqtype.NullRawMessage `db:"metadata" json:"metadata"`
	FinishedAt          sql.NullTime          `db:"finished_at" json:"finished_at"`
	Now                 time.Time             `db:"now" json:"now"`
	ID                  uuid.UUID             `db:"id" json:"id"`
	ChatID              uuid.UUID             `db:"chat_id" json:"chat_id"`
}

// Uses COALESCE so that passing NULL from Go means "keep the
// existing value." This is intentional: debug rows follow a
// write-once-finalize pattern where fields are set at creation
// or finalization and never cleared back to NULL. The @now
// parameter keeps updated_at under the caller's clock, matching
// the injectable quartz.Clock used by FinalizeStale sweeps.
func (q *sqlQuerier) UpdateChatDebugStep(ctx context.Context, arg UpdateChatDebugStepParams) (ChatDebugStep, error) {
	row := q.db.QueryRowContext(ctx, updateChatDebugStep,
		arg.Status,
		arg.HistoryTipMessageID,
		arg.AssistantMessageID,
		arg.NormalizedRequest,
		arg.NormalizedResponse,
		arg.Usage,
		arg.Attempts,
		arg.Error,
		arg.Metadata,
		arg.FinishedAt,
		arg.Now,
		arg.ID,
		arg.ChatID,
	)
	var i ChatDebugStep
	err := row.Scan(
		&i.ID,
		&i.RunID,
		&i.ChatID,
		&i.StepNumber,
		&i.Operation,
		&i.Status,
		&i.HistoryTipMessageID,
		&i.AssistantMessageID,
		&i.NormalizedRequest,
		&i.NormalizedResponse,
		&i.Usage,
		&i.Attempts,
		&i.Error,
		&i.Metadata,
		&i.StartedAt,
		&i.UpdatedAt,
		&i.FinishedAt,
	)
	return i, err
}

const deleteOldChatFiles = `-- name: DeleteOldChatFiles :execrows
WITH kept_file_ids AS (
    -- NOTE: This uses updated_at as a proxy for archive time
    -- because there is no archived_at column. Correctness
    -- requires that updated_at is never backdated on archived
    -- chats. See ArchiveChatByID.
    SELECT DISTINCT cfl.file_id
    FROM chat_file_links cfl
    JOIN chats c ON c.id = cfl.chat_id
    WHERE c.archived = false
       OR c.updated_at >= $1::timestamptz
),
deletable AS (
    SELECT cf.id
    FROM chat_files cf
    LEFT JOIN kept_file_ids k ON cf.id = k.file_id
    WHERE cf.created_at < $1::timestamptz
      AND k.file_id IS NULL
    ORDER BY cf.created_at ASC
    LIMIT $2
)
DELETE FROM chat_files
USING deletable
WHERE chat_files.id = deletable.id
`

type DeleteOldChatFilesParams struct {
	BeforeTime time.Time `db:"before_time" json:"before_time"`
	LimitCount int32     `db:"limit_count" json:"limit_count"`
}

// TODO(cian): Add indexes on chats(archived, updated_at) and
// chat_files(created_at) for purge query performance.
// See: https://github.com/coder/internal/issues/1438
// Deletes chat files that are older than the given threshold and are
// not referenced by any chat that is still active or was archived
// within the same threshold window. This covers two cases:
//  1. Orphaned files not linked to any chat.
//  2. Files whose every referencing chat has been archived for longer
//     than the retention period.
func (q *sqlQuerier) DeleteOldChatFiles(ctx context.Context, arg DeleteOldChatFilesParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteOldChatFiles, arg.BeforeTime, arg.LimitCount)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const getChatFileByID = `-- name: GetChatFileByID :one
SELECT id, owner_id, organization_id, created_at, name, mimetype, data FROM chat_files WHERE id = $1::uuid
`

func (q *sqlQuerier) GetChatFileByID(ctx context.Context, id uuid.UUID) (ChatFile, error) {
	row := q.db.QueryRowContext(ctx, getChatFileByID, id)
	var i ChatFile
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.OrganizationID,
		&i.CreatedAt,
		&i.Name,
		&i.Mimetype,
		&i.Data,
	)
	return i, err
}

const getChatFileMetadataByChatID = `-- name: GetChatFileMetadataByChatID :many
SELECT cf.id, cf.owner_id, cf.organization_id, cf.name, cf.mimetype, cf.created_at
FROM chat_files cf
JOIN chat_file_links cfl ON cfl.file_id = cf.id
WHERE cfl.chat_id = $1::uuid
ORDER BY cf.created_at ASC
`

type GetChatFileMetadataByChatIDRow struct {
	ID             uuid.UUID `db:"id" json:"id"`
	OwnerID        uuid.UUID `db:"owner_id" json:"owner_id"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	Name           string    `db:"name" json:"name"`
	Mimetype       string    `db:"mimetype" json:"mimetype"`
	CreatedAt      time.Time `db:"created_at" json:"created_at"`
}

// GetChatFileMetadataByChatID returns lightweight file metadata for
// all files linked to a chat. The data column is excluded to avoid
// loading file content.
func (q *sqlQuerier) GetChatFileMetadataByChatID(ctx context.Context, chatID uuid.UUID) ([]GetChatFileMetadataByChatIDRow, error) {
	rows, err := q.db.QueryContext(ctx, getChatFileMetadataByChatID, chatID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatFileMetadataByChatIDRow
	for rows.Next() {
		var i GetChatFileMetadataByChatIDRow
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.OrganizationID,
			&i.Name,
			&i.Mimetype,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatFilesByIDs = `-- name: GetChatFilesByIDs :many
SELECT id, owner_id, organization_id, created_at, name, mimetype, data FROM chat_files WHERE id = ANY($1::uuid[])
`

func (q *sqlQuerier) GetChatFilesByIDs(ctx context.Context, ids []uuid.UUID) ([]ChatFile, error) {
	rows, err := q.db.QueryContext(ctx, getChatFilesByIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatFile
	for rows.Next() {
		var i ChatFile
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.OrganizationID,
			&i.CreatedAt,
			&i.Name,
			&i.Mimetype,
			&i.Data,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertChatFile = `-- name: InsertChatFile :one
INSERT INTO chat_files (owner_id, organization_id, name, mimetype, data)
VALUES ($1::uuid, $2::uuid, $3::text, $4::text, $5::bytea)
RETURNING id, owner_id, organization_id, created_at, name, mimetype
`

type InsertChatFileParams struct {
	OwnerID        uuid.UUID `db:"owner_id" json:"owner_id"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	Name           string    `db:"name" json:"name"`
	Mimetype       string    `db:"mimetype" json:"mimetype"`
	Data           []byte    `db:"data" json:"data"`
}

type InsertChatFileRow struct {
	ID             uuid.UUID `db:"id" json:"id"`
	OwnerID        uuid.UUID `db:"owner_id" json:"owner_id"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	CreatedAt      time.Time `db:"created_at" json:"created_at"`
	Name           string    `db:"name" json:"name"`
	Mimetype       string    `db:"mimetype" json:"mimetype"`
}

func (q *sqlQuerier) InsertChatFile(ctx context.Context, arg InsertChatFileParams) (InsertChatFileRow, error) {
	row := q.db.QueryRowContext(ctx, insertChatFile,
		arg.OwnerID,
		arg.OrganizationID,
		arg.Name,
		arg.Mimetype,
		arg.Data,
	)
	var i InsertChatFileRow
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.OrganizationID,
		&i.CreatedAt,
		&i.Name,
		&i.Mimetype,
	)
	return i, err
}

const getPRInsightsPerModel = `-- name: GetPRInsightsPerModel :many
WITH pr_costs AS (
    SELECT
        prc.pr_key,
        COALESCE(SUM(cc.cost_micros), 0) AS cost_micros
    FROM (
        SELECT DISTINCT
            COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key,
            related.id AS chat_id
        FROM chat_diff_statuses cds
        JOIN chats c ON c.id = cds.chat_id
        JOIN chats related
          ON related.id = c.id
          OR (related.parent_chat_id = c.id
              AND NOT EXISTS (
                  SELECT 1 FROM chat_diff_statuses cds2
                  WHERE cds2.chat_id = related.id
                    AND cds2.pull_request_state IS NOT NULL
              ))
        WHERE cds.pull_request_state IS NOT NULL
          AND c.created_at >= $1::timestamptz
          AND c.created_at < $2::timestamptz
          AND ($3::uuid IS NULL OR c.owner_id = $3::uuid)
    ) prc
    LEFT JOIN LATERAL (
        SELECT COALESCE(SUM(cm.total_cost_micros), 0) AS cost_micros
        FROM chat_messages cm
        WHERE cm.chat_id = prc.chat_id
          AND cm.total_cost_micros IS NOT NULL
    ) cc ON TRUE
    GROUP BY prc.pr_key
),
deduped AS (
    SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text))
        COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key,
        cds.pull_request_state,
        cds.additions,
        cds.deletions,
        cmc.id AS model_config_id,
        cmc.display_name,
        cmc.model,
        cmc.provider
    FROM chat_diff_statuses cds
    JOIN chats c ON c.id = cds.chat_id
    LEFT JOIN chat_model_configs cmc ON cmc.id = c.last_model_config_id
    WHERE cds.pull_request_state IS NOT NULL
      AND c.created_at >= $1::timestamptz
      AND c.created_at < $2::timestamptz
      AND ($3::uuid IS NULL OR c.owner_id = $3::uuid)
    ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), c.created_at DESC, c.id DESC
)
SELECT
    d.model_config_id,
    COALESCE(NULLIF(d.display_name, ''), NULLIF(d.model, ''), 'Unknown')::text AS display_name,
    COALESCE(d.provider, 'unknown')::text AS provider,
    COUNT(*)::bigint AS total_prs,
    COUNT(*) FILTER (WHERE d.pull_request_state = 'merged')::bigint AS merged_prs,
    COALESCE(SUM(d.additions), 0)::bigint AS total_additions,
    COALESCE(SUM(d.deletions), 0)::bigint AS total_deletions,
    COALESCE(SUM(pc.cost_micros), 0)::bigint AS total_cost_micros,
    COALESCE(SUM(pc.cost_micros) FILTER (WHERE d.pull_request_state = 'merged'), 0)::bigint AS merged_cost_micros
FROM deduped d
JOIN pr_costs pc ON pc.pr_key = d.pr_key
GROUP BY d.model_config_id, d.display_name, d.model, d.provider
ORDER BY total_prs DESC
`

type GetPRInsightsPerModelParams struct {
	StartDate time.Time     `db:"start_date" json:"start_date"`
	EndDate   time.Time     `db:"end_date" json:"end_date"`
	OwnerID   uuid.NullUUID `db:"owner_id" json:"owner_id"`
}

type GetPRInsightsPerModelRow struct {
	ModelConfigID    uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
	DisplayName      string        `db:"display_name" json:"display_name"`
	Provider         string        `db:"provider" json:"provider"`
	TotalPrs         int64         `db:"total_prs" json:"total_prs"`
	MergedPrs        int64         `db:"merged_prs" json:"merged_prs"`
	TotalAdditions   int64         `db:"total_additions" json:"total_additions"`
	TotalDeletions   int64         `db:"total_deletions" json:"total_deletions"`
	TotalCostMicros  int64         `db:"total_cost_micros" json:"total_cost_micros"`
	MergedCostMicros int64         `db:"merged_cost_micros" json:"merged_cost_micros"`
}

// Returns PR metrics grouped by the model used for each chat.
// Uses two CTEs: pr_costs sums cost for the PR-linked chat and its
// direct children (that lack their own PR), and deduped picks one row
// per PR for state/additions/deletions/model (model comes from the
// most recent chat).
func (q *sqlQuerier) GetPRInsightsPerModel(ctx context.Context, arg GetPRInsightsPerModelParams) ([]GetPRInsightsPerModelRow, error) {
	rows, err := q.db.QueryContext(ctx, getPRInsightsPerModel, arg.StartDate, arg.EndDate, arg.OwnerID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetPRInsightsPerModelRow
	for rows.Next() {
		var i GetPRInsightsPerModelRow
		if err := rows.Scan(
			&i.ModelConfigID,
			&i.DisplayName,
			&i.Provider,
			&i.TotalPrs,
			&i.MergedPrs,
			&i.TotalAdditions,
			&i.TotalDeletions,
			&i.TotalCostMicros,
			&i.MergedCostMicros,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPRInsightsPullRequests = `-- name: GetPRInsightsPullRequests :many
WITH pr_costs AS (
    SELECT
        prc.pr_key,
        COALESCE(SUM(cc.cost_micros), 0) AS cost_micros
    FROM (
        SELECT DISTINCT
            COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key,
            related.id AS chat_id
        FROM chat_diff_statuses cds
        JOIN chats c ON c.id = cds.chat_id
        JOIN chats related
          ON related.id = c.id
          OR (related.parent_chat_id = c.id
              AND NOT EXISTS (
                  SELECT 1 FROM chat_diff_statuses cds2
                  WHERE cds2.chat_id = related.id
                    AND cds2.pull_request_state IS NOT NULL
              ))
        WHERE cds.pull_request_state IS NOT NULL
          AND c.created_at >= $1::timestamptz
          AND c.created_at < $2::timestamptz
          AND ($3::uuid IS NULL OR c.owner_id = $3::uuid)
    ) prc
    LEFT JOIN LATERAL (
        SELECT COALESCE(SUM(cm.total_cost_micros), 0) AS cost_micros
        FROM chat_messages cm
        WHERE cm.chat_id = prc.chat_id
          AND cm.total_cost_micros IS NOT NULL
    ) cc ON TRUE
    GROUP BY prc.pr_key
),
deduped AS (
    SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text))
        COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key,
        c.id AS chat_id,
        cds.pull_request_title AS pr_title,
        cds.url AS pr_url,
        cds.pr_number,
        cds.pull_request_state AS state,
        cds.pull_request_draft AS draft,
        cds.additions,
        cds.deletions,
        cds.changed_files,
        cds.commits,
        cds.approved,
        cds.changes_requested,
        cds.reviewer_count,
        cds.author_login,
        cds.author_avatar_url,
        COALESCE(cds.base_branch, '')::text AS base_branch,
        COALESCE(NULLIF(cmc.display_name, ''), NULLIF(cmc.model, ''), 'Unknown')::text AS model_display_name,
        c.created_at
    FROM chat_diff_statuses cds
    JOIN chats c ON c.id = cds.chat_id
    LEFT JOIN chat_model_configs cmc ON cmc.id = c.last_model_config_id
    WHERE cds.pull_request_state IS NOT NULL
      AND c.created_at >= $1::timestamptz
      AND c.created_at < $2::timestamptz
      AND ($3::uuid IS NULL OR c.owner_id = $3::uuid)
    ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), c.created_at DESC, c.id DESC
)
SELECT chat_id, pr_title, pr_url, pr_number, state, draft, additions, deletions, changed_files, commits, approved, changes_requested, reviewer_count, author_login, author_avatar_url, base_branch, model_display_name, cost_micros, created_at FROM (
    SELECT
        d.chat_id,
        d.pr_title,
        d.pr_url,
        d.pr_number,
        d.state,
        d.draft,
        d.additions,
        d.deletions,
        d.changed_files,
        d.commits,
        d.approved,
        d.changes_requested,
        d.reviewer_count,
        d.author_login,
        d.author_avatar_url,
        d.base_branch,
        d.model_display_name,
        COALESCE(pc.cost_micros, 0)::bigint AS cost_micros,
        d.created_at
    FROM deduped d
    JOIN pr_costs pc ON pc.pr_key = d.pr_key
) sub
ORDER BY sub.created_at DESC
LIMIT 500
`

type GetPRInsightsPullRequestsParams struct {
	StartDate time.Time     `db:"start_date" json:"start_date"`
	EndDate   time.Time     `db:"end_date" json:"end_date"`
	OwnerID   uuid.NullUUID `db:"owner_id" json:"owner_id"`
}

type GetPRInsightsPullRequestsRow struct {
	ChatID           uuid.UUID      `db:"chat_id" json:"chat_id"`
	PrTitle          string         `db:"pr_title" json:"pr_title"`
	PrUrl            sql.NullString `db:"pr_url" json:"pr_url"`
	PrNumber         sql.NullInt32  `db:"pr_number" json:"pr_number"`
	State            sql.NullString `db:"state" json:"state"`
	Draft            bool           `db:"draft" json:"draft"`
	Additions        int32          `db:"additions" json:"additions"`
	Deletions        int32          `db:"deletions" json:"deletions"`
	ChangedFiles     int32          `db:"changed_files" json:"changed_files"`
	Commits          sql.NullInt32  `db:"commits" json:"commits"`
	Approved         sql.NullBool   `db:"approved" json:"approved"`
	ChangesRequested bool           `db:"changes_requested" json:"changes_requested"`
	ReviewerCount    sql.NullInt32  `db:"reviewer_count" json:"reviewer_count"`
	AuthorLogin      sql.NullString `db:"author_login" json:"author_login"`
	AuthorAvatarUrl  sql.NullString `db:"author_avatar_url" json:"author_avatar_url"`
	BaseBranch       string         `db:"base_branch" json:"base_branch"`
	ModelDisplayName string         `db:"model_display_name" json:"model_display_name"`
	CostMicros       int64          `db:"cost_micros" json:"cost_micros"`
	CreatedAt        time.Time      `db:"created_at" json:"created_at"`
}

// Returns all individual PR rows with cost for the selected time range.
// Uses two CTEs: pr_costs sums cost for the PR-linked chat and its
// direct children (that lack their own PR), and deduped picks one row
// per PR for metadata. A safety-cap LIMIT guards against unexpectedly
// large result sets from direct API callers.
func (q *sqlQuerier) GetPRInsightsPullRequests(ctx context.Context, arg GetPRInsightsPullRequestsParams) ([]GetPRInsightsPullRequestsRow, error) {
	rows, err := q.db.QueryContext(ctx, getPRInsightsPullRequests, arg.StartDate, arg.EndDate, arg.OwnerID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetPRInsightsPullRequestsRow
	for rows.Next() {
		var i GetPRInsightsPullRequestsRow
		if err := rows.Scan(
			&i.ChatID,
			&i.PrTitle,
			&i.PrUrl,
			&i.PrNumber,
			&i.State,
			&i.Draft,
			&i.Additions,
			&i.Deletions,
			&i.ChangedFiles,
			&i.Commits,
			&i.Approved,
			&i.ChangesRequested,
			&i.ReviewerCount,
			&i.AuthorLogin,
			&i.AuthorAvatarUrl,
			&i.BaseBranch,
			&i.ModelDisplayName,
			&i.CostMicros,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPRInsightsSummary = `-- name: GetPRInsightsSummary :one

WITH pr_costs AS (
    SELECT
        prc.pr_key,
        COALESCE(SUM(cc.cost_micros), 0) AS cost_micros
    FROM (
        -- For each PR, include the chat that references it plus any
        -- direct children (subagents) that do not have their own PR.
        SELECT DISTINCT
            COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key,
            related.id AS chat_id
        FROM chat_diff_statuses cds
        JOIN chats c ON c.id = cds.chat_id
        JOIN chats related
          ON related.id = c.id
          OR (related.parent_chat_id = c.id
              AND NOT EXISTS (
                  SELECT 1 FROM chat_diff_statuses cds2
                  WHERE cds2.chat_id = related.id
                    AND cds2.pull_request_state IS NOT NULL
              ))
        WHERE cds.pull_request_state IS NOT NULL
          AND c.created_at >= $1::timestamptz
          AND c.created_at < $2::timestamptz
          AND ($3::uuid IS NULL OR c.owner_id = $3::uuid)
    ) prc
    LEFT JOIN LATERAL (
        SELECT COALESCE(SUM(cm.total_cost_micros), 0) AS cost_micros
        FROM chat_messages cm
        WHERE cm.chat_id = prc.chat_id
          AND cm.total_cost_micros IS NOT NULL
    ) cc ON TRUE
    GROUP BY prc.pr_key
),
deduped AS (
    SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text))
        COALESCE(NULLIF(cds.url, ''), c.id::text) AS pr_key,
        cds.pull_request_state,
        cds.additions,
        cds.deletions
    FROM chat_diff_statuses cds
    JOIN chats c ON c.id = cds.chat_id
    WHERE cds.pull_request_state IS NOT NULL
      AND c.created_at >= $1::timestamptz
      AND c.created_at < $2::timestamptz
      AND ($3::uuid IS NULL OR c.owner_id = $3::uuid)
    ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), c.created_at DESC, c.id DESC
)
SELECT
    COUNT(*)::bigint AS total_prs_created,
    COUNT(*) FILTER (WHERE d.pull_request_state = 'merged')::bigint AS total_prs_merged,
    COUNT(*) FILTER (WHERE d.pull_request_state = 'closed')::bigint AS total_prs_closed,
    COALESCE(SUM(d.additions), 0)::bigint AS total_additions,
    COALESCE(SUM(d.deletions), 0)::bigint AS total_deletions,
    COALESCE(SUM(pc.cost_micros), 0)::bigint AS total_cost_micros,
    COALESCE(SUM(pc.cost_micros) FILTER (WHERE d.pull_request_state = 'merged'), 0)::bigint AS merged_cost_micros
FROM deduped d
JOIN pr_costs pc ON pc.pr_key = d.pr_key
`

type GetPRInsightsSummaryParams struct {
	StartDate time.Time     `db:"start_date" json:"start_date"`
	EndDate   time.Time     `db:"end_date" json:"end_date"`
	OwnerID   uuid.NullUUID `db:"owner_id" json:"owner_id"`
}

type GetPRInsightsSummaryRow struct {
	TotalPrsCreated  int64 `db:"total_prs_created" json:"total_prs_created"`
	TotalPrsMerged   int64 `db:"total_prs_merged" json:"total_prs_merged"`
	TotalPrsClosed   int64 `db:"total_prs_closed" json:"total_prs_closed"`
	TotalAdditions   int64 `db:"total_additions" json:"total_additions"`
	TotalDeletions   int64 `db:"total_deletions" json:"total_deletions"`
	TotalCostMicros  int64 `db:"total_cost_micros" json:"total_cost_micros"`
	MergedCostMicros int64 `db:"merged_cost_micros" json:"merged_cost_micros"`
}

// PR Insights queries for the /agents analytics dashboard.
// These aggregate data from chat_diff_statuses (PR metadata) joined
// with chats and chat_messages (cost) to power the PR Insights view.
//
// Cost is computed per PR by summing the PR-linked chat's own cost plus
// the costs of any direct children (subagents) it spawned that do NOT
// have their own PR association. If a child chat has its own
// chat_diff_statuses entry (with a non-NULL pull_request_state), its
// cost is attributed to that child's PR instead — preventing
// double-counting when sibling chats create different PRs.
// Subagent trees are at most 2 levels deep (enforced by the
// application layer). PR metadata (state, additions, deletions)
// comes from the most recent chat via DISTINCT ON so that each PR
// is counted exactly once.
// Returns aggregate PR metrics for the given date range.
// The handler calls this twice (current + previous period) for trends.
// Uses two CTEs: pr_costs sums cost for the PR-linked chat and its
// direct children (that lack their own PR), and deduped picks one row
// per PR for state/additions/deletions.
func (q *sqlQuerier) GetPRInsightsSummary(ctx context.Context, arg GetPRInsightsSummaryParams) (GetPRInsightsSummaryRow, error) {
	row := q.db.QueryRowContext(ctx, getPRInsightsSummary, arg.StartDate, arg.EndDate, arg.OwnerID)
	var i GetPRInsightsSummaryRow
	err := row.Scan(
		&i.TotalPrsCreated,
		&i.TotalPrsMerged,
		&i.TotalPrsClosed,
		&i.TotalAdditions,
		&i.TotalDeletions,
		&i.TotalCostMicros,
		&i.MergedCostMicros,
	)
	return i, err
}

const getPRInsightsTimeSeries = `-- name: GetPRInsightsTimeSeries :many
WITH deduped AS (
    SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text))
        cds.pull_request_state,
        c.created_at
    FROM chat_diff_statuses cds
    JOIN chats c ON c.id = cds.chat_id
    WHERE cds.pull_request_state IS NOT NULL
      AND c.created_at >= $1::timestamptz
      AND c.created_at < $2::timestamptz
      AND ($3::uuid IS NULL OR c.owner_id = $3::uuid)
    ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), c.created_at DESC, c.id DESC
)
SELECT
    date_trunc('day', created_at)::timestamptz AS date,
    COUNT(*)::bigint AS prs_created,
    COUNT(*) FILTER (WHERE pull_request_state = 'merged')::bigint AS prs_merged,
    COUNT(*) FILTER (WHERE pull_request_state = 'closed')::bigint AS prs_closed
FROM deduped
GROUP BY date_trunc('day', created_at)
ORDER BY date_trunc('day', created_at)
`

type GetPRInsightsTimeSeriesParams struct {
	StartDate time.Time     `db:"start_date" json:"start_date"`
	EndDate   time.Time     `db:"end_date" json:"end_date"`
	OwnerID   uuid.NullUUID `db:"owner_id" json:"owner_id"`
}

type GetPRInsightsTimeSeriesRow struct {
	Date       time.Time `db:"date" json:"date"`
	PrsCreated int64     `db:"prs_created" json:"prs_created"`
	PrsMerged  int64     `db:"prs_merged" json:"prs_merged"`
	PrsClosed  int64     `db:"prs_closed" json:"prs_closed"`
}

// Returns daily PR counts grouped by state for the chart.
// Uses a CTE to deduplicate by PR URL so that multiple chats referencing
// the same pull request are only counted once (keeping the most recent chat).
func (q *sqlQuerier) GetPRInsightsTimeSeries(ctx context.Context, arg GetPRInsightsTimeSeriesParams) ([]GetPRInsightsTimeSeriesRow, error) {
	rows, err := q.db.QueryContext(ctx, getPRInsightsTimeSeries, arg.StartDate, arg.EndDate, arg.OwnerID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetPRInsightsTimeSeriesRow
	for rows.Next() {
		var i GetPRInsightsTimeSeriesRow
		if err := rows.Scan(
			&i.Date,
			&i.PrsCreated,
			&i.PrsMerged,
			&i.PrsClosed,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteChatModelConfigByID = `-- name: DeleteChatModelConfigByID :exec
UPDATE
    chat_model_configs
SET
    deleted = TRUE,
    deleted_at = NOW(),
    updated_at = NOW()
WHERE
    id = $1::uuid
`

func (q *sqlQuerier) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteChatModelConfigByID, id)
	return err
}

const deleteChatModelConfigsByAIProviderID = `-- name: DeleteChatModelConfigsByAIProviderID :exec
UPDATE
    chat_model_configs
SET
    deleted = TRUE,
    deleted_at = NOW(),
    updated_at = NOW()
WHERE
    ai_provider_id = $1::uuid
    AND deleted = FALSE
`

func (q *sqlQuerier) DeleteChatModelConfigsByAIProviderID(ctx context.Context, aiProviderID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteChatModelConfigsByAIProviderID, aiProviderID)
	return err
}

const deleteChatModelConfigsByProvider = `-- name: DeleteChatModelConfigsByProvider :exec
UPDATE
    chat_model_configs
SET
    deleted = TRUE,
    deleted_at = NOW(),
    updated_at = NOW()
WHERE
    provider = $1::text
    AND deleted = FALSE
`

func (q *sqlQuerier) DeleteChatModelConfigsByProvider(ctx context.Context, provider string) error {
	_, err := q.db.ExecContext(ctx, deleteChatModelConfigsByProvider, provider)
	return err
}

const getChatModelConfigByID = `-- name: GetChatModelConfigByID :one
SELECT
    id, provider, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
FROM
    chat_model_configs
WHERE
    id = $1::uuid
    AND deleted = FALSE
`

func (q *sqlQuerier) GetChatModelConfigByID(ctx context.Context, id uuid.UUID) (ChatModelConfig, error) {
	row := q.db.QueryRowContext(ctx, getChatModelConfigByID, id)
	var i ChatModelConfig
	err := row.Scan(
		&i.ID,
		&i.Provider,
		&i.Model,
		&i.DisplayName,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.Enabled,
		&i.IsDefault,
		&i.Deleted,
		&i.DeletedAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ContextLimit,
		&i.CompressionThreshold,
		&i.Options,
		&i.AIProviderID,
	)
	return i, err
}

const getChatModelConfigs = `-- name: GetChatModelConfigs :many
SELECT
    id, provider, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
FROM
    chat_model_configs
WHERE
    deleted = FALSE
ORDER BY
    provider ASC,
    model ASC,
    updated_at DESC,
    id DESC
`

func (q *sqlQuerier) GetChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error) {
	rows, err := q.db.QueryContext(ctx, getChatModelConfigs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatModelConfig
	for rows.Next() {
		var i ChatModelConfig
		if err := rows.Scan(
			&i.ID,
			&i.Provider,
			&i.Model,
			&i.DisplayName,
			&i.CreatedBy,
			&i.UpdatedBy,
			&i.Enabled,
			&i.IsDefault,
			&i.Deleted,
			&i.DeletedAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ContextLimit,
			&i.CompressionThreshold,
			&i.Options,
			&i.AIProviderID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getDefaultChatModelConfig = `-- name: GetDefaultChatModelConfig :one
SELECT
    id, provider, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
FROM
    chat_model_configs
WHERE
    is_default = TRUE
    AND deleted = FALSE
`

func (q *sqlQuerier) GetDefaultChatModelConfig(ctx context.Context) (ChatModelConfig, error) {
	row := q.db.QueryRowContext(ctx, getDefaultChatModelConfig)
	var i ChatModelConfig
	err := row.Scan(
		&i.ID,
		&i.Provider,
		&i.Model,
		&i.DisplayName,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.Enabled,
		&i.IsDefault,
		&i.Deleted,
		&i.DeletedAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ContextLimit,
		&i.CompressionThreshold,
		&i.Options,
		&i.AIProviderID,
	)
	return i, err
}

const getEnabledChatModelConfigByID = `-- name: GetEnabledChatModelConfigByID :one
SELECT
    cmc.id, cmc.provider, cmc.model, cmc.display_name, cmc.created_by, cmc.updated_by, cmc.enabled, cmc.is_default, cmc.deleted, cmc.deleted_at, cmc.created_at, cmc.updated_at, cmc.context_limit, cmc.compression_threshold, cmc.options, cmc.ai_provider_id
FROM
    chat_model_configs cmc
JOIN
    ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE
    cmc.id = $1::uuid
    AND cmc.deleted = FALSE
    AND cmc.enabled = TRUE
    AND ap.enabled = TRUE
    AND ap.deleted = FALSE
`

// Providers can be disabled independently of their model configs.
// Check both to ensure the selected config is actually usable.
func (q *sqlQuerier) GetEnabledChatModelConfigByID(ctx context.Context, id uuid.UUID) (ChatModelConfig, error) {
	row := q.db.QueryRowContext(ctx, getEnabledChatModelConfigByID, id)
	var i ChatModelConfig
	err := row.Scan(
		&i.ID,
		&i.Provider,
		&i.Model,
		&i.DisplayName,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.Enabled,
		&i.IsDefault,
		&i.Deleted,
		&i.DeletedAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ContextLimit,
		&i.CompressionThreshold,
		&i.Options,
		&i.AIProviderID,
	)
	return i, err
}

const getEnabledChatModelConfigs = `-- name: GetEnabledChatModelConfigs :many
SELECT
    cmc.id, cmc.provider, cmc.model, cmc.display_name, cmc.created_by, cmc.updated_by, cmc.enabled, cmc.is_default, cmc.deleted, cmc.deleted_at, cmc.created_at, cmc.updated_at, cmc.context_limit, cmc.compression_threshold, cmc.options, cmc.ai_provider_id
FROM
    chat_model_configs cmc
JOIN
    ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE
    cmc.enabled = TRUE
    AND cmc.deleted = FALSE
    AND ap.enabled = TRUE
    AND ap.deleted = FALSE
ORDER BY
    cmc.provider ASC,
    cmc.model ASC,
    cmc.updated_at DESC,
    cmc.id DESC
`

func (q *sqlQuerier) GetEnabledChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error) {
	rows, err := q.db.QueryContext(ctx, getEnabledChatModelConfigs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatModelConfig
	for rows.Next() {
		var i ChatModelConfig
		if err := rows.Scan(
			&i.ID,
			&i.Provider,
			&i.Model,
			&i.DisplayName,
			&i.CreatedBy,
			&i.UpdatedBy,
			&i.Enabled,
			&i.IsDefault,
			&i.Deleted,
			&i.DeletedAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ContextLimit,
			&i.CompressionThreshold,
			&i.Options,
			&i.AIProviderID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertChatModelConfig = `-- name: InsertChatModelConfig :one
INSERT INTO chat_model_configs (
    provider,
    model,
    display_name,
    created_by,
    updated_by,
    enabled,
    is_default,
    context_limit,
    compression_threshold,
    options,
    ai_provider_id
) VALUES (
    $1::text,
    $2::text,
    $3::text,
    $4::uuid,
    $5::uuid,
    $6::boolean,
    $7::boolean,
    $8::bigint,
    $9::integer,
    $10::jsonb,
    $11::uuid
)
RETURNING
    id, provider, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
`

type InsertChatModelConfigParams struct {
	Provider             string          `db:"provider" json:"provider"`
	Model                string          `db:"model" json:"model"`
	DisplayName          string          `db:"display_name" json:"display_name"`
	CreatedBy            uuid.NullUUID   `db:"created_by" json:"created_by"`
	UpdatedBy            uuid.NullUUID   `db:"updated_by" json:"updated_by"`
	Enabled              bool            `db:"enabled" json:"enabled"`
	IsDefault            bool            `db:"is_default" json:"is_default"`
	ContextLimit         int64           `db:"context_limit" json:"context_limit"`
	CompressionThreshold int32           `db:"compression_threshold" json:"compression_threshold"`
	Options              json.RawMessage `db:"options" json:"options"`
	AIProviderID         uuid.NullUUID   `db:"ai_provider_id" json:"ai_provider_id"`
}

func (q *sqlQuerier) InsertChatModelConfig(ctx context.Context, arg InsertChatModelConfigParams) (ChatModelConfig, error) {
	row := q.db.QueryRowContext(ctx, insertChatModelConfig,
		arg.Provider,
		arg.Model,
		arg.DisplayName,
		arg.CreatedBy,
		arg.UpdatedBy,
		arg.Enabled,
		arg.IsDefault,
		arg.ContextLimit,
		arg.CompressionThreshold,
		arg.Options,
		arg.AIProviderID,
	)
	var i ChatModelConfig
	err := row.Scan(
		&i.ID,
		&i.Provider,
		&i.Model,
		&i.DisplayName,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.Enabled,
		&i.IsDefault,
		&i.Deleted,
		&i.DeletedAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ContextLimit,
		&i.CompressionThreshold,
		&i.Options,
		&i.AIProviderID,
	)
	return i, err
}

const unsetDefaultChatModelConfigs = `-- name: UnsetDefaultChatModelConfigs :exec
UPDATE
    chat_model_configs
SET
    is_default = FALSE,
    updated_at = NOW()
WHERE
    is_default = TRUE
    AND deleted = FALSE
`

func (q *sqlQuerier) UnsetDefaultChatModelConfigs(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, unsetDefaultChatModelConfigs)
	return err
}

const updateChatModelConfig = `-- name: UpdateChatModelConfig :one
UPDATE
    chat_model_configs
SET
    provider = $1::text,
    model = $2::text,
    display_name = $3::text,
    updated_by = $4::uuid,
    enabled = $5::boolean,
    is_default = $6::boolean,
    context_limit = $7::bigint,
    compression_threshold = $8::integer,
    options = $9::jsonb,
    ai_provider_id = $10::uuid,
    updated_at = NOW()
WHERE
    id = $11::uuid
    AND deleted = FALSE
RETURNING
    id, provider, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options, ai_provider_id
`

type UpdateChatModelConfigParams struct {
	Provider             string          `db:"provider" json:"provider"`
	Model                string          `db:"model" json:"model"`
	DisplayName          string          `db:"display_name" json:"display_name"`
	UpdatedBy            uuid.NullUUID   `db:"updated_by" json:"updated_by"`
	Enabled              bool            `db:"enabled" json:"enabled"`
	IsDefault            bool            `db:"is_default" json:"is_default"`
	ContextLimit         int64           `db:"context_limit" json:"context_limit"`
	CompressionThreshold int32           `db:"compression_threshold" json:"compression_threshold"`
	Options              json.RawMessage `db:"options" json:"options"`
	AIProviderID         uuid.NullUUID   `db:"ai_provider_id" json:"ai_provider_id"`
	ID                   uuid.UUID       `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatModelConfig(ctx context.Context, arg UpdateChatModelConfigParams) (ChatModelConfig, error) {
	row := q.db.QueryRowContext(ctx, updateChatModelConfig,
		arg.Provider,
		arg.Model,
		arg.DisplayName,
		arg.UpdatedBy,
		arg.Enabled,
		arg.IsDefault,
		arg.ContextLimit,
		arg.CompressionThreshold,
		arg.Options,
		arg.AIProviderID,
		arg.ID,
	)
	var i ChatModelConfig
	err := row.Scan(
		&i.ID,
		&i.Provider,
		&i.Model,
		&i.DisplayName,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.Enabled,
		&i.IsDefault,
		&i.Deleted,
		&i.DeletedAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ContextLimit,
		&i.CompressionThreshold,
		&i.Options,
		&i.AIProviderID,
	)
	return i, err
}

const acquireChats = `-- name: AcquireChats :many
WITH acquired_chats AS (
UPDATE
    chats
SET
    status = 'running'::chat_status,
    started_at = $1::timestamptz,
    heartbeat_at = $1::timestamptz,
    updated_at = $1::timestamptz,
    worker_id = $2::uuid
WHERE
    id = ANY(
        SELECT
            id
        FROM
            chats
        WHERE
            status = 'pending'::chat_status
            AND archived = false
        ORDER BY
            updated_at ASC
        FOR UPDATE
            SKIP LOCKED
        LIMIT
            $3::int
    )
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        acquired_chats.id,
        acquired_chats.owner_id,
        acquired_chats.workspace_id,
        acquired_chats.title,
        acquired_chats.status,
        acquired_chats.worker_id,
        acquired_chats.started_at,
        acquired_chats.heartbeat_at,
        acquired_chats.created_at,
        acquired_chats.updated_at,
        acquired_chats.parent_chat_id,
        acquired_chats.root_chat_id,
        acquired_chats.last_model_config_id,
        acquired_chats.archived,
        acquired_chats.last_error,
        acquired_chats.mode,
        acquired_chats.mcp_server_ids,
        acquired_chats.labels,
        acquired_chats.build_id,
        acquired_chats.agent_id,
        acquired_chats.pin_order,
        acquired_chats.last_read_message_id,
        acquired_chats.last_injected_context,
        acquired_chats.dynamic_tools,
        acquired_chats.organization_id,
        acquired_chats.plan_mode,
        acquired_chats.client_type,
        acquired_chats.last_turn_summary,
        COALESCE(root.user_acl, acquired_chats.user_acl) AS user_acl,
        COALESCE(root.group_acl, acquired_chats.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        acquired_chats
    LEFT JOIN chats root ON root.id = COALESCE(acquired_chats.root_chat_id, acquired_chats.parent_chat_id)
    JOIN visible_users owner ON owner.id = acquired_chats.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type AcquireChatsParams struct {
	StartedAt time.Time `db:"started_at" json:"started_at"`
	WorkerID  uuid.UUID `db:"worker_id" json:"worker_id"`
	NumChats  int32     `db:"num_chats" json:"num_chats"`
}

// Acquires up to @num_chats pending chats for processing. Uses SKIP LOCKED
// to prevent multiple replicas from acquiring the same chat.
func (q *sqlQuerier) AcquireChats(ctx context.Context, arg AcquireChatsParams) ([]Chat, error) {
	rows, err := q.db.QueryContext(ctx, acquireChats, arg.StartedAt, arg.WorkerID, arg.NumChats)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Chat
	for rows.Next() {
		var i Chat
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.WorkspaceID,
			&i.Title,
			&i.Status,
			&i.WorkerID,
			&i.StartedAt,
			&i.HeartbeatAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentChatID,
			&i.RootChatID,
			&i.LastModelConfigID,
			&i.Archived,
			&i.LastError,
			&i.Mode,
			pq.Array(&i.MCPServerIDs),
			&i.Labels,
			&i.BuildID,
			&i.AgentID,
			&i.PinOrder,
			&i.LastReadMessageID,
			&i.LastInjectedContext,
			&i.DynamicTools,
			&i.OrganizationID,
			&i.PlanMode,
			&i.ClientType,
			&i.LastTurnSummary,
			&i.UserACL,
			&i.GroupACL,
			&i.OwnerUsername,
			&i.OwnerName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const acquireStaleChatDiffStatuses = `-- name: AcquireStaleChatDiffStatuses :many
WITH acquired AS (
    UPDATE
        chat_diff_statuses
    SET
        -- Claim for 5 minutes. The worker sets the real stale_at
        -- after refresh. If the worker crashes, rows become eligible
        -- again after this interval.
        -- NOTE: updated_at is intentionally NOT touched here so
        -- the worker can read it as "when was this row last
        -- externally changed" (by MarkStale or a successful
        -- refresh).
        stale_at = NOW() + INTERVAL '5 minutes'
    WHERE
        chat_id IN (
            SELECT
                cds.chat_id
            FROM
                chat_diff_statuses cds
            INNER JOIN
                chats c ON c.id = cds.chat_id
            WHERE
                cds.stale_at <= NOW()
                AND cds.git_remote_origin != ''
                AND cds.git_branch != ''
                AND c.archived = FALSE
            ORDER BY
                cds.stale_at ASC
            FOR UPDATE OF cds
                SKIP LOCKED
            LIMIT
                $1::int
        )
    RETURNING chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
)
SELECT
    acquired.chat_id, acquired.url, acquired.pull_request_state, acquired.changes_requested, acquired.additions, acquired.deletions, acquired.changed_files, acquired.refreshed_at, acquired.stale_at, acquired.created_at, acquired.updated_at, acquired.git_branch, acquired.git_remote_origin, acquired.pull_request_title, acquired.pull_request_draft, acquired.author_login, acquired.author_avatar_url, acquired.base_branch, acquired.pr_number, acquired.commits, acquired.approved, acquired.reviewer_count, acquired.head_branch,
    c.owner_id
FROM
    acquired
INNER JOIN
    chats c ON c.id = acquired.chat_id
`

type AcquireStaleChatDiffStatusesRow struct {
	ChatID           uuid.UUID      `db:"chat_id" json:"chat_id"`
	Url              sql.NullString `db:"url" json:"url"`
	PullRequestState sql.NullString `db:"pull_request_state" json:"pull_request_state"`
	ChangesRequested bool           `db:"changes_requested" json:"changes_requested"`
	Additions        int32          `db:"additions" json:"additions"`
	Deletions        int32          `db:"deletions" json:"deletions"`
	ChangedFiles     int32          `db:"changed_files" json:"changed_files"`
	RefreshedAt      sql.NullTime   `db:"refreshed_at" json:"refreshed_at"`
	StaleAt          time.Time      `db:"stale_at" json:"stale_at"`
	CreatedAt        time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt        time.Time      `db:"updated_at" json:"updated_at"`
	GitBranch        string         `db:"git_branch" json:"git_branch"`
	GitRemoteOrigin  string         `db:"git_remote_origin" json:"git_remote_origin"`
	PullRequestTitle string         `db:"pull_request_title" json:"pull_request_title"`
	PullRequestDraft bool           `db:"pull_request_draft" json:"pull_request_draft"`
	AuthorLogin      sql.NullString `db:"author_login" json:"author_login"`
	AuthorAvatarUrl  sql.NullString `db:"author_avatar_url" json:"author_avatar_url"`
	BaseBranch       sql.NullString `db:"base_branch" json:"base_branch"`
	PrNumber         sql.NullInt32  `db:"pr_number" json:"pr_number"`
	Commits          sql.NullInt32  `db:"commits" json:"commits"`
	Approved         sql.NullBool   `db:"approved" json:"approved"`
	ReviewerCount    sql.NullInt32  `db:"reviewer_count" json:"reviewer_count"`
	HeadBranch       sql.NullString `db:"head_branch" json:"head_branch"`
	OwnerID          uuid.UUID      `db:"owner_id" json:"owner_id"`
}

func (q *sqlQuerier) AcquireStaleChatDiffStatuses(ctx context.Context, limitVal int32) ([]AcquireStaleChatDiffStatusesRow, error) {
	rows, err := q.db.QueryContext(ctx, acquireStaleChatDiffStatuses, limitVal)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AcquireStaleChatDiffStatusesRow
	for rows.Next() {
		var i AcquireStaleChatDiffStatusesRow
		if err := rows.Scan(
			&i.ChatID,
			&i.Url,
			&i.PullRequestState,
			&i.ChangesRequested,
			&i.Additions,
			&i.Deletions,
			&i.ChangedFiles,
			&i.RefreshedAt,
			&i.StaleAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.GitBranch,
			&i.GitRemoteOrigin,
			&i.PullRequestTitle,
			&i.PullRequestDraft,
			&i.AuthorLogin,
			&i.AuthorAvatarUrl,
			&i.BaseBranch,
			&i.PrNumber,
			&i.Commits,
			&i.Approved,
			&i.ReviewerCount,
			&i.HeadBranch,
			&i.OwnerID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const archiveChatByID = `-- name: ArchiveChatByID :many
WITH updated_chats AS (
    UPDATE chats
    SET archived = true, pin_order = 0, updated_at = NOW()
    WHERE id = $1::uuid OR root_chat_id = $1::uuid
    RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chats.id,
        updated_chats.owner_id,
        updated_chats.workspace_id,
        updated_chats.title,
        updated_chats.status,
        updated_chats.worker_id,
        updated_chats.started_at,
        updated_chats.heartbeat_at,
        updated_chats.created_at,
        updated_chats.updated_at,
        updated_chats.parent_chat_id,
        updated_chats.root_chat_id,
        updated_chats.last_model_config_id,
        updated_chats.archived,
        updated_chats.last_error,
        updated_chats.mode,
        updated_chats.mcp_server_ids,
        updated_chats.labels,
        updated_chats.build_id,
        updated_chats.agent_id,
        updated_chats.pin_order,
        updated_chats.last_read_message_id,
        updated_chats.last_injected_context,
        updated_chats.dynamic_tools,
        updated_chats.organization_id,
        updated_chats.plan_mode,
        updated_chats.client_type,
        updated_chats.last_turn_summary,
        COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chats
    LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chats.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC
`

func (q *sqlQuerier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, error) {
	rows, err := q.db.QueryContext(ctx, archiveChatByID, id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Chat
	for rows.Next() {
		var i Chat
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.WorkspaceID,
			&i.Title,
			&i.Status,
			&i.WorkerID,
			&i.StartedAt,
			&i.HeartbeatAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentChatID,
			&i.RootChatID,
			&i.LastModelConfigID,
			&i.Archived,
			&i.LastError,
			&i.Mode,
			pq.Array(&i.MCPServerIDs),
			&i.Labels,
			&i.BuildID,
			&i.AgentID,
			&i.PinOrder,
			&i.LastReadMessageID,
			&i.LastInjectedContext,
			&i.DynamicTools,
			&i.OrganizationID,
			&i.PlanMode,
			&i.ClientType,
			&i.LastTurnSummary,
			&i.UserACL,
			&i.GroupACL,
			&i.OwnerUsername,
			&i.OwnerName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const autoArchiveInactiveChats = `-- name: AutoArchiveInactiveChats :many
WITH to_archive AS (
    SELECT
        c.id,
        -- Activity = MAX(cm.created_at) across the family, or c.created_at
        -- when the family has no non-deleted messages.
        COALESCE(activity.last_activity_at, c.created_at) AS last_activity_at
    FROM chats c
    LEFT JOIN LATERAL (
        SELECT MAX(cm.created_at) AS last_activity_at
        FROM chat_messages cm
        JOIN chats fc ON fc.id = cm.chat_id
        WHERE (fc.id = c.id OR fc.root_chat_id = c.id)
          AND cm.deleted = false
    ) activity ON TRUE
    WHERE c.archived = false
      AND c.pin_order = 0
      AND c.parent_chat_id IS NULL -- roots only
      -- Redundant filter helps the planner use the partial index on created_at.
      AND c.created_at < $1::timestamptz
      -- New active statuses must be added here to prevent archiving.
      AND c.status NOT IN ('running', 'pending', 'paused', 'requires_action')
      AND COALESCE(activity.last_activity_at, c.created_at) < $1::timestamptz
    -- Sorting by created_at lets Postgres drive the scan from the
    -- partial index instead of evaluating every LATERAL subquery
    -- before sorting. All candidates are past the cutoff, so the
    -- archive order is immaterial once the backlog drains.
    ORDER BY c.created_at ASC
    LIMIT $2
),
archived AS (
    UPDATE chats c
    SET archived = true, pin_order = 0, updated_at = NOW()
    FROM to_archive t
    WHERE (c.id = t.id OR c.root_chat_id = t.id) -- cascade to children
      AND c.archived = false
    RETURNING c.id, c.owner_id, c.workspace_id, c.title, c.status, c.worker_id, c.started_at, c.heartbeat_at, c.created_at, c.updated_at, c.parent_chat_id, c.root_chat_id, c.last_model_config_id, c.archived, c.last_error, c.mode, c.mcp_server_ids, c.labels, c.build_id, c.agent_id, c.pin_order, c.last_read_message_id, c.last_injected_context, c.dynamic_tools, c.organization_id, c.plan_mode, c.client_type, c.last_turn_summary, c.user_acl, c.group_acl
)
SELECT
    a.id, a.owner_id, a.workspace_id, a.title, a.status, a.worker_id, a.started_at, a.heartbeat_at, a.created_at, a.updated_at, a.parent_chat_id, a.root_chat_id, a.last_model_config_id, a.archived, a.last_error, a.mode, a.mcp_server_ids, a.labels, a.build_id, a.agent_id, a.pin_order, a.last_read_message_id, a.last_injected_context, a.dynamic_tools, a.organization_id, a.plan_mode, a.client_type, a.last_turn_summary, a.user_acl, a.group_acl,
    -- Children inherit their root's activity so last_activity_at is never null.
    COALESCE(
        t.last_activity_at,
        (SELECT tr.last_activity_at FROM to_archive tr WHERE tr.id = a.root_chat_id),
        a.created_at
    )::timestamptz AS last_activity_at
FROM archived a
LEFT JOIN to_archive t ON t.id = a.id
ORDER BY (a.root_chat_id IS NULL) DESC, a.owner_id ASC, a.created_at ASC, a.id ASC
`

type AutoArchiveInactiveChatsParams struct {
	ArchiveCutoff time.Time `db:"archive_cutoff" json:"archive_cutoff"`
	LimitCount    int32     `db:"limit_count" json:"limit_count"`
}

type AutoArchiveInactiveChatsRow struct {
	ID                  uuid.UUID             `db:"id" json:"id"`
	OwnerID             uuid.UUID             `db:"owner_id" json:"owner_id"`
	WorkspaceID         uuid.NullUUID         `db:"workspace_id" json:"workspace_id"`
	Title               string                `db:"title" json:"title"`
	Status              ChatStatus            `db:"status" json:"status"`
	WorkerID            uuid.NullUUID         `db:"worker_id" json:"worker_id"`
	StartedAt           sql.NullTime          `db:"started_at" json:"started_at"`
	HeartbeatAt         sql.NullTime          `db:"heartbeat_at" json:"heartbeat_at"`
	CreatedAt           time.Time             `db:"created_at" json:"created_at"`
	UpdatedAt           time.Time             `db:"updated_at" json:"updated_at"`
	ParentChatID        uuid.NullUUID         `db:"parent_chat_id" json:"parent_chat_id"`
	RootChatID          uuid.NullUUID         `db:"root_chat_id" json:"root_chat_id"`
	LastModelConfigID   uuid.UUID             `db:"last_model_config_id" json:"last_model_config_id"`
	Archived            bool                  `db:"archived" json:"archived"`
	LastError           pqtype.NullRawMessage `db:"last_error" json:"last_error"`
	Mode                NullChatMode          `db:"mode" json:"mode"`
	MCPServerIDs        []uuid.UUID           `db:"mcp_server_ids" json:"mcp_server_ids"`
	Labels              json.RawMessage       `db:"labels" json:"labels"`
	BuildID             uuid.NullUUID         `db:"build_id" json:"build_id"`
	AgentID             uuid.NullUUID         `db:"agent_id" json:"agent_id"`
	PinOrder            int32                 `db:"pin_order" json:"pin_order"`
	LastReadMessageID   sql.NullInt64         `db:"last_read_message_id" json:"last_read_message_id"`
	LastInjectedContext pqtype.NullRawMessage `db:"last_injected_context" json:"last_injected_context"`
	DynamicTools        pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"`
	OrganizationID      uuid.UUID             `db:"organization_id" json:"organization_id"`
	PlanMode            NullChatPlanMode      `db:"plan_mode" json:"plan_mode"`
	ClientType          ChatClientType        `db:"client_type" json:"client_type"`
	LastTurnSummary     sql.NullString        `db:"last_turn_summary" json:"last_turn_summary"`
	UserACL             json.RawMessage       `db:"user_acl" json:"user_acl"`
	GroupACL            json.RawMessage       `db:"group_acl" json:"group_acl"`
	LastActivityAt      time.Time             `db:"last_activity_at" json:"last_activity_at"`
}

// Archives inactive root chats (pinned and already-archived chats skipped),
// cascading to children via root_chat_id. Limits apply to roots, not total
// rows. The Go caller passes @archive_cutoff as UTC midnight so that all
// chats sharing the same last-activity date are archived together.
// Used by dbpurge.
// created_at ASC flows through to dbpurge's digest truncation; see
// buildDigestData in dbpurge.go for the tradeoff rationale.
func (q *sqlQuerier) AutoArchiveInactiveChats(ctx context.Context, arg AutoArchiveInactiveChatsParams) ([]AutoArchiveInactiveChatsRow, error) {
	rows, err := q.db.QueryContext(ctx, autoArchiveInactiveChats, arg.ArchiveCutoff, arg.LimitCount)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AutoArchiveInactiveChatsRow
	for rows.Next() {
		var i AutoArchiveInactiveChatsRow
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.WorkspaceID,
			&i.Title,
			&i.Status,
			&i.WorkerID,
			&i.StartedAt,
			&i.HeartbeatAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentChatID,
			&i.RootChatID,
			&i.LastModelConfigID,
			&i.Archived,
			&i.LastError,
			&i.Mode,
			pq.Array(&i.MCPServerIDs),
			&i.Labels,
			&i.BuildID,
			&i.AgentID,
			&i.PinOrder,
			&i.LastReadMessageID,
			&i.LastInjectedContext,
			&i.DynamicTools,
			&i.OrganizationID,
			&i.PlanMode,
			&i.ClientType,
			&i.LastTurnSummary,
			&i.UserACL,
			&i.GroupACL,
			&i.LastActivityAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const backoffChatDiffStatus = `-- name: BackoffChatDiffStatus :exec
UPDATE
    chat_diff_statuses
SET
    -- NOTE: updated_at is intentionally NOT touched here so
    -- the worker can read it as "when was this row last
    -- externally changed" (by MarkStale or a successful
    -- refresh).
    stale_at = $1::timestamptz
WHERE
    chat_id = $2::uuid
`

type BackoffChatDiffStatusParams struct {
	StaleAt time.Time `db:"stale_at" json:"stale_at"`
	ChatID  uuid.UUID `db:"chat_id" json:"chat_id"`
}

func (q *sqlQuerier) BackoffChatDiffStatus(ctx context.Context, arg BackoffChatDiffStatusParams) error {
	_, err := q.db.ExecContext(ctx, backoffChatDiffStatus, arg.StaleAt, arg.ChatID)
	return err
}

const clearChatMessageProviderResponseIDsByChatID = `-- name: ClearChatMessageProviderResponseIDsByChatID :exec
UPDATE chat_messages
SET provider_response_id = NULL
WHERE chat_id = $1::uuid
    AND deleted = false
    AND provider_response_id IS NOT NULL
`

func (q *sqlQuerier) ClearChatMessageProviderResponseIDsByChatID(ctx context.Context, chatID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, clearChatMessageProviderResponseIDsByChatID, chatID)
	return err
}

const countEnabledModelsWithoutPricing = `-- name: CountEnabledModelsWithoutPricing :one
SELECT COUNT(*)::bigint AS count
FROM chat_model_configs
WHERE enabled = TRUE
  AND deleted = FALSE
  AND (
    options->'cost' IS NULL
    OR options->'cost' = 'null'::jsonb
    OR (
      (options->'cost'->>'input_price_per_million_tokens' IS NULL)
      AND (options->'cost'->>'output_price_per_million_tokens' IS NULL)
    )
  )
`

// Counts enabled, non-deleted model configs that lack both input and
// output pricing in their JSONB options.cost configuration.
func (q *sqlQuerier) CountEnabledModelsWithoutPricing(ctx context.Context) (int64, error) {
	row := q.db.QueryRowContext(ctx, countEnabledModelsWithoutPricing)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const deleteAllChatQueuedMessages = `-- name: DeleteAllChatQueuedMessages :exec
DELETE FROM chat_queued_messages WHERE chat_id = $1
`

func (q *sqlQuerier) DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteAllChatQueuedMessages, chatID)
	return err
}

const deleteChatQueuedMessage = `-- name: DeleteChatQueuedMessage :exec
DELETE FROM chat_queued_messages WHERE id = $1 AND chat_id = $2
`

type DeleteChatQueuedMessageParams struct {
	ID     int64     `db:"id" json:"id"`
	ChatID uuid.UUID `db:"chat_id" json:"chat_id"`
}

func (q *sqlQuerier) DeleteChatQueuedMessage(ctx context.Context, arg DeleteChatQueuedMessageParams) error {
	_, err := q.db.ExecContext(ctx, deleteChatQueuedMessage, arg.ID, arg.ChatID)
	return err
}

const deleteChatUsageLimitGroupOverride = `-- name: DeleteChatUsageLimitGroupOverride :exec
UPDATE groups SET chat_spend_limit_micros = NULL WHERE id = $1::uuid
`

func (q *sqlQuerier) DeleteChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteChatUsageLimitGroupOverride, groupID)
	return err
}

const deleteChatUsageLimitUserOverride = `-- name: DeleteChatUsageLimitUserOverride :exec
UPDATE users SET chat_spend_limit_micros = NULL WHERE id = $1::uuid
`

func (q *sqlQuerier) DeleteChatUsageLimitUserOverride(ctx context.Context, userID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteChatUsageLimitUserOverride, userID)
	return err
}

const deleteOldChats = `-- name: DeleteOldChats :execrows
WITH deletable AS (
    SELECT id
    FROM chats
    WHERE archived = true
      AND updated_at < $1::timestamptz
    ORDER BY updated_at ASC
    LIMIT $2
)
DELETE FROM chats
USING deletable
WHERE chats.id = deletable.id
  AND chats.archived = true
`

type DeleteOldChatsParams struct {
	BeforeTime time.Time `db:"before_time" json:"before_time"`
	LimitCount int32     `db:"limit_count" json:"limit_count"`
}

// Deletes chats that have been archived for longer than the given
// threshold. Active (non-archived) chats are never deleted.
// Related chat_messages, chat_diff_statuses, and
// chat_queued_messages are removed via ON DELETE CASCADE.
// Parent/root references on child chats are SET NULL.
func (q *sqlQuerier) DeleteOldChats(ctx context.Context, arg DeleteOldChatsParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteOldChats, arg.BeforeTime, arg.LimitCount)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const getActiveChatsByAgentID = `-- name: GetActiveChatsByAgentID :many
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
WHERE agent_id = $1::uuid
    AND archived = false
    -- Active statuses only: waiting, pending, running, paused,
    -- requires_action.
    -- Excludes completed and error (terminal states).
    AND status IN ('waiting', 'running', 'paused', 'pending', 'requires_action')
ORDER BY updated_at DESC
`

func (q *sqlQuerier) GetActiveChatsByAgentID(ctx context.Context, agentID uuid.UUID) ([]Chat, error) {
	rows, err := q.db.QueryContext(ctx, getActiveChatsByAgentID, agentID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Chat
	for rows.Next() {
		var i Chat
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.WorkspaceID,
			&i.Title,
			&i.Status,
			&i.WorkerID,
			&i.StartedAt,
			&i.HeartbeatAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentChatID,
			&i.RootChatID,
			&i.LastModelConfigID,
			&i.Archived,
			&i.LastError,
			&i.Mode,
			pq.Array(&i.MCPServerIDs),
			&i.Labels,
			&i.BuildID,
			&i.AgentID,
			&i.PinOrder,
			&i.LastReadMessageID,
			&i.LastInjectedContext,
			&i.DynamicTools,
			&i.OrganizationID,
			&i.PlanMode,
			&i.ClientType,
			&i.LastTurnSummary,
			&i.UserACL,
			&i.GroupACL,
			&i.OwnerUsername,
			&i.OwnerName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatACLByID = `-- name: GetChatACLByID :one
SELECT
    user_acl AS users,
    group_acl AS groups
FROM
    chats
WHERE
    id = $1::uuid
`

type GetChatACLByIDRow struct {
	Users  ChatACL `db:"users" json:"users"`
	Groups ChatACL `db:"groups" json:"groups"`
}

func (q *sqlQuerier) GetChatACLByID(ctx context.Context, id uuid.UUID) (GetChatACLByIDRow, error) {
	row := q.db.QueryRowContext(ctx, getChatACLByID, id)
	var i GetChatACLByIDRow
	err := row.Scan(&i.Users, &i.Groups)
	return i, err
}

const getChatByID = `-- name: GetChatByID :one
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
WHERE id = $1::uuid
`

func (q *sqlQuerier) GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error) {
	row := q.db.QueryRowContext(ctx, getChatByID, id)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const getChatByIDForUpdate = `-- name: GetChatByIDForUpdate :one
WITH locked_chat AS (
    SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
    FROM chats
    WHERE id = $1::uuid
    FOR UPDATE
),
chats_expanded AS (
    SELECT
        locked_chat.id,
        locked_chat.owner_id,
        locked_chat.workspace_id,
        locked_chat.title,
        locked_chat.status,
        locked_chat.worker_id,
        locked_chat.started_at,
        locked_chat.heartbeat_at,
        locked_chat.created_at,
        locked_chat.updated_at,
        locked_chat.parent_chat_id,
        locked_chat.root_chat_id,
        locked_chat.last_model_config_id,
        locked_chat.archived,
        locked_chat.last_error,
        locked_chat.mode,
        locked_chat.mcp_server_ids,
        locked_chat.labels,
        locked_chat.build_id,
        locked_chat.agent_id,
        locked_chat.pin_order,
        locked_chat.last_read_message_id,
        locked_chat.last_injected_context,
        locked_chat.dynamic_tools,
        locked_chat.organization_id,
        locked_chat.plan_mode,
        locked_chat.client_type,
        locked_chat.last_turn_summary,
        COALESCE(root.user_acl, locked_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, locked_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        locked_chat
    LEFT JOIN chats root ON root.id = COALESCE(locked_chat.root_chat_id, locked_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = locked_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

func (q *sqlQuerier) GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error) {
	row := q.db.QueryRowContext(ctx, getChatByIDForUpdate, id)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const getChatCostPerChat = `-- name: GetChatCostPerChat :many
WITH chat_costs AS (
    SELECT
        COALESCE(c.root_chat_id, c.id) AS root_chat_id,
        COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
        COUNT(*) FILTER (
            WHERE cm.input_tokens IS NOT NULL
                OR cm.output_tokens IS NOT NULL
                OR cm.reasoning_tokens IS NOT NULL
                OR cm.cache_creation_tokens IS NOT NULL
                OR cm.cache_read_tokens IS NOT NULL
        )::bigint AS message_count,
        COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
        COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
        COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
        COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens,
        COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms
    FROM chat_messages cm
    JOIN chats c ON c.id = cm.chat_id
    WHERE c.owner_id = $1::uuid
      AND cm.role = 'assistant'
      AND cm.created_at >= $2::timestamptz
      AND cm.created_at < $3::timestamptz
    GROUP BY COALESCE(c.root_chat_id, c.id)
)
SELECT
    cc.root_chat_id,
    COALESCE(rc.title, '') AS chat_title,
    cc.total_cost_micros,
    cc.message_count,
    cc.total_input_tokens,
    cc.total_output_tokens,
    cc.total_cache_read_tokens,
    cc.total_cache_creation_tokens,
    cc.total_runtime_ms
FROM chat_costs cc
LEFT JOIN chats rc ON rc.id = cc.root_chat_id
ORDER BY cc.total_cost_micros DESC
`

type GetChatCostPerChatParams struct {
	OwnerID   uuid.UUID `db:"owner_id" json:"owner_id"`
	StartDate time.Time `db:"start_date" json:"start_date"`
	EndDate   time.Time `db:"end_date" json:"end_date"`
}

type GetChatCostPerChatRow struct {
	RootChatID               uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
	ChatTitle                string    `db:"chat_title" json:"chat_title"`
	TotalCostMicros          int64     `db:"total_cost_micros" json:"total_cost_micros"`
	MessageCount             int64     `db:"message_count" json:"message_count"`
	TotalInputTokens         int64     `db:"total_input_tokens" json:"total_input_tokens"`
	TotalOutputTokens        int64     `db:"total_output_tokens" json:"total_output_tokens"`
	TotalCacheReadTokens     int64     `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
	TotalCacheCreationTokens int64     `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
	TotalRuntimeMs           int64     `db:"total_runtime_ms" json:"total_runtime_ms"`
}

// Per-root-chat cost breakdown for a single user within a date range.
// Groups by root_chat_id so forked chats roll up under their root.
// Only counts assistant-role messages.
func (q *sqlQuerier) GetChatCostPerChat(ctx context.Context, arg GetChatCostPerChatParams) ([]GetChatCostPerChatRow, error) {
	rows, err := q.db.QueryContext(ctx, getChatCostPerChat, arg.OwnerID, arg.StartDate, arg.EndDate)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatCostPerChatRow
	for rows.Next() {
		var i GetChatCostPerChatRow
		if err := rows.Scan(
			&i.RootChatID,
			&i.ChatTitle,
			&i.TotalCostMicros,
			&i.MessageCount,
			&i.TotalInputTokens,
			&i.TotalOutputTokens,
			&i.TotalCacheReadTokens,
			&i.TotalCacheCreationTokens,
			&i.TotalRuntimeMs,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatCostPerModel = `-- name: GetChatCostPerModel :many
SELECT
    cmc.id AS model_config_id,
    cmc.display_name,
    cmc.provider,
    cmc.model,
    COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
    COUNT(*) FILTER (
        WHERE cm.input_tokens IS NOT NULL
            OR cm.output_tokens IS NOT NULL
            OR cm.reasoning_tokens IS NOT NULL
            OR cm.cache_creation_tokens IS NOT NULL
            OR cm.cache_read_tokens IS NOT NULL
    )::bigint AS message_count,
    COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
    COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
    COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
    COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens,
    COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms
FROM
    chat_messages cm
JOIN
    chats c ON c.id = cm.chat_id
JOIN
    chat_model_configs cmc ON cmc.id = cm.model_config_id
WHERE
    c.owner_id = $1::uuid
    AND cm.role = 'assistant'
    AND cm.created_at >= $2::timestamptz
    AND cm.created_at < $3::timestamptz
GROUP BY
    cmc.id, cmc.display_name, cmc.provider, cmc.model
ORDER BY
    total_cost_micros DESC
`

type GetChatCostPerModelParams struct {
	OwnerID   uuid.UUID `db:"owner_id" json:"owner_id"`
	StartDate time.Time `db:"start_date" json:"start_date"`
	EndDate   time.Time `db:"end_date" json:"end_date"`
}

type GetChatCostPerModelRow struct {
	ModelConfigID            uuid.UUID `db:"model_config_id" json:"model_config_id"`
	DisplayName              string    `db:"display_name" json:"display_name"`
	Provider                 string    `db:"provider" json:"provider"`
	Model                    string    `db:"model" json:"model"`
	TotalCostMicros          int64     `db:"total_cost_micros" json:"total_cost_micros"`
	MessageCount             int64     `db:"message_count" json:"message_count"`
	TotalInputTokens         int64     `db:"total_input_tokens" json:"total_input_tokens"`
	TotalOutputTokens        int64     `db:"total_output_tokens" json:"total_output_tokens"`
	TotalCacheReadTokens     int64     `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
	TotalCacheCreationTokens int64     `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
	TotalRuntimeMs           int64     `db:"total_runtime_ms" json:"total_runtime_ms"`
}

// Per-model cost breakdown for a single user within a date range.
// Only counts assistant-role messages that have a model_config_id.
func (q *sqlQuerier) GetChatCostPerModel(ctx context.Context, arg GetChatCostPerModelParams) ([]GetChatCostPerModelRow, error) {
	rows, err := q.db.QueryContext(ctx, getChatCostPerModel, arg.OwnerID, arg.StartDate, arg.EndDate)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatCostPerModelRow
	for rows.Next() {
		var i GetChatCostPerModelRow
		if err := rows.Scan(
			&i.ModelConfigID,
			&i.DisplayName,
			&i.Provider,
			&i.Model,
			&i.TotalCostMicros,
			&i.MessageCount,
			&i.TotalInputTokens,
			&i.TotalOutputTokens,
			&i.TotalCacheReadTokens,
			&i.TotalCacheCreationTokens,
			&i.TotalRuntimeMs,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatCostPerUser = `-- name: GetChatCostPerUser :many
WITH chat_cost_users AS (
    SELECT
        c.owner_id AS user_id,
        u.username,
        u.name,
        u.avatar_url,
        COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
        COUNT(*) FILTER (
            WHERE cm.input_tokens IS NOT NULL
                OR cm.output_tokens IS NOT NULL
                OR cm.reasoning_tokens IS NOT NULL
                OR cm.cache_creation_tokens IS NOT NULL
                OR cm.cache_read_tokens IS NOT NULL
        )::bigint AS message_count,
        COUNT(DISTINCT COALESCE(c.root_chat_id, c.id))::bigint AS chat_count,
        COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
        COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
        COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
        COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens,
        COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms
    FROM
        chat_messages cm
    JOIN
        chats c ON c.id = cm.chat_id
    JOIN
        users u ON u.id = c.owner_id
    WHERE
        cm.role = 'assistant'
        AND cm.created_at >= $3::timestamptz
        AND cm.created_at < $4::timestamptz
        AND (
            $5::text = ''
            OR u.username ILIKE '%' || $5::text || '%'
            OR u.name ILIKE '%' || $5::text || '%'
        )
    GROUP BY
        c.owner_id,
        u.username,
        u.name,
        u.avatar_url
)
SELECT
    user_id,
    username,
    name,
    avatar_url,
    total_cost_micros,
    message_count,
    chat_count,
    total_input_tokens,
    total_output_tokens,
    total_cache_read_tokens,
    total_cache_creation_tokens,
    total_runtime_ms,
    COUNT(*) OVER()::bigint AS total_count
FROM
    chat_cost_users
ORDER BY
    total_cost_micros DESC,
    username ASC
LIMIT
    $2::int
OFFSET
    $1::int
`

type GetChatCostPerUserParams struct {
	PageOffset int32     `db:"page_offset" json:"page_offset"`
	PageLimit  int32     `db:"page_limit" json:"page_limit"`
	StartDate  time.Time `db:"start_date" json:"start_date"`
	EndDate    time.Time `db:"end_date" json:"end_date"`
	Username   string    `db:"username" json:"username"`
}

type GetChatCostPerUserRow struct {
	UserID                   uuid.UUID `db:"user_id" json:"user_id"`
	Username                 string    `db:"username" json:"username"`
	Name                     string    `db:"name" json:"name"`
	AvatarURL                string    `db:"avatar_url" json:"avatar_url"`
	TotalCostMicros          int64     `db:"total_cost_micros" json:"total_cost_micros"`
	MessageCount             int64     `db:"message_count" json:"message_count"`
	ChatCount                int64     `db:"chat_count" json:"chat_count"`
	TotalInputTokens         int64     `db:"total_input_tokens" json:"total_input_tokens"`
	TotalOutputTokens        int64     `db:"total_output_tokens" json:"total_output_tokens"`
	TotalCacheReadTokens     int64     `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
	TotalCacheCreationTokens int64     `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
	TotalRuntimeMs           int64     `db:"total_runtime_ms" json:"total_runtime_ms"`
	TotalCount               int64     `db:"total_count" json:"total_count"`
}

// Deployment-wide per-user cost rollup within a date range.
// Only counts assistant-role messages.
func (q *sqlQuerier) GetChatCostPerUser(ctx context.Context, arg GetChatCostPerUserParams) ([]GetChatCostPerUserRow, error) {
	rows, err := q.db.QueryContext(ctx, getChatCostPerUser,
		arg.PageOffset,
		arg.PageLimit,
		arg.StartDate,
		arg.EndDate,
		arg.Username,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatCostPerUserRow
	for rows.Next() {
		var i GetChatCostPerUserRow
		if err := rows.Scan(
			&i.UserID,
			&i.Username,
			&i.Name,
			&i.AvatarURL,
			&i.TotalCostMicros,
			&i.MessageCount,
			&i.ChatCount,
			&i.TotalInputTokens,
			&i.TotalOutputTokens,
			&i.TotalCacheReadTokens,
			&i.TotalCacheCreationTokens,
			&i.TotalRuntimeMs,
			&i.TotalCount,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatCostSummary = `-- name: GetChatCostSummary :one
SELECT
    COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
    COUNT(*) FILTER (
        WHERE cm.total_cost_micros IS NOT NULL
    )::bigint AS priced_message_count,
    COUNT(*) FILTER (
        WHERE cm.total_cost_micros IS NULL
            AND (
                cm.input_tokens IS NOT NULL
                OR cm.output_tokens IS NOT NULL
                OR cm.reasoning_tokens IS NOT NULL
                OR cm.cache_creation_tokens IS NOT NULL
                OR cm.cache_read_tokens IS NOT NULL
            )
    )::bigint AS unpriced_message_count,
    COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
    COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
    COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
    COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens,
    COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms
FROM
    chat_messages cm
JOIN
    chats c ON c.id = cm.chat_id
WHERE
    c.owner_id = $1::uuid
    AND cm.role = 'assistant'
    AND cm.created_at >= $2::timestamptz
    AND cm.created_at < $3::timestamptz
`

type GetChatCostSummaryParams struct {
	OwnerID   uuid.UUID `db:"owner_id" json:"owner_id"`
	StartDate time.Time `db:"start_date" json:"start_date"`
	EndDate   time.Time `db:"end_date" json:"end_date"`
}

type GetChatCostSummaryRow struct {
	TotalCostMicros          int64 `db:"total_cost_micros" json:"total_cost_micros"`
	PricedMessageCount       int64 `db:"priced_message_count" json:"priced_message_count"`
	UnpricedMessageCount     int64 `db:"unpriced_message_count" json:"unpriced_message_count"`
	TotalInputTokens         int64 `db:"total_input_tokens" json:"total_input_tokens"`
	TotalOutputTokens        int64 `db:"total_output_tokens" json:"total_output_tokens"`
	TotalCacheReadTokens     int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
	TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
	TotalRuntimeMs           int64 `db:"total_runtime_ms" json:"total_runtime_ms"`
}

// Aggregate cost summary for a single user within a date range.
// Only counts assistant-role messages.
func (q *sqlQuerier) GetChatCostSummary(ctx context.Context, arg GetChatCostSummaryParams) (GetChatCostSummaryRow, error) {
	row := q.db.QueryRowContext(ctx, getChatCostSummary, arg.OwnerID, arg.StartDate, arg.EndDate)
	var i GetChatCostSummaryRow
	err := row.Scan(
		&i.TotalCostMicros,
		&i.PricedMessageCount,
		&i.UnpricedMessageCount,
		&i.TotalInputTokens,
		&i.TotalOutputTokens,
		&i.TotalCacheReadTokens,
		&i.TotalCacheCreationTokens,
		&i.TotalRuntimeMs,
	)
	return i, err
}

const getChatDiffStatusByChatID = `-- name: GetChatDiffStatusByChatID :one
SELECT
    chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
FROM
    chat_diff_statuses
WHERE
    chat_id = $1::uuid
`

func (q *sqlQuerier) GetChatDiffStatusByChatID(ctx context.Context, chatID uuid.UUID) (ChatDiffStatus, error) {
	row := q.db.QueryRowContext(ctx, getChatDiffStatusByChatID, chatID)
	var i ChatDiffStatus
	err := row.Scan(
		&i.ChatID,
		&i.Url,
		&i.PullRequestState,
		&i.ChangesRequested,
		&i.Additions,
		&i.Deletions,
		&i.ChangedFiles,
		&i.RefreshedAt,
		&i.StaleAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.GitBranch,
		&i.GitRemoteOrigin,
		&i.PullRequestTitle,
		&i.PullRequestDraft,
		&i.AuthorLogin,
		&i.AuthorAvatarUrl,
		&i.BaseBranch,
		&i.PrNumber,
		&i.Commits,
		&i.Approved,
		&i.ReviewerCount,
		&i.HeadBranch,
	)
	return i, err
}

const getChatDiffStatusSummary = `-- name: GetChatDiffStatusSummary :one
WITH deduped AS (
    SELECT DISTINCT ON (COALESCE(NULLIF(cds.url, ''), c.id::text))
        cds.pull_request_state
    FROM chat_diff_statuses cds
    JOIN chats c ON c.id = cds.chat_id
    WHERE cds.pull_request_state IN ('open', 'merged', 'closed')
    ORDER BY COALESCE(NULLIF(cds.url, ''), c.id::text), cds.updated_at DESC, c.id DESC
)
SELECT
    COUNT(*)::bigint AS total,
    COUNT(*) FILTER (WHERE pull_request_state = 'open')::bigint AS open,
    COUNT(*) FILTER (WHERE pull_request_state = 'merged')::bigint AS merged,
    COUNT(*) FILTER (WHERE pull_request_state = 'closed')::bigint AS closed
FROM deduped
`

type GetChatDiffStatusSummaryRow struct {
	Total  int64 `db:"total" json:"total"`
	Open   int64 `db:"open" json:"open"`
	Merged int64 `db:"merged" json:"merged"`
	Closed int64 `db:"closed" json:"closed"`
}

// Returns aggregate PR counts across all agent chats for telemetry.
// Deduplicates by PR URL so forked chats referencing the same pull
// request are counted once (using the most recently refreshed state).
// Total is derived from the three recognized state buckets and
// always equals open + merged + closed; other non-NULL states are
// intentionally excluded from these aggregates.
func (q *sqlQuerier) GetChatDiffStatusSummary(ctx context.Context) (GetChatDiffStatusSummaryRow, error) {
	row := q.db.QueryRowContext(ctx, getChatDiffStatusSummary)
	var i GetChatDiffStatusSummaryRow
	err := row.Scan(
		&i.Total,
		&i.Open,
		&i.Merged,
		&i.Closed,
	)
	return i, err
}

const getChatDiffStatusesByChatIDs = `-- name: GetChatDiffStatusesByChatIDs :many
SELECT
    chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
FROM
    chat_diff_statuses
WHERE
    chat_id = ANY($1::uuid[])
`

func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds []uuid.UUID) ([]ChatDiffStatus, error) {
	rows, err := q.db.QueryContext(ctx, getChatDiffStatusesByChatIDs, pq.Array(chatIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatDiffStatus
	for rows.Next() {
		var i ChatDiffStatus
		if err := rows.Scan(
			&i.ChatID,
			&i.Url,
			&i.PullRequestState,
			&i.ChangesRequested,
			&i.Additions,
			&i.Deletions,
			&i.ChangedFiles,
			&i.RefreshedAt,
			&i.StaleAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.GitBranch,
			&i.GitRemoteOrigin,
			&i.PullRequestTitle,
			&i.PullRequestDraft,
			&i.AuthorLogin,
			&i.AuthorAvatarUrl,
			&i.BaseBranch,
			&i.PrNumber,
			&i.Commits,
			&i.Approved,
			&i.ReviewerCount,
			&i.HeadBranch,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatMessageByID = `-- name: GetChatMessageByID :one
SELECT
    id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id
FROM
    chat_messages
WHERE
    id = $1::bigint
    AND deleted = false
`

func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMessage, error) {
	row := q.db.QueryRowContext(ctx, getChatMessageByID, id)
	var i ChatMessage
	err := row.Scan(
		&i.ID,
		&i.ChatID,
		&i.ModelConfigID,
		&i.CreatedAt,
		&i.Role,
		&i.Content,
		&i.Visibility,
		&i.InputTokens,
		&i.OutputTokens,
		&i.TotalTokens,
		&i.ReasoningTokens,
		&i.CacheCreationTokens,
		&i.CacheReadTokens,
		&i.ContextLimit,
		&i.Compressed,
		&i.CreatedBy,
		&i.ContentVersion,
		&i.TotalCostMicros,
		&i.RuntimeMs,
		&i.Deleted,
		&i.ProviderResponseID,
		&i.APIKeyID,
	)
	return i, err
}

const getChatMessageSummariesPerChat = `-- name: GetChatMessageSummariesPerChat :many
SELECT
    cm.chat_id,
    COUNT(*)::bigint AS message_count,
    COUNT(*) FILTER (WHERE cm.role = 'user')::bigint AS user_message_count,
    COUNT(*) FILTER (WHERE cm.role = 'assistant')::bigint AS assistant_message_count,
    COUNT(*) FILTER (WHERE cm.role = 'tool')::bigint AS tool_message_count,
    COUNT(*) FILTER (WHERE cm.role = 'system')::bigint AS system_message_count,
    COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
    COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
    COALESCE(SUM(cm.reasoning_tokens), 0)::bigint AS total_reasoning_tokens,
    COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens,
    COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
    COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_cost_micros,
    COALESCE(SUM(cm.runtime_ms), 0)::bigint AS total_runtime_ms,
    COUNT(DISTINCT cm.model_config_id)::bigint AS distinct_model_count,
    COUNT(*) FILTER (WHERE cm.compressed)::bigint AS compressed_message_count
FROM chat_messages cm
WHERE cm.created_at > $1
  AND cm.deleted = false
GROUP BY cm.chat_id
`

type GetChatMessageSummariesPerChatRow struct {
	ChatID                   uuid.UUID `db:"chat_id" json:"chat_id"`
	MessageCount             int64     `db:"message_count" json:"message_count"`
	UserMessageCount         int64     `db:"user_message_count" json:"user_message_count"`
	AssistantMessageCount    int64     `db:"assistant_message_count" json:"assistant_message_count"`
	ToolMessageCount         int64     `db:"tool_message_count" json:"tool_message_count"`
	SystemMessageCount       int64     `db:"system_message_count" json:"system_message_count"`
	TotalInputTokens         int64     `db:"total_input_tokens" json:"total_input_tokens"`
	TotalOutputTokens        int64     `db:"total_output_tokens" json:"total_output_tokens"`
	TotalReasoningTokens     int64     `db:"total_reasoning_tokens" json:"total_reasoning_tokens"`
	TotalCacheCreationTokens int64     `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
	TotalCacheReadTokens     int64     `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
	TotalCostMicros          int64     `db:"total_cost_micros" json:"total_cost_micros"`
	TotalRuntimeMs           int64     `db:"total_runtime_ms" json:"total_runtime_ms"`
	DistinctModelCount       int64     `db:"distinct_model_count" json:"distinct_model_count"`
	CompressedMessageCount   int64     `db:"compressed_message_count" json:"compressed_message_count"`
}

// Aggregates message-level metrics per chat for messages created
// after the given timestamp. Uses message created_at so that
// ongoing activity in long-running chats is captured each window.
func (q *sqlQuerier) GetChatMessageSummariesPerChat(ctx context.Context, createdAfter time.Time) ([]GetChatMessageSummariesPerChatRow, error) {
	rows, err := q.db.QueryContext(ctx, getChatMessageSummariesPerChat, createdAfter)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatMessageSummariesPerChatRow
	for rows.Next() {
		var i GetChatMessageSummariesPerChatRow
		if err := rows.Scan(
			&i.ChatID,
			&i.MessageCount,
			&i.UserMessageCount,
			&i.AssistantMessageCount,
			&i.ToolMessageCount,
			&i.SystemMessageCount,
			&i.TotalInputTokens,
			&i.TotalOutputTokens,
			&i.TotalReasoningTokens,
			&i.TotalCacheCreationTokens,
			&i.TotalCacheReadTokens,
			&i.TotalCostMicros,
			&i.TotalRuntimeMs,
			&i.DistinctModelCount,
			&i.CompressedMessageCount,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many
SELECT
    id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id
FROM
    chat_messages
WHERE
    chat_id = $1::uuid
    AND id > $2::bigint
    AND visibility IN ('user', 'both')
    AND deleted = false
ORDER BY
    created_at ASC
`

type GetChatMessagesByChatIDParams struct {
	ChatID  uuid.UUID `db:"chat_id" json:"chat_id"`
	AfterID int64     `db:"after_id" json:"after_id"`
}

func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMessagesByChatIDParams) ([]ChatMessage, error) {
	rows, err := q.db.QueryContext(ctx, getChatMessagesByChatID, arg.ChatID, arg.AfterID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatMessage
	for rows.Next() {
		var i ChatMessage
		if err := rows.Scan(
			&i.ID,
			&i.ChatID,
			&i.ModelConfigID,
			&i.CreatedAt,
			&i.Role,
			&i.Content,
			&i.Visibility,
			&i.InputTokens,
			&i.OutputTokens,
			&i.TotalTokens,
			&i.ReasoningTokens,
			&i.CacheCreationTokens,
			&i.CacheReadTokens,
			&i.ContextLimit,
			&i.Compressed,
			&i.CreatedBy,
			&i.ContentVersion,
			&i.TotalCostMicros,
			&i.RuntimeMs,
			&i.Deleted,
			&i.ProviderResponseID,
			&i.APIKeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatMessagesByChatIDAscPaginated = `-- name: GetChatMessagesByChatIDAscPaginated :many
SELECT
    id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id
FROM
    chat_messages
WHERE
    chat_id = $1::uuid
    AND id > $2::bigint
    AND visibility IN ('user', 'both')
    AND deleted = false
ORDER BY
    id ASC
LIMIT
    COALESCE(NULLIF($3::int, 0), 50)
`

type GetChatMessagesByChatIDAscPaginatedParams struct {
	ChatID   uuid.UUID `db:"chat_id" json:"chat_id"`
	AfterID  int64     `db:"after_id" json:"after_id"`
	LimitVal int32     `db:"limit_val" json:"limit_val"`
}

func (q *sqlQuerier) GetChatMessagesByChatIDAscPaginated(ctx context.Context, arg GetChatMessagesByChatIDAscPaginatedParams) ([]ChatMessage, error) {
	rows, err := q.db.QueryContext(ctx, getChatMessagesByChatIDAscPaginated, arg.ChatID, arg.AfterID, arg.LimitVal)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatMessage
	for rows.Next() {
		var i ChatMessage
		if err := rows.Scan(
			&i.ID,
			&i.ChatID,
			&i.ModelConfigID,
			&i.CreatedAt,
			&i.Role,
			&i.Content,
			&i.Visibility,
			&i.InputTokens,
			&i.OutputTokens,
			&i.TotalTokens,
			&i.ReasoningTokens,
			&i.CacheCreationTokens,
			&i.CacheReadTokens,
			&i.ContextLimit,
			&i.Compressed,
			&i.CreatedBy,
			&i.ContentVersion,
			&i.TotalCostMicros,
			&i.RuntimeMs,
			&i.Deleted,
			&i.ProviderResponseID,
			&i.APIKeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatMessagesByChatIDDescPaginated = `-- name: GetChatMessagesByChatIDDescPaginated :many
SELECT
    id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id
FROM
    chat_messages
WHERE
    chat_id = $1::uuid
    AND CASE
        WHEN $2::bigint > 0 THEN id < $2::bigint
        ELSE true
    END
    AND CASE
        WHEN $3::bigint > 0 THEN id > $3::bigint
        ELSE true
    END
    AND visibility IN ('user', 'both')
    AND deleted = false
ORDER BY
    id DESC
LIMIT
    COALESCE(NULLIF($4::int, 0), 50)
`

type GetChatMessagesByChatIDDescPaginatedParams struct {
	ChatID   uuid.UUID `db:"chat_id" json:"chat_id"`
	BeforeID int64     `db:"before_id" json:"before_id"`
	AfterID  int64     `db:"after_id" json:"after_id"`
	LimitVal int32     `db:"limit_val" json:"limit_val"`
}

func (q *sqlQuerier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, arg GetChatMessagesByChatIDDescPaginatedParams) ([]ChatMessage, error) {
	rows, err := q.db.QueryContext(ctx, getChatMessagesByChatIDDescPaginated,
		arg.ChatID,
		arg.BeforeID,
		arg.AfterID,
		arg.LimitVal,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatMessage
	for rows.Next() {
		var i ChatMessage
		if err := rows.Scan(
			&i.ID,
			&i.ChatID,
			&i.ModelConfigID,
			&i.CreatedAt,
			&i.Role,
			&i.Content,
			&i.Visibility,
			&i.InputTokens,
			&i.OutputTokens,
			&i.TotalTokens,
			&i.ReasoningTokens,
			&i.CacheCreationTokens,
			&i.CacheReadTokens,
			&i.ContextLimit,
			&i.Compressed,
			&i.CreatedBy,
			&i.ContentVersion,
			&i.TotalCostMicros,
			&i.RuntimeMs,
			&i.Deleted,
			&i.ProviderResponseID,
			&i.APIKeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatMessagesForPromptByChatID = `-- name: GetChatMessagesForPromptByChatID :many
WITH latest_compressed_summary AS (
    SELECT
        id
    FROM
        chat_messages
    WHERE
        chat_id = $1::uuid
        AND compressed = TRUE
        AND deleted = false
        AND visibility = 'model'
    ORDER BY
        created_at DESC,
        id DESC
    LIMIT
        1
)
SELECT
    id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id
FROM
    chat_messages
WHERE
    chat_id = $1::uuid
    AND visibility IN ('model', 'both')
    AND deleted = false
    AND (
        (
            role = 'system'
            AND compressed = FALSE
        )
        OR (
            compressed = FALSE
            AND (
                NOT EXISTS (
                    SELECT
                        1
                    FROM
                        latest_compressed_summary
                )
                OR id > (
                    SELECT
                        id
                    FROM
                        latest_compressed_summary
                )
            )
        )
        OR id = (
            SELECT
                id
            FROM
                latest_compressed_summary
        )
    )
ORDER BY
    created_at ASC,
    id ASC
`

func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatMessage, error) {
	rows, err := q.db.QueryContext(ctx, getChatMessagesForPromptByChatID, chatID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatMessage
	for rows.Next() {
		var i ChatMessage
		if err := rows.Scan(
			&i.ID,
			&i.ChatID,
			&i.ModelConfigID,
			&i.CreatedAt,
			&i.Role,
			&i.Content,
			&i.Visibility,
			&i.InputTokens,
			&i.OutputTokens,
			&i.TotalTokens,
			&i.ReasoningTokens,
			&i.CacheCreationTokens,
			&i.CacheReadTokens,
			&i.ContextLimit,
			&i.Compressed,
			&i.CreatedBy,
			&i.ContentVersion,
			&i.TotalCostMicros,
			&i.RuntimeMs,
			&i.Deleted,
			&i.ProviderResponseID,
			&i.APIKeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatModelConfigsForTelemetry = `-- name: GetChatModelConfigsForTelemetry :many
SELECT id, provider, model, context_limit, enabled, is_default
FROM chat_model_configs
WHERE deleted = false
`

type GetChatModelConfigsForTelemetryRow struct {
	ID           uuid.UUID `db:"id" json:"id"`
	Provider     string    `db:"provider" json:"provider"`
	Model        string    `db:"model" json:"model"`
	ContextLimit int64     `db:"context_limit" json:"context_limit"`
	Enabled      bool      `db:"enabled" json:"enabled"`
	IsDefault    bool      `db:"is_default" json:"is_default"`
}

// Returns all model configurations for telemetry snapshot collection.
func (q *sqlQuerier) GetChatModelConfigsForTelemetry(ctx context.Context) ([]GetChatModelConfigsForTelemetryRow, error) {
	rows, err := q.db.QueryContext(ctx, getChatModelConfigsForTelemetry)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatModelConfigsForTelemetryRow
	for rows.Next() {
		var i GetChatModelConfigsForTelemetryRow
		if err := rows.Scan(
			&i.ID,
			&i.Provider,
			&i.Model,
			&i.ContextLimit,
			&i.Enabled,
			&i.IsDefault,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatQueuedMessages = `-- name: GetChatQueuedMessages :many
SELECT id, chat_id, content, created_at, model_config_id, api_key_id FROM chat_queued_messages
WHERE chat_id = $1
ORDER BY created_at ASC, id ASC
`

func (q *sqlQuerier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]ChatQueuedMessage, error) {
	rows, err := q.db.QueryContext(ctx, getChatQueuedMessages, chatID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatQueuedMessage
	for rows.Next() {
		var i ChatQueuedMessage
		if err := rows.Scan(
			&i.ID,
			&i.ChatID,
			&i.Content,
			&i.CreatedAt,
			&i.ModelConfigID,
			&i.APIKeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatUsageLimitConfig = `-- name: GetChatUsageLimitConfig :one
SELECT id, singleton, enabled, default_limit_micros, period, created_at, updated_at FROM chat_usage_limit_config WHERE singleton = TRUE LIMIT 1
`

func (q *sqlQuerier) GetChatUsageLimitConfig(ctx context.Context) (ChatUsageLimitConfig, error) {
	row := q.db.QueryRowContext(ctx, getChatUsageLimitConfig)
	var i ChatUsageLimitConfig
	err := row.Scan(
		&i.ID,
		&i.Singleton,
		&i.Enabled,
		&i.DefaultLimitMicros,
		&i.Period,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getChatUsageLimitGroupOverride = `-- name: GetChatUsageLimitGroupOverride :one
SELECT id AS group_id, chat_spend_limit_micros AS spend_limit_micros
FROM groups
WHERE id = $1::uuid AND chat_spend_limit_micros IS NOT NULL
`

type GetChatUsageLimitGroupOverrideRow struct {
	GroupID          uuid.UUID     `db:"group_id" json:"group_id"`
	SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"`
}

func (q *sqlQuerier) GetChatUsageLimitGroupOverride(ctx context.Context, groupID uuid.UUID) (GetChatUsageLimitGroupOverrideRow, error) {
	row := q.db.QueryRowContext(ctx, getChatUsageLimitGroupOverride, groupID)
	var i GetChatUsageLimitGroupOverrideRow
	err := row.Scan(&i.GroupID, &i.SpendLimitMicros)
	return i, err
}

const getChatUsageLimitUserOverride = `-- name: GetChatUsageLimitUserOverride :one
SELECT id AS user_id, chat_spend_limit_micros AS spend_limit_micros
FROM users
WHERE id = $1::uuid AND chat_spend_limit_micros IS NOT NULL
`

type GetChatUsageLimitUserOverrideRow struct {
	UserID           uuid.UUID     `db:"user_id" json:"user_id"`
	SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"`
}

func (q *sqlQuerier) GetChatUsageLimitUserOverride(ctx context.Context, userID uuid.UUID) (GetChatUsageLimitUserOverrideRow, error) {
	row := q.db.QueryRowContext(ctx, getChatUsageLimitUserOverride, userID)
	var i GetChatUsageLimitUserOverrideRow
	err := row.Scan(&i.UserID, &i.SpendLimitMicros)
	return i, err
}

const getChatUserPromptsByChatID = `-- name: GetChatUserPromptsByChatID :many
SELECT
    cm.id,
    string_agg(part->>'text', '' ORDER BY ordinality)::text AS text
FROM
    chat_messages cm,
    jsonb_array_elements(cm.content) WITH ORDINALITY AS t(part, ordinality)
WHERE
    cm.chat_id = $1::uuid
    AND cm.role = 'user'
    AND cm.deleted = false
    AND cm.visibility IN ('user', 'both')
    AND jsonb_typeof(cm.content) = 'array'
    AND part->>'type' = 'text'
GROUP BY
    cm.id
HAVING
    string_agg(part->>'text', '') ~ '\S'
ORDER BY
    cm.id DESC
LIMIT
    COALESCE(NULLIF($2::int, 0), 500)
`

type GetChatUserPromptsByChatIDParams struct {
	ChatID   uuid.UUID `db:"chat_id" json:"chat_id"`
	LimitVal int32     `db:"limit_val" json:"limit_val"`
}

type GetChatUserPromptsByChatIDRow struct {
	ID   int64  `db:"id" json:"id"`
	Text string `db:"text" json:"text"`
}

// Returns the concatenated text of each user-visible user prompt in a
// chat, newest first. Used by the composer to populate the up/down
// arrow prompt-history cycle. Non-text parts (tool calls, files,
// attachments, ...) are excluded; messages whose text payload is
// entirely whitespace are dropped so cycling never lands on a blank
// entry. The jsonb_typeof guard skips legacy V0 rows whose content is
// a scalar JSON string (predates migration 000434) so the lateral
// jsonb_array_elements never raises "cannot extract elements from a
// scalar". Backed by idx_chat_messages_user_prompts.
func (q *sqlQuerier) GetChatUserPromptsByChatID(ctx context.Context, arg GetChatUserPromptsByChatIDParams) ([]GetChatUserPromptsByChatIDRow, error) {
	rows, err := q.db.QueryContext(ctx, getChatUserPromptsByChatID, arg.ChatID, arg.LimitVal)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatUserPromptsByChatIDRow
	for rows.Next() {
		var i GetChatUserPromptsByChatIDRow
		if err := rows.Scan(&i.ID, &i.Text); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChats = `-- name: GetChats :many
WITH cursor_chat AS (
    SELECT
        pin_order,
        updated_at,
        id
    FROM chats
    WHERE id = $5
)
SELECT
    chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name,
    EXISTS (
        SELECT 1 FROM chat_messages cm
        WHERE cm.chat_id = chats_expanded.id
            AND cm.role = 'assistant'
            AND cm.deleted = false
            AND cm.id > COALESCE(chats_expanded.last_read_message_id, 0)
    ) AS has_unread
FROM
    chats_expanded
WHERE
    CASE
        WHEN $1::boolean THEN chats_expanded.owner_id = $2::uuid
        ELSE true
    END
    AND CASE
        WHEN $3::boolean THEN chats_expanded.owner_id != $2::uuid
        ELSE true
    END
    AND CASE
        WHEN $4 :: boolean IS NULL THEN true
        ELSE chats_expanded.archived = $4 :: boolean
    END
    AND CASE
        -- Cursor pagination: the last element on a page acts as the cursor.
        -- The 4-tuple matches the ORDER BY below. All columns sort DESC
        -- (pin_order is negated so lower values sort first in DESC order),
        -- which lets us use a single tuple < comparison.
        WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
            (CASE WHEN chats_expanded.pin_order > 0 THEN 1 ELSE 0 END, -chats_expanded.pin_order, chats_expanded.updated_at, chats_expanded.id) < (
                SELECT
                    CASE WHEN cursor_chat.pin_order > 0 THEN 1 ELSE 0 END,
                    -cursor_chat.pin_order,
                    cursor_chat.updated_at,
                    cursor_chat.id
                FROM
                    cursor_chat
            )
        )
        ELSE true
    END
    AND CASE
        WHEN $6::jsonb IS NOT NULL THEN chats_expanded.labels @> $6::jsonb
        ELSE true
    END
    -- Match chats whose linked diff URL (e.g. a pull request URL)
    -- equals the given value, case-insensitively. The URL may live on
    -- a delegated sub-agent's diff status, so we surface the root chat
    -- when any descendant matches.
    AND CASE
        WHEN $7::text IS NOT NULL THEN EXISTS (
            SELECT 1
            FROM chat_diff_statuses cds
            JOIN chats c2 ON c2.id = cds.chat_id
            WHERE cds.url IS NOT NULL
              AND cds.url <> ''
              AND LOWER(cds.url) = LOWER($7::text)
              AND (c2.id = chats_expanded.id OR c2.root_chat_id = chats_expanded.id)
        )
        ELSE true
    END
    -- Filter by title substring (case-insensitive). Applied when the
    -- caller provides a non-empty title_query.
    AND CASE
        WHEN $8 :: text != '' THEN chats_expanded.title ILIKE '%' || $8 || '%'
        ELSE true
    END
    AND CASE
        WHEN $9::boolean IS NOT NULL THEN (
            EXISTS (
                SELECT 1 FROM chat_messages cm
                WHERE cm.chat_id = chats_expanded.id
                    AND cm.role = 'assistant'
                    AND cm.deleted = false
                    AND cm.id > COALESCE(chats_expanded.last_read_message_id, 0)
            )
        ) = $9::boolean
        ELSE true
    END
    -- Filter by pull request status. Unlike the diff_url filter above,
    -- this intentionally checks only the root chat's own diff status.
    -- Child chats share the same workspace and git branch as their
    -- parent, so gitsync populates identical PR state on both; traversing
    -- descendants would be redundant.
    AND CASE
        WHEN COALESCE(array_length($10::text[], 1), 0) > 0 THEN EXISTS (
            SELECT 1
            FROM chat_diff_statuses cds
            WHERE cds.chat_id = chats_expanded.id
                AND (
                    CASE
                        WHEN cds.pull_request_state = 'open' AND cds.pull_request_draft THEN 'draft'
                        WHEN cds.pull_request_state = 'open' THEN 'open'
                        ELSE cds.pull_request_state
                    END
                ) = ANY($10::text[])
        )
        ELSE true
    END
    -- Filter by PR number (exact match on chat's diff status).
    AND CASE
        WHEN $11::int != 0 THEN EXISTS (
            SELECT 1
            FROM chat_diff_statuses cds
            WHERE cds.chat_id = chats_expanded.id
                AND cds.pr_number = $11
        )
        ELSE true
    END
    -- Filter by repository (substring match on remote origin or PR URL).
    AND CASE
        WHEN $12::text != '' THEN EXISTS (
            SELECT 1
            FROM chat_diff_statuses cds
            WHERE cds.chat_id = chats_expanded.id
                AND (
                    cds.git_remote_origin ILIKE '%' || $12 || '%'
                    OR cds.url ILIKE '%' || $12 || '%'
                )
        )
        ELSE true
    END
    -- Filter by pull request title (case-insensitive substring).
    AND CASE
        WHEN $13::text != '' THEN EXISTS (
            SELECT 1
            FROM chat_diff_statuses cds
            WHERE cds.chat_id = chats_expanded.id
                AND cds.pull_request_title ILIKE '%' || $13 || '%'
        )
        ELSE true
    END
    -- Paginate over root chats only. Children are fetched
    -- separately via GetChildChatsByParentIDs and embedded under
    -- each parent. Other callers that need the full set should
    -- use a narrower query (e.g. GetChatsByWorkspaceIDs).
    AND chats_expanded.parent_chat_id IS NULL
    -- Authorize Filter clause will be injected below in GetAuthorizedChats
    -- @authorize_filter
ORDER BY
    -- Pinned chats (pin_order > 0) sort before unpinned ones. Within
    -- pinned chats, lower pin_order values come first. The negation
    -- trick (-pin_order) keeps all sort columns DESC so the cursor
    -- tuple < comparison works with uniform direction.
    CASE WHEN chats_expanded.pin_order > 0 THEN 1 ELSE 0 END DESC,
    -chats_expanded.pin_order DESC,
    chats_expanded.updated_at DESC,
    chats_expanded.id DESC
OFFSET $14
LIMIT
    -- The chat list is unbounded and expected to grow large.
    -- Default to 50 to prevent accidental excessively large queries.
    COALESCE(NULLIF($15 :: int, 0), 50)
`

type GetChatsParams struct {
	OwnedOnly           bool                  `db:"owned_only" json:"owned_only"`
	ViewerID            uuid.UUID             `db:"viewer_id" json:"viewer_id"`
	SharedOnly          bool                  `db:"shared_only" json:"shared_only"`
	Archived            sql.NullBool          `db:"archived" json:"archived"`
	AfterID             uuid.UUID             `db:"after_id" json:"after_id"`
	LabelFilter         pqtype.NullRawMessage `db:"label_filter" json:"label_filter"`
	DiffURL             sql.NullString        `db:"diff_url" json:"diff_url"`
	TitleQuery          string                `db:"title_query" json:"title_query"`
	HasUnread           sql.NullBool          `db:"has_unread" json:"has_unread"`
	PullRequestStatuses []string              `db:"pull_request_statuses" json:"pull_request_statuses"`
	PrNumber            int32                 `db:"pr_number" json:"pr_number"`
	RepoQuery           string                `db:"repo_query" json:"repo_query"`
	PrTitleQuery        string                `db:"pr_title_query" json:"pr_title_query"`
	OffsetOpt           int32                 `db:"offset_opt" json:"offset_opt"`
	LimitOpt            int32                 `db:"limit_opt" json:"limit_opt"`
}

type GetChatsRow struct {
	Chat      Chat `db:"chat" json:"chat"`
	HasUnread bool `db:"has_unread" json:"has_unread"`
}

func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]GetChatsRow, error) {
	rows, err := q.db.QueryContext(ctx, getChats,
		arg.OwnedOnly,
		arg.ViewerID,
		arg.SharedOnly,
		arg.Archived,
		arg.AfterID,
		arg.LabelFilter,
		arg.DiffURL,
		arg.TitleQuery,
		arg.HasUnread,
		pq.Array(arg.PullRequestStatuses),
		arg.PrNumber,
		arg.RepoQuery,
		arg.PrTitleQuery,
		arg.OffsetOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatsRow
	for rows.Next() {
		var i GetChatsRow
		if err := rows.Scan(
			&i.Chat.ID,
			&i.Chat.OwnerID,
			&i.Chat.WorkspaceID,
			&i.Chat.Title,
			&i.Chat.Status,
			&i.Chat.WorkerID,
			&i.Chat.StartedAt,
			&i.Chat.HeartbeatAt,
			&i.Chat.CreatedAt,
			&i.Chat.UpdatedAt,
			&i.Chat.ParentChatID,
			&i.Chat.RootChatID,
			&i.Chat.LastModelConfigID,
			&i.Chat.Archived,
			&i.Chat.LastError,
			&i.Chat.Mode,
			pq.Array(&i.Chat.MCPServerIDs),
			&i.Chat.Labels,
			&i.Chat.BuildID,
			&i.Chat.AgentID,
			&i.Chat.PinOrder,
			&i.Chat.LastReadMessageID,
			&i.Chat.LastInjectedContext,
			&i.Chat.DynamicTools,
			&i.Chat.OrganizationID,
			&i.Chat.PlanMode,
			&i.Chat.ClientType,
			&i.Chat.LastTurnSummary,
			&i.Chat.UserACL,
			&i.Chat.GroupACL,
			&i.Chat.OwnerUsername,
			&i.Chat.OwnerName,
			&i.HasUnread,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatsByChatFileID = `-- name: GetChatsByChatFileID :many
SELECT
    id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM
    chats_expanded
WHERE
    id IN (
        SELECT chat_id
        FROM chat_file_links
        WHERE file_id = $1::uuid
    )
    -- Authorize Filter clause will be injected below in GetAuthorizedChatsByChatFileID.
    -- @authorize_filter
`

func (q *sqlQuerier) GetChatsByChatFileID(ctx context.Context, fileID uuid.UUID) ([]Chat, error) {
	rows, err := q.db.QueryContext(ctx, getChatsByChatFileID, fileID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Chat
	for rows.Next() {
		var i Chat
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.WorkspaceID,
			&i.Title,
			&i.Status,
			&i.WorkerID,
			&i.StartedAt,
			&i.HeartbeatAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentChatID,
			&i.RootChatID,
			&i.LastModelConfigID,
			&i.Archived,
			&i.LastError,
			&i.Mode,
			pq.Array(&i.MCPServerIDs),
			&i.Labels,
			&i.BuildID,
			&i.AgentID,
			&i.PinOrder,
			&i.LastReadMessageID,
			&i.LastInjectedContext,
			&i.DynamicTools,
			&i.OrganizationID,
			&i.PlanMode,
			&i.ClientType,
			&i.LastTurnSummary,
			&i.UserACL,
			&i.GroupACL,
			&i.OwnerUsername,
			&i.OwnerName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatsByWorkspaceIDs = `-- name: GetChatsByWorkspaceIDs :many
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
WHERE archived = false
  AND workspace_id = ANY($1::uuid[])
ORDER BY workspace_id, updated_at DESC
`

func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]Chat, error) {
	rows, err := q.db.QueryContext(ctx, getChatsByWorkspaceIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Chat
	for rows.Next() {
		var i Chat
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.WorkspaceID,
			&i.Title,
			&i.Status,
			&i.WorkerID,
			&i.StartedAt,
			&i.HeartbeatAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentChatID,
			&i.RootChatID,
			&i.LastModelConfigID,
			&i.Archived,
			&i.LastError,
			&i.Mode,
			pq.Array(&i.MCPServerIDs),
			&i.Labels,
			&i.BuildID,
			&i.AgentID,
			&i.PinOrder,
			&i.LastReadMessageID,
			&i.LastInjectedContext,
			&i.DynamicTools,
			&i.OrganizationID,
			&i.PlanMode,
			&i.ClientType,
			&i.LastTurnSummary,
			&i.UserACL,
			&i.GroupACL,
			&i.OwnerUsername,
			&i.OwnerName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChatsUpdatedAfter = `-- name: GetChatsUpdatedAfter :many
SELECT
    c.id, c.owner_id, c.created_at, c.updated_at, c.status,
    (c.parent_chat_id IS NOT NULL)::bool AS has_parent,
    c.root_chat_id, c.workspace_id,
    c.mode, c.archived, c.last_model_config_id, c.client_type,
    cds.pull_request_state
FROM chats c
LEFT JOIN chat_diff_statuses cds ON cds.chat_id = c.id
WHERE c.updated_at > $1
`

type GetChatsUpdatedAfterRow struct {
	ID                uuid.UUID      `db:"id" json:"id"`
	OwnerID           uuid.UUID      `db:"owner_id" json:"owner_id"`
	CreatedAt         time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt         time.Time      `db:"updated_at" json:"updated_at"`
	Status            ChatStatus     `db:"status" json:"status"`
	HasParent         bool           `db:"has_parent" json:"has_parent"`
	RootChatID        uuid.NullUUID  `db:"root_chat_id" json:"root_chat_id"`
	WorkspaceID       uuid.NullUUID  `db:"workspace_id" json:"workspace_id"`
	Mode              NullChatMode   `db:"mode" json:"mode"`
	Archived          bool           `db:"archived" json:"archived"`
	LastModelConfigID uuid.UUID      `db:"last_model_config_id" json:"last_model_config_id"`
	ClientType        ChatClientType `db:"client_type" json:"client_type"`
	PullRequestState  sql.NullString `db:"pull_request_state" json:"pull_request_state"`
}

// Retrieves chats updated after the given timestamp for telemetry
// snapshot collection. Uses updated_at so that long-running chats
// still appear in each snapshot window while they are active.
func (q *sqlQuerier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time.Time) ([]GetChatsUpdatedAfterRow, error) {
	rows, err := q.db.QueryContext(ctx, getChatsUpdatedAfter, updatedAfter)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChatsUpdatedAfterRow
	for rows.Next() {
		var i GetChatsUpdatedAfterRow
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Status,
			&i.HasParent,
			&i.RootChatID,
			&i.WorkspaceID,
			&i.Mode,
			&i.Archived,
			&i.LastModelConfigID,
			&i.ClientType,
			&i.PullRequestState,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getChildChatsByParentIDs = `-- name: GetChildChatsByParentIDs :many
SELECT
    chats_expanded.id, chats_expanded.owner_id, chats_expanded.workspace_id, chats_expanded.title, chats_expanded.status, chats_expanded.worker_id, chats_expanded.started_at, chats_expanded.heartbeat_at, chats_expanded.created_at, chats_expanded.updated_at, chats_expanded.parent_chat_id, chats_expanded.root_chat_id, chats_expanded.last_model_config_id, chats_expanded.archived, chats_expanded.last_error, chats_expanded.mode, chats_expanded.mcp_server_ids, chats_expanded.labels, chats_expanded.build_id, chats_expanded.agent_id, chats_expanded.pin_order, chats_expanded.last_read_message_id, chats_expanded.last_injected_context, chats_expanded.dynamic_tools, chats_expanded.organization_id, chats_expanded.plan_mode, chats_expanded.client_type, chats_expanded.last_turn_summary, chats_expanded.user_acl, chats_expanded.group_acl, chats_expanded.owner_username, chats_expanded.owner_name,
    EXISTS (
        SELECT 1 FROM chat_messages cm
        WHERE cm.chat_id = chats_expanded.id
            AND cm.role = 'assistant'
            AND cm.deleted = false
            AND cm.id > COALESCE(chats_expanded.last_read_message_id, 0)
    ) AS has_unread
FROM
    chats_expanded
WHERE
    chats_expanded.parent_chat_id = ANY($1 :: uuid[])
    AND CASE
        WHEN $2 :: boolean IS NULL THEN true
        ELSE chats_expanded.archived = $2 :: boolean
    END
ORDER BY
    chats_expanded.created_at DESC,
    chats_expanded.id DESC
`

type GetChildChatsByParentIDsParams struct {
	ParentIds []uuid.UUID  `db:"parent_ids" json:"parent_ids"`
	Archived  sql.NullBool `db:"archived" json:"archived"`
}

type GetChildChatsByParentIDsRow struct {
	Chat      Chat `db:"chat" json:"chat"`
	HasUnread bool `db:"has_unread" json:"has_unread"`
}

// Fetches child chats of the given parents, optionally filtered by
// archive state (NULL = all, true/false = match). The archive
// invariant (parent archived implies child archived) is enforced
// at write time, not here.
func (q *sqlQuerier) GetChildChatsByParentIDs(ctx context.Context, arg GetChildChatsByParentIDsParams) ([]GetChildChatsByParentIDsRow, error) {
	rows, err := q.db.QueryContext(ctx, getChildChatsByParentIDs, pq.Array(arg.ParentIds), arg.Archived)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetChildChatsByParentIDsRow
	for rows.Next() {
		var i GetChildChatsByParentIDsRow
		if err := rows.Scan(
			&i.Chat.ID,
			&i.Chat.OwnerID,
			&i.Chat.WorkspaceID,
			&i.Chat.Title,
			&i.Chat.Status,
			&i.Chat.WorkerID,
			&i.Chat.StartedAt,
			&i.Chat.HeartbeatAt,
			&i.Chat.CreatedAt,
			&i.Chat.UpdatedAt,
			&i.Chat.ParentChatID,
			&i.Chat.RootChatID,
			&i.Chat.LastModelConfigID,
			&i.Chat.Archived,
			&i.Chat.LastError,
			&i.Chat.Mode,
			pq.Array(&i.Chat.MCPServerIDs),
			&i.Chat.Labels,
			&i.Chat.BuildID,
			&i.Chat.AgentID,
			&i.Chat.PinOrder,
			&i.Chat.LastReadMessageID,
			&i.Chat.LastInjectedContext,
			&i.Chat.DynamicTools,
			&i.Chat.OrganizationID,
			&i.Chat.PlanMode,
			&i.Chat.ClientType,
			&i.Chat.LastTurnSummary,
			&i.Chat.UserACL,
			&i.Chat.GroupACL,
			&i.Chat.OwnerUsername,
			&i.Chat.OwnerName,
			&i.HasUnread,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getLastChatMessageByRole = `-- name: GetLastChatMessageByRole :one
SELECT
    id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id
FROM
    chat_messages
WHERE
    chat_id = $1::uuid
    AND role = $2::chat_message_role
    AND deleted = false
ORDER BY
    created_at DESC, id DESC
LIMIT
    1
`

type GetLastChatMessageByRoleParams struct {
	ChatID uuid.UUID       `db:"chat_id" json:"chat_id"`
	Role   ChatMessageRole `db:"role" json:"role"`
}

func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastChatMessageByRoleParams) (ChatMessage, error) {
	row := q.db.QueryRowContext(ctx, getLastChatMessageByRole, arg.ChatID, arg.Role)
	var i ChatMessage
	err := row.Scan(
		&i.ID,
		&i.ChatID,
		&i.ModelConfigID,
		&i.CreatedAt,
		&i.Role,
		&i.Content,
		&i.Visibility,
		&i.InputTokens,
		&i.OutputTokens,
		&i.TotalTokens,
		&i.ReasoningTokens,
		&i.CacheCreationTokens,
		&i.CacheReadTokens,
		&i.ContextLimit,
		&i.Compressed,
		&i.CreatedBy,
		&i.ContentVersion,
		&i.TotalCostMicros,
		&i.RuntimeMs,
		&i.Deleted,
		&i.ProviderResponseID,
		&i.APIKeyID,
	)
	return i, err
}

const getStaleChats = `-- name: GetStaleChats :many
SELECT
    id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM
    chats_expanded
WHERE
    (status = 'running'::chat_status
        AND heartbeat_at < $1::timestamptz)
    OR (status = 'requires_action'::chat_status
        AND updated_at < $1::timestamptz)
    OR (status = 'waiting'::chat_status
        AND updated_at < $1::timestamptz
        AND EXISTS (
            SELECT 1 FROM chat_queued_messages cqm
            WHERE cqm.chat_id = chats_expanded.id
        ))
`

// Find chats that appear stuck and need recovery:
//  1. Running chats whose heartbeat has expired (worker crash).
//  2. requires_action chats past the timeout threshold (client
//     disappeared).
//  3. Waiting chats with a non-empty queue and stale updated_at
//     (deferred-promote stranding when the worker dies before its
//     post-cancel cleanup runs).
func (q *sqlQuerier) GetStaleChats(ctx context.Context, staleThreshold time.Time) ([]Chat, error) {
	rows, err := q.db.QueryContext(ctx, getStaleChats, staleThreshold)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Chat
	for rows.Next() {
		var i Chat
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.WorkspaceID,
			&i.Title,
			&i.Status,
			&i.WorkerID,
			&i.StartedAt,
			&i.HeartbeatAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentChatID,
			&i.RootChatID,
			&i.LastModelConfigID,
			&i.Archived,
			&i.LastError,
			&i.Mode,
			pq.Array(&i.MCPServerIDs),
			&i.Labels,
			&i.BuildID,
			&i.AgentID,
			&i.PinOrder,
			&i.LastReadMessageID,
			&i.LastInjectedContext,
			&i.DynamicTools,
			&i.OrganizationID,
			&i.PlanMode,
			&i.ClientType,
			&i.LastTurnSummary,
			&i.UserACL,
			&i.GroupACL,
			&i.OwnerUsername,
			&i.OwnerName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUserChatSpendInPeriod = `-- name: GetUserChatSpendInPeriod :one
SELECT COALESCE(SUM(cm.total_cost_micros), 0)::bigint AS total_spend_micros
FROM chat_messages cm
JOIN chats c ON c.id = cm.chat_id
WHERE c.owner_id = $1::uuid
  AND ($2::uuid IS NULL
       OR c.organization_id = $2::uuid)
  AND cm.created_at >= $3::timestamptz
  AND cm.created_at < $4::timestamptz
  AND cm.total_cost_micros IS NOT NULL
`

type GetUserChatSpendInPeriodParams struct {
	UserID         uuid.UUID     `db:"user_id" json:"user_id"`
	OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
	StartTime      time.Time     `db:"start_time" json:"start_time"`
	EndTime        time.Time     `db:"end_time" json:"end_time"`
}

// Returns the total spend for a user in the given period.
// When organization_id is NULL, spend across all organizations is
// returned (global behavior). Otherwise only spend within the
// specified organization is included.
func (q *sqlQuerier) GetUserChatSpendInPeriod(ctx context.Context, arg GetUserChatSpendInPeriodParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, getUserChatSpendInPeriod,
		arg.UserID,
		arg.OrganizationID,
		arg.StartTime,
		arg.EndTime,
	)
	var total_spend_micros int64
	err := row.Scan(&total_spend_micros)
	return total_spend_micros, err
}

const getUserGroupSpendLimit = `-- name: GetUserGroupSpendLimit :one
SELECT COALESCE(MIN(g.chat_spend_limit_micros), -1)::bigint AS limit_micros
FROM groups g
JOIN group_members_expanded gme ON gme.group_id = g.id
WHERE gme.user_id = $1::uuid
  AND ($2::uuid IS NULL
       OR g.organization_id = $2::uuid)
  AND g.chat_spend_limit_micros IS NOT NULL
`

type GetUserGroupSpendLimitParams struct {
	UserID         uuid.UUID     `db:"user_id" json:"user_id"`
	OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
}

// Returns the minimum (most restrictive) group limit for a user.
// Returns -1 if no group limits match the specified scope.
// When organization_id is NULL, groups across all organizations are
// considered (global behavior). Otherwise only groups within the
// specified organization are considered.
func (q *sqlQuerier) GetUserGroupSpendLimit(ctx context.Context, arg GetUserGroupSpendLimitParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, getUserGroupSpendLimit, arg.UserID, arg.OrganizationID)
	var limit_micros int64
	err := row.Scan(&limit_micros)
	return limit_micros, err
}

const insertChat = `-- name: InsertChat :one
WITH inserted_chat AS (
INSERT INTO chats (
    organization_id,
    owner_id,
    workspace_id,
    build_id,
    agent_id,
    parent_chat_id,
    root_chat_id,
    last_model_config_id,
    title,
    mode,
    plan_mode,
    status,
    mcp_server_ids,
    labels,
    dynamic_tools,
    client_type
) VALUES (
    $1::uuid,
    $2::uuid,
    $3::uuid,
    $4::uuid,
    $5::uuid,
    $6::uuid,
    $7::uuid,
    $8::uuid,
    $9::text,
    $10::chat_mode,
    $11::chat_plan_mode,
    $12::chat_status,
    COALESCE($13::uuid[], '{}'::uuid[]),
    COALESCE($14::jsonb, '{}'::jsonb),
    $15::jsonb,
    $16::chat_client_type
)
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        inserted_chat.id,
        inserted_chat.owner_id,
        inserted_chat.workspace_id,
        inserted_chat.title,
        inserted_chat.status,
        inserted_chat.worker_id,
        inserted_chat.started_at,
        inserted_chat.heartbeat_at,
        inserted_chat.created_at,
        inserted_chat.updated_at,
        inserted_chat.parent_chat_id,
        inserted_chat.root_chat_id,
        inserted_chat.last_model_config_id,
        inserted_chat.archived,
        inserted_chat.last_error,
        inserted_chat.mode,
        inserted_chat.mcp_server_ids,
        inserted_chat.labels,
        inserted_chat.build_id,
        inserted_chat.agent_id,
        inserted_chat.pin_order,
        inserted_chat.last_read_message_id,
        inserted_chat.last_injected_context,
        inserted_chat.dynamic_tools,
        inserted_chat.organization_id,
        inserted_chat.plan_mode,
        inserted_chat.client_type,
        inserted_chat.last_turn_summary,
        COALESCE(root.user_acl, inserted_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, inserted_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        inserted_chat
    LEFT JOIN chats root ON root.id = COALESCE(inserted_chat.root_chat_id, inserted_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = inserted_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type InsertChatParams struct {
	OrganizationID    uuid.UUID             `db:"organization_id" json:"organization_id"`
	OwnerID           uuid.UUID             `db:"owner_id" json:"owner_id"`
	WorkspaceID       uuid.NullUUID         `db:"workspace_id" json:"workspace_id"`
	BuildID           uuid.NullUUID         `db:"build_id" json:"build_id"`
	AgentID           uuid.NullUUID         `db:"agent_id" json:"agent_id"`
	ParentChatID      uuid.NullUUID         `db:"parent_chat_id" json:"parent_chat_id"`
	RootChatID        uuid.NullUUID         `db:"root_chat_id" json:"root_chat_id"`
	LastModelConfigID uuid.UUID             `db:"last_model_config_id" json:"last_model_config_id"`
	Title             string                `db:"title" json:"title"`
	Mode              NullChatMode          `db:"mode" json:"mode"`
	PlanMode          NullChatPlanMode      `db:"plan_mode" json:"plan_mode"`
	Status            ChatStatus            `db:"status" json:"status"`
	MCPServerIDs      []uuid.UUID           `db:"mcp_server_ids" json:"mcp_server_ids"`
	Labels            pqtype.NullRawMessage `db:"labels" json:"labels"`
	DynamicTools      pqtype.NullRawMessage `db:"dynamic_tools" json:"dynamic_tools"`
	ClientType        ChatClientType        `db:"client_type" json:"client_type"`
}

func (q *sqlQuerier) InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, insertChat,
		arg.OrganizationID,
		arg.OwnerID,
		arg.WorkspaceID,
		arg.BuildID,
		arg.AgentID,
		arg.ParentChatID,
		arg.RootChatID,
		arg.LastModelConfigID,
		arg.Title,
		arg.Mode,
		arg.PlanMode,
		arg.Status,
		pq.Array(arg.MCPServerIDs),
		arg.Labels,
		arg.DynamicTools,
		arg.ClientType,
	)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const insertChatMessages = `-- name: InsertChatMessages :many
WITH updated_chat AS (
    UPDATE
        chats
    SET
        last_model_config_id = (
            SELECT val
            FROM UNNEST($4::uuid[])
                WITH ORDINALITY AS t(val, ord)
            WHERE val != '00000000-0000-0000-0000-000000000000'::uuid
            ORDER BY ord DESC
            LIMIT 1
        )
    WHERE
        id = $1::uuid
        AND EXISTS (
            SELECT 1
            FROM UNNEST($4::uuid[])
            WHERE unnest != '00000000-0000-0000-0000-000000000000'::uuid
        )
        AND chats.last_model_config_id IS DISTINCT FROM (
            SELECT val
            FROM UNNEST($4::uuid[])
                WITH ORDINALITY AS t(val, ord)
            WHERE val != '00000000-0000-0000-0000-000000000000'::uuid
            ORDER BY ord DESC
            LIMIT 1
        )
)
INSERT INTO chat_messages (
    chat_id,
    created_by,
    api_key_id,
    model_config_id,
    role,
    content,
    content_version,
    visibility,
    input_tokens,
    output_tokens,
    total_tokens,
    reasoning_tokens,
    cache_creation_tokens,
    cache_read_tokens,
    context_limit,
    compressed,
    total_cost_micros,
    runtime_ms,
    provider_response_id
)
SELECT
    $1::uuid,
    NULLIF(UNNEST($2::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid),
    NULLIF(UNNEST($3::text[]), ''),
    NULLIF(UNNEST($4::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid),
    UNNEST($5::chat_message_role[]),
    UNNEST($6::text[])::jsonb,
    UNNEST($7::smallint[]),
    UNNEST($8::chat_message_visibility[]),
    NULLIF(UNNEST($9::bigint[]), 0),
    NULLIF(UNNEST($10::bigint[]), 0),
    NULLIF(UNNEST($11::bigint[]), 0),
    NULLIF(UNNEST($12::bigint[]), 0),
    NULLIF(UNNEST($13::bigint[]), 0),
    NULLIF(UNNEST($14::bigint[]), 0),
    NULLIF(UNNEST($15::bigint[]), 0),
    UNNEST($16::boolean[]),
    NULLIF(UNNEST($17::bigint[]), 0),
    NULLIF(UNNEST($18::bigint[]), 0),
    NULLIF(UNNEST($19::text[]), '')
RETURNING
    id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id
`

type InsertChatMessagesParams struct {
	ChatID              uuid.UUID               `db:"chat_id" json:"chat_id"`
	CreatedBy           []uuid.UUID             `db:"created_by" json:"created_by"`
	APIKeyID            []string                `db:"api_key_id" json:"api_key_id"`
	ModelConfigID       []uuid.UUID             `db:"model_config_id" json:"model_config_id"`
	Role                []ChatMessageRole       `db:"role" json:"role"`
	Content             []string                `db:"content" json:"content"`
	ContentVersion      []int16                 `db:"content_version" json:"content_version"`
	Visibility          []ChatMessageVisibility `db:"visibility" json:"visibility"`
	InputTokens         []int64                 `db:"input_tokens" json:"input_tokens"`
	OutputTokens        []int64                 `db:"output_tokens" json:"output_tokens"`
	TotalTokens         []int64                 `db:"total_tokens" json:"total_tokens"`
	ReasoningTokens     []int64                 `db:"reasoning_tokens" json:"reasoning_tokens"`
	CacheCreationTokens []int64                 `db:"cache_creation_tokens" json:"cache_creation_tokens"`
	CacheReadTokens     []int64                 `db:"cache_read_tokens" json:"cache_read_tokens"`
	ContextLimit        []int64                 `db:"context_limit" json:"context_limit"`
	Compressed          []bool                  `db:"compressed" json:"compressed"`
	TotalCostMicros     []int64                 `db:"total_cost_micros" json:"total_cost_micros"`
	RuntimeMs           []int64                 `db:"runtime_ms" json:"runtime_ms"`
	ProviderResponseID  []string                `db:"provider_response_id" json:"provider_response_id"`
}

func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error) {
	rows, err := q.db.QueryContext(ctx, insertChatMessages,
		arg.ChatID,
		pq.Array(arg.CreatedBy),
		pq.Array(arg.APIKeyID),
		pq.Array(arg.ModelConfigID),
		pq.Array(arg.Role),
		pq.Array(arg.Content),
		pq.Array(arg.ContentVersion),
		pq.Array(arg.Visibility),
		pq.Array(arg.InputTokens),
		pq.Array(arg.OutputTokens),
		pq.Array(arg.TotalTokens),
		pq.Array(arg.ReasoningTokens),
		pq.Array(arg.CacheCreationTokens),
		pq.Array(arg.CacheReadTokens),
		pq.Array(arg.ContextLimit),
		pq.Array(arg.Compressed),
		pq.Array(arg.TotalCostMicros),
		pq.Array(arg.RuntimeMs),
		pq.Array(arg.ProviderResponseID),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ChatMessage
	for rows.Next() {
		var i ChatMessage
		if err := rows.Scan(
			&i.ID,
			&i.ChatID,
			&i.ModelConfigID,
			&i.CreatedAt,
			&i.Role,
			&i.Content,
			&i.Visibility,
			&i.InputTokens,
			&i.OutputTokens,
			&i.TotalTokens,
			&i.ReasoningTokens,
			&i.CacheCreationTokens,
			&i.CacheReadTokens,
			&i.ContextLimit,
			&i.Compressed,
			&i.CreatedBy,
			&i.ContentVersion,
			&i.TotalCostMicros,
			&i.RuntimeMs,
			&i.Deleted,
			&i.ProviderResponseID,
			&i.APIKeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertChatQueuedMessage = `-- name: InsertChatQueuedMessage :one
INSERT INTO chat_queued_messages (chat_id, content, model_config_id, api_key_id)
VALUES (
    $1,
    $2,
    $3::uuid,
    $4::text
)
RETURNING id, chat_id, content, created_at, model_config_id, api_key_id
`

type InsertChatQueuedMessageParams struct {
	ChatID        uuid.UUID       `db:"chat_id" json:"chat_id"`
	Content       json.RawMessage `db:"content" json:"content"`
	ModelConfigID uuid.NullUUID   `db:"model_config_id" json:"model_config_id"`
	APIKeyID      sql.NullString  `db:"api_key_id" json:"api_key_id"`
}

func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChatQueuedMessageParams) (ChatQueuedMessage, error) {
	row := q.db.QueryRowContext(ctx, insertChatQueuedMessage,
		arg.ChatID,
		arg.Content,
		arg.ModelConfigID,
		arg.APIKeyID,
	)
	var i ChatQueuedMessage
	err := row.Scan(
		&i.ID,
		&i.ChatID,
		&i.Content,
		&i.CreatedAt,
		&i.ModelConfigID,
		&i.APIKeyID,
	)
	return i, err
}

const linkChatFiles = `-- name: LinkChatFiles :one
WITH current AS (
    SELECT COUNT(*) AS cnt
    FROM chat_file_links
    WHERE chat_id = $1::uuid
),
new_links AS (
    SELECT $1::uuid AS chat_id, unnest($2::uuid[]) AS file_id
),
genuinely_new AS (
    SELECT nl.chat_id, nl.file_id
    FROM new_links nl
    WHERE NOT EXISTS (
        SELECT 1 FROM chat_file_links cfl
        WHERE cfl.chat_id = nl.chat_id AND cfl.file_id = nl.file_id
    )
),
inserted AS (
    INSERT INTO chat_file_links (chat_id, file_id)
    SELECT gn.chat_id, gn.file_id
    FROM genuinely_new gn, current c
    WHERE c.cnt + (SELECT COUNT(*) FROM genuinely_new) <= $3::int
    ON CONFLICT (chat_id, file_id) DO NOTHING
    RETURNING file_id
)
SELECT
    (SELECT COUNT(*)::int FROM genuinely_new) -
    (SELECT COUNT(*)::int FROM inserted) AS rejected_new_files
`

type LinkChatFilesParams struct {
	ChatID       uuid.UUID   `db:"chat_id" json:"chat_id"`
	FileIds      []uuid.UUID `db:"file_ids" json:"file_ids"`
	MaxFileLinks int32       `db:"max_file_links" json:"max_file_links"`
}

// LinkChatFiles inserts file associations into the chat_file_links
// join table with deduplication (ON CONFLICT DO NOTHING). The INSERT
// is conditional: it only proceeds when the total number of links
// (existing + genuinely new) does not exceed max_file_links. Returns
// the number of genuinely new file IDs that were NOT inserted due to
// the cap. A return value of 0 means all files were linked (or were
// already linked). A positive value means the cap blocked that many
// new links.
func (q *sqlQuerier) LinkChatFiles(ctx context.Context, arg LinkChatFilesParams) (int32, error) {
	row := q.db.QueryRowContext(ctx, linkChatFiles, arg.ChatID, pq.Array(arg.FileIds), arg.MaxFileLinks)
	var rejected_new_files int32
	err := row.Scan(&rejected_new_files)
	return rejected_new_files, err
}

const listChatUsageLimitGroupOverrides = `-- name: ListChatUsageLimitGroupOverrides :many
SELECT
    g.id AS group_id,
    g.name AS group_name,
    g.display_name AS group_display_name,
    g.avatar_url AS group_avatar_url,
    g.chat_spend_limit_micros AS spend_limit_micros,
    (SELECT COUNT(*)
        FROM group_members_expanded gme
        WHERE gme.group_id = g.id
          AND gme.user_is_system = FALSE) AS member_count
FROM groups g
WHERE g.chat_spend_limit_micros IS NOT NULL
ORDER BY g.name ASC
`

type ListChatUsageLimitGroupOverridesRow struct {
	GroupID          uuid.UUID     `db:"group_id" json:"group_id"`
	GroupName        string        `db:"group_name" json:"group_name"`
	GroupDisplayName string        `db:"group_display_name" json:"group_display_name"`
	GroupAvatarUrl   string        `db:"group_avatar_url" json:"group_avatar_url"`
	SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"`
	MemberCount      int64         `db:"member_count" json:"member_count"`
}

func (q *sqlQuerier) ListChatUsageLimitGroupOverrides(ctx context.Context) ([]ListChatUsageLimitGroupOverridesRow, error) {
	rows, err := q.db.QueryContext(ctx, listChatUsageLimitGroupOverrides)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListChatUsageLimitGroupOverridesRow
	for rows.Next() {
		var i ListChatUsageLimitGroupOverridesRow
		if err := rows.Scan(
			&i.GroupID,
			&i.GroupName,
			&i.GroupDisplayName,
			&i.GroupAvatarUrl,
			&i.SpendLimitMicros,
			&i.MemberCount,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listChatUsageLimitOverrides = `-- name: ListChatUsageLimitOverrides :many
SELECT u.id AS user_id, u.username, u.name, u.avatar_url,
       u.chat_spend_limit_micros AS spend_limit_micros
FROM users u
WHERE u.chat_spend_limit_micros IS NOT NULL
ORDER BY u.username ASC
`

type ListChatUsageLimitOverridesRow struct {
	UserID           uuid.UUID     `db:"user_id" json:"user_id"`
	Username         string        `db:"username" json:"username"`
	Name             string        `db:"name" json:"name"`
	AvatarURL        string        `db:"avatar_url" json:"avatar_url"`
	SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"`
}

func (q *sqlQuerier) ListChatUsageLimitOverrides(ctx context.Context) ([]ListChatUsageLimitOverridesRow, error) {
	rows, err := q.db.QueryContext(ctx, listChatUsageLimitOverrides)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListChatUsageLimitOverridesRow
	for rows.Next() {
		var i ListChatUsageLimitOverridesRow
		if err := rows.Scan(
			&i.UserID,
			&i.Username,
			&i.Name,
			&i.AvatarURL,
			&i.SpendLimitMicros,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const pinChatByID = `-- name: PinChatByID :exec
WITH target_chat AS (
    SELECT
        id,
        owner_id
    FROM
        chats
    WHERE
        id = $1::uuid
),
ranked AS (
    SELECT
        c.id,
        ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS next_pin_order
    FROM
        chats c
    JOIN
        target_chat ON c.owner_id = target_chat.owner_id
    WHERE
        c.pin_order > 0
        AND c.archived = FALSE
        AND c.id <> target_chat.id
),
updates AS (
    SELECT
        ranked.id,
        ranked.next_pin_order AS pin_order
    FROM
        ranked
    UNION ALL
    SELECT
        target_chat.id,
        COALESCE((
            SELECT
                MAX(ranked.next_pin_order)
            FROM
                ranked
        ), 0) + 1 AS pin_order
    FROM
        target_chat
)
UPDATE
    chats c
SET
    pin_order = updates.pin_order
FROM
    updates
WHERE
    c.id = updates.id
`

// Under READ COMMITTED, concurrent pin operations for the same
// owner may momentarily produce duplicate pin_order values because
// each CTE snapshot does not see the other's writes. The next
// pin/unpin/reorder operation's ROW_NUMBER() self-heals the
// sequence, so this is acceptable.
func (q *sqlQuerier) PinChatByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, pinChatByID, id)
	return err
}

const popNextQueuedMessage = `-- name: PopNextQueuedMessage :one
DELETE FROM chat_queued_messages
WHERE id = (
    SELECT cqm.id FROM chat_queued_messages cqm
    WHERE cqm.chat_id = $1
    ORDER BY cqm.created_at ASC, cqm.id ASC
    LIMIT 1
)
RETURNING id, chat_id, content, created_at, model_config_id, api_key_id
`

func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error) {
	row := q.db.QueryRowContext(ctx, popNextQueuedMessage, chatID)
	var i ChatQueuedMessage
	err := row.Scan(
		&i.ID,
		&i.ChatID,
		&i.Content,
		&i.CreatedAt,
		&i.ModelConfigID,
		&i.APIKeyID,
	)
	return i, err
}

const reorderChatQueuedMessageToFront = `-- name: ReorderChatQueuedMessageToFront :execrows
UPDATE chat_queued_messages AS target
SET created_at = (
    SELECT MIN(inner_cqm.created_at) - INTERVAL '1 microsecond'
    FROM chat_queued_messages AS inner_cqm
    WHERE inner_cqm.chat_id = $1
)
WHERE target.id = $2 AND target.chat_id = $1
`

type ReorderChatQueuedMessageToFrontParams struct {
	ChatID   uuid.UUID `db:"chat_id" json:"chat_id"`
	TargetID int64     `db:"target_id" json:"target_id"`
}

// Mutates only created_at on the target row; ids are unchanged so
// consumers can keep tracking queued messages by id.
func (q *sqlQuerier) ReorderChatQueuedMessageToFront(ctx context.Context, arg ReorderChatQueuedMessageToFrontParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, reorderChatQueuedMessageToFront, arg.ChatID, arg.TargetID)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const resolveUserChatSpendLimit = `-- name: ResolveUserChatSpendLimit :one
SELECT CASE
    WHEN NOT cfg.enabled THEN -1
    WHEN u.chat_spend_limit_micros IS NOT NULL THEN u.chat_spend_limit_micros
    WHEN gl.limit_micros IS NOT NULL THEN gl.limit_micros
    ELSE cfg.default_limit_micros
END::bigint AS effective_limit_micros,
CASE
    WHEN NOT cfg.enabled THEN 'disabled'
    WHEN u.chat_spend_limit_micros IS NOT NULL THEN 'user'
    WHEN gl.limit_micros IS NOT NULL THEN 'group'
    ELSE 'default'
END AS limit_source
FROM chat_usage_limit_config cfg
CROSS JOIN users u
LEFT JOIN LATERAL (
    SELECT MIN(g.chat_spend_limit_micros) AS limit_micros
    FROM groups g
    JOIN group_members_expanded gme ON gme.group_id = g.id
    WHERE gme.user_id = $1::uuid
      AND ($2::uuid IS NULL
           OR g.organization_id = $2::uuid)
      AND g.chat_spend_limit_micros IS NOT NULL
) gl ON TRUE
WHERE u.id = $1::uuid
LIMIT 1
`

type ResolveUserChatSpendLimitParams struct {
	UserID         uuid.UUID     `db:"user_id" json:"user_id"`
	OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
}

type ResolveUserChatSpendLimitRow struct {
	EffectiveLimitMicros int64  `db:"effective_limit_micros" json:"effective_limit_micros"`
	LimitSource          string `db:"limit_source" json:"limit_source"`
}

// Resolves the effective spend limit for a user using the hierarchy:
//  1. Individual user override (highest priority, applies globally across
//     all organizations since it lives on the users table)
//  2. Minimum group limit across the user's groups
//  3. Global default from config
//
// Returns -1 if limits are not enabled.
// When organization_id is NULL, groups across all organizations are
// considered (global behavior). Otherwise only groups within the
// specified organization are considered.
// limit_source indicates which tier won: 'user', 'group', 'default',
// or 'disabled'.
func (q *sqlQuerier) ResolveUserChatSpendLimit(ctx context.Context, arg ResolveUserChatSpendLimitParams) (ResolveUserChatSpendLimitRow, error) {
	row := q.db.QueryRowContext(ctx, resolveUserChatSpendLimit, arg.UserID, arg.OrganizationID)
	var i ResolveUserChatSpendLimitRow
	err := row.Scan(&i.EffectiveLimitMicros, &i.LimitSource)
	return i, err
}

const softDeleteChatMessageByID = `-- name: SoftDeleteChatMessageByID :exec
UPDATE
    chat_messages
SET
    deleted = true
WHERE
    id = $1::bigint
`

func (q *sqlQuerier) SoftDeleteChatMessageByID(ctx context.Context, id int64) error {
	_, err := q.db.ExecContext(ctx, softDeleteChatMessageByID, id)
	return err
}

const softDeleteChatMessagesAfterID = `-- name: SoftDeleteChatMessagesAfterID :exec
UPDATE
    chat_messages
SET
    deleted = true
WHERE
    chat_id = $1::uuid
    AND id > $2::bigint
`

type SoftDeleteChatMessagesAfterIDParams struct {
	ChatID  uuid.UUID `db:"chat_id" json:"chat_id"`
	AfterID int64     `db:"after_id" json:"after_id"`
}

func (q *sqlQuerier) SoftDeleteChatMessagesAfterID(ctx context.Context, arg SoftDeleteChatMessagesAfterIDParams) error {
	_, err := q.db.ExecContext(ctx, softDeleteChatMessagesAfterID, arg.ChatID, arg.AfterID)
	return err
}

const softDeleteContextFileMessages = `-- name: SoftDeleteContextFileMessages :exec
UPDATE chat_messages SET deleted = true
WHERE chat_id = $1::uuid
    AND deleted = false
    AND content::jsonb @> '[{"type": "context-file"}]'
`

func (q *sqlQuerier) SoftDeleteContextFileMessages(ctx context.Context, chatID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, softDeleteContextFileMessages, chatID)
	return err
}

const unarchiveChatByID = `-- name: UnarchiveChatByID :many
WITH updated_chats AS (
    UPDATE chats SET
        archived = false,
        updated_at = NOW()
    WHERE id = $1::uuid OR root_chat_id = $1::uuid
    RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chats.id,
        updated_chats.owner_id,
        updated_chats.workspace_id,
        updated_chats.title,
        updated_chats.status,
        updated_chats.worker_id,
        updated_chats.started_at,
        updated_chats.heartbeat_at,
        updated_chats.created_at,
        updated_chats.updated_at,
        updated_chats.parent_chat_id,
        updated_chats.root_chat_id,
        updated_chats.last_model_config_id,
        updated_chats.archived,
        updated_chats.last_error,
        updated_chats.mode,
        updated_chats.mcp_server_ids,
        updated_chats.labels,
        updated_chats.build_id,
        updated_chats.agent_id,
        updated_chats.pin_order,
        updated_chats.last_read_message_id,
        updated_chats.last_injected_context,
        updated_chats.dynamic_tools,
        updated_chats.organization_id,
        updated_chats.plan_mode,
        updated_chats.client_type,
        updated_chats.last_turn_summary,
        COALESCE(root.user_acl, updated_chats.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chats.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chats
    LEFT JOIN chats root ON root.id = COALESCE(updated_chats.root_chat_id, updated_chats.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chats.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
ORDER BY (chats_expanded.id = $1::uuid) DESC, chats_expanded.created_at ASC, chats_expanded.id ASC
`

// Unarchives a chat (and its children). Stale file references are
// handled automatically by FK cascades on chat_file_links: when
// dbpurge deletes a chat_files row, the corresponding
// chat_file_links rows are cascade-deleted by PostgreSQL.
func (q *sqlQuerier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, error) {
	rows, err := q.db.QueryContext(ctx, unarchiveChatByID, id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Chat
	for rows.Next() {
		var i Chat
		if err := rows.Scan(
			&i.ID,
			&i.OwnerID,
			&i.WorkspaceID,
			&i.Title,
			&i.Status,
			&i.WorkerID,
			&i.StartedAt,
			&i.HeartbeatAt,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ParentChatID,
			&i.RootChatID,
			&i.LastModelConfigID,
			&i.Archived,
			&i.LastError,
			&i.Mode,
			pq.Array(&i.MCPServerIDs),
			&i.Labels,
			&i.BuildID,
			&i.AgentID,
			&i.PinOrder,
			&i.LastReadMessageID,
			&i.LastInjectedContext,
			&i.DynamicTools,
			&i.OrganizationID,
			&i.PlanMode,
			&i.ClientType,
			&i.LastTurnSummary,
			&i.UserACL,
			&i.GroupACL,
			&i.OwnerUsername,
			&i.OwnerName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const unpinChatByID = `-- name: UnpinChatByID :exec
WITH target_chat AS (
    SELECT
        id,
        owner_id
    FROM
        chats
    WHERE
        id = $1::uuid
),
ranked AS (
    SELECT
        c.id,
        ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS current_position
    FROM
        chats c
    JOIN
        target_chat ON c.owner_id = target_chat.owner_id
    WHERE
        c.pin_order > 0
        AND c.archived = FALSE
),
target AS (
    SELECT
        ranked.id,
        ranked.current_position
    FROM
        ranked
    WHERE
        ranked.id = $1::uuid
),
updates AS (
    SELECT
        ranked.id,
        CASE
            WHEN ranked.id = target.id THEN 0
            WHEN ranked.current_position > target.current_position THEN ranked.current_position - 1
            ELSE ranked.current_position
        END AS pin_order
    FROM
        ranked
    CROSS JOIN
        target
)
UPDATE
    chats c
SET
    pin_order = updates.pin_order
FROM
    updates
WHERE
    c.id = updates.id
`

func (q *sqlQuerier) UnpinChatByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, unpinChatByID, id)
	return err
}

const updateChatACLByID = `-- name: UpdateChatACLByID :exec
UPDATE
    chats
SET
    user_acl = $1,
    group_acl = $2
WHERE
    id = $3::uuid
`

type UpdateChatACLByIDParams struct {
	UserACL  ChatACL   `db:"user_acl" json:"user_acl"`
	GroupACL ChatACL   `db:"group_acl" json:"group_acl"`
	ID       uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatACLByID(ctx context.Context, arg UpdateChatACLByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateChatACLByID, arg.UserACL, arg.GroupACL, arg.ID)
	return err
}

const updateChatBuildAgentBinding = `-- name: UpdateChatBuildAgentBinding :one
WITH updated_chat AS (
UPDATE chats SET
    build_id = $1::uuid,
    agent_id = $2::uuid,
    updated_at = NOW()
WHERE
    id = $3::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatBuildAgentBindingParams struct {
	BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
	AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
	ID      uuid.UUID     `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatBuildAgentBinding(ctx context.Context, arg UpdateChatBuildAgentBindingParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatBuildAgentBinding, arg.BuildID, arg.AgentID, arg.ID)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatByID = `-- name: UpdateChatByID :one
WITH updated_chat AS (
UPDATE
    chats
SET
    title = $1::text,
    updated_at = NOW()
WHERE
    id = $2::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatByIDParams struct {
	Title string    `db:"title" json:"title"`
	ID    uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatByID, arg.Title, arg.ID)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatHeartbeats = `-- name: UpdateChatHeartbeats :many
UPDATE
    chats
SET
    heartbeat_at = $1::timestamptz
WHERE
    id = ANY($2::uuid[])
    AND worker_id = $3::uuid
    AND status = 'running'::chat_status
RETURNING id
`

type UpdateChatHeartbeatsParams struct {
	Now      time.Time   `db:"now" json:"now"`
	IDs      []uuid.UUID `db:"ids" json:"ids"`
	WorkerID uuid.UUID   `db:"worker_id" json:"worker_id"`
}

// Bumps the heartbeat timestamp for the given set of chat IDs,
// provided they are still running and owned by the specified
// worker. Returns the IDs that were actually updated so the
// caller can detect stolen or completed chats via set-difference.
func (q *sqlQuerier) UpdateChatHeartbeats(ctx context.Context, arg UpdateChatHeartbeatsParams) ([]uuid.UUID, error) {
	rows, err := q.db.QueryContext(ctx, updateChatHeartbeats, arg.Now, pq.Array(arg.IDs), arg.WorkerID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []uuid.UUID
	for rows.Next() {
		var id uuid.UUID
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		items = append(items, id)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateChatLabelsByID = `-- name: UpdateChatLabelsByID :one
WITH updated_chat AS (
UPDATE
    chats
SET
    labels = $1::jsonb,
    updated_at = NOW()
WHERE
    id = $2::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatLabelsByIDParams struct {
	Labels json.RawMessage `db:"labels" json:"labels"`
	ID     uuid.UUID       `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLabelsByIDParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatLabelsByID, arg.Labels, arg.ID)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatLastInjectedContext = `-- name: UpdateChatLastInjectedContext :one
WITH updated_chat AS (
UPDATE chats SET
    last_injected_context = $1::jsonb
WHERE
    id = $2::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatLastInjectedContextParams struct {
	LastInjectedContext pqtype.NullRawMessage `db:"last_injected_context" json:"last_injected_context"`
	ID                  uuid.UUID             `db:"id" json:"id"`
}

// Updates the cached injected context parts (AGENTS.md +
// skills) on the chat row. Called only when context changes
// (first workspace attach or agent change). updated_at is
// intentionally not touched to avoid reordering the chat list.
func (q *sqlQuerier) UpdateChatLastInjectedContext(ctx context.Context, arg UpdateChatLastInjectedContextParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatLastInjectedContext, arg.LastInjectedContext, arg.ID)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatLastModelConfigByID = `-- name: UpdateChatLastModelConfigByID :one
WITH updated_chat AS (
UPDATE
    chats
SET
    -- NOTE: updated_at is intentionally NOT touched here to avoid changing list ordering.
    last_model_config_id = $1::uuid
WHERE
    id = $2::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatLastModelConfigByIDParams struct {
	LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
	ID                uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatLastModelConfigByID(ctx context.Context, arg UpdateChatLastModelConfigByIDParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatLastModelConfigByID, arg.LastModelConfigID, arg.ID)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatLastReadMessageID = `-- name: UpdateChatLastReadMessageID :exec
UPDATE chats
SET last_read_message_id = $1::bigint
WHERE id = $2::uuid
`

type UpdateChatLastReadMessageIDParams struct {
	LastReadMessageID int64     `db:"last_read_message_id" json:"last_read_message_id"`
	ID                uuid.UUID `db:"id" json:"id"`
}

// Updates the last read message ID for a chat. This is used to track
// which messages the owner has seen, enabling unread indicators.
func (q *sqlQuerier) UpdateChatLastReadMessageID(ctx context.Context, arg UpdateChatLastReadMessageIDParams) error {
	_, err := q.db.ExecContext(ctx, updateChatLastReadMessageID, arg.LastReadMessageID, arg.ID)
	return err
}

const updateChatLastTurnSummary = `-- name: UpdateChatLastTurnSummary :execrows
UPDATE chats
SET
    last_turn_summary = NULLIF(REGEXP_REPLACE(
        $1::text, '^[[:space:]]+|[[:space:]]+$', '', 'g'
    ), '')
WHERE
    id = $2::uuid
    AND updated_at = $3::timestamptz
`

type UpdateChatLastTurnSummaryParams struct {
	LastTurnSummary   sql.NullString `db:"last_turn_summary" json:"last_turn_summary"`
	ID                uuid.UUID      `db:"id" json:"id"`
	ExpectedUpdatedAt time.Time      `db:"expected_updated_at" json:"expected_updated_at"`
}

// Updates the cached last completed turn summary for sidebar display.
// Empty or whitespace-only summaries are stored as NULL here so direct
// query callers cannot accidentally persist blank sidebar text.
// This intentionally preserves updated_at. The staleness guard relies on
// every new-turn query, such as UpdateChatStatus and AcquireChats, bumping
// updated_at. Future chat-field updates that do not bump updated_at can let
// stale summaries persist. If this query ever bumps updated_at, later
// goroutine summary writes will be rejected as stale.
// Two summary workers using the same freshness marker are last-write-wins.
func (q *sqlQuerier) UpdateChatLastTurnSummary(ctx context.Context, arg UpdateChatLastTurnSummaryParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, updateChatLastTurnSummary, arg.LastTurnSummary, arg.ID, arg.ExpectedUpdatedAt)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const updateChatMCPServerIDs = `-- name: UpdateChatMCPServerIDs :one
WITH updated_chat AS (
UPDATE
    chats
SET
    mcp_server_ids = $1::uuid[],
    updated_at = NOW()
WHERE
    id = $2::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatMCPServerIDsParams struct {
	MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
	ID           uuid.UUID   `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatMCPServerIDsParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatMCPServerIDs, pq.Array(arg.MCPServerIDs), arg.ID)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatMessageByID = `-- name: UpdateChatMessageByID :one
UPDATE
    chat_messages
SET
    model_config_id = COALESCE($1::uuid, model_config_id),
    content = $2::jsonb
WHERE
    id = $3::bigint
RETURNING
    id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id
`

type UpdateChatMessageByIDParams struct {
	ModelConfigID uuid.NullUUID         `db:"model_config_id" json:"model_config_id"`
	Content       pqtype.NullRawMessage `db:"content" json:"content"`
	ID            int64                 `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMessageByIDParams) (ChatMessage, error) {
	row := q.db.QueryRowContext(ctx, updateChatMessageByID, arg.ModelConfigID, arg.Content, arg.ID)
	var i ChatMessage
	err := row.Scan(
		&i.ID,
		&i.ChatID,
		&i.ModelConfigID,
		&i.CreatedAt,
		&i.Role,
		&i.Content,
		&i.Visibility,
		&i.InputTokens,
		&i.OutputTokens,
		&i.TotalTokens,
		&i.ReasoningTokens,
		&i.CacheCreationTokens,
		&i.CacheReadTokens,
		&i.ContextLimit,
		&i.Compressed,
		&i.CreatedBy,
		&i.ContentVersion,
		&i.TotalCostMicros,
		&i.RuntimeMs,
		&i.Deleted,
		&i.ProviderResponseID,
		&i.APIKeyID,
	)
	return i, err
}

const updateChatPinOrder = `-- name: UpdateChatPinOrder :exec
WITH target_chat AS (
    SELECT
        id,
        owner_id
    FROM
        chats
    WHERE
        id = $1::uuid
),
ranked AS (
    SELECT
        c.id,
        ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS current_position,
        COUNT(*) OVER () :: integer AS pinned_count
    FROM
        chats c
    JOIN
        target_chat ON c.owner_id = target_chat.owner_id
    WHERE
        c.pin_order > 0
        AND c.archived = FALSE
),
target AS (
    SELECT
        ranked.id,
        ranked.current_position,
        LEAST(GREATEST($2::integer, 1), ranked.pinned_count) AS desired_position
    FROM
        ranked
    WHERE
        ranked.id = $1::uuid
),
updates AS (
    SELECT
        ranked.id,
        CASE
            WHEN ranked.id = target.id THEN target.desired_position
            WHEN target.desired_position < target.current_position
                AND ranked.current_position >= target.desired_position
                AND ranked.current_position < target.current_position THEN ranked.current_position + 1
            WHEN target.desired_position > target.current_position
                AND ranked.current_position > target.current_position
                AND ranked.current_position <= target.desired_position THEN ranked.current_position - 1
            ELSE ranked.current_position
        END AS pin_order
    FROM
        ranked
    CROSS JOIN
        target
)
UPDATE
    chats c
SET
    pin_order = updates.pin_order
FROM
    updates
WHERE
    c.id = updates.id
`

type UpdateChatPinOrderParams struct {
	ID       uuid.UUID `db:"id" json:"id"`
	PinOrder int32     `db:"pin_order" json:"pin_order"`
}

func (q *sqlQuerier) UpdateChatPinOrder(ctx context.Context, arg UpdateChatPinOrderParams) error {
	_, err := q.db.ExecContext(ctx, updateChatPinOrder, arg.ID, arg.PinOrder)
	return err
}

const updateChatPlanModeByID = `-- name: UpdateChatPlanModeByID :one
WITH updated_chat AS (
UPDATE
    chats
SET
    -- NOTE: updated_at is intentionally NOT touched here to avoid changing list ordering.
    plan_mode = $1::chat_plan_mode
WHERE
    id = $2::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatPlanModeByIDParams struct {
	PlanMode NullChatPlanMode `db:"plan_mode" json:"plan_mode"`
	ID       uuid.UUID        `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatPlanModeByID(ctx context.Context, arg UpdateChatPlanModeByIDParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatPlanModeByID, arg.PlanMode, arg.ID)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatStatus = `-- name: UpdateChatStatus :one
WITH updated_chat AS (
UPDATE
    chats
SET
    status = $1::chat_status,
    worker_id = $2::uuid,
    started_at = $3::timestamptz,
    heartbeat_at = $4::timestamptz,
    last_error = $5::jsonb,
    updated_at = NOW()
WHERE
    id = $6::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatStatusParams struct {
	Status      ChatStatus            `db:"status" json:"status"`
	WorkerID    uuid.NullUUID         `db:"worker_id" json:"worker_id"`
	StartedAt   sql.NullTime          `db:"started_at" json:"started_at"`
	HeartbeatAt sql.NullTime          `db:"heartbeat_at" json:"heartbeat_at"`
	LastError   pqtype.NullRawMessage `db:"last_error" json:"last_error"`
	ID          uuid.UUID             `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatStatus(ctx context.Context, arg UpdateChatStatusParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatStatus,
		arg.Status,
		arg.WorkerID,
		arg.StartedAt,
		arg.HeartbeatAt,
		arg.LastError,
		arg.ID,
	)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatStatusPreserveUpdatedAt = `-- name: UpdateChatStatusPreserveUpdatedAt :one
WITH updated_chat AS (
UPDATE
    chats
SET
    status = $1::chat_status,
    worker_id = $2::uuid,
    started_at = $3::timestamptz,
    heartbeat_at = $4::timestamptz,
    last_error = $5::jsonb,
    updated_at = $6::timestamptz
WHERE
    id = $7::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatStatusPreserveUpdatedAtParams struct {
	Status      ChatStatus            `db:"status" json:"status"`
	WorkerID    uuid.NullUUID         `db:"worker_id" json:"worker_id"`
	StartedAt   sql.NullTime          `db:"started_at" json:"started_at"`
	HeartbeatAt sql.NullTime          `db:"heartbeat_at" json:"heartbeat_at"`
	LastError   pqtype.NullRawMessage `db:"last_error" json:"last_error"`
	UpdatedAt   time.Time             `db:"updated_at" json:"updated_at"`
	ID          uuid.UUID             `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg UpdateChatStatusPreserveUpdatedAtParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatStatusPreserveUpdatedAt,
		arg.Status,
		arg.WorkerID,
		arg.StartedAt,
		arg.HeartbeatAt,
		arg.LastError,
		arg.UpdatedAt,
		arg.ID,
	)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatTitleByID = `-- name: UpdateChatTitleByID :one
WITH updated_chat AS (
UPDATE
    chats
SET
    -- NOTE: updated_at is intentionally NOT touched here to avoid
    -- changing list ordering when a user renames an older chat
    -- out-of-band.
    title = $1::text
WHERE
    id = $2::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatTitleByIDParams struct {
	Title string    `db:"title" json:"title"`
	ID    uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatTitleByID(ctx context.Context, arg UpdateChatTitleByIDParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatTitleByID, arg.Title, arg.ID)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const updateChatWorkspaceBinding = `-- name: UpdateChatWorkspaceBinding :one
WITH updated_chat AS (
UPDATE chats SET
    workspace_id = $1::uuid,
    build_id = $2::uuid,
    agent_id = $3::uuid,
    updated_at = NOW()
WHERE id = $4::uuid
RETURNING id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl
),
chats_expanded AS (
    SELECT
        updated_chat.id,
        updated_chat.owner_id,
        updated_chat.workspace_id,
        updated_chat.title,
        updated_chat.status,
        updated_chat.worker_id,
        updated_chat.started_at,
        updated_chat.heartbeat_at,
        updated_chat.created_at,
        updated_chat.updated_at,
        updated_chat.parent_chat_id,
        updated_chat.root_chat_id,
        updated_chat.last_model_config_id,
        updated_chat.archived,
        updated_chat.last_error,
        updated_chat.mode,
        updated_chat.mcp_server_ids,
        updated_chat.labels,
        updated_chat.build_id,
        updated_chat.agent_id,
        updated_chat.pin_order,
        updated_chat.last_read_message_id,
        updated_chat.last_injected_context,
        updated_chat.dynamic_tools,
        updated_chat.organization_id,
        updated_chat.plan_mode,
        updated_chat.client_type,
        updated_chat.last_turn_summary,
        COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
        COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
        owner.username AS owner_username,
        owner.name AS owner_name
    FROM
        updated_chat
    LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
    JOIN visible_users owner ON owner.id = updated_chat.owner_id
)
SELECT id, owner_id, workspace_id, title, status, worker_id, started_at, heartbeat_at, created_at, updated_at, parent_chat_id, root_chat_id, last_model_config_id, archived, last_error, mode, mcp_server_ids, labels, build_id, agent_id, pin_order, last_read_message_id, last_injected_context, dynamic_tools, organization_id, plan_mode, client_type, last_turn_summary, user_acl, group_acl, owner_username, owner_name
FROM chats_expanded
`

type UpdateChatWorkspaceBindingParams struct {
	WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
	BuildID     uuid.NullUUID `db:"build_id" json:"build_id"`
	AgentID     uuid.NullUUID `db:"agent_id" json:"agent_id"`
	ID          uuid.UUID     `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateChatWorkspaceBindingParams) (Chat, error) {
	row := q.db.QueryRowContext(ctx, updateChatWorkspaceBinding,
		arg.WorkspaceID,
		arg.BuildID,
		arg.AgentID,
		arg.ID,
	)
	var i Chat
	err := row.Scan(
		&i.ID,
		&i.OwnerID,
		&i.WorkspaceID,
		&i.Title,
		&i.Status,
		&i.WorkerID,
		&i.StartedAt,
		&i.HeartbeatAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ParentChatID,
		&i.RootChatID,
		&i.LastModelConfigID,
		&i.Archived,
		&i.LastError,
		&i.Mode,
		pq.Array(&i.MCPServerIDs),
		&i.Labels,
		&i.BuildID,
		&i.AgentID,
		&i.PinOrder,
		&i.LastReadMessageID,
		&i.LastInjectedContext,
		&i.DynamicTools,
		&i.OrganizationID,
		&i.PlanMode,
		&i.ClientType,
		&i.LastTurnSummary,
		&i.UserACL,
		&i.GroupACL,
		&i.OwnerUsername,
		&i.OwnerName,
	)
	return i, err
}

const upsertChatDiffStatus = `-- name: UpsertChatDiffStatus :one
INSERT INTO chat_diff_statuses (
    chat_id,
    url,
    pull_request_state,
    pull_request_title,
    pull_request_draft,
    changes_requested,
    additions,
    deletions,
    changed_files,
    author_login,
    author_avatar_url,
    base_branch,
    head_branch,
    pr_number,
    commits,
    approved,
    reviewer_count,
    refreshed_at,
    stale_at
) VALUES (
    $1::uuid,
    $2::text,
    $3::text,
    $4::text,
    $5::boolean,
    $6::boolean,
    $7::integer,
    $8::integer,
    $9::integer,
    $10::text,
    $11::text,
    $12::text,
    $13::text,
    $14::integer,
    $15::integer,
    $16::boolean,
    $17::integer,
    $18::timestamptz,
    $19::timestamptz
)
ON CONFLICT (chat_id) DO UPDATE
SET
    url = EXCLUDED.url,
    pull_request_state = EXCLUDED.pull_request_state,
    pull_request_title = EXCLUDED.pull_request_title,
    pull_request_draft = EXCLUDED.pull_request_draft,
    changes_requested = EXCLUDED.changes_requested,
    additions = EXCLUDED.additions,
    deletions = EXCLUDED.deletions,
    changed_files = EXCLUDED.changed_files,
    author_login = EXCLUDED.author_login,
    author_avatar_url = EXCLUDED.author_avatar_url,
    base_branch = EXCLUDED.base_branch,
    head_branch = EXCLUDED.head_branch,
    pr_number = EXCLUDED.pr_number,
    commits = EXCLUDED.commits,
    approved = EXCLUDED.approved,
    reviewer_count = EXCLUDED.reviewer_count,
    refreshed_at = EXCLUDED.refreshed_at,
    stale_at = EXCLUDED.stale_at,
    updated_at = NOW()
RETURNING
    chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
`

type UpsertChatDiffStatusParams struct {
	ChatID           uuid.UUID      `db:"chat_id" json:"chat_id"`
	Url              sql.NullString `db:"url" json:"url"`
	PullRequestState sql.NullString `db:"pull_request_state" json:"pull_request_state"`
	PullRequestTitle string         `db:"pull_request_title" json:"pull_request_title"`
	PullRequestDraft bool           `db:"pull_request_draft" json:"pull_request_draft"`
	ChangesRequested bool           `db:"changes_requested" json:"changes_requested"`
	Additions        int32          `db:"additions" json:"additions"`
	Deletions        int32          `db:"deletions" json:"deletions"`
	ChangedFiles     int32          `db:"changed_files" json:"changed_files"`
	AuthorLogin      sql.NullString `db:"author_login" json:"author_login"`
	AuthorAvatarUrl  sql.NullString `db:"author_avatar_url" json:"author_avatar_url"`
	BaseBranch       sql.NullString `db:"base_branch" json:"base_branch"`
	HeadBranch       sql.NullString `db:"head_branch" json:"head_branch"`
	PrNumber         sql.NullInt32  `db:"pr_number" json:"pr_number"`
	Commits          sql.NullInt32  `db:"commits" json:"commits"`
	Approved         sql.NullBool   `db:"approved" json:"approved"`
	ReviewerCount    sql.NullInt32  `db:"reviewer_count" json:"reviewer_count"`
	RefreshedAt      time.Time      `db:"refreshed_at" json:"refreshed_at"`
	StaleAt          time.Time      `db:"stale_at" json:"stale_at"`
}

func (q *sqlQuerier) UpsertChatDiffStatus(ctx context.Context, arg UpsertChatDiffStatusParams) (ChatDiffStatus, error) {
	row := q.db.QueryRowContext(ctx, upsertChatDiffStatus,
		arg.ChatID,
		arg.Url,
		arg.PullRequestState,
		arg.PullRequestTitle,
		arg.PullRequestDraft,
		arg.ChangesRequested,
		arg.Additions,
		arg.Deletions,
		arg.ChangedFiles,
		arg.AuthorLogin,
		arg.AuthorAvatarUrl,
		arg.BaseBranch,
		arg.HeadBranch,
		arg.PrNumber,
		arg.Commits,
		arg.Approved,
		arg.ReviewerCount,
		arg.RefreshedAt,
		arg.StaleAt,
	)
	var i ChatDiffStatus
	err := row.Scan(
		&i.ChatID,
		&i.Url,
		&i.PullRequestState,
		&i.ChangesRequested,
		&i.Additions,
		&i.Deletions,
		&i.ChangedFiles,
		&i.RefreshedAt,
		&i.StaleAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.GitBranch,
		&i.GitRemoteOrigin,
		&i.PullRequestTitle,
		&i.PullRequestDraft,
		&i.AuthorLogin,
		&i.AuthorAvatarUrl,
		&i.BaseBranch,
		&i.PrNumber,
		&i.Commits,
		&i.Approved,
		&i.ReviewerCount,
		&i.HeadBranch,
	)
	return i, err
}

const upsertChatDiffStatusReference = `-- name: UpsertChatDiffStatusReference :one
INSERT INTO chat_diff_statuses (
    chat_id,
    url,
    git_branch,
    git_remote_origin,
    stale_at
) VALUES (
    $1::uuid,
    $2::text,
    $3::text,
    $4::text,
    $5::timestamptz
)
ON CONFLICT (chat_id) DO UPDATE
SET
    url = CASE
        WHEN EXCLUDED.url IS NOT NULL THEN EXCLUDED.url
        ELSE chat_diff_statuses.url
    END,
    git_branch = CASE
        WHEN EXCLUDED.git_branch != '' THEN EXCLUDED.git_branch
        ELSE chat_diff_statuses.git_branch
    END,
    git_remote_origin = CASE
        WHEN EXCLUDED.git_remote_origin != '' THEN EXCLUDED.git_remote_origin
        ELSE chat_diff_statuses.git_remote_origin
    END,
    stale_at = EXCLUDED.stale_at,
    updated_at = NOW()
RETURNING
    chat_id, url, pull_request_state, changes_requested, additions, deletions, changed_files, refreshed_at, stale_at, created_at, updated_at, git_branch, git_remote_origin, pull_request_title, pull_request_draft, author_login, author_avatar_url, base_branch, pr_number, commits, approved, reviewer_count, head_branch
`

type UpsertChatDiffStatusReferenceParams struct {
	ChatID          uuid.UUID      `db:"chat_id" json:"chat_id"`
	Url             sql.NullString `db:"url" json:"url"`
	GitBranch       string         `db:"git_branch" json:"git_branch"`
	GitRemoteOrigin string         `db:"git_remote_origin" json:"git_remote_origin"`
	StaleAt         time.Time      `db:"stale_at" json:"stale_at"`
}

func (q *sqlQuerier) UpsertChatDiffStatusReference(ctx context.Context, arg UpsertChatDiffStatusReferenceParams) (ChatDiffStatus, error) {
	row := q.db.QueryRowContext(ctx, upsertChatDiffStatusReference,
		arg.ChatID,
		arg.Url,
		arg.GitBranch,
		arg.GitRemoteOrigin,
		arg.StaleAt,
	)
	var i ChatDiffStatus
	err := row.Scan(
		&i.ChatID,
		&i.Url,
		&i.PullRequestState,
		&i.ChangesRequested,
		&i.Additions,
		&i.Deletions,
		&i.ChangedFiles,
		&i.RefreshedAt,
		&i.StaleAt,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.GitBranch,
		&i.GitRemoteOrigin,
		&i.PullRequestTitle,
		&i.PullRequestDraft,
		&i.AuthorLogin,
		&i.AuthorAvatarUrl,
		&i.BaseBranch,
		&i.PrNumber,
		&i.Commits,
		&i.Approved,
		&i.ReviewerCount,
		&i.HeadBranch,
	)
	return i, err
}

const upsertChatUsageLimitConfig = `-- name: UpsertChatUsageLimitConfig :one
INSERT INTO chat_usage_limit_config (singleton, enabled, default_limit_micros, period, updated_at)
VALUES (TRUE, $1::boolean, $2::bigint, $3::text, NOW())
ON CONFLICT (singleton) DO UPDATE SET
    enabled = EXCLUDED.enabled,
    default_limit_micros = EXCLUDED.default_limit_micros,
    period = EXCLUDED.period,
    updated_at = NOW()
RETURNING id, singleton, enabled, default_limit_micros, period, created_at, updated_at
`

type UpsertChatUsageLimitConfigParams struct {
	Enabled            bool   `db:"enabled" json:"enabled"`
	DefaultLimitMicros int64  `db:"default_limit_micros" json:"default_limit_micros"`
	Period             string `db:"period" json:"period"`
}

func (q *sqlQuerier) UpsertChatUsageLimitConfig(ctx context.Context, arg UpsertChatUsageLimitConfigParams) (ChatUsageLimitConfig, error) {
	row := q.db.QueryRowContext(ctx, upsertChatUsageLimitConfig, arg.Enabled, arg.DefaultLimitMicros, arg.Period)
	var i ChatUsageLimitConfig
	err := row.Scan(
		&i.ID,
		&i.Singleton,
		&i.Enabled,
		&i.DefaultLimitMicros,
		&i.Period,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const upsertChatUsageLimitGroupOverride = `-- name: UpsertChatUsageLimitGroupOverride :one
UPDATE groups
SET chat_spend_limit_micros = $1::bigint
WHERE id = $2::uuid
RETURNING id AS group_id, name, display_name, avatar_url, chat_spend_limit_micros AS spend_limit_micros
`

type UpsertChatUsageLimitGroupOverrideParams struct {
	SpendLimitMicros int64     `db:"spend_limit_micros" json:"spend_limit_micros"`
	GroupID          uuid.UUID `db:"group_id" json:"group_id"`
}

type UpsertChatUsageLimitGroupOverrideRow struct {
	GroupID          uuid.UUID     `db:"group_id" json:"group_id"`
	Name             string        `db:"name" json:"name"`
	DisplayName      string        `db:"display_name" json:"display_name"`
	AvatarURL        string        `db:"avatar_url" json:"avatar_url"`
	SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"`
}

func (q *sqlQuerier) UpsertChatUsageLimitGroupOverride(ctx context.Context, arg UpsertChatUsageLimitGroupOverrideParams) (UpsertChatUsageLimitGroupOverrideRow, error) {
	row := q.db.QueryRowContext(ctx, upsertChatUsageLimitGroupOverride, arg.SpendLimitMicros, arg.GroupID)
	var i UpsertChatUsageLimitGroupOverrideRow
	err := row.Scan(
		&i.GroupID,
		&i.Name,
		&i.DisplayName,
		&i.AvatarURL,
		&i.SpendLimitMicros,
	)
	return i, err
}

const upsertChatUsageLimitUserOverride = `-- name: UpsertChatUsageLimitUserOverride :one
UPDATE users
SET chat_spend_limit_micros = $1::bigint
WHERE id = $2::uuid
RETURNING id AS user_id, username, name, avatar_url, chat_spend_limit_micros AS spend_limit_micros
`

type UpsertChatUsageLimitUserOverrideParams struct {
	SpendLimitMicros int64     `db:"spend_limit_micros" json:"spend_limit_micros"`
	UserID           uuid.UUID `db:"user_id" json:"user_id"`
}

type UpsertChatUsageLimitUserOverrideRow struct {
	UserID           uuid.UUID     `db:"user_id" json:"user_id"`
	Username         string        `db:"username" json:"username"`
	Name             string        `db:"name" json:"name"`
	AvatarURL        string        `db:"avatar_url" json:"avatar_url"`
	SpendLimitMicros sql.NullInt64 `db:"spend_limit_micros" json:"spend_limit_micros"`
}

func (q *sqlQuerier) UpsertChatUsageLimitUserOverride(ctx context.Context, arg UpsertChatUsageLimitUserOverrideParams) (UpsertChatUsageLimitUserOverrideRow, error) {
	row := q.db.QueryRowContext(ctx, upsertChatUsageLimitUserOverride, arg.SpendLimitMicros, arg.UserID)
	var i UpsertChatUsageLimitUserOverrideRow
	err := row.Scan(
		&i.UserID,
		&i.Username,
		&i.Name,
		&i.AvatarURL,
		&i.SpendLimitMicros,
	)
	return i, err
}

const batchUpsertConnectionLogs = `-- name: BatchUpsertConnectionLogs :exec
INSERT INTO connection_logs (
    id, connect_time, organization_id, workspace_owner_id, workspace_id,
    workspace_name, agent_name, type, code, ip, user_agent, user_id,
    slug_or_port, connection_id, disconnect_reason, disconnect_time
)
SELECT
    u.id,
    u.connect_time,
    u.organization_id,
    u.workspace_owner_id,
    u.workspace_id,
    u.workspace_name,
    u.agent_name,
    u.type,
    -- Use the validity flag to distinguish "no code" (NULL) from a
    -- legitimate zero exit code.
    CASE WHEN u.code_valid THEN u.code ELSE NULL END,
    u.ip,
    NULLIF(u.user_agent, ''),
    NULLIF(u.user_id, '00000000-0000-0000-0000-000000000000'::uuid),
    NULLIF(u.slug_or_port, ''),
    NULLIF(u.connection_id, '00000000-0000-0000-0000-000000000000'::uuid),
    NULLIF(u.disconnect_reason, ''),
    NULLIF(u.disconnect_time, '0001-01-01 00:00:00Z'::timestamptz)
FROM (
    SELECT
        unnest($1::uuid[]) AS id,
        unnest($2::timestamptz[]) AS connect_time,
        unnest($3::uuid[]) AS organization_id,
        unnest($4::uuid[]) AS workspace_owner_id,
        unnest($5::uuid[]) AS workspace_id,
        unnest($6::text[]) AS workspace_name,
        unnest($7::text[]) AS agent_name,
        unnest($8::connection_type[]) AS type,
        unnest($9::int4[]) AS code,
        unnest($10::bool[]) AS code_valid,
        unnest($11::inet[]) AS ip,
        unnest($12::text[]) AS user_agent,
        unnest($13::uuid[]) AS user_id,
        unnest($14::text[]) AS slug_or_port,
        unnest($15::uuid[]) AS connection_id,
        unnest($16::text[]) AS disconnect_reason,
        unnest($17::timestamptz[]) AS disconnect_time
) AS u
ON CONFLICT (connection_id, workspace_id, agent_name)
DO UPDATE SET
    -- Pick the earliest real connect_time. The zero sentinel
    -- ('0001-01-01') means the batch didn't know the connect_time
    -- (e.g. a pure disconnect event), so we keep the existing value.
    connect_time = CASE
        WHEN EXCLUDED.connect_time = '0001-01-01 00:00:00Z'::timestamptz
        THEN connection_logs.connect_time
        WHEN connection_logs.connect_time = '0001-01-01 00:00:00Z'::timestamptz
        THEN EXCLUDED.connect_time
        ELSE LEAST(connection_logs.connect_time, EXCLUDED.connect_time)
    END,
    disconnect_time = CASE
        WHEN connection_logs.disconnect_time IS NULL
        THEN EXCLUDED.disconnect_time
        ELSE connection_logs.disconnect_time
    END,
    disconnect_reason = CASE
        WHEN connection_logs.disconnect_reason IS NULL
        THEN EXCLUDED.disconnect_reason
        ELSE connection_logs.disconnect_reason
    END,
    code = CASE
        WHEN connection_logs.code IS NULL
        THEN EXCLUDED.code
        ELSE connection_logs.code
    END
`

type BatchUpsertConnectionLogsParams struct {
	ID               []uuid.UUID      `db:"id" json:"id"`
	ConnectTime      []time.Time      `db:"connect_time" json:"connect_time"`
	OrganizationID   []uuid.UUID      `db:"organization_id" json:"organization_id"`
	WorkspaceOwnerID []uuid.UUID      `db:"workspace_owner_id" json:"workspace_owner_id"`
	WorkspaceID      []uuid.UUID      `db:"workspace_id" json:"workspace_id"`
	WorkspaceName    []string         `db:"workspace_name" json:"workspace_name"`
	AgentName        []string         `db:"agent_name" json:"agent_name"`
	Type             []ConnectionType `db:"type" json:"type"`
	Code             []int32          `db:"code" json:"code"`
	CodeValid        []bool           `db:"code_valid" json:"code_valid"`
	Ip               []pqtype.Inet    `db:"ip" json:"ip"`
	UserAgent        []string         `db:"user_agent" json:"user_agent"`
	UserID           []uuid.UUID      `db:"user_id" json:"user_id"`
	SlugOrPort       []string         `db:"slug_or_port" json:"slug_or_port"`
	ConnectionID     []uuid.UUID      `db:"connection_id" json:"connection_id"`
	DisconnectReason []string         `db:"disconnect_reason" json:"disconnect_reason"`
	DisconnectTime   []time.Time      `db:"disconnect_time" json:"disconnect_time"`
}

func (q *sqlQuerier) BatchUpsertConnectionLogs(ctx context.Context, arg BatchUpsertConnectionLogsParams) error {
	_, err := q.db.ExecContext(ctx, batchUpsertConnectionLogs,
		pq.Array(arg.ID),
		pq.Array(arg.ConnectTime),
		pq.Array(arg.OrganizationID),
		pq.Array(arg.WorkspaceOwnerID),
		pq.Array(arg.WorkspaceID),
		pq.Array(arg.WorkspaceName),
		pq.Array(arg.AgentName),
		pq.Array(arg.Type),
		pq.Array(arg.Code),
		pq.Array(arg.CodeValid),
		pq.Array(arg.Ip),
		pq.Array(arg.UserAgent),
		pq.Array(arg.UserID),
		pq.Array(arg.SlugOrPort),
		pq.Array(arg.ConnectionID),
		pq.Array(arg.DisconnectReason),
		pq.Array(arg.DisconnectTime),
	)
	return err
}

const countConnectionLogs = `-- name: CountConnectionLogs :one
SELECT COUNT(*) AS count FROM (
	SELECT 1
	FROM
		connection_logs
	JOIN users AS workspace_owner ON
		connection_logs.workspace_owner_id = workspace_owner.id
	LEFT JOIN users ON
		connection_logs.user_id = users.id
	JOIN organizations ON
		connection_logs.organization_id = organizations.id
	WHERE
		-- Filter organization_id
		CASE
			WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
				connection_logs.organization_id = $1
			ELSE true
		END
		-- Filter by workspace owner username
		AND CASE
			WHEN $2 :: text != '' THEN
				workspace_owner_id = (
					SELECT id FROM users
					WHERE lower(username) = lower($2) AND deleted = false
				)
			ELSE true
		END
		-- Filter by workspace_owner_id
		AND CASE
			WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
				workspace_owner_id = $3
			ELSE true
		END
		-- Filter by workspace_owner_email
		AND CASE
			WHEN $4 :: text != '' THEN
				workspace_owner_id = (
					SELECT id FROM users
					WHERE email = $4 AND deleted = false
				)
			ELSE true
		END
		-- Filter by type
		AND CASE
			WHEN $5 :: text != '' THEN
				type = $5 :: connection_type
			ELSE true
		END
		-- Filter by user_id
		AND CASE
			WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
				user_id = $6
			ELSE true
		END
		-- Filter by username
		AND CASE
			WHEN $7 :: text != '' THEN
				user_id = (
					SELECT id FROM users
					WHERE lower(username) = lower($7) AND deleted = false
				)
			ELSE true
		END
		-- Filter by user_email
		AND CASE
			WHEN $8 :: text != '' THEN
				users.email = $8
			ELSE true
		END
		-- Filter by connected_after
		AND CASE
			WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
				connect_time >= $9
			ELSE true
		END
		-- Filter by connected_before
		AND CASE
			WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
				connect_time <= $10
			ELSE true
		END
		-- Filter by workspace_id
		AND CASE
			WHEN $11 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
				connection_logs.workspace_id = $11
			ELSE true
		END
		-- Filter by connection_id
		AND CASE
			WHEN $12 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
				connection_logs.connection_id = $12
			ELSE true
		END
		-- Filter by whether the session has a disconnect_time
		AND CASE
			WHEN $13 :: text != '' THEN
				(($13 = 'ongoing' AND disconnect_time IS NULL) OR
				($13 = 'completed' AND disconnect_time IS NOT NULL)) AND
				-- Exclude web events, since we don't know their close time.
				"type" NOT IN ('workspace_app', 'port_forwarding')
			ELSE true
		END
		-- Authorize Filter clause will be injected below in
		-- CountAuthorizedConnectionLogs
		-- @authorize_filter
	-- NOTE: See the CountAuditLogs LIMIT note.
	LIMIT NULLIF($14::int, 0) + 1
) AS limited_count
`

type CountConnectionLogsParams struct {
	OrganizationID      uuid.UUID `db:"organization_id" json:"organization_id"`
	WorkspaceOwner      string    `db:"workspace_owner" json:"workspace_owner"`
	WorkspaceOwnerID    uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"`
	WorkspaceOwnerEmail string    `db:"workspace_owner_email" json:"workspace_owner_email"`
	Type                string    `db:"type" json:"type"`
	UserID              uuid.UUID `db:"user_id" json:"user_id"`
	Username            string    `db:"username" json:"username"`
	UserEmail           string    `db:"user_email" json:"user_email"`
	ConnectedAfter      time.Time `db:"connected_after" json:"connected_after"`
	ConnectedBefore     time.Time `db:"connected_before" json:"connected_before"`
	WorkspaceID         uuid.UUID `db:"workspace_id" json:"workspace_id"`
	ConnectionID        uuid.UUID `db:"connection_id" json:"connection_id"`
	Status              string    `db:"status" json:"status"`
	CountCap            int32     `db:"count_cap" json:"count_cap"`
}

func (q *sqlQuerier) CountConnectionLogs(ctx context.Context, arg CountConnectionLogsParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, countConnectionLogs,
		arg.OrganizationID,
		arg.WorkspaceOwner,
		arg.WorkspaceOwnerID,
		arg.WorkspaceOwnerEmail,
		arg.Type,
		arg.UserID,
		arg.Username,
		arg.UserEmail,
		arg.ConnectedAfter,
		arg.ConnectedBefore,
		arg.WorkspaceID,
		arg.ConnectionID,
		arg.Status,
		arg.CountCap,
	)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const deleteOldConnectionLogs = `-- name: DeleteOldConnectionLogs :execrows
WITH old_logs AS (
	SELECT id
	FROM connection_logs
	WHERE connect_time < $1::timestamp with time zone
	ORDER BY connect_time ASC
	LIMIT $2
)
DELETE FROM connection_logs
USING old_logs
WHERE connection_logs.id = old_logs.id
`

type DeleteOldConnectionLogsParams struct {
	BeforeTime time.Time `db:"before_time" json:"before_time"`
	LimitCount int32     `db:"limit_count" json:"limit_count"`
}

func (q *sqlQuerier) DeleteOldConnectionLogs(ctx context.Context, arg DeleteOldConnectionLogsParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteOldConnectionLogs, arg.BeforeTime, arg.LimitCount)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const getConnectionLogsOffset = `-- name: GetConnectionLogsOffset :many
SELECT
	connection_logs.id, connection_logs.connect_time, connection_logs.organization_id, connection_logs.workspace_owner_id, connection_logs.workspace_id, connection_logs.workspace_name, connection_logs.agent_name, connection_logs.type, connection_logs.ip, connection_logs.code, connection_logs.user_agent, connection_logs.user_id, connection_logs.slug_or_port, connection_logs.connection_id, connection_logs.disconnect_time, connection_logs.disconnect_reason,
	-- sqlc.embed(users) would be nice but it does not seem to play well with
	-- left joins. This user metadata is necessary for parity with the audit logs
	-- API.
	users.username AS user_username,
	users.name AS user_name,
	users.email AS user_email,
	users.created_at AS user_created_at,
	users.updated_at AS user_updated_at,
	users.last_seen_at AS user_last_seen_at,
	users.status AS user_status,
	users.login_type AS user_login_type,
	users.rbac_roles AS user_roles,
	users.avatar_url AS user_avatar_url,
	users.deleted AS user_deleted,
	users.quiet_hours_schedule AS user_quiet_hours_schedule,
	workspace_owner.username AS workspace_owner_username,
	organizations.name AS organization_name,
	organizations.display_name AS organization_display_name,
	organizations.icon AS organization_icon
FROM
	connection_logs
JOIN users AS workspace_owner ON
	connection_logs.workspace_owner_id = workspace_owner.id
LEFT JOIN users ON
	connection_logs.user_id = users.id
JOIN organizations ON
	connection_logs.organization_id = organizations.id
WHERE
	-- Filter organization_id
	CASE
		WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			connection_logs.organization_id = $1
		ELSE true
	END
	-- Filter by workspace owner username
	AND CASE
		WHEN $2 :: text != '' THEN
			workspace_owner_id = (
				SELECT id FROM users
				WHERE lower(username) = lower($2) AND deleted = false
			)
		ELSE true
	END
	-- Filter by workspace_owner_id
	AND CASE
		WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			workspace_owner_id = $3
		ELSE true
	END
	-- Filter by workspace_owner_email
	AND CASE
		WHEN $4 :: text != '' THEN
			workspace_owner_id = (
				SELECT id FROM users
				WHERE email = $4 AND deleted = false
			)
		ELSE true
	END
	-- Filter by type
	AND CASE
		WHEN $5 :: text != '' THEN
			type = $5 :: connection_type
		ELSE true
	END
	-- Filter by user_id
	AND CASE
		WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			user_id = $6
		ELSE true
	END
	-- Filter by username
	AND CASE
		WHEN $7 :: text != '' THEN
			user_id = (
				SELECT id FROM users
				WHERE lower(username) = lower($7) AND deleted = false
			)
		ELSE true
	END
	-- Filter by user_email
	AND CASE
		WHEN $8 :: text != '' THEN
			users.email = $8
		ELSE true
	END
	-- Filter by connected_after
	AND CASE
		WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			connect_time >= $9
		ELSE true
	END
	-- Filter by connected_before
	AND CASE
		WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			connect_time <= $10
		ELSE true
	END
	-- Filter by workspace_id
	AND CASE
		WHEN $11 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			connection_logs.workspace_id = $11
		ELSE true
	END
	-- Filter by connection_id
	AND CASE
		WHEN $12 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			connection_logs.connection_id = $12
		ELSE true
	END
	-- Filter by whether the session has a disconnect_time
	AND CASE
		WHEN $13 :: text != '' THEN
			(($13 = 'ongoing' AND disconnect_time IS NULL) OR
			($13 = 'completed' AND disconnect_time IS NOT NULL)) AND
			-- Exclude web events, since we don't know their close time.
			"type" NOT IN ('workspace_app', 'port_forwarding')
		ELSE true
	END
	-- Authorize Filter clause will be injected below in
	-- GetAuthorizedConnectionLogsOffset
	-- @authorize_filter
ORDER BY
	connect_time DESC
LIMIT
	-- a limit of 0 means "no limit". The connection log table is unbounded
	-- in size, and is expected to be quite large. Implement a default
	-- limit of 100 to prevent accidental excessively large queries.
	COALESCE(NULLIF($15 :: int, 0), 100)
OFFSET
	$14
`

type GetConnectionLogsOffsetParams struct {
	OrganizationID      uuid.UUID `db:"organization_id" json:"organization_id"`
	WorkspaceOwner      string    `db:"workspace_owner" json:"workspace_owner"`
	WorkspaceOwnerID    uuid.UUID `db:"workspace_owner_id" json:"workspace_owner_id"`
	WorkspaceOwnerEmail string    `db:"workspace_owner_email" json:"workspace_owner_email"`
	Type                string    `db:"type" json:"type"`
	UserID              uuid.UUID `db:"user_id" json:"user_id"`
	Username            string    `db:"username" json:"username"`
	UserEmail           string    `db:"user_email" json:"user_email"`
	ConnectedAfter      time.Time `db:"connected_after" json:"connected_after"`
	ConnectedBefore     time.Time `db:"connected_before" json:"connected_before"`
	WorkspaceID         uuid.UUID `db:"workspace_id" json:"workspace_id"`
	ConnectionID        uuid.UUID `db:"connection_id" json:"connection_id"`
	Status              string    `db:"status" json:"status"`
	OffsetOpt           int32     `db:"offset_opt" json:"offset_opt"`
	LimitOpt            int32     `db:"limit_opt" json:"limit_opt"`
}

type GetConnectionLogsOffsetRow struct {
	ConnectionLog           ConnectionLog  `db:"connection_log" json:"connection_log"`
	UserUsername            sql.NullString `db:"user_username" json:"user_username"`
	UserName                sql.NullString `db:"user_name" json:"user_name"`
	UserEmail               sql.NullString `db:"user_email" json:"user_email"`
	UserCreatedAt           sql.NullTime   `db:"user_created_at" json:"user_created_at"`
	UserUpdatedAt           sql.NullTime   `db:"user_updated_at" json:"user_updated_at"`
	UserLastSeenAt          sql.NullTime   `db:"user_last_seen_at" json:"user_last_seen_at"`
	UserStatus              NullUserStatus `db:"user_status" json:"user_status"`
	UserLoginType           NullLoginType  `db:"user_login_type" json:"user_login_type"`
	UserRoles               pq.StringArray `db:"user_roles" json:"user_roles"`
	UserAvatarUrl           sql.NullString `db:"user_avatar_url" json:"user_avatar_url"`
	UserDeleted             sql.NullBool   `db:"user_deleted" json:"user_deleted"`
	UserQuietHoursSchedule  sql.NullString `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
	WorkspaceOwnerUsername  string         `db:"workspace_owner_username" json:"workspace_owner_username"`
	OrganizationName        string         `db:"organization_name" json:"organization_name"`
	OrganizationDisplayName string         `db:"organization_display_name" json:"organization_display_name"`
	OrganizationIcon        string         `db:"organization_icon" json:"organization_icon"`
}

func (q *sqlQuerier) GetConnectionLogsOffset(ctx context.Context, arg GetConnectionLogsOffsetParams) ([]GetConnectionLogsOffsetRow, error) {
	rows, err := q.db.QueryContext(ctx, getConnectionLogsOffset,
		arg.OrganizationID,
		arg.WorkspaceOwner,
		arg.WorkspaceOwnerID,
		arg.WorkspaceOwnerEmail,
		arg.Type,
		arg.UserID,
		arg.Username,
		arg.UserEmail,
		arg.ConnectedAfter,
		arg.ConnectedBefore,
		arg.WorkspaceID,
		arg.ConnectionID,
		arg.Status,
		arg.OffsetOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetConnectionLogsOffsetRow
	for rows.Next() {
		var i GetConnectionLogsOffsetRow
		if err := rows.Scan(
			&i.ConnectionLog.ID,
			&i.ConnectionLog.ConnectTime,
			&i.ConnectionLog.OrganizationID,
			&i.ConnectionLog.WorkspaceOwnerID,
			&i.ConnectionLog.WorkspaceID,
			&i.ConnectionLog.WorkspaceName,
			&i.ConnectionLog.AgentName,
			&i.ConnectionLog.Type,
			&i.ConnectionLog.Ip,
			&i.ConnectionLog.Code,
			&i.ConnectionLog.UserAgent,
			&i.ConnectionLog.UserID,
			&i.ConnectionLog.SlugOrPort,
			&i.ConnectionLog.ConnectionID,
			&i.ConnectionLog.DisconnectTime,
			&i.ConnectionLog.DisconnectReason,
			&i.UserUsername,
			&i.UserName,
			&i.UserEmail,
			&i.UserCreatedAt,
			&i.UserUpdatedAt,
			&i.UserLastSeenAt,
			&i.UserStatus,
			&i.UserLoginType,
			&i.UserRoles,
			&i.UserAvatarUrl,
			&i.UserDeleted,
			&i.UserQuietHoursSchedule,
			&i.WorkspaceOwnerUsername,
			&i.OrganizationName,
			&i.OrganizationDisplayName,
			&i.OrganizationIcon,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteCryptoKey = `-- name: DeleteCryptoKey :one
UPDATE crypto_keys
SET secret = NULL, secret_key_id = NULL
WHERE feature = $1 AND sequence = $2 RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`

type DeleteCryptoKeyParams struct {
	Feature  CryptoKeyFeature `db:"feature" json:"feature"`
	Sequence int32            `db:"sequence" json:"sequence"`
}

func (q *sqlQuerier) DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error) {
	row := q.db.QueryRowContext(ctx, deleteCryptoKey, arg.Feature, arg.Sequence)
	var i CryptoKey
	err := row.Scan(
		&i.Feature,
		&i.Sequence,
		&i.Secret,
		&i.SecretKeyID,
		&i.StartsAt,
		&i.DeletesAt,
	)
	return i, err
}

const getCryptoKeyByFeatureAndSequence = `-- name: GetCryptoKeyByFeatureAndSequence :one
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE feature = $1
  AND sequence = $2
  AND secret IS NOT NULL
`

type GetCryptoKeyByFeatureAndSequenceParams struct {
	Feature  CryptoKeyFeature `db:"feature" json:"feature"`
	Sequence int32            `db:"sequence" json:"sequence"`
}

func (q *sqlQuerier) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg GetCryptoKeyByFeatureAndSequenceParams) (CryptoKey, error) {
	row := q.db.QueryRowContext(ctx, getCryptoKeyByFeatureAndSequence, arg.Feature, arg.Sequence)
	var i CryptoKey
	err := row.Scan(
		&i.Feature,
		&i.Sequence,
		&i.Secret,
		&i.SecretKeyID,
		&i.StartsAt,
		&i.DeletesAt,
	)
	return i, err
}

const getCryptoKeys = `-- name: GetCryptoKeys :many
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE secret IS NOT NULL
`

func (q *sqlQuerier) GetCryptoKeys(ctx context.Context) ([]CryptoKey, error) {
	rows, err := q.db.QueryContext(ctx, getCryptoKeys)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []CryptoKey
	for rows.Next() {
		var i CryptoKey
		if err := rows.Scan(
			&i.Feature,
			&i.Sequence,
			&i.Secret,
			&i.SecretKeyID,
			&i.StartsAt,
			&i.DeletesAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getCryptoKeysByFeature = `-- name: GetCryptoKeysByFeature :many
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE feature = $1
AND secret IS NOT NULL
ORDER BY sequence DESC
`

func (q *sqlQuerier) GetCryptoKeysByFeature(ctx context.Context, feature CryptoKeyFeature) ([]CryptoKey, error) {
	rows, err := q.db.QueryContext(ctx, getCryptoKeysByFeature, feature)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []CryptoKey
	for rows.Next() {
		var i CryptoKey
		if err := rows.Scan(
			&i.Feature,
			&i.Sequence,
			&i.Secret,
			&i.SecretKeyID,
			&i.StartsAt,
			&i.DeletesAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getLatestCryptoKeyByFeature = `-- name: GetLatestCryptoKeyByFeature :one
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE feature = $1
ORDER BY sequence DESC
LIMIT 1
`

func (q *sqlQuerier) GetLatestCryptoKeyByFeature(ctx context.Context, feature CryptoKeyFeature) (CryptoKey, error) {
	row := q.db.QueryRowContext(ctx, getLatestCryptoKeyByFeature, feature)
	var i CryptoKey
	err := row.Scan(
		&i.Feature,
		&i.Sequence,
		&i.Secret,
		&i.SecretKeyID,
		&i.StartsAt,
		&i.DeletesAt,
	)
	return i, err
}

const insertCryptoKey = `-- name: InsertCryptoKey :one
INSERT INTO crypto_keys (
    feature,
    sequence,
    secret,
    starts_at,
    secret_key_id
) VALUES (
    $1,
    $2,
    $3,
    $4,
    $5
) RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`

type InsertCryptoKeyParams struct {
	Feature     CryptoKeyFeature `db:"feature" json:"feature"`
	Sequence    int32            `db:"sequence" json:"sequence"`
	Secret      sql.NullString   `db:"secret" json:"secret"`
	StartsAt    time.Time        `db:"starts_at" json:"starts_at"`
	SecretKeyID sql.NullString   `db:"secret_key_id" json:"secret_key_id"`
}

func (q *sqlQuerier) InsertCryptoKey(ctx context.Context, arg InsertCryptoKeyParams) (CryptoKey, error) {
	row := q.db.QueryRowContext(ctx, insertCryptoKey,
		arg.Feature,
		arg.Sequence,
		arg.Secret,
		arg.StartsAt,
		arg.SecretKeyID,
	)
	var i CryptoKey
	err := row.Scan(
		&i.Feature,
		&i.Sequence,
		&i.Secret,
		&i.SecretKeyID,
		&i.StartsAt,
		&i.DeletesAt,
	)
	return i, err
}

const updateCryptoKeyDeletesAt = `-- name: UpdateCryptoKeyDeletesAt :one
UPDATE crypto_keys
SET deletes_at = $3
WHERE feature = $1 AND sequence = $2 RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`

type UpdateCryptoKeyDeletesAtParams struct {
	Feature   CryptoKeyFeature `db:"feature" json:"feature"`
	Sequence  int32            `db:"sequence" json:"sequence"`
	DeletesAt sql.NullTime     `db:"deletes_at" json:"deletes_at"`
}

func (q *sqlQuerier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error) {
	row := q.db.QueryRowContext(ctx, updateCryptoKeyDeletesAt, arg.Feature, arg.Sequence, arg.DeletesAt)
	var i CryptoKey
	err := row.Scan(
		&i.Feature,
		&i.Sequence,
		&i.Secret,
		&i.SecretKeyID,
		&i.StartsAt,
		&i.DeletesAt,
	)
	return i, err
}

const getDBCryptKeys = `-- name: GetDBCryptKeys :many
SELECT number, active_key_digest, revoked_key_digest, created_at, revoked_at, test FROM dbcrypt_keys ORDER BY number ASC
`

func (q *sqlQuerier) GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error) {
	rows, err := q.db.QueryContext(ctx, getDBCryptKeys)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []DBCryptKey
	for rows.Next() {
		var i DBCryptKey
		if err := rows.Scan(
			&i.Number,
			&i.ActiveKeyDigest,
			&i.RevokedKeyDigest,
			&i.CreatedAt,
			&i.RevokedAt,
			&i.Test,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertDBCryptKey = `-- name: InsertDBCryptKey :exec
INSERT INTO dbcrypt_keys
	(number, active_key_digest, created_at, test)
VALUES ($1::int, $2::text, CURRENT_TIMESTAMP, $3::text)
`

type InsertDBCryptKeyParams struct {
	Number          int32  `db:"number" json:"number"`
	ActiveKeyDigest string `db:"active_key_digest" json:"active_key_digest"`
	Test            string `db:"test" json:"test"`
}

func (q *sqlQuerier) InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error {
	_, err := q.db.ExecContext(ctx, insertDBCryptKey, arg.Number, arg.ActiveKeyDigest, arg.Test)
	return err
}

const revokeDBCryptKey = `-- name: RevokeDBCryptKey :exec
UPDATE dbcrypt_keys
SET
	revoked_key_digest = active_key_digest,
	active_key_digest = revoked_key_digest,
	revoked_at = CURRENT_TIMESTAMP
WHERE
	active_key_digest = $1::text
AND
	revoked_key_digest IS NULL
`

func (q *sqlQuerier) RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error {
	_, err := q.db.ExecContext(ctx, revokeDBCryptKey, activeKeyDigest)
	return err
}

const deleteExternalAuthLink = `-- name: DeleteExternalAuthLink :exec
DELETE FROM external_auth_links WHERE provider_id = $1 AND user_id = $2
`

type DeleteExternalAuthLinkParams struct {
	ProviderID string    `db:"provider_id" json:"provider_id"`
	UserID     uuid.UUID `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error {
	_, err := q.db.ExecContext(ctx, deleteExternalAuthLink, arg.ProviderID, arg.UserID)
	return err
}

const getExternalAuthLink = `-- name: GetExternalAuthLink :one
SELECT provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra, oauth_refresh_failure_reason FROM external_auth_links WHERE provider_id = $1 AND user_id = $2
`

type GetExternalAuthLinkParams struct {
	ProviderID string    `db:"provider_id" json:"provider_id"`
	UserID     uuid.UUID `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error) {
	row := q.db.QueryRowContext(ctx, getExternalAuthLink, arg.ProviderID, arg.UserID)
	var i ExternalAuthLink
	err := row.Scan(
		&i.ProviderID,
		&i.UserID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OAuthAccessToken,
		&i.OAuthRefreshToken,
		&i.OAuthExpiry,
		&i.OAuthAccessTokenKeyID,
		&i.OAuthRefreshTokenKeyID,
		&i.OAuthExtra,
		&i.OauthRefreshFailureReason,
	)
	return i, err
}

const getExternalAuthLinksByUserID = `-- name: GetExternalAuthLinksByUserID :many
SELECT provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra, oauth_refresh_failure_reason FROM external_auth_links WHERE user_id = $1
`

func (q *sqlQuerier) GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error) {
	rows, err := q.db.QueryContext(ctx, getExternalAuthLinksByUserID, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ExternalAuthLink
	for rows.Next() {
		var i ExternalAuthLink
		if err := rows.Scan(
			&i.ProviderID,
			&i.UserID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.OAuthAccessToken,
			&i.OAuthRefreshToken,
			&i.OAuthExpiry,
			&i.OAuthAccessTokenKeyID,
			&i.OAuthRefreshTokenKeyID,
			&i.OAuthExtra,
			&i.OauthRefreshFailureReason,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertExternalAuthLink = `-- name: InsertExternalAuthLink :one
INSERT INTO external_auth_links (
    provider_id,
    user_id,
    created_at,
    updated_at,
    oauth_access_token,
    oauth_access_token_key_id,
    oauth_refresh_token,
    oauth_refresh_token_key_id,
    oauth_expiry,
	oauth_extra
) VALUES (
    $1,
    $2,
    $3,
    $4,
    $5,
    $6,
    $7,
    $8,
    $9,
	$10
) RETURNING provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra, oauth_refresh_failure_reason
`

type InsertExternalAuthLinkParams struct {
	ProviderID             string                `db:"provider_id" json:"provider_id"`
	UserID                 uuid.UUID             `db:"user_id" json:"user_id"`
	CreatedAt              time.Time             `db:"created_at" json:"created_at"`
	UpdatedAt              time.Time             `db:"updated_at" json:"updated_at"`
	OAuthAccessToken       string                `db:"oauth_access_token" json:"oauth_access_token"`
	OAuthAccessTokenKeyID  sql.NullString        `db:"oauth_access_token_key_id" json:"oauth_access_token_key_id"`
	OAuthRefreshToken      string                `db:"oauth_refresh_token" json:"oauth_refresh_token"`
	OAuthRefreshTokenKeyID sql.NullString        `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
	OAuthExpiry            time.Time             `db:"oauth_expiry" json:"oauth_expiry"`
	OAuthExtra             pqtype.NullRawMessage `db:"oauth_extra" json:"oauth_extra"`
}

func (q *sqlQuerier) InsertExternalAuthLink(ctx context.Context, arg InsertExternalAuthLinkParams) (ExternalAuthLink, error) {
	row := q.db.QueryRowContext(ctx, insertExternalAuthLink,
		arg.ProviderID,
		arg.UserID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.OAuthAccessToken,
		arg.OAuthAccessTokenKeyID,
		arg.OAuthRefreshToken,
		arg.OAuthRefreshTokenKeyID,
		arg.OAuthExpiry,
		arg.OAuthExtra,
	)
	var i ExternalAuthLink
	err := row.Scan(
		&i.ProviderID,
		&i.UserID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OAuthAccessToken,
		&i.OAuthRefreshToken,
		&i.OAuthExpiry,
		&i.OAuthAccessTokenKeyID,
		&i.OAuthRefreshTokenKeyID,
		&i.OAuthExtra,
		&i.OauthRefreshFailureReason,
	)
	return i, err
}

const updateExternalAuthLink = `-- name: UpdateExternalAuthLink :one
UPDATE external_auth_links SET
    updated_at = $3,
    oauth_access_token = $4,
    oauth_access_token_key_id = $5,
    oauth_refresh_token = $6,
    oauth_refresh_token_key_id = $7,
    oauth_expiry = $8,
	oauth_extra = $9,
	-- Only 'UpdateExternalAuthLinkRefreshToken' supports updating the oauth_refresh_failure_reason.
	-- Any updates to the external auth link, will be assumed to change the state and clear
	-- any cached errors.
	oauth_refresh_failure_reason = ''
WHERE provider_id = $1 AND user_id = $2 RETURNING provider_id, user_id, created_at, updated_at, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, oauth_extra, oauth_refresh_failure_reason
`

type UpdateExternalAuthLinkParams struct {
	ProviderID             string                `db:"provider_id" json:"provider_id"`
	UserID                 uuid.UUID             `db:"user_id" json:"user_id"`
	UpdatedAt              time.Time             `db:"updated_at" json:"updated_at"`
	OAuthAccessToken       string                `db:"oauth_access_token" json:"oauth_access_token"`
	OAuthAccessTokenKeyID  sql.NullString        `db:"oauth_access_token_key_id" json:"oauth_access_token_key_id"`
	OAuthRefreshToken      string                `db:"oauth_refresh_token" json:"oauth_refresh_token"`
	OAuthRefreshTokenKeyID sql.NullString        `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
	OAuthExpiry            time.Time             `db:"oauth_expiry" json:"oauth_expiry"`
	OAuthExtra             pqtype.NullRawMessage `db:"oauth_extra" json:"oauth_extra"`
}

func (q *sqlQuerier) UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error) {
	row := q.db.QueryRowContext(ctx, updateExternalAuthLink,
		arg.ProviderID,
		arg.UserID,
		arg.UpdatedAt,
		arg.OAuthAccessToken,
		arg.OAuthAccessTokenKeyID,
		arg.OAuthRefreshToken,
		arg.OAuthRefreshTokenKeyID,
		arg.OAuthExpiry,
		arg.OAuthExtra,
	)
	var i ExternalAuthLink
	err := row.Scan(
		&i.ProviderID,
		&i.UserID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OAuthAccessToken,
		&i.OAuthRefreshToken,
		&i.OAuthExpiry,
		&i.OAuthAccessTokenKeyID,
		&i.OAuthRefreshTokenKeyID,
		&i.OAuthExtra,
		&i.OauthRefreshFailureReason,
	)
	return i, err
}

const updateExternalAuthLinkRefreshToken = `-- name: UpdateExternalAuthLinkRefreshToken :exec
UPDATE
	external_auth_links
SET
	-- oauth_refresh_failure_reason can be set to cache the failure reason
	-- for subsequent refresh attempts.
	oauth_refresh_failure_reason = $1,
	oauth_refresh_token = $2,
	updated_at = $3
WHERE
    provider_id = $4
AND
    user_id = $5
AND
    oauth_refresh_token = $6
AND
    -- Required for sqlc to generate a parameter for the oauth_refresh_token_key_id
    $7 :: text = $7 :: text
`

type UpdateExternalAuthLinkRefreshTokenParams struct {
	OauthRefreshFailureReason string    `db:"oauth_refresh_failure_reason" json:"oauth_refresh_failure_reason"`
	OAuthRefreshToken         string    `db:"oauth_refresh_token" json:"oauth_refresh_token"`
	UpdatedAt                 time.Time `db:"updated_at" json:"updated_at"`
	ProviderID                string    `db:"provider_id" json:"provider_id"`
	UserID                    uuid.UUID `db:"user_id" json:"user_id"`
	OldOauthRefreshToken      string    `db:"old_oauth_refresh_token" json:"old_oauth_refresh_token"`
	OAuthRefreshTokenKeyID    string    `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
}

// Optimistic lock: only update the row if the refresh token in the database
// still matches the one we read before attempting the refresh. This prevents
// a concurrent caller that lost a token-refresh race from overwriting a valid
// token stored by the winner.
func (q *sqlQuerier) UpdateExternalAuthLinkRefreshToken(ctx context.Context, arg UpdateExternalAuthLinkRefreshTokenParams) error {
	_, err := q.db.ExecContext(ctx, updateExternalAuthLinkRefreshToken,
		arg.OauthRefreshFailureReason,
		arg.OAuthRefreshToken,
		arg.UpdatedAt,
		arg.ProviderID,
		arg.UserID,
		arg.OldOauthRefreshToken,
		arg.OAuthRefreshTokenKeyID,
	)
	return err
}

const getFileByHashAndCreator = `-- name: GetFileByHashAndCreator :one
SELECT
	hash, created_at, created_by, mimetype, data, id
FROM
	files
WHERE
	hash = $1
AND
	created_by = $2
LIMIT
	1
`

type GetFileByHashAndCreatorParams struct {
	Hash      string    `db:"hash" json:"hash"`
	CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
}

func (q *sqlQuerier) GetFileByHashAndCreator(ctx context.Context, arg GetFileByHashAndCreatorParams) (File, error) {
	row := q.db.QueryRowContext(ctx, getFileByHashAndCreator, arg.Hash, arg.CreatedBy)
	var i File
	err := row.Scan(
		&i.Hash,
		&i.CreatedAt,
		&i.CreatedBy,
		&i.Mimetype,
		&i.Data,
		&i.ID,
	)
	return i, err
}

const getFileByID = `-- name: GetFileByID :one
SELECT
	hash, created_at, created_by, mimetype, data, id
FROM
	files
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetFileByID(ctx context.Context, id uuid.UUID) (File, error) {
	row := q.db.QueryRowContext(ctx, getFileByID, id)
	var i File
	err := row.Scan(
		&i.Hash,
		&i.CreatedAt,
		&i.CreatedBy,
		&i.Mimetype,
		&i.Data,
		&i.ID,
	)
	return i, err
}

const getFileTemplates = `-- name: GetFileTemplates :many
SELECT
	files.id AS file_id,
	files.created_by AS file_created_by,
	templates.id AS template_id,
	templates.organization_id AS template_organization_id,
	templates.created_by AS template_created_by,
	templates.user_acl,
	templates.group_acl
FROM
	templates
INNER JOIN
	template_versions
	ON templates.id = template_versions.template_id
INNER JOIN
	provisioner_jobs
	ON job_id = provisioner_jobs.id
INNER JOIN
	files
	ON files.id = provisioner_jobs.file_id
WHERE
    -- Only fetch template version associated files.
	storage_method = 'file'
	AND provisioner_jobs.type = 'template_version_import'
	AND file_id = $1
`

type GetFileTemplatesRow struct {
	FileID                 uuid.UUID   `db:"file_id" json:"file_id"`
	FileCreatedBy          uuid.UUID   `db:"file_created_by" json:"file_created_by"`
	TemplateID             uuid.UUID   `db:"template_id" json:"template_id"`
	TemplateOrganizationID uuid.UUID   `db:"template_organization_id" json:"template_organization_id"`
	TemplateCreatedBy      uuid.UUID   `db:"template_created_by" json:"template_created_by"`
	UserACL                TemplateACL `db:"user_acl" json:"user_acl"`
	GroupACL               TemplateACL `db:"group_acl" json:"group_acl"`
}

// Get all templates that use a file.
func (q *sqlQuerier) GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error) {
	rows, err := q.db.QueryContext(ctx, getFileTemplates, fileID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetFileTemplatesRow
	for rows.Next() {
		var i GetFileTemplatesRow
		if err := rows.Scan(
			&i.FileID,
			&i.FileCreatedBy,
			&i.TemplateID,
			&i.TemplateOrganizationID,
			&i.TemplateCreatedBy,
			&i.UserACL,
			&i.GroupACL,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertFile = `-- name: InsertFile :one
INSERT INTO
	files (id, hash, created_at, created_by, mimetype, "data")
VALUES
	($1, $2, $3, $4, $5, $6) RETURNING hash, created_at, created_by, mimetype, data, id
`

type InsertFileParams struct {
	ID        uuid.UUID `db:"id" json:"id"`
	Hash      string    `db:"hash" json:"hash"`
	CreatedAt time.Time `db:"created_at" json:"created_at"`
	CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
	Mimetype  string    `db:"mimetype" json:"mimetype"`
	Data      []byte    `db:"data" json:"data"`
}

func (q *sqlQuerier) InsertFile(ctx context.Context, arg InsertFileParams) (File, error) {
	row := q.db.QueryRowContext(ctx, insertFile,
		arg.ID,
		arg.Hash,
		arg.CreatedAt,
		arg.CreatedBy,
		arg.Mimetype,
		arg.Data,
	)
	var i File
	err := row.Scan(
		&i.Hash,
		&i.CreatedAt,
		&i.CreatedBy,
		&i.Mimetype,
		&i.Data,
		&i.ID,
	)
	return i, err
}

const getGitSSHKey = `-- name: GetGitSSHKey :one
SELECT
	user_id, created_at, updated_at, private_key, public_key
FROM
	gitsshkeys
WHERE
	user_id = $1
`

func (q *sqlQuerier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error) {
	row := q.db.QueryRowContext(ctx, getGitSSHKey, userID)
	var i GitSSHKey
	err := row.Scan(
		&i.UserID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.PrivateKey,
		&i.PublicKey,
	)
	return i, err
}

const insertGitSSHKey = `-- name: InsertGitSSHKey :one
INSERT INTO
	gitsshkeys (
		user_id,
		created_at,
		updated_at,
		private_key,
		public_key
	)
VALUES
	($1, $2, $3, $4, $5) RETURNING user_id, created_at, updated_at, private_key, public_key
`

type InsertGitSSHKeyParams struct {
	UserID     uuid.UUID `db:"user_id" json:"user_id"`
	CreatedAt  time.Time `db:"created_at" json:"created_at"`
	UpdatedAt  time.Time `db:"updated_at" json:"updated_at"`
	PrivateKey string    `db:"private_key" json:"private_key"`
	PublicKey  string    `db:"public_key" json:"public_key"`
}

func (q *sqlQuerier) InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error) {
	row := q.db.QueryRowContext(ctx, insertGitSSHKey,
		arg.UserID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.PrivateKey,
		arg.PublicKey,
	)
	var i GitSSHKey
	err := row.Scan(
		&i.UserID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.PrivateKey,
		&i.PublicKey,
	)
	return i, err
}

const updateGitSSHKey = `-- name: UpdateGitSSHKey :one
UPDATE
	gitsshkeys
SET
	updated_at = $2,
	private_key = $3,
	public_key = $4
WHERE
	user_id = $1
RETURNING
	user_id, created_at, updated_at, private_key, public_key
`

type UpdateGitSSHKeyParams struct {
	UserID     uuid.UUID `db:"user_id" json:"user_id"`
	UpdatedAt  time.Time `db:"updated_at" json:"updated_at"`
	PrivateKey string    `db:"private_key" json:"private_key"`
	PublicKey  string    `db:"public_key" json:"public_key"`
}

func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error) {
	row := q.db.QueryRowContext(ctx, updateGitSSHKey,
		arg.UserID,
		arg.UpdatedAt,
		arg.PrivateKey,
		arg.PublicKey,
	)
	var i GitSSHKey
	err := row.Scan(
		&i.UserID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.PrivateKey,
		&i.PublicKey,
	)
	return i, err
}

const deleteGroupMemberFromGroup = `-- name: DeleteGroupMemberFromGroup :exec
DELETE FROM
	group_members
WHERE
	user_id = $1 AND
	group_id = $2
`

type DeleteGroupMemberFromGroupParams struct {
	UserID  uuid.UUID `db:"user_id" json:"user_id"`
	GroupID uuid.UUID `db:"group_id" json:"group_id"`
}

func (q *sqlQuerier) DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error {
	_, err := q.db.ExecContext(ctx, deleteGroupMemberFromGroup, arg.UserID, arg.GroupID)
	return err
}

const getGroupMembers = `-- name: GetGroupMembers :many
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id FROM group_members_expanded
WHERE CASE
      WHEN $1::bool THEN TRUE
      ELSE
        user_is_system = false
        END
`

func (q *sqlQuerier) GetGroupMembers(ctx context.Context, includeSystem bool) ([]GroupMember, error) {
	rows, err := q.db.QueryContext(ctx, getGroupMembers, includeSystem)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GroupMember
	for rows.Next() {
		var i GroupMember
		if err := rows.Scan(
			&i.UserID,
			&i.UserEmail,
			&i.UserUsername,
			&i.UserHashedPassword,
			&i.UserCreatedAt,
			&i.UserUpdatedAt,
			&i.UserStatus,
			pq.Array(&i.UserRbacRoles),
			&i.UserLoginType,
			&i.UserAvatarUrl,
			&i.UserDeleted,
			&i.UserLastSeenAt,
			&i.UserQuietHoursSchedule,
			&i.UserName,
			&i.UserGithubComUserID,
			&i.UserIsSystem,
			&i.UserIsServiceAccount,
			&i.OrganizationID,
			&i.GroupName,
			&i.GroupID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getGroupMembersByGroupID = `-- name: GetGroupMembersByGroupID :many
SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id
FROM group_members_expanded
WHERE group_id = $1
  -- Filter by system type
  AND CASE
      WHEN $2::bool THEN TRUE
      ELSE
        user_is_system = false
      END
`

type GetGroupMembersByGroupIDParams struct {
	GroupID       uuid.UUID `db:"group_id" json:"group_id"`
	IncludeSystem bool      `db:"include_system" json:"include_system"`
}

func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, arg GetGroupMembersByGroupIDParams) ([]GroupMember, error) {
	rows, err := q.db.QueryContext(ctx, getGroupMembersByGroupID, arg.GroupID, arg.IncludeSystem)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GroupMember
	for rows.Next() {
		var i GroupMember
		if err := rows.Scan(
			&i.UserID,
			&i.UserEmail,
			&i.UserUsername,
			&i.UserHashedPassword,
			&i.UserCreatedAt,
			&i.UserUpdatedAt,
			&i.UserStatus,
			pq.Array(&i.UserRbacRoles),
			&i.UserLoginType,
			&i.UserAvatarUrl,
			&i.UserDeleted,
			&i.UserLastSeenAt,
			&i.UserQuietHoursSchedule,
			&i.UserName,
			&i.UserGithubComUserID,
			&i.UserIsSystem,
			&i.UserIsServiceAccount,
			&i.OrganizationID,
			&i.GroupName,
			&i.GroupID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getGroupMembersByGroupIDPaginated = `-- name: GetGroupMembersByGroupIDPaginated :many
SELECT
	user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id, COUNT(*) OVER() AS count
FROM
	group_members_expanded
WHERE
	group_members_expanded.group_id = $1
	AND CASE
		-- This allows using the last element on a page as effectively a cursor.
		-- This is an important option for scripts that need to paginate without
		-- duplicating or missing data.
		WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
			-- The pagination cursor is the last ID of the previous page.
			-- The query is ordered by the username field, so select all
			-- rows after the cursor.
			(LOWER(user_username)) > (
				SELECT
					LOWER(user_username)
				FROM
					group_members_expanded
				WHERE
					group_id = $1
					AND user_id = $2
			)
		)
		ELSE true
	END
	-- Start filters
	-- Filter by email or username
	AND CASE
		WHEN $3 :: text != '' THEN (
			user_email ILIKE concat('%', $3, '%')
			OR user_username ILIKE concat('%', $3, '%')
		)
		ELSE true
	END
	-- Filter by name (display name)
	AND CASE
		WHEN $4 :: text != '' THEN
			user_name ILIKE concat('%', $4, '%')
		ELSE true
	END
	-- Filter by status
	AND CASE
		-- @status needs to be a text because it can be empty, If it was
		-- user_status enum, it would not.
		WHEN cardinality($5 :: user_status[]) > 0 THEN
			user_status = ANY($5 :: user_status[])
		ELSE true
	END
	-- Filter by rbac_roles
	AND CASE
		-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
		-- everyone is a member.
		WHEN cardinality($6 :: text[]) > 0 AND 'member' != ANY($6 :: text[]) THEN
			user_rbac_roles && $6 :: text[]
		ELSE true
	END
	-- Filter by last_seen
	AND CASE
		WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			user_last_seen_at <= $7
		ELSE true
	END
	AND CASE
		WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			user_last_seen_at >= $8
		ELSE true
	END
	-- Filter by created_at
	AND CASE
		WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			user_created_at <= $9
		ELSE true
	END
	AND CASE
		WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			user_created_at >= $10
		ELSE true
	END
	-- Filter by system type
	AND CASE
		WHEN $11::bool THEN TRUE
		ELSE user_is_system = false
	END
	-- Filter by github.com user ID
	AND CASE
		WHEN $12 :: bigint != 0 THEN
			user_github_com_user_id = $12
		ELSE true
	END
	-- Filter by login_type
	AND CASE
		WHEN cardinality($13 :: login_type[]) > 0 THEN
			user_login_type = ANY($13 :: login_type[])
		ELSE true
	END
	-- Filter by service account.
	AND CASE
		WHEN $14 :: boolean IS NOT NULL THEN
			user_is_service_account = $14 :: boolean
		ELSE true
	END
	-- End of filters
ORDER BY
	-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
	LOWER(user_username) ASC OFFSET $15
LIMIT
	-- A null limit means "no limit", so 0 means return all
	NULLIF($16 :: int, 0)
`

type GetGroupMembersByGroupIDPaginatedParams struct {
	GroupID          uuid.UUID    `db:"group_id" json:"group_id"`
	AfterID          uuid.UUID    `db:"after_id" json:"after_id"`
	Search           string       `db:"search" json:"search"`
	Name             string       `db:"name" json:"name"`
	Status           []UserStatus `db:"status" json:"status"`
	RbacRole         []string     `db:"rbac_role" json:"rbac_role"`
	LastSeenBefore   time.Time    `db:"last_seen_before" json:"last_seen_before"`
	LastSeenAfter    time.Time    `db:"last_seen_after" json:"last_seen_after"`
	CreatedBefore    time.Time    `db:"created_before" json:"created_before"`
	CreatedAfter     time.Time    `db:"created_after" json:"created_after"`
	IncludeSystem    bool         `db:"include_system" json:"include_system"`
	GithubComUserID  int64        `db:"github_com_user_id" json:"github_com_user_id"`
	LoginType        []LoginType  `db:"login_type" json:"login_type"`
	IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"`
	OffsetOpt        int32        `db:"offset_opt" json:"offset_opt"`
	LimitOpt         int32        `db:"limit_opt" json:"limit_opt"`
}

type GetGroupMembersByGroupIDPaginatedRow struct {
	UserID                 uuid.UUID     `db:"user_id" json:"user_id"`
	UserEmail              string        `db:"user_email" json:"user_email"`
	UserUsername           string        `db:"user_username" json:"user_username"`
	UserHashedPassword     []byte        `db:"user_hashed_password" json:"user_hashed_password"`
	UserCreatedAt          time.Time     `db:"user_created_at" json:"user_created_at"`
	UserUpdatedAt          time.Time     `db:"user_updated_at" json:"user_updated_at"`
	UserStatus             UserStatus    `db:"user_status" json:"user_status"`
	UserRbacRoles          []string      `db:"user_rbac_roles" json:"user_rbac_roles"`
	UserLoginType          LoginType     `db:"user_login_type" json:"user_login_type"`
	UserAvatarUrl          string        `db:"user_avatar_url" json:"user_avatar_url"`
	UserDeleted            bool          `db:"user_deleted" json:"user_deleted"`
	UserLastSeenAt         time.Time     `db:"user_last_seen_at" json:"user_last_seen_at"`
	UserQuietHoursSchedule string        `db:"user_quiet_hours_schedule" json:"user_quiet_hours_schedule"`
	UserName               string        `db:"user_name" json:"user_name"`
	UserGithubComUserID    sql.NullInt64 `db:"user_github_com_user_id" json:"user_github_com_user_id"`
	UserIsSystem           bool          `db:"user_is_system" json:"user_is_system"`
	UserIsServiceAccount   bool          `db:"user_is_service_account" json:"user_is_service_account"`
	OrganizationID         uuid.UUID     `db:"organization_id" json:"organization_id"`
	GroupName              string        `db:"group_name" json:"group_name"`
	GroupID                uuid.UUID     `db:"group_id" json:"group_id"`
	Count                  int64         `db:"count" json:"count"`
}

func (q *sqlQuerier) GetGroupMembersByGroupIDPaginated(ctx context.Context, arg GetGroupMembersByGroupIDPaginatedParams) ([]GetGroupMembersByGroupIDPaginatedRow, error) {
	rows, err := q.db.QueryContext(ctx, getGroupMembersByGroupIDPaginated,
		arg.GroupID,
		arg.AfterID,
		arg.Search,
		arg.Name,
		pq.Array(arg.Status),
		pq.Array(arg.RbacRole),
		arg.LastSeenBefore,
		arg.LastSeenAfter,
		arg.CreatedBefore,
		arg.CreatedAfter,
		arg.IncludeSystem,
		arg.GithubComUserID,
		pq.Array(arg.LoginType),
		arg.IsServiceAccount,
		arg.OffsetOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetGroupMembersByGroupIDPaginatedRow
	for rows.Next() {
		var i GetGroupMembersByGroupIDPaginatedRow
		if err := rows.Scan(
			&i.UserID,
			&i.UserEmail,
			&i.UserUsername,
			&i.UserHashedPassword,
			&i.UserCreatedAt,
			&i.UserUpdatedAt,
			&i.UserStatus,
			pq.Array(&i.UserRbacRoles),
			&i.UserLoginType,
			&i.UserAvatarUrl,
			&i.UserDeleted,
			&i.UserLastSeenAt,
			&i.UserQuietHoursSchedule,
			&i.UserName,
			&i.UserGithubComUserID,
			&i.UserIsSystem,
			&i.UserIsServiceAccount,
			&i.OrganizationID,
			&i.GroupName,
			&i.GroupID,
			&i.Count,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getGroupMembersCountByGroupID = `-- name: GetGroupMembersCountByGroupID :one
SELECT COUNT(*)
FROM group_members_expanded
WHERE group_id = $1
  -- Filter by system type
  AND CASE
      WHEN $2::bool THEN TRUE
      ELSE
        user_is_system = false
        END
`

type GetGroupMembersCountByGroupIDParams struct {
	GroupID       uuid.UUID `db:"group_id" json:"group_id"`
	IncludeSystem bool      `db:"include_system" json:"include_system"`
}

// Returns the total count of members in a group. Shows the total
// count even if the caller does not have read access to ResourceGroupMember.
// They only need ResourceGroup read access.
func (q *sqlQuerier) GetGroupMembersCountByGroupID(ctx context.Context, arg GetGroupMembersCountByGroupIDParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, getGroupMembersCountByGroupID, arg.GroupID, arg.IncludeSystem)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const getGroupMembersCountByGroupIDs = `-- name: GetGroupMembersCountByGroupIDs :many
SELECT
	group_id,
	COUNT(*) AS member_count
FROM group_members_expanded
WHERE group_id = ANY($1 :: uuid[])
	AND CASE
		WHEN $2::bool THEN TRUE
		ELSE user_is_system = false
	END
GROUP BY group_id
`

type GetGroupMembersCountByGroupIDsParams struct {
	GroupIds      []uuid.UUID `db:"group_ids" json:"group_ids"`
	IncludeSystem bool        `db:"include_system" json:"include_system"`
}

type GetGroupMembersCountByGroupIDsRow struct {
	GroupID     uuid.UUID `db:"group_id" json:"group_id"`
	MemberCount int64     `db:"member_count" json:"member_count"`
}

// Returns the total member count for each of the given group IDs in a
// single query. Used to avoid N+1 lookups when listing many groups. Like
// GetGroupMembersCountByGroupID, the count is returned even when the
// caller does not have read access to individual group members.
func (q *sqlQuerier) GetGroupMembersCountByGroupIDs(ctx context.Context, arg GetGroupMembersCountByGroupIDsParams) ([]GetGroupMembersCountByGroupIDsRow, error) {
	rows, err := q.db.QueryContext(ctx, getGroupMembersCountByGroupIDs, pq.Array(arg.GroupIds), arg.IncludeSystem)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetGroupMembersCountByGroupIDsRow
	for rows.Next() {
		var i GetGroupMembersCountByGroupIDsRow
		if err := rows.Scan(&i.GroupID, &i.MemberCount); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertGroupMember = `-- name: InsertGroupMember :exec
INSERT INTO
    group_members (user_id, group_id)
VALUES
    ($1, $2)
`

type InsertGroupMemberParams struct {
	UserID  uuid.UUID `db:"user_id" json:"user_id"`
	GroupID uuid.UUID `db:"group_id" json:"group_id"`
}

func (q *sqlQuerier) InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error {
	_, err := q.db.ExecContext(ctx, insertGroupMember, arg.UserID, arg.GroupID)
	return err
}

const insertUserGroupsByID = `-- name: InsertUserGroupsByID :many
WITH groups AS (
	SELECT
		id
	FROM
		groups
	WHERE
		groups.id = ANY($2 :: uuid [])
)
INSERT INTO
	group_members (user_id, group_id)
SELECT
	$1,
	groups.id
FROM
	groups
ON CONFLICT DO NOTHING
RETURNING group_id
`

type InsertUserGroupsByIDParams struct {
	UserID   uuid.UUID   `db:"user_id" json:"user_id"`
	GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
}

// InsertUserGroupsByID adds a user to all provided groups, if they exist.
// If there is a conflict, the user is already a member
func (q *sqlQuerier) InsertUserGroupsByID(ctx context.Context, arg InsertUserGroupsByIDParams) ([]uuid.UUID, error) {
	rows, err := q.db.QueryContext(ctx, insertUserGroupsByID, arg.UserID, pq.Array(arg.GroupIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []uuid.UUID
	for rows.Next() {
		var group_id uuid.UUID
		if err := rows.Scan(&group_id); err != nil {
			return nil, err
		}
		items = append(items, group_id)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const removeUserFromGroups = `-- name: RemoveUserFromGroups :many
DELETE FROM
	group_members
WHERE
	user_id = $1 AND
	group_id = ANY($2 :: uuid [])
RETURNING group_id
`

type RemoveUserFromGroupsParams struct {
	UserID   uuid.UUID   `db:"user_id" json:"user_id"`
	GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
}

func (q *sqlQuerier) RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error) {
	rows, err := q.db.QueryContext(ctx, removeUserFromGroups, arg.UserID, pq.Array(arg.GroupIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []uuid.UUID
	for rows.Next() {
		var group_id uuid.UUID
		if err := rows.Scan(&group_id); err != nil {
			return nil, err
		}
		items = append(items, group_id)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteGroupByID = `-- name: DeleteGroupByID :exec
DELETE FROM
	groups
WHERE
	id = $1
`

func (q *sqlQuerier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteGroupByID, id)
	return err
}

const getGroupByID = `-- name: GetGroupByID :one
SELECT
	id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
FROM
	groups
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error) {
	row := q.db.QueryRowContext(ctx, getGroupByID, id)
	var i Group
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.OrganizationID,
		&i.AvatarURL,
		&i.QuotaAllowance,
		&i.DisplayName,
		&i.Source,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const getGroupByOrgAndName = `-- name: GetGroupByOrgAndName :one
SELECT
	id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
FROM
	groups
WHERE
	organization_id = $1
AND
	name = $2
LIMIT
	1
`

type GetGroupByOrgAndNameParams struct {
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	Name           string    `db:"name" json:"name"`
}

func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error) {
	row := q.db.QueryRowContext(ctx, getGroupByOrgAndName, arg.OrganizationID, arg.Name)
	var i Group
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.OrganizationID,
		&i.AvatarURL,
		&i.QuotaAllowance,
		&i.DisplayName,
		&i.Source,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const getGroups = `-- name: GetGroups :many
SELECT
		groups.id, groups.name, groups.organization_id, groups.avatar_url, groups.quota_allowance, groups.display_name, groups.source, groups.chat_spend_limit_micros,
		organizations.name AS organization_name,
		organizations.display_name AS organization_display_name
FROM
		groups
INNER JOIN
		organizations ON groups.organization_id = organizations.id
WHERE
		true
		AND CASE
				WHEN $1:: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
						groups.organization_id = $1
				ELSE true
		END
		AND CASE
				-- Filter to only include groups a user is a member of
				WHEN $2::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
						EXISTS (
								SELECT
										1
								FROM
										-- this view handles the 'everyone' group in orgs.
										group_members_expanded
								WHERE
										group_members_expanded.group_id = groups.id
								AND
										group_members_expanded.user_id = $2
						)
				ELSE true
		END
		AND CASE WHEN array_length($3 :: text[], 1) > 0  THEN
				groups.name = ANY($3)
			ELSE true
		END
		AND CASE WHEN array_length($4 :: uuid[], 1) > 0  THEN
				groups.id = ANY($4)
			ELSE true
		END
		-- Filter by group name or display name (substring, case-insensitive).
		AND CASE WHEN $5 :: text != '' THEN (
				groups.name ILIKE concat('%', $5, '%')
				OR groups.display_name ILIKE concat('%', $5, '%')
			)
			ELSE true
		END
LIMIT NULLIF($6 :: int, 0)
`

type GetGroupsParams struct {
	OrganizationID uuid.UUID   `db:"organization_id" json:"organization_id"`
	HasMemberID    uuid.UUID   `db:"has_member_id" json:"has_member_id"`
	GroupNames     []string    `db:"group_names" json:"group_names"`
	GroupIds       []uuid.UUID `db:"group_ids" json:"group_ids"`
	Search         string      `db:"search" json:"search"`
	LimitOpt       int32       `db:"limit_opt" json:"limit_opt"`
}

type GetGroupsRow struct {
	Group                   Group  `db:"group" json:"group"`
	OrganizationName        string `db:"organization_name" json:"organization_name"`
	OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
}

// A limit of 0 means "no limit".
func (q *sqlQuerier) GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error) {
	rows, err := q.db.QueryContext(ctx, getGroups,
		arg.OrganizationID,
		arg.HasMemberID,
		pq.Array(arg.GroupNames),
		pq.Array(arg.GroupIds),
		arg.Search,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetGroupsRow
	for rows.Next() {
		var i GetGroupsRow
		if err := rows.Scan(
			&i.Group.ID,
			&i.Group.Name,
			&i.Group.OrganizationID,
			&i.Group.AvatarURL,
			&i.Group.QuotaAllowance,
			&i.Group.DisplayName,
			&i.Group.Source,
			&i.Group.ChatSpendLimitMicros,
			&i.OrganizationName,
			&i.OrganizationDisplayName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertAllUsersGroup = `-- name: InsertAllUsersGroup :one
INSERT INTO groups (
	id,
	name,
	organization_id
)
VALUES
	($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
`

// We use the organization_id as the id
// for simplicity since all users is
// every member of the org.
func (q *sqlQuerier) InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error) {
	row := q.db.QueryRowContext(ctx, insertAllUsersGroup, organizationID)
	var i Group
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.OrganizationID,
		&i.AvatarURL,
		&i.QuotaAllowance,
		&i.DisplayName,
		&i.Source,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const insertGroup = `-- name: InsertGroup :one
INSERT INTO groups (
	id,
	name,
	display_name,
	organization_id,
	avatar_url,
	quota_allowance
)
VALUES
	($1, $2, $3, $4, $5, $6) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
`

type InsertGroupParams struct {
	ID             uuid.UUID `db:"id" json:"id"`
	Name           string    `db:"name" json:"name"`
	DisplayName    string    `db:"display_name" json:"display_name"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	AvatarURL      string    `db:"avatar_url" json:"avatar_url"`
	QuotaAllowance int32     `db:"quota_allowance" json:"quota_allowance"`
}

func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) {
	row := q.db.QueryRowContext(ctx, insertGroup,
		arg.ID,
		arg.Name,
		arg.DisplayName,
		arg.OrganizationID,
		arg.AvatarURL,
		arg.QuotaAllowance,
	)
	var i Group
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.OrganizationID,
		&i.AvatarURL,
		&i.QuotaAllowance,
		&i.DisplayName,
		&i.Source,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const insertMissingGroups = `-- name: InsertMissingGroups :many
INSERT INTO groups (
	id,
	name,
	organization_id,
						source
)
SELECT
						gen_random_uuid(),
						group_name,
						$1,
						$2
FROM
						UNNEST($3 :: text[]) AS group_name
ON CONFLICT DO NOTHING
RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
`

type InsertMissingGroupsParams struct {
	OrganizationID uuid.UUID   `db:"organization_id" json:"organization_id"`
	Source         GroupSource `db:"source" json:"source"`
	GroupNames     []string    `db:"group_names" json:"group_names"`
}

// Inserts any group by name that does not exist. All new groups are given
// a random uuid, are inserted into the same organization. They have the default
// values for avatar, display name, and quota allowance (all zero values).
// If the name conflicts, do nothing.
func (q *sqlQuerier) InsertMissingGroups(ctx context.Context, arg InsertMissingGroupsParams) ([]Group, error) {
	rows, err := q.db.QueryContext(ctx, insertMissingGroups, arg.OrganizationID, arg.Source, pq.Array(arg.GroupNames))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Group
	for rows.Next() {
		var i Group
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.OrganizationID,
			&i.AvatarURL,
			&i.QuotaAllowance,
			&i.DisplayName,
			&i.Source,
			&i.ChatSpendLimitMicros,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateGroupByID = `-- name: UpdateGroupByID :one
UPDATE
	groups
SET
	name = $1,
	display_name = $2,
	avatar_url = $3,
	quota_allowance = $4
WHERE
	id = $5
RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name, source, chat_spend_limit_micros
`

type UpdateGroupByIDParams struct {
	Name           string    `db:"name" json:"name"`
	DisplayName    string    `db:"display_name" json:"display_name"`
	AvatarURL      string    `db:"avatar_url" json:"avatar_url"`
	QuotaAllowance int32     `db:"quota_allowance" json:"quota_allowance"`
	ID             uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) {
	row := q.db.QueryRowContext(ctx, updateGroupByID,
		arg.Name,
		arg.DisplayName,
		arg.AvatarURL,
		arg.QuotaAllowance,
		arg.ID,
	)
	var i Group
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.OrganizationID,
		&i.AvatarURL,
		&i.QuotaAllowance,
		&i.DisplayName,
		&i.Source,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const validateGroupIDs = `-- name: ValidateGroupIDs :one
WITH input AS (
	SELECT
		unnest($1::uuid[]) AS id
)
SELECT
	array_agg(input.id)::uuid[] as invalid_group_ids,
	COUNT(*) = 0 as ok
FROM
	-- Preserve rows where there is not a matching left (groups) row for each
	-- right (input) row...
	groups
	RIGHT JOIN input ON groups.id = input.id
WHERE
	-- ...so that we can retain exactly those rows where an input ID does not
	-- match an existing group.
	groups.id IS NULL
`

type ValidateGroupIDsRow struct {
	InvalidGroupIds []uuid.UUID `db:"invalid_group_ids" json:"invalid_group_ids"`
	Ok              bool        `db:"ok" json:"ok"`
}

func (q *sqlQuerier) ValidateGroupIDs(ctx context.Context, groupIds []uuid.UUID) (ValidateGroupIDsRow, error) {
	row := q.db.QueryRowContext(ctx, validateGroupIDs, pq.Array(groupIds))
	var i ValidateGroupIDsRow
	err := row.Scan(pq.Array(&i.InvalidGroupIds), &i.Ok)
	return i, err
}

const getTemplateAppInsights = `-- name: GetTemplateAppInsights :many
WITH
	-- Create a list of all unique apps by template, this is used to
	-- filter out irrelevant template usage stats.
	apps AS (
		SELECT DISTINCT ON (ws.template_id, app.slug)
			ws.template_id,
			app.slug,
			app.display_name,
			app.icon
		FROM
			workspaces ws
		JOIN
			workspace_builds AS build
		ON
			build.workspace_id = ws.id
		JOIN
			workspace_resources AS resource
		ON
			resource.job_id = build.job_id
		JOIN
			workspace_agents AS agent
		ON
			agent.resource_id = resource.id
		JOIN
			workspace_apps AS app
		ON
			app.agent_id = agent.id
		WHERE
			-- Partial query parameter filter.
			CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN ws.template_id = ANY($1::uuid[]) ELSE TRUE END
		ORDER BY
			ws.template_id, app.slug, app.created_at DESC
	),
	-- Join apps and template usage stats to filter out irrelevant rows.
	-- Note that this way of joining will eliminate all data-points that
	-- aren't for "real" apps. That means ports are ignored (even though
	-- they're part of the dataset), as well as are "[terminal]" entries
	-- which are alternate datapoints for reconnecting pty usage.
	template_usage_stats_with_apps AS (
		SELECT
			tus.start_time,
			tus.template_id,
			tus.user_id,
			apps.slug,
			apps.display_name,
			apps.icon,
			(tus.app_usage_mins -> apps.slug)::smallint AS usage_mins
		FROM
			apps
		JOIN
			template_usage_stats AS tus
		ON
			-- Query parameter filter.
			tus.start_time >= $2::timestamptz
			AND tus.end_time <= $3::timestamptz
			AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN tus.template_id = ANY($1::uuid[]) ELSE TRUE END
			-- Primary join condition.
			AND tus.template_id = apps.template_id
			AND tus.app_usage_mins ? apps.slug -- Key exists in object.
	),
	-- Group the app insights by interval, user and unique app. This
	-- allows us to deduplicate a user using the same app across
	-- multiple templates.
	app_insights AS (
		SELECT
			user_id,
			slug,
			display_name,
			icon,
			-- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
			LEAST(SUM(usage_mins), 30) AS usage_mins
		FROM
			template_usage_stats_with_apps
		GROUP BY
			start_time, user_id, slug, display_name, icon
	),
	-- Analyze the users unique app usage across all templates. Count
	-- usage across consecutive intervals as continuous usage.
	times_used AS (
		SELECT DISTINCT ON (user_id, slug, display_name, icon, uniq)
			slug,
			display_name,
			icon,
			-- Turn start_time into a unique identifier that identifies a users
			-- continuous app usage. The value of uniq is otherwise garbage.
			--
			-- Since we're aggregating per user app usage across templates,
			-- there can be duplicate start_times. To handle this, we use the
			-- dense_rank() function, otherwise row_number() would suffice.
			start_time - (
				dense_rank() OVER (
					PARTITION BY
						user_id, slug, display_name, icon
					ORDER BY
						start_time
				) * '30 minutes'::interval
			) AS uniq
		FROM
			template_usage_stats_with_apps
	),
	-- Even though we allow identical apps to be aggregated across
	-- templates, we still want to be able to report which templates
	-- the data comes from.
	templates AS (
		SELECT
			slug,
			display_name,
			icon,
			array_agg(DISTINCT template_id)::uuid[] AS template_ids
		FROM
			template_usage_stats_with_apps
		GROUP BY
			slug, display_name, icon
	)

SELECT
	t.template_ids,
	COUNT(DISTINCT ai.user_id) AS active_users,
	ai.slug,
	ai.display_name,
	ai.icon,
	(SUM(ai.usage_mins) * 60)::bigint AS usage_seconds,
	COALESCE((
		SELECT
			COUNT(*)
		FROM
			times_used
		WHERE
			times_used.slug = ai.slug
			AND times_used.display_name = ai.display_name
			AND times_used.icon = ai.icon
	), 0)::bigint AS times_used
FROM
	app_insights AS ai
JOIN
	templates AS t
ON
	t.slug = ai.slug
	AND t.display_name = ai.display_name
	AND t.icon = ai.icon
GROUP BY
	t.template_ids, ai.slug, ai.display_name, ai.icon
`

type GetTemplateAppInsightsParams struct {
	TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
	StartTime   time.Time   `db:"start_time" json:"start_time"`
	EndTime     time.Time   `db:"end_time" json:"end_time"`
}

type GetTemplateAppInsightsRow struct {
	TemplateIDs  []uuid.UUID `db:"template_ids" json:"template_ids"`
	ActiveUsers  int64       `db:"active_users" json:"active_users"`
	Slug         string      `db:"slug" json:"slug"`
	DisplayName  string      `db:"display_name" json:"display_name"`
	Icon         string      `db:"icon" json:"icon"`
	UsageSeconds int64       `db:"usage_seconds" json:"usage_seconds"`
	TimesUsed    int64       `db:"times_used" json:"times_used"`
}

// GetTemplateAppInsights returns the aggregate usage of each app in a given
// timeframe. The result can be filtered on template_ids, meaning only user data
// from workspaces based on those templates will be included.
func (q *sqlQuerier) GetTemplateAppInsights(ctx context.Context, arg GetTemplateAppInsightsParams) ([]GetTemplateAppInsightsRow, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateAppInsights, pq.Array(arg.TemplateIDs), arg.StartTime, arg.EndTime)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTemplateAppInsightsRow
	for rows.Next() {
		var i GetTemplateAppInsightsRow
		if err := rows.Scan(
			pq.Array(&i.TemplateIDs),
			&i.ActiveUsers,
			&i.Slug,
			&i.DisplayName,
			&i.Icon,
			&i.UsageSeconds,
			&i.TimesUsed,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplateAppInsightsByTemplate = `-- name: GetTemplateAppInsightsByTemplate :many
WITH
	filtered_stats AS (
		SELECT
		was.workspace_id,
		was.user_id,
		was.agent_id,
		was.access_method,
		was.slug_or_port,
		was.session_started_at,
		was.session_ended_at
		FROM
			workspace_app_stats AS was
		WHERE
			was.session_ended_at >= $1::timestamptz
			AND was.session_started_at <  $2::timestamptz
	),
	-- This CTE is used to explode app usage into minute buckets, then
	-- flatten the users app usage within the template so that usage in
	-- multiple workspaces under one template is only counted once for
	-- every minute.
	app_insights AS (
		SELECT
			w.template_id,
			fs.user_id,
			-- Both app stats and agent stats track web terminal usage, but
			-- by different means. The app stats value should be more
			-- accurate so we don't want to discard it just yet.
			CASE
				WHEN fs.access_method = 'terminal'
				THEN '[terminal]' -- Unique name, app names can't contain brackets.
				ELSE fs.slug_or_port
			END::text AS app_name,
			COALESCE(wa.display_name, '') AS display_name,
			(wa.slug IS NOT NULL)::boolean AS is_app,
			COUNT(DISTINCT s.minute_bucket) AS app_minutes
		FROM
			filtered_stats AS fs
		JOIN
			workspaces AS w
		ON
			w.id = fs.workspace_id
		-- We do a left join here because we want to include user IDs that have used
		-- e.g. ports when counting active users.
		LEFT JOIN
			workspace_apps wa
		ON
			wa.agent_id = fs.agent_id
			AND wa.slug = fs.slug_or_port
		-- Generate a series of minute buckets for each session for computing the
		-- mintes/bucket.
		CROSS JOIN
			generate_series(
				date_trunc('minute', fs.session_started_at),
				-- Subtract 1 μs to avoid creating an extra series.
				date_trunc('minute', fs.session_ended_at - '1 microsecond'::interval),
				'1 minute'::interval
			) AS s(minute_bucket)
		WHERE
			s.minute_bucket >= $1::timestamptz
			AND s.minute_bucket < $2::timestamptz
		GROUP BY
			w.template_id, fs.user_id, fs.access_method, fs.slug_or_port, wa.display_name, wa.slug
	)

SELECT
	template_id,
	app_name AS slug_or_port,
	display_name AS display_name,
	COUNT(DISTINCT user_id)::bigint AS active_users,
	(SUM(app_minutes) * 60)::bigint AS usage_seconds
FROM
	app_insights
WHERE
	is_app IS TRUE
GROUP BY
	template_id, slug_or_port, display_name
`

type GetTemplateAppInsightsByTemplateParams struct {
	StartTime time.Time `db:"start_time" json:"start_time"`
	EndTime   time.Time `db:"end_time" json:"end_time"`
}

type GetTemplateAppInsightsByTemplateRow struct {
	TemplateID   uuid.UUID `db:"template_id" json:"template_id"`
	SlugOrPort   string    `db:"slug_or_port" json:"slug_or_port"`
	DisplayName  string    `db:"display_name" json:"display_name"`
	ActiveUsers  int64     `db:"active_users" json:"active_users"`
	UsageSeconds int64     `db:"usage_seconds" json:"usage_seconds"`
}

// GetTemplateAppInsightsByTemplate is used for Prometheus metrics. Keep
// in sync with GetTemplateAppInsights and UpsertTemplateUsageStats.
func (q *sqlQuerier) GetTemplateAppInsightsByTemplate(ctx context.Context, arg GetTemplateAppInsightsByTemplateParams) ([]GetTemplateAppInsightsByTemplateRow, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateAppInsightsByTemplate, arg.StartTime, arg.EndTime)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTemplateAppInsightsByTemplateRow
	for rows.Next() {
		var i GetTemplateAppInsightsByTemplateRow
		if err := rows.Scan(
			&i.TemplateID,
			&i.SlugOrPort,
			&i.DisplayName,
			&i.ActiveUsers,
			&i.UsageSeconds,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplateInsights = `-- name: GetTemplateInsights :one
WITH
	insights AS (
		SELECT
			user_id,
			-- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
			LEAST(SUM(usage_mins), 30) AS usage_mins,
			LEAST(SUM(ssh_mins), 30) AS ssh_mins,
			LEAST(SUM(sftp_mins), 30) AS sftp_mins,
			LEAST(SUM(reconnecting_pty_mins), 30) AS reconnecting_pty_mins,
			LEAST(SUM(vscode_mins), 30) AS vscode_mins,
			LEAST(SUM(jetbrains_mins), 30) AS jetbrains_mins
		FROM
			template_usage_stats
		WHERE
			start_time >= $1::timestamptz
			AND end_time <= $2::timestamptz
			AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
		GROUP BY
			start_time, user_id
	),
	templates AS (
		SELECT
			array_agg(DISTINCT template_id) AS template_ids,
			array_agg(DISTINCT template_id) FILTER (WHERE ssh_mins > 0) AS ssh_template_ids,
			array_agg(DISTINCT template_id) FILTER (WHERE sftp_mins > 0) AS sftp_template_ids,
			array_agg(DISTINCT template_id) FILTER (WHERE reconnecting_pty_mins > 0) AS reconnecting_pty_template_ids,
			array_agg(DISTINCT template_id) FILTER (WHERE vscode_mins > 0) AS vscode_template_ids,
			array_agg(DISTINCT template_id) FILTER (WHERE jetbrains_mins > 0) AS jetbrains_template_ids
		FROM
			template_usage_stats
		WHERE
			start_time >= $1::timestamptz
			AND end_time <= $2::timestamptz
			AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
	)

SELECT
	COALESCE((SELECT template_ids FROM templates), '{}')::uuid[] AS template_ids, -- Includes app usage.
	COALESCE((SELECT ssh_template_ids FROM templates), '{}')::uuid[] AS ssh_template_ids,
	COALESCE((SELECT sftp_template_ids FROM templates), '{}')::uuid[] AS sftp_template_ids,
	COALESCE((SELECT reconnecting_pty_template_ids FROM templates), '{}')::uuid[] AS reconnecting_pty_template_ids,
	COALESCE((SELECT vscode_template_ids FROM templates), '{}')::uuid[] AS vscode_template_ids,
	COALESCE((SELECT jetbrains_template_ids FROM templates), '{}')::uuid[] AS jetbrains_template_ids,
	COALESCE(COUNT(DISTINCT user_id), 0)::bigint AS active_users, -- Includes app usage.
	COALESCE(SUM(usage_mins) * 60, 0)::bigint AS usage_total_seconds, -- Includes app usage.
	COALESCE(SUM(ssh_mins) * 60, 0)::bigint AS usage_ssh_seconds,
	COALESCE(SUM(sftp_mins) * 60, 0)::bigint AS usage_sftp_seconds,
	COALESCE(SUM(reconnecting_pty_mins) * 60, 0)::bigint AS usage_reconnecting_pty_seconds,
	COALESCE(SUM(vscode_mins) * 60, 0)::bigint AS usage_vscode_seconds,
	COALESCE(SUM(jetbrains_mins) * 60, 0)::bigint AS usage_jetbrains_seconds
FROM
	insights
`

type GetTemplateInsightsParams struct {
	StartTime   time.Time   `db:"start_time" json:"start_time"`
	EndTime     time.Time   `db:"end_time" json:"end_time"`
	TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}

type GetTemplateInsightsRow struct {
	TemplateIDs                 []uuid.UUID `db:"template_ids" json:"template_ids"`
	SshTemplateIds              []uuid.UUID `db:"ssh_template_ids" json:"ssh_template_ids"`
	SftpTemplateIds             []uuid.UUID `db:"sftp_template_ids" json:"sftp_template_ids"`
	ReconnectingPtyTemplateIds  []uuid.UUID `db:"reconnecting_pty_template_ids" json:"reconnecting_pty_template_ids"`
	VscodeTemplateIds           []uuid.UUID `db:"vscode_template_ids" json:"vscode_template_ids"`
	JetbrainsTemplateIds        []uuid.UUID `db:"jetbrains_template_ids" json:"jetbrains_template_ids"`
	ActiveUsers                 int64       `db:"active_users" json:"active_users"`
	UsageTotalSeconds           int64       `db:"usage_total_seconds" json:"usage_total_seconds"`
	UsageSshSeconds             int64       `db:"usage_ssh_seconds" json:"usage_ssh_seconds"`
	UsageSftpSeconds            int64       `db:"usage_sftp_seconds" json:"usage_sftp_seconds"`
	UsageReconnectingPtySeconds int64       `db:"usage_reconnecting_pty_seconds" json:"usage_reconnecting_pty_seconds"`
	UsageVscodeSeconds          int64       `db:"usage_vscode_seconds" json:"usage_vscode_seconds"`
	UsageJetbrainsSeconds       int64       `db:"usage_jetbrains_seconds" json:"usage_jetbrains_seconds"`
}

// GetTemplateInsights returns the aggregate user-produced usage of all
// workspaces in a given timeframe. The template IDs, active users, and
// usage_seconds all reflect any usage in the template, including apps.
//
// When combining data from multiple templates, we must make a guess at
// how the user behaved for the 30 minute interval. In this case we make
// the assumption that if the user used two workspaces for 15 minutes,
// they did so sequentially, thus we sum the usage up to a maximum of
// 30 minutes with LEAST(SUM(n), 30).
func (q *sqlQuerier) GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) {
	row := q.db.QueryRowContext(ctx, getTemplateInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
	var i GetTemplateInsightsRow
	err := row.Scan(
		pq.Array(&i.TemplateIDs),
		pq.Array(&i.SshTemplateIds),
		pq.Array(&i.SftpTemplateIds),
		pq.Array(&i.ReconnectingPtyTemplateIds),
		pq.Array(&i.VscodeTemplateIds),
		pq.Array(&i.JetbrainsTemplateIds),
		&i.ActiveUsers,
		&i.UsageTotalSeconds,
		&i.UsageSshSeconds,
		&i.UsageSftpSeconds,
		&i.UsageReconnectingPtySeconds,
		&i.UsageVscodeSeconds,
		&i.UsageJetbrainsSeconds,
	)
	return i, err
}

const getTemplateInsightsByInterval = `-- name: GetTemplateInsightsByInterval :many
WITH
	ts AS (
		SELECT
			d::timestamptz AS from_,
			LEAST(
				(d::timestamptz + ($2::int || ' day')::interval)::timestamptz,
				$3::timestamptz
			)::timestamptz AS to_
		FROM
			generate_series(
				$4::timestamptz,
				-- Subtract 1 μs to avoid creating an extra series.
				($3::timestamptz) - '1 microsecond'::interval,
				($2::int || ' day')::interval
			) AS d
	)

SELECT
	ts.from_ AS start_time,
	ts.to_ AS end_time,
	array_remove(array_agg(DISTINCT tus.template_id), NULL)::uuid[] AS template_ids,
	COUNT(DISTINCT tus.user_id) AS active_users
FROM
	ts
LEFT JOIN
	template_usage_stats AS tus
ON
	tus.start_time >= ts.from_
	AND tus.start_time < ts.to_ -- End time exclusion criteria optimization for index.
	AND tus.end_time <= ts.to_
	AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN tus.template_id = ANY($1::uuid[]) ELSE TRUE END
GROUP BY
	ts.from_, ts.to_
`

type GetTemplateInsightsByIntervalParams struct {
	TemplateIDs  []uuid.UUID `db:"template_ids" json:"template_ids"`
	IntervalDays int32       `db:"interval_days" json:"interval_days"`
	EndTime      time.Time   `db:"end_time" json:"end_time"`
	StartTime    time.Time   `db:"start_time" json:"start_time"`
}

type GetTemplateInsightsByIntervalRow struct {
	StartTime   time.Time   `db:"start_time" json:"start_time"`
	EndTime     time.Time   `db:"end_time" json:"end_time"`
	TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
	ActiveUsers int64       `db:"active_users" json:"active_users"`
}

// GetTemplateInsightsByInterval returns all intervals between start and end
// time, if end time is a partial interval, it will be included in the results and
// that interval will be shorter than a full one. If there is no data for a selected
// interval/template, it will be included in the results with 0 active users.
func (q *sqlQuerier) GetTemplateInsightsByInterval(ctx context.Context, arg GetTemplateInsightsByIntervalParams) ([]GetTemplateInsightsByIntervalRow, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateInsightsByInterval,
		pq.Array(arg.TemplateIDs),
		arg.IntervalDays,
		arg.EndTime,
		arg.StartTime,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTemplateInsightsByIntervalRow
	for rows.Next() {
		var i GetTemplateInsightsByIntervalRow
		if err := rows.Scan(
			&i.StartTime,
			&i.EndTime,
			pq.Array(&i.TemplateIDs),
			&i.ActiveUsers,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplateInsightsByTemplate = `-- name: GetTemplateInsightsByTemplate :many
WITH
	-- This CTE is used to truncate agent usage into minute buckets, then
	-- flatten the users agent usage within the template so that usage in
	-- multiple workspaces under one template is only counted once for
	-- every minute (per user).
	insights AS (
		SELECT
			template_id,
			user_id,
			COUNT(DISTINCT CASE WHEN session_count_ssh > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS ssh_mins,
			-- TODO(mafredri): Enable when we have the column.
			-- COUNT(DISTINCT CASE WHEN session_count_sftp > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS sftp_mins,
			COUNT(DISTINCT CASE WHEN session_count_reconnecting_pty > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS reconnecting_pty_mins,
			COUNT(DISTINCT CASE WHEN session_count_vscode > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS vscode_mins,
			COUNT(DISTINCT CASE WHEN session_count_jetbrains > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS jetbrains_mins,
			-- NOTE(mafredri): The agent stats are currently very unreliable, and
			-- sometimes the connections are missing, even during active sessions.
			-- Since we can't fully rely on this, we check for "any connection
			-- within this bucket". A better solution here would be preferable.
			MAX(connection_count) > 0 AS has_connection
		FROM
			workspace_agent_stats
		WHERE
			created_at >= $1::timestamptz
			AND created_at < $2::timestamptz
			-- Inclusion criteria to filter out empty results.
			AND (
				session_count_ssh > 0
				-- TODO(mafredri): Enable when we have the column.
				-- OR session_count_sftp > 0
				OR session_count_reconnecting_pty > 0
				OR session_count_vscode > 0
				OR session_count_jetbrains > 0
			)
		GROUP BY
			template_id, user_id
	)

SELECT
	template_id,
	COUNT(DISTINCT user_id)::bigint AS active_users,
	(SUM(vscode_mins) * 60)::bigint AS usage_vscode_seconds,
	(SUM(jetbrains_mins) * 60)::bigint AS usage_jetbrains_seconds,
	(SUM(reconnecting_pty_mins) * 60)::bigint AS usage_reconnecting_pty_seconds,
	(SUM(ssh_mins) * 60)::bigint AS usage_ssh_seconds
FROM
	insights
WHERE
	has_connection
GROUP BY
	template_id
`

type GetTemplateInsightsByTemplateParams struct {
	StartTime time.Time `db:"start_time" json:"start_time"`
	EndTime   time.Time `db:"end_time" json:"end_time"`
}

type GetTemplateInsightsByTemplateRow struct {
	TemplateID                  uuid.UUID `db:"template_id" json:"template_id"`
	ActiveUsers                 int64     `db:"active_users" json:"active_users"`
	UsageVscodeSeconds          int64     `db:"usage_vscode_seconds" json:"usage_vscode_seconds"`
	UsageJetbrainsSeconds       int64     `db:"usage_jetbrains_seconds" json:"usage_jetbrains_seconds"`
	UsageReconnectingPtySeconds int64     `db:"usage_reconnecting_pty_seconds" json:"usage_reconnecting_pty_seconds"`
	UsageSshSeconds             int64     `db:"usage_ssh_seconds" json:"usage_ssh_seconds"`
}

// GetTemplateInsightsByTemplate is used for Prometheus metrics. Keep
// in sync with GetTemplateInsights and UpsertTemplateUsageStats.
func (q *sqlQuerier) GetTemplateInsightsByTemplate(ctx context.Context, arg GetTemplateInsightsByTemplateParams) ([]GetTemplateInsightsByTemplateRow, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateInsightsByTemplate, arg.StartTime, arg.EndTime)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTemplateInsightsByTemplateRow
	for rows.Next() {
		var i GetTemplateInsightsByTemplateRow
		if err := rows.Scan(
			&i.TemplateID,
			&i.ActiveUsers,
			&i.UsageVscodeSeconds,
			&i.UsageJetbrainsSeconds,
			&i.UsageReconnectingPtySeconds,
			&i.UsageSshSeconds,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplateParameterInsights = `-- name: GetTemplateParameterInsights :many
WITH latest_workspace_builds AS (
	SELECT
		wb.id,
		wbmax.template_id,
		wb.template_version_id
	FROM (
		SELECT
			tv.template_id, wbmax.workspace_id, MAX(wbmax.build_number) as max_build_number
		FROM workspace_builds wbmax
		JOIN template_versions tv ON (tv.id = wbmax.template_version_id)
		WHERE
			wbmax.created_at >= $1::timestamptz
			AND wbmax.created_at < $2::timestamptz
			AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN tv.template_id = ANY($3::uuid[]) ELSE TRUE END
		GROUP BY tv.template_id, wbmax.workspace_id
	) wbmax
	JOIN workspace_builds wb ON (
		wb.workspace_id = wbmax.workspace_id
		AND wb.build_number = wbmax.max_build_number
	)
), unique_template_params AS (
	SELECT
		ROW_NUMBER() OVER () AS num,
		array_agg(DISTINCT wb.template_id)::uuid[] AS template_ids,
		array_agg(wb.id)::uuid[] AS workspace_build_ids,
		tvp.name,
		tvp.type,
		tvp.display_name,
		tvp.description,
		tvp.options
	FROM latest_workspace_builds wb
	JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id)
	GROUP BY tvp.name, tvp.type, tvp.display_name, tvp.description, tvp.options
)

SELECT
	utp.num,
	utp.template_ids,
	utp.name,
	utp.type,
	utp.display_name,
	utp.description,
	utp.options,
	wbp.value,
	COUNT(wbp.value) AS count
FROM unique_template_params utp
JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name)
GROUP BY utp.num, utp.template_ids, utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value
`

type GetTemplateParameterInsightsParams struct {
	StartTime   time.Time   `db:"start_time" json:"start_time"`
	EndTime     time.Time   `db:"end_time" json:"end_time"`
	TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}

type GetTemplateParameterInsightsRow struct {
	Num         int64           `db:"num" json:"num"`
	TemplateIDs []uuid.UUID     `db:"template_ids" json:"template_ids"`
	Name        string          `db:"name" json:"name"`
	Type        string          `db:"type" json:"type"`
	DisplayName string          `db:"display_name" json:"display_name"`
	Description string          `db:"description" json:"description"`
	Options     json.RawMessage `db:"options" json:"options"`
	Value       string          `db:"value" json:"value"`
	Count       int64           `db:"count" json:"count"`
}

// GetTemplateParameterInsights does for each template in a given timeframe,
// look for the latest workspace build (for every workspace) that has been
// created in the timeframe and return the aggregate usage counts of parameter
// values.
func (q *sqlQuerier) GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateParameterInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTemplateParameterInsightsRow
	for rows.Next() {
		var i GetTemplateParameterInsightsRow
		if err := rows.Scan(
			&i.Num,
			pq.Array(&i.TemplateIDs),
			&i.Name,
			&i.Type,
			&i.DisplayName,
			&i.Description,
			&i.Options,
			&i.Value,
			&i.Count,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplateUsageStats = `-- name: GetTemplateUsageStats :many
SELECT
	start_time, end_time, template_id, user_id, median_latency_ms, usage_mins, ssh_mins, sftp_mins, reconnecting_pty_mins, vscode_mins, jetbrains_mins, app_usage_mins
FROM
	template_usage_stats
WHERE
	start_time >= $1::timestamptz
	AND end_time <= $2::timestamptz
	AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
`

type GetTemplateUsageStatsParams struct {
	StartTime   time.Time   `db:"start_time" json:"start_time"`
	EndTime     time.Time   `db:"end_time" json:"end_time"`
	TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}

func (q *sqlQuerier) GetTemplateUsageStats(ctx context.Context, arg GetTemplateUsageStatsParams) ([]TemplateUsageStat, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateUsageStats, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateUsageStat
	for rows.Next() {
		var i TemplateUsageStat
		if err := rows.Scan(
			&i.StartTime,
			&i.EndTime,
			&i.TemplateID,
			&i.UserID,
			&i.MedianLatencyMs,
			&i.UsageMins,
			&i.SshMins,
			&i.SftpMins,
			&i.ReconnectingPtyMins,
			&i.VscodeMins,
			&i.JetbrainsMins,
			&i.AppUsageMins,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUserActivityInsights = `-- name: GetUserActivityInsights :many
WITH
	deployment_stats AS (
		SELECT
			start_time,
			user_id,
			array_agg(template_id) AS template_ids,
			-- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
			LEAST(SUM(usage_mins), 30) AS usage_mins
		FROM
			template_usage_stats
		WHERE
			start_time >= $1::timestamptz
			AND end_time <= $2::timestamptz
			AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
		GROUP BY
			start_time, user_id
	),
	template_ids AS (
		SELECT
			user_id,
			array_agg(DISTINCT template_id) AS ids
		FROM
			deployment_stats, unnest(template_ids) template_id
		GROUP BY
			user_id
	)

SELECT
	ds.user_id,
	u.username,
	u.avatar_url,
	t.ids::uuid[] AS template_ids,
	(SUM(ds.usage_mins) * 60)::bigint AS usage_seconds
FROM
	deployment_stats ds
JOIN
	users u
ON
	u.id = ds.user_id
JOIN
	template_ids t
ON
	ds.user_id = t.user_id
GROUP BY
	ds.user_id, u.username, u.avatar_url, t.ids
ORDER BY
	ds.user_id ASC
`

type GetUserActivityInsightsParams struct {
	StartTime   time.Time   `db:"start_time" json:"start_time"`
	EndTime     time.Time   `db:"end_time" json:"end_time"`
	TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}

type GetUserActivityInsightsRow struct {
	UserID       uuid.UUID   `db:"user_id" json:"user_id"`
	Username     string      `db:"username" json:"username"`
	AvatarURL    string      `db:"avatar_url" json:"avatar_url"`
	TemplateIDs  []uuid.UUID `db:"template_ids" json:"template_ids"`
	UsageSeconds int64       `db:"usage_seconds" json:"usage_seconds"`
}

// GetUserActivityInsights returns the ranking with top active users.
// The result can be filtered on template_ids, meaning only user data
// from workspaces based on those templates will be included.
// Note: The usage_seconds and usage_seconds_cumulative differ only when
// requesting deployment-wide (or multiple template) data. Cumulative
// produces a bloated value if a user has used multiple templates
// simultaneously.
func (q *sqlQuerier) GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) {
	rows, err := q.db.QueryContext(ctx, getUserActivityInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetUserActivityInsightsRow
	for rows.Next() {
		var i GetUserActivityInsightsRow
		if err := rows.Scan(
			&i.UserID,
			&i.Username,
			&i.AvatarURL,
			pq.Array(&i.TemplateIDs),
			&i.UsageSeconds,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUserLatencyInsights = `-- name: GetUserLatencyInsights :many
SELECT
	tus.user_id,
	u.username,
	u.avatar_url,
	array_agg(DISTINCT tus.template_id)::uuid[] AS template_ids,
	COALESCE((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY tus.median_latency_ms)), -1)::float AS workspace_connection_latency_50,
	COALESCE((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY tus.median_latency_ms)), -1)::float AS workspace_connection_latency_95
FROM
	template_usage_stats tus
JOIN
	users u
ON
	u.id = tus.user_id
WHERE
	tus.start_time >= $1::timestamptz
	AND tus.end_time <= $2::timestamptz
	AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN tus.template_id = ANY($3::uuid[]) ELSE TRUE END
GROUP BY
	tus.user_id, u.username, u.avatar_url
ORDER BY
	tus.user_id ASC
`

type GetUserLatencyInsightsParams struct {
	StartTime   time.Time   `db:"start_time" json:"start_time"`
	EndTime     time.Time   `db:"end_time" json:"end_time"`
	TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}

type GetUserLatencyInsightsRow struct {
	UserID                       uuid.UUID   `db:"user_id" json:"user_id"`
	Username                     string      `db:"username" json:"username"`
	AvatarURL                    string      `db:"avatar_url" json:"avatar_url"`
	TemplateIDs                  []uuid.UUID `db:"template_ids" json:"template_ids"`
	WorkspaceConnectionLatency50 float64     `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
	WorkspaceConnectionLatency95 float64     `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
}

// GetUserLatencyInsights returns the median and 95th percentile connection
// latency that users have experienced. The result can be filtered on
// template_ids, meaning only user data from workspaces based on those templates
// will be included.
func (q *sqlQuerier) GetUserLatencyInsights(ctx context.Context, arg GetUserLatencyInsightsParams) ([]GetUserLatencyInsightsRow, error) {
	rows, err := q.db.QueryContext(ctx, getUserLatencyInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetUserLatencyInsightsRow
	for rows.Next() {
		var i GetUserLatencyInsightsRow
		if err := rows.Scan(
			&i.UserID,
			&i.Username,
			&i.AvatarURL,
			pq.Array(&i.TemplateIDs),
			&i.WorkspaceConnectionLatency50,
			&i.WorkspaceConnectionLatency95,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUserStatusCounts = `-- name: GetUserStatusCounts :many
WITH
system_users AS (
    SELECT id FROM users WHERE is_system = TRUE
),
	-- dates_of_interest generates the dates that will represent the horizontal axis of the chart.
dates_of_interest AS (
  SELECT timezone($1::text, gs_local) AS date
  FROM generate_series(
    timezone($1::text, $2::timestamptz),
    timezone($1::text, $3::timestamptz),
    interval '1 day'
  ) AS gs_local
),
	-- latest_status_before_range selects the last status of each user before the start_time.
	-- This represents the status of all users at the start of the time range.
latest_status_before_range AS (
    SELECT
        DISTINCT usc.user_id,
        usc.new_status,
        usc.changed_at
    FROM user_status_changes usc
	LEFT JOIN LATERAL (
		SELECT COUNT(*) > 0 AS deleted
		FROM user_deleted ud
		WHERE ud.user_id = usc.user_id AND (ud.deleted_at < usc.changed_at OR ud.deleted_at < $2)
	) AS ud ON true
    WHERE usc.user_id NOT IN (SELECT id FROM system_users)
        AND NOT ud.deleted
        AND usc.changed_at < $2::timestamptz
    ORDER BY usc.user_id, usc.changed_at DESC
),
	-- status_changes_during_range selects the statuses of each user during the start_time and end_time.
status_changes_during_range AS (
    SELECT
        usc.user_id,
        usc.new_status,
        usc.changed_at
    FROM user_status_changes usc
	LEFT JOIN LATERAL (
		SELECT COUNT(*) > 0 AS deleted
		FROM user_deleted ud
		WHERE ud.user_id = usc.user_id AND ud.deleted_at < usc.changed_at
	) AS ud ON true
    WHERE usc.user_id NOT IN (SELECT id FROM system_users)
        AND NOT ud.deleted
        AND usc.changed_at >= $2::timestamptz
        AND usc.changed_at <= $3::timestamptz
),
relevant_status_changes AS (
    SELECT user_id, new_status, changed_at
    FROM latest_status_before_range

    UNION ALL

    SELECT user_id, new_status, changed_at
    FROM status_changes_during_range
),
	-- statuses selects all the distinct statuses that were present just before and during the time range.
	-- Each status will have a series on the chart.
statuses AS (
	SELECT DISTINCT new_status FROM relevant_status_changes
),
	-- ranked_status_change_per_user_per_date selects the latest status change for each user on each date.
	-- The last status for a user on every given date will be counted.
ranked_status_change_per_user_per_date AS (
	SELECT
	d.date,
	rsc1.user_id,
	ROW_NUMBER() OVER (PARTITION BY d.date, rsc1.user_id ORDER BY rsc1.changed_at DESC) AS rn,
	rsc1.new_status
	FROM dates_of_interest d
	LEFT JOIN relevant_status_changes rsc1 ON rsc1.changed_at <= d.date
)
SELECT
	rscpupd.date::timestamptz AS date,
	statuses.new_status AS status,
	COUNT(rscpupd.user_id) FILTER (
		WHERE rscpupd.rn = 1
		AND (
			rscpupd.new_status = statuses.new_status
			AND (
				-- Include users who haven't been deleted
				NOT EXISTS (SELECT 1 FROM user_deleted WHERE user_id = rscpupd.user_id)
				OR
				-- Or users whose deletion date is after the current date we're looking at
				rscpupd.date < (SELECT deleted_at FROM user_deleted WHERE user_id = rscpupd.user_id)
			)
		)
	) AS count
FROM ranked_status_change_per_user_per_date rscpupd
CROSS JOIN statuses
GROUP BY rscpupd.date, statuses.new_status
ORDER BY rscpupd.date
`

type GetUserStatusCountsParams struct {
	Tz        string    `db:"tz" json:"tz"`
	StartTime time.Time `db:"start_time" json:"start_time"`
	EndTime   time.Time `db:"end_time" json:"end_time"`
}

type GetUserStatusCountsRow struct {
	Date   time.Time  `db:"date" json:"date"`
	Status UserStatus `db:"status" json:"status"`
	Count  int64      `db:"count" json:"count"`
}

// GetUserStatusCounts returns the count of users in each status over time.
// The time range is inclusively defined by the start_time and end_time parameters.
func (q *sqlQuerier) GetUserStatusCounts(ctx context.Context, arg GetUserStatusCountsParams) ([]GetUserStatusCountsRow, error) {
	rows, err := q.db.QueryContext(ctx, getUserStatusCounts, arg.Tz, arg.StartTime, arg.EndTime)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetUserStatusCountsRow
	for rows.Next() {
		var i GetUserStatusCountsRow
		if err := rows.Scan(&i.Date, &i.Status, &i.Count); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const upsertTemplateUsageStats = `-- name: UpsertTemplateUsageStats :exec
WITH
	latest_start AS (
		SELECT
			-- Truncate to hour so that we always look at even ranges of data.
			date_trunc('hour', COALESCE(
				MAX(start_time) - '1 hour'::interval,
				-- Fallback when there are no template usage stats yet.
				-- App stats can exist before this, but not agent stats,
				-- limit the lookback to avoid inconsistency.
				(SELECT MIN(created_at) FROM workspace_agent_stats)
			)) AS t
		FROM
			template_usage_stats
	),
	filtered_app_stats AS (
        SELECT
            was.workspace_id,
            was.user_id,
            was.agent_id,
            was.access_method,
            was.slug_or_port,
            was.session_started_at,
            was.session_ended_at
        FROM
            workspace_app_stats AS was
        WHERE
            was.session_ended_at >= (SELECT t FROM latest_start)
            AND was.session_started_at < NOW()
    ),
	workspace_app_stat_buckets AS (
		SELECT
			-- Truncate the minute to the nearest half hour, this is the bucket size
			-- for the data.
			date_trunc('hour', s.minute_bucket) + trunc(date_part('minute', s.minute_bucket) / 30) * 30 * '1 minute'::interval AS time_bucket,
			w.template_id,
			fas.user_id,
			-- Both app stats and agent stats track web terminal usage, but
			-- by different means. The app stats value should be more
			-- accurate so we don't want to discard it just yet.
			CASE
				WHEN fas.access_method = 'terminal'
				THEN '[terminal]' -- Unique name, app names can't contain brackets.
				ELSE fas.slug_or_port
			END AS app_name,
			COUNT(DISTINCT s.minute_bucket) AS app_minutes,
			-- Store each unique minute bucket for later merge between datasets.
			array_agg(DISTINCT s.minute_bucket) AS minute_buckets
		FROM
			filtered_app_stats AS fas
		JOIN
			workspaces AS w
		ON
			w.id = fas.workspace_id
		-- Generate a series of minute buckets for each session for computing the
		-- mintes/bucket.
		CROSS JOIN
			generate_series(
				date_trunc('minute', fas.session_started_at),
				-- Subtract 1 μs to avoid creating an extra series.
				date_trunc('minute', fas.session_ended_at - '1 microsecond'::interval),
				'1 minute'::interval
			) AS s(minute_bucket)
		WHERE
			-- s.minute_bucket >= @start_time::timestamptz
			-- AND s.minute_bucket < @end_time::timestamptz
			s.minute_bucket >= (SELECT t FROM latest_start)
			AND s.minute_bucket < NOW()
		GROUP BY
			time_bucket, w.template_id, fas.user_id, fas.access_method, fas.slug_or_port
	),
	agent_stats_buckets AS (
		SELECT
			-- Truncate the minute to the nearest half hour, this is the bucket size
			-- for the data.
			date_trunc('hour', created_at) + trunc(date_part('minute', created_at) / 30) * 30 * '1 minute'::interval AS time_bucket,
			template_id,
			user_id,
			-- Store each unique minute bucket for later merge between datasets.
			array_agg(
				DISTINCT CASE
				WHEN
					session_count_ssh > 0
					-- TODO(mafredri): Enable when we have the column.
					-- OR session_count_sftp > 0
					OR session_count_reconnecting_pty > 0
					OR session_count_vscode > 0
					OR session_count_jetbrains > 0
				THEN
					date_trunc('minute', created_at)
				ELSE
					NULL
				END
			) AS minute_buckets,
			COUNT(DISTINCT CASE WHEN session_count_ssh > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS ssh_mins,
			-- TODO(mafredri): Enable when we have the column.
			-- COUNT(DISTINCT CASE WHEN session_count_sftp > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS sftp_mins,
			COUNT(DISTINCT CASE WHEN session_count_reconnecting_pty > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS reconnecting_pty_mins,
			COUNT(DISTINCT CASE WHEN session_count_vscode > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS vscode_mins,
			COUNT(DISTINCT CASE WHEN session_count_jetbrains > 0 THEN date_trunc('minute', created_at) ELSE NULL END) AS jetbrains_mins,
			-- NOTE(mafredri): The agent stats are currently very unreliable, and
			-- sometimes the connections are missing, even during active sessions.
			-- Since we can't fully rely on this, we check for "any connection
			-- during this half-hour". A better solution here would be preferable.
			MAX(connection_count) > 0 AS has_connection
		FROM
			workspace_agent_stats
		WHERE
			-- created_at >= @start_time::timestamptz
			-- AND created_at < @end_time::timestamptz
			created_at >= (SELECT t FROM latest_start)
			AND created_at < NOW()
			-- Inclusion criteria to filter out empty results.
			AND (
				session_count_ssh > 0
				-- TODO(mafredri): Enable when we have the column.
				-- OR session_count_sftp > 0
				OR session_count_reconnecting_pty > 0
				OR session_count_vscode > 0
				OR session_count_jetbrains > 0
			)
		GROUP BY
			time_bucket, template_id, user_id
	),
	stats AS (
		SELECT
			stats.time_bucket AS start_time,
			stats.time_bucket + '30 minutes'::interval AS end_time,
			stats.template_id,
			stats.user_id,
			-- Sum/distinct to handle zero/duplicate values due union and to unnest.
			COUNT(DISTINCT minute_bucket) AS usage_mins,
			array_agg(DISTINCT minute_bucket) AS minute_buckets,
			SUM(DISTINCT stats.ssh_mins) AS ssh_mins,
			SUM(DISTINCT stats.sftp_mins) AS sftp_mins,
			SUM(DISTINCT stats.reconnecting_pty_mins) AS reconnecting_pty_mins,
			SUM(DISTINCT stats.vscode_mins) AS vscode_mins,
			SUM(DISTINCT stats.jetbrains_mins) AS jetbrains_mins,
			-- This is what we unnested, re-nest as json.
			jsonb_object_agg(stats.app_name, stats.app_minutes) FILTER (WHERE stats.app_name IS NOT NULL) AS app_usage_mins
		FROM (
			SELECT
				time_bucket,
				template_id,
				user_id,
				0 AS ssh_mins,
				0 AS sftp_mins,
				0 AS reconnecting_pty_mins,
				0 AS vscode_mins,
				0 AS jetbrains_mins,
				app_name,
				app_minutes,
				minute_buckets
			FROM
				workspace_app_stat_buckets

			UNION ALL

			SELECT
				time_bucket,
				template_id,
				user_id,
				ssh_mins,
				-- TODO(mafredri): Enable when we have the column.
				0 AS sftp_mins,
				reconnecting_pty_mins,
				vscode_mins,
				jetbrains_mins,
				NULL AS app_name,
				NULL AS app_minutes,
				minute_buckets
			FROM
				agent_stats_buckets
			WHERE
				-- See note in the agent_stats_buckets CTE.
				has_connection
		) AS stats, unnest(minute_buckets) AS minute_bucket
		GROUP BY
			stats.time_bucket, stats.template_id, stats.user_id
	),
	minute_buckets AS (
		-- Create distinct minute buckets for user-activity, so we can filter out
		-- irrelevant latencies.
		SELECT DISTINCT ON (stats.start_time, stats.template_id, stats.user_id, minute_bucket)
			stats.start_time,
			stats.template_id,
			stats.user_id,
			minute_bucket
		FROM
			stats, unnest(minute_buckets) AS minute_bucket
	),
	latencies AS (
		-- Select all non-zero latencies for all the minutes that a user used the
		-- workspace in some way.
		SELECT
			mb.start_time,
			mb.template_id,
			mb.user_id,
			-- TODO(mafredri): We're doing medians on medians here, we may want to
			-- improve upon this at some point.
			PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY was.connection_median_latency_ms)::real AS median_latency_ms
		FROM
			minute_buckets AS mb
		JOIN
			workspace_agent_stats AS was
		ON
			was.created_at >= (SELECT t FROM latest_start)
			AND was.created_at < NOW()
			AND date_trunc('minute', was.created_at) = mb.minute_bucket
			AND was.template_id = mb.template_id
			AND was.user_id = mb.user_id
			AND was.connection_median_latency_ms > 0
		GROUP BY
			mb.start_time, mb.template_id, mb.user_id
	)

INSERT INTO template_usage_stats AS tus (
	start_time,
	end_time,
	template_id,
	user_id,
	usage_mins,
	median_latency_ms,
	ssh_mins,
	sftp_mins,
	reconnecting_pty_mins,
	vscode_mins,
	jetbrains_mins,
	app_usage_mins
) (
	SELECT
		stats.start_time,
		stats.end_time,
		stats.template_id,
		stats.user_id,
		stats.usage_mins,
		latencies.median_latency_ms,
		stats.ssh_mins,
		stats.sftp_mins,
		stats.reconnecting_pty_mins,
		stats.vscode_mins,
		stats.jetbrains_mins,
		stats.app_usage_mins
	FROM
		stats
	LEFT JOIN
		latencies
	ON
		-- The latencies group-by ensures there at most one row.
		latencies.start_time = stats.start_time
		AND latencies.template_id = stats.template_id
		AND latencies.user_id = stats.user_id
)
ON CONFLICT
	(start_time, template_id, user_id)
DO UPDATE
SET
	usage_mins = EXCLUDED.usage_mins,
	median_latency_ms = EXCLUDED.median_latency_ms,
	ssh_mins = EXCLUDED.ssh_mins,
	sftp_mins = EXCLUDED.sftp_mins,
	reconnecting_pty_mins = EXCLUDED.reconnecting_pty_mins,
	vscode_mins = EXCLUDED.vscode_mins,
	jetbrains_mins = EXCLUDED.jetbrains_mins,
	app_usage_mins = EXCLUDED.app_usage_mins
WHERE
	(tus.*) IS DISTINCT FROM (EXCLUDED.*)
`

// This query aggregates the workspace_agent_stats and workspace_app_stats data
// into a single table for efficient storage and querying. Half-hour buckets are
// used to store the data, and the minutes are summed for each user and template
// combination. The result is stored in the template_usage_stats table.
func (q *sqlQuerier) UpsertTemplateUsageStats(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, upsertTemplateUsageStats)
	return err
}

const deleteLicense = `-- name: DeleteLicense :one
DELETE
FROM licenses
WHERE id = $1
RETURNING id
`

func (q *sqlQuerier) DeleteLicense(ctx context.Context, id int32) (int32, error) {
	row := q.db.QueryRowContext(ctx, deleteLicense, id)
	var id_2 int32
	err := row.Scan(&id_2)
	return id_2, err
}

const getLicenseByID = `-- name: GetLicenseByID :one
SELECT
	id, uploaded_at, jwt, exp, uuid
FROM
	licenses
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetLicenseByID(ctx context.Context, id int32) (License, error) {
	row := q.db.QueryRowContext(ctx, getLicenseByID, id)
	var i License
	err := row.Scan(
		&i.ID,
		&i.UploadedAt,
		&i.JWT,
		&i.Exp,
		&i.UUID,
	)
	return i, err
}

const getLicenses = `-- name: GetLicenses :many
SELECT id, uploaded_at, jwt, exp, uuid
FROM licenses
ORDER BY (id)
`

func (q *sqlQuerier) GetLicenses(ctx context.Context) ([]License, error) {
	rows, err := q.db.QueryContext(ctx, getLicenses)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []License
	for rows.Next() {
		var i License
		if err := rows.Scan(
			&i.ID,
			&i.UploadedAt,
			&i.JWT,
			&i.Exp,
			&i.UUID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUnexpiredLicenses = `-- name: GetUnexpiredLicenses :many
SELECT id, uploaded_at, jwt, exp, uuid
FROM licenses
WHERE exp > NOW()
ORDER BY (id)
`

func (q *sqlQuerier) GetUnexpiredLicenses(ctx context.Context) ([]License, error) {
	rows, err := q.db.QueryContext(ctx, getUnexpiredLicenses)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []License
	for rows.Next() {
		var i License
		if err := rows.Scan(
			&i.ID,
			&i.UploadedAt,
			&i.JWT,
			&i.Exp,
			&i.UUID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertLicense = `-- name: InsertLicense :one
INSERT INTO
	licenses (
	uploaded_at,
	jwt,
	exp,
	uuid
)
VALUES
	($1, $2, $3, $4) RETURNING id, uploaded_at, jwt, exp, uuid
`

type InsertLicenseParams struct {
	UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
	JWT        string    `db:"jwt" json:"jwt"`
	Exp        time.Time `db:"exp" json:"exp"`
	UUID       uuid.UUID `db:"uuid" json:"uuid"`
}

func (q *sqlQuerier) InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error) {
	row := q.db.QueryRowContext(ctx, insertLicense,
		arg.UploadedAt,
		arg.JWT,
		arg.Exp,
		arg.UUID,
	)
	var i License
	err := row.Scan(
		&i.ID,
		&i.UploadedAt,
		&i.JWT,
		&i.Exp,
		&i.UUID,
	)
	return i, err
}

const acquireLock = `-- name: AcquireLock :exec
SELECT pg_advisory_xact_lock($1)
`

// Blocks until the lock is acquired.
//
// This must be called from within a transaction. The lock will be automatically
// released when the transaction ends.
func (q *sqlQuerier) AcquireLock(ctx context.Context, pgAdvisoryXactLock int64) error {
	_, err := q.db.ExecContext(ctx, acquireLock, pgAdvisoryXactLock)
	return err
}

const tryAcquireLock = `-- name: TryAcquireLock :one
SELECT pg_try_advisory_xact_lock($1)
`

// Non blocking lock. Returns true if the lock was acquired, false otherwise.
//
// This must be called from within a transaction. The lock will be automatically
// released when the transaction ends.
func (q *sqlQuerier) TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error) {
	row := q.db.QueryRowContext(ctx, tryAcquireLock, pgTryAdvisoryXactLock)
	var pg_try_advisory_xact_lock bool
	err := row.Scan(&pg_try_advisory_xact_lock)
	return pg_try_advisory_xact_lock, err
}

const cleanupDeletedMCPServerIDsFromChats = `-- name: CleanupDeletedMCPServerIDsFromChats :exec
UPDATE chats
SET mcp_server_ids = (
    SELECT COALESCE(array_agg(sid), '{}')
    FROM unnest(chats.mcp_server_ids) AS sid
    WHERE sid IN (SELECT id FROM mcp_server_configs)
)
WHERE mcp_server_ids != '{}'
  AND NOT (mcp_server_ids <@ COALESCE((SELECT array_agg(id) FROM mcp_server_configs), '{}'))
`

func (q *sqlQuerier) CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, cleanupDeletedMCPServerIDsFromChats)
	return err
}

const deleteMCPServerConfigByID = `-- name: DeleteMCPServerConfigByID :exec
DELETE FROM
    mcp_server_configs
WHERE
    id = $1::uuid
`

func (q *sqlQuerier) DeleteMCPServerConfigByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteMCPServerConfigByID, id)
	return err
}

const deleteMCPServerUserToken = `-- name: DeleteMCPServerUserToken :exec
DELETE FROM
    mcp_server_user_tokens
WHERE
    mcp_server_config_id = $1::uuid
    AND user_id = $2::uuid
`

type DeleteMCPServerUserTokenParams struct {
	MCPServerConfigID uuid.UUID `db:"mcp_server_config_id" json:"mcp_server_config_id"`
	UserID            uuid.UUID `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) DeleteMCPServerUserToken(ctx context.Context, arg DeleteMCPServerUserTokenParams) error {
	_, err := q.db.ExecContext(ctx, deleteMCPServerUserToken, arg.MCPServerConfigID, arg.UserID)
	return err
}

const getEnabledMCPServerConfigs = `-- name: GetEnabledMCPServerConfigs :many
SELECT
    id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
FROM
    mcp_server_configs
WHERE
    enabled = TRUE
ORDER BY
    display_name ASC
`

func (q *sqlQuerier) GetEnabledMCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error) {
	rows, err := q.db.QueryContext(ctx, getEnabledMCPServerConfigs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []MCPServerConfig
	for rows.Next() {
		var i MCPServerConfig
		if err := rows.Scan(
			&i.ID,
			&i.DisplayName,
			&i.Slug,
			&i.Description,
			&i.IconURL,
			&i.Transport,
			&i.Url,
			&i.AuthType,
			&i.OAuth2ClientID,
			&i.OAuth2ClientSecret,
			&i.OAuth2ClientSecretKeyID,
			&i.OAuth2AuthURL,
			&i.OAuth2TokenURL,
			&i.OAuth2Scopes,
			&i.APIKeyHeader,
			&i.APIKeyValue,
			&i.APIKeyValueKeyID,
			&i.CustomHeaders,
			&i.CustomHeadersKeyID,
			pq.Array(&i.ToolAllowList),
			pq.Array(&i.ToolDenyList),
			&i.Availability,
			&i.Enabled,
			&i.CreatedBy,
			&i.UpdatedBy,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ModelIntent,
			&i.AllowInPlanMode,
			&i.ForwardCoderHeaders,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getForcedMCPServerConfigs = `-- name: GetForcedMCPServerConfigs :many
SELECT
    id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
FROM
    mcp_server_configs
WHERE
    enabled = TRUE
    AND availability = 'force_on'
ORDER BY
    display_name ASC
`

func (q *sqlQuerier) GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error) {
	rows, err := q.db.QueryContext(ctx, getForcedMCPServerConfigs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []MCPServerConfig
	for rows.Next() {
		var i MCPServerConfig
		if err := rows.Scan(
			&i.ID,
			&i.DisplayName,
			&i.Slug,
			&i.Description,
			&i.IconURL,
			&i.Transport,
			&i.Url,
			&i.AuthType,
			&i.OAuth2ClientID,
			&i.OAuth2ClientSecret,
			&i.OAuth2ClientSecretKeyID,
			&i.OAuth2AuthURL,
			&i.OAuth2TokenURL,
			&i.OAuth2Scopes,
			&i.APIKeyHeader,
			&i.APIKeyValue,
			&i.APIKeyValueKeyID,
			&i.CustomHeaders,
			&i.CustomHeadersKeyID,
			pq.Array(&i.ToolAllowList),
			pq.Array(&i.ToolDenyList),
			&i.Availability,
			&i.Enabled,
			&i.CreatedBy,
			&i.UpdatedBy,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ModelIntent,
			&i.AllowInPlanMode,
			&i.ForwardCoderHeaders,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getMCPServerConfigByID = `-- name: GetMCPServerConfigByID :one
SELECT
    id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
FROM
    mcp_server_configs
WHERE
    id = $1::uuid
`

func (q *sqlQuerier) GetMCPServerConfigByID(ctx context.Context, id uuid.UUID) (MCPServerConfig, error) {
	row := q.db.QueryRowContext(ctx, getMCPServerConfigByID, id)
	var i MCPServerConfig
	err := row.Scan(
		&i.ID,
		&i.DisplayName,
		&i.Slug,
		&i.Description,
		&i.IconURL,
		&i.Transport,
		&i.Url,
		&i.AuthType,
		&i.OAuth2ClientID,
		&i.OAuth2ClientSecret,
		&i.OAuth2ClientSecretKeyID,
		&i.OAuth2AuthURL,
		&i.OAuth2TokenURL,
		&i.OAuth2Scopes,
		&i.APIKeyHeader,
		&i.APIKeyValue,
		&i.APIKeyValueKeyID,
		&i.CustomHeaders,
		&i.CustomHeadersKeyID,
		pq.Array(&i.ToolAllowList),
		pq.Array(&i.ToolDenyList),
		&i.Availability,
		&i.Enabled,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ModelIntent,
		&i.AllowInPlanMode,
		&i.ForwardCoderHeaders,
	)
	return i, err
}

const getMCPServerConfigBySlug = `-- name: GetMCPServerConfigBySlug :one
SELECT
    id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
FROM
    mcp_server_configs
WHERE
    slug = $1::text
`

func (q *sqlQuerier) GetMCPServerConfigBySlug(ctx context.Context, slug string) (MCPServerConfig, error) {
	row := q.db.QueryRowContext(ctx, getMCPServerConfigBySlug, slug)
	var i MCPServerConfig
	err := row.Scan(
		&i.ID,
		&i.DisplayName,
		&i.Slug,
		&i.Description,
		&i.IconURL,
		&i.Transport,
		&i.Url,
		&i.AuthType,
		&i.OAuth2ClientID,
		&i.OAuth2ClientSecret,
		&i.OAuth2ClientSecretKeyID,
		&i.OAuth2AuthURL,
		&i.OAuth2TokenURL,
		&i.OAuth2Scopes,
		&i.APIKeyHeader,
		&i.APIKeyValue,
		&i.APIKeyValueKeyID,
		&i.CustomHeaders,
		&i.CustomHeadersKeyID,
		pq.Array(&i.ToolAllowList),
		pq.Array(&i.ToolDenyList),
		&i.Availability,
		&i.Enabled,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ModelIntent,
		&i.AllowInPlanMode,
		&i.ForwardCoderHeaders,
	)
	return i, err
}

const getMCPServerConfigs = `-- name: GetMCPServerConfigs :many
SELECT
    id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
FROM
    mcp_server_configs
ORDER BY
    display_name ASC
`

func (q *sqlQuerier) GetMCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error) {
	rows, err := q.db.QueryContext(ctx, getMCPServerConfigs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []MCPServerConfig
	for rows.Next() {
		var i MCPServerConfig
		if err := rows.Scan(
			&i.ID,
			&i.DisplayName,
			&i.Slug,
			&i.Description,
			&i.IconURL,
			&i.Transport,
			&i.Url,
			&i.AuthType,
			&i.OAuth2ClientID,
			&i.OAuth2ClientSecret,
			&i.OAuth2ClientSecretKeyID,
			&i.OAuth2AuthURL,
			&i.OAuth2TokenURL,
			&i.OAuth2Scopes,
			&i.APIKeyHeader,
			&i.APIKeyValue,
			&i.APIKeyValueKeyID,
			&i.CustomHeaders,
			&i.CustomHeadersKeyID,
			pq.Array(&i.ToolAllowList),
			pq.Array(&i.ToolDenyList),
			&i.Availability,
			&i.Enabled,
			&i.CreatedBy,
			&i.UpdatedBy,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ModelIntent,
			&i.AllowInPlanMode,
			&i.ForwardCoderHeaders,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getMCPServerConfigsByIDs = `-- name: GetMCPServerConfigsByIDs :many
SELECT
    id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
FROM
    mcp_server_configs
WHERE
    id = ANY($1::uuid[])
ORDER BY
    display_name ASC
`

func (q *sqlQuerier) GetMCPServerConfigsByIDs(ctx context.Context, ids []uuid.UUID) ([]MCPServerConfig, error) {
	rows, err := q.db.QueryContext(ctx, getMCPServerConfigsByIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []MCPServerConfig
	for rows.Next() {
		var i MCPServerConfig
		if err := rows.Scan(
			&i.ID,
			&i.DisplayName,
			&i.Slug,
			&i.Description,
			&i.IconURL,
			&i.Transport,
			&i.Url,
			&i.AuthType,
			&i.OAuth2ClientID,
			&i.OAuth2ClientSecret,
			&i.OAuth2ClientSecretKeyID,
			&i.OAuth2AuthURL,
			&i.OAuth2TokenURL,
			&i.OAuth2Scopes,
			&i.APIKeyHeader,
			&i.APIKeyValue,
			&i.APIKeyValueKeyID,
			&i.CustomHeaders,
			&i.CustomHeadersKeyID,
			pq.Array(&i.ToolAllowList),
			pq.Array(&i.ToolDenyList),
			&i.Availability,
			&i.Enabled,
			&i.CreatedBy,
			&i.UpdatedBy,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ModelIntent,
			&i.AllowInPlanMode,
			&i.ForwardCoderHeaders,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getMCPServerUserToken = `-- name: GetMCPServerUserToken :one
SELECT
    id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at
FROM
    mcp_server_user_tokens
WHERE
    mcp_server_config_id = $1::uuid
    AND user_id = $2::uuid
`

type GetMCPServerUserTokenParams struct {
	MCPServerConfigID uuid.UUID `db:"mcp_server_config_id" json:"mcp_server_config_id"`
	UserID            uuid.UUID `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) GetMCPServerUserToken(ctx context.Context, arg GetMCPServerUserTokenParams) (MCPServerUserToken, error) {
	row := q.db.QueryRowContext(ctx, getMCPServerUserToken, arg.MCPServerConfigID, arg.UserID)
	var i MCPServerUserToken
	err := row.Scan(
		&i.ID,
		&i.MCPServerConfigID,
		&i.UserID,
		&i.AccessToken,
		&i.AccessTokenKeyID,
		&i.RefreshToken,
		&i.RefreshTokenKeyID,
		&i.TokenType,
		&i.Expiry,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getMCPServerUserTokensByUserID = `-- name: GetMCPServerUserTokensByUserID :many
SELECT
    id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at
FROM
    mcp_server_user_tokens
WHERE
    user_id = $1::uuid
`

func (q *sqlQuerier) GetMCPServerUserTokensByUserID(ctx context.Context, userID uuid.UUID) ([]MCPServerUserToken, error) {
	rows, err := q.db.QueryContext(ctx, getMCPServerUserTokensByUserID, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []MCPServerUserToken
	for rows.Next() {
		var i MCPServerUserToken
		if err := rows.Scan(
			&i.ID,
			&i.MCPServerConfigID,
			&i.UserID,
			&i.AccessToken,
			&i.AccessTokenKeyID,
			&i.RefreshToken,
			&i.RefreshTokenKeyID,
			&i.TokenType,
			&i.Expiry,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertMCPServerConfig = `-- name: InsertMCPServerConfig :one
INSERT INTO mcp_server_configs (
    display_name,
    slug,
    description,
    icon_url,
    transport,
    url,
    auth_type,
    oauth2_client_id,
    oauth2_client_secret,
    oauth2_client_secret_key_id,
    oauth2_auth_url,
    oauth2_token_url,
    oauth2_scopes,
    api_key_header,
    api_key_value,
    api_key_value_key_id,
    custom_headers,
    custom_headers_key_id,
    tool_allow_list,
    tool_deny_list,
    availability,
    enabled,
    model_intent,
    allow_in_plan_mode,
    forward_coder_headers,
    created_by,
    updated_by
) VALUES (
    $1::text,
    $2::text,
    $3::text,
    $4::text,
    $5::text,
    $6::text,
    $7::text,
    $8::text,
    $9::text,
    $10::text,
    $11::text,
    $12::text,
    $13::text,
    $14::text,
    $15::text,
    $16::text,
    $17::text,
    $18::text,
    $19::text[],
    $20::text[],
    $21::text,
    $22::boolean,
    $23::boolean,
    $24::boolean,
    $25::boolean,
    $26::uuid,
    $27::uuid
)
RETURNING
    id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
`

type InsertMCPServerConfigParams struct {
	DisplayName             string         `db:"display_name" json:"display_name"`
	Slug                    string         `db:"slug" json:"slug"`
	Description             string         `db:"description" json:"description"`
	IconURL                 string         `db:"icon_url" json:"icon_url"`
	Transport               string         `db:"transport" json:"transport"`
	Url                     string         `db:"url" json:"url"`
	AuthType                string         `db:"auth_type" json:"auth_type"`
	OAuth2ClientID          string         `db:"oauth2_client_id" json:"oauth2_client_id"`
	OAuth2ClientSecret      string         `db:"oauth2_client_secret" json:"oauth2_client_secret"`
	OAuth2ClientSecretKeyID sql.NullString `db:"oauth2_client_secret_key_id" json:"oauth2_client_secret_key_id"`
	OAuth2AuthURL           string         `db:"oauth2_auth_url" json:"oauth2_auth_url"`
	OAuth2TokenURL          string         `db:"oauth2_token_url" json:"oauth2_token_url"`
	OAuth2Scopes            string         `db:"oauth2_scopes" json:"oauth2_scopes"`
	APIKeyHeader            string         `db:"api_key_header" json:"api_key_header"`
	APIKeyValue             string         `db:"api_key_value" json:"api_key_value"`
	APIKeyValueKeyID        sql.NullString `db:"api_key_value_key_id" json:"api_key_value_key_id"`
	CustomHeaders           string         `db:"custom_headers" json:"custom_headers"`
	CustomHeadersKeyID      sql.NullString `db:"custom_headers_key_id" json:"custom_headers_key_id"`
	ToolAllowList           []string       `db:"tool_allow_list" json:"tool_allow_list"`
	ToolDenyList            []string       `db:"tool_deny_list" json:"tool_deny_list"`
	Availability            string         `db:"availability" json:"availability"`
	Enabled                 bool           `db:"enabled" json:"enabled"`
	ModelIntent             bool           `db:"model_intent" json:"model_intent"`
	AllowInPlanMode         bool           `db:"allow_in_plan_mode" json:"allow_in_plan_mode"`
	ForwardCoderHeaders     bool           `db:"forward_coder_headers" json:"forward_coder_headers"`
	CreatedBy               uuid.UUID      `db:"created_by" json:"created_by"`
	UpdatedBy               uuid.UUID      `db:"updated_by" json:"updated_by"`
}

func (q *sqlQuerier) InsertMCPServerConfig(ctx context.Context, arg InsertMCPServerConfigParams) (MCPServerConfig, error) {
	row := q.db.QueryRowContext(ctx, insertMCPServerConfig,
		arg.DisplayName,
		arg.Slug,
		arg.Description,
		arg.IconURL,
		arg.Transport,
		arg.Url,
		arg.AuthType,
		arg.OAuth2ClientID,
		arg.OAuth2ClientSecret,
		arg.OAuth2ClientSecretKeyID,
		arg.OAuth2AuthURL,
		arg.OAuth2TokenURL,
		arg.OAuth2Scopes,
		arg.APIKeyHeader,
		arg.APIKeyValue,
		arg.APIKeyValueKeyID,
		arg.CustomHeaders,
		arg.CustomHeadersKeyID,
		pq.Array(arg.ToolAllowList),
		pq.Array(arg.ToolDenyList),
		arg.Availability,
		arg.Enabled,
		arg.ModelIntent,
		arg.AllowInPlanMode,
		arg.ForwardCoderHeaders,
		arg.CreatedBy,
		arg.UpdatedBy,
	)
	var i MCPServerConfig
	err := row.Scan(
		&i.ID,
		&i.DisplayName,
		&i.Slug,
		&i.Description,
		&i.IconURL,
		&i.Transport,
		&i.Url,
		&i.AuthType,
		&i.OAuth2ClientID,
		&i.OAuth2ClientSecret,
		&i.OAuth2ClientSecretKeyID,
		&i.OAuth2AuthURL,
		&i.OAuth2TokenURL,
		&i.OAuth2Scopes,
		&i.APIKeyHeader,
		&i.APIKeyValue,
		&i.APIKeyValueKeyID,
		&i.CustomHeaders,
		&i.CustomHeadersKeyID,
		pq.Array(&i.ToolAllowList),
		pq.Array(&i.ToolDenyList),
		&i.Availability,
		&i.Enabled,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ModelIntent,
		&i.AllowInPlanMode,
		&i.ForwardCoderHeaders,
	)
	return i, err
}

const updateMCPServerConfig = `-- name: UpdateMCPServerConfig :one
UPDATE
    mcp_server_configs
SET
    display_name = $1::text,
    slug = $2::text,
    description = $3::text,
    icon_url = $4::text,
    transport = $5::text,
    url = $6::text,
    auth_type = $7::text,
    oauth2_client_id = $8::text,
    oauth2_client_secret = $9::text,
    oauth2_client_secret_key_id = $10::text,
    oauth2_auth_url = $11::text,
    oauth2_token_url = $12::text,
    oauth2_scopes = $13::text,
    api_key_header = $14::text,
    api_key_value = $15::text,
    api_key_value_key_id = $16::text,
    custom_headers = $17::text,
    custom_headers_key_id = $18::text,
    tool_allow_list = $19::text[],
    tool_deny_list = $20::text[],
    availability = $21::text,
    enabled = $22::boolean,
    model_intent = $23::boolean,
    allow_in_plan_mode = $24::boolean,
    forward_coder_headers = $25::boolean,
    updated_by = $26::uuid,
    updated_at = NOW()
WHERE
    id = $27::uuid
RETURNING
    id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
`

type UpdateMCPServerConfigParams struct {
	DisplayName             string         `db:"display_name" json:"display_name"`
	Slug                    string         `db:"slug" json:"slug"`
	Description             string         `db:"description" json:"description"`
	IconURL                 string         `db:"icon_url" json:"icon_url"`
	Transport               string         `db:"transport" json:"transport"`
	Url                     string         `db:"url" json:"url"`
	AuthType                string         `db:"auth_type" json:"auth_type"`
	OAuth2ClientID          string         `db:"oauth2_client_id" json:"oauth2_client_id"`
	OAuth2ClientSecret      string         `db:"oauth2_client_secret" json:"oauth2_client_secret"`
	OAuth2ClientSecretKeyID sql.NullString `db:"oauth2_client_secret_key_id" json:"oauth2_client_secret_key_id"`
	OAuth2AuthURL           string         `db:"oauth2_auth_url" json:"oauth2_auth_url"`
	OAuth2TokenURL          string         `db:"oauth2_token_url" json:"oauth2_token_url"`
	OAuth2Scopes            string         `db:"oauth2_scopes" json:"oauth2_scopes"`
	APIKeyHeader            string         `db:"api_key_header" json:"api_key_header"`
	APIKeyValue             string         `db:"api_key_value" json:"api_key_value"`
	APIKeyValueKeyID        sql.NullString `db:"api_key_value_key_id" json:"api_key_value_key_id"`
	CustomHeaders           string         `db:"custom_headers" json:"custom_headers"`
	CustomHeadersKeyID      sql.NullString `db:"custom_headers_key_id" json:"custom_headers_key_id"`
	ToolAllowList           []string       `db:"tool_allow_list" json:"tool_allow_list"`
	ToolDenyList            []string       `db:"tool_deny_list" json:"tool_deny_list"`
	Availability            string         `db:"availability" json:"availability"`
	Enabled                 bool           `db:"enabled" json:"enabled"`
	ModelIntent             bool           `db:"model_intent" json:"model_intent"`
	AllowInPlanMode         bool           `db:"allow_in_plan_mode" json:"allow_in_plan_mode"`
	ForwardCoderHeaders     bool           `db:"forward_coder_headers" json:"forward_coder_headers"`
	UpdatedBy               uuid.UUID      `db:"updated_by" json:"updated_by"`
	ID                      uuid.UUID      `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPServerConfigParams) (MCPServerConfig, error) {
	row := q.db.QueryRowContext(ctx, updateMCPServerConfig,
		arg.DisplayName,
		arg.Slug,
		arg.Description,
		arg.IconURL,
		arg.Transport,
		arg.Url,
		arg.AuthType,
		arg.OAuth2ClientID,
		arg.OAuth2ClientSecret,
		arg.OAuth2ClientSecretKeyID,
		arg.OAuth2AuthURL,
		arg.OAuth2TokenURL,
		arg.OAuth2Scopes,
		arg.APIKeyHeader,
		arg.APIKeyValue,
		arg.APIKeyValueKeyID,
		arg.CustomHeaders,
		arg.CustomHeadersKeyID,
		pq.Array(arg.ToolAllowList),
		pq.Array(arg.ToolDenyList),
		arg.Availability,
		arg.Enabled,
		arg.ModelIntent,
		arg.AllowInPlanMode,
		arg.ForwardCoderHeaders,
		arg.UpdatedBy,
		arg.ID,
	)
	var i MCPServerConfig
	err := row.Scan(
		&i.ID,
		&i.DisplayName,
		&i.Slug,
		&i.Description,
		&i.IconURL,
		&i.Transport,
		&i.Url,
		&i.AuthType,
		&i.OAuth2ClientID,
		&i.OAuth2ClientSecret,
		&i.OAuth2ClientSecretKeyID,
		&i.OAuth2AuthURL,
		&i.OAuth2TokenURL,
		&i.OAuth2Scopes,
		&i.APIKeyHeader,
		&i.APIKeyValue,
		&i.APIKeyValueKeyID,
		&i.CustomHeaders,
		&i.CustomHeadersKeyID,
		pq.Array(&i.ToolAllowList),
		pq.Array(&i.ToolDenyList),
		&i.Availability,
		&i.Enabled,
		&i.CreatedBy,
		&i.UpdatedBy,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ModelIntent,
		&i.AllowInPlanMode,
		&i.ForwardCoderHeaders,
	)
	return i, err
}

const upsertMCPServerUserToken = `-- name: UpsertMCPServerUserToken :one
INSERT INTO mcp_server_user_tokens (
    mcp_server_config_id,
    user_id,
    access_token,
    access_token_key_id,
    refresh_token,
    refresh_token_key_id,
    token_type,
    expiry
) VALUES (
    $1::uuid,
    $2::uuid,
    $3::text,
    $4::text,
    $5::text,
    $6::text,
    $7::text,
    $8::timestamptz
)
ON CONFLICT (mcp_server_config_id, user_id) DO UPDATE SET
    access_token = $3::text,
    access_token_key_id = $4::text,
    refresh_token = $5::text,
    refresh_token_key_id = $6::text,
    token_type = $7::text,
    expiry = $8::timestamptz,
    updated_at = NOW()
RETURNING
    id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at
`

type UpsertMCPServerUserTokenParams struct {
	MCPServerConfigID uuid.UUID      `db:"mcp_server_config_id" json:"mcp_server_config_id"`
	UserID            uuid.UUID      `db:"user_id" json:"user_id"`
	AccessToken       string         `db:"access_token" json:"access_token"`
	AccessTokenKeyID  sql.NullString `db:"access_token_key_id" json:"access_token_key_id"`
	RefreshToken      string         `db:"refresh_token" json:"refresh_token"`
	RefreshTokenKeyID sql.NullString `db:"refresh_token_key_id" json:"refresh_token_key_id"`
	TokenType         string         `db:"token_type" json:"token_type"`
	Expiry            sql.NullTime   `db:"expiry" json:"expiry"`
}

func (q *sqlQuerier) UpsertMCPServerUserToken(ctx context.Context, arg UpsertMCPServerUserTokenParams) (MCPServerUserToken, error) {
	row := q.db.QueryRowContext(ctx, upsertMCPServerUserToken,
		arg.MCPServerConfigID,
		arg.UserID,
		arg.AccessToken,
		arg.AccessTokenKeyID,
		arg.RefreshToken,
		arg.RefreshTokenKeyID,
		arg.TokenType,
		arg.Expiry,
	)
	var i MCPServerUserToken
	err := row.Scan(
		&i.ID,
		&i.MCPServerConfigID,
		&i.UserID,
		&i.AccessToken,
		&i.AccessTokenKeyID,
		&i.RefreshToken,
		&i.RefreshTokenKeyID,
		&i.TokenType,
		&i.Expiry,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const acquireNotificationMessages = `-- name: AcquireNotificationMessages :many
WITH acquired AS (
    UPDATE
        notification_messages
            SET queued_seconds = GREATEST(0, EXTRACT(EPOCH FROM (NOW() - updated_at)))::FLOAT,
                updated_at = NOW(),
                status = 'leased'::notification_message_status,
                status_reason = 'Leased by notifier ' || $1::uuid,
                leased_until = NOW() + CONCAT($2::int, ' seconds')::interval
            WHERE id IN (SELECT nm.id
                         FROM notification_messages AS nm
                         WHERE (
                             (
                                 -- message is in acquirable states
                                 nm.status IN (
                                               'pending'::notification_message_status,
                                               'temporary_failure'::notification_message_status
                                     )
                                 )
                                 -- or somehow the message was left in leased for longer than its lease period
                                 OR (
                                 nm.status = 'leased'::notification_message_status
                                     AND nm.leased_until < NOW()
                                 )
                             )
                           AND (
                             -- exclude all messages which have exceeded the max attempts; these will be purged later
                             nm.attempt_count IS NULL OR nm.attempt_count < $3::int
                             )
                           -- if set, do not retry until we've exceeded the wait time
                           AND (
                             CASE
                                 WHEN nm.next_retry_after IS NOT NULL THEN nm.next_retry_after < NOW()
                                 ELSE true
                                 END
                             )
                         ORDER BY nm.created_at ASC
                                  -- Ensure that multiple concurrent readers cannot retrieve the same rows
                             FOR UPDATE OF nm
                                 SKIP LOCKED
                         LIMIT $4)
            RETURNING id, notification_template_id, user_id, method, status, status_reason, created_by, payload, attempt_count, targets, created_at, updated_at, leased_until, next_retry_after, queued_seconds, dedupe_hash)
SELECT
    -- message
    nm.id,
    nm.payload,
    nm.method,
    nm.attempt_count::int                                                 AS attempt_count,
    nm.queued_seconds::float                                              AS queued_seconds,
    -- template
    nt.id                                                                 AS template_id,
    nt.title_template,
    nt.body_template,
    -- preferences
    (CASE WHEN np.disabled IS NULL THEN false ELSE np.disabled END)::bool AS disabled
FROM acquired nm
         JOIN notification_templates nt ON nm.notification_template_id = nt.id
         LEFT JOIN notification_preferences AS np
                   ON (np.user_id = nm.user_id AND np.notification_template_id = nm.notification_template_id)
`

type AcquireNotificationMessagesParams struct {
	NotifierID      uuid.UUID `db:"notifier_id" json:"notifier_id"`
	LeaseSeconds    int32     `db:"lease_seconds" json:"lease_seconds"`
	MaxAttemptCount int32     `db:"max_attempt_count" json:"max_attempt_count"`
	Count           int32     `db:"count" json:"count"`
}

type AcquireNotificationMessagesRow struct {
	ID            uuid.UUID          `db:"id" json:"id"`
	Payload       json.RawMessage    `db:"payload" json:"payload"`
	Method        NotificationMethod `db:"method" json:"method"`
	AttemptCount  int32              `db:"attempt_count" json:"attempt_count"`
	QueuedSeconds float64            `db:"queued_seconds" json:"queued_seconds"`
	TemplateID    uuid.UUID          `db:"template_id" json:"template_id"`
	TitleTemplate string             `db:"title_template" json:"title_template"`
	BodyTemplate  string             `db:"body_template" json:"body_template"`
	Disabled      bool               `db:"disabled" json:"disabled"`
}

// Acquires the lease for a given count of notification messages, to enable concurrent dequeuing and subsequent sending.
// Only rows that aren't already leased (or ones which are leased but have exceeded their lease period) are returned.
//
// A "lease" here refers to a notifier taking ownership of a notification_messages row. A lease survives for the duration
// of CODER_NOTIFICATIONS_LEASE_PERIOD. Once a message is delivered, its status is updated and the lease expires (set to NULL).
// If a message exceeds its lease, that implies the notifier did not shutdown cleanly, or the table update failed somehow,
// and the row will then be eligible to be dequeued by another notifier.
//
// SKIP LOCKED is used to jump over locked rows. This prevents multiple notifiers from acquiring the same messages.
// See: https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE
func (q *sqlQuerier) AcquireNotificationMessages(ctx context.Context, arg AcquireNotificationMessagesParams) ([]AcquireNotificationMessagesRow, error) {
	rows, err := q.db.QueryContext(ctx, acquireNotificationMessages,
		arg.NotifierID,
		arg.LeaseSeconds,
		arg.MaxAttemptCount,
		arg.Count,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []AcquireNotificationMessagesRow
	for rows.Next() {
		var i AcquireNotificationMessagesRow
		if err := rows.Scan(
			&i.ID,
			&i.Payload,
			&i.Method,
			&i.AttemptCount,
			&i.QueuedSeconds,
			&i.TemplateID,
			&i.TitleTemplate,
			&i.BodyTemplate,
			&i.Disabled,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const bulkMarkNotificationMessagesFailed = `-- name: BulkMarkNotificationMessagesFailed :execrows
UPDATE notification_messages
SET queued_seconds   = 0,
    updated_at       = subquery.failed_at,
    attempt_count    = attempt_count + 1,
    status           = CASE
                           WHEN attempt_count + 1 < $1::int THEN subquery.status
                           ELSE 'permanent_failure'::notification_message_status END,
    status_reason    = subquery.status_reason,
    leased_until     = NULL,
    next_retry_after = CASE
                           WHEN (attempt_count + 1 < $1::int)
                               THEN NOW() + CONCAT($2::int, ' seconds')::interval END
FROM (SELECT UNNEST($3::uuid[])                             AS id,
             UNNEST($4::timestamptz[])               AS failed_at,
             UNNEST($5::notification_message_status[]) AS status,
             UNNEST($6::text[])                  AS status_reason) AS subquery
WHERE notification_messages.id = subquery.id
`

type BulkMarkNotificationMessagesFailedParams struct {
	MaxAttempts   int32                       `db:"max_attempts" json:"max_attempts"`
	RetryInterval int32                       `db:"retry_interval" json:"retry_interval"`
	IDs           []uuid.UUID                 `db:"ids" json:"ids"`
	FailedAts     []time.Time                 `db:"failed_ats" json:"failed_ats"`
	Statuses      []NotificationMessageStatus `db:"statuses" json:"statuses"`
	StatusReasons []string                    `db:"status_reasons" json:"status_reasons"`
}

func (q *sqlQuerier) BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, bulkMarkNotificationMessagesFailed,
		arg.MaxAttempts,
		arg.RetryInterval,
		pq.Array(arg.IDs),
		pq.Array(arg.FailedAts),
		pq.Array(arg.Statuses),
		pq.Array(arg.StatusReasons),
	)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const bulkMarkNotificationMessagesSent = `-- name: BulkMarkNotificationMessagesSent :execrows
UPDATE notification_messages
SET queued_seconds   = 0,
    updated_at       = new_values.sent_at,
    attempt_count    = attempt_count + 1,
    status           = 'sent'::notification_message_status,
    status_reason    = NULL,
    leased_until     = NULL,
    next_retry_after = NULL
FROM (SELECT UNNEST($1::uuid[])             AS id,
             UNNEST($2::timestamptz[]) AS sent_at)
         AS new_values
WHERE notification_messages.id = new_values.id
`

type BulkMarkNotificationMessagesSentParams struct {
	IDs     []uuid.UUID `db:"ids" json:"ids"`
	SentAts []time.Time `db:"sent_ats" json:"sent_ats"`
}

func (q *sqlQuerier) BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, bulkMarkNotificationMessagesSent, pq.Array(arg.IDs), pq.Array(arg.SentAts))
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const deleteAllWebpushSubscriptions = `-- name: DeleteAllWebpushSubscriptions :exec
TRUNCATE TABLE webpush_subscriptions
`

// Deletes all existing webpush subscriptions.
// This should be called when the VAPID keypair is regenerated, as the old
// keypair will no longer be valid and all existing subscriptions will need to
// be recreated.
func (q *sqlQuerier) DeleteAllWebpushSubscriptions(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, deleteAllWebpushSubscriptions)
	return err
}

const deleteOldNotificationMessages = `-- name: DeleteOldNotificationMessages :exec
DELETE
FROM notification_messages
WHERE id IN
      (SELECT id
       FROM notification_messages AS nested
       WHERE nested.updated_at < NOW() - INTERVAL '7 days')
`

// Delete all notification messages which have not been updated for over a week.
func (q *sqlQuerier) DeleteOldNotificationMessages(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, deleteOldNotificationMessages)
	return err
}

const deleteWebpushSubscriptionByUserIDAndEndpoint = `-- name: DeleteWebpushSubscriptionByUserIDAndEndpoint :exec
DELETE FROM webpush_subscriptions
WHERE user_id = $1 AND endpoint = $2
`

type DeleteWebpushSubscriptionByUserIDAndEndpointParams struct {
	UserID   uuid.UUID `db:"user_id" json:"user_id"`
	Endpoint string    `db:"endpoint" json:"endpoint"`
}

func (q *sqlQuerier) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
	_, err := q.db.ExecContext(ctx, deleteWebpushSubscriptionByUserIDAndEndpoint, arg.UserID, arg.Endpoint)
	return err
}

const deleteWebpushSubscriptions = `-- name: DeleteWebpushSubscriptions :exec
DELETE FROM webpush_subscriptions
WHERE id = ANY($1::uuid[])
`

func (q *sqlQuerier) DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteWebpushSubscriptions, pq.Array(ids))
	return err
}

const enqueueNotificationMessage = `-- name: EnqueueNotificationMessage :exec
INSERT INTO notification_messages (id, notification_template_id, user_id, method, payload, targets, created_by, created_at)
VALUES ($1,
        $2,
        $3,
        $4::notification_method,
        $5::jsonb,
        $6,
        $7,
        $8)
`

type EnqueueNotificationMessageParams struct {
	ID                     uuid.UUID          `db:"id" json:"id"`
	NotificationTemplateID uuid.UUID          `db:"notification_template_id" json:"notification_template_id"`
	UserID                 uuid.UUID          `db:"user_id" json:"user_id"`
	Method                 NotificationMethod `db:"method" json:"method"`
	Payload                json.RawMessage    `db:"payload" json:"payload"`
	Targets                []uuid.UUID        `db:"targets" json:"targets"`
	CreatedBy              string             `db:"created_by" json:"created_by"`
	CreatedAt              time.Time          `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) EnqueueNotificationMessage(ctx context.Context, arg EnqueueNotificationMessageParams) error {
	_, err := q.db.ExecContext(ctx, enqueueNotificationMessage,
		arg.ID,
		arg.NotificationTemplateID,
		arg.UserID,
		arg.Method,
		arg.Payload,
		pq.Array(arg.Targets),
		arg.CreatedBy,
		arg.CreatedAt,
	)
	return err
}

const fetchNewMessageMetadata = `-- name: FetchNewMessageMetadata :one
SELECT nt.name                                                    AS notification_name,
       nt.id                                                      AS notification_template_id,
       nt.actions                                                 AS actions,
       nt.method                                                  AS custom_method,
       u.id                                                       AS user_id,
       u.email                                                    AS user_email,
       COALESCE(NULLIF(u.name, ''), NULLIF(u.username, ''))::text AS user_name,
       u.username                                                 AS user_username
FROM notification_templates nt,
     users u
WHERE nt.id = $1
  AND u.id = $2
`

type FetchNewMessageMetadataParams struct {
	NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
	UserID                 uuid.UUID `db:"user_id" json:"user_id"`
}

type FetchNewMessageMetadataRow struct {
	NotificationName       string                 `db:"notification_name" json:"notification_name"`
	NotificationTemplateID uuid.UUID              `db:"notification_template_id" json:"notification_template_id"`
	Actions                []byte                 `db:"actions" json:"actions"`
	CustomMethod           NullNotificationMethod `db:"custom_method" json:"custom_method"`
	UserID                 uuid.UUID              `db:"user_id" json:"user_id"`
	UserEmail              string                 `db:"user_email" json:"user_email"`
	UserName               string                 `db:"user_name" json:"user_name"`
	UserUsername           string                 `db:"user_username" json:"user_username"`
}

// This is used to build up the notification_message's JSON payload.
func (q *sqlQuerier) FetchNewMessageMetadata(ctx context.Context, arg FetchNewMessageMetadataParams) (FetchNewMessageMetadataRow, error) {
	row := q.db.QueryRowContext(ctx, fetchNewMessageMetadata, arg.NotificationTemplateID, arg.UserID)
	var i FetchNewMessageMetadataRow
	err := row.Scan(
		&i.NotificationName,
		&i.NotificationTemplateID,
		&i.Actions,
		&i.CustomMethod,
		&i.UserID,
		&i.UserEmail,
		&i.UserName,
		&i.UserUsername,
	)
	return i, err
}

const getNotificationMessagesByStatus = `-- name: GetNotificationMessagesByStatus :many
SELECT id, notification_template_id, user_id, method, status, status_reason, created_by, payload, attempt_count, targets, created_at, updated_at, leased_until, next_retry_after, queued_seconds, dedupe_hash
FROM notification_messages
WHERE status = $1
LIMIT $2::int
`

type GetNotificationMessagesByStatusParams struct {
	Status NotificationMessageStatus `db:"status" json:"status"`
	Limit  int32                     `db:"limit" json:"limit"`
}

func (q *sqlQuerier) GetNotificationMessagesByStatus(ctx context.Context, arg GetNotificationMessagesByStatusParams) ([]NotificationMessage, error) {
	rows, err := q.db.QueryContext(ctx, getNotificationMessagesByStatus, arg.Status, arg.Limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []NotificationMessage
	for rows.Next() {
		var i NotificationMessage
		if err := rows.Scan(
			&i.ID,
			&i.NotificationTemplateID,
			&i.UserID,
			&i.Method,
			&i.Status,
			&i.StatusReason,
			&i.CreatedBy,
			&i.Payload,
			&i.AttemptCount,
			pq.Array(&i.Targets),
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.LeasedUntil,
			&i.NextRetryAfter,
			&i.QueuedSeconds,
			&i.DedupeHash,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getNotificationReportGeneratorLogByTemplate = `-- name: GetNotificationReportGeneratorLogByTemplate :one
SELECT
	notification_template_id, last_generated_at
FROM
	notification_report_generator_logs
WHERE
	notification_template_id = $1::uuid
`

// Fetch the notification report generator log indicating recent activity.
func (q *sqlQuerier) GetNotificationReportGeneratorLogByTemplate(ctx context.Context, templateID uuid.UUID) (NotificationReportGeneratorLog, error) {
	row := q.db.QueryRowContext(ctx, getNotificationReportGeneratorLogByTemplate, templateID)
	var i NotificationReportGeneratorLog
	err := row.Scan(&i.NotificationTemplateID, &i.LastGeneratedAt)
	return i, err
}

const getNotificationTemplateByID = `-- name: GetNotificationTemplateByID :one
SELECT id, name, title_template, body_template, actions, "group", method, kind, enabled_by_default
FROM notification_templates
WHERE id = $1::uuid
`

func (q *sqlQuerier) GetNotificationTemplateByID(ctx context.Context, id uuid.UUID) (NotificationTemplate, error) {
	row := q.db.QueryRowContext(ctx, getNotificationTemplateByID, id)
	var i NotificationTemplate
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.TitleTemplate,
		&i.BodyTemplate,
		&i.Actions,
		&i.Group,
		&i.Method,
		&i.Kind,
		&i.EnabledByDefault,
	)
	return i, err
}

const getNotificationTemplatesByKind = `-- name: GetNotificationTemplatesByKind :many
SELECT id, name, title_template, body_template, actions, "group", method, kind, enabled_by_default
FROM notification_templates
WHERE kind = $1::notification_template_kind
ORDER BY name ASC
`

func (q *sqlQuerier) GetNotificationTemplatesByKind(ctx context.Context, kind NotificationTemplateKind) ([]NotificationTemplate, error) {
	rows, err := q.db.QueryContext(ctx, getNotificationTemplatesByKind, kind)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []NotificationTemplate
	for rows.Next() {
		var i NotificationTemplate
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.TitleTemplate,
			&i.BodyTemplate,
			&i.Actions,
			&i.Group,
			&i.Method,
			&i.Kind,
			&i.EnabledByDefault,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUserNotificationPreferences = `-- name: GetUserNotificationPreferences :many
SELECT user_id, notification_template_id, disabled, created_at, updated_at
FROM notification_preferences
WHERE user_id = $1::uuid
`

func (q *sqlQuerier) GetUserNotificationPreferences(ctx context.Context, userID uuid.UUID) ([]NotificationPreference, error) {
	rows, err := q.db.QueryContext(ctx, getUserNotificationPreferences, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []NotificationPreference
	for rows.Next() {
		var i NotificationPreference
		if err := rows.Scan(
			&i.UserID,
			&i.NotificationTemplateID,
			&i.Disabled,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWebpushSubscriptionsByUserID = `-- name: GetWebpushSubscriptionsByUserID :many
SELECT id, user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key
FROM webpush_subscriptions
WHERE user_id = $1::uuid
`

func (q *sqlQuerier) GetWebpushSubscriptionsByUserID(ctx context.Context, userID uuid.UUID) ([]WebpushSubscription, error) {
	rows, err := q.db.QueryContext(ctx, getWebpushSubscriptionsByUserID, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WebpushSubscription
	for rows.Next() {
		var i WebpushSubscription
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.CreatedAt,
			&i.Endpoint,
			&i.EndpointP256dhKey,
			&i.EndpointAuthKey,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWebpushSubscription = `-- name: InsertWebpushSubscription :one
INSERT INTO webpush_subscriptions (user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_id, endpoint) DO UPDATE
    SET endpoint_p256dh_key = EXCLUDED.endpoint_p256dh_key,
        endpoint_auth_key   = EXCLUDED.endpoint_auth_key,
        created_at          = EXCLUDED.created_at
RETURNING id, user_id, created_at, endpoint, endpoint_p256dh_key, endpoint_auth_key
`

type InsertWebpushSubscriptionParams struct {
	UserID            uuid.UUID `db:"user_id" json:"user_id"`
	CreatedAt         time.Time `db:"created_at" json:"created_at"`
	Endpoint          string    `db:"endpoint" json:"endpoint"`
	EndpointP256dhKey string    `db:"endpoint_p256dh_key" json:"endpoint_p256dh_key"`
	EndpointAuthKey   string    `db:"endpoint_auth_key" json:"endpoint_auth_key"`
}

// Inserts or updates a webpush subscription. The (user_id, endpoint) pair
// is unique; re-subscribing the same endpoint replaces the keys instead of
// inserting a duplicate row. This is the recovery path after a PWA reinstall
// on iOS, where the browser may keep the same endpoint with rotated keys.
func (q *sqlQuerier) InsertWebpushSubscription(ctx context.Context, arg InsertWebpushSubscriptionParams) (WebpushSubscription, error) {
	row := q.db.QueryRowContext(ctx, insertWebpushSubscription,
		arg.UserID,
		arg.CreatedAt,
		arg.Endpoint,
		arg.EndpointP256dhKey,
		arg.EndpointAuthKey,
	)
	var i WebpushSubscription
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.CreatedAt,
		&i.Endpoint,
		&i.EndpointP256dhKey,
		&i.EndpointAuthKey,
	)
	return i, err
}

const updateNotificationTemplateMethodByID = `-- name: UpdateNotificationTemplateMethodByID :one
UPDATE notification_templates
SET method = $1::notification_method
WHERE id = $2::uuid
RETURNING id, name, title_template, body_template, actions, "group", method, kind, enabled_by_default
`

type UpdateNotificationTemplateMethodByIDParams struct {
	Method NullNotificationMethod `db:"method" json:"method"`
	ID     uuid.UUID              `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error) {
	row := q.db.QueryRowContext(ctx, updateNotificationTemplateMethodByID, arg.Method, arg.ID)
	var i NotificationTemplate
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.TitleTemplate,
		&i.BodyTemplate,
		&i.Actions,
		&i.Group,
		&i.Method,
		&i.Kind,
		&i.EnabledByDefault,
	)
	return i, err
}

const updateUserNotificationPreferences = `-- name: UpdateUserNotificationPreferences :execrows
INSERT
INTO notification_preferences (user_id, notification_template_id, disabled)
SELECT $1::uuid, new_values.notification_template_id, new_values.disabled
FROM (SELECT UNNEST($2::uuid[]) AS notification_template_id,
             UNNEST($3::bool[])                 AS disabled) AS new_values
ON CONFLICT (user_id, notification_template_id) DO UPDATE
    SET disabled   = EXCLUDED.disabled,
        updated_at = CURRENT_TIMESTAMP
`

type UpdateUserNotificationPreferencesParams struct {
	UserID                  uuid.UUID   `db:"user_id" json:"user_id"`
	NotificationTemplateIds []uuid.UUID `db:"notification_template_ids" json:"notification_template_ids"`
	Disableds               []bool      `db:"disableds" json:"disableds"`
}

func (q *sqlQuerier) UpdateUserNotificationPreferences(ctx context.Context, arg UpdateUserNotificationPreferencesParams) (int64, error) {
	result, err := q.db.ExecContext(ctx, updateUserNotificationPreferences, arg.UserID, pq.Array(arg.NotificationTemplateIds), pq.Array(arg.Disableds))
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const upsertNotificationReportGeneratorLog = `-- name: UpsertNotificationReportGeneratorLog :exec
INSERT INTO notification_report_generator_logs (notification_template_id, last_generated_at) VALUES ($1, $2)
ON CONFLICT (notification_template_id) DO UPDATE set last_generated_at = EXCLUDED.last_generated_at
WHERE notification_report_generator_logs.notification_template_id = EXCLUDED.notification_template_id
`

type UpsertNotificationReportGeneratorLogParams struct {
	NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
	LastGeneratedAt        time.Time `db:"last_generated_at" json:"last_generated_at"`
}

// Insert or update notification report generator logs with recent activity.
func (q *sqlQuerier) UpsertNotificationReportGeneratorLog(ctx context.Context, arg UpsertNotificationReportGeneratorLogParams) error {
	_, err := q.db.ExecContext(ctx, upsertNotificationReportGeneratorLog, arg.NotificationTemplateID, arg.LastGeneratedAt)
	return err
}

const countUnreadInboxNotificationsByUserID = `-- name: CountUnreadInboxNotificationsByUserID :one
SELECT COUNT(*) FROM inbox_notifications WHERE user_id = $1 AND read_at IS NULL
`

func (q *sqlQuerier) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
	row := q.db.QueryRowContext(ctx, countUnreadInboxNotificationsByUserID, userID)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const getFilteredInboxNotificationsByUserID = `-- name: GetFilteredInboxNotificationsByUserID :many
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE
	user_id = $1 AND
	($2::UUID[] IS NULL OR template_id = ANY($2::UUID[])) AND
	($3::UUID[] IS NULL OR targets @> $3::UUID[]) AND
	($4::inbox_notification_read_status = 'all' OR ($4::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR ($4::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
	($5::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < $5::TIMESTAMPTZ)
	ORDER BY created_at DESC
	LIMIT (COALESCE(NULLIF($6 :: INT, 0), 25))
`

type GetFilteredInboxNotificationsByUserIDParams struct {
	UserID       uuid.UUID                   `db:"user_id" json:"user_id"`
	Templates    []uuid.UUID                 `db:"templates" json:"templates"`
	Targets      []uuid.UUID                 `db:"targets" json:"targets"`
	ReadStatus   InboxNotificationReadStatus `db:"read_status" json:"read_status"`
	CreatedAtOpt time.Time                   `db:"created_at_opt" json:"created_at_opt"`
	LimitOpt     int32                       `db:"limit_opt" json:"limit_opt"`
}

// Fetches inbox notifications for a user filtered by templates and targets
// param user_id: The user ID
// param templates: The template IDs to filter by - the template_id = ANY(@templates::UUID[]) condition checks if the template_id is in the @templates array
// param targets: The target IDs to filter by - the targets @> COALESCE(@targets, ARRAY[]::UUID[]) condition checks if the targets array (from the DB) contains all the elements in the @targets array
// param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
// param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
// param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
func (q *sqlQuerier) GetFilteredInboxNotificationsByUserID(ctx context.Context, arg GetFilteredInboxNotificationsByUserIDParams) ([]InboxNotification, error) {
	rows, err := q.db.QueryContext(ctx, getFilteredInboxNotificationsByUserID,
		arg.UserID,
		pq.Array(arg.Templates),
		pq.Array(arg.Targets),
		arg.ReadStatus,
		arg.CreatedAtOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []InboxNotification
	for rows.Next() {
		var i InboxNotification
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.TemplateID,
			pq.Array(&i.Targets),
			&i.Title,
			&i.Content,
			&i.Icon,
			&i.Actions,
			&i.ReadAt,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getInboxNotificationByID = `-- name: GetInboxNotificationByID :one
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE id = $1
`

func (q *sqlQuerier) GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (InboxNotification, error) {
	row := q.db.QueryRowContext(ctx, getInboxNotificationByID, id)
	var i InboxNotification
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.TemplateID,
		pq.Array(&i.Targets),
		&i.Title,
		&i.Content,
		&i.Icon,
		&i.Actions,
		&i.ReadAt,
		&i.CreatedAt,
	)
	return i, err
}

const getInboxNotificationsByUserID = `-- name: GetInboxNotificationsByUserID :many
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE
	user_id = $1 AND
	($2::inbox_notification_read_status = 'all' OR ($2::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR ($2::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
	($3::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < $3::TIMESTAMPTZ)
	ORDER BY created_at DESC
	LIMIT (COALESCE(NULLIF($4 :: INT, 0), 25))
`

type GetInboxNotificationsByUserIDParams struct {
	UserID       uuid.UUID                   `db:"user_id" json:"user_id"`
	ReadStatus   InboxNotificationReadStatus `db:"read_status" json:"read_status"`
	CreatedAtOpt time.Time                   `db:"created_at_opt" json:"created_at_opt"`
	LimitOpt     int32                       `db:"limit_opt" json:"limit_opt"`
}

// Fetches inbox notifications for a user filtered by templates and targets
// param user_id: The user ID
// param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
// param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
// param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
func (q *sqlQuerier) GetInboxNotificationsByUserID(ctx context.Context, arg GetInboxNotificationsByUserIDParams) ([]InboxNotification, error) {
	rows, err := q.db.QueryContext(ctx, getInboxNotificationsByUserID,
		arg.UserID,
		arg.ReadStatus,
		arg.CreatedAtOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []InboxNotification
	for rows.Next() {
		var i InboxNotification
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.TemplateID,
			pq.Array(&i.Targets),
			&i.Title,
			&i.Content,
			&i.Icon,
			&i.Actions,
			&i.ReadAt,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertInboxNotification = `-- name: InsertInboxNotification :one
INSERT INTO
    inbox_notifications (
        id,
        user_id,
        template_id,
        targets,
        title,
        content,
        icon,
        actions,
        created_at
    )
VALUES
    ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at
`

type InsertInboxNotificationParams struct {
	ID         uuid.UUID       `db:"id" json:"id"`
	UserID     uuid.UUID       `db:"user_id" json:"user_id"`
	TemplateID uuid.UUID       `db:"template_id" json:"template_id"`
	Targets    []uuid.UUID     `db:"targets" json:"targets"`
	Title      string          `db:"title" json:"title"`
	Content    string          `db:"content" json:"content"`
	Icon       string          `db:"icon" json:"icon"`
	Actions    json.RawMessage `db:"actions" json:"actions"`
	CreatedAt  time.Time       `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) InsertInboxNotification(ctx context.Context, arg InsertInboxNotificationParams) (InboxNotification, error) {
	row := q.db.QueryRowContext(ctx, insertInboxNotification,
		arg.ID,
		arg.UserID,
		arg.TemplateID,
		pq.Array(arg.Targets),
		arg.Title,
		arg.Content,
		arg.Icon,
		arg.Actions,
		arg.CreatedAt,
	)
	var i InboxNotification
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.TemplateID,
		pq.Array(&i.Targets),
		&i.Title,
		&i.Content,
		&i.Icon,
		&i.Actions,
		&i.ReadAt,
		&i.CreatedAt,
	)
	return i, err
}

const markAllInboxNotificationsAsRead = `-- name: MarkAllInboxNotificationsAsRead :exec
UPDATE
	inbox_notifications
SET
	read_at = $1
WHERE
	user_id = $2 and read_at IS NULL
`

type MarkAllInboxNotificationsAsReadParams struct {
	ReadAt sql.NullTime `db:"read_at" json:"read_at"`
	UserID uuid.UUID    `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) MarkAllInboxNotificationsAsRead(ctx context.Context, arg MarkAllInboxNotificationsAsReadParams) error {
	_, err := q.db.ExecContext(ctx, markAllInboxNotificationsAsRead, arg.ReadAt, arg.UserID)
	return err
}

const updateInboxNotificationReadStatus = `-- name: UpdateInboxNotificationReadStatus :exec
UPDATE
    inbox_notifications
SET
    read_at = $1
WHERE
    id = $2
`

type UpdateInboxNotificationReadStatusParams struct {
	ReadAt sql.NullTime `db:"read_at" json:"read_at"`
	ID     uuid.UUID    `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateInboxNotificationReadStatus(ctx context.Context, arg UpdateInboxNotificationReadStatusParams) error {
	_, err := q.db.ExecContext(ctx, updateInboxNotificationReadStatus, arg.ReadAt, arg.ID)
	return err
}

const deleteOAuth2ProviderAppByClientID = `-- name: DeleteOAuth2ProviderAppByClientID :exec
DELETE FROM oauth2_provider_apps WHERE id = $1
`

func (q *sqlQuerier) DeleteOAuth2ProviderAppByClientID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppByClientID, id)
	return err
}

const deleteOAuth2ProviderAppByID = `-- name: DeleteOAuth2ProviderAppByID :exec
DELETE FROM oauth2_provider_apps WHERE id = $1
`

func (q *sqlQuerier) DeleteOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppByID, id)
	return err
}

const deleteOAuth2ProviderAppCodeByID = `-- name: DeleteOAuth2ProviderAppCodeByID :exec
DELETE FROM oauth2_provider_app_codes WHERE id = $1
`

func (q *sqlQuerier) DeleteOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppCodeByID, id)
	return err
}

const deleteOAuth2ProviderAppCodesByAppAndUserID = `-- name: DeleteOAuth2ProviderAppCodesByAppAndUserID :exec
DELETE FROM oauth2_provider_app_codes WHERE app_id = $1 AND user_id = $2
`

type DeleteOAuth2ProviderAppCodesByAppAndUserIDParams struct {
	AppID  uuid.UUID `db:"app_id" json:"app_id"`
	UserID uuid.UUID `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppCodesByAppAndUserIDParams) error {
	_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppCodesByAppAndUserID, arg.AppID, arg.UserID)
	return err
}

const deleteOAuth2ProviderAppSecretByID = `-- name: DeleteOAuth2ProviderAppSecretByID :exec
DELETE FROM oauth2_provider_app_secrets WHERE id = $1
`

func (q *sqlQuerier) DeleteOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppSecretByID, id)
	return err
}

const deleteOAuth2ProviderAppTokensByAppAndUserID = `-- name: DeleteOAuth2ProviderAppTokensByAppAndUserID :exec
DELETE FROM
  oauth2_provider_app_tokens
USING
  oauth2_provider_app_secrets
WHERE
  oauth2_provider_app_secrets.id = oauth2_provider_app_tokens.app_secret_id
  AND oauth2_provider_app_secrets.app_id = $1
  AND oauth2_provider_app_tokens.user_id = $2
`

type DeleteOAuth2ProviderAppTokensByAppAndUserIDParams struct {
	AppID  uuid.UUID `db:"app_id" json:"app_id"`
	UserID uuid.UUID `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) DeleteOAuth2ProviderAppTokensByAppAndUserID(ctx context.Context, arg DeleteOAuth2ProviderAppTokensByAppAndUserIDParams) error {
	_, err := q.db.ExecContext(ctx, deleteOAuth2ProviderAppTokensByAppAndUserID, arg.AppID, arg.UserID)
	return err
}

const getOAuth2ProviderAppByClientID = `-- name: GetOAuth2ProviderAppByClientID :one

SELECT id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps WHERE id = $1
`

// RFC 7591/7592 Dynamic Client Registration queries
func (q *sqlQuerier) GetOAuth2ProviderAppByClientID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppByClientID, id)
	var i OAuth2ProviderApp
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Icon,
		&i.CallbackURL,
		pq.Array(&i.RedirectUris),
		&i.ClientType,
		&i.DynamicallyRegistered,
		&i.ClientIDIssuedAt,
		&i.ClientSecretExpiresAt,
		pq.Array(&i.GrantTypes),
		pq.Array(&i.ResponseTypes),
		&i.TokenEndpointAuthMethod,
		&i.Scope,
		pq.Array(&i.Contacts),
		&i.ClientUri,
		&i.LogoUri,
		&i.TosUri,
		&i.PolicyUri,
		&i.JwksUri,
		&i.Jwks,
		&i.SoftwareID,
		&i.SoftwareVersion,
		&i.RegistrationAccessToken,
		&i.RegistrationClientUri,
	)
	return i, err
}

const getOAuth2ProviderAppByID = `-- name: GetOAuth2ProviderAppByID :one
SELECT id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps WHERE id = $1
`

func (q *sqlQuerier) GetOAuth2ProviderAppByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderApp, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppByID, id)
	var i OAuth2ProviderApp
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Icon,
		&i.CallbackURL,
		pq.Array(&i.RedirectUris),
		&i.ClientType,
		&i.DynamicallyRegistered,
		&i.ClientIDIssuedAt,
		&i.ClientSecretExpiresAt,
		pq.Array(&i.GrantTypes),
		pq.Array(&i.ResponseTypes),
		&i.TokenEndpointAuthMethod,
		&i.Scope,
		pq.Array(&i.Contacts),
		&i.ClientUri,
		&i.LogoUri,
		&i.TosUri,
		&i.PolicyUri,
		&i.JwksUri,
		&i.Jwks,
		&i.SoftwareID,
		&i.SoftwareVersion,
		&i.RegistrationAccessToken,
		&i.RegistrationClientUri,
	)
	return i, err
}

const getOAuth2ProviderAppCodeByID = `-- name: GetOAuth2ProviderAppCodeByID :one
SELECT id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, resource_uri, code_challenge, code_challenge_method, state_hash, redirect_uri FROM oauth2_provider_app_codes WHERE id = $1
`

func (q *sqlQuerier) GetOAuth2ProviderAppCodeByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppCode, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppCodeByID, id)
	var i OAuth2ProviderAppCode
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.ExpiresAt,
		&i.SecretPrefix,
		&i.HashedSecret,
		&i.UserID,
		&i.AppID,
		&i.ResourceUri,
		&i.CodeChallenge,
		&i.CodeChallengeMethod,
		&i.StateHash,
		&i.RedirectUri,
	)
	return i, err
}

const getOAuth2ProviderAppCodeByPrefix = `-- name: GetOAuth2ProviderAppCodeByPrefix :one
SELECT id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, resource_uri, code_challenge, code_challenge_method, state_hash, redirect_uri FROM oauth2_provider_app_codes WHERE secret_prefix = $1
`

func (q *sqlQuerier) GetOAuth2ProviderAppCodeByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppCode, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppCodeByPrefix, secretPrefix)
	var i OAuth2ProviderAppCode
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.ExpiresAt,
		&i.SecretPrefix,
		&i.HashedSecret,
		&i.UserID,
		&i.AppID,
		&i.ResourceUri,
		&i.CodeChallenge,
		&i.CodeChallengeMethod,
		&i.StateHash,
		&i.RedirectUri,
	)
	return i, err
}

const getOAuth2ProviderAppSecretByID = `-- name: GetOAuth2ProviderAppSecretByID :one
SELECT id, created_at, last_used_at, hashed_secret, display_secret, app_id, secret_prefix FROM oauth2_provider_app_secrets WHERE id = $1
`

func (q *sqlQuerier) GetOAuth2ProviderAppSecretByID(ctx context.Context, id uuid.UUID) (OAuth2ProviderAppSecret, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppSecretByID, id)
	var i OAuth2ProviderAppSecret
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.LastUsedAt,
		&i.HashedSecret,
		&i.DisplaySecret,
		&i.AppID,
		&i.SecretPrefix,
	)
	return i, err
}

const getOAuth2ProviderAppSecretByPrefix = `-- name: GetOAuth2ProviderAppSecretByPrefix :one
SELECT id, created_at, last_used_at, hashed_secret, display_secret, app_id, secret_prefix FROM oauth2_provider_app_secrets WHERE secret_prefix = $1
`

func (q *sqlQuerier) GetOAuth2ProviderAppSecretByPrefix(ctx context.Context, secretPrefix []byte) (OAuth2ProviderAppSecret, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppSecretByPrefix, secretPrefix)
	var i OAuth2ProviderAppSecret
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.LastUsedAt,
		&i.HashedSecret,
		&i.DisplaySecret,
		&i.AppID,
		&i.SecretPrefix,
	)
	return i, err
}

const getOAuth2ProviderAppSecretsByAppID = `-- name: GetOAuth2ProviderAppSecretsByAppID :many
SELECT id, created_at, last_used_at, hashed_secret, display_secret, app_id, secret_prefix FROM oauth2_provider_app_secrets WHERE app_id = $1 ORDER BY (created_at, id) ASC
`

func (q *sqlQuerier) GetOAuth2ProviderAppSecretsByAppID(ctx context.Context, appID uuid.UUID) ([]OAuth2ProviderAppSecret, error) {
	rows, err := q.db.QueryContext(ctx, getOAuth2ProviderAppSecretsByAppID, appID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []OAuth2ProviderAppSecret
	for rows.Next() {
		var i OAuth2ProviderAppSecret
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.LastUsedAt,
			&i.HashedSecret,
			&i.DisplaySecret,
			&i.AppID,
			&i.SecretPrefix,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getOAuth2ProviderAppTokenByAPIKeyID = `-- name: GetOAuth2ProviderAppTokenByAPIKeyID :one
SELECT id, created_at, expires_at, hash_prefix, refresh_hash, app_secret_id, api_key_id, audience, user_id FROM oauth2_provider_app_tokens WHERE api_key_id = $1
`

func (q *sqlQuerier) GetOAuth2ProviderAppTokenByAPIKeyID(ctx context.Context, apiKeyID string) (OAuth2ProviderAppToken, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppTokenByAPIKeyID, apiKeyID)
	var i OAuth2ProviderAppToken
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.ExpiresAt,
		&i.HashPrefix,
		&i.RefreshHash,
		&i.AppSecretID,
		&i.APIKeyID,
		&i.Audience,
		&i.UserID,
	)
	return i, err
}

const getOAuth2ProviderAppTokenByPrefix = `-- name: GetOAuth2ProviderAppTokenByPrefix :one
SELECT id, created_at, expires_at, hash_prefix, refresh_hash, app_secret_id, api_key_id, audience, user_id FROM oauth2_provider_app_tokens WHERE hash_prefix = $1
`

func (q *sqlQuerier) GetOAuth2ProviderAppTokenByPrefix(ctx context.Context, hashPrefix []byte) (OAuth2ProviderAppToken, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2ProviderAppTokenByPrefix, hashPrefix)
	var i OAuth2ProviderAppToken
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.ExpiresAt,
		&i.HashPrefix,
		&i.RefreshHash,
		&i.AppSecretID,
		&i.APIKeyID,
		&i.Audience,
		&i.UserID,
	)
	return i, err
}

const getOAuth2ProviderApps = `-- name: GetOAuth2ProviderApps :many
SELECT id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri FROM oauth2_provider_apps ORDER BY (name, id) ASC
`

func (q *sqlQuerier) GetOAuth2ProviderApps(ctx context.Context) ([]OAuth2ProviderApp, error) {
	rows, err := q.db.QueryContext(ctx, getOAuth2ProviderApps)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []OAuth2ProviderApp
	for rows.Next() {
		var i OAuth2ProviderApp
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.Icon,
			&i.CallbackURL,
			pq.Array(&i.RedirectUris),
			&i.ClientType,
			&i.DynamicallyRegistered,
			&i.ClientIDIssuedAt,
			&i.ClientSecretExpiresAt,
			pq.Array(&i.GrantTypes),
			pq.Array(&i.ResponseTypes),
			&i.TokenEndpointAuthMethod,
			&i.Scope,
			pq.Array(&i.Contacts),
			&i.ClientUri,
			&i.LogoUri,
			&i.TosUri,
			&i.PolicyUri,
			&i.JwksUri,
			&i.Jwks,
			&i.SoftwareID,
			&i.SoftwareVersion,
			&i.RegistrationAccessToken,
			&i.RegistrationClientUri,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getOAuth2ProviderAppsByUserID = `-- name: GetOAuth2ProviderAppsByUserID :many
SELECT
  COUNT(DISTINCT oauth2_provider_app_tokens.id) as token_count,
  oauth2_provider_apps.id, oauth2_provider_apps.created_at, oauth2_provider_apps.updated_at, oauth2_provider_apps.name, oauth2_provider_apps.icon, oauth2_provider_apps.callback_url, oauth2_provider_apps.redirect_uris, oauth2_provider_apps.client_type, oauth2_provider_apps.dynamically_registered, oauth2_provider_apps.client_id_issued_at, oauth2_provider_apps.client_secret_expires_at, oauth2_provider_apps.grant_types, oauth2_provider_apps.response_types, oauth2_provider_apps.token_endpoint_auth_method, oauth2_provider_apps.scope, oauth2_provider_apps.contacts, oauth2_provider_apps.client_uri, oauth2_provider_apps.logo_uri, oauth2_provider_apps.tos_uri, oauth2_provider_apps.policy_uri, oauth2_provider_apps.jwks_uri, oauth2_provider_apps.jwks, oauth2_provider_apps.software_id, oauth2_provider_apps.software_version, oauth2_provider_apps.registration_access_token, oauth2_provider_apps.registration_client_uri
FROM oauth2_provider_app_tokens
  INNER JOIN oauth2_provider_app_secrets
    ON oauth2_provider_app_secrets.id = oauth2_provider_app_tokens.app_secret_id
  INNER JOIN oauth2_provider_apps
    ON oauth2_provider_apps.id = oauth2_provider_app_secrets.app_id
WHERE
  oauth2_provider_app_tokens.user_id = $1
GROUP BY
  oauth2_provider_apps.id
`

type GetOAuth2ProviderAppsByUserIDRow struct {
	TokenCount        int64             `db:"token_count" json:"token_count"`
	OAuth2ProviderApp OAuth2ProviderApp `db:"oauth2_provider_app" json:"oauth2_provider_app"`
}

func (q *sqlQuerier) GetOAuth2ProviderAppsByUserID(ctx context.Context, userID uuid.UUID) ([]GetOAuth2ProviderAppsByUserIDRow, error) {
	rows, err := q.db.QueryContext(ctx, getOAuth2ProviderAppsByUserID, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetOAuth2ProviderAppsByUserIDRow
	for rows.Next() {
		var i GetOAuth2ProviderAppsByUserIDRow
		if err := rows.Scan(
			&i.TokenCount,
			&i.OAuth2ProviderApp.ID,
			&i.OAuth2ProviderApp.CreatedAt,
			&i.OAuth2ProviderApp.UpdatedAt,
			&i.OAuth2ProviderApp.Name,
			&i.OAuth2ProviderApp.Icon,
			&i.OAuth2ProviderApp.CallbackURL,
			pq.Array(&i.OAuth2ProviderApp.RedirectUris),
			&i.OAuth2ProviderApp.ClientType,
			&i.OAuth2ProviderApp.DynamicallyRegistered,
			&i.OAuth2ProviderApp.ClientIDIssuedAt,
			&i.OAuth2ProviderApp.ClientSecretExpiresAt,
			pq.Array(&i.OAuth2ProviderApp.GrantTypes),
			pq.Array(&i.OAuth2ProviderApp.ResponseTypes),
			&i.OAuth2ProviderApp.TokenEndpointAuthMethod,
			&i.OAuth2ProviderApp.Scope,
			pq.Array(&i.OAuth2ProviderApp.Contacts),
			&i.OAuth2ProviderApp.ClientUri,
			&i.OAuth2ProviderApp.LogoUri,
			&i.OAuth2ProviderApp.TosUri,
			&i.OAuth2ProviderApp.PolicyUri,
			&i.OAuth2ProviderApp.JwksUri,
			&i.OAuth2ProviderApp.Jwks,
			&i.OAuth2ProviderApp.SoftwareID,
			&i.OAuth2ProviderApp.SoftwareVersion,
			&i.OAuth2ProviderApp.RegistrationAccessToken,
			&i.OAuth2ProviderApp.RegistrationClientUri,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertOAuth2ProviderApp = `-- name: InsertOAuth2ProviderApp :one
INSERT INTO oauth2_provider_apps (
    id,
    created_at,
    updated_at,
    name,
    icon,
    callback_url,
    redirect_uris,
    client_type,
    dynamically_registered,
    client_id_issued_at,
    client_secret_expires_at,
    grant_types,
    response_types,
    token_endpoint_auth_method,
    scope,
    contacts,
    client_uri,
    logo_uri,
    tos_uri,
    policy_uri,
    jwks_uri,
    jwks,
    software_id,
    software_version,
    registration_access_token,
    registration_client_uri
) VALUES(
    $1,
    $2,
    $3,
    $4,
    $5,
    $6,
    $7,
    $8,
    $9,
    $10,
    $11,
    $12,
    $13,
    $14,
    $15,
    $16,
    $17,
    $18,
    $19,
    $20,
    $21,
    $22,
    $23,
    $24,
    $25,
    $26
) RETURNING id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri
`

type InsertOAuth2ProviderAppParams struct {
	ID                      uuid.UUID             `db:"id" json:"id"`
	CreatedAt               time.Time             `db:"created_at" json:"created_at"`
	UpdatedAt               time.Time             `db:"updated_at" json:"updated_at"`
	Name                    string                `db:"name" json:"name"`
	Icon                    string                `db:"icon" json:"icon"`
	CallbackURL             string                `db:"callback_url" json:"callback_url"`
	RedirectUris            []string              `db:"redirect_uris" json:"redirect_uris"`
	ClientType              sql.NullString        `db:"client_type" json:"client_type"`
	DynamicallyRegistered   sql.NullBool          `db:"dynamically_registered" json:"dynamically_registered"`
	ClientIDIssuedAt        sql.NullTime          `db:"client_id_issued_at" json:"client_id_issued_at"`
	ClientSecretExpiresAt   sql.NullTime          `db:"client_secret_expires_at" json:"client_secret_expires_at"`
	GrantTypes              []string              `db:"grant_types" json:"grant_types"`
	ResponseTypes           []string              `db:"response_types" json:"response_types"`
	TokenEndpointAuthMethod sql.NullString        `db:"token_endpoint_auth_method" json:"token_endpoint_auth_method"`
	Scope                   sql.NullString        `db:"scope" json:"scope"`
	Contacts                []string              `db:"contacts" json:"contacts"`
	ClientUri               sql.NullString        `db:"client_uri" json:"client_uri"`
	LogoUri                 sql.NullString        `db:"logo_uri" json:"logo_uri"`
	TosUri                  sql.NullString        `db:"tos_uri" json:"tos_uri"`
	PolicyUri               sql.NullString        `db:"policy_uri" json:"policy_uri"`
	JwksUri                 sql.NullString        `db:"jwks_uri" json:"jwks_uri"`
	Jwks                    pqtype.NullRawMessage `db:"jwks" json:"jwks"`
	SoftwareID              sql.NullString        `db:"software_id" json:"software_id"`
	SoftwareVersion         sql.NullString        `db:"software_version" json:"software_version"`
	RegistrationAccessToken []byte                `db:"registration_access_token" json:"registration_access_token"`
	RegistrationClientUri   sql.NullString        `db:"registration_client_uri" json:"registration_client_uri"`
}

func (q *sqlQuerier) InsertOAuth2ProviderApp(ctx context.Context, arg InsertOAuth2ProviderAppParams) (OAuth2ProviderApp, error) {
	row := q.db.QueryRowContext(ctx, insertOAuth2ProviderApp,
		arg.ID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.Name,
		arg.Icon,
		arg.CallbackURL,
		pq.Array(arg.RedirectUris),
		arg.ClientType,
		arg.DynamicallyRegistered,
		arg.ClientIDIssuedAt,
		arg.ClientSecretExpiresAt,
		pq.Array(arg.GrantTypes),
		pq.Array(arg.ResponseTypes),
		arg.TokenEndpointAuthMethod,
		arg.Scope,
		pq.Array(arg.Contacts),
		arg.ClientUri,
		arg.LogoUri,
		arg.TosUri,
		arg.PolicyUri,
		arg.JwksUri,
		arg.Jwks,
		arg.SoftwareID,
		arg.SoftwareVersion,
		arg.RegistrationAccessToken,
		arg.RegistrationClientUri,
	)
	var i OAuth2ProviderApp
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Icon,
		&i.CallbackURL,
		pq.Array(&i.RedirectUris),
		&i.ClientType,
		&i.DynamicallyRegistered,
		&i.ClientIDIssuedAt,
		&i.ClientSecretExpiresAt,
		pq.Array(&i.GrantTypes),
		pq.Array(&i.ResponseTypes),
		&i.TokenEndpointAuthMethod,
		&i.Scope,
		pq.Array(&i.Contacts),
		&i.ClientUri,
		&i.LogoUri,
		&i.TosUri,
		&i.PolicyUri,
		&i.JwksUri,
		&i.Jwks,
		&i.SoftwareID,
		&i.SoftwareVersion,
		&i.RegistrationAccessToken,
		&i.RegistrationClientUri,
	)
	return i, err
}

const insertOAuth2ProviderAppCode = `-- name: InsertOAuth2ProviderAppCode :one
INSERT INTO oauth2_provider_app_codes (
    id,
    created_at,
    expires_at,
    secret_prefix,
    hashed_secret,
    app_id,
    user_id,
    resource_uri,
    code_challenge,
    code_challenge_method,
    state_hash,
    redirect_uri
) VALUES(
    $1,
    $2,
    $3,
    $4,
    $5,
    $6,
    $7,
    $8,
    $9,
    $10,
    $11,
    $12
) RETURNING id, created_at, expires_at, secret_prefix, hashed_secret, user_id, app_id, resource_uri, code_challenge, code_challenge_method, state_hash, redirect_uri
`

type InsertOAuth2ProviderAppCodeParams struct {
	ID                  uuid.UUID      `db:"id" json:"id"`
	CreatedAt           time.Time      `db:"created_at" json:"created_at"`
	ExpiresAt           time.Time      `db:"expires_at" json:"expires_at"`
	SecretPrefix        []byte         `db:"secret_prefix" json:"secret_prefix"`
	HashedSecret        []byte         `db:"hashed_secret" json:"hashed_secret"`
	AppID               uuid.UUID      `db:"app_id" json:"app_id"`
	UserID              uuid.UUID      `db:"user_id" json:"user_id"`
	ResourceUri         sql.NullString `db:"resource_uri" json:"resource_uri"`
	CodeChallenge       sql.NullString `db:"code_challenge" json:"code_challenge"`
	CodeChallengeMethod sql.NullString `db:"code_challenge_method" json:"code_challenge_method"`
	StateHash           sql.NullString `db:"state_hash" json:"state_hash"`
	RedirectUri         sql.NullString `db:"redirect_uri" json:"redirect_uri"`
}

func (q *sqlQuerier) InsertOAuth2ProviderAppCode(ctx context.Context, arg InsertOAuth2ProviderAppCodeParams) (OAuth2ProviderAppCode, error) {
	row := q.db.QueryRowContext(ctx, insertOAuth2ProviderAppCode,
		arg.ID,
		arg.CreatedAt,
		arg.ExpiresAt,
		arg.SecretPrefix,
		arg.HashedSecret,
		arg.AppID,
		arg.UserID,
		arg.ResourceUri,
		arg.CodeChallenge,
		arg.CodeChallengeMethod,
		arg.StateHash,
		arg.RedirectUri,
	)
	var i OAuth2ProviderAppCode
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.ExpiresAt,
		&i.SecretPrefix,
		&i.HashedSecret,
		&i.UserID,
		&i.AppID,
		&i.ResourceUri,
		&i.CodeChallenge,
		&i.CodeChallengeMethod,
		&i.StateHash,
		&i.RedirectUri,
	)
	return i, err
}

const insertOAuth2ProviderAppSecret = `-- name: InsertOAuth2ProviderAppSecret :one
INSERT INTO oauth2_provider_app_secrets (
    id,
    created_at,
    secret_prefix,
    hashed_secret,
    display_secret,
    app_id
) VALUES(
    $1,
    $2,
    $3,
    $4,
    $5,
    $6
) RETURNING id, created_at, last_used_at, hashed_secret, display_secret, app_id, secret_prefix
`

type InsertOAuth2ProviderAppSecretParams struct {
	ID            uuid.UUID `db:"id" json:"id"`
	CreatedAt     time.Time `db:"created_at" json:"created_at"`
	SecretPrefix  []byte    `db:"secret_prefix" json:"secret_prefix"`
	HashedSecret  []byte    `db:"hashed_secret" json:"hashed_secret"`
	DisplaySecret string    `db:"display_secret" json:"display_secret"`
	AppID         uuid.UUID `db:"app_id" json:"app_id"`
}

func (q *sqlQuerier) InsertOAuth2ProviderAppSecret(ctx context.Context, arg InsertOAuth2ProviderAppSecretParams) (OAuth2ProviderAppSecret, error) {
	row := q.db.QueryRowContext(ctx, insertOAuth2ProviderAppSecret,
		arg.ID,
		arg.CreatedAt,
		arg.SecretPrefix,
		arg.HashedSecret,
		arg.DisplaySecret,
		arg.AppID,
	)
	var i OAuth2ProviderAppSecret
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.LastUsedAt,
		&i.HashedSecret,
		&i.DisplaySecret,
		&i.AppID,
		&i.SecretPrefix,
	)
	return i, err
}

const insertOAuth2ProviderAppToken = `-- name: InsertOAuth2ProviderAppToken :one
INSERT INTO oauth2_provider_app_tokens (
    id,
    created_at,
    expires_at,
    hash_prefix,
    refresh_hash,
    app_secret_id,
    api_key_id,
    user_id,
    audience
) VALUES(
    $1,
    $2,
    $3,
    $4,
    $5,
    $6,
    $7,
    $8,
    $9
) RETURNING id, created_at, expires_at, hash_prefix, refresh_hash, app_secret_id, api_key_id, audience, user_id
`

type InsertOAuth2ProviderAppTokenParams struct {
	ID          uuid.UUID      `db:"id" json:"id"`
	CreatedAt   time.Time      `db:"created_at" json:"created_at"`
	ExpiresAt   time.Time      `db:"expires_at" json:"expires_at"`
	HashPrefix  []byte         `db:"hash_prefix" json:"hash_prefix"`
	RefreshHash []byte         `db:"refresh_hash" json:"refresh_hash"`
	AppSecretID uuid.UUID      `db:"app_secret_id" json:"app_secret_id"`
	APIKeyID    string         `db:"api_key_id" json:"api_key_id"`
	UserID      uuid.UUID      `db:"user_id" json:"user_id"`
	Audience    sql.NullString `db:"audience" json:"audience"`
}

func (q *sqlQuerier) InsertOAuth2ProviderAppToken(ctx context.Context, arg InsertOAuth2ProviderAppTokenParams) (OAuth2ProviderAppToken, error) {
	row := q.db.QueryRowContext(ctx, insertOAuth2ProviderAppToken,
		arg.ID,
		arg.CreatedAt,
		arg.ExpiresAt,
		arg.HashPrefix,
		arg.RefreshHash,
		arg.AppSecretID,
		arg.APIKeyID,
		arg.UserID,
		arg.Audience,
	)
	var i OAuth2ProviderAppToken
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.ExpiresAt,
		&i.HashPrefix,
		&i.RefreshHash,
		&i.AppSecretID,
		&i.APIKeyID,
		&i.Audience,
		&i.UserID,
	)
	return i, err
}

const updateOAuth2ProviderAppByClientID = `-- name: UpdateOAuth2ProviderAppByClientID :one
UPDATE oauth2_provider_apps SET
    updated_at = $2,
    name = $3,
    icon = $4,
    callback_url = $5,
    redirect_uris = $6,
    client_type = $7,
    client_secret_expires_at = $8,
    grant_types = $9,
    response_types = $10,
    token_endpoint_auth_method = $11,
    scope = $12,
    contacts = $13,
    client_uri = $14,
    logo_uri = $15,
    tos_uri = $16,
    policy_uri = $17,
    jwks_uri = $18,
    jwks = $19,
    software_id = $20,
    software_version = $21
WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri
`

type UpdateOAuth2ProviderAppByClientIDParams struct {
	ID                      uuid.UUID             `db:"id" json:"id"`
	UpdatedAt               time.Time             `db:"updated_at" json:"updated_at"`
	Name                    string                `db:"name" json:"name"`
	Icon                    string                `db:"icon" json:"icon"`
	CallbackURL             string                `db:"callback_url" json:"callback_url"`
	RedirectUris            []string              `db:"redirect_uris" json:"redirect_uris"`
	ClientType              sql.NullString        `db:"client_type" json:"client_type"`
	ClientSecretExpiresAt   sql.NullTime          `db:"client_secret_expires_at" json:"client_secret_expires_at"`
	GrantTypes              []string              `db:"grant_types" json:"grant_types"`
	ResponseTypes           []string              `db:"response_types" json:"response_types"`
	TokenEndpointAuthMethod sql.NullString        `db:"token_endpoint_auth_method" json:"token_endpoint_auth_method"`
	Scope                   sql.NullString        `db:"scope" json:"scope"`
	Contacts                []string              `db:"contacts" json:"contacts"`
	ClientUri               sql.NullString        `db:"client_uri" json:"client_uri"`
	LogoUri                 sql.NullString        `db:"logo_uri" json:"logo_uri"`
	TosUri                  sql.NullString        `db:"tos_uri" json:"tos_uri"`
	PolicyUri               sql.NullString        `db:"policy_uri" json:"policy_uri"`
	JwksUri                 sql.NullString        `db:"jwks_uri" json:"jwks_uri"`
	Jwks                    pqtype.NullRawMessage `db:"jwks" json:"jwks"`
	SoftwareID              sql.NullString        `db:"software_id" json:"software_id"`
	SoftwareVersion         sql.NullString        `db:"software_version" json:"software_version"`
}

func (q *sqlQuerier) UpdateOAuth2ProviderAppByClientID(ctx context.Context, arg UpdateOAuth2ProviderAppByClientIDParams) (OAuth2ProviderApp, error) {
	row := q.db.QueryRowContext(ctx, updateOAuth2ProviderAppByClientID,
		arg.ID,
		arg.UpdatedAt,
		arg.Name,
		arg.Icon,
		arg.CallbackURL,
		pq.Array(arg.RedirectUris),
		arg.ClientType,
		arg.ClientSecretExpiresAt,
		pq.Array(arg.GrantTypes),
		pq.Array(arg.ResponseTypes),
		arg.TokenEndpointAuthMethod,
		arg.Scope,
		pq.Array(arg.Contacts),
		arg.ClientUri,
		arg.LogoUri,
		arg.TosUri,
		arg.PolicyUri,
		arg.JwksUri,
		arg.Jwks,
		arg.SoftwareID,
		arg.SoftwareVersion,
	)
	var i OAuth2ProviderApp
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Icon,
		&i.CallbackURL,
		pq.Array(&i.RedirectUris),
		&i.ClientType,
		&i.DynamicallyRegistered,
		&i.ClientIDIssuedAt,
		&i.ClientSecretExpiresAt,
		pq.Array(&i.GrantTypes),
		pq.Array(&i.ResponseTypes),
		&i.TokenEndpointAuthMethod,
		&i.Scope,
		pq.Array(&i.Contacts),
		&i.ClientUri,
		&i.LogoUri,
		&i.TosUri,
		&i.PolicyUri,
		&i.JwksUri,
		&i.Jwks,
		&i.SoftwareID,
		&i.SoftwareVersion,
		&i.RegistrationAccessToken,
		&i.RegistrationClientUri,
	)
	return i, err
}

const updateOAuth2ProviderAppByID = `-- name: UpdateOAuth2ProviderAppByID :one
UPDATE oauth2_provider_apps SET
    updated_at = $2,
    name = $3,
    icon = $4,
    callback_url = $5,
    redirect_uris = $6,
    client_type = $7,
    dynamically_registered = $8,
    client_secret_expires_at = $9,
    grant_types = $10,
    response_types = $11,
    token_endpoint_auth_method = $12,
    scope = $13,
    contacts = $14,
    client_uri = $15,
    logo_uri = $16,
    tos_uri = $17,
    policy_uri = $18,
    jwks_uri = $19,
    jwks = $20,
    software_id = $21,
    software_version = $22
WHERE id = $1 RETURNING id, created_at, updated_at, name, icon, callback_url, redirect_uris, client_type, dynamically_registered, client_id_issued_at, client_secret_expires_at, grant_types, response_types, token_endpoint_auth_method, scope, contacts, client_uri, logo_uri, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, registration_access_token, registration_client_uri
`

type UpdateOAuth2ProviderAppByIDParams struct {
	ID                      uuid.UUID             `db:"id" json:"id"`
	UpdatedAt               time.Time             `db:"updated_at" json:"updated_at"`
	Name                    string                `db:"name" json:"name"`
	Icon                    string                `db:"icon" json:"icon"`
	CallbackURL             string                `db:"callback_url" json:"callback_url"`
	RedirectUris            []string              `db:"redirect_uris" json:"redirect_uris"`
	ClientType              sql.NullString        `db:"client_type" json:"client_type"`
	DynamicallyRegistered   sql.NullBool          `db:"dynamically_registered" json:"dynamically_registered"`
	ClientSecretExpiresAt   sql.NullTime          `db:"client_secret_expires_at" json:"client_secret_expires_at"`
	GrantTypes              []string              `db:"grant_types" json:"grant_types"`
	ResponseTypes           []string              `db:"response_types" json:"response_types"`
	TokenEndpointAuthMethod sql.NullString        `db:"token_endpoint_auth_method" json:"token_endpoint_auth_method"`
	Scope                   sql.NullString        `db:"scope" json:"scope"`
	Contacts                []string              `db:"contacts" json:"contacts"`
	ClientUri               sql.NullString        `db:"client_uri" json:"client_uri"`
	LogoUri                 sql.NullString        `db:"logo_uri" json:"logo_uri"`
	TosUri                  sql.NullString        `db:"tos_uri" json:"tos_uri"`
	PolicyUri               sql.NullString        `db:"policy_uri" json:"policy_uri"`
	JwksUri                 sql.NullString        `db:"jwks_uri" json:"jwks_uri"`
	Jwks                    pqtype.NullRawMessage `db:"jwks" json:"jwks"`
	SoftwareID              sql.NullString        `db:"software_id" json:"software_id"`
	SoftwareVersion         sql.NullString        `db:"software_version" json:"software_version"`
}

func (q *sqlQuerier) UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) {
	row := q.db.QueryRowContext(ctx, updateOAuth2ProviderAppByID,
		arg.ID,
		arg.UpdatedAt,
		arg.Name,
		arg.Icon,
		arg.CallbackURL,
		pq.Array(arg.RedirectUris),
		arg.ClientType,
		arg.DynamicallyRegistered,
		arg.ClientSecretExpiresAt,
		pq.Array(arg.GrantTypes),
		pq.Array(arg.ResponseTypes),
		arg.TokenEndpointAuthMethod,
		arg.Scope,
		pq.Array(arg.Contacts),
		arg.ClientUri,
		arg.LogoUri,
		arg.TosUri,
		arg.PolicyUri,
		arg.JwksUri,
		arg.Jwks,
		arg.SoftwareID,
		arg.SoftwareVersion,
	)
	var i OAuth2ProviderApp
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Icon,
		&i.CallbackURL,
		pq.Array(&i.RedirectUris),
		&i.ClientType,
		&i.DynamicallyRegistered,
		&i.ClientIDIssuedAt,
		&i.ClientSecretExpiresAt,
		pq.Array(&i.GrantTypes),
		pq.Array(&i.ResponseTypes),
		&i.TokenEndpointAuthMethod,
		&i.Scope,
		pq.Array(&i.Contacts),
		&i.ClientUri,
		&i.LogoUri,
		&i.TosUri,
		&i.PolicyUri,
		&i.JwksUri,
		&i.Jwks,
		&i.SoftwareID,
		&i.SoftwareVersion,
		&i.RegistrationAccessToken,
		&i.RegistrationClientUri,
	)
	return i, err
}

const deleteOrganizationMember = `-- name: DeleteOrganizationMember :exec
DELETE
	FROM
		organization_members
	WHERE
		organization_id = $1 AND
		user_id = $2
`

type DeleteOrganizationMemberParams struct {
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	UserID         uuid.UUID `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error {
	_, err := q.db.ExecContext(ctx, deleteOrganizationMember, arg.OrganizationID, arg.UserID)
	return err
}

const getOrganizationIDsByMemberIDs = `-- name: GetOrganizationIDsByMemberIDs :many
SELECT
    user_id, array_agg(organization_id) :: uuid [ ] AS "organization_IDs"
FROM
    organization_members
WHERE
    user_id = ANY($1 :: uuid [ ])
GROUP BY
    user_id
`

type GetOrganizationIDsByMemberIDsRow struct {
	UserID          uuid.UUID   `db:"user_id" json:"user_id"`
	OrganizationIDs []uuid.UUID `db:"organization_IDs" json:"organization_IDs"`
}

func (q *sqlQuerier) GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error) {
	rows, err := q.db.QueryContext(ctx, getOrganizationIDsByMemberIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetOrganizationIDsByMemberIDsRow
	for rows.Next() {
		var i GetOrganizationIDsByMemberIDsRow
		if err := rows.Scan(&i.UserID, pq.Array(&i.OrganizationIDs)); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertOrganizationMember = `-- name: InsertOrganizationMember :one
INSERT INTO
	organization_members (
		organization_id,
		user_id,
		created_at,
		updated_at,
		roles
	)
VALUES
	($1, $2, $3, $4, $5) RETURNING user_id, organization_id, created_at, updated_at, roles
`

type InsertOrganizationMemberParams struct {
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	UserID         uuid.UUID `db:"user_id" json:"user_id"`
	CreatedAt      time.Time `db:"created_at" json:"created_at"`
	UpdatedAt      time.Time `db:"updated_at" json:"updated_at"`
	Roles          []string  `db:"roles" json:"roles"`
}

func (q *sqlQuerier) InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error) {
	row := q.db.QueryRowContext(ctx, insertOrganizationMember,
		arg.OrganizationID,
		arg.UserID,
		arg.CreatedAt,
		arg.UpdatedAt,
		pq.Array(arg.Roles),
	)
	var i OrganizationMember
	err := row.Scan(
		&i.UserID,
		&i.OrganizationID,
		&i.CreatedAt,
		&i.UpdatedAt,
		pq.Array(&i.Roles),
	)
	return i, err
}

const organizationMembers = `-- name: OrganizationMembers :many
SELECT
	organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles,
	users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles",
	users.last_seen_at, users.status, users.login_type, users.is_service_account,
	users.created_at as user_created_at, users.updated_at as user_updated_at
FROM
	organization_members
		INNER JOIN
	users ON organization_members.user_id = users.id AND users.deleted = false
WHERE
	-- Filter by organization id
	CASE
		WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			organization_id = $1
		ELSE true
	END
	-- Filter by user id
	AND CASE
		WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			user_id = $2
		ELSE true
	END
  -- Filter by system type
  	AND CASE
		  WHEN $3::bool THEN TRUE
		  ELSE
			  is_system = false
	END
  -- Filter by github user ID. Note that this requires a join on the users table.
  AND CASE
    WHEN $4 :: bigint != 0 THEN
      users.github_com_user_id = $4
    ELSE true
  END
`

type OrganizationMembersParams struct {
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	UserID         uuid.UUID `db:"user_id" json:"user_id"`
	IncludeSystem  bool      `db:"include_system" json:"include_system"`
	GithubUserID   int64     `db:"github_user_id" json:"github_user_id"`
}

type OrganizationMembersRow struct {
	OrganizationMember OrganizationMember `db:"organization_member" json:"organization_member"`
	Username           string             `db:"username" json:"username"`
	AvatarURL          string             `db:"avatar_url" json:"avatar_url"`
	Name               string             `db:"name" json:"name"`
	Email              string             `db:"email" json:"email"`
	GlobalRoles        pq.StringArray     `db:"global_roles" json:"global_roles"`
	LastSeenAt         time.Time          `db:"last_seen_at" json:"last_seen_at"`
	Status             UserStatus         `db:"status" json:"status"`
	LoginType          LoginType          `db:"login_type" json:"login_type"`
	IsServiceAccount   bool               `db:"is_service_account" json:"is_service_account"`
	UserCreatedAt      time.Time          `db:"user_created_at" json:"user_created_at"`
	UserUpdatedAt      time.Time          `db:"user_updated_at" json:"user_updated_at"`
}

// Arguments are optional with uuid.Nil to ignore.
//   - Use just 'organization_id' to get all members of an org
//   - Use just 'user_id' to get all orgs a user is a member of
//   - Use both to get a specific org member row
func (q *sqlQuerier) OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error) {
	rows, err := q.db.QueryContext(ctx, organizationMembers,
		arg.OrganizationID,
		arg.UserID,
		arg.IncludeSystem,
		arg.GithubUserID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []OrganizationMembersRow
	for rows.Next() {
		var i OrganizationMembersRow
		if err := rows.Scan(
			&i.OrganizationMember.UserID,
			&i.OrganizationMember.OrganizationID,
			&i.OrganizationMember.CreatedAt,
			&i.OrganizationMember.UpdatedAt,
			pq.Array(&i.OrganizationMember.Roles),
			&i.Username,
			&i.AvatarURL,
			&i.Name,
			&i.Email,
			&i.GlobalRoles,
			&i.LastSeenAt,
			&i.Status,
			&i.LoginType,
			&i.IsServiceAccount,
			&i.UserCreatedAt,
			&i.UserUpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const paginatedOrganizationMembers = `-- name: PaginatedOrganizationMembers :many
SELECT
	organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles,
	users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles",
	users.last_seen_at, users.status, users.login_type, users.is_service_account,
	users.created_at as user_created_at, users.updated_at as user_updated_at,
	COUNT(*) OVER() AS count
FROM
	organization_members
INNER JOIN
	users ON organization_members.user_id = users.id AND users.deleted = false
WHERE
	CASE
		-- This allows using the last element on a page as effectively a cursor.
		-- This is an important option for scripts that need to paginate without
		-- duplicating or missing data.
		WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
			-- The pagination cursor is the last ID of the previous page.
			-- The query is ordered by the username field, so select all
			-- rows after the cursor.
			(LOWER(users.username)) > (
				SELECT
					LOWER(users.username)
				FROM
					organization_members
				INNER JOIN
					users ON organization_members.user_id = users.id
				WHERE
					organization_members.user_id = $1
			)
		)
		ELSE true
	END
	-- Start filters
	-- Filter by organization id
	AND CASE
		WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			organization_id = $2
		ELSE true
	END
	-- Filter by email or username
	AND CASE
		WHEN $3 :: text != '' THEN (
			users.email ILIKE concat('%', $3, '%')
			OR users.username ILIKE concat('%', $3, '%')
		)
		ELSE true
	END
	-- Filter by name (display name)
	AND CASE
		WHEN $4 :: text != '' THEN
			users.name ILIKE concat('%', $4, '%')
		ELSE true
	END
	-- Filter by status
	AND CASE
		-- @status needs to be a text because it can be empty, If it was
		-- user_status enum, it would not.
		WHEN cardinality($5 :: user_status[]) > 0 THEN
			users.status = ANY($5 :: user_status[])
		ELSE true
	END
	-- Filter by global rbac_roles
	AND CASE
		-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
		-- everyone is a member.
		WHEN cardinality($6 :: text[]) > 0 AND 'member' != ANY($6 :: text[]) THEN
			users.rbac_roles && $6 :: text[]
		ELSE true
	END
	-- Filter by last_seen
	AND CASE
		WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			users.last_seen_at <= $7
		ELSE true
	END
	AND CASE
		WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			users.last_seen_at >= $8
		ELSE true
	END
	-- Filter by created_at (user creation date, not date added to org)
	AND CASE
		WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			users.created_at <= $9
		ELSE true
	END
	AND CASE
		WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			users.created_at >= $10
		ELSE true
	END
	 -- Filter by system type
	AND CASE
		WHEN $11::bool THEN TRUE
		ELSE users.is_system = false
	END
	 -- Filter by github.com user ID
	AND CASE
		WHEN $12 :: bigint != 0 THEN
			users.github_com_user_id = $12
		ELSE true
	END
	-- Filter by login_type
	AND CASE
		WHEN cardinality($13 :: login_type[]) > 0 THEN
			users.login_type = ANY($13 :: login_type[])
		ELSE true
	END
	-- Filter by service account.
	AND CASE
		WHEN $14 :: boolean IS NOT NULL THEN
			users.is_service_account = $14 :: boolean
		ELSE true
	END
	-- End of filters
ORDER BY
	-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
	LOWER(users.username) ASC OFFSET $15
LIMIT
	-- A null limit means "no limit", so 0 means return all
	NULLIF($16 :: int, 0)
`

type PaginatedOrganizationMembersParams struct {
	AfterID          uuid.UUID    `db:"after_id" json:"after_id"`
	OrganizationID   uuid.UUID    `db:"organization_id" json:"organization_id"`
	Search           string       `db:"search" json:"search"`
	Name             string       `db:"name" json:"name"`
	Status           []UserStatus `db:"status" json:"status"`
	RbacRole         []string     `db:"rbac_role" json:"rbac_role"`
	LastSeenBefore   time.Time    `db:"last_seen_before" json:"last_seen_before"`
	LastSeenAfter    time.Time    `db:"last_seen_after" json:"last_seen_after"`
	CreatedBefore    time.Time    `db:"created_before" json:"created_before"`
	CreatedAfter     time.Time    `db:"created_after" json:"created_after"`
	IncludeSystem    bool         `db:"include_system" json:"include_system"`
	GithubComUserID  int64        `db:"github_com_user_id" json:"github_com_user_id"`
	LoginType        []LoginType  `db:"login_type" json:"login_type"`
	IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"`
	OffsetOpt        int32        `db:"offset_opt" json:"offset_opt"`
	LimitOpt         int32        `db:"limit_opt" json:"limit_opt"`
}

type PaginatedOrganizationMembersRow struct {
	OrganizationMember OrganizationMember `db:"organization_member" json:"organization_member"`
	Username           string             `db:"username" json:"username"`
	AvatarURL          string             `db:"avatar_url" json:"avatar_url"`
	Name               string             `db:"name" json:"name"`
	Email              string             `db:"email" json:"email"`
	GlobalRoles        pq.StringArray     `db:"global_roles" json:"global_roles"`
	LastSeenAt         time.Time          `db:"last_seen_at" json:"last_seen_at"`
	Status             UserStatus         `db:"status" json:"status"`
	LoginType          LoginType          `db:"login_type" json:"login_type"`
	IsServiceAccount   bool               `db:"is_service_account" json:"is_service_account"`
	UserCreatedAt      time.Time          `db:"user_created_at" json:"user_created_at"`
	UserUpdatedAt      time.Time          `db:"user_updated_at" json:"user_updated_at"`
	Count              int64              `db:"count" json:"count"`
}

func (q *sqlQuerier) PaginatedOrganizationMembers(ctx context.Context, arg PaginatedOrganizationMembersParams) ([]PaginatedOrganizationMembersRow, error) {
	rows, err := q.db.QueryContext(ctx, paginatedOrganizationMembers,
		arg.AfterID,
		arg.OrganizationID,
		arg.Search,
		arg.Name,
		pq.Array(arg.Status),
		pq.Array(arg.RbacRole),
		arg.LastSeenBefore,
		arg.LastSeenAfter,
		arg.CreatedBefore,
		arg.CreatedAfter,
		arg.IncludeSystem,
		arg.GithubComUserID,
		pq.Array(arg.LoginType),
		arg.IsServiceAccount,
		arg.OffsetOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []PaginatedOrganizationMembersRow
	for rows.Next() {
		var i PaginatedOrganizationMembersRow
		if err := rows.Scan(
			&i.OrganizationMember.UserID,
			&i.OrganizationMember.OrganizationID,
			&i.OrganizationMember.CreatedAt,
			&i.OrganizationMember.UpdatedAt,
			pq.Array(&i.OrganizationMember.Roles),
			&i.Username,
			&i.AvatarURL,
			&i.Name,
			&i.Email,
			&i.GlobalRoles,
			&i.LastSeenAt,
			&i.Status,
			&i.LoginType,
			&i.IsServiceAccount,
			&i.UserCreatedAt,
			&i.UserUpdatedAt,
			&i.Count,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateMemberRoles = `-- name: UpdateMemberRoles :one
UPDATE
	organization_members
SET
	-- Remove all duplicates from the roles.
	roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
WHERE
	user_id = $2
	AND organization_id = $3
RETURNING user_id, organization_id, created_at, updated_at, roles
`

type UpdateMemberRolesParams struct {
	GrantedRoles []string  `db:"granted_roles" json:"granted_roles"`
	UserID       uuid.UUID `db:"user_id" json:"user_id"`
	OrgID        uuid.UUID `db:"org_id" json:"org_id"`
}

func (q *sqlQuerier) UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) {
	row := q.db.QueryRowContext(ctx, updateMemberRoles, pq.Array(arg.GrantedRoles), arg.UserID, arg.OrgID)
	var i OrganizationMember
	err := row.Scan(
		&i.UserID,
		&i.OrganizationID,
		&i.CreatedAt,
		&i.UpdatedAt,
		pq.Array(&i.Roles),
	)
	return i, err
}

const getDefaultOrganization = `-- name: GetDefaultOrganization :one
SELECT
    id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
    organizations
WHERE
    is_default = true
LIMIT
    1
`

func (q *sqlQuerier) GetDefaultOrganization(ctx context.Context) (Organization, error) {
	row := q.db.QueryRowContext(ctx, getDefaultOrganization)
	var i Organization
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.Description,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.IsDefault,
		&i.DisplayName,
		&i.Icon,
		&i.Deleted,
		&i.ShareableWorkspaceOwners,
	)
	return i, err
}

const getOrganizationByID = `-- name: GetOrganizationByID :one
SELECT
    id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
    organizations
WHERE
    id = $1
`

func (q *sqlQuerier) GetOrganizationByID(ctx context.Context, id uuid.UUID) (Organization, error) {
	row := q.db.QueryRowContext(ctx, getOrganizationByID, id)
	var i Organization
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.Description,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.IsDefault,
		&i.DisplayName,
		&i.Icon,
		&i.Deleted,
		&i.ShareableWorkspaceOwners,
	)
	return i, err
}

const getOrganizationByName = `-- name: GetOrganizationByName :one
SELECT
    id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
    organizations
WHERE
    -- Optionally include deleted organizations
    deleted = $1 AND
    LOWER("name") = LOWER($2)
LIMIT
    1
`

type GetOrganizationByNameParams struct {
	Deleted bool   `db:"deleted" json:"deleted"`
	Name    string `db:"name" json:"name"`
}

func (q *sqlQuerier) GetOrganizationByName(ctx context.Context, arg GetOrganizationByNameParams) (Organization, error) {
	row := q.db.QueryRowContext(ctx, getOrganizationByName, arg.Deleted, arg.Name)
	var i Organization
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.Description,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.IsDefault,
		&i.DisplayName,
		&i.Icon,
		&i.Deleted,
		&i.ShareableWorkspaceOwners,
	)
	return i, err
}

const getOrganizationResourceCountByID = `-- name: GetOrganizationResourceCountByID :one
SELECT
	(
		SELECT
			count(*)
		FROM
			workspaces
		WHERE
			workspaces.organization_id = $1
			AND workspaces.deleted = FALSE) AS workspace_count,
	(
		SELECT
			count(*)
		FROM
			GROUPS
		WHERE
			groups.organization_id = $1) AS group_count,
	(
		SELECT
			count(*)
		FROM
			templates
		WHERE
			templates.organization_id = $1
			AND templates.deleted = FALSE) AS template_count,
	(
		SELECT
			count(*)
		FROM
			organization_members
		LEFT JOIN users ON organization_members.user_id = users.id
	WHERE
		organization_members.organization_id = $1
		AND users.deleted = FALSE) AS member_count,
(
	SELECT
		count(*)
	FROM
		provisioner_keys
	WHERE
		provisioner_keys.organization_id = $1) AS provisioner_key_count
`

type GetOrganizationResourceCountByIDRow struct {
	WorkspaceCount      int64 `db:"workspace_count" json:"workspace_count"`
	GroupCount          int64 `db:"group_count" json:"group_count"`
	TemplateCount       int64 `db:"template_count" json:"template_count"`
	MemberCount         int64 `db:"member_count" json:"member_count"`
	ProvisionerKeyCount int64 `db:"provisioner_key_count" json:"provisioner_key_count"`
}

func (q *sqlQuerier) GetOrganizationResourceCountByID(ctx context.Context, organizationID uuid.UUID) (GetOrganizationResourceCountByIDRow, error) {
	row := q.db.QueryRowContext(ctx, getOrganizationResourceCountByID, organizationID)
	var i GetOrganizationResourceCountByIDRow
	err := row.Scan(
		&i.WorkspaceCount,
		&i.GroupCount,
		&i.TemplateCount,
		&i.MemberCount,
		&i.ProvisionerKeyCount,
	)
	return i, err
}

const getOrganizations = `-- name: GetOrganizations :many
SELECT
    id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
    organizations
WHERE
    -- Optionally include deleted organizations
    deleted = $1
      -- Filter by ids
    AND CASE
        WHEN array_length($2 :: uuid[], 1) > 0 THEN
            id = ANY($2)
        ELSE true
    END
    AND CASE
          WHEN $3::text != '' THEN
              LOWER("name") = LOWER($3)
          ELSE true
    END
`

type GetOrganizationsParams struct {
	Deleted bool        `db:"deleted" json:"deleted"`
	IDs     []uuid.UUID `db:"ids" json:"ids"`
	Name    string      `db:"name" json:"name"`
}

func (q *sqlQuerier) GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error) {
	rows, err := q.db.QueryContext(ctx, getOrganizations, arg.Deleted, pq.Array(arg.IDs), arg.Name)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Organization
	for rows.Next() {
		var i Organization
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.Description,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.IsDefault,
			&i.DisplayName,
			&i.Icon,
			&i.Deleted,
			&i.ShareableWorkspaceOwners,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getOrganizationsByUserID = `-- name: GetOrganizationsByUserID :many
SELECT
    id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
FROM
    organizations
WHERE
    -- Optionally provide a filter for deleted organizations.
  	CASE WHEN
  	    $2 :: boolean IS NULL THEN
			true
		ELSE
			deleted = $2
	END AND
    id = ANY(
        SELECT
            organization_id
        FROM
            organization_members
        WHERE
            user_id = $1
    )
`

type GetOrganizationsByUserIDParams struct {
	UserID  uuid.UUID    `db:"user_id" json:"user_id"`
	Deleted sql.NullBool `db:"deleted" json:"deleted"`
}

func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error) {
	rows, err := q.db.QueryContext(ctx, getOrganizationsByUserID, arg.UserID, arg.Deleted)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Organization
	for rows.Next() {
		var i Organization
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.Description,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.IsDefault,
			&i.DisplayName,
			&i.Icon,
			&i.Deleted,
			&i.ShareableWorkspaceOwners,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertOrganization = `-- name: InsertOrganization :one
INSERT INTO
    organizations (id, "name", display_name, description, icon, created_at, updated_at, is_default)
VALUES
    -- If no organizations exist, and this is the first, make it the default.
    ($1, $2, $3, $4, $5, $6, $7, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
`

type InsertOrganizationParams struct {
	ID          uuid.UUID `db:"id" json:"id"`
	Name        string    `db:"name" json:"name"`
	DisplayName string    `db:"display_name" json:"display_name"`
	Description string    `db:"description" json:"description"`
	Icon        string    `db:"icon" json:"icon"`
	CreatedAt   time.Time `db:"created_at" json:"created_at"`
	UpdatedAt   time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error) {
	row := q.db.QueryRowContext(ctx, insertOrganization,
		arg.ID,
		arg.Name,
		arg.DisplayName,
		arg.Description,
		arg.Icon,
		arg.CreatedAt,
		arg.UpdatedAt,
	)
	var i Organization
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.Description,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.IsDefault,
		&i.DisplayName,
		&i.Icon,
		&i.Deleted,
		&i.ShareableWorkspaceOwners,
	)
	return i, err
}

const updateOrganization = `-- name: UpdateOrganization :one
UPDATE
    organizations
SET
    updated_at = $1,
    name = $2,
    display_name = $3,
    description = $4,
    icon = $5
WHERE
    id = $6
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
`

type UpdateOrganizationParams struct {
	UpdatedAt   time.Time `db:"updated_at" json:"updated_at"`
	Name        string    `db:"name" json:"name"`
	DisplayName string    `db:"display_name" json:"display_name"`
	Description string    `db:"description" json:"description"`
	Icon        string    `db:"icon" json:"icon"`
	ID          uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) {
	row := q.db.QueryRowContext(ctx, updateOrganization,
		arg.UpdatedAt,
		arg.Name,
		arg.DisplayName,
		arg.Description,
		arg.Icon,
		arg.ID,
	)
	var i Organization
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.Description,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.IsDefault,
		&i.DisplayName,
		&i.Icon,
		&i.Deleted,
		&i.ShareableWorkspaceOwners,
	)
	return i, err
}

const updateOrganizationDeletedByID = `-- name: UpdateOrganizationDeletedByID :exec
UPDATE organizations
SET
    deleted = true,
    updated_at = $1
WHERE
    id = $2 AND
    is_default = false
`

type UpdateOrganizationDeletedByIDParams struct {
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
	ID        uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateOrganizationDeletedByID(ctx context.Context, arg UpdateOrganizationDeletedByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateOrganizationDeletedByID, arg.UpdatedAt, arg.ID)
	return err
}

const updateOrganizationWorkspaceSharingSettings = `-- name: UpdateOrganizationWorkspaceSharingSettings :one
UPDATE
    organizations
SET
    shareable_workspace_owners = $1,
    updated_at = $2
WHERE
    id = $3
RETURNING id, name, description, created_at, updated_at, is_default, display_name, icon, deleted, shareable_workspace_owners
`

type UpdateOrganizationWorkspaceSharingSettingsParams struct {
	ShareableWorkspaceOwners ShareableWorkspaceOwners `db:"shareable_workspace_owners" json:"shareable_workspace_owners"`
	UpdatedAt                time.Time                `db:"updated_at" json:"updated_at"`
	ID                       uuid.UUID                `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg UpdateOrganizationWorkspaceSharingSettingsParams) (Organization, error) {
	row := q.db.QueryRowContext(ctx, updateOrganizationWorkspaceSharingSettings, arg.ShareableWorkspaceOwners, arg.UpdatedAt, arg.ID)
	var i Organization
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.Description,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.IsDefault,
		&i.DisplayName,
		&i.Icon,
		&i.Deleted,
		&i.ShareableWorkspaceOwners,
	)
	return i, err
}

const getParameterSchemasByJobID = `-- name: GetParameterSchemasByJobID :many
SELECT
	id, created_at, job_id, name, description, default_source_scheme, default_source_value, allow_override_source, default_destination_scheme, allow_override_destination, default_refresh, redisplay_value, validation_error, validation_condition, validation_type_system, validation_value_type, index
FROM
	parameter_schemas
WHERE
	job_id = $1
ORDER BY
	index
`

func (q *sqlQuerier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error) {
	rows, err := q.db.QueryContext(ctx, getParameterSchemasByJobID, jobID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ParameterSchema
	for rows.Next() {
		var i ParameterSchema
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.JobID,
			&i.Name,
			&i.Description,
			&i.DefaultSourceScheme,
			&i.DefaultSourceValue,
			&i.AllowOverrideSource,
			&i.DefaultDestinationScheme,
			&i.AllowOverrideDestination,
			&i.DefaultRefresh,
			&i.RedisplayValue,
			&i.ValidationError,
			&i.ValidationCondition,
			&i.ValidationTypeSystem,
			&i.ValidationValueType,
			&i.Index,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const claimPrebuiltWorkspace = `-- name: ClaimPrebuiltWorkspace :one
UPDATE workspaces w
SET owner_id   = $1::uuid,
	name       = $2::text,
	updated_at = $3::timestamptz,
	-- Update autostart_schedule, next_start_at and ttl according to template and workspace-level
	-- configurations, allowing the workspace to be managed by the lifecycle executor as expected.
	autostart_schedule = $4,
	next_start_at = $5,
	ttl = $6,
	-- Update last_used_at during claim to ensure the claimed workspace is treated as recently used.
	-- This avoids unintended dormancy caused by prebuilds having stale usage timestamps.
	last_used_at = $3::timestamptz,
	-- Clear dormant and deletion timestamps as a safeguard to ensure a clean lifecycle state after claim.
	-- These fields should not be set on prebuilds, but we defensively reset them here to prevent
	-- accidental dormancy or deletion by the lifecycle executor.
	dormant_at = NULL,
	deleting_at = NULL
WHERE w.id IN (
	SELECT p.id
	FROM workspace_prebuilds p
		INNER JOIN workspace_latest_builds b ON b.workspace_id = p.id
		INNER JOIN templates t ON p.template_id = t.id
	WHERE (b.transition = 'start'::workspace_transition
		AND b.job_status IN ('succeeded'::provisioner_job_status))
		-- The prebuilds system should never try to claim a prebuild for an inactive template version.
		-- Nevertheless, this filter is here as a defensive measure:
		AND b.template_version_id = t.active_version_id
		AND p.current_preset_id = $7::uuid
		AND p.ready
		AND NOT t.deleted
	LIMIT 1 FOR UPDATE OF p SKIP LOCKED -- Ensure that a concurrent request will not select the same prebuild.
)
RETURNING w.id, w.name
`

type ClaimPrebuiltWorkspaceParams struct {
	NewUserID         uuid.UUID      `db:"new_user_id" json:"new_user_id"`
	NewName           string         `db:"new_name" json:"new_name"`
	Now               time.Time      `db:"now" json:"now"`
	AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
	NextStartAt       sql.NullTime   `db:"next_start_at" json:"next_start_at"`
	WorkspaceTtl      sql.NullInt64  `db:"workspace_ttl" json:"workspace_ttl"`
	PresetID          uuid.UUID      `db:"preset_id" json:"preset_id"`
}

type ClaimPrebuiltWorkspaceRow struct {
	ID   uuid.UUID `db:"id" json:"id"`
	Name string    `db:"name" json:"name"`
}

func (q *sqlQuerier) ClaimPrebuiltWorkspace(ctx context.Context, arg ClaimPrebuiltWorkspaceParams) (ClaimPrebuiltWorkspaceRow, error) {
	row := q.db.QueryRowContext(ctx, claimPrebuiltWorkspace,
		arg.NewUserID,
		arg.NewName,
		arg.Now,
		arg.AutostartSchedule,
		arg.NextStartAt,
		arg.WorkspaceTtl,
		arg.PresetID,
	)
	var i ClaimPrebuiltWorkspaceRow
	err := row.Scan(&i.ID, &i.Name)
	return i, err
}

const countInProgressPrebuilds = `-- name: CountInProgressPrebuilds :many
SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count, wlb.template_version_preset_id as preset_id
FROM workspace_latest_builds wlb
		INNER JOIN workspace_prebuild_builds wpb ON wpb.id = wlb.id
		-- We only need these counts for active template versions.
		-- It doesn't influence whether we create or delete prebuilds
		-- for inactive template versions. This is because we never create
		-- prebuilds for inactive template versions, we always delete
		-- running prebuilds for inactive template versions, and we ignore
		-- prebuilds that are still building.
		INNER JOIN templates t ON t.active_version_id = wlb.template_version_id
WHERE wlb.job_status IN ('pending'::provisioner_job_status, 'running'::provisioner_job_status)
  -- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running.
GROUP BY t.id, wpb.template_version_id, wpb.transition, wlb.template_version_preset_id
`

type CountInProgressPrebuildsRow struct {
	TemplateID        uuid.UUID           `db:"template_id" json:"template_id"`
	TemplateVersionID uuid.UUID           `db:"template_version_id" json:"template_version_id"`
	Transition        WorkspaceTransition `db:"transition" json:"transition"`
	Count             int32               `db:"count" json:"count"`
	PresetID          uuid.NullUUID       `db:"preset_id" json:"preset_id"`
}

// CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by preset ID and transition.
// Prebuild considered in-progress if it's in the "pending", "starting", "stopping", or "deleting" state.
func (q *sqlQuerier) CountInProgressPrebuilds(ctx context.Context) ([]CountInProgressPrebuildsRow, error) {
	rows, err := q.db.QueryContext(ctx, countInProgressPrebuilds)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []CountInProgressPrebuildsRow
	for rows.Next() {
		var i CountInProgressPrebuildsRow
		if err := rows.Scan(
			&i.TemplateID,
			&i.TemplateVersionID,
			&i.Transition,
			&i.Count,
			&i.PresetID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const countPendingNonActivePrebuilds = `-- name: CountPendingNonActivePrebuilds :many
SELECT
	wpb.template_version_preset_id AS preset_id,
	COUNT(*)::int AS count
FROM workspace_prebuild_builds wpb
INNER JOIN provisioner_jobs pj ON pj.id = wpb.job_id
INNER JOIN workspaces w ON w.id = wpb.workspace_id
INNER JOIN templates t ON t.id = w.template_id
WHERE
	wpb.template_version_id != t.active_version_id
	-- Only considers initial builds, i.e. created by the reconciliation loop
	AND wpb.build_number = 1
	-- Only consider 'start' transitions (provisioning), not 'stop'/'delete' (deprovisioning)
	-- Deprovisioning jobs should complete naturally as they're already cleaning up resources
	AND wpb.transition = 'start'::workspace_transition
	-- Pending jobs that have not yet been picked up by a provisioner
	AND pj.job_status = 'pending'::provisioner_job_status
	AND pj.worker_id IS NULL
	AND pj.canceled_at IS NULL
	AND pj.completed_at IS NULL
GROUP BY wpb.template_version_preset_id
`

type CountPendingNonActivePrebuildsRow struct {
	PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"`
	Count    int32         `db:"count" json:"count"`
}

// CountPendingNonActivePrebuilds returns the number of pending prebuilds for non-active template versions
func (q *sqlQuerier) CountPendingNonActivePrebuilds(ctx context.Context) ([]CountPendingNonActivePrebuildsRow, error) {
	rows, err := q.db.QueryContext(ctx, countPendingNonActivePrebuilds)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []CountPendingNonActivePrebuildsRow
	for rows.Next() {
		var i CountPendingNonActivePrebuildsRow
		if err := rows.Scan(&i.PresetID, &i.Count); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const findMatchingPresetID = `-- name: FindMatchingPresetID :one
WITH provided_params AS (
	SELECT
		unnest($1::text[]) AS name,
		unnest($2::text[]) AS value
),
preset_matches AS (
	SELECT
		tvp.id AS template_version_preset_id,
		COALESCE(COUNT(tvpp.name), 0) AS total_preset_params,
		COALESCE(COUNT(pp.name), 0) AS matching_params
	FROM template_version_presets tvp
	LEFT JOIN template_version_preset_parameters tvpp ON tvpp.template_version_preset_id = tvp.id
	LEFT JOIN provided_params pp ON pp.name = tvpp.name AND pp.value = tvpp.value
	WHERE tvp.template_version_id = $3
	GROUP BY tvp.id
)
SELECT pm.template_version_preset_id
FROM preset_matches pm
WHERE pm.total_preset_params = pm.matching_params  -- All preset parameters must match
ORDER BY pm.total_preset_params DESC               -- Return the preset with the most parameters
LIMIT 1
`

type FindMatchingPresetIDParams struct {
	ParameterNames    []string  `db:"parameter_names" json:"parameter_names"`
	ParameterValues   []string  `db:"parameter_values" json:"parameter_values"`
	TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
}

// FindMatchingPresetID finds a preset ID that is the largest exact subset of the provided parameters.
// It returns the preset ID if a match is found, or NULL if no match is found.
// The query finds presets where all preset parameters are present in the provided parameters,
// and returns the preset with the most parameters (largest subset).
func (q *sqlQuerier) FindMatchingPresetID(ctx context.Context, arg FindMatchingPresetIDParams) (uuid.UUID, error) {
	row := q.db.QueryRowContext(ctx, findMatchingPresetID, pq.Array(arg.ParameterNames), pq.Array(arg.ParameterValues), arg.TemplateVersionID)
	var template_version_preset_id uuid.UUID
	err := row.Scan(&template_version_preset_id)
	return template_version_preset_id, err
}

const getOrganizationsWithPrebuildStatus = `-- name: GetOrganizationsWithPrebuildStatus :many
WITH orgs_with_prebuilds AS (
	-- Get unique organizations that have presets with prebuilds configured
	SELECT DISTINCT o.id, o.name
	FROM organizations o
	INNER JOIN templates t ON t.organization_id = o.id
	INNER JOIN template_versions tv ON tv.template_id = t.id
	INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id
	WHERE tvp.desired_instances IS NOT NULL
),
prebuild_user_membership AS (
	-- Check if the user is a member of the organizations
	SELECT om.organization_id
	FROM organization_members om
	INNER JOIN orgs_with_prebuilds owp ON owp.id = om.organization_id
	WHERE om.user_id = $1::uuid
),
prebuild_groups AS (
	-- Check if the organizations have the prebuilds group
	SELECT g.organization_id, g.id as group_id
	FROM groups g
	INNER JOIN orgs_with_prebuilds owp ON owp.id = g.organization_id
	WHERE g.name = $2::text
),
prebuild_group_membership AS (
	-- Check if the user is in the prebuilds group
	SELECT pg.organization_id
	FROM prebuild_groups pg
	INNER JOIN group_members gm ON gm.group_id = pg.group_id
	WHERE gm.user_id = $1::uuid
)
SELECT
	owp.id AS organization_id,
	owp.name AS organization_name,
	(pum.organization_id IS NOT NULL)::boolean AS has_prebuild_user,
	pg.group_id AS prebuilds_group_id,
	(pgm.organization_id IS NOT NULL)::boolean AS has_prebuild_user_in_group
FROM orgs_with_prebuilds owp
LEFT JOIN prebuild_groups pg ON pg.organization_id = owp.id
LEFT JOIN prebuild_user_membership pum ON pum.organization_id = owp.id
LEFT JOIN prebuild_group_membership pgm ON pgm.organization_id = owp.id
`

type GetOrganizationsWithPrebuildStatusParams struct {
	UserID    uuid.UUID `db:"user_id" json:"user_id"`
	GroupName string    `db:"group_name" json:"group_name"`
}

type GetOrganizationsWithPrebuildStatusRow struct {
	OrganizationID         uuid.UUID     `db:"organization_id" json:"organization_id"`
	OrganizationName       string        `db:"organization_name" json:"organization_name"`
	HasPrebuildUser        bool          `db:"has_prebuild_user" json:"has_prebuild_user"`
	PrebuildsGroupID       uuid.NullUUID `db:"prebuilds_group_id" json:"prebuilds_group_id"`
	HasPrebuildUserInGroup bool          `db:"has_prebuild_user_in_group" json:"has_prebuild_user_in_group"`
}

// GetOrganizationsWithPrebuildStatus returns organizations with prebuilds configured and their
// membership status for the prebuilds system user (org membership, group existence, group membership).
func (q *sqlQuerier) GetOrganizationsWithPrebuildStatus(ctx context.Context, arg GetOrganizationsWithPrebuildStatusParams) ([]GetOrganizationsWithPrebuildStatusRow, error) {
	rows, err := q.db.QueryContext(ctx, getOrganizationsWithPrebuildStatus, arg.UserID, arg.GroupName)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetOrganizationsWithPrebuildStatusRow
	for rows.Next() {
		var i GetOrganizationsWithPrebuildStatusRow
		if err := rows.Scan(
			&i.OrganizationID,
			&i.OrganizationName,
			&i.HasPrebuildUser,
			&i.PrebuildsGroupID,
			&i.HasPrebuildUserInGroup,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPrebuildMetrics = `-- name: GetPrebuildMetrics :many
SELECT
	t.name as template_name,
	tvp.name as preset_name,
	o.name as organization_name,
	COUNT(*) as created_count,
	COUNT(*) FILTER (WHERE pj.job_status = 'failed'::provisioner_job_status) as failed_count,
	COUNT(*) FILTER (
			WHERE w.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid -- The system user responsible for prebuilds.
		) as claimed_count
FROM workspaces w
INNER JOIN workspace_prebuild_builds wpb ON wpb.workspace_id = w.id
INNER JOIN templates t ON t.id = w.template_id
INNER JOIN template_version_presets tvp ON tvp.id = wpb.template_version_preset_id
INNER JOIN provisioner_jobs pj ON pj.id = wpb.job_id
INNER JOIN organizations o ON o.id = w.organization_id
WHERE NOT t.deleted AND wpb.build_number = 1
GROUP BY t.name, tvp.name, o.name
ORDER BY t.name, tvp.name, o.name
`

type GetPrebuildMetricsRow struct {
	TemplateName     string `db:"template_name" json:"template_name"`
	PresetName       string `db:"preset_name" json:"preset_name"`
	OrganizationName string `db:"organization_name" json:"organization_name"`
	CreatedCount     int64  `db:"created_count" json:"created_count"`
	FailedCount      int64  `db:"failed_count" json:"failed_count"`
	ClaimedCount     int64  `db:"claimed_count" json:"claimed_count"`
}

func (q *sqlQuerier) GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error) {
	rows, err := q.db.QueryContext(ctx, getPrebuildMetrics)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetPrebuildMetricsRow
	for rows.Next() {
		var i GetPrebuildMetricsRow
		if err := rows.Scan(
			&i.TemplateName,
			&i.PresetName,
			&i.OrganizationName,
			&i.CreatedCount,
			&i.FailedCount,
			&i.ClaimedCount,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPresetsAtFailureLimit = `-- name: GetPresetsAtFailureLimit :many
WITH filtered_builds AS (
	-- Only select builds which are for prebuild creations
	SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances
	FROM template_version_presets tvp
			INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id
			INNER JOIN workspaces w ON wlb.workspace_id = w.id
			INNER JOIN template_versions tv ON wlb.template_version_id = tv.id
			INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id
	WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
		AND wlb.transition = 'start'::workspace_transition
		AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'
),
time_sorted_builds AS (
	-- Group builds by preset, then sort each group by created_at.
	SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances,
		ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn
	FROM filtered_builds fb
)
SELECT
	tsb.template_version_id,
	tsb.preset_id
FROM time_sorted_builds tsb
WHERE tsb.rn <= $1::bigint
	AND tsb.job_status = 'failed'::provisioner_job_status
GROUP BY tsb.template_version_id, tsb.preset_id
HAVING COUNT(*) = $1::bigint
`

type GetPresetsAtFailureLimitRow struct {
	TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
	PresetID          uuid.UUID `db:"preset_id" json:"preset_id"`
}

// GetPresetsAtFailureLimit groups workspace builds by preset ID.
// Each preset is associated with exactly one template version ID.
// For each preset, the query checks the last hard_limit builds.
// If all of them failed, the preset is considered to have hit the hard failure limit.
// The query returns a list of preset IDs that have reached this failure threshold.
// Only active template versions with configured presets are considered.
// For each preset, check the last hard_limit builds.
// If all of them failed, the preset is considered to have hit the hard failure limit.
func (q *sqlQuerier) GetPresetsAtFailureLimit(ctx context.Context, hardLimit int64) ([]GetPresetsAtFailureLimitRow, error) {
	rows, err := q.db.QueryContext(ctx, getPresetsAtFailureLimit, hardLimit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetPresetsAtFailureLimitRow
	for rows.Next() {
		var i GetPresetsAtFailureLimitRow
		if err := rows.Scan(&i.TemplateVersionID, &i.PresetID); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPresetsBackoff = `-- name: GetPresetsBackoff :many
WITH filtered_builds AS (
	-- Only select builds which are for prebuild creations
	SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances
	FROM template_version_presets tvp
			INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id
			INNER JOIN workspaces w ON wlb.workspace_id = w.id
			INNER JOIN template_versions tv ON wlb.template_version_id = tv.id
			INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id
	WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
		AND wlb.transition = 'start'::workspace_transition
		AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'
		AND NOT t.deleted
),
time_sorted_builds AS (
	-- Group builds by preset, then sort each group by created_at.
	SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances,
		ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn
	FROM filtered_builds fb
),
failed_count AS (
	-- Count failed builds per preset in the given period
	SELECT preset_id, COUNT(*) AS num_failed
	FROM filtered_builds
	WHERE job_status = 'failed'::provisioner_job_status
		AND created_at >= $1::timestamptz
	GROUP BY preset_id
)
SELECT
		tsb.template_version_id,
		tsb.preset_id,
		COALESCE(fc.num_failed, 0)::int  AS num_failed,
		MAX(tsb.created_at)::timestamptz AS last_build_at
FROM time_sorted_builds tsb
		LEFT JOIN failed_count fc ON fc.preset_id = tsb.preset_id
WHERE tsb.rn <= tsb.desired_instances -- Fetch the last N builds, where N is the number of desired instances; if any fail, we backoff
		AND tsb.job_status = 'failed'::provisioner_job_status
		AND created_at >= $1::timestamptz
GROUP BY tsb.template_version_id, tsb.preset_id, fc.num_failed
`

type GetPresetsBackoffRow struct {
	TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
	PresetID          uuid.UUID `db:"preset_id" json:"preset_id"`
	NumFailed         int32     `db:"num_failed" json:"num_failed"`
	LastBuildAt       time.Time `db:"last_build_at" json:"last_build_at"`
}

// GetPresetsBackoff groups workspace builds by preset ID.
// Each preset is associated with exactly one template version ID.
// For each group, the query checks up to N of the most recent jobs that occurred within the
// lookback period, where N equals the number of desired instances for the corresponding preset.
// If at least one of the job within a group has failed, we should backoff on the corresponding preset ID.
// Query returns a list of preset IDs for which we should backoff.
// Only active template versions with configured presets are considered.
// We also return the number of failed workspace builds that occurred during the lookback period.
//
// NOTE:
// - To **decide whether to back off**, we look at up to the N most recent builds (within the defined lookback period).
// - To **calculate the number of failed builds**, we consider all builds within the defined lookback period.
//
// The number of failed builds is used downstream to determine the backoff duration.
func (q *sqlQuerier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]GetPresetsBackoffRow, error) {
	rows, err := q.db.QueryContext(ctx, getPresetsBackoff, lookback)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetPresetsBackoffRow
	for rows.Next() {
		var i GetPresetsBackoffRow
		if err := rows.Scan(
			&i.TemplateVersionID,
			&i.PresetID,
			&i.NumFailed,
			&i.LastBuildAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getRunningPrebuiltWorkspaces = `-- name: GetRunningPrebuiltWorkspaces :many
WITH latest_prebuilds AS (
	-- All workspaces that match the following criteria:
	-- 1. Owned by prebuilds user
	-- 2. Not deleted
	-- 3. Latest build is a 'start' transition
	-- 4. Latest build was successful
	SELECT
		workspaces.id,
		workspaces.name,
		workspaces.template_id,
		workspace_latest_builds.template_version_id,
		workspace_latest_builds.job_id,
		workspaces.created_at
	FROM workspace_latest_builds
	JOIN workspaces ON workspaces.id = workspace_latest_builds.workspace_id
	WHERE workspace_latest_builds.transition = 'start'::workspace_transition
	AND workspace_latest_builds.job_status = 'succeeded'::provisioner_job_status
	AND workspaces.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
	AND NOT workspaces.deleted
),
workspace_latest_presets AS (
	-- For each of the above workspaces, the preset_id of the most recent
	-- successful start transition.
	SELECT DISTINCT ON (latest_prebuilds.id)
		latest_prebuilds.id AS workspace_id,
		workspace_builds.template_version_preset_id AS current_preset_id
	FROM latest_prebuilds
	JOIN workspace_builds ON workspace_builds.workspace_id = latest_prebuilds.id
	WHERE workspace_builds.transition = 'start'::workspace_transition
	AND   workspace_builds.template_version_preset_id IS NOT NULL
	ORDER BY latest_prebuilds.id, workspace_builds.build_number DESC
),
ready_agents AS (
	-- For each of the above workspaces, check if all agents are ready.
	SELECT
		latest_prebuilds.job_id,
		BOOL_AND(workspace_agents.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)::boolean AS ready
	FROM latest_prebuilds
	JOIN workspace_resources ON workspace_resources.job_id = latest_prebuilds.job_id
	JOIN workspace_agents ON workspace_agents.resource_id = workspace_resources.id
	WHERE workspace_agents.deleted = false
	AND workspace_agents.parent_id IS NULL
	GROUP BY latest_prebuilds.job_id
)
SELECT
	latest_prebuilds.id,
	latest_prebuilds.name,
	latest_prebuilds.template_id,
	latest_prebuilds.template_version_id,
	workspace_latest_presets.current_preset_id,
	COALESCE(ready_agents.ready, false)::boolean AS ready,
	latest_prebuilds.created_at
FROM latest_prebuilds
LEFT JOIN ready_agents ON ready_agents.job_id = latest_prebuilds.job_id
LEFT JOIN workspace_latest_presets ON workspace_latest_presets.workspace_id = latest_prebuilds.id
ORDER BY latest_prebuilds.id
`

type GetRunningPrebuiltWorkspacesRow struct {
	ID                uuid.UUID     `db:"id" json:"id"`
	Name              string        `db:"name" json:"name"`
	TemplateID        uuid.UUID     `db:"template_id" json:"template_id"`
	TemplateVersionID uuid.UUID     `db:"template_version_id" json:"template_version_id"`
	CurrentPresetID   uuid.NullUUID `db:"current_preset_id" json:"current_preset_id"`
	Ready             bool          `db:"ready" json:"ready"`
	CreatedAt         time.Time     `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]GetRunningPrebuiltWorkspacesRow, error) {
	rows, err := q.db.QueryContext(ctx, getRunningPrebuiltWorkspaces)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetRunningPrebuiltWorkspacesRow
	for rows.Next() {
		var i GetRunningPrebuiltWorkspacesRow
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.TemplateID,
			&i.TemplateVersionID,
			&i.CurrentPresetID,
			&i.Ready,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplatePresetsWithPrebuilds = `-- name: GetTemplatePresetsWithPrebuilds :many
SELECT
		t.id                        AS template_id,
		t.name                      AS template_name,
		o.id                        AS organization_id,
		o.name                      AS organization_name,
		tv.id                       AS template_version_id,
		tv.name                     AS template_version_name,
		tv.id = t.active_version_id AS using_active_version,
		tvp.id,
		tvp.name,
		tvp.desired_instances       AS desired_instances,
		tvp.scheduling_timezone,
		tvp.invalidate_after_secs   AS ttl,
		tvp.prebuild_status,
		tvp.last_invalidated_at,
		t.deleted,
		t.deprecated != ''          AS deprecated
FROM templates t
		INNER JOIN template_versions tv ON tv.template_id = t.id
		INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id
		INNER JOIN organizations o ON o.id = t.organization_id
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
  -- AND NOT t.deleted -- We don't exclude deleted templates because there's no constraint in the DB preventing a soft deletion on a template while workspaces are running.
	AND (t.id = $1::uuid OR $1 IS NULL)
`

type GetTemplatePresetsWithPrebuildsRow struct {
	TemplateID          uuid.UUID      `db:"template_id" json:"template_id"`
	TemplateName        string         `db:"template_name" json:"template_name"`
	OrganizationID      uuid.UUID      `db:"organization_id" json:"organization_id"`
	OrganizationName    string         `db:"organization_name" json:"organization_name"`
	TemplateVersionID   uuid.UUID      `db:"template_version_id" json:"template_version_id"`
	TemplateVersionName string         `db:"template_version_name" json:"template_version_name"`
	UsingActiveVersion  bool           `db:"using_active_version" json:"using_active_version"`
	ID                  uuid.UUID      `db:"id" json:"id"`
	Name                string         `db:"name" json:"name"`
	DesiredInstances    sql.NullInt32  `db:"desired_instances" json:"desired_instances"`
	SchedulingTimezone  string         `db:"scheduling_timezone" json:"scheduling_timezone"`
	Ttl                 sql.NullInt32  `db:"ttl" json:"ttl"`
	PrebuildStatus      PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
	LastInvalidatedAt   sql.NullTime   `db:"last_invalidated_at" json:"last_invalidated_at"`
	Deleted             bool           `db:"deleted" json:"deleted"`
	Deprecated          bool           `db:"deprecated" json:"deprecated"`
}

// GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds.
// It also returns the number of desired instances for each preset.
// If template_id is specified, only template versions associated with that template will be returned.
func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]GetTemplatePresetsWithPrebuildsRow, error) {
	rows, err := q.db.QueryContext(ctx, getTemplatePresetsWithPrebuilds, templateID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTemplatePresetsWithPrebuildsRow
	for rows.Next() {
		var i GetTemplatePresetsWithPrebuildsRow
		if err := rows.Scan(
			&i.TemplateID,
			&i.TemplateName,
			&i.OrganizationID,
			&i.OrganizationName,
			&i.TemplateVersionID,
			&i.TemplateVersionName,
			&i.UsingActiveVersion,
			&i.ID,
			&i.Name,
			&i.DesiredInstances,
			&i.SchedulingTimezone,
			&i.Ttl,
			&i.PrebuildStatus,
			&i.LastInvalidatedAt,
			&i.Deleted,
			&i.Deprecated,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updatePrebuildProvisionerJobWithCancel = `-- name: UpdatePrebuildProvisionerJobWithCancel :many
WITH jobs_to_cancel AS (
	SELECT pj.id, w.id AS workspace_id, w.template_id, wpb.template_version_preset_id
	FROM provisioner_jobs pj
	INNER JOIN workspace_prebuild_builds wpb ON wpb.job_id = pj.id
	INNER JOIN workspaces w ON w.id = wpb.workspace_id
	INNER JOIN templates t ON t.id = w.template_id
	WHERE
		wpb.template_version_id != t.active_version_id
		AND wpb.template_version_preset_id = $2
		-- Only considers initial builds, i.e. created by the reconciliation loop
		AND wpb.build_number = 1
		-- Only consider 'start' transitions (provisioning), not 'stop'/'delete' (deprovisioning)
		-- Deprovisioning jobs should complete naturally as they're already cleaning up resources
		AND wpb.transition = 'start'::workspace_transition
		-- Pending jobs that have not yet been picked up by a provisioner
		AND pj.job_status = 'pending'::provisioner_job_status
		AND pj.worker_id IS NULL
		AND pj.canceled_at IS NULL
		AND pj.completed_at IS NULL
)
UPDATE provisioner_jobs
SET
	canceled_at = $1::timestamptz,
	completed_at = $1::timestamptz
FROM jobs_to_cancel
WHERE provisioner_jobs.id = jobs_to_cancel.id
RETURNING jobs_to_cancel.id, jobs_to_cancel.workspace_id, jobs_to_cancel.template_id, jobs_to_cancel.template_version_preset_id
`

type UpdatePrebuildProvisionerJobWithCancelParams struct {
	Now      time.Time     `db:"now" json:"now"`
	PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"`
}

type UpdatePrebuildProvisionerJobWithCancelRow struct {
	ID                      uuid.UUID     `db:"id" json:"id"`
	WorkspaceID             uuid.UUID     `db:"workspace_id" json:"workspace_id"`
	TemplateID              uuid.UUID     `db:"template_id" json:"template_id"`
	TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
}

// Cancels all pending provisioner jobs for prebuilt workspaces on a specific preset from an
// inactive template version.
// This is an optimization to clean up stale pending jobs.
func (q *sqlQuerier) UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]UpdatePrebuildProvisionerJobWithCancelRow, error) {
	rows, err := q.db.QueryContext(ctx, updatePrebuildProvisionerJobWithCancel, arg.Now, arg.PresetID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UpdatePrebuildProvisionerJobWithCancelRow
	for rows.Next() {
		var i UpdatePrebuildProvisionerJobWithCancelRow
		if err := rows.Scan(
			&i.ID,
			&i.WorkspaceID,
			&i.TemplateID,
			&i.TemplateVersionPresetID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getActivePresetPrebuildSchedules = `-- name: GetActivePresetPrebuildSchedules :many
SELECT
	tvpps.id, tvpps.preset_id, tvpps.cron_expression, tvpps.desired_instances
FROM
	template_version_preset_prebuild_schedules tvpps
		INNER JOIN template_version_presets tvp ON tvp.id = tvpps.preset_id
		INNER JOIN template_versions tv ON tv.id = tvp.template_version_id
		INNER JOIN templates t ON t.id = tv.template_id
WHERE
	-- Template version is active, and template is not deleted or deprecated
	tv.id = t.active_version_id
	AND NOT t.deleted
	AND t.deprecated = ''
`

func (q *sqlQuerier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]TemplateVersionPresetPrebuildSchedule, error) {
	rows, err := q.db.QueryContext(ctx, getActivePresetPrebuildSchedules)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersionPresetPrebuildSchedule
	for rows.Next() {
		var i TemplateVersionPresetPrebuildSchedule
		if err := rows.Scan(
			&i.ID,
			&i.PresetID,
			&i.CronExpression,
			&i.DesiredInstances,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPresetByID = `-- name: GetPresetByID :one
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tvp.scheduling_timezone, tvp.is_default, tvp.description, tvp.icon, tvp.last_invalidated_at, tv.template_id, tv.organization_id FROM
	template_version_presets tvp
	INNER JOIN template_versions tv ON tvp.template_version_id = tv.id
WHERE tvp.id = $1
`

type GetPresetByIDRow struct {
	ID                  uuid.UUID      `db:"id" json:"id"`
	TemplateVersionID   uuid.UUID      `db:"template_version_id" json:"template_version_id"`
	Name                string         `db:"name" json:"name"`
	CreatedAt           time.Time      `db:"created_at" json:"created_at"`
	DesiredInstances    sql.NullInt32  `db:"desired_instances" json:"desired_instances"`
	InvalidateAfterSecs sql.NullInt32  `db:"invalidate_after_secs" json:"invalidate_after_secs"`
	PrebuildStatus      PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
	SchedulingTimezone  string         `db:"scheduling_timezone" json:"scheduling_timezone"`
	IsDefault           bool           `db:"is_default" json:"is_default"`
	Description         string         `db:"description" json:"description"`
	Icon                string         `db:"icon" json:"icon"`
	LastInvalidatedAt   sql.NullTime   `db:"last_invalidated_at" json:"last_invalidated_at"`
	TemplateID          uuid.NullUUID  `db:"template_id" json:"template_id"`
	OrganizationID      uuid.UUID      `db:"organization_id" json:"organization_id"`
}

func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error) {
	row := q.db.QueryRowContext(ctx, getPresetByID, presetID)
	var i GetPresetByIDRow
	err := row.Scan(
		&i.ID,
		&i.TemplateVersionID,
		&i.Name,
		&i.CreatedAt,
		&i.DesiredInstances,
		&i.InvalidateAfterSecs,
		&i.PrebuildStatus,
		&i.SchedulingTimezone,
		&i.IsDefault,
		&i.Description,
		&i.Icon,
		&i.LastInvalidatedAt,
		&i.TemplateID,
		&i.OrganizationID,
	)
	return i, err
}

const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one
SELECT
	template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status, template_version_presets.scheduling_timezone, template_version_presets.is_default, template_version_presets.description, template_version_presets.icon, template_version_presets.last_invalidated_at
FROM
	template_version_presets
	INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
WHERE
	workspace_builds.id = $1
`

func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error) {
	row := q.db.QueryRowContext(ctx, getPresetByWorkspaceBuildID, workspaceBuildID)
	var i TemplateVersionPreset
	err := row.Scan(
		&i.ID,
		&i.TemplateVersionID,
		&i.Name,
		&i.CreatedAt,
		&i.DesiredInstances,
		&i.InvalidateAfterSecs,
		&i.PrebuildStatus,
		&i.SchedulingTimezone,
		&i.IsDefault,
		&i.Description,
		&i.Icon,
		&i.LastInvalidatedAt,
	)
	return i, err
}

const getPresetParametersByPresetID = `-- name: GetPresetParametersByPresetID :many
SELECT
	tvpp.id, tvpp.template_version_preset_id, tvpp.name, tvpp.value
FROM
	template_version_preset_parameters tvpp
WHERE
	tvpp.template_version_preset_id = $1
`

func (q *sqlQuerier) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error) {
	rows, err := q.db.QueryContext(ctx, getPresetParametersByPresetID, presetID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersionPresetParameter
	for rows.Next() {
		var i TemplateVersionPresetParameter
		if err := rows.Scan(
			&i.ID,
			&i.TemplateVersionPresetID,
			&i.Name,
			&i.Value,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPresetParametersByTemplateVersionID = `-- name: GetPresetParametersByTemplateVersionID :many
SELECT
	template_version_preset_parameters.id, template_version_preset_parameters.template_version_preset_id, template_version_preset_parameters.name, template_version_preset_parameters.value
FROM
	template_version_preset_parameters
	INNER JOIN template_version_presets ON template_version_preset_parameters.template_version_preset_id = template_version_presets.id
WHERE
	template_version_presets.template_version_id = $1
`

func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error) {
	rows, err := q.db.QueryContext(ctx, getPresetParametersByTemplateVersionID, templateVersionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersionPresetParameter
	for rows.Next() {
		var i TemplateVersionPresetParameter
		if err := rows.Scan(
			&i.ID,
			&i.TemplateVersionPresetID,
			&i.Name,
			&i.Value,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
SELECT
	id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
FROM
	template_version_presets
WHERE
	template_version_id = $1
`

func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPreset, error) {
	rows, err := q.db.QueryContext(ctx, getPresetsByTemplateVersionID, templateVersionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersionPreset
	for rows.Next() {
		var i TemplateVersionPreset
		if err := rows.Scan(
			&i.ID,
			&i.TemplateVersionID,
			&i.Name,
			&i.CreatedAt,
			&i.DesiredInstances,
			&i.InvalidateAfterSecs,
			&i.PrebuildStatus,
			&i.SchedulingTimezone,
			&i.IsDefault,
			&i.Description,
			&i.Icon,
			&i.LastInvalidatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertPreset = `-- name: InsertPreset :one
INSERT INTO template_version_presets (
	id,
	template_version_id,
	name,
	created_at,
	desired_instances,
	invalidate_after_secs,
	scheduling_timezone,
	is_default,
	description,
	icon,
	last_invalidated_at
)
VALUES (
	$1,
	$2,
	$3,
	$4,
	$5,
	$6,
	$7,
	$8,
	$9,
	$10,
	$11
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
`

type InsertPresetParams struct {
	ID                  uuid.UUID     `db:"id" json:"id"`
	TemplateVersionID   uuid.UUID     `db:"template_version_id" json:"template_version_id"`
	Name                string        `db:"name" json:"name"`
	CreatedAt           time.Time     `db:"created_at" json:"created_at"`
	DesiredInstances    sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
	InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
	SchedulingTimezone  string        `db:"scheduling_timezone" json:"scheduling_timezone"`
	IsDefault           bool          `db:"is_default" json:"is_default"`
	Description         string        `db:"description" json:"description"`
	Icon                string        `db:"icon" json:"icon"`
	LastInvalidatedAt   sql.NullTime  `db:"last_invalidated_at" json:"last_invalidated_at"`
}

func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) {
	row := q.db.QueryRowContext(ctx, insertPreset,
		arg.ID,
		arg.TemplateVersionID,
		arg.Name,
		arg.CreatedAt,
		arg.DesiredInstances,
		arg.InvalidateAfterSecs,
		arg.SchedulingTimezone,
		arg.IsDefault,
		arg.Description,
		arg.Icon,
		arg.LastInvalidatedAt,
	)
	var i TemplateVersionPreset
	err := row.Scan(
		&i.ID,
		&i.TemplateVersionID,
		&i.Name,
		&i.CreatedAt,
		&i.DesiredInstances,
		&i.InvalidateAfterSecs,
		&i.PrebuildStatus,
		&i.SchedulingTimezone,
		&i.IsDefault,
		&i.Description,
		&i.Icon,
		&i.LastInvalidatedAt,
	)
	return i, err
}

const insertPresetParameters = `-- name: InsertPresetParameters :many
INSERT INTO
	template_version_preset_parameters (template_version_preset_id, name, value)
SELECT
	$1,
	unnest($2 :: TEXT[]),
	unnest($3 :: TEXT[])
RETURNING id, template_version_preset_id, name, value
`

type InsertPresetParametersParams struct {
	TemplateVersionPresetID uuid.UUID `db:"template_version_preset_id" json:"template_version_preset_id"`
	Names                   []string  `db:"names" json:"names"`
	Values                  []string  `db:"values" json:"values"`
}

func (q *sqlQuerier) InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error) {
	rows, err := q.db.QueryContext(ctx, insertPresetParameters, arg.TemplateVersionPresetID, pq.Array(arg.Names), pq.Array(arg.Values))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersionPresetParameter
	for rows.Next() {
		var i TemplateVersionPresetParameter
		if err := rows.Scan(
			&i.ID,
			&i.TemplateVersionPresetID,
			&i.Name,
			&i.Value,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertPresetPrebuildSchedule = `-- name: InsertPresetPrebuildSchedule :one
INSERT INTO template_version_preset_prebuild_schedules (
	preset_id,
	cron_expression,
	desired_instances
)
VALUES (
	$1,
	$2,
	$3
) RETURNING id, preset_id, cron_expression, desired_instances
`

type InsertPresetPrebuildScheduleParams struct {
	PresetID         uuid.UUID `db:"preset_id" json:"preset_id"`
	CronExpression   string    `db:"cron_expression" json:"cron_expression"`
	DesiredInstances int32     `db:"desired_instances" json:"desired_instances"`
}

func (q *sqlQuerier) InsertPresetPrebuildSchedule(ctx context.Context, arg InsertPresetPrebuildScheduleParams) (TemplateVersionPresetPrebuildSchedule, error) {
	row := q.db.QueryRowContext(ctx, insertPresetPrebuildSchedule, arg.PresetID, arg.CronExpression, arg.DesiredInstances)
	var i TemplateVersionPresetPrebuildSchedule
	err := row.Scan(
		&i.ID,
		&i.PresetID,
		&i.CronExpression,
		&i.DesiredInstances,
	)
	return i, err
}

const updatePresetPrebuildStatus = `-- name: UpdatePresetPrebuildStatus :exec
UPDATE template_version_presets
SET prebuild_status = $1
WHERE id = $2
`

type UpdatePresetPrebuildStatusParams struct {
	Status   PrebuildStatus `db:"status" json:"status"`
	PresetID uuid.UUID      `db:"preset_id" json:"preset_id"`
}

func (q *sqlQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg UpdatePresetPrebuildStatusParams) error {
	_, err := q.db.ExecContext(ctx, updatePresetPrebuildStatus, arg.Status, arg.PresetID)
	return err
}

const updatePresetsLastInvalidatedAt = `-- name: UpdatePresetsLastInvalidatedAt :many
UPDATE
	template_version_presets tvp
SET
	last_invalidated_at = $1
FROM
	templates t
	JOIN template_versions tv ON tv.id = t.active_version_id
WHERE
	t.id = $2
	AND tvp.template_version_id = tv.id
RETURNING
	t.name AS template_name,
	tv.name AS template_version_name,
	tvp.name AS template_version_preset_name
`

type UpdatePresetsLastInvalidatedAtParams struct {
	LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
	TemplateID        uuid.UUID    `db:"template_id" json:"template_id"`
}

type UpdatePresetsLastInvalidatedAtRow struct {
	TemplateName              string `db:"template_name" json:"template_name"`
	TemplateVersionName       string `db:"template_version_name" json:"template_version_name"`
	TemplateVersionPresetName string `db:"template_version_preset_name" json:"template_version_preset_name"`
}

func (q *sqlQuerier) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg UpdatePresetsLastInvalidatedAtParams) ([]UpdatePresetsLastInvalidatedAtRow, error) {
	rows, err := q.db.QueryContext(ctx, updatePresetsLastInvalidatedAt, arg.LastInvalidatedAt, arg.TemplateID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UpdatePresetsLastInvalidatedAtRow
	for rows.Next() {
		var i UpdatePresetsLastInvalidatedAtRow
		if err := rows.Scan(&i.TemplateName, &i.TemplateVersionName, &i.TemplateVersionPresetName); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteOldProvisionerDaemons = `-- name: DeleteOldProvisionerDaemons :exec
DELETE FROM provisioner_daemons WHERE (
	(created_at < (NOW() - INTERVAL '7 days') AND last_seen_at IS NULL) OR
	(last_seen_at IS NOT NULL AND last_seen_at < (NOW() - INTERVAL '7 days'))
)
`

// Delete provisioner daemons that have been created at least a week ago
// and have not connected to coderd since a week.
// A provisioner daemon with "zeroed" last_seen_at column indicates possible
// connectivity issues (no provisioner daemon activity since registration).
func (q *sqlQuerier) DeleteOldProvisionerDaemons(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, deleteOldProvisionerDaemons)
	return err
}

const getEligibleProvisionerDaemonsByProvisionerJobIDs = `-- name: GetEligibleProvisionerDaemonsByProvisionerJobIDs :many
SELECT DISTINCT
    provisioner_jobs.id as job_id, provisioner_daemons.id, provisioner_daemons.created_at, provisioner_daemons.name, provisioner_daemons.provisioners, provisioner_daemons.replica_id, provisioner_daemons.tags, provisioner_daemons.last_seen_at, provisioner_daemons.version, provisioner_daemons.api_version, provisioner_daemons.organization_id, provisioner_daemons.key_id
FROM
    provisioner_jobs
JOIN
    provisioner_daemons ON provisioner_daemons.organization_id = provisioner_jobs.organization_id
    AND provisioner_tagset_contains(provisioner_daemons.tags::tagset, provisioner_jobs.tags::tagset)
    AND provisioner_jobs.provisioner = ANY(provisioner_daemons.provisioners)
WHERE
    provisioner_jobs.id = ANY($1 :: uuid[])
`

type GetEligibleProvisionerDaemonsByProvisionerJobIDsRow struct {
	JobID             uuid.UUID         `db:"job_id" json:"job_id"`
	ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"`
}

func (q *sqlQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
	rows, err := q.db.QueryContext(ctx, getEligibleProvisionerDaemonsByProvisionerJobIDs, pq.Array(provisionerJobIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
	for rows.Next() {
		var i GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
		if err := rows.Scan(
			&i.JobID,
			&i.ProvisionerDaemon.ID,
			&i.ProvisionerDaemon.CreatedAt,
			&i.ProvisionerDaemon.Name,
			pq.Array(&i.ProvisionerDaemon.Provisioners),
			&i.ProvisionerDaemon.ReplicaID,
			&i.ProvisionerDaemon.Tags,
			&i.ProvisionerDaemon.LastSeenAt,
			&i.ProvisionerDaemon.Version,
			&i.ProvisionerDaemon.APIVersion,
			&i.ProvisionerDaemon.OrganizationID,
			&i.ProvisionerDaemon.KeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many
SELECT
	id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
FROM
	provisioner_daemons
`

func (q *sqlQuerier) GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerDaemons)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerDaemon
	for rows.Next() {
		var i ProvisionerDaemon
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.Name,
			pq.Array(&i.Provisioners),
			&i.ReplicaID,
			&i.Tags,
			&i.LastSeenAt,
			&i.Version,
			&i.APIVersion,
			&i.OrganizationID,
			&i.KeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getProvisionerDaemonsByOrganization = `-- name: GetProvisionerDaemonsByOrganization :many
SELECT
	id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
FROM
	provisioner_daemons
WHERE
	-- This is the original search criteria:
	organization_id = $1 :: uuid
	AND
	-- adding support for searching by tags:
	($2 :: tagset = 'null' :: tagset OR provisioner_tagset_contains(provisioner_daemons.tags::tagset, $2::tagset))
`

type GetProvisionerDaemonsByOrganizationParams struct {
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	WantTags       StringMap `db:"want_tags" json:"want_tags"`
}

func (q *sqlQuerier) GetProvisionerDaemonsByOrganization(ctx context.Context, arg GetProvisionerDaemonsByOrganizationParams) ([]ProvisionerDaemon, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsByOrganization, arg.OrganizationID, arg.WantTags)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerDaemon
	for rows.Next() {
		var i ProvisionerDaemon
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.Name,
			pq.Array(&i.Provisioners),
			&i.ReplicaID,
			&i.Tags,
			&i.LastSeenAt,
			&i.Version,
			&i.APIVersion,
			&i.OrganizationID,
			&i.KeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getProvisionerDaemonsWithStatusByOrganization = `-- name: GetProvisionerDaemonsWithStatusByOrganization :many
SELECT
	pd.id, pd.created_at, pd.name, pd.provisioners, pd.replica_id, pd.tags, pd.last_seen_at, pd.version, pd.api_version, pd.organization_id, pd.key_id,
	CASE
		WHEN current_job.id IS NOT NULL THEN 'busy'::provisioner_daemon_status
		WHEN (COALESCE($1::bool, false) = true
		      OR 'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
		     AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
		THEN 'offline'::provisioner_daemon_status
		ELSE 'idle'::provisioner_daemon_status
	END AS status,
	pk.name AS key_name,
	-- NOTE(mafredri): sqlc.embed doesn't support nullable tables nor renaming them.
	current_job.id AS current_job_id,
	current_job.job_status AS current_job_status,
	previous_job.id AS previous_job_id,
	previous_job.job_status AS previous_job_status,
	COALESCE(current_template.name, ''::text) AS current_job_template_name,
	COALESCE(current_template.display_name, ''::text) AS current_job_template_display_name,
	COALESCE(current_template.icon, ''::text) AS current_job_template_icon,
	COALESCE(previous_template.name, ''::text) AS previous_job_template_name,
	COALESCE(previous_template.display_name, ''::text) AS previous_job_template_display_name,
	COALESCE(previous_template.icon, ''::text) AS previous_job_template_icon
FROM
	provisioner_daemons pd
JOIN
	provisioner_keys pk ON pk.id = pd.key_id
LEFT JOIN
	provisioner_jobs current_job ON (
		current_job.worker_id = pd.id
		AND current_job.organization_id = pd.organization_id
		AND current_job.completed_at IS NULL
	)
LEFT JOIN
	provisioner_jobs previous_job ON (
		previous_job.id = (
			SELECT
				id
			FROM
				provisioner_jobs
			WHERE
				worker_id = pd.id
				AND organization_id = pd.organization_id
				AND completed_at IS NOT NULL
			ORDER BY
				completed_at DESC
			LIMIT 1
		)
		AND previous_job.organization_id = pd.organization_id
	)
LEFT JOIN
	workspace_builds current_build ON current_build.id = CASE WHEN current_job.input ? 'workspace_build_id' THEN (current_job.input->>'workspace_build_id')::uuid END
LEFT JOIN
	-- We should always have a template version, either explicitly or implicitly via workspace build.
	template_versions current_version ON (
		current_version.id = CASE WHEN current_job.input ? 'template_version_id' THEN (current_job.input->>'template_version_id')::uuid ELSE current_build.template_version_id END
		AND current_version.organization_id = pd.organization_id
	)
LEFT JOIN
	templates current_template ON (
		current_template.id = current_version.template_id
		AND current_template.organization_id = pd.organization_id
	)
LEFT JOIN
	workspace_builds previous_build ON previous_build.id = CASE WHEN previous_job.input ? 'workspace_build_id' THEN (previous_job.input->>'workspace_build_id')::uuid END
LEFT JOIN
	-- We should always have a template version, either explicitly or implicitly via workspace build.
	template_versions previous_version ON (
		previous_version.id = CASE WHEN previous_job.input ? 'template_version_id' THEN (previous_job.input->>'template_version_id')::uuid ELSE previous_build.template_version_id END
		AND previous_version.organization_id = pd.organization_id
	)
LEFT JOIN
	templates previous_template ON (
		previous_template.id = previous_version.template_id
		AND previous_template.organization_id = pd.organization_id
	)
WHERE
	pd.organization_id = $4::uuid
	AND (COALESCE(array_length($5::uuid[], 1), 0) = 0 OR pd.id = ANY($5::uuid[]))
	AND ($6::tagset = 'null'::tagset OR provisioner_tagset_contains(pd.tags::tagset, $6::tagset))
	-- Filter by max age if provided
	AND (
		$7::bigint IS NULL
		OR pd.last_seen_at IS NULL
		OR pd.last_seen_at >= (NOW() - ($7::bigint || ' ms')::interval)
	)
	AND (
		-- Always include online daemons
		(pd.last_seen_at IS NOT NULL AND pd.last_seen_at >= (NOW() - ($3::bigint || ' ms')::interval))
		-- Include offline daemons if offline param is true or 'offline' status is requested
		OR (
			(pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
			AND (
				COALESCE($1::bool, false) = true
				OR 'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[])
			)
		)
	)
	AND (
		-- Filter daemons by any statuses if provided
		COALESCE(array_length($2::provisioner_daemon_status[], 1), 0) = 0
		OR (current_job.id IS NOT NULL AND 'busy'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
		OR (current_job.id IS NULL AND 'idle'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[]))
		OR (
		    'offline'::provisioner_daemon_status = ANY($2::provisioner_daemon_status[])
		    AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
		)
		OR (
		    COALESCE($1::bool, false) = true
		    AND (pd.last_seen_at IS NULL OR pd.last_seen_at < (NOW() - ($3::bigint || ' ms')::interval))
		)
	)
ORDER BY
	pd.created_at DESC
LIMIT
	$8::int
`

type GetProvisionerDaemonsWithStatusByOrganizationParams struct {
	Offline         sql.NullBool              `db:"offline" json:"offline"`
	Statuses        []ProvisionerDaemonStatus `db:"statuses" json:"statuses"`
	StaleIntervalMS int64                     `db:"stale_interval_ms" json:"stale_interval_ms"`
	OrganizationID  uuid.UUID                 `db:"organization_id" json:"organization_id"`
	IDs             []uuid.UUID               `db:"ids" json:"ids"`
	Tags            StringMap                 `db:"tags" json:"tags"`
	MaxAgeMs        sql.NullInt64             `db:"max_age_ms" json:"max_age_ms"`
	Limit           sql.NullInt32             `db:"limit" json:"limit"`
}

type GetProvisionerDaemonsWithStatusByOrganizationRow struct {
	ProvisionerDaemon              ProvisionerDaemon        `db:"provisioner_daemon" json:"provisioner_daemon"`
	Status                         ProvisionerDaemonStatus  `db:"status" json:"status"`
	KeyName                        string                   `db:"key_name" json:"key_name"`
	CurrentJobID                   uuid.NullUUID            `db:"current_job_id" json:"current_job_id"`
	CurrentJobStatus               NullProvisionerJobStatus `db:"current_job_status" json:"current_job_status"`
	PreviousJobID                  uuid.NullUUID            `db:"previous_job_id" json:"previous_job_id"`
	PreviousJobStatus              NullProvisionerJobStatus `db:"previous_job_status" json:"previous_job_status"`
	CurrentJobTemplateName         string                   `db:"current_job_template_name" json:"current_job_template_name"`
	CurrentJobTemplateDisplayName  string                   `db:"current_job_template_display_name" json:"current_job_template_display_name"`
	CurrentJobTemplateIcon         string                   `db:"current_job_template_icon" json:"current_job_template_icon"`
	PreviousJobTemplateName        string                   `db:"previous_job_template_name" json:"previous_job_template_name"`
	PreviousJobTemplateDisplayName string                   `db:"previous_job_template_display_name" json:"previous_job_template_display_name"`
	PreviousJobTemplateIcon        string                   `db:"previous_job_template_icon" json:"previous_job_template_icon"`
}

// Current job information.
// Previous job information.
func (q *sqlQuerier) GetProvisionerDaemonsWithStatusByOrganization(ctx context.Context, arg GetProvisionerDaemonsWithStatusByOrganizationParams) ([]GetProvisionerDaemonsWithStatusByOrganizationRow, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerDaemonsWithStatusByOrganization,
		arg.Offline,
		pq.Array(arg.Statuses),
		arg.StaleIntervalMS,
		arg.OrganizationID,
		pq.Array(arg.IDs),
		arg.Tags,
		arg.MaxAgeMs,
		arg.Limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetProvisionerDaemonsWithStatusByOrganizationRow
	for rows.Next() {
		var i GetProvisionerDaemonsWithStatusByOrganizationRow
		if err := rows.Scan(
			&i.ProvisionerDaemon.ID,
			&i.ProvisionerDaemon.CreatedAt,
			&i.ProvisionerDaemon.Name,
			pq.Array(&i.ProvisionerDaemon.Provisioners),
			&i.ProvisionerDaemon.ReplicaID,
			&i.ProvisionerDaemon.Tags,
			&i.ProvisionerDaemon.LastSeenAt,
			&i.ProvisionerDaemon.Version,
			&i.ProvisionerDaemon.APIVersion,
			&i.ProvisionerDaemon.OrganizationID,
			&i.ProvisionerDaemon.KeyID,
			&i.Status,
			&i.KeyName,
			&i.CurrentJobID,
			&i.CurrentJobStatus,
			&i.PreviousJobID,
			&i.PreviousJobStatus,
			&i.CurrentJobTemplateName,
			&i.CurrentJobTemplateDisplayName,
			&i.CurrentJobTemplateIcon,
			&i.PreviousJobTemplateName,
			&i.PreviousJobTemplateDisplayName,
			&i.PreviousJobTemplateIcon,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateProvisionerDaemonLastSeenAt = `-- name: UpdateProvisionerDaemonLastSeenAt :exec
UPDATE provisioner_daemons
SET
	last_seen_at = $1
WHERE
	id = $2
AND
	last_seen_at <= $1
`

type UpdateProvisionerDaemonLastSeenAtParams struct {
	LastSeenAt sql.NullTime `db:"last_seen_at" json:"last_seen_at"`
	ID         uuid.UUID    `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error {
	_, err := q.db.ExecContext(ctx, updateProvisionerDaemonLastSeenAt, arg.LastSeenAt, arg.ID)
	return err
}

const upsertProvisionerDaemon = `-- name: UpsertProvisionerDaemon :one
INSERT INTO
	provisioner_daemons (
		id,
		created_at,
		"name",
		provisioners,
		tags,
		last_seen_at,
		"version",
		organization_id,
		api_version,
		key_id
	)
VALUES (
	gen_random_uuid(),
	$1,
	$2,
	$3,
	$4,
	$5,
	$6,
	$7,
	$8,
	$9
) ON CONFLICT("organization_id", "name", LOWER(COALESCE(tags ->> 'owner'::text, ''::text))) DO UPDATE SET
	provisioners = $3,
	tags = $4,
	last_seen_at = $5,
	"version" = $6,
	api_version = $8,
	organization_id = $7,
	key_id = $9
RETURNING id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
`

type UpsertProvisionerDaemonParams struct {
	CreatedAt      time.Time         `db:"created_at" json:"created_at"`
	Name           string            `db:"name" json:"name"`
	Provisioners   []ProvisionerType `db:"provisioners" json:"provisioners"`
	Tags           StringMap         `db:"tags" json:"tags"`
	LastSeenAt     sql.NullTime      `db:"last_seen_at" json:"last_seen_at"`
	Version        string            `db:"version" json:"version"`
	OrganizationID uuid.UUID         `db:"organization_id" json:"organization_id"`
	APIVersion     string            `db:"api_version" json:"api_version"`
	KeyID          uuid.UUID         `db:"key_id" json:"key_id"`
}

func (q *sqlQuerier) UpsertProvisionerDaemon(ctx context.Context, arg UpsertProvisionerDaemonParams) (ProvisionerDaemon, error) {
	row := q.db.QueryRowContext(ctx, upsertProvisionerDaemon,
		arg.CreatedAt,
		arg.Name,
		pq.Array(arg.Provisioners),
		arg.Tags,
		arg.LastSeenAt,
		arg.Version,
		arg.OrganizationID,
		arg.APIVersion,
		arg.KeyID,
	)
	var i ProvisionerDaemon
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.Name,
		pq.Array(&i.Provisioners),
		&i.ReplicaID,
		&i.Tags,
		&i.LastSeenAt,
		&i.Version,
		&i.APIVersion,
		&i.OrganizationID,
		&i.KeyID,
	)
	return i, err
}

const getProvisionerLogsAfterID = `-- name: GetProvisionerLogsAfterID :many
SELECT
	job_id, created_at, source, level, stage, output, id
FROM
	provisioner_job_logs
WHERE
	job_id = $1
	AND (
		id > $2
	) ORDER BY id ASC
`

type GetProvisionerLogsAfterIDParams struct {
	JobID        uuid.UUID `db:"job_id" json:"job_id"`
	CreatedAfter int64     `db:"created_after" json:"created_after"`
}

func (q *sqlQuerier) GetProvisionerLogsAfterID(ctx context.Context, arg GetProvisionerLogsAfterIDParams) ([]ProvisionerJobLog, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerLogsAfterID, arg.JobID, arg.CreatedAfter)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerJobLog
	for rows.Next() {
		var i ProvisionerJobLog
		if err := rows.Scan(
			&i.JobID,
			&i.CreatedAt,
			&i.Source,
			&i.Level,
			&i.Stage,
			&i.Output,
			&i.ID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertProvisionerJobLogs = `-- name: InsertProvisionerJobLogs :many
INSERT INTO
	provisioner_job_logs
SELECT
	$1 :: uuid AS job_id,
	unnest($2 :: timestamptz [ ]) AS created_at,
	unnest($3 :: log_source [ ]) AS source,
	unnest($4 :: log_level [ ]) AS LEVEL,
	unnest($5 :: VARCHAR(128) [ ]) AS stage,
	unnest($6 :: VARCHAR(1024) [ ]) AS output RETURNING job_id, created_at, source, level, stage, output, id
`

type InsertProvisionerJobLogsParams struct {
	JobID     uuid.UUID   `db:"job_id" json:"job_id"`
	CreatedAt []time.Time `db:"created_at" json:"created_at"`
	Source    []LogSource `db:"source" json:"source"`
	Level     []LogLevel  `db:"level" json:"level"`
	Stage     []string    `db:"stage" json:"stage"`
	Output    []string    `db:"output" json:"output"`
}

func (q *sqlQuerier) InsertProvisionerJobLogs(ctx context.Context, arg InsertProvisionerJobLogsParams) ([]ProvisionerJobLog, error) {
	rows, err := q.db.QueryContext(ctx, insertProvisionerJobLogs,
		arg.JobID,
		pq.Array(arg.CreatedAt),
		pq.Array(arg.Source),
		pq.Array(arg.Level),
		pq.Array(arg.Stage),
		pq.Array(arg.Output),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerJobLog
	for rows.Next() {
		var i ProvisionerJobLog
		if err := rows.Scan(
			&i.JobID,
			&i.CreatedAt,
			&i.Source,
			&i.Level,
			&i.Stage,
			&i.Output,
			&i.ID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateProvisionerJobLogsLength = `-- name: UpdateProvisionerJobLogsLength :exec
UPDATE
	provisioner_jobs
SET
	logs_length = logs_length + $2
WHERE
	id = $1
`

type UpdateProvisionerJobLogsLengthParams struct {
	ID         uuid.UUID `db:"id" json:"id"`
	LogsLength int32     `db:"logs_length" json:"logs_length"`
}

func (q *sqlQuerier) UpdateProvisionerJobLogsLength(ctx context.Context, arg UpdateProvisionerJobLogsLengthParams) error {
	_, err := q.db.ExecContext(ctx, updateProvisionerJobLogsLength, arg.ID, arg.LogsLength)
	return err
}

const updateProvisionerJobLogsOverflowed = `-- name: UpdateProvisionerJobLogsOverflowed :exec
UPDATE
	provisioner_jobs
SET
	logs_overflowed = $2
WHERE
	id = $1
`

type UpdateProvisionerJobLogsOverflowedParams struct {
	ID             uuid.UUID `db:"id" json:"id"`
	LogsOverflowed bool      `db:"logs_overflowed" json:"logs_overflowed"`
}

func (q *sqlQuerier) UpdateProvisionerJobLogsOverflowed(ctx context.Context, arg UpdateProvisionerJobLogsOverflowedParams) error {
	_, err := q.db.ExecContext(ctx, updateProvisionerJobLogsOverflowed, arg.ID, arg.LogsOverflowed)
	return err
}

const acquireProvisionerJob = `-- name: AcquireProvisionerJob :one
UPDATE
	provisioner_jobs
SET
	started_at = $1,
	updated_at = $1,
	worker_id = $2
WHERE
	id = (
		SELECT
			id
		FROM
			provisioner_jobs AS potential_job
		WHERE
			potential_job.started_at IS NULL
			AND potential_job.completed_at IS NULL
			AND potential_job.organization_id = $3
			-- Ensure the caller has the correct provisioner.
			AND potential_job.provisioner = ANY($4 :: provisioner_type [ ])
			-- elsewhere, we use the tagset type, but here we use jsonb for backward compatibility
			-- they are aliases and the code that calls this query already relies on a different type
			AND provisioner_tagset_contains($5 :: jsonb, potential_job.tags :: jsonb)
		ORDER BY
			-- Ensure that human-initiated jobs are prioritized over prebuilds.
			potential_job.initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC,
			potential_job.created_at ASC
		FOR UPDATE
		SKIP LOCKED
		LIMIT
			1
	) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
`

type AcquireProvisionerJobParams struct {
	StartedAt       sql.NullTime      `db:"started_at" json:"started_at"`
	WorkerID        uuid.NullUUID     `db:"worker_id" json:"worker_id"`
	OrganizationID  uuid.UUID         `db:"organization_id" json:"organization_id"`
	Types           []ProvisionerType `db:"types" json:"types"`
	ProvisionerTags json.RawMessage   `db:"provisioner_tags" json:"provisioner_tags"`
}

// Acquires the lock for a single job that isn't started, completed,
// canceled, and that matches an array of provisioner types.
//
// SKIP LOCKED is used to jump over locked rows. This prevents
// multiple provisioners from acquiring the same jobs. See:
// https://www.postgresql.org/docs/9.5/sql-select.html#SQL-FOR-UPDATE-SHARE
func (q *sqlQuerier) AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error) {
	row := q.db.QueryRowContext(ctx, acquireProvisionerJob,
		arg.StartedAt,
		arg.WorkerID,
		arg.OrganizationID,
		pq.Array(arg.Types),
		arg.ProvisionerTags,
	)
	var i ProvisionerJob
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.StartedAt,
		&i.CanceledAt,
		&i.CompletedAt,
		&i.Error,
		&i.OrganizationID,
		&i.InitiatorID,
		&i.Provisioner,
		&i.StorageMethod,
		&i.Type,
		&i.Input,
		&i.WorkerID,
		&i.FileID,
		&i.Tags,
		&i.ErrorCode,
		&i.TraceMetadata,
		&i.JobStatus,
		&i.LogsLength,
		&i.LogsOverflowed,
	)
	return i, err
}

const getProvisionerJobByID = `-- name: GetProvisionerJobByID :one
SELECT
	id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
FROM
	provisioner_jobs
WHERE
	id = $1
`

func (q *sqlQuerier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
	row := q.db.QueryRowContext(ctx, getProvisionerJobByID, id)
	var i ProvisionerJob
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.StartedAt,
		&i.CanceledAt,
		&i.CompletedAt,
		&i.Error,
		&i.OrganizationID,
		&i.InitiatorID,
		&i.Provisioner,
		&i.StorageMethod,
		&i.Type,
		&i.Input,
		&i.WorkerID,
		&i.FileID,
		&i.Tags,
		&i.ErrorCode,
		&i.TraceMetadata,
		&i.JobStatus,
		&i.LogsLength,
		&i.LogsOverflowed,
	)
	return i, err
}

const getProvisionerJobByIDForUpdate = `-- name: GetProvisionerJobByIDForUpdate :one
SELECT
	id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
FROM
	provisioner_jobs
WHERE
	id = $1
FOR UPDATE
SKIP LOCKED
`

// Gets a single provisioner job by ID for update.
// This is used to securely reap jobs that have been hung/pending for a long time.
func (q *sqlQuerier) GetProvisionerJobByIDForUpdate(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
	row := q.db.QueryRowContext(ctx, getProvisionerJobByIDForUpdate, id)
	var i ProvisionerJob
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.StartedAt,
		&i.CanceledAt,
		&i.CompletedAt,
		&i.Error,
		&i.OrganizationID,
		&i.InitiatorID,
		&i.Provisioner,
		&i.StorageMethod,
		&i.Type,
		&i.Input,
		&i.WorkerID,
		&i.FileID,
		&i.Tags,
		&i.ErrorCode,
		&i.TraceMetadata,
		&i.JobStatus,
		&i.LogsLength,
		&i.LogsOverflowed,
	)
	return i, err
}

const getProvisionerJobByIDWithLock = `-- name: GetProvisionerJobByIDWithLock :one
SELECT
	id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
FROM
	provisioner_jobs
WHERE
	id = $1
FOR UPDATE
`

// Gets a provisioner job by ID with exclusive lock.
// Blocks until the row is available for update.
func (q *sqlQuerier) GetProvisionerJobByIDWithLock(ctx context.Context, id uuid.UUID) (ProvisionerJob, error) {
	row := q.db.QueryRowContext(ctx, getProvisionerJobByIDWithLock, id)
	var i ProvisionerJob
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.StartedAt,
		&i.CanceledAt,
		&i.CompletedAt,
		&i.Error,
		&i.OrganizationID,
		&i.InitiatorID,
		&i.Provisioner,
		&i.StorageMethod,
		&i.Type,
		&i.Input,
		&i.WorkerID,
		&i.FileID,
		&i.Tags,
		&i.ErrorCode,
		&i.TraceMetadata,
		&i.JobStatus,
		&i.LogsLength,
		&i.LogsOverflowed,
	)
	return i, err
}

const getProvisionerJobTimingsByJobID = `-- name: GetProvisionerJobTimingsByJobID :many
SELECT job_id, started_at, ended_at, stage, source, action, resource FROM provisioner_job_timings
WHERE job_id = $1
ORDER BY started_at ASC
`

func (q *sqlQuerier) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]ProvisionerJobTiming, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerJobTimingsByJobID, jobID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerJobTiming
	for rows.Next() {
		var i ProvisionerJobTiming
		if err := rows.Scan(
			&i.JobID,
			&i.StartedAt,
			&i.EndedAt,
			&i.Stage,
			&i.Source,
			&i.Action,
			&i.Resource,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getProvisionerJobsByIDsWithQueuePosition = `-- name: GetProvisionerJobsByIDsWithQueuePosition :many
WITH filtered_provisioner_jobs AS (
	-- Step 1: Filter provisioner_jobs
	SELECT
		id, created_at, tags
	FROM
		provisioner_jobs
	WHERE
		id = ANY($1 :: uuid [ ]) -- Apply filter early to reduce dataset size before expensive JOIN
),
pending_jobs AS (
	-- Step 2: Extract only pending jobs
	SELECT
		id, initiator_id, created_at, tags
	FROM
		provisioner_jobs
	WHERE
		job_status = 'pending'
),
unique_daemon_tags AS (
	SELECT DISTINCT tags FROM provisioner_daemons pd
	WHERE pd.last_seen_at IS NOT NULL
	  AND pd.last_seen_at >= (NOW() - ($2::bigint || ' ms')::interval)
),
relevant_daemon_tags AS (
	SELECT udt.tags
	FROM unique_daemon_tags udt
	WHERE EXISTS (
		SELECT 1 FROM filtered_provisioner_jobs fpj
		WHERE provisioner_tagset_contains(udt.tags, fpj.tags)
	)
),
ranked_jobs AS (
	-- Step 3: Rank only pending jobs based on provisioner availability
	SELECT
		pj.id,
		pj.created_at,
		ROW_NUMBER() OVER (PARTITION BY rdt.tags ORDER BY pj.initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC, pj.created_at ASC) AS queue_position,
		COUNT(*) OVER (PARTITION BY rdt.tags) AS queue_size
	FROM
		pending_jobs pj
	INNER JOIN
		relevant_daemon_tags rdt
	ON
		provisioner_tagset_contains(rdt.tags, pj.tags)
),
final_jobs AS (
	-- Step 4: Compute best queue position and max queue size per job
	SELECT
		fpj.id,
		fpj.created_at,
		COALESCE(MIN(rj.queue_position), 0) :: BIGINT AS queue_position, -- Best queue position across provisioners
		COALESCE(MAX(rj.queue_size), 0) :: BIGINT AS queue_size -- Max queue size across provisioners
	FROM
		filtered_provisioner_jobs fpj -- Use the pre-filtered dataset instead of full provisioner_jobs
			LEFT JOIN ranked_jobs rj
					ON fpj.id = rj.id -- Join with the ranking jobs CTE to assign a rank to each specified provisioner job.
	GROUP BY
		fpj.id, fpj.created_at
)
SELECT
	-- Step 5: Final SELECT with INNER JOIN provisioner_jobs
	fj.id,
	fj.created_at,
	pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status, pj.logs_length, pj.logs_overflowed,
	fj.queue_position,
	fj.queue_size
FROM
	final_jobs fj
		INNER JOIN provisioner_jobs pj
				ON fj.id = pj.id -- Ensure we retrieve full details from ` + "`" + `provisioner_jobs` + "`" + `.
                                 -- JOIN with pj is required for sqlc.embed(pj) to compile successfully.
ORDER BY
	fj.created_at
`

type GetProvisionerJobsByIDsWithQueuePositionParams struct {
	IDs             []uuid.UUID `db:"ids" json:"ids"`
	StaleIntervalMS int64       `db:"stale_interval_ms" json:"stale_interval_ms"`
}

type GetProvisionerJobsByIDsWithQueuePositionRow struct {
	ID             uuid.UUID      `db:"id" json:"id"`
	CreatedAt      time.Time      `db:"created_at" json:"created_at"`
	ProvisionerJob ProvisionerJob `db:"provisioner_job" json:"provisioner_job"`
	QueuePosition  int64          `db:"queue_position" json:"queue_position"`
	QueueSize      int64          `db:"queue_size" json:"queue_size"`
}

func (q *sqlQuerier) GetProvisionerJobsByIDsWithQueuePosition(ctx context.Context, arg GetProvisionerJobsByIDsWithQueuePositionParams) ([]GetProvisionerJobsByIDsWithQueuePositionRow, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerJobsByIDsWithQueuePosition, pq.Array(arg.IDs), arg.StaleIntervalMS)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetProvisionerJobsByIDsWithQueuePositionRow
	for rows.Next() {
		var i GetProvisionerJobsByIDsWithQueuePositionRow
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.ProvisionerJob.ID,
			&i.ProvisionerJob.CreatedAt,
			&i.ProvisionerJob.UpdatedAt,
			&i.ProvisionerJob.StartedAt,
			&i.ProvisionerJob.CanceledAt,
			&i.ProvisionerJob.CompletedAt,
			&i.ProvisionerJob.Error,
			&i.ProvisionerJob.OrganizationID,
			&i.ProvisionerJob.InitiatorID,
			&i.ProvisionerJob.Provisioner,
			&i.ProvisionerJob.StorageMethod,
			&i.ProvisionerJob.Type,
			&i.ProvisionerJob.Input,
			&i.ProvisionerJob.WorkerID,
			&i.ProvisionerJob.FileID,
			&i.ProvisionerJob.Tags,
			&i.ProvisionerJob.ErrorCode,
			&i.ProvisionerJob.TraceMetadata,
			&i.ProvisionerJob.JobStatus,
			&i.ProvisionerJob.LogsLength,
			&i.ProvisionerJob.LogsOverflowed,
			&i.QueuePosition,
			&i.QueueSize,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner = `-- name: GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner :many
WITH pending_jobs AS (
    SELECT
        id, initiator_id, created_at
    FROM
        provisioner_jobs
    WHERE
        started_at IS NULL
    AND
        canceled_at IS NULL
    AND
        completed_at IS NULL
    AND
        error IS NULL
),
queue_position AS (
    SELECT
        id,
        ROW_NUMBER() OVER (ORDER BY initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid ASC, created_at ASC) AS queue_position
    FROM
        pending_jobs
),
queue_size AS (
	SELECT COUNT(*) AS count FROM pending_jobs
)
SELECT
	pj.id, pj.created_at, pj.updated_at, pj.started_at, pj.canceled_at, pj.completed_at, pj.error, pj.organization_id, pj.initiator_id, pj.provisioner, pj.storage_method, pj.type, pj.input, pj.worker_id, pj.file_id, pj.tags, pj.error_code, pj.trace_metadata, pj.job_status, pj.logs_length, pj.logs_overflowed,
    COALESCE(qp.queue_position, 0) AS queue_position,
    COALESCE(qs.count, 0) AS queue_size,
	-- Use subquery to utilize ORDER BY in array_agg since it cannot be
	-- combined with FILTER.
	(
		SELECT
			-- Order for stable output.
			array_agg(pd.id ORDER BY pd.created_at ASC)::uuid[]
		FROM
			provisioner_daemons pd
		WHERE
			-- See AcquireProvisionerJob.
			pj.started_at IS NULL
			AND pj.organization_id = pd.organization_id
			AND pj.provisioner = ANY(pd.provisioners)
			AND provisioner_tagset_contains(pd.tags, pj.tags)
	) AS available_workers,
	-- Include template and workspace information.
	COALESCE(tv.name, '') AS template_version_name,
	t.id AS template_id,
	COALESCE(t.name, '') AS template_name,
	COALESCE(t.display_name, '') AS template_display_name,
	COALESCE(t.icon, '') AS template_icon,
	w.id AS workspace_id,
	COALESCE(w.name, '') AS workspace_name,
	-- Include the name of the provisioner_daemon associated to the job
	COALESCE(pd.name, '') AS worker_name,
	wb.transition as workspace_build_transition
FROM
	provisioner_jobs pj
LEFT JOIN
	queue_position qp ON qp.id = pj.id
LEFT JOIN
	queue_size qs ON TRUE
LEFT JOIN
	workspace_builds wb ON wb.id = CASE WHEN pj.input ? 'workspace_build_id' THEN (pj.input->>'workspace_build_id')::uuid END
LEFT JOIN
	workspaces w ON (
		w.id = wb.workspace_id
		AND w.organization_id = pj.organization_id
	)
LEFT JOIN
	-- We should always have a template version, either explicitly or implicitly via workspace build.
	template_versions tv ON (
		tv.id = CASE WHEN pj.input ? 'template_version_id' THEN (pj.input->>'template_version_id')::uuid ELSE wb.template_version_id END
		AND tv.organization_id = pj.organization_id
	)
LEFT JOIN
	templates t ON (
		t.id = tv.template_id
		AND t.organization_id = pj.organization_id
	)
LEFT JOIN
	-- Join to get the daemon name corresponding to the job's worker_id
	provisioner_daemons pd ON pd.id = pj.worker_id
WHERE
	pj.organization_id = $1::uuid
	AND (COALESCE(array_length($2::uuid[], 1), 0) = 0 OR pj.id = ANY($2::uuid[]))
	AND (COALESCE(array_length($3::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY($3::provisioner_job_status[]))
	AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pj.tags::tagset, $4::tagset))
	AND ($5::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR pj.initiator_id = $5::uuid)
GROUP BY
	pj.id,
	qp.queue_position,
	qs.count,
	tv.name,
	t.id,
	t.name,
	t.display_name,
	t.icon,
	w.id,
	w.name,
	pd.name,
	wb.transition
ORDER BY
	pj.created_at DESC
LIMIT
	$6::int
`

type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams struct {
	OrganizationID uuid.UUID              `db:"organization_id" json:"organization_id"`
	IDs            []uuid.UUID            `db:"ids" json:"ids"`
	Status         []ProvisionerJobStatus `db:"status" json:"status"`
	Tags           StringMap              `db:"tags" json:"tags"`
	InitiatorID    uuid.UUID              `db:"initiator_id" json:"initiator_id"`
	Limit          sql.NullInt32          `db:"limit" json:"limit"`
}

type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow struct {
	ProvisionerJob           ProvisionerJob          `db:"provisioner_job" json:"provisioner_job"`
	QueuePosition            int64                   `db:"queue_position" json:"queue_position"`
	QueueSize                int64                   `db:"queue_size" json:"queue_size"`
	AvailableWorkers         []uuid.UUID             `db:"available_workers" json:"available_workers"`
	TemplateVersionName      string                  `db:"template_version_name" json:"template_version_name"`
	TemplateID               uuid.NullUUID           `db:"template_id" json:"template_id"`
	TemplateName             string                  `db:"template_name" json:"template_name"`
	TemplateDisplayName      string                  `db:"template_display_name" json:"template_display_name"`
	TemplateIcon             string                  `db:"template_icon" json:"template_icon"`
	WorkspaceID              uuid.NullUUID           `db:"workspace_id" json:"workspace_id"`
	WorkspaceName            string                  `db:"workspace_name" json:"workspace_name"`
	WorkerName               string                  `db:"worker_name" json:"worker_name"`
	WorkspaceBuildTransition NullWorkspaceTransition `db:"workspace_build_transition" json:"workspace_build_transition"`
}

func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx context.Context, arg GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams) ([]GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner,
		arg.OrganizationID,
		pq.Array(arg.IDs),
		pq.Array(arg.Status),
		arg.Tags,
		arg.InitiatorID,
		arg.Limit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow
	for rows.Next() {
		var i GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow
		if err := rows.Scan(
			&i.ProvisionerJob.ID,
			&i.ProvisionerJob.CreatedAt,
			&i.ProvisionerJob.UpdatedAt,
			&i.ProvisionerJob.StartedAt,
			&i.ProvisionerJob.CanceledAt,
			&i.ProvisionerJob.CompletedAt,
			&i.ProvisionerJob.Error,
			&i.ProvisionerJob.OrganizationID,
			&i.ProvisionerJob.InitiatorID,
			&i.ProvisionerJob.Provisioner,
			&i.ProvisionerJob.StorageMethod,
			&i.ProvisionerJob.Type,
			&i.ProvisionerJob.Input,
			&i.ProvisionerJob.WorkerID,
			&i.ProvisionerJob.FileID,
			&i.ProvisionerJob.Tags,
			&i.ProvisionerJob.ErrorCode,
			&i.ProvisionerJob.TraceMetadata,
			&i.ProvisionerJob.JobStatus,
			&i.ProvisionerJob.LogsLength,
			&i.ProvisionerJob.LogsOverflowed,
			&i.QueuePosition,
			&i.QueueSize,
			pq.Array(&i.AvailableWorkers),
			&i.TemplateVersionName,
			&i.TemplateID,
			&i.TemplateName,
			&i.TemplateDisplayName,
			&i.TemplateIcon,
			&i.WorkspaceID,
			&i.WorkspaceName,
			&i.WorkerName,
			&i.WorkspaceBuildTransition,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getProvisionerJobsCreatedAfter = `-- name: GetProvisionerJobsCreatedAfter :many
SELECT id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed FROM provisioner_jobs WHERE created_at > $1
`

func (q *sqlQuerier) GetProvisionerJobsCreatedAfter(ctx context.Context, createdAt time.Time) ([]ProvisionerJob, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerJobsCreatedAfter, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerJob
	for rows.Next() {
		var i ProvisionerJob
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.StartedAt,
			&i.CanceledAt,
			&i.CompletedAt,
			&i.Error,
			&i.OrganizationID,
			&i.InitiatorID,
			&i.Provisioner,
			&i.StorageMethod,
			&i.Type,
			&i.Input,
			&i.WorkerID,
			&i.FileID,
			&i.Tags,
			&i.ErrorCode,
			&i.TraceMetadata,
			&i.JobStatus,
			&i.LogsLength,
			&i.LogsOverflowed,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getProvisionerJobsToBeReaped = `-- name: GetProvisionerJobsToBeReaped :many
SELECT
	id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
FROM
	provisioner_jobs
WHERE
	(
		-- If the job has not been started before @pending_since, reap it.
		updated_at < $1
		AND started_at IS NULL
		AND completed_at IS NULL
	)
	OR
	(
		-- If the job has been started but not completed before @hung_since, reap it.
		updated_at < $2
		AND started_at IS NOT NULL
		AND completed_at IS NULL
	)
ORDER BY random()
LIMIT $3
`

type GetProvisionerJobsToBeReapedParams struct {
	PendingSince time.Time `db:"pending_since" json:"pending_since"`
	HungSince    time.Time `db:"hung_since" json:"hung_since"`
	MaxJobs      int32     `db:"max_jobs" json:"max_jobs"`
}

// To avoid repeatedly attempting to reap the same jobs, we randomly order and limit to @max_jobs.
func (q *sqlQuerier) GetProvisionerJobsToBeReaped(ctx context.Context, arg GetProvisionerJobsToBeReapedParams) ([]ProvisionerJob, error) {
	rows, err := q.db.QueryContext(ctx, getProvisionerJobsToBeReaped, arg.PendingSince, arg.HungSince, arg.MaxJobs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerJob
	for rows.Next() {
		var i ProvisionerJob
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.StartedAt,
			&i.CanceledAt,
			&i.CompletedAt,
			&i.Error,
			&i.OrganizationID,
			&i.InitiatorID,
			&i.Provisioner,
			&i.StorageMethod,
			&i.Type,
			&i.Input,
			&i.WorkerID,
			&i.FileID,
			&i.Tags,
			&i.ErrorCode,
			&i.TraceMetadata,
			&i.JobStatus,
			&i.LogsLength,
			&i.LogsOverflowed,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertProvisionerJob = `-- name: InsertProvisionerJob :one
INSERT INTO
	provisioner_jobs (
		id,
		created_at,
		updated_at,
		organization_id,
		initiator_id,
		provisioner,
		storage_method,
		file_id,
		"type",
		"input",
		tags,
		trace_metadata,
		logs_overflowed
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, updated_at, started_at, canceled_at, completed_at, error, organization_id, initiator_id, provisioner, storage_method, type, input, worker_id, file_id, tags, error_code, trace_metadata, job_status, logs_length, logs_overflowed
`

type InsertProvisionerJobParams struct {
	ID             uuid.UUID                `db:"id" json:"id"`
	CreatedAt      time.Time                `db:"created_at" json:"created_at"`
	UpdatedAt      time.Time                `db:"updated_at" json:"updated_at"`
	OrganizationID uuid.UUID                `db:"organization_id" json:"organization_id"`
	InitiatorID    uuid.UUID                `db:"initiator_id" json:"initiator_id"`
	Provisioner    ProvisionerType          `db:"provisioner" json:"provisioner"`
	StorageMethod  ProvisionerStorageMethod `db:"storage_method" json:"storage_method"`
	FileID         uuid.UUID                `db:"file_id" json:"file_id"`
	Type           ProvisionerJobType       `db:"type" json:"type"`
	Input          json.RawMessage          `db:"input" json:"input"`
	Tags           StringMap                `db:"tags" json:"tags"`
	TraceMetadata  pqtype.NullRawMessage    `db:"trace_metadata" json:"trace_metadata"`
	LogsOverflowed bool                     `db:"logs_overflowed" json:"logs_overflowed"`
}

func (q *sqlQuerier) InsertProvisionerJob(ctx context.Context, arg InsertProvisionerJobParams) (ProvisionerJob, error) {
	row := q.db.QueryRowContext(ctx, insertProvisionerJob,
		arg.ID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.OrganizationID,
		arg.InitiatorID,
		arg.Provisioner,
		arg.StorageMethod,
		arg.FileID,
		arg.Type,
		arg.Input,
		arg.Tags,
		arg.TraceMetadata,
		arg.LogsOverflowed,
	)
	var i ProvisionerJob
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.StartedAt,
		&i.CanceledAt,
		&i.CompletedAt,
		&i.Error,
		&i.OrganizationID,
		&i.InitiatorID,
		&i.Provisioner,
		&i.StorageMethod,
		&i.Type,
		&i.Input,
		&i.WorkerID,
		&i.FileID,
		&i.Tags,
		&i.ErrorCode,
		&i.TraceMetadata,
		&i.JobStatus,
		&i.LogsLength,
		&i.LogsOverflowed,
	)
	return i, err
}

const insertProvisionerJobTimings = `-- name: InsertProvisionerJobTimings :many
INSERT INTO provisioner_job_timings (job_id, started_at, ended_at, stage, source, action, resource)
SELECT
    $1::uuid AS provisioner_job_id,
    unnest($2::timestamptz[]),
    unnest($3::timestamptz[]),
    unnest($4::provisioner_job_timing_stage[]),
    unnest($5::text[]),
    unnest($6::text[]),
    unnest($7::text[])
RETURNING job_id, started_at, ended_at, stage, source, action, resource
`

type InsertProvisionerJobTimingsParams struct {
	JobID     uuid.UUID                   `db:"job_id" json:"job_id"`
	StartedAt []time.Time                 `db:"started_at" json:"started_at"`
	EndedAt   []time.Time                 `db:"ended_at" json:"ended_at"`
	Stage     []ProvisionerJobTimingStage `db:"stage" json:"stage"`
	Source    []string                    `db:"source" json:"source"`
	Action    []string                    `db:"action" json:"action"`
	Resource  []string                    `db:"resource" json:"resource"`
}

func (q *sqlQuerier) InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) {
	rows, err := q.db.QueryContext(ctx, insertProvisionerJobTimings,
		arg.JobID,
		pq.Array(arg.StartedAt),
		pq.Array(arg.EndedAt),
		pq.Array(arg.Stage),
		pq.Array(arg.Source),
		pq.Array(arg.Action),
		pq.Array(arg.Resource),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerJobTiming
	for rows.Next() {
		var i ProvisionerJobTiming
		if err := rows.Scan(
			&i.JobID,
			&i.StartedAt,
			&i.EndedAt,
			&i.Stage,
			&i.Source,
			&i.Action,
			&i.Resource,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateProvisionerJobByID = `-- name: UpdateProvisionerJobByID :exec
UPDATE
	provisioner_jobs
SET
	updated_at = $2
WHERE
	id = $1
`

type UpdateProvisionerJobByIDParams struct {
	ID        uuid.UUID `db:"id" json:"id"`
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateProvisionerJobByID, arg.ID, arg.UpdatedAt)
	return err
}

const updateProvisionerJobWithCancelByID = `-- name: UpdateProvisionerJobWithCancelByID :exec
UPDATE
	provisioner_jobs
SET
	canceled_at = $2,
	completed_at = $3
WHERE
	id = $1
`

type UpdateProvisionerJobWithCancelByIDParams struct {
	ID          uuid.UUID    `db:"id" json:"id"`
	CanceledAt  sql.NullTime `db:"canceled_at" json:"canceled_at"`
	CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
}

func (q *sqlQuerier) UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCancelByID, arg.ID, arg.CanceledAt, arg.CompletedAt)
	return err
}

const updateProvisionerJobWithCompleteByID = `-- name: UpdateProvisionerJobWithCompleteByID :exec
UPDATE
	provisioner_jobs
SET
	updated_at = $2,
	completed_at = $3,
	error = $4,
	error_code = $5
WHERE
	id = $1
`

type UpdateProvisionerJobWithCompleteByIDParams struct {
	ID          uuid.UUID      `db:"id" json:"id"`
	UpdatedAt   time.Time      `db:"updated_at" json:"updated_at"`
	CompletedAt sql.NullTime   `db:"completed_at" json:"completed_at"`
	Error       sql.NullString `db:"error" json:"error"`
	ErrorCode   sql.NullString `db:"error_code" json:"error_code"`
}

func (q *sqlQuerier) UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCompleteByID,
		arg.ID,
		arg.UpdatedAt,
		arg.CompletedAt,
		arg.Error,
		arg.ErrorCode,
	)
	return err
}

const updateProvisionerJobWithCompleteWithStartedAtByID = `-- name: UpdateProvisionerJobWithCompleteWithStartedAtByID :exec
UPDATE
	provisioner_jobs
SET
	updated_at = $2,
	completed_at = $3,
	error = $4,
	error_code = $5,
	started_at = $6
WHERE
	id = $1
`

type UpdateProvisionerJobWithCompleteWithStartedAtByIDParams struct {
	ID          uuid.UUID      `db:"id" json:"id"`
	UpdatedAt   time.Time      `db:"updated_at" json:"updated_at"`
	CompletedAt sql.NullTime   `db:"completed_at" json:"completed_at"`
	Error       sql.NullString `db:"error" json:"error"`
	ErrorCode   sql.NullString `db:"error_code" json:"error_code"`
	StartedAt   sql.NullTime   `db:"started_at" json:"started_at"`
}

func (q *sqlQuerier) UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateProvisionerJobWithCompleteWithStartedAtByID,
		arg.ID,
		arg.UpdatedAt,
		arg.CompletedAt,
		arg.Error,
		arg.ErrorCode,
		arg.StartedAt,
	)
	return err
}

const deleteProvisionerKey = `-- name: DeleteProvisionerKey :exec
DELETE FROM
    provisioner_keys
WHERE
    id = $1
`

func (q *sqlQuerier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteProvisionerKey, id)
	return err
}

const getProvisionerKeyByHashedSecret = `-- name: GetProvisionerKeyByHashedSecret :one
SELECT
    id, created_at, organization_id, name, hashed_secret, tags
FROM
    provisioner_keys
WHERE
    hashed_secret = $1
`

func (q *sqlQuerier) GetProvisionerKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (ProvisionerKey, error) {
	row := q.db.QueryRowContext(ctx, getProvisionerKeyByHashedSecret, hashedSecret)
	var i ProvisionerKey
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.OrganizationID,
		&i.Name,
		&i.HashedSecret,
		&i.Tags,
	)
	return i, err
}

const getProvisionerKeyByID = `-- name: GetProvisionerKeyByID :one
SELECT
    id, created_at, organization_id, name, hashed_secret, tags
FROM
    provisioner_keys
WHERE
    id = $1
`

func (q *sqlQuerier) GetProvisionerKeyByID(ctx context.Context, id uuid.UUID) (ProvisionerKey, error) {
	row := q.db.QueryRowContext(ctx, getProvisionerKeyByID, id)
	var i ProvisionerKey
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.OrganizationID,
		&i.Name,
		&i.HashedSecret,
		&i.Tags,
	)
	return i, err
}

const getProvisionerKeyByName = `-- name: GetProvisionerKeyByName :one
SELECT
    id, created_at, organization_id, name, hashed_secret, tags
FROM
    provisioner_keys
WHERE
    organization_id = $1
AND
    lower(name) = lower($2)
`

type GetProvisionerKeyByNameParams struct {
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	Name           string    `db:"name" json:"name"`
}

func (q *sqlQuerier) GetProvisionerKeyByName(ctx context.Context, arg GetProvisionerKeyByNameParams) (ProvisionerKey, error) {
	row := q.db.QueryRowContext(ctx, getProvisionerKeyByName, arg.OrganizationID, arg.Name)
	var i ProvisionerKey
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.OrganizationID,
		&i.Name,
		&i.HashedSecret,
		&i.Tags,
	)
	return i, err
}

const insertProvisionerKey = `-- name: InsertProvisionerKey :one
INSERT INTO
    provisioner_keys (
        id,
        created_at,
        organization_id,
        name,
        hashed_secret,
        tags
    )
VALUES
    ($1, $2, $3, lower($6), $4, $5) RETURNING id, created_at, organization_id, name, hashed_secret, tags
`

type InsertProvisionerKeyParams struct {
	ID             uuid.UUID `db:"id" json:"id"`
	CreatedAt      time.Time `db:"created_at" json:"created_at"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	HashedSecret   []byte    `db:"hashed_secret" json:"hashed_secret"`
	Tags           StringMap `db:"tags" json:"tags"`
	Name           string    `db:"name" json:"name"`
}

func (q *sqlQuerier) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) {
	row := q.db.QueryRowContext(ctx, insertProvisionerKey,
		arg.ID,
		arg.CreatedAt,
		arg.OrganizationID,
		arg.HashedSecret,
		arg.Tags,
		arg.Name,
	)
	var i ProvisionerKey
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.OrganizationID,
		&i.Name,
		&i.HashedSecret,
		&i.Tags,
	)
	return i, err
}

const listProvisionerKeysByOrganization = `-- name: ListProvisionerKeysByOrganization :many
SELECT
    id, created_at, organization_id, name, hashed_secret, tags
FROM
    provisioner_keys
WHERE
    organization_id = $1
`

func (q *sqlQuerier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) {
	rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganization, organizationID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerKey
	for rows.Next() {
		var i ProvisionerKey
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.OrganizationID,
			&i.Name,
			&i.HashedSecret,
			&i.Tags,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listProvisionerKeysByOrganizationExcludeReserved = `-- name: ListProvisionerKeysByOrganizationExcludeReserved :many
SELECT
    id, created_at, organization_id, name, hashed_secret, tags
FROM
    provisioner_keys
WHERE
    organization_id = $1
AND
    -- exclude reserved built-in key
    id != '00000000-0000-0000-0000-000000000001'::uuid
AND
    -- exclude reserved user-auth key
    id != '00000000-0000-0000-0000-000000000002'::uuid
AND
    -- exclude reserved psk key
    id != '00000000-0000-0000-0000-000000000003'::uuid
`

func (q *sqlQuerier) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) {
	rows, err := q.db.QueryContext(ctx, listProvisionerKeysByOrganizationExcludeReserved, organizationID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ProvisionerKey
	for rows.Next() {
		var i ProvisionerKey
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.OrganizationID,
			&i.Name,
			&i.HashedSecret,
			&i.Tags,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceProxies = `-- name: GetWorkspaceProxies :many
SELECT
	id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
FROM
	workspace_proxies
WHERE
	deleted = false
`

func (q *sqlQuerier) GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceProxies)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceProxy
	for rows.Next() {
		var i WorkspaceProxy
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.DisplayName,
			&i.Icon,
			&i.Url,
			&i.WildcardHostname,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Deleted,
			&i.TokenHashedSecret,
			&i.RegionID,
			&i.DerpEnabled,
			&i.DerpOnly,
			&i.Version,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceProxyByHostname = `-- name: GetWorkspaceProxyByHostname :one
SELECT
	id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
FROM
	workspace_proxies
WHERE
	-- Validate that the @hostname has been sanitized and is not empty. This
	-- doesn't prevent SQL injection (already prevented by using prepared
	-- queries), but it does prevent carefully crafted hostnames from matching
	-- when they shouldn't.
	--
	-- Periods don't need to be escaped because they're not special characters
	-- in SQL matches unlike regular expressions.
	$1 :: text SIMILAR TO '[a-zA-Z0-9._-]+' AND
	deleted = false AND

	-- Validate that the hostname matches either the wildcard hostname or the
	-- access URL (ignoring scheme, port and path).
	(
		(
			$2 :: bool = true AND
			url SIMILAR TO '[^:]*://' || $1 :: text || '([:/]?%)*'
		) OR
		(
			$3 :: bool = true AND
			$1 :: text LIKE replace(wildcard_hostname, '*', '%')
		)
	)
LIMIT
	1
`

type GetWorkspaceProxyByHostnameParams struct {
	Hostname              string `db:"hostname" json:"hostname"`
	AllowAccessUrl        bool   `db:"allow_access_url" json:"allow_access_url"`
	AllowWildcardHostname bool   `db:"allow_wildcard_hostname" json:"allow_wildcard_hostname"`
}

// Finds a workspace proxy that has an access URL or app hostname that matches
// the provided hostname. This is to check if a hostname matches any workspace
// proxy.
//
// The hostname must be sanitized to only contain [a-zA-Z0-9.-] before calling
// this query. The scheme, port and path should be stripped.
func (q *sqlQuerier) GetWorkspaceProxyByHostname(ctx context.Context, arg GetWorkspaceProxyByHostnameParams) (WorkspaceProxy, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceProxyByHostname, arg.Hostname, arg.AllowAccessUrl, arg.AllowWildcardHostname)
	var i WorkspaceProxy
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.DisplayName,
		&i.Icon,
		&i.Url,
		&i.WildcardHostname,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Deleted,
		&i.TokenHashedSecret,
		&i.RegionID,
		&i.DerpEnabled,
		&i.DerpOnly,
		&i.Version,
	)
	return i, err
}

const getWorkspaceProxyByID = `-- name: GetWorkspaceProxyByID :one
SELECT
	id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
FROM
	workspace_proxies
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetWorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceProxyByID, id)
	var i WorkspaceProxy
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.DisplayName,
		&i.Icon,
		&i.Url,
		&i.WildcardHostname,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Deleted,
		&i.TokenHashedSecret,
		&i.RegionID,
		&i.DerpEnabled,
		&i.DerpOnly,
		&i.Version,
	)
	return i, err
}

const getWorkspaceProxyByName = `-- name: GetWorkspaceProxyByName :one
SELECT
	id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
FROM
	workspace_proxies
WHERE
	name = $1
	AND deleted = false
LIMIT
	1
`

func (q *sqlQuerier) GetWorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceProxyByName, name)
	var i WorkspaceProxy
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.DisplayName,
		&i.Icon,
		&i.Url,
		&i.WildcardHostname,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Deleted,
		&i.TokenHashedSecret,
		&i.RegionID,
		&i.DerpEnabled,
		&i.DerpOnly,
		&i.Version,
	)
	return i, err
}

const insertWorkspaceProxy = `-- name: InsertWorkspaceProxy :one
INSERT INTO
	workspace_proxies (
		id,
		url,
		wildcard_hostname,
		name,
		display_name,
		icon,
		derp_enabled,
		derp_only,
		token_hashed_secret,
		created_at,
		updated_at,
		deleted
	)
VALUES
	($1, '', '', $2, $3, $4, $5, $6, $7, $8, $9, false) RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
`

type InsertWorkspaceProxyParams struct {
	ID                uuid.UUID `db:"id" json:"id"`
	Name              string    `db:"name" json:"name"`
	DisplayName       string    `db:"display_name" json:"display_name"`
	Icon              string    `db:"icon" json:"icon"`
	DerpEnabled       bool      `db:"derp_enabled" json:"derp_enabled"`
	DerpOnly          bool      `db:"derp_only" json:"derp_only"`
	TokenHashedSecret []byte    `db:"token_hashed_secret" json:"token_hashed_secret"`
	CreatedAt         time.Time `db:"created_at" json:"created_at"`
	UpdatedAt         time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) {
	row := q.db.QueryRowContext(ctx, insertWorkspaceProxy,
		arg.ID,
		arg.Name,
		arg.DisplayName,
		arg.Icon,
		arg.DerpEnabled,
		arg.DerpOnly,
		arg.TokenHashedSecret,
		arg.CreatedAt,
		arg.UpdatedAt,
	)
	var i WorkspaceProxy
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.DisplayName,
		&i.Icon,
		&i.Url,
		&i.WildcardHostname,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Deleted,
		&i.TokenHashedSecret,
		&i.RegionID,
		&i.DerpEnabled,
		&i.DerpOnly,
		&i.Version,
	)
	return i, err
}

const registerWorkspaceProxy = `-- name: RegisterWorkspaceProxy :one
UPDATE
	workspace_proxies
SET
	url = $1 :: text,
	wildcard_hostname = $2 :: text,
	derp_enabled = $3 :: boolean,
	derp_only = $4 :: boolean,
	version = $5 :: text,
	updated_at = Now()
WHERE
	id = $6
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
`

type RegisterWorkspaceProxyParams struct {
	Url              string    `db:"url" json:"url"`
	WildcardHostname string    `db:"wildcard_hostname" json:"wildcard_hostname"`
	DerpEnabled      bool      `db:"derp_enabled" json:"derp_enabled"`
	DerpOnly         bool      `db:"derp_only" json:"derp_only"`
	Version          string    `db:"version" json:"version"`
	ID               uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) {
	row := q.db.QueryRowContext(ctx, registerWorkspaceProxy,
		arg.Url,
		arg.WildcardHostname,
		arg.DerpEnabled,
		arg.DerpOnly,
		arg.Version,
		arg.ID,
	)
	var i WorkspaceProxy
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.DisplayName,
		&i.Icon,
		&i.Url,
		&i.WildcardHostname,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Deleted,
		&i.TokenHashedSecret,
		&i.RegionID,
		&i.DerpEnabled,
		&i.DerpOnly,
		&i.Version,
	)
	return i, err
}

const updateWorkspaceProxy = `-- name: UpdateWorkspaceProxy :one
UPDATE
	workspace_proxies
SET
	-- These values should always be provided.
	name = $1,
	display_name = $2,
	icon = $3,
	-- Only update the token if a new one is provided.
	-- So this is an optional field.
	token_hashed_secret = CASE
		WHEN length($4 :: bytea) > 0  THEN $4 :: bytea
		ELSE  workspace_proxies.token_hashed_secret
	END,
	-- Always update this timestamp.
	updated_at = Now()
WHERE
	id = $5
RETURNING id, name, display_name, icon, url, wildcard_hostname, created_at, updated_at, deleted, token_hashed_secret, region_id, derp_enabled, derp_only, version
`

type UpdateWorkspaceProxyParams struct {
	Name              string    `db:"name" json:"name"`
	DisplayName       string    `db:"display_name" json:"display_name"`
	Icon              string    `db:"icon" json:"icon"`
	TokenHashedSecret []byte    `db:"token_hashed_secret" json:"token_hashed_secret"`
	ID                uuid.UUID `db:"id" json:"id"`
}

// This allows editing the properties of a workspace proxy.
func (q *sqlQuerier) UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) {
	row := q.db.QueryRowContext(ctx, updateWorkspaceProxy,
		arg.Name,
		arg.DisplayName,
		arg.Icon,
		arg.TokenHashedSecret,
		arg.ID,
	)
	var i WorkspaceProxy
	err := row.Scan(
		&i.ID,
		&i.Name,
		&i.DisplayName,
		&i.Icon,
		&i.Url,
		&i.WildcardHostname,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Deleted,
		&i.TokenHashedSecret,
		&i.RegionID,
		&i.DerpEnabled,
		&i.DerpOnly,
		&i.Version,
	)
	return i, err
}

const updateWorkspaceProxyDeleted = `-- name: UpdateWorkspaceProxyDeleted :exec
UPDATE
	workspace_proxies
SET
	updated_at = Now(),
	deleted = $1
WHERE
	id = $2
`

type UpdateWorkspaceProxyDeletedParams struct {
	Deleted bool      `db:"deleted" json:"deleted"`
	ID      uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceProxyDeleted, arg.Deleted, arg.ID)
	return err
}

const getQuotaAllowanceForUser = `-- name: GetQuotaAllowanceForUser :one
SELECT
	coalesce(SUM(groups.quota_allowance), 0)::BIGINT
FROM
	(
		-- Select all groups this user is a member of. This will also include
		-- the "Everyone" group for organizations the user is a member of.
		SELECT user_id, user_email, user_username, user_hashed_password, user_created_at, user_updated_at, user_status, user_rbac_roles, user_login_type, user_avatar_url, user_deleted, user_last_seen_at, user_quiet_hours_schedule, user_name, user_github_com_user_id, user_is_system, user_is_service_account, organization_id, group_name, group_id FROM group_members_expanded
		         WHERE
		             $1 = user_id AND
		             $2 = group_members_expanded.organization_id
	) AS members
INNER JOIN groups ON
	members.group_id = groups.id
`

type GetQuotaAllowanceForUserParams struct {
	UserID         uuid.UUID `db:"user_id" json:"user_id"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}

func (q *sqlQuerier) GetQuotaAllowanceForUser(ctx context.Context, arg GetQuotaAllowanceForUserParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, getQuotaAllowanceForUser, arg.UserID, arg.OrganizationID)
	var column_1 int64
	err := row.Scan(&column_1)
	return column_1, err
}

const getQuotaConsumedForUser = `-- name: GetQuotaConsumedForUser :one
WITH latest_builds AS (
SELECT
	DISTINCT ON
	(wb.workspace_id) wb.workspace_id,
	wb.daily_cost
FROM
	workspace_builds wb
 -- This INNER JOIN prevents a seq scan of the workspace_builds table.
 -- Limit the rows to the absolute minimum required, which is all workspaces
 -- in a given organization for a given user.
INNER JOIN
	workspaces on wb.workspace_id = workspaces.id
WHERE
	-- Only return workspaces that match the user + organization.
	-- Quotas are calculated per user per organization.
	NOT workspaces.deleted AND
	workspaces.owner_id = $1 AND
	workspaces.organization_id = $2
ORDER BY
	wb.workspace_id,
	wb.build_number DESC
)
SELECT
	coalesce(SUM(daily_cost), 0)::BIGINT
FROM
	latest_builds
`

type GetQuotaConsumedForUserParams struct {
	OwnerID        uuid.UUID `db:"owner_id" json:"owner_id"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}

func (q *sqlQuerier) GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error) {
	row := q.db.QueryRowContext(ctx, getQuotaConsumedForUser, arg.OwnerID, arg.OrganizationID)
	var column_1 int64
	err := row.Scan(&column_1)
	return column_1, err
}

const deleteReplicasUpdatedBefore = `-- name: DeleteReplicasUpdatedBefore :exec
DELETE FROM replicas WHERE updated_at < $1
`

func (q *sqlQuerier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error {
	_, err := q.db.ExecContext(ctx, deleteReplicasUpdatedBefore, updatedAt)
	return err
}

const getReplicaByID = `-- name: GetReplicaByID :one
SELECT id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary" FROM replicas WHERE id = $1
`

func (q *sqlQuerier) GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error) {
	row := q.db.QueryRowContext(ctx, getReplicaByID, id)
	var i Replica
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.StartedAt,
		&i.StoppedAt,
		&i.UpdatedAt,
		&i.Hostname,
		&i.RegionID,
		&i.RelayAddress,
		&i.DatabaseLatency,
		&i.Version,
		&i.Error,
		&i.Primary,
	)
	return i, err
}

const getReplicasUpdatedAfter = `-- name: GetReplicasUpdatedAfter :many
SELECT id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary" FROM replicas WHERE updated_at > $1 AND stopped_at IS NULL
`

func (q *sqlQuerier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error) {
	rows, err := q.db.QueryContext(ctx, getReplicasUpdatedAfter, updatedAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Replica
	for rows.Next() {
		var i Replica
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.StartedAt,
			&i.StoppedAt,
			&i.UpdatedAt,
			&i.Hostname,
			&i.RegionID,
			&i.RelayAddress,
			&i.DatabaseLatency,
			&i.Version,
			&i.Error,
			&i.Primary,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertReplica = `-- name: InsertReplica :one
INSERT INTO replicas (
    id,
    created_at,
    started_at,
    updated_at,
    hostname,
    region_id,
    relay_address,
    version,
    database_latency,
	"primary"
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary"
`

type InsertReplicaParams struct {
	ID              uuid.UUID `db:"id" json:"id"`
	CreatedAt       time.Time `db:"created_at" json:"created_at"`
	StartedAt       time.Time `db:"started_at" json:"started_at"`
	UpdatedAt       time.Time `db:"updated_at" json:"updated_at"`
	Hostname        string    `db:"hostname" json:"hostname"`
	RegionID        int32     `db:"region_id" json:"region_id"`
	RelayAddress    string    `db:"relay_address" json:"relay_address"`
	Version         string    `db:"version" json:"version"`
	DatabaseLatency int32     `db:"database_latency" json:"database_latency"`
	Primary         bool      `db:"primary" json:"primary"`
}

func (q *sqlQuerier) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) {
	row := q.db.QueryRowContext(ctx, insertReplica,
		arg.ID,
		arg.CreatedAt,
		arg.StartedAt,
		arg.UpdatedAt,
		arg.Hostname,
		arg.RegionID,
		arg.RelayAddress,
		arg.Version,
		arg.DatabaseLatency,
		arg.Primary,
	)
	var i Replica
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.StartedAt,
		&i.StoppedAt,
		&i.UpdatedAt,
		&i.Hostname,
		&i.RegionID,
		&i.RelayAddress,
		&i.DatabaseLatency,
		&i.Version,
		&i.Error,
		&i.Primary,
	)
	return i, err
}

const updateReplica = `-- name: UpdateReplica :one
UPDATE replicas SET
    updated_at = $2,
    started_at = $3,
    stopped_at = $4,
    relay_address = $5,
    region_id = $6,
    hostname = $7,
    version = $8,
    error = $9,
    database_latency = $10,
	"primary" = $11
WHERE id = $1 RETURNING id, created_at, started_at, stopped_at, updated_at, hostname, region_id, relay_address, database_latency, version, error, "primary"
`

type UpdateReplicaParams struct {
	ID              uuid.UUID    `db:"id" json:"id"`
	UpdatedAt       time.Time    `db:"updated_at" json:"updated_at"`
	StartedAt       time.Time    `db:"started_at" json:"started_at"`
	StoppedAt       sql.NullTime `db:"stopped_at" json:"stopped_at"`
	RelayAddress    string       `db:"relay_address" json:"relay_address"`
	RegionID        int32        `db:"region_id" json:"region_id"`
	Hostname        string       `db:"hostname" json:"hostname"`
	Version         string       `db:"version" json:"version"`
	Error           string       `db:"error" json:"error"`
	DatabaseLatency int32        `db:"database_latency" json:"database_latency"`
	Primary         bool         `db:"primary" json:"primary"`
}

func (q *sqlQuerier) UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) {
	row := q.db.QueryRowContext(ctx, updateReplica,
		arg.ID,
		arg.UpdatedAt,
		arg.StartedAt,
		arg.StoppedAt,
		arg.RelayAddress,
		arg.RegionID,
		arg.Hostname,
		arg.Version,
		arg.Error,
		arg.DatabaseLatency,
		arg.Primary,
	)
	var i Replica
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.StartedAt,
		&i.StoppedAt,
		&i.UpdatedAt,
		&i.Hostname,
		&i.RegionID,
		&i.RelayAddress,
		&i.DatabaseLatency,
		&i.Version,
		&i.Error,
		&i.Primary,
	)
	return i, err
}

const customRoles = `-- name: CustomRoles :many
SELECT
	name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id, is_system, member_permissions
FROM
	custom_roles
WHERE
	true
	-- @lookup_roles will filter for exact (role_name, org_id) pairs
	-- To do this manually in SQL, you can construct an array and cast it:
	-- cast(ARRAY[('customrole','ece79dac-926e-44ca-9790-2ff7c5eb6e0c')] AS name_organization_pair[])
	AND CASE WHEN array_length($1 :: name_organization_pair[], 1) > 0  THEN
		-- Using 'coalesce' to avoid troubles with null literals being an empty string.
		(name, coalesce(organization_id, '00000000-0000-0000-0000-000000000000' ::uuid)) = ANY ($1::name_organization_pair[])
	ELSE true
	END
	-- This allows fetching all roles, or just site wide roles
	AND CASE WHEN $2 :: boolean  THEN
		organization_id IS null
	ELSE true
	END
	-- Allows fetching all roles to a particular organization
	AND CASE WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid  THEN
		organization_id = $3
	ELSE true
	END
	-- Filter system roles. By default, system roles are excluded.
	-- System roles are managed by Coder and should be hidden from user-facing APIs.
	-- The authorization system uses @include_system_roles = true to load them.
	AND CASE WHEN $4 :: boolean THEN
		true
	ELSE
		is_system = false
	END
`

type CustomRolesParams struct {
	LookupRoles        []NameOrganizationPair `db:"lookup_roles" json:"lookup_roles"`
	ExcludeOrgRoles    bool                   `db:"exclude_org_roles" json:"exclude_org_roles"`
	OrganizationID     uuid.UUID              `db:"organization_id" json:"organization_id"`
	IncludeSystemRoles bool                   `db:"include_system_roles" json:"include_system_roles"`
}

func (q *sqlQuerier) CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error) {
	rows, err := q.db.QueryContext(ctx, customRoles,
		pq.Array(arg.LookupRoles),
		arg.ExcludeOrgRoles,
		arg.OrganizationID,
		arg.IncludeSystemRoles,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []CustomRole
	for rows.Next() {
		var i CustomRole
		if err := rows.Scan(
			&i.Name,
			&i.DisplayName,
			&i.SitePermissions,
			&i.OrgPermissions,
			&i.UserPermissions,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.OrganizationID,
			&i.ID,
			&i.IsSystem,
			&i.MemberPermissions,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteCustomRole = `-- name: DeleteCustomRole :exec
DELETE FROM
	custom_roles
WHERE
	name = lower($1)
	AND organization_id = $2
	-- Prevents accidental deletion of system roles even if the API
	-- layer check is bypassed due to a bug.
	AND is_system = false
`

type DeleteCustomRoleParams struct {
	Name           string        `db:"name" json:"name"`
	OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
}

func (q *sqlQuerier) DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error {
	_, err := q.db.ExecContext(ctx, deleteCustomRole, arg.Name, arg.OrganizationID)
	return err
}

const insertCustomRole = `-- name: InsertCustomRole :one
INSERT INTO
	custom_roles (
	name,
	display_name,
	organization_id,
	site_permissions,
	org_permissions,
	user_permissions,
	member_permissions,
	is_system,
	created_at,
	updated_at
)
VALUES (
	-- Always force lowercase names
	lower($1),
	$2,
	$3,
	$4,
	$5,
	$6,
	$7,
	$8,
	now(),
	now()
)
RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id, is_system, member_permissions
`

type InsertCustomRoleParams struct {
	Name              string                `db:"name" json:"name"`
	DisplayName       string                `db:"display_name" json:"display_name"`
	OrganizationID    uuid.NullUUID         `db:"organization_id" json:"organization_id"`
	SitePermissions   CustomRolePermissions `db:"site_permissions" json:"site_permissions"`
	OrgPermissions    CustomRolePermissions `db:"org_permissions" json:"org_permissions"`
	UserPermissions   CustomRolePermissions `db:"user_permissions" json:"user_permissions"`
	MemberPermissions CustomRolePermissions `db:"member_permissions" json:"member_permissions"`
	IsSystem          bool                  `db:"is_system" json:"is_system"`
}

func (q *sqlQuerier) InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error) {
	row := q.db.QueryRowContext(ctx, insertCustomRole,
		arg.Name,
		arg.DisplayName,
		arg.OrganizationID,
		arg.SitePermissions,
		arg.OrgPermissions,
		arg.UserPermissions,
		arg.MemberPermissions,
		arg.IsSystem,
	)
	var i CustomRole
	err := row.Scan(
		&i.Name,
		&i.DisplayName,
		&i.SitePermissions,
		&i.OrgPermissions,
		&i.UserPermissions,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OrganizationID,
		&i.ID,
		&i.IsSystem,
		&i.MemberPermissions,
	)
	return i, err
}

const updateCustomRole = `-- name: UpdateCustomRole :one
UPDATE
	custom_roles
SET
	display_name = $1,
	site_permissions = $2,
	org_permissions = $3,
	user_permissions = $4,
	member_permissions = $5,
	updated_at = now()
WHERE
	name = lower($6)
	AND organization_id = $7
RETURNING name, display_name, site_permissions, org_permissions, user_permissions, created_at, updated_at, organization_id, id, is_system, member_permissions
`

type UpdateCustomRoleParams struct {
	DisplayName       string                `db:"display_name" json:"display_name"`
	SitePermissions   CustomRolePermissions `db:"site_permissions" json:"site_permissions"`
	OrgPermissions    CustomRolePermissions `db:"org_permissions" json:"org_permissions"`
	UserPermissions   CustomRolePermissions `db:"user_permissions" json:"user_permissions"`
	MemberPermissions CustomRolePermissions `db:"member_permissions" json:"member_permissions"`
	Name              string                `db:"name" json:"name"`
	OrganizationID    uuid.NullUUID         `db:"organization_id" json:"organization_id"`
}

func (q *sqlQuerier) UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error) {
	row := q.db.QueryRowContext(ctx, updateCustomRole,
		arg.DisplayName,
		arg.SitePermissions,
		arg.OrgPermissions,
		arg.UserPermissions,
		arg.MemberPermissions,
		arg.Name,
		arg.OrganizationID,
	)
	var i CustomRole
	err := row.Scan(
		&i.Name,
		&i.DisplayName,
		&i.SitePermissions,
		&i.OrgPermissions,
		&i.UserPermissions,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OrganizationID,
		&i.ID,
		&i.IsSystem,
		&i.MemberPermissions,
	)
	return i, err
}

const deleteRuntimeConfig = `-- name: DeleteRuntimeConfig :exec
DELETE FROM site_configs
WHERE site_configs.key = $1
`

func (q *sqlQuerier) DeleteRuntimeConfig(ctx context.Context, key string) error {
	_, err := q.db.ExecContext(ctx, deleteRuntimeConfig, key)
	return err
}

const getAnnouncementBanners = `-- name: GetAnnouncementBanners :one
SELECT value FROM site_configs WHERE key = 'announcement_banners'
`

func (q *sqlQuerier) GetAnnouncementBanners(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getAnnouncementBanners)
	var value string
	err := row.Scan(&value)
	return value, err
}

const getApplicationName = `-- name: GetApplicationName :one
SELECT value FROM site_configs WHERE key = 'application_name'
`

func (q *sqlQuerier) GetApplicationName(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getApplicationName)
	var value string
	err := row.Scan(&value)
	return value, err
}

const getChatAdvisorConfig = `-- name: GetChatAdvisorConfig :one
SELECT
    COALESCE((SELECT value FROM site_configs WHERE key = 'agents_advisor_config'), '{}') :: text AS advisor_config
`

// GetChatAdvisorConfig returns the deployment-wide runtime configuration
// for the experimental chat advisor as a JSON blob. Callers unmarshal the
// result into codersdk.AdvisorConfig. Returns '{}' when unset so zero
// values apply by default.
func (q *sqlQuerier) GetChatAdvisorConfig(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatAdvisorConfig)
	var advisor_config string
	err := row.Scan(&advisor_config)
	return advisor_config, err
}

const getChatAutoArchiveDays = `-- name: GetChatAutoArchiveDays :one
SELECT COALESCE(
    (SELECT value::integer FROM site_configs
     WHERE key = 'agents_chat_auto_archive_days'),
    $1::integer
) :: integer AS auto_archive_days
`

// Auto-archive window in days. 0 disables.
func (q *sqlQuerier) GetChatAutoArchiveDays(ctx context.Context, defaultAutoArchiveDays int32) (int32, error) {
	row := q.db.QueryRowContext(ctx, getChatAutoArchiveDays, defaultAutoArchiveDays)
	var auto_archive_days int32
	err := row.Scan(&auto_archive_days)
	return auto_archive_days, err
}

const getChatComputerUseProvider = `-- name: GetChatComputerUseProvider :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'agents_computer_use_provider'), '') :: text AS provider
`

func (q *sqlQuerier) GetChatComputerUseProvider(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatComputerUseProvider)
	var provider string
	err := row.Scan(&provider)
	return provider, err
}

const getChatDebugLoggingAllowUsers = `-- name: GetChatDebugLoggingAllowUsers :one
SELECT
	COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_debug_logging_allow_users'), false) :: boolean AS allow_users
`

// GetChatDebugLoggingAllowUsers returns the runtime admin setting that
// allows users to opt into chat debug logging when the deployment does
// not already force debug logging on globally.
func (q *sqlQuerier) GetChatDebugLoggingAllowUsers(ctx context.Context) (bool, error) {
	row := q.db.QueryRowContext(ctx, getChatDebugLoggingAllowUsers)
	var allow_users bool
	err := row.Scan(&allow_users)
	return allow_users, err
}

const getChatDebugRetentionDays = `-- name: GetChatDebugRetentionDays :one
SELECT COALESCE(
    (SELECT value::integer FROM site_configs
     WHERE key = 'agents_chat_debug_retention_days'),
    $1::integer
) :: integer AS debug_retention_days
`

// Chat debug run retention window in days. 0 disables.
func (q *sqlQuerier) GetChatDebugRetentionDays(ctx context.Context, defaultDebugRetentionDays int32) (int32, error) {
	row := q.db.QueryRowContext(ctx, getChatDebugRetentionDays, defaultDebugRetentionDays)
	var debug_retention_days int32
	err := row.Scan(&debug_retention_days)
	return debug_retention_days, err
}

const getChatDesktopEnabled = `-- name: GetChatDesktopEnabled :one
SELECT
	COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_desktop_enabled'), false) :: boolean AS enable_desktop
`

func (q *sqlQuerier) GetChatDesktopEnabled(ctx context.Context) (bool, error) {
	row := q.db.QueryRowContext(ctx, getChatDesktopEnabled)
	var enable_desktop bool
	err := row.Scan(&enable_desktop)
	return enable_desktop, err
}

const getChatExploreModelOverride = `-- name: GetChatExploreModelOverride :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_explore_model_override'), '') :: text AS model_config_id
`

func (q *sqlQuerier) GetChatExploreModelOverride(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatExploreModelOverride)
	var model_config_id string
	err := row.Scan(&model_config_id)
	return model_config_id, err
}

const getChatGeneralModelOverride = `-- name: GetChatGeneralModelOverride :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_general_model_override'), '') :: text AS model_config_id
`

func (q *sqlQuerier) GetChatGeneralModelOverride(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatGeneralModelOverride)
	var model_config_id string
	err := row.Scan(&model_config_id)
	return model_config_id, err
}

const getChatIncludeDefaultSystemPrompt = `-- name: GetChatIncludeDefaultSystemPrompt :one
SELECT
    COALESCE(
        (SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_include_default_system_prompt'),
        NOT EXISTS (
            SELECT 1
            FROM site_configs
            WHERE key = 'agents_chat_system_prompt'
                AND value != ''
        )
    ) :: boolean AS include_default_system_prompt
`

// GetChatIncludeDefaultSystemPrompt preserves the legacy default
// for deployments created before the explicit include-default toggle.
// When the toggle is unset, a non-empty custom prompt implies false;
// otherwise the setting defaults to true.
func (q *sqlQuerier) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
	row := q.db.QueryRowContext(ctx, getChatIncludeDefaultSystemPrompt)
	var include_default_system_prompt bool
	err := row.Scan(&include_default_system_prompt)
	return include_default_system_prompt, err
}

const getChatPersonalModelOverridesEnabled = `-- name: GetChatPersonalModelOverridesEnabled :one
SELECT
	COALESCE((SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_personal_model_overrides_enabled'), false) :: boolean AS enabled
`

// GetChatPersonalModelOverridesEnabled returns whether users may configure
// personal chat model overrides. It defaults to false when unset.
func (q *sqlQuerier) GetChatPersonalModelOverridesEnabled(ctx context.Context) (bool, error) {
	row := q.db.QueryRowContext(ctx, getChatPersonalModelOverridesEnabled)
	var enabled bool
	err := row.Scan(&enabled)
	return enabled, err
}

const getChatPlanModeInstructions = `-- name: GetChatPlanModeInstructions :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_plan_mode_instructions'), '') :: text AS plan_mode_instructions
`

func (q *sqlQuerier) GetChatPlanModeInstructions(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatPlanModeInstructions)
	var plan_mode_instructions string
	err := row.Scan(&plan_mode_instructions)
	return plan_mode_instructions, err
}

const getChatRetentionDays = `-- name: GetChatRetentionDays :one
SELECT COALESCE(
    (SELECT value::integer FROM site_configs
     WHERE key = 'agents_chat_retention_days'),
    30
) :: integer AS retention_days
`

// Returns the chat retention period in days. Chats archived longer
// than this and orphaned chat files older than this are purged by
// dbpurge. Returns 30 (days) when no value has been configured.
// A value of 0 disables chat purging entirely.
func (q *sqlQuerier) GetChatRetentionDays(ctx context.Context) (int32, error) {
	row := q.db.QueryRowContext(ctx, getChatRetentionDays)
	var retention_days int32
	err := row.Scan(&retention_days)
	return retention_days, err
}

const getChatSystemPrompt = `-- name: GetChatSystemPrompt :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt
`

func (q *sqlQuerier) GetChatSystemPrompt(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatSystemPrompt)
	var chat_system_prompt string
	err := row.Scan(&chat_system_prompt)
	return chat_system_prompt, err
}

const getChatSystemPromptConfig = `-- name: GetChatSystemPromptConfig :one
SELECT
    COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt,
    COALESCE(
        (SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_include_default_system_prompt'),
        NOT EXISTS (
            SELECT 1
            FROM site_configs
            WHERE key = 'agents_chat_system_prompt'
                AND value != ''
        )
    ) :: boolean AS include_default_system_prompt
`

type GetChatSystemPromptConfigRow struct {
	ChatSystemPrompt           string `db:"chat_system_prompt" json:"chat_system_prompt"`
	IncludeDefaultSystemPrompt bool   `db:"include_default_system_prompt" json:"include_default_system_prompt"`
}

// GetChatSystemPromptConfig returns both chat system prompt settings in a
// single read to avoid torn reads between separate site-config lookups.
// The include-default fallback preserves the legacy behavior where a
// non-empty custom prompt implied opting out before the explicit toggle
// existed.
func (q *sqlQuerier) GetChatSystemPromptConfig(ctx context.Context) (GetChatSystemPromptConfigRow, error) {
	row := q.db.QueryRowContext(ctx, getChatSystemPromptConfig)
	var i GetChatSystemPromptConfigRow
	err := row.Scan(&i.ChatSystemPrompt, &i.IncludeDefaultSystemPrompt)
	return i, err
}

const getChatTemplateAllowlist = `-- name: GetChatTemplateAllowlist :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'agents_template_allowlist'), '') :: text AS template_allowlist
`

// GetChatTemplateAllowlist returns the JSON-encoded template allowlist.
// Returns an empty string when no allowlist has been configured (all templates allowed).
func (q *sqlQuerier) GetChatTemplateAllowlist(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatTemplateAllowlist)
	var template_allowlist string
	err := row.Scan(&template_allowlist)
	return template_allowlist, err
}

const getChatTitleGenerationModelOverride = `-- name: GetChatTitleGenerationModelOverride :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_title_generation_model_override'), '') :: text AS model_config_id
`

func (q *sqlQuerier) GetChatTitleGenerationModelOverride(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatTitleGenerationModelOverride)
	var model_config_id string
	err := row.Scan(&model_config_id)
	return model_config_id, err
}

const getChatWorkspaceTTL = `-- name: GetChatWorkspaceTTL :one
SELECT
    COALESCE(
        (SELECT value FROM site_configs WHERE key = 'agents_workspace_ttl'),
        '0s'
    )::text AS workspace_ttl
`

// Returns the global TTL for chat workspaces as a Go duration string.
// Returns "0s" (disabled) when no value has been configured.
func (q *sqlQuerier) GetChatWorkspaceTTL(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getChatWorkspaceTTL)
	var workspace_ttl string
	err := row.Scan(&workspace_ttl)
	return workspace_ttl, err
}

const getDERPMeshKey = `-- name: GetDERPMeshKey :one
SELECT value FROM site_configs WHERE key = 'derp_mesh_key'
`

func (q *sqlQuerier) GetDERPMeshKey(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getDERPMeshKey)
	var value string
	err := row.Scan(&value)
	return value, err
}

const getDefaultProxyConfig = `-- name: GetDefaultProxyConfig :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_display_name'), 'Default') :: text AS display_name,
	COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_icon_url'), '/emojis/1f3e1.png') :: text AS icon_url
`

type GetDefaultProxyConfigRow struct {
	DisplayName string `db:"display_name" json:"display_name"`
	IconURL     string `db:"icon_url" json:"icon_url"`
}

func (q *sqlQuerier) GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error) {
	row := q.db.QueryRowContext(ctx, getDefaultProxyConfig)
	var i GetDefaultProxyConfigRow
	err := row.Scan(&i.DisplayName, &i.IconURL)
	return i, err
}

const getDeploymentID = `-- name: GetDeploymentID :one
SELECT value FROM site_configs WHERE key = 'deployment_id'
`

func (q *sqlQuerier) GetDeploymentID(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getDeploymentID)
	var value string
	err := row.Scan(&value)
	return value, err
}

const getHealthSettings = `-- name: GetHealthSettings :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'health_settings'), '{}') :: text AS health_settings
`

func (q *sqlQuerier) GetHealthSettings(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getHealthSettings)
	var health_settings string
	err := row.Scan(&health_settings)
	return health_settings, err
}

const getLastUpdateCheck = `-- name: GetLastUpdateCheck :one
SELECT value FROM site_configs WHERE key = 'last_update_check'
`

func (q *sqlQuerier) GetLastUpdateCheck(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getLastUpdateCheck)
	var value string
	err := row.Scan(&value)
	return value, err
}

const getLogoURL = `-- name: GetLogoURL :one
SELECT value FROM site_configs WHERE key = 'logo_url'
`

func (q *sqlQuerier) GetLogoURL(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getLogoURL)
	var value string
	err := row.Scan(&value)
	return value, err
}

const getNotificationsSettings = `-- name: GetNotificationsSettings :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'notifications_settings'), '{}') :: text AS notifications_settings
`

func (q *sqlQuerier) GetNotificationsSettings(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getNotificationsSettings)
	var notifications_settings string
	err := row.Scan(&notifications_settings)
	return notifications_settings, err
}

const getOAuth2GithubDefaultEligible = `-- name: GetOAuth2GithubDefaultEligible :one
SELECT
	CASE
		WHEN value = 'true' THEN TRUE
		ELSE FALSE
	END
FROM site_configs
WHERE key = 'oauth2_github_default_eligible'
`

func (q *sqlQuerier) GetOAuth2GithubDefaultEligible(ctx context.Context) (bool, error) {
	row := q.db.QueryRowContext(ctx, getOAuth2GithubDefaultEligible)
	var column_1 bool
	err := row.Scan(&column_1)
	return column_1, err
}

const getPrebuildsSettings = `-- name: GetPrebuildsSettings :one
SELECT
	COALESCE((SELECT value FROM site_configs WHERE key = 'prebuilds_settings'), '{}') :: text AS prebuilds_settings
`

func (q *sqlQuerier) GetPrebuildsSettings(ctx context.Context) (string, error) {
	row := q.db.QueryRowContext(ctx, getPrebuildsSettings)
	var prebuilds_settings string
	err := row.Scan(&prebuilds_settings)
	return prebuilds_settings, err
}

const getRuntimeConfig = `-- name: GetRuntimeConfig :one
SELECT value FROM site_configs WHERE site_configs.key = $1
`

func (q *sqlQuerier) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
	row := q.db.QueryRowContext(ctx, getRuntimeConfig, key)
	var value string
	err := row.Scan(&value)
	return value, err
}

const getWebpushVAPIDKeys = `-- name: GetWebpushVAPIDKeys :one
SELECT
    COALESCE((SELECT value FROM site_configs WHERE key = 'webpush_vapid_public_key'), '') :: text AS vapid_public_key,
    COALESCE((SELECT value FROM site_configs WHERE key = 'webpush_vapid_private_key'), '') :: text AS vapid_private_key
`

type GetWebpushVAPIDKeysRow struct {
	VapidPublicKey  string `db:"vapid_public_key" json:"vapid_public_key"`
	VapidPrivateKey string `db:"vapid_private_key" json:"vapid_private_key"`
}

func (q *sqlQuerier) GetWebpushVAPIDKeys(ctx context.Context) (GetWebpushVAPIDKeysRow, error) {
	row := q.db.QueryRowContext(ctx, getWebpushVAPIDKeys)
	var i GetWebpushVAPIDKeysRow
	err := row.Scan(&i.VapidPublicKey, &i.VapidPrivateKey)
	return i, err
}

const insertDERPMeshKey = `-- name: InsertDERPMeshKey :exec
INSERT INTO site_configs (key, value) VALUES ('derp_mesh_key', $1)
`

func (q *sqlQuerier) InsertDERPMeshKey(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, insertDERPMeshKey, value)
	return err
}

const insertDeploymentID = `-- name: InsertDeploymentID :exec
INSERT INTO site_configs (key, value) VALUES ('deployment_id', $1)
`

func (q *sqlQuerier) InsertDeploymentID(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, insertDeploymentID, value)
	return err
}

const upsertAnnouncementBanners = `-- name: UpsertAnnouncementBanners :exec
INSERT INTO site_configs (key, value) VALUES ('announcement_banners', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'announcement_banners'
`

func (q *sqlQuerier) UpsertAnnouncementBanners(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertAnnouncementBanners, value)
	return err
}

const upsertApplicationName = `-- name: UpsertApplicationName :exec
INSERT INTO site_configs (key, value) VALUES ('application_name', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'application_name'
`

func (q *sqlQuerier) UpsertApplicationName(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertApplicationName, value)
	return err
}

const upsertChatAdvisorConfig = `-- name: UpsertChatAdvisorConfig :exec
INSERT INTO site_configs (key, value) VALUES ('agents_advisor_config', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_advisor_config'
`

// UpsertChatAdvisorConfig stores the deployment-wide runtime configuration
// for the experimental chat advisor. Callers marshal codersdk.AdvisorConfig
// to JSON before invoking this query.
func (q *sqlQuerier) UpsertChatAdvisorConfig(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertChatAdvisorConfig, value)
	return err
}

const upsertChatAutoArchiveDays = `-- name: UpsertChatAutoArchiveDays :exec
INSERT INTO site_configs (key, value)
VALUES ('agents_chat_auto_archive_days', CAST($1 AS integer)::text)
ON CONFLICT (key) DO UPDATE SET value = CAST($1 AS integer)::text
WHERE site_configs.key = 'agents_chat_auto_archive_days'
`

func (q *sqlQuerier) UpsertChatAutoArchiveDays(ctx context.Context, autoArchiveDays int32) error {
	_, err := q.db.ExecContext(ctx, upsertChatAutoArchiveDays, autoArchiveDays)
	return err
}

const upsertChatComputerUseProvider = `-- name: UpsertChatComputerUseProvider :exec
INSERT INTO site_configs (key, value) VALUES ('agents_computer_use_provider', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_computer_use_provider'
`

func (q *sqlQuerier) UpsertChatComputerUseProvider(ctx context.Context, provider string) error {
	_, err := q.db.ExecContext(ctx, upsertChatComputerUseProvider, provider)
	return err
}

const upsertChatDebugLoggingAllowUsers = `-- name: UpsertChatDebugLoggingAllowUsers :exec
INSERT INTO site_configs (key, value)
VALUES (
    'agents_chat_debug_logging_allow_users',
    CASE
        WHEN $1::bool THEN 'true'
        ELSE 'false'
    END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
    WHEN $1::bool THEN 'true'
    ELSE 'false'
END
WHERE site_configs.key = 'agents_chat_debug_logging_allow_users'
`

// UpsertChatDebugLoggingAllowUsers updates the runtime admin setting that
// allows users to opt into chat debug logging.
func (q *sqlQuerier) UpsertChatDebugLoggingAllowUsers(ctx context.Context, allowUsers bool) error {
	_, err := q.db.ExecContext(ctx, upsertChatDebugLoggingAllowUsers, allowUsers)
	return err
}

const upsertChatDebugRetentionDays = `-- name: UpsertChatDebugRetentionDays :exec
INSERT INTO site_configs (key, value)
VALUES ('agents_chat_debug_retention_days', CAST($1 AS integer)::text)
ON CONFLICT (key) DO UPDATE SET value = CAST($1 AS integer)::text
WHERE site_configs.key = 'agents_chat_debug_retention_days'
`

func (q *sqlQuerier) UpsertChatDebugRetentionDays(ctx context.Context, debugRetentionDays int32) error {
	_, err := q.db.ExecContext(ctx, upsertChatDebugRetentionDays, debugRetentionDays)
	return err
}

const upsertChatDesktopEnabled = `-- name: UpsertChatDesktopEnabled :exec
INSERT INTO site_configs (key, value)
VALUES (
    'agents_desktop_enabled',
    CASE
        WHEN $1::bool THEN 'true'
        ELSE 'false'
    END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
    WHEN $1::bool THEN 'true'
    ELSE 'false'
END
WHERE site_configs.key = 'agents_desktop_enabled'
`

func (q *sqlQuerier) UpsertChatDesktopEnabled(ctx context.Context, enableDesktop bool) error {
	_, err := q.db.ExecContext(ctx, upsertChatDesktopEnabled, enableDesktop)
	return err
}

const upsertChatExploreModelOverride = `-- name: UpsertChatExploreModelOverride :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_explore_model_override', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_explore_model_override'
`

func (q *sqlQuerier) UpsertChatExploreModelOverride(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertChatExploreModelOverride, value)
	return err
}

const upsertChatGeneralModelOverride = `-- name: UpsertChatGeneralModelOverride :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_general_model_override', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_general_model_override'
`

func (q *sqlQuerier) UpsertChatGeneralModelOverride(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertChatGeneralModelOverride, value)
	return err
}

const upsertChatIncludeDefaultSystemPrompt = `-- name: UpsertChatIncludeDefaultSystemPrompt :exec
INSERT INTO site_configs (key, value)
VALUES (
    'agents_chat_include_default_system_prompt',
    CASE
        WHEN $1::bool THEN 'true'
        ELSE 'false'
    END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
    WHEN $1::bool THEN 'true'
    ELSE 'false'
END
WHERE site_configs.key = 'agents_chat_include_default_system_prompt'
`

func (q *sqlQuerier) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error {
	_, err := q.db.ExecContext(ctx, upsertChatIncludeDefaultSystemPrompt, includeDefaultSystemPrompt)
	return err
}

const upsertChatPersonalModelOverridesEnabled = `-- name: UpsertChatPersonalModelOverridesEnabled :exec
INSERT INTO site_configs (key, value)
VALUES (
    'agents_chat_personal_model_overrides_enabled',
    CASE
        WHEN $1::bool THEN 'true'
        ELSE 'false'
    END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
    WHEN $1::bool THEN 'true'
    ELSE 'false'
END
WHERE site_configs.key = 'agents_chat_personal_model_overrides_enabled'
`

// UpsertChatPersonalModelOverridesEnabled updates whether users may configure
// personal chat model overrides.
func (q *sqlQuerier) UpsertChatPersonalModelOverridesEnabled(ctx context.Context, enabled bool) error {
	_, err := q.db.ExecContext(ctx, upsertChatPersonalModelOverridesEnabled, enabled)
	return err
}

const upsertChatPlanModeInstructions = `-- name: UpsertChatPlanModeInstructions :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_plan_mode_instructions', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_plan_mode_instructions'
`

func (q *sqlQuerier) UpsertChatPlanModeInstructions(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertChatPlanModeInstructions, value)
	return err
}

const upsertChatRetentionDays = `-- name: UpsertChatRetentionDays :exec
INSERT INTO site_configs (key, value)
VALUES ('agents_chat_retention_days', CAST($1 AS integer)::text)
ON CONFLICT (key) DO UPDATE SET value = CAST($1 AS integer)::text
WHERE site_configs.key = 'agents_chat_retention_days'
`

func (q *sqlQuerier) UpsertChatRetentionDays(ctx context.Context, retentionDays int32) error {
	_, err := q.db.ExecContext(ctx, upsertChatRetentionDays, retentionDays)
	return err
}

const upsertChatSystemPrompt = `-- name: UpsertChatSystemPrompt :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_system_prompt', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_system_prompt'
`

func (q *sqlQuerier) UpsertChatSystemPrompt(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertChatSystemPrompt, value)
	return err
}

const upsertChatTemplateAllowlist = `-- name: UpsertChatTemplateAllowlist :exec
INSERT INTO site_configs (key, value) VALUES ('agents_template_allowlist', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_template_allowlist'
`

func (q *sqlQuerier) UpsertChatTemplateAllowlist(ctx context.Context, templateAllowlist string) error {
	_, err := q.db.ExecContext(ctx, upsertChatTemplateAllowlist, templateAllowlist)
	return err
}

const upsertChatTitleGenerationModelOverride = `-- name: UpsertChatTitleGenerationModelOverride :exec
INSERT INTO site_configs (key, value) VALUES ('agents_chat_title_generation_model_override', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_title_generation_model_override'
`

func (q *sqlQuerier) UpsertChatTitleGenerationModelOverride(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertChatTitleGenerationModelOverride, value)
	return err
}

const upsertChatWorkspaceTTL = `-- name: UpsertChatWorkspaceTTL :exec
INSERT INTO site_configs (key, value)
VALUES ('agents_workspace_ttl', $1::text)
ON CONFLICT (key) DO UPDATE
SET value = $1::text
WHERE site_configs.key = 'agents_workspace_ttl'
`

func (q *sqlQuerier) UpsertChatWorkspaceTTL(ctx context.Context, workspaceTtl string) error {
	_, err := q.db.ExecContext(ctx, upsertChatWorkspaceTTL, workspaceTtl)
	return err
}

const upsertDefaultProxy = `-- name: UpsertDefaultProxy :exec
INSERT INTO site_configs (key, value)
VALUES
    ('default_proxy_display_name', $1 :: text),
    ('default_proxy_icon_url', $2 :: text)
ON CONFLICT
    (key)
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key
`

type UpsertDefaultProxyParams struct {
	DisplayName string `db:"display_name" json:"display_name"`
	IconURL     string `db:"icon_url" json:"icon_url"`
}

// The default proxy is implied and not actually stored in the database.
// So we need to store it's configuration here for display purposes.
// The functional values are immutable and controlled implicitly.
func (q *sqlQuerier) UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error {
	_, err := q.db.ExecContext(ctx, upsertDefaultProxy, arg.DisplayName, arg.IconURL)
	return err
}

const upsertHealthSettings = `-- name: UpsertHealthSettings :exec
INSERT INTO site_configs (key, value) VALUES ('health_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'health_settings'
`

func (q *sqlQuerier) UpsertHealthSettings(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertHealthSettings, value)
	return err
}

const upsertLastUpdateCheck = `-- name: UpsertLastUpdateCheck :exec
INSERT INTO site_configs (key, value) VALUES ('last_update_check', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'last_update_check'
`

func (q *sqlQuerier) UpsertLastUpdateCheck(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertLastUpdateCheck, value)
	return err
}

const upsertLogoURL = `-- name: UpsertLogoURL :exec
INSERT INTO site_configs (key, value) VALUES ('logo_url', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'logo_url'
`

func (q *sqlQuerier) UpsertLogoURL(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertLogoURL, value)
	return err
}

const upsertNotificationsSettings = `-- name: UpsertNotificationsSettings :exec
INSERT INTO site_configs (key, value) VALUES ('notifications_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'notifications_settings'
`

func (q *sqlQuerier) UpsertNotificationsSettings(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertNotificationsSettings, value)
	return err
}

const upsertOAuth2GithubDefaultEligible = `-- name: UpsertOAuth2GithubDefaultEligible :exec
INSERT INTO site_configs (key, value)
VALUES (
    'oauth2_github_default_eligible',
    CASE
        WHEN $1::bool THEN 'true'
        ELSE 'false'
    END
)
ON CONFLICT (key) DO UPDATE
SET value = CASE
    WHEN $1::bool THEN 'true'
    ELSE 'false'
END
WHERE site_configs.key = 'oauth2_github_default_eligible'
`

func (q *sqlQuerier) UpsertOAuth2GithubDefaultEligible(ctx context.Context, eligible bool) error {
	_, err := q.db.ExecContext(ctx, upsertOAuth2GithubDefaultEligible, eligible)
	return err
}

const upsertPrebuildsSettings = `-- name: UpsertPrebuildsSettings :exec
INSERT INTO site_configs (key, value) VALUES ('prebuilds_settings', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'prebuilds_settings'
`

func (q *sqlQuerier) UpsertPrebuildsSettings(ctx context.Context, value string) error {
	_, err := q.db.ExecContext(ctx, upsertPrebuildsSettings, value)
	return err
}

const upsertRuntimeConfig = `-- name: UpsertRuntimeConfig :exec
INSERT INTO site_configs (key, value) VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = $2 WHERE site_configs.key = $1
`

type UpsertRuntimeConfigParams struct {
	Key   string `db:"key" json:"key"`
	Value string `db:"value" json:"value"`
}

func (q *sqlQuerier) UpsertRuntimeConfig(ctx context.Context, arg UpsertRuntimeConfigParams) error {
	_, err := q.db.ExecContext(ctx, upsertRuntimeConfig, arg.Key, arg.Value)
	return err
}

const upsertWebpushVAPIDKeys = `-- name: UpsertWebpushVAPIDKeys :exec
INSERT INTO site_configs (key, value)
VALUES
    ('webpush_vapid_public_key', $1 :: text),
    ('webpush_vapid_private_key', $2 :: text)
ON CONFLICT (key)
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key
`

type UpsertWebpushVAPIDKeysParams struct {
	VapidPublicKey  string `db:"vapid_public_key" json:"vapid_public_key"`
	VapidPrivateKey string `db:"vapid_private_key" json:"vapid_private_key"`
}

func (q *sqlQuerier) UpsertWebpushVAPIDKeys(ctx context.Context, arg UpsertWebpushVAPIDKeysParams) error {
	_, err := q.db.ExecContext(ctx, upsertWebpushVAPIDKeys, arg.VapidPublicKey, arg.VapidPrivateKey)
	return err
}

const cleanTailnetCoordinators = `-- name: CleanTailnetCoordinators :exec
DELETE
FROM tailnet_coordinators
WHERE heartbeat_at < now() - INTERVAL '24 HOURS'
`

func (q *sqlQuerier) CleanTailnetCoordinators(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, cleanTailnetCoordinators)
	return err
}

const cleanTailnetLostPeers = `-- name: CleanTailnetLostPeers :exec
DELETE
FROM tailnet_peers
WHERE updated_at < now() - INTERVAL '24 HOURS' AND status = 'lost'::tailnet_status
`

func (q *sqlQuerier) CleanTailnetLostPeers(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, cleanTailnetLostPeers)
	return err
}

const cleanTailnetTunnels = `-- name: CleanTailnetTunnels :exec
DELETE FROM tailnet_tunnels
WHERE updated_at < now() - INTERVAL '24 HOURS' AND
      NOT EXISTS (
        SELECT 1 FROM tailnet_peers
        WHERE id = tailnet_tunnels.src_id AND coordinator_id = tailnet_tunnels.coordinator_id
      )
`

func (q *sqlQuerier) CleanTailnetTunnels(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, cleanTailnetTunnels)
	return err
}

const deleteAllTailnetTunnels = `-- name: DeleteAllTailnetTunnels :many
DELETE
FROM tailnet_tunnels
WHERE coordinator_id = $1 and src_id = $2
RETURNING src_id, dst_id
`

type DeleteAllTailnetTunnelsParams struct {
	CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
	SrcID         uuid.UUID `db:"src_id" json:"src_id"`
}

type DeleteAllTailnetTunnelsRow struct {
	SrcID uuid.UUID `db:"src_id" json:"src_id"`
	DstID uuid.UUID `db:"dst_id" json:"dst_id"`
}

func (q *sqlQuerier) DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) ([]DeleteAllTailnetTunnelsRow, error) {
	rows, err := q.db.QueryContext(ctx, deleteAllTailnetTunnels, arg.CoordinatorID, arg.SrcID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []DeleteAllTailnetTunnelsRow
	for rows.Next() {
		var i DeleteAllTailnetTunnelsRow
		if err := rows.Scan(&i.SrcID, &i.DstID); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteTailnetPeer = `-- name: DeleteTailnetPeer :one
DELETE
FROM tailnet_peers
WHERE id = $1 and coordinator_id = $2
RETURNING id, coordinator_id
`

type DeleteTailnetPeerParams struct {
	ID            uuid.UUID `db:"id" json:"id"`
	CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
}

type DeleteTailnetPeerRow struct {
	ID            uuid.UUID `db:"id" json:"id"`
	CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
}

func (q *sqlQuerier) DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error) {
	row := q.db.QueryRowContext(ctx, deleteTailnetPeer, arg.ID, arg.CoordinatorID)
	var i DeleteTailnetPeerRow
	err := row.Scan(&i.ID, &i.CoordinatorID)
	return i, err
}

const deleteTailnetTunnel = `-- name: DeleteTailnetTunnel :one
DELETE
FROM tailnet_tunnels
WHERE coordinator_id = $1 and src_id = $2 and dst_id = $3
RETURNING coordinator_id, src_id, dst_id
`

type DeleteTailnetTunnelParams struct {
	CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
	SrcID         uuid.UUID `db:"src_id" json:"src_id"`
	DstID         uuid.UUID `db:"dst_id" json:"dst_id"`
}

type DeleteTailnetTunnelRow struct {
	CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
	SrcID         uuid.UUID `db:"src_id" json:"src_id"`
	DstID         uuid.UUID `db:"dst_id" json:"dst_id"`
}

func (q *sqlQuerier) DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error) {
	row := q.db.QueryRowContext(ctx, deleteTailnetTunnel, arg.CoordinatorID, arg.SrcID, arg.DstID)
	var i DeleteTailnetTunnelRow
	err := row.Scan(&i.CoordinatorID, &i.SrcID, &i.DstID)
	return i, err
}

const getAllTailnetCoordinators = `-- name: GetAllTailnetCoordinators :many

SELECT id, heartbeat_at FROM tailnet_coordinators
`

// For PG Coordinator HTMLDebug
func (q *sqlQuerier) GetAllTailnetCoordinators(ctx context.Context) ([]TailnetCoordinator, error) {
	rows, err := q.db.QueryContext(ctx, getAllTailnetCoordinators)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TailnetCoordinator
	for rows.Next() {
		var i TailnetCoordinator
		if err := rows.Scan(&i.ID, &i.HeartbeatAt); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAllTailnetPeers = `-- name: GetAllTailnetPeers :many
SELECT id, coordinator_id, updated_at, node, status FROM tailnet_peers
`

func (q *sqlQuerier) GetAllTailnetPeers(ctx context.Context) ([]TailnetPeer, error) {
	rows, err := q.db.QueryContext(ctx, getAllTailnetPeers)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TailnetPeer
	for rows.Next() {
		var i TailnetPeer
		if err := rows.Scan(
			&i.ID,
			&i.CoordinatorID,
			&i.UpdatedAt,
			&i.Node,
			&i.Status,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getAllTailnetTunnels = `-- name: GetAllTailnetTunnels :many
SELECT coordinator_id, src_id, dst_id, updated_at FROM tailnet_tunnels
`

func (q *sqlQuerier) GetAllTailnetTunnels(ctx context.Context) ([]TailnetTunnel, error) {
	rows, err := q.db.QueryContext(ctx, getAllTailnetTunnels)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TailnetTunnel
	for rows.Next() {
		var i TailnetTunnel
		if err := rows.Scan(
			&i.CoordinatorID,
			&i.SrcID,
			&i.DstID,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTailnetPeers = `-- name: GetTailnetPeers :many
SELECT id, coordinator_id, updated_at, node, status FROM tailnet_peers WHERE id = $1
`

func (q *sqlQuerier) GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) {
	rows, err := q.db.QueryContext(ctx, getTailnetPeers, id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TailnetPeer
	for rows.Next() {
		var i TailnetPeer
		if err := rows.Scan(
			&i.ID,
			&i.CoordinatorID,
			&i.UpdatedAt,
			&i.Node,
			&i.Status,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTailnetTunnelPeerBindingsBatch = `-- name: GetTailnetTunnelPeerBindingsBatch :many
SELECT tp.id AS peer_id, tp.coordinator_id, tp.updated_at, tp.node, tp.status,
       tunnels.lookup_id
FROM (
  SELECT dst_id AS peer_id, src_id AS lookup_id
  FROM tailnet_tunnels WHERE src_id = ANY($1 :: uuid[])
  UNION
  SELECT src_id AS peer_id, dst_id AS lookup_id
  FROM tailnet_tunnels WHERE dst_id = ANY($1 :: uuid[])
) tunnels
INNER JOIN tailnet_peers tp ON tp.id = tunnels.peer_id
`

type GetTailnetTunnelPeerBindingsBatchRow struct {
	PeerID        uuid.UUID     `db:"peer_id" json:"peer_id"`
	CoordinatorID uuid.UUID     `db:"coordinator_id" json:"coordinator_id"`
	UpdatedAt     time.Time     `db:"updated_at" json:"updated_at"`
	Node          []byte        `db:"node" json:"node"`
	Status        TailnetStatus `db:"status" json:"status"`
	LookupID      uuid.UUID     `db:"lookup_id" json:"lookup_id"`
}

func (q *sqlQuerier) GetTailnetTunnelPeerBindingsBatch(ctx context.Context, ids []uuid.UUID) ([]GetTailnetTunnelPeerBindingsBatchRow, error) {
	rows, err := q.db.QueryContext(ctx, getTailnetTunnelPeerBindingsBatch, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTailnetTunnelPeerBindingsBatchRow
	for rows.Next() {
		var i GetTailnetTunnelPeerBindingsBatchRow
		if err := rows.Scan(
			&i.PeerID,
			&i.CoordinatorID,
			&i.UpdatedAt,
			&i.Node,
			&i.Status,
			&i.LookupID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTailnetTunnelPeerIDsBatch = `-- name: GetTailnetTunnelPeerIDsBatch :many
SELECT src_id AS lookup_id, dst_id AS peer_id, coordinator_id, updated_at
FROM tailnet_tunnels WHERE src_id = ANY($1 :: uuid[])
UNION ALL
SELECT dst_id AS lookup_id, src_id AS peer_id, coordinator_id, updated_at
FROM tailnet_tunnels WHERE dst_id = ANY($1 :: uuid[])
`

type GetTailnetTunnelPeerIDsBatchRow struct {
	LookupID      uuid.UUID `db:"lookup_id" json:"lookup_id"`
	PeerID        uuid.UUID `db:"peer_id" json:"peer_id"`
	CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
	UpdatedAt     time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) GetTailnetTunnelPeerIDsBatch(ctx context.Context, ids []uuid.UUID) ([]GetTailnetTunnelPeerIDsBatchRow, error) {
	rows, err := q.db.QueryContext(ctx, getTailnetTunnelPeerIDsBatch, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTailnetTunnelPeerIDsBatchRow
	for rows.Next() {
		var i GetTailnetTunnelPeerIDsBatchRow
		if err := rows.Scan(
			&i.LookupID,
			&i.PeerID,
			&i.CoordinatorID,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateTailnetPeerStatusByCoordinator = `-- name: UpdateTailnetPeerStatusByCoordinator :many
UPDATE
	tailnet_peers
SET
	status = $2
WHERE
	coordinator_id = $1
RETURNING id
`

type UpdateTailnetPeerStatusByCoordinatorParams struct {
	CoordinatorID uuid.UUID     `db:"coordinator_id" json:"coordinator_id"`
	Status        TailnetStatus `db:"status" json:"status"`
}

func (q *sqlQuerier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) ([]uuid.UUID, error) {
	rows, err := q.db.QueryContext(ctx, updateTailnetPeerStatusByCoordinator, arg.CoordinatorID, arg.Status)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []uuid.UUID
	for rows.Next() {
		var id uuid.UUID
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		items = append(items, id)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const upsertTailnetCoordinator = `-- name: UpsertTailnetCoordinator :one
INSERT INTO
	tailnet_coordinators (
	id,
	heartbeat_at
)
VALUES
	($1, now() at time zone 'utc')
ON CONFLICT (id)
DO UPDATE SET
  id = $1,
  heartbeat_at = now() at time zone 'utc'
RETURNING id, heartbeat_at
`

func (q *sqlQuerier) UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) {
	row := q.db.QueryRowContext(ctx, upsertTailnetCoordinator, id)
	var i TailnetCoordinator
	err := row.Scan(&i.ID, &i.HeartbeatAt)
	return i, err
}

const upsertTailnetPeer = `-- name: UpsertTailnetPeer :one
INSERT INTO
	tailnet_peers (
	id,
	coordinator_id,
	node,
	status,
	updated_at
)
VALUES
	($1, $2, $3, $4, now() at time zone 'utc')
ON CONFLICT (id, coordinator_id)
DO UPDATE SET
	id = $1,
	coordinator_id = $2,
	node = $3,
	status = $4,
	updated_at = now() at time zone 'utc'
RETURNING id, coordinator_id, updated_at, node, status
`

type UpsertTailnetPeerParams struct {
	ID            uuid.UUID     `db:"id" json:"id"`
	CoordinatorID uuid.UUID     `db:"coordinator_id" json:"coordinator_id"`
	Node          []byte        `db:"node" json:"node"`
	Status        TailnetStatus `db:"status" json:"status"`
}

func (q *sqlQuerier) UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) {
	row := q.db.QueryRowContext(ctx, upsertTailnetPeer,
		arg.ID,
		arg.CoordinatorID,
		arg.Node,
		arg.Status,
	)
	var i TailnetPeer
	err := row.Scan(
		&i.ID,
		&i.CoordinatorID,
		&i.UpdatedAt,
		&i.Node,
		&i.Status,
	)
	return i, err
}

const upsertTailnetTunnel = `-- name: UpsertTailnetTunnel :one
INSERT INTO
	tailnet_tunnels (
	coordinator_id,
	src_id,
	dst_id,
	updated_at
)
VALUES
	($1, $2, $3, now() at time zone 'utc')
ON CONFLICT (coordinator_id, src_id, dst_id)
DO UPDATE SET
	coordinator_id = $1,
	src_id = $2,
	dst_id = $3,
	updated_at = now() at time zone 'utc'
RETURNING coordinator_id, src_id, dst_id, updated_at
`

type UpsertTailnetTunnelParams struct {
	CoordinatorID uuid.UUID `db:"coordinator_id" json:"coordinator_id"`
	SrcID         uuid.UUID `db:"src_id" json:"src_id"`
	DstID         uuid.UUID `db:"dst_id" json:"dst_id"`
}

func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) {
	row := q.db.QueryRowContext(ctx, upsertTailnetTunnel, arg.CoordinatorID, arg.SrcID, arg.DstID)
	var i TailnetTunnel
	err := row.Scan(
		&i.CoordinatorID,
		&i.SrcID,
		&i.DstID,
		&i.UpdatedAt,
	)
	return i, err
}

const deleteTask = `-- name: DeleteTask :one
WITH deleted_task AS (
	UPDATE tasks
	SET
		deleted_at = $1::timestamptz
	WHERE
		id = $2::uuid
		AND deleted_at IS NULL
	RETURNING id
), deleted_snapshot AS (
	DELETE FROM task_snapshots
	WHERE task_id = $2::uuid
)
SELECT id FROM deleted_task
`

type DeleteTaskParams struct {
	DeletedAt time.Time `db:"deleted_at" json:"deleted_at"`
	ID        uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) DeleteTask(ctx context.Context, arg DeleteTaskParams) (uuid.UUID, error) {
	row := q.db.QueryRowContext(ctx, deleteTask, arg.DeletedAt, arg.ID)
	var id uuid.UUID
	err := row.Scan(&id)
	return id, err
}

const getTaskByID = `-- name: GetTaskByID :one
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE id = $1::uuid
`

func (q *sqlQuerier) GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error) {
	row := q.db.QueryRowContext(ctx, getTaskByID, id)
	var i Task
	err := row.Scan(
		&i.ID,
		&i.OrganizationID,
		&i.OwnerID,
		&i.Name,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.TemplateParameters,
		&i.Prompt,
		&i.CreatedAt,
		&i.DeletedAt,
		&i.DisplayName,
		&i.WorkspaceGroupACL,
		&i.WorkspaceUserACL,
		&i.Status,
		&i.StatusDebug,
		&i.WorkspaceBuildNumber,
		&i.WorkspaceAgentID,
		&i.WorkspaceAppID,
		&i.WorkspaceAgentLifecycleState,
		&i.WorkspaceAppHealth,
		&i.OwnerUsername,
		&i.OwnerName,
		&i.OwnerAvatarUrl,
	)
	return i, err
}

const getTaskByOwnerIDAndName = `-- name: GetTaskByOwnerIDAndName :one
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status
WHERE
	owner_id = $1::uuid
	AND deleted_at IS NULL
	AND LOWER(name) = LOWER($2::text)
`

type GetTaskByOwnerIDAndNameParams struct {
	OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
	Name    string    `db:"name" json:"name"`
}

func (q *sqlQuerier) GetTaskByOwnerIDAndName(ctx context.Context, arg GetTaskByOwnerIDAndNameParams) (Task, error) {
	row := q.db.QueryRowContext(ctx, getTaskByOwnerIDAndName, arg.OwnerID, arg.Name)
	var i Task
	err := row.Scan(
		&i.ID,
		&i.OrganizationID,
		&i.OwnerID,
		&i.Name,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.TemplateParameters,
		&i.Prompt,
		&i.CreatedAt,
		&i.DeletedAt,
		&i.DisplayName,
		&i.WorkspaceGroupACL,
		&i.WorkspaceUserACL,
		&i.Status,
		&i.StatusDebug,
		&i.WorkspaceBuildNumber,
		&i.WorkspaceAgentID,
		&i.WorkspaceAppID,
		&i.WorkspaceAgentLifecycleState,
		&i.WorkspaceAppHealth,
		&i.OwnerUsername,
		&i.OwnerName,
		&i.OwnerAvatarUrl,
	)
	return i, err
}

const getTaskByWorkspaceID = `-- name: GetTaskByWorkspaceID :one
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE workspace_id = $1::uuid
`

func (q *sqlQuerier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (Task, error) {
	row := q.db.QueryRowContext(ctx, getTaskByWorkspaceID, workspaceID)
	var i Task
	err := row.Scan(
		&i.ID,
		&i.OrganizationID,
		&i.OwnerID,
		&i.Name,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.TemplateParameters,
		&i.Prompt,
		&i.CreatedAt,
		&i.DeletedAt,
		&i.DisplayName,
		&i.WorkspaceGroupACL,
		&i.WorkspaceUserACL,
		&i.Status,
		&i.StatusDebug,
		&i.WorkspaceBuildNumber,
		&i.WorkspaceAgentID,
		&i.WorkspaceAppID,
		&i.WorkspaceAgentLifecycleState,
		&i.WorkspaceAppHealth,
		&i.OwnerUsername,
		&i.OwnerName,
		&i.OwnerAvatarUrl,
	)
	return i, err
}

const getTaskSnapshot = `-- name: GetTaskSnapshot :one
SELECT
	task_id, log_snapshot, log_snapshot_created_at
FROM
	task_snapshots
WHERE
	task_id = $1
`

func (q *sqlQuerier) GetTaskSnapshot(ctx context.Context, taskID uuid.UUID) (TaskSnapshot, error) {
	row := q.db.QueryRowContext(ctx, getTaskSnapshot, taskID)
	var i TaskSnapshot
	err := row.Scan(&i.TaskID, &i.LogSnapshot, &i.LogSnapshotCreatedAt)
	return i, err
}

const getTelemetryTaskEvents = `-- name: GetTelemetryTaskEvents :many
WITH task_app_ids AS (
	SELECT task_id, workspace_app_id
	FROM task_workspace_apps
),
task_status_timeline AS (
	-- All app statuses across every historical app for each task,
	-- plus synthetic "boundary" rows at each stop/start build transition.
	-- This allows us to correctly take gaps due to pause/resume into account.
	SELECT tai.task_id, was.created_at, was.state::text AS state
	FROM workspace_app_statuses was
	JOIN task_app_ids tai ON tai.workspace_app_id = was.app_id
	UNION ALL
	SELECT t.id AS task_id, wb.created_at, '_boundary' AS state
	FROM tasks t
	JOIN workspace_builds wb ON wb.workspace_id = t.workspace_id
	WHERE t.deleted_at IS NULL
		AND t.workspace_id IS NOT NULL
		AND wb.build_number > 1
),
task_event_data AS (
	SELECT
		t.id AS task_id,
		t.workspace_id,
		twa.workspace_app_id,
		-- Latest stop build.
		stop_build.created_at AS stop_build_created_at,
		stop_build.reason AS stop_build_reason,
		-- Latest start build (task_resume only).
		start_build.created_at AS start_build_created_at,
		start_build.reason AS start_build_reason,
		start_build.build_number AS start_build_number,
		-- Last "working" app status (for idle duration).
		lws.created_at AS last_working_status_at,
		-- First app status after resume (for resume-to-status duration).
		-- Only populated for workspaces in an active phase (started more
		-- recently than stopped).
		fsar.created_at AS first_status_after_resume_at,
		-- Cumulative time spent in "working" state.
		active_dur.total_working_ms AS active_duration_ms
	FROM tasks t
	LEFT JOIN LATERAL (
		SELECT task_app.workspace_app_id
		FROM task_workspace_apps task_app
		WHERE task_app.task_id = t.id
		ORDER BY task_app.workspace_build_number DESC
		LIMIT 1
	) twa ON TRUE
	LEFT JOIN LATERAL (
		SELECT wb.created_at, wb.reason, wb.build_number
		FROM workspace_builds wb
		WHERE wb.workspace_id = t.workspace_id
			AND wb.transition = 'stop'
		ORDER BY wb.build_number DESC
		LIMIT 1
	) stop_build ON TRUE
	LEFT JOIN LATERAL (
		SELECT wb.created_at, wb.reason, wb.build_number
		FROM workspace_builds wb
		WHERE wb.workspace_id = t.workspace_id
			AND wb.transition = 'start'
		ORDER BY wb.build_number DESC
		LIMIT 1
	) start_build ON TRUE
	LEFT JOIN LATERAL (
		SELECT tst.created_at
		FROM task_status_timeline tst
		WHERE tst.task_id = t.id
			AND tst.state = 'working'
		-- Only consider status before the latest pause so that
		-- post-resume statuses don't mask pre-pause idle time.
		AND (stop_build.created_at IS NULL
			OR tst.created_at <= stop_build.created_at)
		ORDER BY tst.created_at DESC
		LIMIT 1
	) lws ON TRUE
	LEFT JOIN LATERAL (
		SELECT was.created_at
		FROM workspace_app_statuses was
		WHERE was.app_id = twa.workspace_app_id
			AND was.created_at > start_build.created_at
		ORDER BY was.created_at ASC
		LIMIT 1
	) fsar ON twa.workspace_app_id IS NOT NULL
		AND start_build.created_at IS NOT NULL
		AND (stop_build.created_at IS NULL
			OR start_build.created_at > stop_build.created_at)
	-- Active duration: cumulative time spent in "working" state across all
	-- historical app IDs for this task. Uses LEAD() to convert point-in-time
	-- statuses into intervals, then sums intervals where state='working'. For
	-- the last status, falls back to stop_build time (if paused) or @now (if
	-- still running).
	LEFT JOIN LATERAL (
		SELECT COALESCE(
			SUM(EXTRACT(EPOCH FROM (interval_end - interval_start)) * 1000)::bigint,
			0
		)::bigint AS total_working_ms
		FROM (
			SELECT
				tst.created_at AS interval_start,
				COALESCE(
					LEAD(tst.created_at) OVER (ORDER BY tst.created_at ASC, CASE WHEN tst.state = '_boundary' THEN 1 ELSE 0 END ASC),
					CASE WHEN stop_build.created_at IS NOT NULL
						AND (start_build.created_at IS NULL
							OR stop_build.created_at > start_build.created_at)
					THEN stop_build.created_at
					ELSE $1::timestamptz
					END
				) AS interval_end,
				tst.state
			FROM task_status_timeline tst
			WHERE tst.task_id = t.id
		) intervals
		WHERE intervals.state = 'working'
	) active_dur ON TRUE
	WHERE t.deleted_at IS NULL
		AND t.workspace_id IS NOT NULL
		AND EXISTS (
			SELECT 1 FROM workspace_builds wb
			WHERE wb.workspace_id = t.workspace_id
			  AND wb.created_at > $2
		)
)
SELECT task_id, workspace_id, workspace_app_id, stop_build_created_at, stop_build_reason, start_build_created_at, start_build_reason, start_build_number, last_working_status_at, first_status_after_resume_at, active_duration_ms FROM task_event_data
ORDER BY task_id
`

type GetTelemetryTaskEventsParams struct {
	Now          time.Time `db:"now" json:"now"`
	CreatedAfter time.Time `db:"created_after" json:"created_after"`
}

type GetTelemetryTaskEventsRow struct {
	TaskID                   uuid.UUID       `db:"task_id" json:"task_id"`
	WorkspaceID              uuid.NullUUID   `db:"workspace_id" json:"workspace_id"`
	WorkspaceAppID           uuid.NullUUID   `db:"workspace_app_id" json:"workspace_app_id"`
	StopBuildCreatedAt       sql.NullTime    `db:"stop_build_created_at" json:"stop_build_created_at"`
	StopBuildReason          NullBuildReason `db:"stop_build_reason" json:"stop_build_reason"`
	StartBuildCreatedAt      sql.NullTime    `db:"start_build_created_at" json:"start_build_created_at"`
	StartBuildReason         NullBuildReason `db:"start_build_reason" json:"start_build_reason"`
	StartBuildNumber         sql.NullInt32   `db:"start_build_number" json:"start_build_number"`
	LastWorkingStatusAt      sql.NullTime    `db:"last_working_status_at" json:"last_working_status_at"`
	FirstStatusAfterResumeAt sql.NullTime    `db:"first_status_after_resume_at" json:"first_status_after_resume_at"`
	ActiveDurationMs         int64           `db:"active_duration_ms" json:"active_duration_ms"`
}

// Returns all data needed to build task lifecycle events for telemetry
// in a single round-trip. For each task whose workspace is in the
// given set, fetches:
//   - the latest workspace app binding (task_workspace_apps)
//   - the most recent stop and start builds (workspace_builds)
//   - the last "working" app status (workspace_app_statuses)
//   - the first app status after resume, for active workspaces
//
// Assumptions:
//   - 1:1 relationship between tasks and workspaces. All builds on the
//     workspace are considered task-related.
//   - Idle duration approximation: If the agent reports "working", does
//     work, then reports "done", we miss that working time.
//   - lws and active_dur join across all historical app IDs for the task,
//     because each resume cycle provisions a new app ID. This ensures
//     pre-pause statuses contribute to idle duration and active duration.
func (q *sqlQuerier) GetTelemetryTaskEvents(ctx context.Context, arg GetTelemetryTaskEventsParams) ([]GetTelemetryTaskEventsRow, error) {
	rows, err := q.db.QueryContext(ctx, getTelemetryTaskEvents, arg.Now, arg.CreatedAfter)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTelemetryTaskEventsRow
	for rows.Next() {
		var i GetTelemetryTaskEventsRow
		if err := rows.Scan(
			&i.TaskID,
			&i.WorkspaceID,
			&i.WorkspaceAppID,
			&i.StopBuildCreatedAt,
			&i.StopBuildReason,
			&i.StartBuildCreatedAt,
			&i.StartBuildReason,
			&i.StartBuildNumber,
			&i.LastWorkingStatusAt,
			&i.FirstStatusAfterResumeAt,
			&i.ActiveDurationMs,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertTask = `-- name: InsertTask :one
INSERT INTO tasks
	(id, organization_id, owner_id, name, display_name, workspace_id, template_version_id, template_parameters, prompt, created_at)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name
`

type InsertTaskParams struct {
	ID                 uuid.UUID       `db:"id" json:"id"`
	OrganizationID     uuid.UUID       `db:"organization_id" json:"organization_id"`
	OwnerID            uuid.UUID       `db:"owner_id" json:"owner_id"`
	Name               string          `db:"name" json:"name"`
	DisplayName        string          `db:"display_name" json:"display_name"`
	WorkspaceID        uuid.NullUUID   `db:"workspace_id" json:"workspace_id"`
	TemplateVersionID  uuid.UUID       `db:"template_version_id" json:"template_version_id"`
	TemplateParameters json.RawMessage `db:"template_parameters" json:"template_parameters"`
	Prompt             string          `db:"prompt" json:"prompt"`
	CreatedAt          time.Time       `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) InsertTask(ctx context.Context, arg InsertTaskParams) (TaskTable, error) {
	row := q.db.QueryRowContext(ctx, insertTask,
		arg.ID,
		arg.OrganizationID,
		arg.OwnerID,
		arg.Name,
		arg.DisplayName,
		arg.WorkspaceID,
		arg.TemplateVersionID,
		arg.TemplateParameters,
		arg.Prompt,
		arg.CreatedAt,
	)
	var i TaskTable
	err := row.Scan(
		&i.ID,
		&i.OrganizationID,
		&i.OwnerID,
		&i.Name,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.TemplateParameters,
		&i.Prompt,
		&i.CreatedAt,
		&i.DeletedAt,
		&i.DisplayName,
	)
	return i, err
}

const listTasks = `-- name: ListTasks :many
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status tws
WHERE tws.deleted_at IS NULL
AND CASE WHEN $1::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.owner_id = $1::UUID ELSE TRUE END
AND CASE WHEN $2::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.organization_id = $2::UUID ELSE TRUE END
AND CASE WHEN $3::text != '' THEN tws.status = $3::task_status ELSE TRUE END
ORDER BY tws.created_at DESC
`

type ListTasksParams struct {
	OwnerID        uuid.UUID `db:"owner_id" json:"owner_id"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	Status         string    `db:"status" json:"status"`
}

func (q *sqlQuerier) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task, error) {
	rows, err := q.db.QueryContext(ctx, listTasks, arg.OwnerID, arg.OrganizationID, arg.Status)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Task
	for rows.Next() {
		var i Task
		if err := rows.Scan(
			&i.ID,
			&i.OrganizationID,
			&i.OwnerID,
			&i.Name,
			&i.WorkspaceID,
			&i.TemplateVersionID,
			&i.TemplateParameters,
			&i.Prompt,
			&i.CreatedAt,
			&i.DeletedAt,
			&i.DisplayName,
			&i.WorkspaceGroupACL,
			&i.WorkspaceUserACL,
			&i.Status,
			&i.StatusDebug,
			&i.WorkspaceBuildNumber,
			&i.WorkspaceAgentID,
			&i.WorkspaceAppID,
			&i.WorkspaceAgentLifecycleState,
			&i.WorkspaceAppHealth,
			&i.OwnerUsername,
			&i.OwnerName,
			&i.OwnerAvatarUrl,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateTaskPrompt = `-- name: UpdateTaskPrompt :one
UPDATE
	tasks
SET
	prompt = $1::text
WHERE
	id = $2::uuid
	AND deleted_at IS NULL
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name
`

type UpdateTaskPromptParams struct {
	Prompt string    `db:"prompt" json:"prompt"`
	ID     uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error) {
	row := q.db.QueryRowContext(ctx, updateTaskPrompt, arg.Prompt, arg.ID)
	var i TaskTable
	err := row.Scan(
		&i.ID,
		&i.OrganizationID,
		&i.OwnerID,
		&i.Name,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.TemplateParameters,
		&i.Prompt,
		&i.CreatedAt,
		&i.DeletedAt,
		&i.DisplayName,
	)
	return i, err
}

const updateTaskWorkspaceID = `-- name: UpdateTaskWorkspaceID :one
UPDATE
	tasks
SET
	workspace_id = $2
FROM
	workspaces w
JOIN
	template_versions tv
ON
	tv.template_id = w.template_id
WHERE
	tasks.id = $1
	AND tasks.workspace_id IS NULL
	AND w.id = $2
	AND tv.id = tasks.template_version_id
RETURNING
	tasks.id, tasks.organization_id, tasks.owner_id, tasks.name, tasks.workspace_id, tasks.template_version_id, tasks.template_parameters, tasks.prompt, tasks.created_at, tasks.deleted_at, tasks.display_name
`

type UpdateTaskWorkspaceIDParams struct {
	ID          uuid.UUID     `db:"id" json:"id"`
	WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
}

func (q *sqlQuerier) UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWorkspaceIDParams) (TaskTable, error) {
	row := q.db.QueryRowContext(ctx, updateTaskWorkspaceID, arg.ID, arg.WorkspaceID)
	var i TaskTable
	err := row.Scan(
		&i.ID,
		&i.OrganizationID,
		&i.OwnerID,
		&i.Name,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.TemplateParameters,
		&i.Prompt,
		&i.CreatedAt,
		&i.DeletedAt,
		&i.DisplayName,
	)
	return i, err
}

const upsertTaskSnapshot = `-- name: UpsertTaskSnapshot :exec
INSERT INTO
	task_snapshots (task_id, log_snapshot, log_snapshot_created_at)
VALUES
	($1, $2, $3)
ON CONFLICT
	(task_id)
DO UPDATE SET
	log_snapshot = EXCLUDED.log_snapshot,
	log_snapshot_created_at = EXCLUDED.log_snapshot_created_at
`

type UpsertTaskSnapshotParams struct {
	TaskID               uuid.UUID       `db:"task_id" json:"task_id"`
	LogSnapshot          json.RawMessage `db:"log_snapshot" json:"log_snapshot"`
	LogSnapshotCreatedAt time.Time       `db:"log_snapshot_created_at" json:"log_snapshot_created_at"`
}

func (q *sqlQuerier) UpsertTaskSnapshot(ctx context.Context, arg UpsertTaskSnapshotParams) error {
	_, err := q.db.ExecContext(ctx, upsertTaskSnapshot, arg.TaskID, arg.LogSnapshot, arg.LogSnapshotCreatedAt)
	return err
}

const upsertTaskWorkspaceApp = `-- name: UpsertTaskWorkspaceApp :one
INSERT INTO task_workspace_apps
	(task_id, workspace_build_number, workspace_agent_id, workspace_app_id)
VALUES
	($1, $2, $3, $4)
ON CONFLICT (task_id, workspace_build_number)
DO UPDATE SET
	workspace_agent_id = EXCLUDED.workspace_agent_id,
	workspace_app_id = EXCLUDED.workspace_app_id
RETURNING task_id, workspace_agent_id, workspace_app_id, workspace_build_number
`

type UpsertTaskWorkspaceAppParams struct {
	TaskID               uuid.UUID     `db:"task_id" json:"task_id"`
	WorkspaceBuildNumber int32         `db:"workspace_build_number" json:"workspace_build_number"`
	WorkspaceAgentID     uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"`
	WorkspaceAppID       uuid.NullUUID `db:"workspace_app_id" json:"workspace_app_id"`
}

func (q *sqlQuerier) UpsertTaskWorkspaceApp(ctx context.Context, arg UpsertTaskWorkspaceAppParams) (TaskWorkspaceApp, error) {
	row := q.db.QueryRowContext(ctx, upsertTaskWorkspaceApp,
		arg.TaskID,
		arg.WorkspaceBuildNumber,
		arg.WorkspaceAgentID,
		arg.WorkspaceAppID,
	)
	var i TaskWorkspaceApp
	err := row.Scan(
		&i.TaskID,
		&i.WorkspaceAgentID,
		&i.WorkspaceAppID,
		&i.WorkspaceBuildNumber,
	)
	return i, err
}

const getTelemetryItem = `-- name: GetTelemetryItem :one
SELECT key, value, created_at, updated_at FROM telemetry_items WHERE key = $1
`

func (q *sqlQuerier) GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error) {
	row := q.db.QueryRowContext(ctx, getTelemetryItem, key)
	var i TelemetryItem
	err := row.Scan(
		&i.Key,
		&i.Value,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getTelemetryItems = `-- name: GetTelemetryItems :many
SELECT key, value, created_at, updated_at FROM telemetry_items
`

func (q *sqlQuerier) GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error) {
	rows, err := q.db.QueryContext(ctx, getTelemetryItems)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TelemetryItem
	for rows.Next() {
		var i TelemetryItem
		if err := rows.Scan(
			&i.Key,
			&i.Value,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertTelemetryItemIfNotExists = `-- name: InsertTelemetryItemIfNotExists :exec
INSERT INTO telemetry_items (key, value)
VALUES ($1, $2)
ON CONFLICT (key) DO NOTHING
`

type InsertTelemetryItemIfNotExistsParams struct {
	Key   string `db:"key" json:"key"`
	Value string `db:"value" json:"value"`
}

func (q *sqlQuerier) InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error {
	_, err := q.db.ExecContext(ctx, insertTelemetryItemIfNotExists, arg.Key, arg.Value)
	return err
}

const upsertTelemetryItem = `-- name: UpsertTelemetryItem :exec
INSERT INTO telemetry_items (key, value)
VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW() WHERE telemetry_items.key = $1
`

type UpsertTelemetryItemParams struct {
	Key   string `db:"key" json:"key"`
	Value string `db:"value" json:"value"`
}

func (q *sqlQuerier) UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error {
	_, err := q.db.ExecContext(ctx, upsertTelemetryItem, arg.Key, arg.Value)
	return err
}

const deleteOldTelemetryLocks = `-- name: DeleteOldTelemetryLocks :exec
DELETE FROM
    telemetry_locks
WHERE
    period_ending_at < $1::timestamptz
`

// Deletes old telemetry locks from the telemetry_locks table.
func (q *sqlQuerier) DeleteOldTelemetryLocks(ctx context.Context, periodEndingAtBefore time.Time) error {
	_, err := q.db.ExecContext(ctx, deleteOldTelemetryLocks, periodEndingAtBefore)
	return err
}

const insertTelemetryLock = `-- name: InsertTelemetryLock :exec
INSERT INTO
    telemetry_locks (event_type, period_ending_at)
VALUES
    ($1, $2)
`

type InsertTelemetryLockParams struct {
	EventType      string    `db:"event_type" json:"event_type"`
	PeriodEndingAt time.Time `db:"period_ending_at" json:"period_ending_at"`
}

// Inserts a new lock row into the telemetry_locks table. Replicas should call
// this function prior to attempting to generate or publish a heartbeat event to
// the telemetry service.
// If the query returns a duplicate primary key error, the replica should not
// attempt to generate or publish the event to the telemetry service.
func (q *sqlQuerier) InsertTelemetryLock(ctx context.Context, arg InsertTelemetryLockParams) error {
	_, err := q.db.ExecContext(ctx, insertTelemetryLock, arg.EventType, arg.PeriodEndingAt)
	return err
}

const getTemplateAverageBuildTime = `-- name: GetTemplateAverageBuildTime :one
WITH build_times AS (
SELECT
	EXTRACT(EPOCH FROM (pj.completed_at - pj.started_at))::FLOAT AS exec_time_sec,
	workspace_builds.transition
FROM
	workspace_builds
JOIN template_versions ON
	workspace_builds.template_version_id = template_versions.id
JOIN provisioner_jobs pj ON
	workspace_builds.job_id = pj.id
WHERE
	template_versions.template_id = $1 AND
		(pj.completed_at IS NOT NULL) AND (pj.started_at IS NOT NULL) AND
		(pj.canceled_at IS NULL) AND
		((pj.error IS NULL) OR (pj.error = ''))
ORDER BY
	workspace_builds.created_at DESC
LIMIT 100
)
SELECT
	-- Postgres offers no clear way to DRY this short of a function or other
	-- complexities.
	coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_50,
	coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_50,
	coalesce((PERCENTILE_DISC(0.5) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_50,
	coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'start')), -1)::FLOAT AS start_95,
	coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'stop')), -1)::FLOAT AS stop_95,
	coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_95
FROM build_times
`

type GetTemplateAverageBuildTimeRow struct {
	Start50  float64 `db:"start_50" json:"start_50"`
	Stop50   float64 `db:"stop_50" json:"stop_50"`
	Delete50 float64 `db:"delete_50" json:"delete_50"`
	Start95  float64 `db:"start_95" json:"start_95"`
	Stop95   float64 `db:"stop_95" json:"stop_95"`
	Delete95 float64 `db:"delete_95" json:"delete_95"`
}

func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, templateID uuid.NullUUID) (GetTemplateAverageBuildTimeRow, error) {
	row := q.db.QueryRowContext(ctx, getTemplateAverageBuildTime, templateID)
	var i GetTemplateAverageBuildTimeRow
	err := row.Scan(
		&i.Start50,
		&i.Stop50,
		&i.Delete50,
		&i.Start95,
		&i.Stop95,
		&i.Delete95,
	)
	return i, err
}

const getTemplateByID = `-- name: GetTemplateByID :one
SELECT
	id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
FROM
	template_with_names
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error) {
	row := q.db.QueryRowContext(ctx, getTemplateByID, id)
	var i Template
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OrganizationID,
		&i.Deleted,
		&i.Name,
		&i.Provisioner,
		&i.ActiveVersionID,
		&i.Description,
		&i.DefaultTTL,
		&i.CreatedBy,
		&i.Icon,
		&i.UserACL,
		&i.GroupACL,
		&i.DisplayName,
		&i.AllowUserCancelWorkspaceJobs,
		&i.AllowUserAutostart,
		&i.AllowUserAutostop,
		&i.FailureTTL,
		&i.TimeTilDormant,
		&i.TimeTilDormantAutoDelete,
		&i.AutostopRequirementDaysOfWeek,
		&i.AutostopRequirementWeeks,
		&i.AutostartBlockDaysOfWeek,
		&i.RequireActiveVersion,
		&i.Deprecated,
		&i.ActivityBump,
		&i.MaxPortSharingLevel,
		&i.UseClassicParameterFlow,
		&i.CorsBehavior,
		&i.DisableModuleCache,
		&i.CreatedByAvatarURL,
		&i.CreatedByUsername,
		&i.CreatedByName,
		&i.OrganizationName,
		&i.OrganizationDisplayName,
		&i.OrganizationIcon,
	)
	return i, err
}

const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT
	id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
FROM
	template_with_names AS templates
WHERE
	organization_id = $1
	AND deleted = $2
	AND LOWER("name") = LOWER($3)
LIMIT
	1
`

type GetTemplateByOrganizationAndNameParams struct {
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
	Deleted        bool      `db:"deleted" json:"deleted"`
	Name           string    `db:"name" json:"name"`
}

func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error) {
	row := q.db.QueryRowContext(ctx, getTemplateByOrganizationAndName, arg.OrganizationID, arg.Deleted, arg.Name)
	var i Template
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OrganizationID,
		&i.Deleted,
		&i.Name,
		&i.Provisioner,
		&i.ActiveVersionID,
		&i.Description,
		&i.DefaultTTL,
		&i.CreatedBy,
		&i.Icon,
		&i.UserACL,
		&i.GroupACL,
		&i.DisplayName,
		&i.AllowUserCancelWorkspaceJobs,
		&i.AllowUserAutostart,
		&i.AllowUserAutostop,
		&i.FailureTTL,
		&i.TimeTilDormant,
		&i.TimeTilDormantAutoDelete,
		&i.AutostopRequirementDaysOfWeek,
		&i.AutostopRequirementWeeks,
		&i.AutostartBlockDaysOfWeek,
		&i.RequireActiveVersion,
		&i.Deprecated,
		&i.ActivityBump,
		&i.MaxPortSharingLevel,
		&i.UseClassicParameterFlow,
		&i.CorsBehavior,
		&i.DisableModuleCache,
		&i.CreatedByAvatarURL,
		&i.CreatedByUsername,
		&i.CreatedByName,
		&i.OrganizationName,
		&i.OrganizationDisplayName,
		&i.OrganizationIcon,
	)
	return i, err
}

const getTemplates = `-- name: GetTemplates :many
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
ORDER BY (name, id) ASC
`

func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
	rows, err := q.db.QueryContext(ctx, getTemplates)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Template
	for rows.Next() {
		var i Template
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.OrganizationID,
			&i.Deleted,
			&i.Name,
			&i.Provisioner,
			&i.ActiveVersionID,
			&i.Description,
			&i.DefaultTTL,
			&i.CreatedBy,
			&i.Icon,
			&i.UserACL,
			&i.GroupACL,
			&i.DisplayName,
			&i.AllowUserCancelWorkspaceJobs,
			&i.AllowUserAutostart,
			&i.AllowUserAutostop,
			&i.FailureTTL,
			&i.TimeTilDormant,
			&i.TimeTilDormantAutoDelete,
			&i.AutostopRequirementDaysOfWeek,
			&i.AutostopRequirementWeeks,
			&i.AutostartBlockDaysOfWeek,
			&i.RequireActiveVersion,
			&i.Deprecated,
			&i.ActivityBump,
			&i.MaxPortSharingLevel,
			&i.UseClassicParameterFlow,
			&i.CorsBehavior,
			&i.DisableModuleCache,
			&i.CreatedByAvatarURL,
			&i.CreatedByUsername,
			&i.CreatedByName,
			&i.OrganizationName,
			&i.OrganizationDisplayName,
			&i.OrganizationIcon,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
SELECT
	t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
FROM
	template_with_names AS t
LEFT JOIN
	template_versions tv ON t.active_version_id = tv.id
WHERE
	-- Optionally include deleted templates
	t.deleted = $1
	-- Filter by organization_id
	AND CASE
		WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			t.organization_id = $2
		ELSE true
	END
	-- Filter by exact name
	AND CASE
		WHEN $3 :: text != '' THEN
			LOWER(t.name) = LOWER($3)
		ELSE true
	END
	-- Filter by exact display name
	AND CASE
		WHEN $4 :: text != '' THEN
			LOWER(t.display_name) = LOWER($4)
		ELSE true
	END
	-- Filter by name, matching on substring
	AND CASE
		WHEN $5 :: text != '' THEN
			lower(t.name) ILIKE '%' || lower($5) || '%'
		ELSE true
	END
	-- Filter by display_name, matching on substring (fallback to name if display_name is empty)
	AND CASE
		WHEN $6 :: text != '' THEN
			CASE
				WHEN t.display_name IS NOT NULL AND t.display_name != '' THEN
					lower(t.display_name) ILIKE '%' || lower($6) || '%'
				ELSE
					-- Remove spaces if present since 't.name' cannot have any spaces
					lower(t.name) ILIKE '%' || REPLACE(lower($6), ' ', '') || '%'
			END
		ELSE true
	END
	-- Filter by ids
	AND CASE
		WHEN array_length($7 :: uuid[], 1) > 0 THEN
			t.id = ANY($7)
		ELSE true
	END
	-- Filter by deprecated
	AND CASE
		WHEN $8 :: boolean IS NOT NULL THEN
			CASE
				WHEN $8 :: boolean THEN
					t.deprecated != ''
				ELSE
					t.deprecated = ''
			END
		ELSE true
	END
	-- Filter by has_ai_task in latest version
	AND CASE
		WHEN $9 :: boolean IS NOT NULL THEN
			tv.has_ai_task = $9 :: boolean
		ELSE true
	END
	-- Filter by author_id
	AND CASE
		  WHEN $10 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			  t.created_by = $10
		  ELSE true
	END
	-- Filter by author_username
	AND CASE
		  WHEN $11 :: text != '' THEN
			  t.created_by = (SELECT id FROM users WHERE lower(users.username) = lower($11) AND deleted = false)
		  ELSE true
	END

	-- Filter by has_external_agent in latest version
	AND CASE
		WHEN $12 :: boolean IS NOT NULL THEN
			tv.has_external_agent = $12 :: boolean
		ELSE true
	END
  -- Authorize Filter clause will be injected below in GetAuthorizedTemplates
  -- @authorize_filter
ORDER BY (t.name, t.id) ASC
`

type GetTemplatesWithFilterParams struct {
	Deleted          bool         `db:"deleted" json:"deleted"`
	OrganizationID   uuid.UUID    `db:"organization_id" json:"organization_id"`
	ExactName        string       `db:"exact_name" json:"exact_name"`
	ExactDisplayName string       `db:"exact_display_name" json:"exact_display_name"`
	FuzzyName        string       `db:"fuzzy_name" json:"fuzzy_name"`
	FuzzyDisplayName string       `db:"fuzzy_display_name" json:"fuzzy_display_name"`
	IDs              []uuid.UUID  `db:"ids" json:"ids"`
	Deprecated       sql.NullBool `db:"deprecated" json:"deprecated"`
	HasAITask        sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
	AuthorID         uuid.UUID    `db:"author_id" json:"author_id"`
	AuthorUsername   string       `db:"author_username" json:"author_username"`
	HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
}

func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) {
	rows, err := q.db.QueryContext(ctx, getTemplatesWithFilter,
		arg.Deleted,
		arg.OrganizationID,
		arg.ExactName,
		arg.ExactDisplayName,
		arg.FuzzyName,
		arg.FuzzyDisplayName,
		pq.Array(arg.IDs),
		arg.Deprecated,
		arg.HasAITask,
		arg.AuthorID,
		arg.AuthorUsername,
		arg.HasExternalAgent,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []Template
	for rows.Next() {
		var i Template
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.OrganizationID,
			&i.Deleted,
			&i.Name,
			&i.Provisioner,
			&i.ActiveVersionID,
			&i.Description,
			&i.DefaultTTL,
			&i.CreatedBy,
			&i.Icon,
			&i.UserACL,
			&i.GroupACL,
			&i.DisplayName,
			&i.AllowUserCancelWorkspaceJobs,
			&i.AllowUserAutostart,
			&i.AllowUserAutostop,
			&i.FailureTTL,
			&i.TimeTilDormant,
			&i.TimeTilDormantAutoDelete,
			&i.AutostopRequirementDaysOfWeek,
			&i.AutostopRequirementWeeks,
			&i.AutostartBlockDaysOfWeek,
			&i.RequireActiveVersion,
			&i.Deprecated,
			&i.ActivityBump,
			&i.MaxPortSharingLevel,
			&i.UseClassicParameterFlow,
			&i.CorsBehavior,
			&i.DisableModuleCache,
			&i.CreatedByAvatarURL,
			&i.CreatedByUsername,
			&i.CreatedByName,
			&i.OrganizationName,
			&i.OrganizationDisplayName,
			&i.OrganizationIcon,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertTemplate = `-- name: InsertTemplate :exec
INSERT INTO
	templates (
		id,
		created_at,
		updated_at,
		organization_id,
		"name",
		provisioner,
		active_version_id,
		description,
		created_by,
		icon,
		user_acl,
		group_acl,
		display_name,
		allow_user_cancel_workspace_jobs,
		max_port_sharing_level,
		use_classic_parameter_flow,
		cors_behavior
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
`

type InsertTemplateParams struct {
	ID                           uuid.UUID       `db:"id" json:"id"`
	CreatedAt                    time.Time       `db:"created_at" json:"created_at"`
	UpdatedAt                    time.Time       `db:"updated_at" json:"updated_at"`
	OrganizationID               uuid.UUID       `db:"organization_id" json:"organization_id"`
	Name                         string          `db:"name" json:"name"`
	Provisioner                  ProvisionerType `db:"provisioner" json:"provisioner"`
	ActiveVersionID              uuid.UUID       `db:"active_version_id" json:"active_version_id"`
	Description                  string          `db:"description" json:"description"`
	CreatedBy                    uuid.UUID       `db:"created_by" json:"created_by"`
	Icon                         string          `db:"icon" json:"icon"`
	UserACL                      TemplateACL     `db:"user_acl" json:"user_acl"`
	GroupACL                     TemplateACL     `db:"group_acl" json:"group_acl"`
	DisplayName                  string          `db:"display_name" json:"display_name"`
	AllowUserCancelWorkspaceJobs bool            `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
	MaxPortSharingLevel          AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
	UseClassicParameterFlow      bool            `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
	CorsBehavior                 CorsBehavior    `db:"cors_behavior" json:"cors_behavior"`
}

func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) error {
	_, err := q.db.ExecContext(ctx, insertTemplate,
		arg.ID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.OrganizationID,
		arg.Name,
		arg.Provisioner,
		arg.ActiveVersionID,
		arg.Description,
		arg.CreatedBy,
		arg.Icon,
		arg.UserACL,
		arg.GroupACL,
		arg.DisplayName,
		arg.AllowUserCancelWorkspaceJobs,
		arg.MaxPortSharingLevel,
		arg.UseClassicParameterFlow,
		arg.CorsBehavior,
	)
	return err
}

const updateTemplateACLByID = `-- name: UpdateTemplateACLByID :exec
UPDATE
	templates
SET
	group_acl = $1,
	user_acl = $2
WHERE
	id = $3
`

type UpdateTemplateACLByIDParams struct {
	GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
	UserACL  TemplateACL `db:"user_acl" json:"user_acl"`
	ID       uuid.UUID   `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateACLByID, arg.GroupACL, arg.UserACL, arg.ID)
	return err
}

const updateTemplateAccessControlByID = `-- name: UpdateTemplateAccessControlByID :exec
UPDATE
	templates
SET
	require_active_version = $2,
	deprecated = $3
WHERE
	id = $1
`

type UpdateTemplateAccessControlByIDParams struct {
	ID                   uuid.UUID `db:"id" json:"id"`
	RequireActiveVersion bool      `db:"require_active_version" json:"require_active_version"`
	Deprecated           string    `db:"deprecated" json:"deprecated"`
}

func (q *sqlQuerier) UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateAccessControlByID, arg.ID, arg.RequireActiveVersion, arg.Deprecated)
	return err
}

const updateTemplateActiveVersionByID = `-- name: UpdateTemplateActiveVersionByID :exec
UPDATE
	templates
SET
	active_version_id = $2,
	updated_at = $3
WHERE
	id = $1
`

type UpdateTemplateActiveVersionByIDParams struct {
	ID              uuid.UUID `db:"id" json:"id"`
	ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
	UpdatedAt       time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateActiveVersionByID, arg.ID, arg.ActiveVersionID, arg.UpdatedAt)
	return err
}

const updateTemplateDeletedByID = `-- name: UpdateTemplateDeletedByID :exec
UPDATE
	templates
SET
	deleted = $2,
	updated_at = $3
WHERE
	id = $1
`

type UpdateTemplateDeletedByIDParams struct {
	ID        uuid.UUID `db:"id" json:"id"`
	Deleted   bool      `db:"deleted" json:"deleted"`
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateDeletedByID, arg.ID, arg.Deleted, arg.UpdatedAt)
	return err
}

const updateTemplateMetaByID = `-- name: UpdateTemplateMetaByID :exec
UPDATE
	templates
SET
	updated_at = $2,
	description = $3,
	name = $4,
	icon = $5,
	display_name = $6,
	allow_user_cancel_workspace_jobs = $7,
	group_acl = $8,
	max_port_sharing_level = $9,
	use_classic_parameter_flow = $10,
	cors_behavior = $11,
	disable_module_cache = $12
WHERE
	id = $1
`

type UpdateTemplateMetaByIDParams struct {
	ID                           uuid.UUID       `db:"id" json:"id"`
	UpdatedAt                    time.Time       `db:"updated_at" json:"updated_at"`
	Description                  string          `db:"description" json:"description"`
	Name                         string          `db:"name" json:"name"`
	Icon                         string          `db:"icon" json:"icon"`
	DisplayName                  string          `db:"display_name" json:"display_name"`
	AllowUserCancelWorkspaceJobs bool            `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
	GroupACL                     TemplateACL     `db:"group_acl" json:"group_acl"`
	MaxPortSharingLevel          AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"`
	UseClassicParameterFlow      bool            `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
	CorsBehavior                 CorsBehavior    `db:"cors_behavior" json:"cors_behavior"`
	DisableModuleCache           bool            `db:"disable_module_cache" json:"disable_module_cache"`
}

func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateMetaByID,
		arg.ID,
		arg.UpdatedAt,
		arg.Description,
		arg.Name,
		arg.Icon,
		arg.DisplayName,
		arg.AllowUserCancelWorkspaceJobs,
		arg.GroupACL,
		arg.MaxPortSharingLevel,
		arg.UseClassicParameterFlow,
		arg.CorsBehavior,
		arg.DisableModuleCache,
	)
	return err
}

const updateTemplateScheduleByID = `-- name: UpdateTemplateScheduleByID :exec
UPDATE
	templates
SET
	updated_at = $2,
	allow_user_autostart = $3,
	allow_user_autostop = $4,
	default_ttl = $5,
	activity_bump = $6,
	autostop_requirement_days_of_week = $7,
	autostop_requirement_weeks = $8,
	autostart_block_days_of_week = $9,
	failure_ttl = $10,
	time_til_dormant = $11,
	time_til_dormant_autodelete = $12
WHERE
	id = $1
`

type UpdateTemplateScheduleByIDParams struct {
	ID                            uuid.UUID `db:"id" json:"id"`
	UpdatedAt                     time.Time `db:"updated_at" json:"updated_at"`
	AllowUserAutostart            bool      `db:"allow_user_autostart" json:"allow_user_autostart"`
	AllowUserAutostop             bool      `db:"allow_user_autostop" json:"allow_user_autostop"`
	DefaultTTL                    int64     `db:"default_ttl" json:"default_ttl"`
	ActivityBump                  int64     `db:"activity_bump" json:"activity_bump"`
	AutostopRequirementDaysOfWeek int16     `db:"autostop_requirement_days_of_week" json:"autostop_requirement_days_of_week"`
	AutostopRequirementWeeks      int64     `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
	AutostartBlockDaysOfWeek      int16     `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
	FailureTTL                    int64     `db:"failure_ttl" json:"failure_ttl"`
	TimeTilDormant                int64     `db:"time_til_dormant" json:"time_til_dormant"`
	TimeTilDormantAutoDelete      int64     `db:"time_til_dormant_autodelete" json:"time_til_dormant_autodelete"`
}

func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateScheduleByID,
		arg.ID,
		arg.UpdatedAt,
		arg.AllowUserAutostart,
		arg.AllowUserAutostop,
		arg.DefaultTTL,
		arg.ActivityBump,
		arg.AutostopRequirementDaysOfWeek,
		arg.AutostopRequirementWeeks,
		arg.AutostartBlockDaysOfWeek,
		arg.FailureTTL,
		arg.TimeTilDormant,
		arg.TimeTilDormantAutoDelete,
	)
	return err
}

const getTemplateVersionParameters = `-- name: GetTemplateVersionParameters :many
SELECT template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic, required, display_name, display_order, ephemeral, form_type FROM template_version_parameters WHERE template_version_id = $1 ORDER BY display_order ASC, LOWER(name) ASC
`

func (q *sqlQuerier) GetTemplateVersionParameters(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionParameter, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateVersionParameters, templateVersionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersionParameter
	for rows.Next() {
		var i TemplateVersionParameter
		if err := rows.Scan(
			&i.TemplateVersionID,
			&i.Name,
			&i.Description,
			&i.Type,
			&i.Mutable,
			&i.DefaultValue,
			&i.Icon,
			&i.Options,
			&i.ValidationRegex,
			&i.ValidationMin,
			&i.ValidationMax,
			&i.ValidationError,
			&i.ValidationMonotonic,
			&i.Required,
			&i.DisplayName,
			&i.DisplayOrder,
			&i.Ephemeral,
			&i.FormType,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertTemplateVersionParameter = `-- name: InsertTemplateVersionParameter :one
INSERT INTO
    template_version_parameters (
        template_version_id,
        name,
        description,
        type,
        form_type,
        mutable,
        default_value,
        icon,
        options,
        validation_regex,
        validation_min,
        validation_max,
        validation_error,
        validation_monotonic,
        required,
        display_name,
        display_order,
        ephemeral
    )
VALUES
    (
        $1,
        $2,
        $3,
        $4,
        $5,
        $6,
        $7,
        $8,
        $9,
        $10,
        $11,
        $12,
        $13,
        $14,
        $15,
        $16,
        $17,
        $18
    ) RETURNING template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic, required, display_name, display_order, ephemeral, form_type
`

type InsertTemplateVersionParameterParams struct {
	TemplateVersionID   uuid.UUID         `db:"template_version_id" json:"template_version_id"`
	Name                string            `db:"name" json:"name"`
	Description         string            `db:"description" json:"description"`
	Type                string            `db:"type" json:"type"`
	FormType            ParameterFormType `db:"form_type" json:"form_type"`
	Mutable             bool              `db:"mutable" json:"mutable"`
	DefaultValue        string            `db:"default_value" json:"default_value"`
	Icon                string            `db:"icon" json:"icon"`
	Options             json.RawMessage   `db:"options" json:"options"`
	ValidationRegex     string            `db:"validation_regex" json:"validation_regex"`
	ValidationMin       sql.NullInt32     `db:"validation_min" json:"validation_min"`
	ValidationMax       sql.NullInt32     `db:"validation_max" json:"validation_max"`
	ValidationError     string            `db:"validation_error" json:"validation_error"`
	ValidationMonotonic string            `db:"validation_monotonic" json:"validation_monotonic"`
	Required            bool              `db:"required" json:"required"`
	DisplayName         string            `db:"display_name" json:"display_name"`
	DisplayOrder        int32             `db:"display_order" json:"display_order"`
	Ephemeral           bool              `db:"ephemeral" json:"ephemeral"`
}

func (q *sqlQuerier) InsertTemplateVersionParameter(ctx context.Context, arg InsertTemplateVersionParameterParams) (TemplateVersionParameter, error) {
	row := q.db.QueryRowContext(ctx, insertTemplateVersionParameter,
		arg.TemplateVersionID,
		arg.Name,
		arg.Description,
		arg.Type,
		arg.FormType,
		arg.Mutable,
		arg.DefaultValue,
		arg.Icon,
		arg.Options,
		arg.ValidationRegex,
		arg.ValidationMin,
		arg.ValidationMax,
		arg.ValidationError,
		arg.ValidationMonotonic,
		arg.Required,
		arg.DisplayName,
		arg.DisplayOrder,
		arg.Ephemeral,
	)
	var i TemplateVersionParameter
	err := row.Scan(
		&i.TemplateVersionID,
		&i.Name,
		&i.Description,
		&i.Type,
		&i.Mutable,
		&i.DefaultValue,
		&i.Icon,
		&i.Options,
		&i.ValidationRegex,
		&i.ValidationMin,
		&i.ValidationMax,
		&i.ValidationError,
		&i.ValidationMonotonic,
		&i.Required,
		&i.DisplayName,
		&i.DisplayOrder,
		&i.Ephemeral,
		&i.FormType,
	)
	return i, err
}

const archiveUnusedTemplateVersions = `-- name: ArchiveUnusedTemplateVersions :many
UPDATE
	template_versions
SET
	archived = true,
	updated_at = $1
FROM
	-- Archive all versions that are returned from this query.
	(
		SELECT
			scoped_template_versions.id
		FROM
			-- Scope an archive to a single template and ignore already archived template versions
			(
				SELECT
					id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent
				FROM
					template_versions
				WHERE
					template_versions.template_id = $2 :: uuid
					AND
					archived = false
					AND
					-- This allows archiving a specific template version.
					CASE
						WHEN $3::uuid  != '00000000-0000-0000-0000-000000000000'::uuid THEN
							template_versions.id = $3 :: uuid
						ELSE
							true
						END
			) AS scoped_template_versions
			LEFT JOIN
				provisioner_jobs ON scoped_template_versions.job_id = provisioner_jobs.id
			LEFT JOIN
				templates ON scoped_template_versions.template_id = templates.id
		WHERE
		  -- Actively used template versions (meaning the latest build is using
		  -- the version) are never archived. A "restart" command on the workspace,
		  -- even if failed, would use the version. So it cannot be archived until
		  -- the build is outdated.
		NOT EXISTS (
			-- Return all "used" versions, where "used" is defined as being
			-- used by a latest workspace build.
			SELECT template_version_id FROM (
				SELECT
					DISTINCT ON (workspace_id) template_version_id, transition
				FROM
					workspace_builds
				ORDER BY workspace_id, build_number DESC
				) AS used_versions
			WHERE
				used_versions.transition != 'delete'
				AND
				scoped_template_versions.id = used_versions.template_version_id
		)
		-- Also never archive the active template version
		AND active_version_id != scoped_template_versions.id
		AND CASE
			-- Optionally, only archive versions that match a given
			-- job status like 'failed'.
			WHEN $4 :: provisioner_job_status IS NOT NULL THEN
				provisioner_jobs.job_status = $4 :: provisioner_job_status
			ELSE
				true
		END
		-- Pending or running jobs should not be archived, as they are "in progress"
		AND provisioner_jobs.job_status != 'running'
		AND provisioner_jobs.job_status != 'pending'
	) AS archived_versions
WHERE
	template_versions.id IN (archived_versions.id)
RETURNING template_versions.id
`

type ArchiveUnusedTemplateVersionsParams struct {
	UpdatedAt         time.Time                `db:"updated_at" json:"updated_at"`
	TemplateID        uuid.UUID                `db:"template_id" json:"template_id"`
	TemplateVersionID uuid.UUID                `db:"template_version_id" json:"template_version_id"`
	JobStatus         NullProvisionerJobStatus `db:"job_status" json:"job_status"`
}

// Archiving templates is a soft delete action, so is reversible.
// Archiving prevents the version from being used and discovered
// by listing.
// Only unused template versions will be archived, which are any versions not
// referenced by the latest build of a workspace.
func (q *sqlQuerier) ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
	rows, err := q.db.QueryContext(ctx, archiveUnusedTemplateVersions,
		arg.UpdatedAt,
		arg.TemplateID,
		arg.TemplateVersionID,
		arg.JobStatus,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []uuid.UUID
	for rows.Next() {
		var id uuid.UUID
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		items = append(items, id)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getPreviousTemplateVersion = `-- name: GetPreviousTemplateVersion :one
SELECT
	id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
	template_version_with_user AS template_versions
WHERE
	created_at < (
		SELECT created_at
		FROM template_version_with_user AS tv
		WHERE tv.organization_id = $1 AND tv.name = $2 AND tv.template_id = $3
	)
	AND organization_id = $1
	AND template_id = $3
ORDER BY created_at DESC
LIMIT 1
`

type GetPreviousTemplateVersionParams struct {
	OrganizationID uuid.UUID     `db:"organization_id" json:"organization_id"`
	Name           string        `db:"name" json:"name"`
	TemplateID     uuid.NullUUID `db:"template_id" json:"template_id"`
}

func (q *sqlQuerier) GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error) {
	row := q.db.QueryRowContext(ctx, getPreviousTemplateVersion, arg.OrganizationID, arg.Name, arg.TemplateID)
	var i TemplateVersion
	err := row.Scan(
		&i.ID,
		&i.TemplateID,
		&i.OrganizationID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Readme,
		&i.JobID,
		&i.CreatedBy,
		&i.ExternalAuthProviders,
		&i.Message,
		&i.Archived,
		&i.SourceExampleID,
		&i.HasAITask,
		&i.HasExternalAgent,
		&i.CreatedByAvatarURL,
		&i.CreatedByUsername,
		&i.CreatedByName,
	)
	return i, err
}

const getTemplateVersionByID = `-- name: GetTemplateVersionByID :one
SELECT
	id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
	template_version_with_user AS template_versions
WHERE
	id = $1
`

func (q *sqlQuerier) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) {
	row := q.db.QueryRowContext(ctx, getTemplateVersionByID, id)
	var i TemplateVersion
	err := row.Scan(
		&i.ID,
		&i.TemplateID,
		&i.OrganizationID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Readme,
		&i.JobID,
		&i.CreatedBy,
		&i.ExternalAuthProviders,
		&i.Message,
		&i.Archived,
		&i.SourceExampleID,
		&i.HasAITask,
		&i.HasExternalAgent,
		&i.CreatedByAvatarURL,
		&i.CreatedByUsername,
		&i.CreatedByName,
	)
	return i, err
}

const getTemplateVersionByJobID = `-- name: GetTemplateVersionByJobID :one
SELECT
	id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
	template_version_with_user AS template_versions
WHERE
	job_id = $1
`

func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) {
	row := q.db.QueryRowContext(ctx, getTemplateVersionByJobID, jobID)
	var i TemplateVersion
	err := row.Scan(
		&i.ID,
		&i.TemplateID,
		&i.OrganizationID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Readme,
		&i.JobID,
		&i.CreatedBy,
		&i.ExternalAuthProviders,
		&i.Message,
		&i.Archived,
		&i.SourceExampleID,
		&i.HasAITask,
		&i.HasExternalAgent,
		&i.CreatedByAvatarURL,
		&i.CreatedByUsername,
		&i.CreatedByName,
	)
	return i, err
}

const getTemplateVersionByTemplateIDAndName = `-- name: GetTemplateVersionByTemplateIDAndName :one
SELECT
	id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
	template_version_with_user AS template_versions
WHERE
	template_id = $1
	AND "name" = $2
`

type GetTemplateVersionByTemplateIDAndNameParams struct {
	TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
	Name       string        `db:"name" json:"name"`
}

func (q *sqlQuerier) GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) {
	row := q.db.QueryRowContext(ctx, getTemplateVersionByTemplateIDAndName, arg.TemplateID, arg.Name)
	var i TemplateVersion
	err := row.Scan(
		&i.ID,
		&i.TemplateID,
		&i.OrganizationID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.Readme,
		&i.JobID,
		&i.CreatedBy,
		&i.ExternalAuthProviders,
		&i.Message,
		&i.Archived,
		&i.SourceExampleID,
		&i.HasAITask,
		&i.HasExternalAgent,
		&i.CreatedByAvatarURL,
		&i.CreatedByUsername,
		&i.CreatedByName,
	)
	return i, err
}

const getTemplateVersionsByIDs = `-- name: GetTemplateVersionsByIDs :many
SELECT
	id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
	template_version_with_user AS template_versions
WHERE
	id = ANY($1 :: uuid [ ])
`

func (q *sqlQuerier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UUID) ([]TemplateVersion, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateVersionsByIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersion
	for rows.Next() {
		var i TemplateVersion
		if err := rows.Scan(
			&i.ID,
			&i.TemplateID,
			&i.OrganizationID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.Readme,
			&i.JobID,
			&i.CreatedBy,
			&i.ExternalAuthProviders,
			&i.Message,
			&i.Archived,
			&i.SourceExampleID,
			&i.HasAITask,
			&i.HasExternalAgent,
			&i.CreatedByAvatarURL,
			&i.CreatedByUsername,
			&i.CreatedByName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplateVersionsByTemplateID = `-- name: GetTemplateVersionsByTemplateID :many
SELECT
	id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name
FROM
	template_version_with_user AS template_versions
WHERE
	template_id = $1 :: uuid
	  AND CASE
		-- If no filter is provided, default to returning ALL template versions.
		-- The called should always provide a filter if they want to omit
		-- archived versions.
		WHEN $2 :: boolean IS NULL THEN true
		ELSE template_versions.archived = $2 :: boolean
	END
	AND CASE
		-- This allows using the last element on a page as effectively a cursor.
		-- This is an important option for scripts that need to paginate without
		-- duplicating or missing data.
		WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
			-- The pagination cursor is the last ID of the previous page.
			-- The query is ordered by the created_at field, so select all
			-- rows after the cursor.
			(created_at, id) > (
				SELECT
					created_at, id
				FROM
					template_versions
				WHERE
					id = $3
			)
		)
		ELSE true
	END
ORDER BY
    -- Deterministic and consistent ordering of all rows, even if they share
    -- a timestamp. This is to ensure consistent pagination.
	(created_at, id) ASC OFFSET $4
LIMIT
	-- A null limit means "no limit", so 0 means return all
	NULLIF($5 :: int, 0)
`

type GetTemplateVersionsByTemplateIDParams struct {
	TemplateID uuid.UUID    `db:"template_id" json:"template_id"`
	Archived   sql.NullBool `db:"archived" json:"archived"`
	AfterID    uuid.UUID    `db:"after_id" json:"after_id"`
	OffsetOpt  int32        `db:"offset_opt" json:"offset_opt"`
	LimitOpt   int32        `db:"limit_opt" json:"limit_opt"`
}

func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateVersionsByTemplateID,
		arg.TemplateID,
		arg.Archived,
		arg.AfterID,
		arg.OffsetOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersion
	for rows.Next() {
		var i TemplateVersion
		if err := rows.Scan(
			&i.ID,
			&i.TemplateID,
			&i.OrganizationID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.Readme,
			&i.JobID,
			&i.CreatedBy,
			&i.ExternalAuthProviders,
			&i.Message,
			&i.Archived,
			&i.SourceExampleID,
			&i.HasAITask,
			&i.HasExternalAgent,
			&i.CreatedByAvatarURL,
			&i.CreatedByUsername,
			&i.CreatedByName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getTemplateVersionsCreatedAfter = `-- name: GetTemplateVersionsCreatedAfter :many
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, has_ai_task, has_external_agent, created_by_avatar_url, created_by_username, created_by_name FROM template_version_with_user AS template_versions WHERE created_at > $1
`

func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateVersionsCreatedAfter, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersion
	for rows.Next() {
		var i TemplateVersion
		if err := rows.Scan(
			&i.ID,
			&i.TemplateID,
			&i.OrganizationID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.Readme,
			&i.JobID,
			&i.CreatedBy,
			&i.ExternalAuthProviders,
			&i.Message,
			&i.Archived,
			&i.SourceExampleID,
			&i.HasAITask,
			&i.HasExternalAgent,
			&i.CreatedByAvatarURL,
			&i.CreatedByUsername,
			&i.CreatedByName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertTemplateVersion = `-- name: InsertTemplateVersion :exec
INSERT INTO
	template_versions (
		id,
		template_id,
		organization_id,
		created_at,
		updated_at,
		"name",
		message,
		readme,
		job_id,
		created_by,
		source_example_id
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`

type InsertTemplateVersionParams struct {
	ID              uuid.UUID      `db:"id" json:"id"`
	TemplateID      uuid.NullUUID  `db:"template_id" json:"template_id"`
	OrganizationID  uuid.UUID      `db:"organization_id" json:"organization_id"`
	CreatedAt       time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt       time.Time      `db:"updated_at" json:"updated_at"`
	Name            string         `db:"name" json:"name"`
	Message         string         `db:"message" json:"message"`
	Readme          string         `db:"readme" json:"readme"`
	JobID           uuid.UUID      `db:"job_id" json:"job_id"`
	CreatedBy       uuid.UUID      `db:"created_by" json:"created_by"`
	SourceExampleID sql.NullString `db:"source_example_id" json:"source_example_id"`
}

func (q *sqlQuerier) InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error {
	_, err := q.db.ExecContext(ctx, insertTemplateVersion,
		arg.ID,
		arg.TemplateID,
		arg.OrganizationID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.Name,
		arg.Message,
		arg.Readme,
		arg.JobID,
		arg.CreatedBy,
		arg.SourceExampleID,
	)
	return err
}

const unarchiveTemplateVersion = `-- name: UnarchiveTemplateVersion :exec
UPDATE
	template_versions
SET
	archived = false,
	updated_at = $1
WHERE
		id = $2
`

type UnarchiveTemplateVersionParams struct {
	UpdatedAt         time.Time `db:"updated_at" json:"updated_at"`
	TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
}

// This will always work regardless of the current state of the template version.
func (q *sqlQuerier) UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error {
	_, err := q.db.ExecContext(ctx, unarchiveTemplateVersion, arg.UpdatedAt, arg.TemplateVersionID)
	return err
}

const updateTemplateVersionByID = `-- name: UpdateTemplateVersionByID :exec
UPDATE
	template_versions
SET
	template_id = $2,
	updated_at = $3,
	name = $4,
	message = $5
WHERE
	id = $1
`

type UpdateTemplateVersionByIDParams struct {
	ID         uuid.UUID     `db:"id" json:"id"`
	TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
	UpdatedAt  time.Time     `db:"updated_at" json:"updated_at"`
	Name       string        `db:"name" json:"name"`
	Message    string        `db:"message" json:"message"`
}

func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateVersionByID,
		arg.ID,
		arg.TemplateID,
		arg.UpdatedAt,
		arg.Name,
		arg.Message,
	)
	return err
}

const updateTemplateVersionDescriptionByJobID = `-- name: UpdateTemplateVersionDescriptionByJobID :exec
UPDATE
	template_versions
SET
	readme = $2,
	updated_at = $3
WHERE
	job_id = $1
`

type UpdateTemplateVersionDescriptionByJobIDParams struct {
	JobID     uuid.UUID `db:"job_id" json:"job_id"`
	Readme    string    `db:"readme" json:"readme"`
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateVersionDescriptionByJobID, arg.JobID, arg.Readme, arg.UpdatedAt)
	return err
}

const updateTemplateVersionExternalAuthProvidersByJobID = `-- name: UpdateTemplateVersionExternalAuthProvidersByJobID :exec
UPDATE
	template_versions
SET
	external_auth_providers = $2,
	updated_at = $3
WHERE
	job_id = $1
`

type UpdateTemplateVersionExternalAuthProvidersByJobIDParams struct {
	JobID                 uuid.UUID       `db:"job_id" json:"job_id"`
	ExternalAuthProviders json.RawMessage `db:"external_auth_providers" json:"external_auth_providers"`
	UpdatedAt             time.Time       `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateVersionExternalAuthProvidersByJobID, arg.JobID, arg.ExternalAuthProviders, arg.UpdatedAt)
	return err
}

const updateTemplateVersionFlagsByJobID = `-- name: UpdateTemplateVersionFlagsByJobID :exec
UPDATE
	template_versions
SET
	has_ai_task = $2,
	has_external_agent = $3,
	updated_at = $4
WHERE
	job_id = $1
`

type UpdateTemplateVersionFlagsByJobIDParams struct {
	JobID            uuid.UUID    `db:"job_id" json:"job_id"`
	HasAITask        sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
	HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
	UpdatedAt        time.Time    `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateTemplateVersionFlagsByJobID(ctx context.Context, arg UpdateTemplateVersionFlagsByJobIDParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateVersionFlagsByJobID,
		arg.JobID,
		arg.HasAITask,
		arg.HasExternalAgent,
		arg.UpdatedAt,
	)
	return err
}

const getTemplateVersionTerraformValues = `-- name: GetTemplateVersionTerraformValues :one
SELECT
	template_version_terraform_values.template_version_id, template_version_terraform_values.updated_at, template_version_terraform_values.cached_plan, template_version_terraform_values.cached_module_files, template_version_terraform_values.provisionerd_version
FROM
	template_version_terraform_values
WHERE
	template_version_terraform_values.template_version_id = $1
`

func (q *sqlQuerier) GetTemplateVersionTerraformValues(ctx context.Context, templateVersionID uuid.UUID) (TemplateVersionTerraformValue, error) {
	row := q.db.QueryRowContext(ctx, getTemplateVersionTerraformValues, templateVersionID)
	var i TemplateVersionTerraformValue
	err := row.Scan(
		&i.TemplateVersionID,
		&i.UpdatedAt,
		&i.CachedPlan,
		&i.CachedModuleFiles,
		&i.ProvisionerdVersion,
	)
	return i, err
}

const insertTemplateVersionTerraformValuesByJobID = `-- name: InsertTemplateVersionTerraformValuesByJobID :exec
INSERT INTO
	template_version_terraform_values (
		template_version_id,
		cached_plan,
		cached_module_files,
		updated_at,
	    provisionerd_version
	)
VALUES
	(
		(select id from template_versions where job_id = $1),
		$2,
		$3,
		$4,
		$5
	)
`

type InsertTemplateVersionTerraformValuesByJobIDParams struct {
	JobID               uuid.UUID       `db:"job_id" json:"job_id"`
	CachedPlan          json.RawMessage `db:"cached_plan" json:"cached_plan"`
	CachedModuleFiles   uuid.NullUUID   `db:"cached_module_files" json:"cached_module_files"`
	UpdatedAt           time.Time       `db:"updated_at" json:"updated_at"`
	ProvisionerdVersion string          `db:"provisionerd_version" json:"provisionerd_version"`
}

func (q *sqlQuerier) InsertTemplateVersionTerraformValuesByJobID(ctx context.Context, arg InsertTemplateVersionTerraformValuesByJobIDParams) error {
	_, err := q.db.ExecContext(ctx, insertTemplateVersionTerraformValuesByJobID,
		arg.JobID,
		arg.CachedPlan,
		arg.CachedModuleFiles,
		arg.UpdatedAt,
		arg.ProvisionerdVersion,
	)
	return err
}

const getTemplateVersionVariables = `-- name: GetTemplateVersionVariables :many
SELECT template_version_id, name, description, type, value, default_value, required, sensitive FROM template_version_variables WHERE template_version_id = $1 ORDER BY name
`

func (q *sqlQuerier) GetTemplateVersionVariables(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionVariable, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateVersionVariables, templateVersionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersionVariable
	for rows.Next() {
		var i TemplateVersionVariable
		if err := rows.Scan(
			&i.TemplateVersionID,
			&i.Name,
			&i.Description,
			&i.Type,
			&i.Value,
			&i.DefaultValue,
			&i.Required,
			&i.Sensitive,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertTemplateVersionVariable = `-- name: InsertTemplateVersionVariable :one
INSERT INTO
    template_version_variables (
        template_version_id,
        name,
        description,
        type,
        value,
        default_value,
        required,
        sensitive
    )
VALUES
    (
        $1,
        $2,
        $3,
        $4,
        $5,
        $6,
        $7,
        $8
    ) RETURNING template_version_id, name, description, type, value, default_value, required, sensitive
`

type InsertTemplateVersionVariableParams struct {
	TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
	Name              string    `db:"name" json:"name"`
	Description       string    `db:"description" json:"description"`
	Type              string    `db:"type" json:"type"`
	Value             string    `db:"value" json:"value"`
	DefaultValue      string    `db:"default_value" json:"default_value"`
	Required          bool      `db:"required" json:"required"`
	Sensitive         bool      `db:"sensitive" json:"sensitive"`
}

func (q *sqlQuerier) InsertTemplateVersionVariable(ctx context.Context, arg InsertTemplateVersionVariableParams) (TemplateVersionVariable, error) {
	row := q.db.QueryRowContext(ctx, insertTemplateVersionVariable,
		arg.TemplateVersionID,
		arg.Name,
		arg.Description,
		arg.Type,
		arg.Value,
		arg.DefaultValue,
		arg.Required,
		arg.Sensitive,
	)
	var i TemplateVersionVariable
	err := row.Scan(
		&i.TemplateVersionID,
		&i.Name,
		&i.Description,
		&i.Type,
		&i.Value,
		&i.DefaultValue,
		&i.Required,
		&i.Sensitive,
	)
	return i, err
}

const getTemplateVersionWorkspaceTags = `-- name: GetTemplateVersionWorkspaceTags :many
SELECT template_version_id, key, value FROM template_version_workspace_tags WHERE template_version_id = $1 ORDER BY LOWER(key) ASC
`

func (q *sqlQuerier) GetTemplateVersionWorkspaceTags(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionWorkspaceTag, error) {
	rows, err := q.db.QueryContext(ctx, getTemplateVersionWorkspaceTags, templateVersionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []TemplateVersionWorkspaceTag
	for rows.Next() {
		var i TemplateVersionWorkspaceTag
		if err := rows.Scan(&i.TemplateVersionID, &i.Key, &i.Value); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertTemplateVersionWorkspaceTag = `-- name: InsertTemplateVersionWorkspaceTag :one
INSERT INTO
    template_version_workspace_tags (
        template_version_id,
        key,
        value
    )
VALUES
    (
        $1,
        $2,
        $3
    ) RETURNING template_version_id, key, value
`

type InsertTemplateVersionWorkspaceTagParams struct {
	TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
	Key               string    `db:"key" json:"key"`
	Value             string    `db:"value" json:"value"`
}

func (q *sqlQuerier) InsertTemplateVersionWorkspaceTag(ctx context.Context, arg InsertTemplateVersionWorkspaceTagParams) (TemplateVersionWorkspaceTag, error) {
	row := q.db.QueryRowContext(ctx, insertTemplateVersionWorkspaceTag, arg.TemplateVersionID, arg.Key, arg.Value)
	var i TemplateVersionWorkspaceTag
	err := row.Scan(&i.TemplateVersionID, &i.Key, &i.Value)
	return i, err
}

const disableForeignKeysAndTriggers = `-- name: DisableForeignKeysAndTriggers :exec
DO $$
DECLARE
    table_record record;
BEGIN
    FOR table_record IN
        SELECT table_schema, table_name
        FROM information_schema.tables
        WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
        AND table_type = 'BASE TABLE'
    LOOP
        EXECUTE format('ALTER TABLE %I.%I DISABLE TRIGGER ALL',
                    table_record.table_schema,
                    table_record.table_name);
    END LOOP;
END;
$$
`

// Disable foreign keys and triggers for all tables.
// Deprecated: disable foreign keys was created to aid in migrating off
// of the test-only in-memory database. Do not use this in new code.
func (q *sqlQuerier) DisableForeignKeysAndTriggers(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, disableForeignKeysAndTriggers)
	return err
}

const getTotalUsageDCManagedAgentsV1 = `-- name: GetTotalUsageDCManagedAgentsV1 :one
SELECT
    -- The first cast is necessary since you can't sum strings, and the second
    -- cast is necessary to make sqlc happy.
    COALESCE(SUM((usage_data->>'count')::bigint), 0)::bigint AS total_count
FROM
    usage_events_daily
WHERE
    event_type = 'dc_managed_agents_v1'
    -- Parentheses are necessary to avoid sqlc from generating an extra
    -- argument.
    AND day BETWEEN date_trunc('day', ($1::timestamptz) AT TIME ZONE 'UTC')::date AND date_trunc('day', ($2::timestamptz) AT TIME ZONE 'UTC')::date
`

type GetTotalUsageDCManagedAgentsV1Params struct {
	StartDate time.Time `db:"start_date" json:"start_date"`
	EndDate   time.Time `db:"end_date" json:"end_date"`
}

// Gets the total number of managed agents created between two dates. Uses the
// aggregate table to avoid large scans or a complex index on the usage_events
// table.
//
// This has the trade off that we can't count accurately between two exact
// timestamps. The provided timestamps will be converted to UTC and truncated to
// the events that happened on and between the two dates. Both dates are
// inclusive.
func (q *sqlQuerier) GetTotalUsageDCManagedAgentsV1(ctx context.Context, arg GetTotalUsageDCManagedAgentsV1Params) (int64, error) {
	row := q.db.QueryRowContext(ctx, getTotalUsageDCManagedAgentsV1, arg.StartDate, arg.EndDate)
	var total_count int64
	err := row.Scan(&total_count)
	return total_count, err
}

const insertUsageEvent = `-- name: InsertUsageEvent :exec
INSERT INTO
    usage_events (
        id,
        event_type,
        event_data,
        created_at,
        publish_started_at,
        published_at,
        failure_message
    )
VALUES
    ($1, $2, $3, $4, NULL, NULL, NULL)
ON CONFLICT (id) DO NOTHING
`

type InsertUsageEventParams struct {
	ID        string          `db:"id" json:"id"`
	EventType string          `db:"event_type" json:"event_type"`
	EventData json.RawMessage `db:"event_data" json:"event_data"`
	CreatedAt time.Time       `db:"created_at" json:"created_at"`
}

// Duplicate events are ignored intentionally to allow for multiple replicas to
// publish heartbeat events.
func (q *sqlQuerier) InsertUsageEvent(ctx context.Context, arg InsertUsageEventParams) error {
	_, err := q.db.ExecContext(ctx, insertUsageEvent,
		arg.ID,
		arg.EventType,
		arg.EventData,
		arg.CreatedAt,
	)
	return err
}

const selectUsageEventsForPublishing = `-- name: SelectUsageEventsForPublishing :many
WITH usage_events AS (
    UPDATE
        usage_events
    SET
        publish_started_at = $1::timestamptz
    WHERE
        id IN (
            SELECT
                potential_event.id
            FROM
                usage_events potential_event
            WHERE
                -- Do not publish events that have already been published or
                -- have permanently failed to publish.
                potential_event.published_at IS NULL
                -- Do not publish events that are already being published by
                -- another replica.
                AND (
                    potential_event.publish_started_at IS NULL
                    -- If the event has publish_started_at set, it must be older
                    -- than an hour ago. This is so we can retry publishing
                    -- events where the replica exited or couldn't update the
                    -- row.
                    -- The parentheses around @now::timestamptz are necessary to
                    -- avoid sqlc from generating an extra argument.
                    OR potential_event.publish_started_at < ($1::timestamptz) - INTERVAL '1 hour'
                )
                -- Do not publish events older than 30 days. Tallyman will
                -- always permanently reject these events anyways. This is to
                -- avoid duplicate events being billed to customers, as
                -- Metronome will only deduplicate events within 34 days.
                -- Also, the same parentheses thing here as above.
                AND potential_event.created_at > ($1::timestamptz) - INTERVAL '30 days'
            ORDER BY potential_event.created_at ASC
            FOR UPDATE SKIP LOCKED
            LIMIT 100
        )
    RETURNING id, event_type, event_data, created_at, publish_started_at, published_at, failure_message
)
SELECT id, event_type, event_data, created_at, publish_started_at, published_at, failure_message
FROM usage_events
ORDER BY created_at ASC
`

// Note that this selects from the CTE, not the original table. The CTE is named
// the same as the original table to trick sqlc into reusing the existing struct
// for the table.
// The CTE and the reorder is required because UPDATE doesn't guarantee order.
func (q *sqlQuerier) SelectUsageEventsForPublishing(ctx context.Context, now time.Time) ([]UsageEvent, error) {
	rows, err := q.db.QueryContext(ctx, selectUsageEventsForPublishing, now)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UsageEvent
	for rows.Next() {
		var i UsageEvent
		if err := rows.Scan(
			&i.ID,
			&i.EventType,
			&i.EventData,
			&i.CreatedAt,
			&i.PublishStartedAt,
			&i.PublishedAt,
			&i.FailureMessage,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateUsageEventsPostPublish = `-- name: UpdateUsageEventsPostPublish :exec
UPDATE
    usage_events
SET
    publish_started_at = NULL,
    published_at = CASE WHEN input.set_published_at THEN $1::timestamptz ELSE NULL END,
    failure_message = NULLIF(input.failure_message, '')
FROM (
    SELECT
        UNNEST($2::text[]) AS id,
        UNNEST($3::text[]) AS failure_message,
        UNNEST($4::boolean[]) AS set_published_at
) input
WHERE
    input.id = usage_events.id
    -- If the number of ids, failure messages, and set published ats are not the
    -- same, do not do anything. Unfortunately you can't really throw from a
    -- query without writing a function or doing some jank like dividing by
    -- zero, so this is the best we can do.
    AND cardinality($2::text[]) = cardinality($3::text[])
    AND cardinality($2::text[]) = cardinality($4::boolean[])
`

type UpdateUsageEventsPostPublishParams struct {
	Now             time.Time `db:"now" json:"now"`
	IDs             []string  `db:"ids" json:"ids"`
	FailureMessages []string  `db:"failure_messages" json:"failure_messages"`
	SetPublishedAts []bool    `db:"set_published_ats" json:"set_published_ats"`
}

func (q *sqlQuerier) UpdateUsageEventsPostPublish(ctx context.Context, arg UpdateUsageEventsPostPublishParams) error {
	_, err := q.db.ExecContext(ctx, updateUsageEventsPostPublish,
		arg.Now,
		pq.Array(arg.IDs),
		pq.Array(arg.FailureMessages),
		pq.Array(arg.SetPublishedAts),
	)
	return err
}

const usageEventExistsByID = `-- name: UsageEventExistsByID :one
SELECT EXISTS(
    SELECT 1 FROM usage_events WHERE id = $1
)::bool
`

func (q *sqlQuerier) UsageEventExistsByID(ctx context.Context, id string) (bool, error) {
	row := q.db.QueryRowContext(ctx, usageEventExistsByID, id)
	var column_1 bool
	err := row.Scan(&column_1)
	return column_1, err
}

const deleteUserAIProviderKey = `-- name: DeleteUserAIProviderKey :exec
DELETE FROM
    user_ai_provider_keys
WHERE
    user_id = $1::uuid
    AND ai_provider_id = $2::uuid
`

type DeleteUserAIProviderKeyParams struct {
	UserID       uuid.UUID `db:"user_id" json:"user_id"`
	AIProviderID uuid.UUID `db:"ai_provider_id" json:"ai_provider_id"`
}

func (q *sqlQuerier) DeleteUserAIProviderKey(ctx context.Context, arg DeleteUserAIProviderKeyParams) error {
	_, err := q.db.ExecContext(ctx, deleteUserAIProviderKey, arg.UserID, arg.AIProviderID)
	return err
}

const deleteUserAIProviderKeysByProviderID = `-- name: DeleteUserAIProviderKeysByProviderID :exec
DELETE FROM
    user_ai_provider_keys
WHERE
    ai_provider_id = $1::uuid
`

func (q *sqlQuerier) DeleteUserAIProviderKeysByProviderID(ctx context.Context, aiProviderID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteUserAIProviderKeysByProviderID, aiProviderID)
	return err
}

const getUserAIProviderKeyByProviderID = `-- name: GetUserAIProviderKeyByProviderID :one
SELECT
    id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
    user_ai_provider_keys
WHERE
    user_id = $1::uuid
    AND ai_provider_id = $2::uuid
`

type GetUserAIProviderKeyByProviderIDParams struct {
	UserID       uuid.UUID `db:"user_id" json:"user_id"`
	AIProviderID uuid.UUID `db:"ai_provider_id" json:"ai_provider_id"`
}

func (q *sqlQuerier) GetUserAIProviderKeyByProviderID(ctx context.Context, arg GetUserAIProviderKeyByProviderIDParams) (UserAiProviderKey, error) {
	row := q.db.QueryRowContext(ctx, getUserAIProviderKeyByProviderID, arg.UserID, arg.AIProviderID)
	var i UserAiProviderKey
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.AIProviderID,
		&i.APIKey,
		&i.ApiKeyKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getUserAIProviderKeys = `-- name: GetUserAIProviderKeys :many
SELECT
    id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
    user_ai_provider_keys
ORDER BY
    user_id ASC,
    ai_provider_id ASC,
    created_at ASC,
    id ASC
`

// GetUserAIProviderKeys is used by dbcrypt key rotation. Request paths should use
// user-scoped lookups instead of this bulk accessor.
func (q *sqlQuerier) GetUserAIProviderKeys(ctx context.Context) ([]UserAiProviderKey, error) {
	rows, err := q.db.QueryContext(ctx, getUserAIProviderKeys)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UserAiProviderKey
	for rows.Next() {
		var i UserAiProviderKey
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.AIProviderID,
			&i.APIKey,
			&i.ApiKeyKeyID,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUserAIProviderKeysByUserID = `-- name: GetUserAIProviderKeysByUserID :many
SELECT
    id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
FROM
    user_ai_provider_keys
WHERE
    user_id = $1::uuid
ORDER BY
    ai_provider_id ASC,
    created_at ASC,
    id ASC
`

func (q *sqlQuerier) GetUserAIProviderKeysByUserID(ctx context.Context, userID uuid.UUID) ([]UserAiProviderKey, error) {
	rows, err := q.db.QueryContext(ctx, getUserAIProviderKeysByUserID, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UserAiProviderKey
	for rows.Next() {
		var i UserAiProviderKey
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.AIProviderID,
			&i.APIKey,
			&i.ApiKeyKeyID,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateEncryptedUserAIProviderKey = `-- name: UpdateEncryptedUserAIProviderKey :one
UPDATE
    user_ai_provider_keys
SET
    api_key = $1::text,
    api_key_key_id = $2::text,
    updated_at = NOW()
WHERE
    id = $3::uuid
RETURNING
    id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
`

type UpdateEncryptedUserAIProviderKeyParams struct {
	APIKey      string         `db:"api_key" json:"api_key"`
	ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
	ID          uuid.UUID      `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateEncryptedUserAIProviderKey(ctx context.Context, arg UpdateEncryptedUserAIProviderKeyParams) (UserAiProviderKey, error) {
	row := q.db.QueryRowContext(ctx, updateEncryptedUserAIProviderKey, arg.APIKey, arg.ApiKeyKeyID, arg.ID)
	var i UserAiProviderKey
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.AIProviderID,
		&i.APIKey,
		&i.ApiKeyKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const updateUserAIProviderKey = `-- name: UpdateUserAIProviderKey :one
UPDATE
    user_ai_provider_keys
SET
    api_key = $1::text,
    api_key_key_id = $2::text,
    updated_at = NOW()
WHERE
    user_id = $3::uuid
    AND ai_provider_id = $4::uuid
RETURNING
    id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
`

type UpdateUserAIProviderKeyParams struct {
	APIKey       string         `db:"api_key" json:"api_key"`
	ApiKeyKeyID  sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
	UserID       uuid.UUID      `db:"user_id" json:"user_id"`
	AIProviderID uuid.UUID      `db:"ai_provider_id" json:"ai_provider_id"`
}

func (q *sqlQuerier) UpdateUserAIProviderKey(ctx context.Context, arg UpdateUserAIProviderKeyParams) (UserAiProviderKey, error) {
	row := q.db.QueryRowContext(ctx, updateUserAIProviderKey,
		arg.APIKey,
		arg.ApiKeyKeyID,
		arg.UserID,
		arg.AIProviderID,
	)
	var i UserAiProviderKey
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.AIProviderID,
		&i.APIKey,
		&i.ApiKeyKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const upsertUserAIProviderKey = `-- name: UpsertUserAIProviderKey :one
INSERT INTO user_ai_provider_keys (
    id,
    user_id,
    ai_provider_id,
    api_key,
    api_key_key_id,
    created_at,
    updated_at
) VALUES (
    $1::uuid,
    $2::uuid,
    $3::uuid,
    $4::text,
    $5::text,
    $6::timestamptz,
    $7::timestamptz
)
ON CONFLICT (user_id, ai_provider_id) DO UPDATE
SET
    api_key = EXCLUDED.api_key,
    api_key_key_id = EXCLUDED.api_key_key_id,
    updated_at = EXCLUDED.updated_at
RETURNING
    id, user_id, ai_provider_id, api_key, api_key_key_id, created_at, updated_at
`

type UpsertUserAIProviderKeyParams struct {
	ID           uuid.UUID      `db:"id" json:"id"`
	UserID       uuid.UUID      `db:"user_id" json:"user_id"`
	AIProviderID uuid.UUID      `db:"ai_provider_id" json:"ai_provider_id"`
	APIKey       string         `db:"api_key" json:"api_key"`
	ApiKeyKeyID  sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
	CreatedAt    time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt    time.Time      `db:"updated_at" json:"updated_at"`
}

// UpsertUserAIProviderKey preserves the original id and created_at when the
// user/provider pair already exists. On conflict, callers provide id and
// created_at for the insert path only.
func (q *sqlQuerier) UpsertUserAIProviderKey(ctx context.Context, arg UpsertUserAIProviderKeyParams) (UserAiProviderKey, error) {
	row := q.db.QueryRowContext(ctx, upsertUserAIProviderKey,
		arg.ID,
		arg.UserID,
		arg.AIProviderID,
		arg.APIKey,
		arg.ApiKeyKeyID,
		arg.CreatedAt,
		arg.UpdatedAt,
	)
	var i UserAiProviderKey
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.AIProviderID,
		&i.APIKey,
		&i.ApiKeyKeyID,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getUserLinkByLinkedID = `-- name: GetUserLinkByLinkedID :one
SELECT
	user_links.user_id, user_links.login_type, user_links.linked_id, user_links.oauth_access_token, user_links.oauth_refresh_token, user_links.oauth_expiry, user_links.oauth_access_token_key_id, user_links.oauth_refresh_token_key_id, user_links.claims
FROM
	user_links
INNER JOIN
	users ON user_links.user_id = users.id
WHERE
	linked_id = $1
	AND
	deleted = false
`

func (q *sqlQuerier) GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error) {
	row := q.db.QueryRowContext(ctx, getUserLinkByLinkedID, linkedID)
	var i UserLink
	err := row.Scan(
		&i.UserID,
		&i.LoginType,
		&i.LinkedID,
		&i.OAuthAccessToken,
		&i.OAuthRefreshToken,
		&i.OAuthExpiry,
		&i.OAuthAccessTokenKeyID,
		&i.OAuthRefreshTokenKeyID,
		&i.Claims,
	)
	return i, err
}

const getUserLinkByUserIDLoginType = `-- name: GetUserLinkByUserIDLoginType :one
SELECT
	user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims
FROM
	user_links
WHERE
	user_id = $1 AND login_type = $2
`

type GetUserLinkByUserIDLoginTypeParams struct {
	UserID    uuid.UUID `db:"user_id" json:"user_id"`
	LoginType LoginType `db:"login_type" json:"login_type"`
}

func (q *sqlQuerier) GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error) {
	row := q.db.QueryRowContext(ctx, getUserLinkByUserIDLoginType, arg.UserID, arg.LoginType)
	var i UserLink
	err := row.Scan(
		&i.UserID,
		&i.LoginType,
		&i.LinkedID,
		&i.OAuthAccessToken,
		&i.OAuthRefreshToken,
		&i.OAuthExpiry,
		&i.OAuthAccessTokenKeyID,
		&i.OAuthRefreshTokenKeyID,
		&i.Claims,
	)
	return i, err
}

const getUserLinksByUserID = `-- name: GetUserLinksByUserID :many
SELECT user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims FROM user_links WHERE user_id = $1
`

func (q *sqlQuerier) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error) {
	rows, err := q.db.QueryContext(ctx, getUserLinksByUserID, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UserLink
	for rows.Next() {
		var i UserLink
		if err := rows.Scan(
			&i.UserID,
			&i.LoginType,
			&i.LinkedID,
			&i.OAuthAccessToken,
			&i.OAuthRefreshToken,
			&i.OAuthExpiry,
			&i.OAuthAccessTokenKeyID,
			&i.OAuthRefreshTokenKeyID,
			&i.Claims,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertUserLink = `-- name: InsertUserLink :one
INSERT INTO
	user_links (
		user_id,
		login_type,
		linked_id,
		oauth_access_token,
		oauth_access_token_key_id,
		oauth_refresh_token,
		oauth_refresh_token_key_id,
		oauth_expiry,
		claims
	)
VALUES
	( $1, $2, $3, $4, $5, $6, $7, $8, $9 ) RETURNING user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims
`

type InsertUserLinkParams struct {
	UserID                 uuid.UUID      `db:"user_id" json:"user_id"`
	LoginType              LoginType      `db:"login_type" json:"login_type"`
	LinkedID               string         `db:"linked_id" json:"linked_id"`
	OAuthAccessToken       string         `db:"oauth_access_token" json:"oauth_access_token"`
	OAuthAccessTokenKeyID  sql.NullString `db:"oauth_access_token_key_id" json:"oauth_access_token_key_id"`
	OAuthRefreshToken      string         `db:"oauth_refresh_token" json:"oauth_refresh_token"`
	OAuthRefreshTokenKeyID sql.NullString `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
	OAuthExpiry            time.Time      `db:"oauth_expiry" json:"oauth_expiry"`
	Claims                 UserLinkClaims `db:"claims" json:"claims"`
}

func (q *sqlQuerier) InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error) {
	row := q.db.QueryRowContext(ctx, insertUserLink,
		arg.UserID,
		arg.LoginType,
		arg.LinkedID,
		arg.OAuthAccessToken,
		arg.OAuthAccessTokenKeyID,
		arg.OAuthRefreshToken,
		arg.OAuthRefreshTokenKeyID,
		arg.OAuthExpiry,
		arg.Claims,
	)
	var i UserLink
	err := row.Scan(
		&i.UserID,
		&i.LoginType,
		&i.LinkedID,
		&i.OAuthAccessToken,
		&i.OAuthRefreshToken,
		&i.OAuthExpiry,
		&i.OAuthAccessTokenKeyID,
		&i.OAuthRefreshTokenKeyID,
		&i.Claims,
	)
	return i, err
}

const oIDCClaimFieldValues = `-- name: OIDCClaimFieldValues :many
SELECT
	-- DISTINCT to remove duplicates
	DISTINCT jsonb_array_elements_text(CASE
		-- When the type is an array, filter out any non-string elements.
		-- This is to keep the return type consistent.
		WHEN jsonb_typeof(claims->'merged_claims'->$1::text) = 'array' THEN
			(
				SELECT
					jsonb_agg(element)
				FROM
					jsonb_array_elements(claims->'merged_claims'->$1::text) AS element
				WHERE
					-- Filtering out non-string elements
					jsonb_typeof(element) = 'string'
			)
		-- Some IDPs return a single string instead of an array of strings.
		WHEN jsonb_typeof(claims->'merged_claims'->$1::text) = 'string' THEN
			jsonb_build_array(claims->'merged_claims'->$1::text)
	END)
FROM
	user_links
WHERE
	-- IDP sync only supports string and array (of string) types
	jsonb_typeof(claims->'merged_claims'->$1::text) = ANY(ARRAY['string', 'array'])
	AND login_type = 'oidc'
	AND CASE
		WHEN $2 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid  THEN
			user_links.user_id = ANY(SELECT organization_members.user_id FROM organization_members WHERE organization_id = $2)
		ELSE true
	END
`

type OIDCClaimFieldValuesParams struct {
	ClaimField     string    `db:"claim_field" json:"claim_field"`
	OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}

func (q *sqlQuerier) OIDCClaimFieldValues(ctx context.Context, arg OIDCClaimFieldValuesParams) ([]string, error) {
	rows, err := q.db.QueryContext(ctx, oIDCClaimFieldValues, arg.ClaimField, arg.OrganizationID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []string
	for rows.Next() {
		var jsonb_array_elements_text string
		if err := rows.Scan(&jsonb_array_elements_text); err != nil {
			return nil, err
		}
		items = append(items, jsonb_array_elements_text)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const oIDCClaimFields = `-- name: OIDCClaimFields :many
SELECT
	DISTINCT jsonb_object_keys(claims->'merged_claims')
FROM
	user_links
WHERE
    -- Only return rows where the top level key exists
	claims ? 'merged_claims' AND
    -- 'null' is the default value for the id_token_claims field
	-- jsonb 'null' is not the same as SQL NULL. Strip these out.
	jsonb_typeof(claims->'merged_claims') != 'null' AND
	login_type = 'oidc'
	AND CASE WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid  THEN
		user_links.user_id = ANY(SELECT organization_members.user_id FROM organization_members WHERE organization_id = $1)
		ELSE true
	END
`

// OIDCClaimFields returns a list of distinct keys in the the merged_claims fields.
// This query is used to generate the list of available sync fields for idp sync settings.
func (q *sqlQuerier) OIDCClaimFields(ctx context.Context, organizationID uuid.UUID) ([]string, error) {
	rows, err := q.db.QueryContext(ctx, oIDCClaimFields, organizationID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []string
	for rows.Next() {
		var jsonb_object_keys string
		if err := rows.Scan(&jsonb_object_keys); err != nil {
			return nil, err
		}
		items = append(items, jsonb_object_keys)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateUserLink = `-- name: UpdateUserLink :one
UPDATE
	user_links
SET
	oauth_access_token = $1,
	oauth_access_token_key_id = $2,
	oauth_refresh_token = $3,
	oauth_refresh_token_key_id = $4,
	oauth_expiry = $5,
	claims = $6
WHERE
	user_id = $7 AND login_type = $8 RETURNING user_id, login_type, linked_id, oauth_access_token, oauth_refresh_token, oauth_expiry, oauth_access_token_key_id, oauth_refresh_token_key_id, claims
`

type UpdateUserLinkParams struct {
	OAuthAccessToken       string         `db:"oauth_access_token" json:"oauth_access_token"`
	OAuthAccessTokenKeyID  sql.NullString `db:"oauth_access_token_key_id" json:"oauth_access_token_key_id"`
	OAuthRefreshToken      string         `db:"oauth_refresh_token" json:"oauth_refresh_token"`
	OAuthRefreshTokenKeyID sql.NullString `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
	OAuthExpiry            time.Time      `db:"oauth_expiry" json:"oauth_expiry"`
	Claims                 UserLinkClaims `db:"claims" json:"claims"`
	UserID                 uuid.UUID      `db:"user_id" json:"user_id"`
	LoginType              LoginType      `db:"login_type" json:"login_type"`
}

func (q *sqlQuerier) UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error) {
	row := q.db.QueryRowContext(ctx, updateUserLink,
		arg.OAuthAccessToken,
		arg.OAuthAccessTokenKeyID,
		arg.OAuthRefreshToken,
		arg.OAuthRefreshTokenKeyID,
		arg.OAuthExpiry,
		arg.Claims,
		arg.UserID,
		arg.LoginType,
	)
	var i UserLink
	err := row.Scan(
		&i.UserID,
		&i.LoginType,
		&i.LinkedID,
		&i.OAuthAccessToken,
		&i.OAuthRefreshToken,
		&i.OAuthExpiry,
		&i.OAuthAccessTokenKeyID,
		&i.OAuthRefreshTokenKeyID,
		&i.Claims,
	)
	return i, err
}

const createUserSecret = `-- name: CreateUserSecret :one
INSERT INTO user_secrets (
    id,
    user_id,
    name,
    description,
    value,
    value_key_id,
    env_name,
    file_path
) VALUES (
    $1,
    $2,
    $3,
    $4,
    $5,
    $6,
    $7,
    $8
) RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id
`

type CreateUserSecretParams struct {
	ID          uuid.UUID      `db:"id" json:"id"`
	UserID      uuid.UUID      `db:"user_id" json:"user_id"`
	Name        string         `db:"name" json:"name"`
	Description string         `db:"description" json:"description"`
	Value       string         `db:"value" json:"value"`
	ValueKeyID  sql.NullString `db:"value_key_id" json:"value_key_id"`
	EnvName     string         `db:"env_name" json:"env_name"`
	FilePath    string         `db:"file_path" json:"file_path"`
}

func (q *sqlQuerier) CreateUserSecret(ctx context.Context, arg CreateUserSecretParams) (UserSecret, error) {
	row := q.db.QueryRowContext(ctx, createUserSecret,
		arg.ID,
		arg.UserID,
		arg.Name,
		arg.Description,
		arg.Value,
		arg.ValueKeyID,
		arg.EnvName,
		arg.FilePath,
	)
	var i UserSecret
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Value,
		&i.EnvName,
		&i.FilePath,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ValueKeyID,
	)
	return i, err
}

const deleteUserSecretByUserIDAndName = `-- name: DeleteUserSecretByUserIDAndName :one
DELETE FROM user_secrets
WHERE user_id = $1 AND name = $2
RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id
`

type DeleteUserSecretByUserIDAndNameParams struct {
	UserID uuid.UUID `db:"user_id" json:"user_id"`
	Name   string    `db:"name" json:"name"`
}

func (q *sqlQuerier) DeleteUserSecretByUserIDAndName(ctx context.Context, arg DeleteUserSecretByUserIDAndNameParams) (UserSecret, error) {
	row := q.db.QueryRowContext(ctx, deleteUserSecretByUserIDAndName, arg.UserID, arg.Name)
	var i UserSecret
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Value,
		&i.EnvName,
		&i.FilePath,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ValueKeyID,
	)
	return i, err
}

const getUserSecretByID = `-- name: GetUserSecretByID :one
SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id
FROM user_secrets
WHERE id = $1
`

func (q *sqlQuerier) GetUserSecretByID(ctx context.Context, id uuid.UUID) (UserSecret, error) {
	row := q.db.QueryRowContext(ctx, getUserSecretByID, id)
	var i UserSecret
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Value,
		&i.EnvName,
		&i.FilePath,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ValueKeyID,
	)
	return i, err
}

const getUserSecretByUserIDAndName = `-- name: GetUserSecretByUserIDAndName :one
SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id
FROM user_secrets
WHERE user_id = $1 AND name = $2
`

type GetUserSecretByUserIDAndNameParams struct {
	UserID uuid.UUID `db:"user_id" json:"user_id"`
	Name   string    `db:"name" json:"name"`
}

func (q *sqlQuerier) GetUserSecretByUserIDAndName(ctx context.Context, arg GetUserSecretByUserIDAndNameParams) (UserSecret, error) {
	row := q.db.QueryRowContext(ctx, getUserSecretByUserIDAndName, arg.UserID, arg.Name)
	var i UserSecret
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Value,
		&i.EnvName,
		&i.FilePath,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ValueKeyID,
	)
	return i, err
}

const getUserSecretsTelemetrySummary = `-- name: GetUserSecretsTelemetrySummary :one
WITH active_users AS (
    SELECT id AS user_id
    FROM users
    WHERE deleted = false
      AND is_system = false
      AND status = 'active'::user_status
),
per_user AS (
    SELECT au.user_id, COUNT(us.id)::bigint AS n
    FROM active_users au
    LEFT JOIN user_secrets us ON us.user_id = au.user_id
    GROUP BY au.user_id
),
secrets_filtered AS (
    SELECT us.env_name, us.file_path
    FROM user_secrets us
    JOIN active_users au ON au.user_id = us.user_id
)
SELECT
    COUNT(*) FILTER (WHERE n > 0)::bigint                                                       AS users_with_secrets,
    (SELECT COUNT(*) FROM secrets_filtered)::bigint                                             AS total_secrets,
    (SELECT COUNT(*) FROM secrets_filtered WHERE env_name != '' AND file_path = '' )::bigint    AS env_name_only,
    (SELECT COUNT(*) FROM secrets_filtered WHERE env_name = ''  AND file_path != '')::bigint    AS file_path_only,
    (SELECT COUNT(*) FROM secrets_filtered WHERE env_name != '' AND file_path != '')::bigint    AS both,
    (SELECT COUNT(*) FROM secrets_filtered WHERE env_name = ''  AND file_path = '' )::bigint    AS neither,
    COALESCE(MAX(n), 0)::bigint                                                                 AS secrets_per_user_max,
    COALESCE(percentile_disc(0.25) WITHIN GROUP (ORDER BY n), 0)::bigint                        AS secrets_per_user_p25,
    COALESCE(percentile_disc(0.50) WITHIN GROUP (ORDER BY n), 0)::bigint                        AS secrets_per_user_p50,
    COALESCE(percentile_disc(0.75) WITHIN GROUP (ORDER BY n), 0)::bigint                        AS secrets_per_user_p75,
    COALESCE(percentile_disc(0.90) WITHIN GROUP (ORDER BY n), 0)::bigint                        AS secrets_per_user_p90
FROM per_user
`

type GetUserSecretsTelemetrySummaryRow struct {
	UsersWithSecrets  int64 `db:"users_with_secrets" json:"users_with_secrets"`
	TotalSecrets      int64 `db:"total_secrets" json:"total_secrets"`
	EnvNameOnly       int64 `db:"env_name_only" json:"env_name_only"`
	FilePathOnly      int64 `db:"file_path_only" json:"file_path_only"`
	Both              int64 `db:"both" json:"both"`
	Neither           int64 `db:"neither" json:"neither"`
	SecretsPerUserMax int64 `db:"secrets_per_user_max" json:"secrets_per_user_max"`
	SecretsPerUserP25 int64 `db:"secrets_per_user_p25" json:"secrets_per_user_p25"`
	SecretsPerUserP50 int64 `db:"secrets_per_user_p50" json:"secrets_per_user_p50"`
	SecretsPerUserP75 int64 `db:"secrets_per_user_p75" json:"secrets_per_user_p75"`
	SecretsPerUserP90 int64 `db:"secrets_per_user_p90" json:"secrets_per_user_p90"`
}

// Returns deployment-wide aggregates for the telemetry snapshot.
//
// The denominator for both user-level counts and the per-user
// distribution is active non-system users. Specifically:
//
//   - deleted = false: Coder soft-deletes by flipping users.deleted
//     rather than removing rows. The delete_deleted_user_resources()
//     trigger now removes their user_secrets, but soft-deleted users
//     are still excluded here so they don't dilute the percentile
//     distribution as zero-secret entries.
//   - status = 'active': dormant users (no recent activity) and
//     suspended users (explicitly disabled) cannot use secrets, so
//     they shouldn't dilute the percentile distribution as
//     zero-secret entries.
//   - is_system = false: internal subjects like the prebuilds user
//     never use secrets in the normal flow.
//
// Status transitions move users in and out of this denominator, so a
// snapshot's UsersWithSecrets can drop without any secret being
// deleted.
//
// The percentile distribution is computed across all active non-system
// users, including those with zero secrets, so the percentiles reflect
// deployment-wide adoption rather than only the power-user subset.
// percentile_disc returns an actual integer count from the underlying
// values rather than interpolating between rows.
func (q *sqlQuerier) GetUserSecretsTelemetrySummary(ctx context.Context) (GetUserSecretsTelemetrySummaryRow, error) {
	row := q.db.QueryRowContext(ctx, getUserSecretsTelemetrySummary)
	var i GetUserSecretsTelemetrySummaryRow
	err := row.Scan(
		&i.UsersWithSecrets,
		&i.TotalSecrets,
		&i.EnvNameOnly,
		&i.FilePathOnly,
		&i.Both,
		&i.Neither,
		&i.SecretsPerUserMax,
		&i.SecretsPerUserP25,
		&i.SecretsPerUserP50,
		&i.SecretsPerUserP75,
		&i.SecretsPerUserP90,
	)
	return i, err
}

const listUserSecrets = `-- name: ListUserSecrets :many
SELECT
    id, user_id, name, description,
    env_name, file_path,
    created_at, updated_at
FROM user_secrets
WHERE user_id = $1
ORDER BY name ASC
`

type ListUserSecretsRow struct {
	ID          uuid.UUID `db:"id" json:"id"`
	UserID      uuid.UUID `db:"user_id" json:"user_id"`
	Name        string    `db:"name" json:"name"`
	Description string    `db:"description" json:"description"`
	EnvName     string    `db:"env_name" json:"env_name"`
	FilePath    string    `db:"file_path" json:"file_path"`
	CreatedAt   time.Time `db:"created_at" json:"created_at"`
	UpdatedAt   time.Time `db:"updated_at" json:"updated_at"`
}

// Returns metadata only (no value or value_key_id) for the
// REST API list and get endpoints.
func (q *sqlQuerier) ListUserSecrets(ctx context.Context, userID uuid.UUID) ([]ListUserSecretsRow, error) {
	rows, err := q.db.QueryContext(ctx, listUserSecrets, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListUserSecretsRow
	for rows.Next() {
		var i ListUserSecretsRow
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.Name,
			&i.Description,
			&i.EnvName,
			&i.FilePath,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listUserSecretsWithValues = `-- name: ListUserSecretsWithValues :many
SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id
FROM user_secrets
WHERE user_id = $1
ORDER BY name ASC
`

// Returns all columns including the secret value. Used by the
// provisioner (build-time injection) and the agent manifest
// (runtime injection).
func (q *sqlQuerier) ListUserSecretsWithValues(ctx context.Context, userID uuid.UUID) ([]UserSecret, error) {
	rows, err := q.db.QueryContext(ctx, listUserSecretsWithValues, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UserSecret
	for rows.Next() {
		var i UserSecret
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.Name,
			&i.Description,
			&i.Value,
			&i.EnvName,
			&i.FilePath,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.ValueKeyID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateUserSecretByUserIDAndName = `-- name: UpdateUserSecretByUserIDAndName :one
UPDATE user_secrets
SET
    value       = CASE WHEN $1::bool THEN $2 ELSE value END,
    value_key_id = CASE WHEN $1::bool THEN $3 ELSE value_key_id END,
    description = CASE WHEN $4::bool THEN $5 ELSE description END,
    env_name    = CASE WHEN $6::bool THEN $7 ELSE env_name END,
    file_path   = CASE WHEN $8::bool THEN $9 ELSE file_path END,
    updated_at  = CURRENT_TIMESTAMP
WHERE user_id = $10 AND name = $11
RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id
`

type UpdateUserSecretByUserIDAndNameParams struct {
	UpdateValue       bool           `db:"update_value" json:"update_value"`
	Value             string         `db:"value" json:"value"`
	ValueKeyID        sql.NullString `db:"value_key_id" json:"value_key_id"`
	UpdateDescription bool           `db:"update_description" json:"update_description"`
	Description       string         `db:"description" json:"description"`
	UpdateEnvName     bool           `db:"update_env_name" json:"update_env_name"`
	EnvName           string         `db:"env_name" json:"env_name"`
	UpdateFilePath    bool           `db:"update_file_path" json:"update_file_path"`
	FilePath          string         `db:"file_path" json:"file_path"`
	UserID            uuid.UUID      `db:"user_id" json:"user_id"`
	Name              string         `db:"name" json:"name"`
}

func (q *sqlQuerier) UpdateUserSecretByUserIDAndName(ctx context.Context, arg UpdateUserSecretByUserIDAndNameParams) (UserSecret, error) {
	row := q.db.QueryRowContext(ctx, updateUserSecretByUserIDAndName,
		arg.UpdateValue,
		arg.Value,
		arg.ValueKeyID,
		arg.UpdateDescription,
		arg.Description,
		arg.UpdateEnvName,
		arg.EnvName,
		arg.UpdateFilePath,
		arg.FilePath,
		arg.UserID,
		arg.Name,
	)
	var i UserSecret
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Value,
		&i.EnvName,
		&i.FilePath,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.ValueKeyID,
	)
	return i, err
}

const deleteUserSkillByUserIDAndName = `-- name: DeleteUserSkillByUserIDAndName :one
DELETE FROM user_skills
WHERE user_id = $1 AND name = $2
RETURNING id, user_id, name, description, content, created_at, updated_at
`

type DeleteUserSkillByUserIDAndNameParams struct {
	UserID uuid.UUID `db:"user_id" json:"user_id"`
	Name   string    `db:"name" json:"name"`
}

func (q *sqlQuerier) DeleteUserSkillByUserIDAndName(ctx context.Context, arg DeleteUserSkillByUserIDAndNameParams) (UserSkill, error) {
	row := q.db.QueryRowContext(ctx, deleteUserSkillByUserIDAndName, arg.UserID, arg.Name)
	var i UserSkill
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Content,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const getUserSkillByUserIDAndName = `-- name: GetUserSkillByUserIDAndName :one
SELECT id, user_id, name, description, content, created_at, updated_at
FROM user_skills
WHERE user_id = $1 AND name = $2
`

type GetUserSkillByUserIDAndNameParams struct {
	UserID uuid.UUID `db:"user_id" json:"user_id"`
	Name   string    `db:"name" json:"name"`
}

func (q *sqlQuerier) GetUserSkillByUserIDAndName(ctx context.Context, arg GetUserSkillByUserIDAndNameParams) (UserSkill, error) {
	row := q.db.QueryRowContext(ctx, getUserSkillByUserIDAndName, arg.UserID, arg.Name)
	var i UserSkill
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Content,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const insertUserSkill = `-- name: InsertUserSkill :one
INSERT INTO user_skills (id, user_id, name, description, content)
VALUES ($1::uuid, $2::uuid, $3::text, $4::text, $5::text)
RETURNING id, user_id, name, description, content, created_at, updated_at
`

type InsertUserSkillParams struct {
	ID          uuid.UUID `db:"id" json:"id"`
	UserID      uuid.UUID `db:"user_id" json:"user_id"`
	Name        string    `db:"name" json:"name"`
	Description string    `db:"description" json:"description"`
	Content     string    `db:"content" json:"content"`
}

func (q *sqlQuerier) InsertUserSkill(ctx context.Context, arg InsertUserSkillParams) (UserSkill, error) {
	row := q.db.QueryRowContext(ctx, insertUserSkill,
		arg.ID,
		arg.UserID,
		arg.Name,
		arg.Description,
		arg.Content,
	)
	var i UserSkill
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Content,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const listUserSkillMetadataByUserID = `-- name: ListUserSkillMetadataByUserID :many
SELECT
    id, user_id, name, description, created_at, updated_at
FROM user_skills
WHERE user_id = $1
ORDER BY name ASC
`

type ListUserSkillMetadataByUserIDRow struct {
	ID          uuid.UUID `db:"id" json:"id"`
	UserID      uuid.UUID `db:"user_id" json:"user_id"`
	Name        string    `db:"name" json:"name"`
	Description string    `db:"description" json:"description"`
	CreatedAt   time.Time `db:"created_at" json:"created_at"`
	UpdatedAt   time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]ListUserSkillMetadataByUserIDRow, error) {
	rows, err := q.db.QueryContext(ctx, listUserSkillMetadataByUserID, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListUserSkillMetadataByUserIDRow
	for rows.Next() {
		var i ListUserSkillMetadataByUserIDRow
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.Name,
			&i.Description,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateUserSkillByUserIDAndName = `-- name: UpdateUserSkillByUserIDAndName :one
UPDATE user_skills
SET
    description = $1,
    content     = $2,
    updated_at  = now()
WHERE user_id = $3 AND name = $4
RETURNING id, user_id, name, description, content, created_at, updated_at
`

type UpdateUserSkillByUserIDAndNameParams struct {
	Description string    `db:"description" json:"description"`
	Content     string    `db:"content" json:"content"`
	UserID      uuid.UUID `db:"user_id" json:"user_id"`
	Name        string    `db:"name" json:"name"`
}

func (q *sqlQuerier) UpdateUserSkillByUserIDAndName(ctx context.Context, arg UpdateUserSkillByUserIDAndNameParams) (UserSkill, error) {
	row := q.db.QueryRowContext(ctx, updateUserSkillByUserIDAndName,
		arg.Description,
		arg.Content,
		arg.UserID,
		arg.Name,
	)
	var i UserSkill
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.Content,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

const allUserIDs = `-- name: AllUserIDs :many
SELECT DISTINCT id FROM USERS
	WHERE CASE WHEN $1::bool THEN TRUE ELSE is_system = false END
`

// AllUserIDs returns all UserIDs regardless of user status or deletion.
func (q *sqlQuerier) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error) {
	rows, err := q.db.QueryContext(ctx, allUserIDs, includeSystem)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []uuid.UUID
	for rows.Next() {
		var id uuid.UUID
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		items = append(items, id)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteUserChatCompactionThreshold = `-- name: DeleteUserChatCompactionThreshold :exec
DELETE FROM user_configs WHERE user_id = $1 AND key = $2
`

type DeleteUserChatCompactionThresholdParams struct {
	UserID uuid.UUID `db:"user_id" json:"user_id"`
	Key    string    `db:"key" json:"key"`
}

func (q *sqlQuerier) DeleteUserChatCompactionThreshold(ctx context.Context, arg DeleteUserChatCompactionThresholdParams) error {
	_, err := q.db.ExecContext(ctx, deleteUserChatCompactionThreshold, arg.UserID, arg.Key)
	return err
}

const getActiveUserCount = `-- name: GetActiveUserCount :one
SELECT
	COUNT(*)
FROM
	users
WHERE
	status = 'active'::user_status AND deleted = false
	AND is_service_account = false
	AND CASE WHEN $1::bool THEN TRUE ELSE is_system = false END
`

func (q *sqlQuerier) GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error) {
	row := q.db.QueryRowContext(ctx, getActiveUserCount, includeSystem)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const getAuthorizationUserRoles = `-- name: GetAuthorizationUserRoles :one
SELECT
	-- username and email are returned just to help for logging purposes
	-- status is used to enforce 'suspended' users, as all roles are ignored
	--	when suspended.
	id, username, status, email,
	-- All user roles, including their org roles.
	array_cat(
		-- All users are members
		array_append(users.rbac_roles, 'member'),
		(
			SELECT
				-- The roles are returned as a flat array, org scoped and site side.
				-- Concatenating the organization id scopes the organization roles.
				array_agg(org_roles || ':' || organization_members.organization_id::text)
			FROM
				organization_members,
				-- All org members get an implied role for their orgs. Most members
				-- get organization-member, but service accounts will get
				-- organization-service-account instead. They're largely the same,
				-- but having them be distinct means we can allow configuring
				-- service-accounts to have slightly broader permissions–such as
				-- for workspace sharing.
				unnest(
					array_append(
						roles,
						CASE WHEN users.is_service_account THEN
							'organization-service-account'
						ELSE
							'organization-member'
						END
					)
				) AS org_roles
			WHERE
				user_id = users.id
		)
	) :: text[] AS roles,
	-- All groups the user is in.
	(
		SELECT
			array_agg(
				group_members.group_id :: text
			)
		FROM
			group_members
		WHERE
			user_id = users.id
	) :: text[] AS groups
FROM
	users
WHERE
	id = $1
`

type GetAuthorizationUserRolesRow struct {
	ID       uuid.UUID  `db:"id" json:"id"`
	Username string     `db:"username" json:"username"`
	Status   UserStatus `db:"status" json:"status"`
	Email    string     `db:"email" json:"email"`
	Roles    []string   `db:"roles" json:"roles"`
	Groups   []string   `db:"groups" json:"groups"`
}

// This function returns roles for authorization purposes. Implied member roles
// are included.
func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) {
	row := q.db.QueryRowContext(ctx, getAuthorizationUserRoles, userID)
	var i GetAuthorizationUserRolesRow
	err := row.Scan(
		&i.ID,
		&i.Username,
		&i.Status,
		&i.Email,
		pq.Array(&i.Roles),
		pq.Array(&i.Groups),
	)
	return i, err
}

const getUserAgentChatSendShortcut = `-- name: GetUserAgentChatSendShortcut :one
SELECT
	value AS agent_chat_send_shortcut
FROM
	user_configs
WHERE
	user_id = $1
	AND key = 'preference_agent_chat_send_shortcut'
`

func (q *sqlQuerier) GetUserAgentChatSendShortcut(ctx context.Context, userID uuid.UUID) (string, error) {
	row := q.db.QueryRowContext(ctx, getUserAgentChatSendShortcut, userID)
	var agent_chat_send_shortcut string
	err := row.Scan(&agent_chat_send_shortcut)
	return agent_chat_send_shortcut, err
}

const getUserAppearanceSettings = `-- name: GetUserAppearanceSettings :one
SELECT
	COALESCE(MAX(value) FILTER (WHERE key = 'theme_preference'), '')::text AS theme_preference,
	COALESCE(MAX(value) FILTER (WHERE key = 'theme_mode'), '')::text AS theme_mode,
	COALESCE(MAX(value) FILTER (WHERE key = 'theme_light'), '')::text AS theme_light,
	COALESCE(MAX(value) FILTER (WHERE key = 'theme_dark'), '')::text AS theme_dark,
	COALESCE(MAX(value) FILTER (WHERE key = 'terminal_font'), '')::text AS terminal_font
FROM
	user_configs
WHERE
	user_id = $1
	AND key IN (
		'theme_preference',
		'theme_mode',
		'theme_light',
		'theme_dark',
		'terminal_font'
	)
`

type GetUserAppearanceSettingsRow struct {
	ThemePreference string `db:"theme_preference" json:"theme_preference"`
	ThemeMode       string `db:"theme_mode" json:"theme_mode"`
	ThemeLight      string `db:"theme_light" json:"theme_light"`
	ThemeDark       string `db:"theme_dark" json:"theme_dark"`
	TerminalFont    string `db:"terminal_font" json:"terminal_font"`
}

func (q *sqlQuerier) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (GetUserAppearanceSettingsRow, error) {
	row := q.db.QueryRowContext(ctx, getUserAppearanceSettings, userID)
	var i GetUserAppearanceSettingsRow
	err := row.Scan(
		&i.ThemePreference,
		&i.ThemeMode,
		&i.ThemeLight,
		&i.ThemeDark,
		&i.TerminalFont,
	)
	return i, err
}

const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT
	id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
FROM
	users
WHERE
	(LOWER(username) = LOWER($1) OR ($2 != '' AND LOWER(email) = LOWER($2))) AND
	deleted = false
LIMIT
	1
`

type GetUserByEmailOrUsernameParams struct {
	Username string      `db:"username" json:"username"`
	Email    interface{} `db:"email" json:"email"`
}

func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) {
	row := q.db.QueryRowContext(ctx, getUserByEmailOrUsername, arg.Username, arg.Email)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const getUserByID = `-- name: GetUserByID :one
SELECT
	id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
FROM
	users
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
	row := q.db.QueryRowContext(ctx, getUserByID, id)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const getUserChatCompactionThreshold = `-- name: GetUserChatCompactionThreshold :one
SELECT value AS threshold_percent FROM user_configs
WHERE user_id = $1 AND key = $2
`

type GetUserChatCompactionThresholdParams struct {
	UserID uuid.UUID `db:"user_id" json:"user_id"`
	Key    string    `db:"key" json:"key"`
}

func (q *sqlQuerier) GetUserChatCompactionThreshold(ctx context.Context, arg GetUserChatCompactionThresholdParams) (string, error) {
	row := q.db.QueryRowContext(ctx, getUserChatCompactionThreshold, arg.UserID, arg.Key)
	var threshold_percent string
	err := row.Scan(&threshold_percent)
	return threshold_percent, err
}

const getUserChatCustomPrompt = `-- name: GetUserChatCustomPrompt :one
SELECT
	value as chat_custom_prompt
FROM
	user_configs
WHERE
	user_id = $1
	AND key = 'chat_custom_prompt'
`

func (q *sqlQuerier) GetUserChatCustomPrompt(ctx context.Context, userID uuid.UUID) (string, error) {
	row := q.db.QueryRowContext(ctx, getUserChatCustomPrompt, userID)
	var chat_custom_prompt string
	err := row.Scan(&chat_custom_prompt)
	return chat_custom_prompt, err
}

const getUserChatDebugLoggingEnabled = `-- name: GetUserChatDebugLoggingEnabled :one
SELECT
	COALESCE((
		SELECT value = 'true'
		FROM user_configs
		WHERE user_id = $1
			AND key = 'chat_debug_logging_enabled'
	), false) :: boolean AS debug_logging_enabled
`

func (q *sqlQuerier) GetUserChatDebugLoggingEnabled(ctx context.Context, userID uuid.UUID) (bool, error) {
	row := q.db.QueryRowContext(ctx, getUserChatDebugLoggingEnabled, userID)
	var debug_logging_enabled bool
	err := row.Scan(&debug_logging_enabled)
	return debug_logging_enabled, err
}

const getUserChatPersonalModelOverride = `-- name: GetUserChatPersonalModelOverride :one
SELECT value AS personal_model_override FROM user_configs
WHERE user_id = $1
	AND key = $2
`

type GetUserChatPersonalModelOverrideParams struct {
	UserID uuid.UUID `db:"user_id" json:"user_id"`
	Key    string    `db:"key" json:"key"`
}

func (q *sqlQuerier) GetUserChatPersonalModelOverride(ctx context.Context, arg GetUserChatPersonalModelOverrideParams) (string, error) {
	row := q.db.QueryRowContext(ctx, getUserChatPersonalModelOverride, arg.UserID, arg.Key)
	var personal_model_override string
	err := row.Scan(&personal_model_override)
	return personal_model_override, err
}

const getUserCodeDiffDisplayMode = `-- name: GetUserCodeDiffDisplayMode :one
SELECT
	value AS code_diff_display_mode
FROM
	user_configs
WHERE
	user_id = $1
	AND key = 'preference_code_diff_display_mode'
`

func (q *sqlQuerier) GetUserCodeDiffDisplayMode(ctx context.Context, userID uuid.UUID) (string, error) {
	row := q.db.QueryRowContext(ctx, getUserCodeDiffDisplayMode, userID)
	var code_diff_display_mode string
	err := row.Scan(&code_diff_display_mode)
	return code_diff_display_mode, err
}

const getUserCount = `-- name: GetUserCount :one
SELECT
	COUNT(*)
FROM
	users
WHERE
	deleted = false
  	AND CASE WHEN $1::bool THEN TRUE ELSE is_system = false END
`

func (q *sqlQuerier) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) {
	row := q.db.QueryRowContext(ctx, getUserCount, includeSystem)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const getUserShellToolDisplayMode = `-- name: GetUserShellToolDisplayMode :one
SELECT
	value AS shell_tool_display_mode
FROM
	user_configs
WHERE
	user_id = $1
	AND key = 'preference_shell_tool_display_mode'
`

func (q *sqlQuerier) GetUserShellToolDisplayMode(ctx context.Context, userID uuid.UUID) (string, error) {
	row := q.db.QueryRowContext(ctx, getUserShellToolDisplayMode, userID)
	var shell_tool_display_mode string
	err := row.Scan(&shell_tool_display_mode)
	return shell_tool_display_mode, err
}

const getUserTaskNotificationAlertDismissed = `-- name: GetUserTaskNotificationAlertDismissed :one
SELECT
	value::boolean as task_notification_alert_dismissed
FROM
	user_configs
WHERE
	user_id = $1
	AND key = 'preference_task_notification_alert_dismissed'
`

func (q *sqlQuerier) GetUserTaskNotificationAlertDismissed(ctx context.Context, userID uuid.UUID) (bool, error) {
	row := q.db.QueryRowContext(ctx, getUserTaskNotificationAlertDismissed, userID)
	var task_notification_alert_dismissed bool
	err := row.Scan(&task_notification_alert_dismissed)
	return task_notification_alert_dismissed, err
}

const getUserThinkingDisplayMode = `-- name: GetUserThinkingDisplayMode :one
SELECT
	value AS thinking_display_mode
FROM
	user_configs
WHERE
	user_id = $1
	AND key = 'preference_thinking_display_mode'
`

func (q *sqlQuerier) GetUserThinkingDisplayMode(ctx context.Context, userID uuid.UUID) (string, error) {
	row := q.db.QueryRowContext(ctx, getUserThinkingDisplayMode, userID)
	var thinking_display_mode string
	err := row.Scan(&thinking_display_mode)
	return thinking_display_mode, err
}

const getUsers = `-- name: GetUsers :many
SELECT
	id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros, COUNT(*) OVER() AS count
FROM
	users
WHERE
	users.deleted = false
	AND CASE
		-- This allows using the last element on a page as effectively a cursor.
		-- This is an important option for scripts that need to paginate without
		-- duplicating or missing data.
		WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
			-- The pagination cursor is the last ID of the previous page.
			-- The query is ordered by the username field, so select all
			-- rows after the cursor.
			(LOWER(username)) > (
				SELECT
					LOWER(username)
				FROM
					users
				WHERE
					id = $1
			)
		)
		ELSE true
	END
	-- Start filters
	-- Filter by email or username
	AND CASE
		WHEN $2 :: text != '' THEN (
			email ILIKE concat('%', $2, '%')
			OR username ILIKE concat('%', $2, '%')
		)
		ELSE true
	END
	-- Filter by name (display name)
	AND CASE
		WHEN $3 :: text != '' THEN
			name ILIKE concat('%', $3, '%')
		ELSE true
	END
	-- Filter by exact username
	AND CASE
	  WHEN $4 :: text != '' THEN
		  lower(username) = lower($4)
	  ELSE true
	END
  	-- Filter by exact email
  	AND CASE
	  WHEN $5 :: text != '' THEN
		  lower(email) = lower($5)
	  ELSE true
	END
	-- Filter by status
	AND CASE
		-- @status needs to be a text because it can be empty, If it was
		-- user_status enum, it would not.
		WHEN cardinality($6 :: user_status[]) > 0 THEN
			status = ANY($6 :: user_status[])
		ELSE true
	END
	-- Filter by rbac_roles
	AND CASE
		-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
		-- everyone is a member.
		WHEN cardinality($7 :: text[]) > 0 AND 'member' != ANY($7 :: text[]) THEN
			rbac_roles && $7 :: text[]
		ELSE true
	END
	-- Filter by last_seen
	AND CASE
		WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			last_seen_at <= $8
		ELSE true
	END
	AND CASE
		WHEN $9 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			last_seen_at >= $9
		ELSE true
	END
	-- Filter by created_at
	AND CASE
		WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			created_at <= $10
		ELSE true
	END
	AND CASE
		WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
			created_at >= $11
		ELSE true
	END
	-- Filter by system type
	AND CASE
		WHEN $12::bool THEN TRUE
		ELSE is_system = false
	END
	-- Filter by github.com user ID
	AND CASE
		WHEN $13 :: bigint != 0 THEN
			github_com_user_id = $13
		ELSE true
	END
	-- Filter by login_type
	AND CASE
		WHEN cardinality($14 :: login_type[]) > 0 THEN
			login_type = ANY($14 :: login_type[])
		ELSE true
	END
	-- Filter by service account.
	AND CASE
		WHEN $15 :: boolean IS NOT NULL THEN
			is_service_account = $15 :: boolean
		ELSE true
	END
	-- End of filters

	-- Authorize Filter clause will be injected below in GetAuthorizedUsers
	-- @authorize_filter
ORDER BY
	-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
	LOWER(username) ASC OFFSET $16
LIMIT
	-- A null limit means "no limit", so 0 means return all
	NULLIF($17 :: int, 0)
`

type GetUsersParams struct {
	AfterID          uuid.UUID    `db:"after_id" json:"after_id"`
	Search           string       `db:"search" json:"search"`
	Name             string       `db:"name" json:"name"`
	ExactUsername    string       `db:"exact_username" json:"exact_username"`
	ExactEmail       string       `db:"exact_email" json:"exact_email"`
	Status           []UserStatus `db:"status" json:"status"`
	RbacRole         []string     `db:"rbac_role" json:"rbac_role"`
	LastSeenBefore   time.Time    `db:"last_seen_before" json:"last_seen_before"`
	LastSeenAfter    time.Time    `db:"last_seen_after" json:"last_seen_after"`
	CreatedBefore    time.Time    `db:"created_before" json:"created_before"`
	CreatedAfter     time.Time    `db:"created_after" json:"created_after"`
	IncludeSystem    bool         `db:"include_system" json:"include_system"`
	GithubComUserID  int64        `db:"github_com_user_id" json:"github_com_user_id"`
	LoginType        []LoginType  `db:"login_type" json:"login_type"`
	IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"`
	OffsetOpt        int32        `db:"offset_opt" json:"offset_opt"`
	LimitOpt         int32        `db:"limit_opt" json:"limit_opt"`
}

type GetUsersRow struct {
	ID                       uuid.UUID      `db:"id" json:"id"`
	Email                    string         `db:"email" json:"email"`
	Username                 string         `db:"username" json:"username"`
	HashedPassword           []byte         `db:"hashed_password" json:"hashed_password"`
	CreatedAt                time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt                time.Time      `db:"updated_at" json:"updated_at"`
	Status                   UserStatus     `db:"status" json:"status"`
	RBACRoles                pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
	LoginType                LoginType      `db:"login_type" json:"login_type"`
	AvatarURL                string         `db:"avatar_url" json:"avatar_url"`
	Deleted                  bool           `db:"deleted" json:"deleted"`
	LastSeenAt               time.Time      `db:"last_seen_at" json:"last_seen_at"`
	QuietHoursSchedule       string         `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
	Name                     string         `db:"name" json:"name"`
	GithubComUserID          sql.NullInt64  `db:"github_com_user_id" json:"github_com_user_id"`
	HashedOneTimePasscode    []byte         `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"`
	OneTimePasscodeExpiresAt sql.NullTime   `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"`
	IsSystem                 bool           `db:"is_system" json:"is_system"`
	IsServiceAccount         bool           `db:"is_service_account" json:"is_service_account"`
	ChatSpendLimitMicros     sql.NullInt64  `db:"chat_spend_limit_micros" json:"chat_spend_limit_micros"`
	Count                    int64          `db:"count" json:"count"`
}

// This will never return deleted users.
func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) {
	rows, err := q.db.QueryContext(ctx, getUsers,
		arg.AfterID,
		arg.Search,
		arg.Name,
		arg.ExactUsername,
		arg.ExactEmail,
		pq.Array(arg.Status),
		pq.Array(arg.RbacRole),
		arg.LastSeenBefore,
		arg.LastSeenAfter,
		arg.CreatedBefore,
		arg.CreatedAfter,
		arg.IncludeSystem,
		arg.GithubComUserID,
		pq.Array(arg.LoginType),
		arg.IsServiceAccount,
		arg.OffsetOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetUsersRow
	for rows.Next() {
		var i GetUsersRow
		if err := rows.Scan(
			&i.ID,
			&i.Email,
			&i.Username,
			&i.HashedPassword,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Status,
			&i.RBACRoles,
			&i.LoginType,
			&i.AvatarURL,
			&i.Deleted,
			&i.LastSeenAt,
			&i.QuietHoursSchedule,
			&i.Name,
			&i.GithubComUserID,
			&i.HashedOneTimePasscode,
			&i.OneTimePasscodeExpiresAt,
			&i.IsSystem,
			&i.IsServiceAccount,
			&i.ChatSpendLimitMicros,
			&i.Count,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUsersByIDs = `-- name: GetUsersByIDs :many
SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros FROM users WHERE id = ANY($1 :: uuid [ ])
`

// This shouldn't check for deleted, because it's frequently used
// to look up references to actions. eg. a user could build a workspace
// for another user, then be deleted... we still want them to appear!
func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) {
	rows, err := q.db.QueryContext(ctx, getUsersByIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []User
	for rows.Next() {
		var i User
		if err := rows.Scan(
			&i.ID,
			&i.Email,
			&i.Username,
			&i.HashedPassword,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Status,
			&i.RBACRoles,
			&i.LoginType,
			&i.AvatarURL,
			&i.Deleted,
			&i.LastSeenAt,
			&i.QuietHoursSchedule,
			&i.Name,
			&i.GithubComUserID,
			&i.HashedOneTimePasscode,
			&i.OneTimePasscodeExpiresAt,
			&i.IsSystem,
			&i.IsServiceAccount,
			&i.ChatSpendLimitMicros,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertUser = `-- name: InsertUser :one
INSERT INTO
	users (
		id,
		email,
		username,
		name,
		hashed_password,
		created_at,
		updated_at,
		rbac_roles,
		login_type,
		status,
		is_service_account
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9,
		-- if the status passed in is empty, fallback to dormant, which is what
		-- we were doing before.
		COALESCE(NULLIF($10::text, '')::user_status, 'dormant'::user_status),
		$11::bool
	) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`

type InsertUserParams struct {
	ID               uuid.UUID      `db:"id" json:"id"`
	Email            string         `db:"email" json:"email"`
	Username         string         `db:"username" json:"username"`
	Name             string         `db:"name" json:"name"`
	HashedPassword   []byte         `db:"hashed_password" json:"hashed_password"`
	CreatedAt        time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt        time.Time      `db:"updated_at" json:"updated_at"`
	RBACRoles        pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
	LoginType        LoginType      `db:"login_type" json:"login_type"`
	Status           string         `db:"status" json:"status"`
	IsServiceAccount bool           `db:"is_service_account" json:"is_service_account"`
}

func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
	row := q.db.QueryRowContext(ctx, insertUser,
		arg.ID,
		arg.Email,
		arg.Username,
		arg.Name,
		arg.HashedPassword,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.RBACRoles,
		arg.LoginType,
		arg.Status,
		arg.IsServiceAccount,
	)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const listUserChatCompactionThresholds = `-- name: ListUserChatCompactionThresholds :many
SELECT user_id, key, value FROM user_configs
WHERE user_id = $1
	AND key LIKE 'chat\_compaction\_threshold\_pct:%'
ORDER BY key
`

func (q *sqlQuerier) ListUserChatCompactionThresholds(ctx context.Context, userID uuid.UUID) ([]UserConfig, error) {
	rows, err := q.db.QueryContext(ctx, listUserChatCompactionThresholds, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UserConfig
	for rows.Next() {
		var i UserConfig
		if err := rows.Scan(&i.UserID, &i.Key, &i.Value); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listUserChatPersonalModelOverrides = `-- name: ListUserChatPersonalModelOverrides :many
SELECT key, value FROM user_configs
WHERE user_id = $1
	AND key LIKE 'chat\_personal\_model\_override:%'
ORDER BY key
`

type ListUserChatPersonalModelOverridesRow struct {
	Key   string `db:"key" json:"key"`
	Value string `db:"value" json:"value"`
}

func (q *sqlQuerier) ListUserChatPersonalModelOverrides(ctx context.Context, userID uuid.UUID) ([]ListUserChatPersonalModelOverridesRow, error) {
	rows, err := q.db.QueryContext(ctx, listUserChatPersonalModelOverrides, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListUserChatPersonalModelOverridesRow
	for rows.Next() {
		var i ListUserChatPersonalModelOverridesRow
		if err := rows.Scan(&i.Key, &i.Value); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateInactiveUsersToDormant = `-- name: UpdateInactiveUsersToDormant :many
UPDATE
    users
SET
    status = 'dormant'::user_status,
    updated_at = $1
WHERE
    last_seen_at < $2 :: timestamp
    AND status = 'active'::user_status
		AND NOT is_system
RETURNING id, email, username, last_seen_at
`

type UpdateInactiveUsersToDormantParams struct {
	UpdatedAt     time.Time `db:"updated_at" json:"updated_at"`
	LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
}

type UpdateInactiveUsersToDormantRow struct {
	ID         uuid.UUID `db:"id" json:"id"`
	Email      string    `db:"email" json:"email"`
	Username   string    `db:"username" json:"username"`
	LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
}

func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) {
	rows, err := q.db.QueryContext(ctx, updateInactiveUsersToDormant, arg.UpdatedAt, arg.LastSeenAfter)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []UpdateInactiveUsersToDormantRow
	for rows.Next() {
		var i UpdateInactiveUsersToDormantRow
		if err := rows.Scan(
			&i.ID,
			&i.Email,
			&i.Username,
			&i.LastSeenAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateUserAgentChatSendShortcut = `-- name: UpdateUserAgentChatSendShortcut :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'preference_agent_chat_send_shortcut', $2::text)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'preference_agent_chat_send_shortcut'
RETURNING value AS agent_chat_send_shortcut
`

type UpdateUserAgentChatSendShortcutParams struct {
	UserID                uuid.UUID `db:"user_id" json:"user_id"`
	AgentChatSendShortcut string    `db:"agent_chat_send_shortcut" json:"agent_chat_send_shortcut"`
}

func (q *sqlQuerier) UpdateUserAgentChatSendShortcut(ctx context.Context, arg UpdateUserAgentChatSendShortcutParams) (string, error) {
	row := q.db.QueryRowContext(ctx, updateUserAgentChatSendShortcut, arg.UserID, arg.AgentChatSendShortcut)
	var agent_chat_send_shortcut string
	err := row.Scan(&agent_chat_send_shortcut)
	return agent_chat_send_shortcut, err
}

const updateUserChatCompactionThreshold = `-- name: UpdateUserChatCompactionThreshold :one
INSERT INTO user_configs (user_id, key, value)
VALUES ($1, $2, ($3::int)::text)
ON CONFLICT ON CONSTRAINT user_configs_pkey
DO UPDATE SET value = ($3::int)::text
RETURNING user_id, key, value
`

type UpdateUserChatCompactionThresholdParams struct {
	UserID           uuid.UUID `db:"user_id" json:"user_id"`
	Key              string    `db:"key" json:"key"`
	ThresholdPercent int32     `db:"threshold_percent" json:"threshold_percent"`
}

func (q *sqlQuerier) UpdateUserChatCompactionThreshold(ctx context.Context, arg UpdateUserChatCompactionThresholdParams) (UserConfig, error) {
	row := q.db.QueryRowContext(ctx, updateUserChatCompactionThreshold, arg.UserID, arg.Key, arg.ThresholdPercent)
	var i UserConfig
	err := row.Scan(&i.UserID, &i.Key, &i.Value)
	return i, err
}

const updateUserChatCustomPrompt = `-- name: UpdateUserChatCustomPrompt :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'chat_custom_prompt', $2)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'chat_custom_prompt'
RETURNING user_id, key, value
`

type UpdateUserChatCustomPromptParams struct {
	UserID           uuid.UUID `db:"user_id" json:"user_id"`
	ChatCustomPrompt string    `db:"chat_custom_prompt" json:"chat_custom_prompt"`
}

func (q *sqlQuerier) UpdateUserChatCustomPrompt(ctx context.Context, arg UpdateUserChatCustomPromptParams) (UserConfig, error) {
	row := q.db.QueryRowContext(ctx, updateUserChatCustomPrompt, arg.UserID, arg.ChatCustomPrompt)
	var i UserConfig
	err := row.Scan(&i.UserID, &i.Key, &i.Value)
	return i, err
}

const updateUserCodeDiffDisplayMode = `-- name: UpdateUserCodeDiffDisplayMode :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'preference_code_diff_display_mode', $2::text)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'preference_code_diff_display_mode'
RETURNING value AS code_diff_display_mode
`

type UpdateUserCodeDiffDisplayModeParams struct {
	UserID              uuid.UUID `db:"user_id" json:"user_id"`
	CodeDiffDisplayMode string    `db:"code_diff_display_mode" json:"code_diff_display_mode"`
}

func (q *sqlQuerier) UpdateUserCodeDiffDisplayMode(ctx context.Context, arg UpdateUserCodeDiffDisplayModeParams) (string, error) {
	row := q.db.QueryRowContext(ctx, updateUserCodeDiffDisplayMode, arg.UserID, arg.CodeDiffDisplayMode)
	var code_diff_display_mode string
	err := row.Scan(&code_diff_display_mode)
	return code_diff_display_mode, err
}

const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec
UPDATE
	users
SET
	deleted = true
WHERE
	id = $1
`

func (q *sqlQuerier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, updateUserDeletedByID, id)
	return err
}

const updateUserGithubComUserID = `-- name: UpdateUserGithubComUserID :exec
UPDATE
	users
SET
	github_com_user_id = $2
WHERE
	id = $1
`

type UpdateUserGithubComUserIDParams struct {
	ID              uuid.UUID     `db:"id" json:"id"`
	GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
}

func (q *sqlQuerier) UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error {
	_, err := q.db.ExecContext(ctx, updateUserGithubComUserID, arg.ID, arg.GithubComUserID)
	return err
}

const updateUserHashedOneTimePasscode = `-- name: UpdateUserHashedOneTimePasscode :exec
UPDATE
    users
SET
    hashed_one_time_passcode = $2,
    one_time_passcode_expires_at = $3
WHERE
    id = $1
`

type UpdateUserHashedOneTimePasscodeParams struct {
	ID                       uuid.UUID    `db:"id" json:"id"`
	HashedOneTimePasscode    []byte       `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"`
	OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"`
}

func (q *sqlQuerier) UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error {
	_, err := q.db.ExecContext(ctx, updateUserHashedOneTimePasscode, arg.ID, arg.HashedOneTimePasscode, arg.OneTimePasscodeExpiresAt)
	return err
}

const updateUserHashedPassword = `-- name: UpdateUserHashedPassword :exec
UPDATE
	users
SET
	hashed_password = $2,
	hashed_one_time_passcode = NULL,
	one_time_passcode_expires_at = NULL
WHERE
	id = $1
`

type UpdateUserHashedPasswordParams struct {
	ID             uuid.UUID `db:"id" json:"id"`
	HashedPassword []byte    `db:"hashed_password" json:"hashed_password"`
}

func (q *sqlQuerier) UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error {
	_, err := q.db.ExecContext(ctx, updateUserHashedPassword, arg.ID, arg.HashedPassword)
	return err
}

const updateUserLastSeenAt = `-- name: UpdateUserLastSeenAt :one
UPDATE
	users
SET
	last_seen_at = $2,
	updated_at = $3
WHERE
	id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`

type UpdateUserLastSeenAtParams struct {
	ID         uuid.UUID `db:"id" json:"id"`
	LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
	UpdatedAt  time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) {
	row := q.db.QueryRowContext(ctx, updateUserLastSeenAt, arg.ID, arg.LastSeenAt, arg.UpdatedAt)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const updateUserLoginType = `-- name: UpdateUserLoginType :one
UPDATE
	users
SET
	login_type = $1,
	hashed_password = CASE WHEN $1 = 'password' :: login_type THEN
		users.hashed_password
	ELSE
		-- If the login type is not password, then the password should be
        -- cleared.
		'':: bytea
	END
WHERE
	id = $2
	AND NOT is_system
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`

type UpdateUserLoginTypeParams struct {
	NewLoginType LoginType `db:"new_login_type" json:"new_login_type"`
	UserID       uuid.UUID `db:"user_id" json:"user_id"`
}

func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLoginTypeParams) (User, error) {
	row := q.db.QueryRowContext(ctx, updateUserLoginType, arg.NewLoginType, arg.UserID)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const updateUserProfile = `-- name: UpdateUserProfile :one
UPDATE
	users
SET
	email = $2,
	username = $3,
	avatar_url = $4,
	updated_at = $5,
	name = $6
WHERE
	id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`

type UpdateUserProfileParams struct {
	ID        uuid.UUID `db:"id" json:"id"`
	Email     string    `db:"email" json:"email"`
	Username  string    `db:"username" json:"username"`
	AvatarURL string    `db:"avatar_url" json:"avatar_url"`
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
	Name      string    `db:"name" json:"name"`
}

func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) {
	row := q.db.QueryRowContext(ctx, updateUserProfile,
		arg.ID,
		arg.Email,
		arg.Username,
		arg.AvatarURL,
		arg.UpdatedAt,
		arg.Name,
	)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const updateUserQuietHoursSchedule = `-- name: UpdateUserQuietHoursSchedule :one
UPDATE
	users
SET
	quiet_hours_schedule = $2
WHERE
	id = $1
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`

type UpdateUserQuietHoursScheduleParams struct {
	ID                 uuid.UUID `db:"id" json:"id"`
	QuietHoursSchedule string    `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
}

func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) {
	row := q.db.QueryRowContext(ctx, updateUserQuietHoursSchedule, arg.ID, arg.QuietHoursSchedule)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const updateUserRoles = `-- name: UpdateUserRoles :one
UPDATE
	users
SET
	-- Remove all duplicates from the roles.
	rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
WHERE
	id = $2
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`

type UpdateUserRolesParams struct {
	GrantedRoles []string  `db:"granted_roles" json:"granted_roles"`
	ID           uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) {
	row := q.db.QueryRowContext(ctx, updateUserRoles, pq.Array(arg.GrantedRoles), arg.ID)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const updateUserShellToolDisplayMode = `-- name: UpdateUserShellToolDisplayMode :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'preference_shell_tool_display_mode', $2::text)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'preference_shell_tool_display_mode'
RETURNING value AS shell_tool_display_mode
`

type UpdateUserShellToolDisplayModeParams struct {
	UserID               uuid.UUID `db:"user_id" json:"user_id"`
	ShellToolDisplayMode string    `db:"shell_tool_display_mode" json:"shell_tool_display_mode"`
}

func (q *sqlQuerier) UpdateUserShellToolDisplayMode(ctx context.Context, arg UpdateUserShellToolDisplayModeParams) (string, error) {
	row := q.db.QueryRowContext(ctx, updateUserShellToolDisplayMode, arg.UserID, arg.ShellToolDisplayMode)
	var shell_tool_display_mode string
	err := row.Scan(&shell_tool_display_mode)
	return shell_tool_display_mode, err
}

const updateUserStatus = `-- name: UpdateUserStatus :one
UPDATE
	users
SET
	status = $2,
	updated_at = $3,
	-- If the user is logging in, set last_seen_at to updated_at.
	last_seen_at = CASE WHEN $4 :: boolean THEN $3 :: timestamptz ELSE last_seen_at END
WHERE
	id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, is_service_account, chat_spend_limit_micros
`

type UpdateUserStatusParams struct {
	ID         uuid.UUID  `db:"id" json:"id"`
	Status     UserStatus `db:"status" json:"status"`
	UpdatedAt  time.Time  `db:"updated_at" json:"updated_at"`
	UserIsSeen bool       `db:"user_is_seen" json:"user_is_seen"`
}

func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) {
	row := q.db.QueryRowContext(ctx, updateUserStatus,
		arg.ID,
		arg.Status,
		arg.UpdatedAt,
		arg.UserIsSeen,
	)
	var i User
	err := row.Scan(
		&i.ID,
		&i.Email,
		&i.Username,
		&i.HashedPassword,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Status,
		&i.RBACRoles,
		&i.LoginType,
		&i.AvatarURL,
		&i.Deleted,
		&i.LastSeenAt,
		&i.QuietHoursSchedule,
		&i.Name,
		&i.GithubComUserID,
		&i.HashedOneTimePasscode,
		&i.OneTimePasscodeExpiresAt,
		&i.IsSystem,
		&i.IsServiceAccount,
		&i.ChatSpendLimitMicros,
	)
	return i, err
}

const updateUserTaskNotificationAlertDismissed = `-- name: UpdateUserTaskNotificationAlertDismissed :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'preference_task_notification_alert_dismissed', ($2::boolean)::text)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'preference_task_notification_alert_dismissed'
RETURNING value::boolean AS task_notification_alert_dismissed
`

type UpdateUserTaskNotificationAlertDismissedParams struct {
	UserID                         uuid.UUID `db:"user_id" json:"user_id"`
	TaskNotificationAlertDismissed bool      `db:"task_notification_alert_dismissed" json:"task_notification_alert_dismissed"`
}

func (q *sqlQuerier) UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg UpdateUserTaskNotificationAlertDismissedParams) (bool, error) {
	row := q.db.QueryRowContext(ctx, updateUserTaskNotificationAlertDismissed, arg.UserID, arg.TaskNotificationAlertDismissed)
	var task_notification_alert_dismissed bool
	err := row.Scan(&task_notification_alert_dismissed)
	return task_notification_alert_dismissed, err
}

const updateUserTerminalFont = `-- name: UpdateUserTerminalFont :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'terminal_font', $2)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'terminal_font'
RETURNING user_id, key, value
`

type UpdateUserTerminalFontParams struct {
	UserID       uuid.UUID `db:"user_id" json:"user_id"`
	TerminalFont string    `db:"terminal_font" json:"terminal_font"`
}

func (q *sqlQuerier) UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) {
	row := q.db.QueryRowContext(ctx, updateUserTerminalFont, arg.UserID, arg.TerminalFont)
	var i UserConfig
	err := row.Scan(&i.UserID, &i.Key, &i.Value)
	return i, err
}

const updateUserThemeDark = `-- name: UpdateUserThemeDark :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'theme_dark', $2)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'theme_dark'
RETURNING user_id, key, value
`

type UpdateUserThemeDarkParams struct {
	UserID    uuid.UUID `db:"user_id" json:"user_id"`
	ThemeDark string    `db:"theme_dark" json:"theme_dark"`
}

func (q *sqlQuerier) UpdateUserThemeDark(ctx context.Context, arg UpdateUserThemeDarkParams) (UserConfig, error) {
	row := q.db.QueryRowContext(ctx, updateUserThemeDark, arg.UserID, arg.ThemeDark)
	var i UserConfig
	err := row.Scan(&i.UserID, &i.Key, &i.Value)
	return i, err
}

const updateUserThemeLight = `-- name: UpdateUserThemeLight :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'theme_light', $2)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'theme_light'
RETURNING user_id, key, value
`

type UpdateUserThemeLightParams struct {
	UserID     uuid.UUID `db:"user_id" json:"user_id"`
	ThemeLight string    `db:"theme_light" json:"theme_light"`
}

func (q *sqlQuerier) UpdateUserThemeLight(ctx context.Context, arg UpdateUserThemeLightParams) (UserConfig, error) {
	row := q.db.QueryRowContext(ctx, updateUserThemeLight, arg.UserID, arg.ThemeLight)
	var i UserConfig
	err := row.Scan(&i.UserID, &i.Key, &i.Value)
	return i, err
}

const updateUserThemeMode = `-- name: UpdateUserThemeMode :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'theme_mode', $2)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'theme_mode'
RETURNING user_id, key, value
`

type UpdateUserThemeModeParams struct {
	UserID    uuid.UUID `db:"user_id" json:"user_id"`
	ThemeMode string    `db:"theme_mode" json:"theme_mode"`
}

func (q *sqlQuerier) UpdateUserThemeMode(ctx context.Context, arg UpdateUserThemeModeParams) (UserConfig, error) {
	row := q.db.QueryRowContext(ctx, updateUserThemeMode, arg.UserID, arg.ThemeMode)
	var i UserConfig
	err := row.Scan(&i.UserID, &i.Key, &i.Value)
	return i, err
}

const updateUserThemePreference = `-- name: UpdateUserThemePreference :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'theme_preference', $2)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'theme_preference'
RETURNING user_id, key, value
`

type UpdateUserThemePreferenceParams struct {
	UserID          uuid.UUID `db:"user_id" json:"user_id"`
	ThemePreference string    `db:"theme_preference" json:"theme_preference"`
}

func (q *sqlQuerier) UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) {
	row := q.db.QueryRowContext(ctx, updateUserThemePreference, arg.UserID, arg.ThemePreference)
	var i UserConfig
	err := row.Scan(&i.UserID, &i.Key, &i.Value)
	return i, err
}

const updateUserThinkingDisplayMode = `-- name: UpdateUserThinkingDisplayMode :one
INSERT INTO
	user_configs (user_id, key, value)
VALUES
	($1, 'preference_thinking_display_mode', $2::text)
ON CONFLICT
	ON CONSTRAINT user_configs_pkey
DO UPDATE
SET
	value = $2
WHERE user_configs.user_id = $1
	AND user_configs.key = 'preference_thinking_display_mode'
RETURNING value AS thinking_display_mode
`

type UpdateUserThinkingDisplayModeParams struct {
	UserID              uuid.UUID `db:"user_id" json:"user_id"`
	ThinkingDisplayMode string    `db:"thinking_display_mode" json:"thinking_display_mode"`
}

func (q *sqlQuerier) UpdateUserThinkingDisplayMode(ctx context.Context, arg UpdateUserThinkingDisplayModeParams) (string, error) {
	row := q.db.QueryRowContext(ctx, updateUserThinkingDisplayMode, arg.UserID, arg.ThinkingDisplayMode)
	var thinking_display_mode string
	err := row.Scan(&thinking_display_mode)
	return thinking_display_mode, err
}

const upsertUserChatDebugLoggingEnabled = `-- name: UpsertUserChatDebugLoggingEnabled :exec
INSERT INTO user_configs (user_id, key, value)
VALUES (
	$1,
	'chat_debug_logging_enabled',
	CASE
		WHEN $2::bool THEN 'true'
		ELSE 'false'
	END
)
ON CONFLICT ON CONSTRAINT user_configs_pkey
DO UPDATE SET value = CASE
	WHEN $2::bool THEN 'true'
	ELSE 'false'
END
WHERE user_configs.user_id = $1
	AND user_configs.key = 'chat_debug_logging_enabled'
`

type UpsertUserChatDebugLoggingEnabledParams struct {
	UserID              uuid.UUID `db:"user_id" json:"user_id"`
	DebugLoggingEnabled bool      `db:"debug_logging_enabled" json:"debug_logging_enabled"`
}

func (q *sqlQuerier) UpsertUserChatDebugLoggingEnabled(ctx context.Context, arg UpsertUserChatDebugLoggingEnabledParams) error {
	_, err := q.db.ExecContext(ctx, upsertUserChatDebugLoggingEnabled, arg.UserID, arg.DebugLoggingEnabled)
	return err
}

const upsertUserChatPersonalModelOverride = `-- name: UpsertUserChatPersonalModelOverride :exec
INSERT INTO user_configs (user_id, key, value)
VALUES ($1::uuid, $2::text, $3::text)
ON CONFLICT ON CONSTRAINT user_configs_pkey
DO UPDATE SET value = $3::text
`

type UpsertUserChatPersonalModelOverrideParams struct {
	UserID uuid.UUID `db:"user_id" json:"user_id"`
	Key    string    `db:"key" json:"key"`
	Value  string    `db:"value" json:"value"`
}

func (q *sqlQuerier) UpsertUserChatPersonalModelOverride(ctx context.Context, arg UpsertUserChatPersonalModelOverrideParams) error {
	_, err := q.db.ExecContext(ctx, upsertUserChatPersonalModelOverride, arg.UserID, arg.Key, arg.Value)
	return err
}

const validateUserIDs = `-- name: ValidateUserIDs :one
WITH input AS (
	SELECT
		unnest($1::uuid[]) AS id
)
SELECT
	array_agg(input.id)::uuid[] as invalid_user_ids,
	COUNT(*) = 0 as ok
FROM
	-- Preserve rows where there is not a matching left (users) row for each
	-- right (input) row...
	users
	RIGHT JOIN input ON users.id = input.id
WHERE
	-- ...so that we can retain exactly those rows where an input ID does not
	-- match an existing user...
	users.id IS NULL OR
	-- ...or that only matches a user that was deleted.
	users.deleted = true
`

type ValidateUserIDsRow struct {
	InvalidUserIds []uuid.UUID `db:"invalid_user_ids" json:"invalid_user_ids"`
	Ok             bool        `db:"ok" json:"ok"`
}

func (q *sqlQuerier) ValidateUserIDs(ctx context.Context, userIds []uuid.UUID) (ValidateUserIDsRow, error) {
	row := q.db.QueryRowContext(ctx, validateUserIDs, pq.Array(userIds))
	var i ValidateUserIDsRow
	err := row.Scan(pq.Array(&i.InvalidUserIds), &i.Ok)
	return i, err
}

const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many
SELECT
	id, workspace_agent_id, created_at, workspace_folder, config_path, name, subagent_id
FROM
	workspace_agent_devcontainers
WHERE
	workspace_agent_id = $1
ORDER BY
	created_at, id
`

func (q *sqlQuerier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]WorkspaceAgentDevcontainer, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentDevcontainersByAgentID, workspaceAgentID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentDevcontainer
	for rows.Next() {
		var i WorkspaceAgentDevcontainer
		if err := rows.Scan(
			&i.ID,
			&i.WorkspaceAgentID,
			&i.CreatedAt,
			&i.WorkspaceFolder,
			&i.ConfigPath,
			&i.Name,
			&i.SubagentID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceAgentDevcontainers = `-- name: InsertWorkspaceAgentDevcontainers :many
INSERT INTO
	workspace_agent_devcontainers (workspace_agent_id, created_at, id, name, workspace_folder, config_path, subagent_id)
SELECT
	$1::uuid AS workspace_agent_id,
	$2::timestamptz AS created_at,
	unnest($3::uuid[]) AS id,
	unnest($4::text[]) AS name,
	unnest($5::text[]) AS workspace_folder,
	unnest($6::text[]) AS config_path,
	NULLIF(unnest($7::uuid[]), '00000000-0000-0000-0000-000000000000')::uuid AS subagent_id
RETURNING workspace_agent_devcontainers.id, workspace_agent_devcontainers.workspace_agent_id, workspace_agent_devcontainers.created_at, workspace_agent_devcontainers.workspace_folder, workspace_agent_devcontainers.config_path, workspace_agent_devcontainers.name, workspace_agent_devcontainers.subagent_id
`

type InsertWorkspaceAgentDevcontainersParams struct {
	WorkspaceAgentID uuid.UUID   `db:"workspace_agent_id" json:"workspace_agent_id"`
	CreatedAt        time.Time   `db:"created_at" json:"created_at"`
	ID               []uuid.UUID `db:"id" json:"id"`
	Name             []string    `db:"name" json:"name"`
	WorkspaceFolder  []string    `db:"workspace_folder" json:"workspace_folder"`
	ConfigPath       []string    `db:"config_path" json:"config_path"`
	SubagentID       []uuid.UUID `db:"subagent_id" json:"subagent_id"`
}

func (q *sqlQuerier) InsertWorkspaceAgentDevcontainers(ctx context.Context, arg InsertWorkspaceAgentDevcontainersParams) ([]WorkspaceAgentDevcontainer, error) {
	rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentDevcontainers,
		arg.WorkspaceAgentID,
		arg.CreatedAt,
		pq.Array(arg.ID),
		pq.Array(arg.Name),
		pq.Array(arg.WorkspaceFolder),
		pq.Array(arg.ConfigPath),
		pq.Array(arg.SubagentID),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentDevcontainer
	for rows.Next() {
		var i WorkspaceAgentDevcontainer
		if err := rows.Scan(
			&i.ID,
			&i.WorkspaceAgentID,
			&i.CreatedAt,
			&i.WorkspaceFolder,
			&i.ConfigPath,
			&i.Name,
			&i.SubagentID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const deleteWorkspaceAgentPortShare = `-- name: DeleteWorkspaceAgentPortShare :exec
DELETE FROM
	workspace_agent_port_share
WHERE
	workspace_id = $1
	AND agent_name = $2
	AND port = $3
`

type DeleteWorkspaceAgentPortShareParams struct {
	WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
	AgentName   string    `db:"agent_name" json:"agent_name"`
	Port        int32     `db:"port" json:"port"`
}

func (q *sqlQuerier) DeleteWorkspaceAgentPortShare(ctx context.Context, arg DeleteWorkspaceAgentPortShareParams) error {
	_, err := q.db.ExecContext(ctx, deleteWorkspaceAgentPortShare, arg.WorkspaceID, arg.AgentName, arg.Port)
	return err
}

const deleteWorkspaceAgentPortSharesByTemplate = `-- name: DeleteWorkspaceAgentPortSharesByTemplate :exec
DELETE FROM
	workspace_agent_port_share
WHERE
	workspace_id IN (
		SELECT
			id
		FROM
			workspaces
		WHERE
			template_id = $1
	)
`

func (q *sqlQuerier) DeleteWorkspaceAgentPortSharesByTemplate(ctx context.Context, templateID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteWorkspaceAgentPortSharesByTemplate, templateID)
	return err
}

const getWorkspaceAgentPortShare = `-- name: GetWorkspaceAgentPortShare :one
SELECT
	workspace_id, agent_name, port, share_level, protocol
FROM
	workspace_agent_port_share
WHERE
	workspace_id = $1
	AND agent_name = $2
	AND port = $3
`

type GetWorkspaceAgentPortShareParams struct {
	WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
	AgentName   string    `db:"agent_name" json:"agent_name"`
	Port        int32     `db:"port" json:"port"`
}

func (q *sqlQuerier) GetWorkspaceAgentPortShare(ctx context.Context, arg GetWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceAgentPortShare, arg.WorkspaceID, arg.AgentName, arg.Port)
	var i WorkspaceAgentPortShare
	err := row.Scan(
		&i.WorkspaceID,
		&i.AgentName,
		&i.Port,
		&i.ShareLevel,
		&i.Protocol,
	)
	return i, err
}

const listWorkspaceAgentPortShares = `-- name: ListWorkspaceAgentPortShares :many
SELECT
	workspace_id, agent_name, port, share_level, protocol
FROM
	workspace_agent_port_share
WHERE
	workspace_id = $1
`

func (q *sqlQuerier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error) {
	rows, err := q.db.QueryContext(ctx, listWorkspaceAgentPortShares, workspaceID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentPortShare
	for rows.Next() {
		var i WorkspaceAgentPortShare
		if err := rows.Scan(
			&i.WorkspaceID,
			&i.AgentName,
			&i.Port,
			&i.ShareLevel,
			&i.Protocol,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const reduceWorkspaceAgentShareLevelToAuthenticatedByTemplate = `-- name: ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate :exec
UPDATE
	workspace_agent_port_share
SET
	share_level = 'authenticated'
WHERE
	share_level = 'public'
	AND workspace_id IN (
		SELECT
			id
		FROM
			workspaces
		WHERE
			template_id = $1
	)
`

func (q *sqlQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, reduceWorkspaceAgentShareLevelToAuthenticatedByTemplate, templateID)
	return err
}

const upsertWorkspaceAgentPortShare = `-- name: UpsertWorkspaceAgentPortShare :one
INSERT INTO
	workspace_agent_port_share (
		workspace_id,
		agent_name,
		port,
		share_level,
		protocol
	)
VALUES (
	$1,
	$2,
	$3,
	$4,
	$5
)
ON CONFLICT (
	workspace_id,
	agent_name,
	port
)
DO UPDATE SET
	share_level = $4,
	protocol = $5
RETURNING workspace_id, agent_name, port, share_level, protocol
`

type UpsertWorkspaceAgentPortShareParams struct {
	WorkspaceID uuid.UUID         `db:"workspace_id" json:"workspace_id"`
	AgentName   string            `db:"agent_name" json:"agent_name"`
	Port        int32             `db:"port" json:"port"`
	ShareLevel  AppSharingLevel   `db:"share_level" json:"share_level"`
	Protocol    PortShareProtocol `db:"protocol" json:"protocol"`
}

func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg UpsertWorkspaceAgentPortShareParams) (WorkspaceAgentPortShare, error) {
	row := q.db.QueryRowContext(ctx, upsertWorkspaceAgentPortShare,
		arg.WorkspaceID,
		arg.AgentName,
		arg.Port,
		arg.ShareLevel,
		arg.Protocol,
	)
	var i WorkspaceAgentPortShare
	err := row.Scan(
		&i.WorkspaceID,
		&i.AgentName,
		&i.Port,
		&i.ShareLevel,
		&i.Protocol,
	)
	return i, err
}

const fetchMemoryResourceMonitorsByAgentID = `-- name: FetchMemoryResourceMonitorsByAgentID :one
SELECT
	agent_id, enabled, threshold, created_at, updated_at, state, debounced_until
FROM
	workspace_agent_memory_resource_monitors
WHERE
	agent_id = $1
`

func (q *sqlQuerier) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) (WorkspaceAgentMemoryResourceMonitor, error) {
	row := q.db.QueryRowContext(ctx, fetchMemoryResourceMonitorsByAgentID, agentID)
	var i WorkspaceAgentMemoryResourceMonitor
	err := row.Scan(
		&i.AgentID,
		&i.Enabled,
		&i.Threshold,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.State,
		&i.DebouncedUntil,
	)
	return i, err
}

const fetchMemoryResourceMonitorsUpdatedAfter = `-- name: FetchMemoryResourceMonitorsUpdatedAfter :many
SELECT
	agent_id, enabled, threshold, created_at, updated_at, state, debounced_until
FROM
	workspace_agent_memory_resource_monitors
WHERE
	updated_at > $1
`

func (q *sqlQuerier) FetchMemoryResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentMemoryResourceMonitor, error) {
	rows, err := q.db.QueryContext(ctx, fetchMemoryResourceMonitorsUpdatedAfter, updatedAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentMemoryResourceMonitor
	for rows.Next() {
		var i WorkspaceAgentMemoryResourceMonitor
		if err := rows.Scan(
			&i.AgentID,
			&i.Enabled,
			&i.Threshold,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.State,
			&i.DebouncedUntil,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const fetchVolumesResourceMonitorsByAgentID = `-- name: FetchVolumesResourceMonitorsByAgentID :many
SELECT
	agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until
FROM
	workspace_agent_volume_resource_monitors
WHERE
	agent_id = $1
`

func (q *sqlQuerier) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceAgentVolumeResourceMonitor, error) {
	rows, err := q.db.QueryContext(ctx, fetchVolumesResourceMonitorsByAgentID, agentID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentVolumeResourceMonitor
	for rows.Next() {
		var i WorkspaceAgentVolumeResourceMonitor
		if err := rows.Scan(
			&i.AgentID,
			&i.Enabled,
			&i.Threshold,
			&i.Path,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.State,
			&i.DebouncedUntil,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const fetchVolumesResourceMonitorsUpdatedAfter = `-- name: FetchVolumesResourceMonitorsUpdatedAfter :many
SELECT
	agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until
FROM
	workspace_agent_volume_resource_monitors
WHERE
	updated_at > $1
`

func (q *sqlQuerier) FetchVolumesResourceMonitorsUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]WorkspaceAgentVolumeResourceMonitor, error) {
	rows, err := q.db.QueryContext(ctx, fetchVolumesResourceMonitorsUpdatedAfter, updatedAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentVolumeResourceMonitor
	for rows.Next() {
		var i WorkspaceAgentVolumeResourceMonitor
		if err := rows.Scan(
			&i.AgentID,
			&i.Enabled,
			&i.Threshold,
			&i.Path,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.State,
			&i.DebouncedUntil,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertMemoryResourceMonitor = `-- name: InsertMemoryResourceMonitor :one
INSERT INTO
	workspace_agent_memory_resource_monitors (
		agent_id,
		enabled,
		state,
		threshold,
		created_at,
		updated_at,
		debounced_until
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7) RETURNING agent_id, enabled, threshold, created_at, updated_at, state, debounced_until
`

type InsertMemoryResourceMonitorParams struct {
	AgentID        uuid.UUID                  `db:"agent_id" json:"agent_id"`
	Enabled        bool                       `db:"enabled" json:"enabled"`
	State          WorkspaceAgentMonitorState `db:"state" json:"state"`
	Threshold      int32                      `db:"threshold" json:"threshold"`
	CreatedAt      time.Time                  `db:"created_at" json:"created_at"`
	UpdatedAt      time.Time                  `db:"updated_at" json:"updated_at"`
	DebouncedUntil time.Time                  `db:"debounced_until" json:"debounced_until"`
}

func (q *sqlQuerier) InsertMemoryResourceMonitor(ctx context.Context, arg InsertMemoryResourceMonitorParams) (WorkspaceAgentMemoryResourceMonitor, error) {
	row := q.db.QueryRowContext(ctx, insertMemoryResourceMonitor,
		arg.AgentID,
		arg.Enabled,
		arg.State,
		arg.Threshold,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.DebouncedUntil,
	)
	var i WorkspaceAgentMemoryResourceMonitor
	err := row.Scan(
		&i.AgentID,
		&i.Enabled,
		&i.Threshold,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.State,
		&i.DebouncedUntil,
	)
	return i, err
}

const insertVolumeResourceMonitor = `-- name: InsertVolumeResourceMonitor :one
INSERT INTO
	workspace_agent_volume_resource_monitors (
		agent_id,
		path,
		enabled,
		state,
		threshold,
		created_at,
		updated_at,
		debounced_until
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8) RETURNING agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until
`

type InsertVolumeResourceMonitorParams struct {
	AgentID        uuid.UUID                  `db:"agent_id" json:"agent_id"`
	Path           string                     `db:"path" json:"path"`
	Enabled        bool                       `db:"enabled" json:"enabled"`
	State          WorkspaceAgentMonitorState `db:"state" json:"state"`
	Threshold      int32                      `db:"threshold" json:"threshold"`
	CreatedAt      time.Time                  `db:"created_at" json:"created_at"`
	UpdatedAt      time.Time                  `db:"updated_at" json:"updated_at"`
	DebouncedUntil time.Time                  `db:"debounced_until" json:"debounced_until"`
}

func (q *sqlQuerier) InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error) {
	row := q.db.QueryRowContext(ctx, insertVolumeResourceMonitor,
		arg.AgentID,
		arg.Path,
		arg.Enabled,
		arg.State,
		arg.Threshold,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.DebouncedUntil,
	)
	var i WorkspaceAgentVolumeResourceMonitor
	err := row.Scan(
		&i.AgentID,
		&i.Enabled,
		&i.Threshold,
		&i.Path,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.State,
		&i.DebouncedUntil,
	)
	return i, err
}

const updateMemoryResourceMonitor = `-- name: UpdateMemoryResourceMonitor :exec
UPDATE workspace_agent_memory_resource_monitors
SET
	updated_at = $2,
	state = $3,
	debounced_until = $4
WHERE
	agent_id = $1
`

type UpdateMemoryResourceMonitorParams struct {
	AgentID        uuid.UUID                  `db:"agent_id" json:"agent_id"`
	UpdatedAt      time.Time                  `db:"updated_at" json:"updated_at"`
	State          WorkspaceAgentMonitorState `db:"state" json:"state"`
	DebouncedUntil time.Time                  `db:"debounced_until" json:"debounced_until"`
}

func (q *sqlQuerier) UpdateMemoryResourceMonitor(ctx context.Context, arg UpdateMemoryResourceMonitorParams) error {
	_, err := q.db.ExecContext(ctx, updateMemoryResourceMonitor,
		arg.AgentID,
		arg.UpdatedAt,
		arg.State,
		arg.DebouncedUntil,
	)
	return err
}

const updateVolumeResourceMonitor = `-- name: UpdateVolumeResourceMonitor :exec
UPDATE workspace_agent_volume_resource_monitors
SET
		updated_at = $3,
		state = $4,
		debounced_until = $5
WHERE
		agent_id = $1 AND path = $2
`

type UpdateVolumeResourceMonitorParams struct {
	AgentID        uuid.UUID                  `db:"agent_id" json:"agent_id"`
	Path           string                     `db:"path" json:"path"`
	UpdatedAt      time.Time                  `db:"updated_at" json:"updated_at"`
	State          WorkspaceAgentMonitorState `db:"state" json:"state"`
	DebouncedUntil time.Time                  `db:"debounced_until" json:"debounced_until"`
}

func (q *sqlQuerier) UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error {
	_, err := q.db.ExecContext(ctx, updateVolumeResourceMonitor,
		arg.AgentID,
		arg.Path,
		arg.UpdatedAt,
		arg.State,
		arg.DebouncedUntil,
	)
	return err
}

const batchUpdateWorkspaceAgentMetadata = `-- name: BatchUpdateWorkspaceAgentMetadata :exec
WITH metadata AS (
	SELECT
		unnest($1::uuid[]) AS workspace_agent_id,
		unnest($2::text[]) AS key,
		unnest($3::text[]) AS value,
		unnest($4::text[]) AS error,
		unnest($5::timestamptz[]) AS collected_at
)
UPDATE
	workspace_agent_metadata wam
SET
	value = m.value,
	error = m.error,
	collected_at = m.collected_at
FROM
	metadata m
WHERE
	wam.workspace_agent_id = m.workspace_agent_id
	AND wam.key = m.key
`

type BatchUpdateWorkspaceAgentMetadataParams struct {
	WorkspaceAgentID []uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
	Key              []string    `db:"key" json:"key"`
	Value            []string    `db:"value" json:"value"`
	Error            []string    `db:"error" json:"error"`
	CollectedAt      []time.Time `db:"collected_at" json:"collected_at"`
}

func (q *sqlQuerier) BatchUpdateWorkspaceAgentMetadata(ctx context.Context, arg BatchUpdateWorkspaceAgentMetadataParams) error {
	_, err := q.db.ExecContext(ctx, batchUpdateWorkspaceAgentMetadata,
		pq.Array(arg.WorkspaceAgentID),
		pq.Array(arg.Key),
		pq.Array(arg.Value),
		pq.Array(arg.Error),
		pq.Array(arg.CollectedAt),
	)
	return err
}

const deleteOldWorkspaceAgentLogs = `-- name: DeleteOldWorkspaceAgentLogs :execrows
WITH
	latest_builds AS (
		SELECT
			workspace_id, max(build_number) AS max_build_number
		FROM
			workspace_builds
		GROUP BY
			workspace_id
	),
	old_agents AS (
		SELECT
			wa.id
		FROM
			workspace_agents AS wa
		JOIN
			workspace_resources AS wr
		ON
			wa.resource_id = wr.id
		JOIN
			workspace_builds AS wb
		ON
			wb.job_id = wr.job_id
		LEFT JOIN
			latest_builds
		ON
			latest_builds.workspace_id = wb.workspace_id
		AND
			latest_builds.max_build_number = wb.build_number
		WHERE
			-- Filter out the latest builds for each workspace.
			latest_builds.workspace_id IS NULL
		AND CASE
			-- If the last time the agent connected was before @threshold
			WHEN wa.last_connected_at IS NOT NULL THEN
				 wa.last_connected_at < $1 :: timestamptz
			-- The agent never connected, and was created before @threshold
			ELSE wa.created_at < $1 :: timestamptz
		END
	)
DELETE FROM workspace_agent_logs WHERE agent_id IN (SELECT id FROM old_agents)
`

// If an agent hasn't connected within the retention period, we purge its logs.
// Exception: if the logs are related to the latest build, we keep those around.
// Logs can take up a lot of space, so it's important we clean up frequently.
func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold time.Time) (int64, error) {
	result, err := q.db.ExecContext(ctx, deleteOldWorkspaceAgentLogs, threshold)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

const deleteWorkspaceSubAgentByID = `-- name: DeleteWorkspaceSubAgentByID :exec
UPDATE
	workspace_agents
SET
	deleted = TRUE
WHERE
	id = $1
	AND parent_id IS NOT NULL
	AND deleted = FALSE
`

func (q *sqlQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteWorkspaceSubAgentByID, id)
	return err
}

const getAuthenticatedWorkspaceAgentAndBuildByAuthToken = `-- name: GetAuthenticatedWorkspaceAgentAndBuildByAuthToken :one
SELECT
	workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl,
	workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
	workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.has_external_agent, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name,
	tasks.id AS task_id
FROM
	workspace_agents
JOIN
	workspace_resources
ON
	workspace_agents.resource_id = workspace_resources.id
JOIN
	workspace_build_with_user
ON
	workspace_resources.job_id = workspace_build_with_user.job_id
JOIN
	workspaces
ON
	workspace_build_with_user.workspace_id = workspaces.id
LEFT JOIN
	tasks
ON
	tasks.workspace_id = workspaces.id
WHERE
	-- This should only match 1 agent, so 1 returned row or 0.
	workspace_agents.auth_token = $1::uuid
	AND workspaces.deleted = FALSE
	-- Filter out deleted sub agents.
	AND workspace_agents.deleted = FALSE
	-- Filter out builds that are not the latest, with exception for shutdown case.
	-- Use CASE for short-circuiting: check normal case first (most common), then shutdown case.
	AND CASE
		-- Normal case: Agent's build is the latest build.
		WHEN workspace_build_with_user.build_number = (
			SELECT
				MAX(build_number)
			FROM
				workspace_builds
			WHERE
				workspace_id = workspace_build_with_user.workspace_id
		) THEN TRUE
		-- Shutdown case: Agent from previous START build during STOP build execution.
		WHEN workspace_build_with_user.transition = 'start'
			-- Agent's START build job succeeded.
			AND (SELECT job_status FROM provisioner_jobs WHERE id = workspace_build_with_user.job_id) = 'succeeded'
			-- Latest build is a STOP build whose job is still active,
			-- and agent's build is immediately previous.
			AND EXISTS (
				SELECT 1
				FROM workspace_builds latest
				JOIN provisioner_jobs pj ON pj.id = latest.job_id
				WHERE latest.workspace_id = workspace_build_with_user.workspace_id
				AND latest.build_number = workspace_build_with_user.build_number + 1
				AND latest.build_number = (
					SELECT MAX(build_number)
					FROM workspace_builds l2
					WHERE l2.workspace_id = latest.workspace_id
				)
				AND latest.transition = 'stop'
				AND pj.job_status IN ('pending', 'running')
			) THEN TRUE
		ELSE FALSE
	END
`

type GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow struct {
	WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"`
	WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
	WorkspaceBuild WorkspaceBuild `db:"workspace_build" json:"workspace_build"`
	TaskID         uuid.NullUUID  `db:"task_id" json:"task_id"`
}

// GetAuthenticatedWorkspaceAgentAndBuildByAuthToken returns an authenticated
// workspace agent and its associated build. During normal operation, this is
// the latest build. During shutdown, this may be the previous START build while
// the STOP build is executing, allowing shutdown scripts to authenticate (see
// issue #19467).
func (q *sqlQuerier) GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx context.Context, authToken uuid.UUID) (GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow, error) {
	row := q.db.QueryRowContext(ctx, getAuthenticatedWorkspaceAgentAndBuildByAuthToken, authToken)
	var i GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow
	err := row.Scan(
		&i.WorkspaceTable.ID,
		&i.WorkspaceTable.CreatedAt,
		&i.WorkspaceTable.UpdatedAt,
		&i.WorkspaceTable.OwnerID,
		&i.WorkspaceTable.OrganizationID,
		&i.WorkspaceTable.TemplateID,
		&i.WorkspaceTable.Deleted,
		&i.WorkspaceTable.Name,
		&i.WorkspaceTable.AutostartSchedule,
		&i.WorkspaceTable.Ttl,
		&i.WorkspaceTable.LastUsedAt,
		&i.WorkspaceTable.DormantAt,
		&i.WorkspaceTable.DeletingAt,
		&i.WorkspaceTable.AutomaticUpdates,
		&i.WorkspaceTable.Favorite,
		&i.WorkspaceTable.NextStartAt,
		&i.WorkspaceTable.GroupACL,
		&i.WorkspaceTable.UserACL,
		&i.WorkspaceAgent.ID,
		&i.WorkspaceAgent.CreatedAt,
		&i.WorkspaceAgent.UpdatedAt,
		&i.WorkspaceAgent.Name,
		&i.WorkspaceAgent.FirstConnectedAt,
		&i.WorkspaceAgent.LastConnectedAt,
		&i.WorkspaceAgent.DisconnectedAt,
		&i.WorkspaceAgent.ResourceID,
		&i.WorkspaceAgent.AuthToken,
		&i.WorkspaceAgent.AuthInstanceID,
		&i.WorkspaceAgent.Architecture,
		&i.WorkspaceAgent.EnvironmentVariables,
		&i.WorkspaceAgent.OperatingSystem,
		&i.WorkspaceAgent.InstanceMetadata,
		&i.WorkspaceAgent.ResourceMetadata,
		&i.WorkspaceAgent.Directory,
		&i.WorkspaceAgent.Version,
		&i.WorkspaceAgent.LastConnectedReplicaID,
		&i.WorkspaceAgent.ConnectionTimeoutSeconds,
		&i.WorkspaceAgent.TroubleshootingURL,
		&i.WorkspaceAgent.MOTDFile,
		&i.WorkspaceAgent.LifecycleState,
		&i.WorkspaceAgent.ExpandedDirectory,
		&i.WorkspaceAgent.LogsLength,
		&i.WorkspaceAgent.LogsOverflowed,
		&i.WorkspaceAgent.StartedAt,
		&i.WorkspaceAgent.ReadyAt,
		pq.Array(&i.WorkspaceAgent.Subsystems),
		pq.Array(&i.WorkspaceAgent.DisplayApps),
		&i.WorkspaceAgent.APIVersion,
		&i.WorkspaceAgent.DisplayOrder,
		&i.WorkspaceAgent.ParentID,
		&i.WorkspaceAgent.APIKeyScope,
		&i.WorkspaceAgent.Deleted,
		&i.WorkspaceBuild.ID,
		&i.WorkspaceBuild.CreatedAt,
		&i.WorkspaceBuild.UpdatedAt,
		&i.WorkspaceBuild.WorkspaceID,
		&i.WorkspaceBuild.TemplateVersionID,
		&i.WorkspaceBuild.BuildNumber,
		&i.WorkspaceBuild.Transition,
		&i.WorkspaceBuild.InitiatorID,
		&i.WorkspaceBuild.JobID,
		&i.WorkspaceBuild.Deadline,
		&i.WorkspaceBuild.Reason,
		&i.WorkspaceBuild.DailyCost,
		&i.WorkspaceBuild.MaxDeadline,
		&i.WorkspaceBuild.TemplateVersionPresetID,
		&i.WorkspaceBuild.HasAITask,
		&i.WorkspaceBuild.HasExternalAgent,
		&i.WorkspaceBuild.InitiatorByAvatarUrl,
		&i.WorkspaceBuild.InitiatorByUsername,
		&i.WorkspaceBuild.InitiatorByName,
		&i.TaskID,
	)
	return i, err
}

const getWorkspaceAgentAndWorkspaceByID = `-- name: GetWorkspaceAgentAndWorkspaceByID :one
SELECT
	workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
	workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl,
	users.username as owner_username
FROM
	workspace_agents
JOIN
	workspace_resources ON workspace_agents.resource_id = workspace_resources.id
JOIN
	provisioner_jobs ON workspace_resources.job_id = provisioner_jobs.id
JOIN
	workspace_builds ON provisioner_jobs.id = workspace_builds.job_id
JOIN
	workspaces ON workspace_builds.workspace_id = workspaces.id
JOIN
	users ON workspaces.owner_id = users.id
WHERE
	workspace_agents.id = $1
	AND workspace_agents.deleted = FALSE
	AND provisioner_jobs.type = 'workspace_build'::provisioner_job_type
	AND workspaces.deleted = FALSE
	AND users.deleted = FALSE
LIMIT 1
`

type GetWorkspaceAgentAndWorkspaceByIDRow struct {
	WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
	WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"`
	OwnerUsername  string         `db:"owner_username" json:"owner_username"`
}

func (q *sqlQuerier) GetWorkspaceAgentAndWorkspaceByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentAndWorkspaceByIDRow, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceAgentAndWorkspaceByID, id)
	var i GetWorkspaceAgentAndWorkspaceByIDRow
	err := row.Scan(
		&i.WorkspaceAgent.ID,
		&i.WorkspaceAgent.CreatedAt,
		&i.WorkspaceAgent.UpdatedAt,
		&i.WorkspaceAgent.Name,
		&i.WorkspaceAgent.FirstConnectedAt,
		&i.WorkspaceAgent.LastConnectedAt,
		&i.WorkspaceAgent.DisconnectedAt,
		&i.WorkspaceAgent.ResourceID,
		&i.WorkspaceAgent.AuthToken,
		&i.WorkspaceAgent.AuthInstanceID,
		&i.WorkspaceAgent.Architecture,
		&i.WorkspaceAgent.EnvironmentVariables,
		&i.WorkspaceAgent.OperatingSystem,
		&i.WorkspaceAgent.InstanceMetadata,
		&i.WorkspaceAgent.ResourceMetadata,
		&i.WorkspaceAgent.Directory,
		&i.WorkspaceAgent.Version,
		&i.WorkspaceAgent.LastConnectedReplicaID,
		&i.WorkspaceAgent.ConnectionTimeoutSeconds,
		&i.WorkspaceAgent.TroubleshootingURL,
		&i.WorkspaceAgent.MOTDFile,
		&i.WorkspaceAgent.LifecycleState,
		&i.WorkspaceAgent.ExpandedDirectory,
		&i.WorkspaceAgent.LogsLength,
		&i.WorkspaceAgent.LogsOverflowed,
		&i.WorkspaceAgent.StartedAt,
		&i.WorkspaceAgent.ReadyAt,
		pq.Array(&i.WorkspaceAgent.Subsystems),
		pq.Array(&i.WorkspaceAgent.DisplayApps),
		&i.WorkspaceAgent.APIVersion,
		&i.WorkspaceAgent.DisplayOrder,
		&i.WorkspaceAgent.ParentID,
		&i.WorkspaceAgent.APIKeyScope,
		&i.WorkspaceAgent.Deleted,
		&i.WorkspaceTable.ID,
		&i.WorkspaceTable.CreatedAt,
		&i.WorkspaceTable.UpdatedAt,
		&i.WorkspaceTable.OwnerID,
		&i.WorkspaceTable.OrganizationID,
		&i.WorkspaceTable.TemplateID,
		&i.WorkspaceTable.Deleted,
		&i.WorkspaceTable.Name,
		&i.WorkspaceTable.AutostartSchedule,
		&i.WorkspaceTable.Ttl,
		&i.WorkspaceTable.LastUsedAt,
		&i.WorkspaceTable.DormantAt,
		&i.WorkspaceTable.DeletingAt,
		&i.WorkspaceTable.AutomaticUpdates,
		&i.WorkspaceTable.Favorite,
		&i.WorkspaceTable.NextStartAt,
		&i.WorkspaceTable.GroupACL,
		&i.WorkspaceTable.UserACL,
		&i.OwnerUsername,
	)
	return i, err
}

const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one
SELECT
	id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
FROM
	workspace_agents
WHERE
	id = $1
	-- Filter out deleted sub agents.
	AND deleted = FALSE
`

func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceAgentByID, id)
	var i WorkspaceAgent
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.FirstConnectedAt,
		&i.LastConnectedAt,
		&i.DisconnectedAt,
		&i.ResourceID,
		&i.AuthToken,
		&i.AuthInstanceID,
		&i.Architecture,
		&i.EnvironmentVariables,
		&i.OperatingSystem,
		&i.InstanceMetadata,
		&i.ResourceMetadata,
		&i.Directory,
		&i.Version,
		&i.LastConnectedReplicaID,
		&i.ConnectionTimeoutSeconds,
		&i.TroubleshootingURL,
		&i.MOTDFile,
		&i.LifecycleState,
		&i.ExpandedDirectory,
		&i.LogsLength,
		&i.LogsOverflowed,
		&i.StartedAt,
		&i.ReadyAt,
		pq.Array(&i.Subsystems),
		pq.Array(&i.DisplayApps),
		&i.APIVersion,
		&i.DisplayOrder,
		&i.ParentID,
		&i.APIKeyScope,
		&i.Deleted,
	)
	return i, err
}

const getWorkspaceAgentLifecycleStateByID = `-- name: GetWorkspaceAgentLifecycleStateByID :one
SELECT
	lifecycle_state,
	started_at,
	ready_at
FROM
	workspace_agents
WHERE
	id = $1
`

type GetWorkspaceAgentLifecycleStateByIDRow struct {
	LifecycleState WorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"`
	StartedAt      sql.NullTime                 `db:"started_at" json:"started_at"`
	ReadyAt        sql.NullTime                 `db:"ready_at" json:"ready_at"`
}

func (q *sqlQuerier) GetWorkspaceAgentLifecycleStateByID(ctx context.Context, id uuid.UUID) (GetWorkspaceAgentLifecycleStateByIDRow, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceAgentLifecycleStateByID, id)
	var i GetWorkspaceAgentLifecycleStateByIDRow
	err := row.Scan(&i.LifecycleState, &i.StartedAt, &i.ReadyAt)
	return i, err
}

const getWorkspaceAgentLogSourcesByAgentIDs = `-- name: GetWorkspaceAgentLogSourcesByAgentIDs :many
SELECT workspace_agent_id, id, created_at, display_name, icon FROM workspace_agent_log_sources WHERE workspace_agent_id = ANY($1 :: uuid [ ])
`

func (q *sqlQuerier) GetWorkspaceAgentLogSourcesByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentLogSource, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentLogSourcesByAgentIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentLogSource
	for rows.Next() {
		var i WorkspaceAgentLogSource
		if err := rows.Scan(
			&i.WorkspaceAgentID,
			&i.ID,
			&i.CreatedAt,
			&i.DisplayName,
			&i.Icon,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentLogsAfter = `-- name: GetWorkspaceAgentLogsAfter :many
SELECT
	agent_id, created_at, output, id, level, log_source_id
FROM
	workspace_agent_logs
WHERE
	agent_id = $1
	AND (
		id > $2
	) ORDER BY id ASC
`

type GetWorkspaceAgentLogsAfterParams struct {
	AgentID      uuid.UUID `db:"agent_id" json:"agent_id"`
	CreatedAfter int64     `db:"created_after" json:"created_after"`
}

func (q *sqlQuerier) GetWorkspaceAgentLogsAfter(ctx context.Context, arg GetWorkspaceAgentLogsAfterParams) ([]WorkspaceAgentLog, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentLogsAfter, arg.AgentID, arg.CreatedAfter)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentLog
	for rows.Next() {
		var i WorkspaceAgentLog
		if err := rows.Scan(
			&i.AgentID,
			&i.CreatedAt,
			&i.Output,
			&i.ID,
			&i.Level,
			&i.LogSourceID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentMetadata = `-- name: GetWorkspaceAgentMetadata :many
SELECT
	workspace_agent_id, display_name, key, script, value, error, timeout, interval, collected_at, display_order
FROM
	workspace_agent_metadata
WHERE
	workspace_agent_id = $1
	AND CASE WHEN COALESCE(array_length($2::text[], 1), 0) > 0 THEN key = ANY($2::text[]) ELSE TRUE END
`

type GetWorkspaceAgentMetadataParams struct {
	WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
	Keys             []string  `db:"keys" json:"keys"`
}

func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorkspaceAgentMetadataParams) ([]WorkspaceAgentMetadatum, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentMetadata, arg.WorkspaceAgentID, pq.Array(arg.Keys))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentMetadatum
	for rows.Next() {
		var i WorkspaceAgentMetadatum
		if err := rows.Scan(
			&i.WorkspaceAgentID,
			&i.DisplayName,
			&i.Key,
			&i.Script,
			&i.Value,
			&i.Error,
			&i.Timeout,
			&i.Interval,
			&i.CollectedAt,
			&i.DisplayOrder,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
SELECT
	DISTINCT ON (workspace_agent_script_timings.script_id) workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status,
	workspace_agent_scripts.display_name,
	workspace_agents.id as workspace_agent_id,
	workspace_agents.name as workspace_agent_name
FROM workspace_agent_script_timings
INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
WHERE workspace_builds.id = $1
ORDER BY workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at
`

type GetWorkspaceAgentScriptTimingsByBuildIDRow struct {
	ScriptID           uuid.UUID                        `db:"script_id" json:"script_id"`
	StartedAt          time.Time                        `db:"started_at" json:"started_at"`
	EndedAt            time.Time                        `db:"ended_at" json:"ended_at"`
	ExitCode           int32                            `db:"exit_code" json:"exit_code"`
	Stage              WorkspaceAgentScriptTimingStage  `db:"stage" json:"stage"`
	Status             WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
	DisplayName        string                           `db:"display_name" json:"display_name"`
	WorkspaceAgentID   uuid.UUID                        `db:"workspace_agent_id" json:"workspace_agent_id"`
	WorkspaceAgentName string                           `db:"workspace_agent_name" json:"workspace_agent_name"`
}

func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptTimingsByBuildID, id)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceAgentScriptTimingsByBuildIDRow
	for rows.Next() {
		var i GetWorkspaceAgentScriptTimingsByBuildIDRow
		if err := rows.Scan(
			&i.ScriptID,
			&i.StartedAt,
			&i.EndedAt,
			&i.ExitCode,
			&i.Stage,
			&i.Status,
			&i.DisplayName,
			&i.WorkspaceAgentID,
			&i.WorkspaceAgentName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentsByInstanceID = `-- name: GetWorkspaceAgentsByInstanceID :many
SELECT
	id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
FROM
	workspace_agents
WHERE
	auth_instance_id = $1 :: TEXT
	-- Filter out deleted agents.
	AND deleted = FALSE
	-- Filter out sub agents, they do not authenticate with auth_instance_id.
	AND parent_id IS NULL
ORDER BY
	created_at DESC
`

func (q *sqlQuerier) GetWorkspaceAgentsByInstanceID(ctx context.Context, authInstanceID string) ([]WorkspaceAgent, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByInstanceID, authInstanceID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgent
	for rows.Next() {
		var i WorkspaceAgent
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.FirstConnectedAt,
			&i.LastConnectedAt,
			&i.DisconnectedAt,
			&i.ResourceID,
			&i.AuthToken,
			&i.AuthInstanceID,
			&i.Architecture,
			&i.EnvironmentVariables,
			&i.OperatingSystem,
			&i.InstanceMetadata,
			&i.ResourceMetadata,
			&i.Directory,
			&i.Version,
			&i.LastConnectedReplicaID,
			&i.ConnectionTimeoutSeconds,
			&i.TroubleshootingURL,
			&i.MOTDFile,
			&i.LifecycleState,
			&i.ExpandedDirectory,
			&i.LogsLength,
			&i.LogsOverflowed,
			&i.StartedAt,
			&i.ReadyAt,
			pq.Array(&i.Subsystems),
			pq.Array(&i.DisplayApps),
			&i.APIVersion,
			&i.DisplayOrder,
			&i.ParentID,
			&i.APIKeyScope,
			&i.Deleted,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentsByParentID = `-- name: GetWorkspaceAgentsByParentID :many
SELECT
	id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
FROM
	workspace_agents
WHERE
	parent_id = $1::uuid
	AND deleted = FALSE
`

func (q *sqlQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]WorkspaceAgent, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByParentID, parentID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgent
	for rows.Next() {
		var i WorkspaceAgent
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.FirstConnectedAt,
			&i.LastConnectedAt,
			&i.DisconnectedAt,
			&i.ResourceID,
			&i.AuthToken,
			&i.AuthInstanceID,
			&i.Architecture,
			&i.EnvironmentVariables,
			&i.OperatingSystem,
			&i.InstanceMetadata,
			&i.ResourceMetadata,
			&i.Directory,
			&i.Version,
			&i.LastConnectedReplicaID,
			&i.ConnectionTimeoutSeconds,
			&i.TroubleshootingURL,
			&i.MOTDFile,
			&i.LifecycleState,
			&i.ExpandedDirectory,
			&i.LogsLength,
			&i.LogsOverflowed,
			&i.StartedAt,
			&i.ReadyAt,
			pq.Array(&i.Subsystems),
			pq.Array(&i.DisplayApps),
			&i.APIVersion,
			&i.DisplayOrder,
			&i.ParentID,
			&i.APIKeyScope,
			&i.Deleted,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many
SELECT
	id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
FROM
	workspace_agents
WHERE
	resource_id = ANY($1 :: uuid [ ])
	-- Filter out deleted sub agents.
	AND deleted = FALSE
`

func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByResourceIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgent
	for rows.Next() {
		var i WorkspaceAgent
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.FirstConnectedAt,
			&i.LastConnectedAt,
			&i.DisconnectedAt,
			&i.ResourceID,
			&i.AuthToken,
			&i.AuthInstanceID,
			&i.Architecture,
			&i.EnvironmentVariables,
			&i.OperatingSystem,
			&i.InstanceMetadata,
			&i.ResourceMetadata,
			&i.Directory,
			&i.Version,
			&i.LastConnectedReplicaID,
			&i.ConnectionTimeoutSeconds,
			&i.TroubleshootingURL,
			&i.MOTDFile,
			&i.LifecycleState,
			&i.ExpandedDirectory,
			&i.LogsLength,
			&i.LogsOverflowed,
			&i.StartedAt,
			&i.ReadyAt,
			pq.Array(&i.Subsystems),
			pq.Array(&i.DisplayApps),
			&i.APIVersion,
			&i.DisplayOrder,
			&i.ParentID,
			&i.APIKeyScope,
			&i.Deleted,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentsByWorkspaceAndBuildNumber = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many
SELECT
	workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
FROM
	workspace_agents
JOIN
	workspace_resources ON workspace_agents.resource_id = workspace_resources.id
JOIN
	workspace_builds ON workspace_resources.job_id = workspace_builds.job_id
WHERE
	workspace_builds.workspace_id = $1 :: uuid AND
	workspace_builds.build_number = $2 :: int
	-- Filter out deleted sub agents.
	AND workspace_agents.deleted = FALSE
`

type GetWorkspaceAgentsByWorkspaceAndBuildNumberParams struct {
	WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
	BuildNumber int32     `db:"build_number" json:"build_number"`
}

func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Context, arg GetWorkspaceAgentsByWorkspaceAndBuildNumberParams) ([]WorkspaceAgent, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByWorkspaceAndBuildNumber, arg.WorkspaceID, arg.BuildNumber)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgent
	for rows.Next() {
		var i WorkspaceAgent
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.FirstConnectedAt,
			&i.LastConnectedAt,
			&i.DisconnectedAt,
			&i.ResourceID,
			&i.AuthToken,
			&i.AuthInstanceID,
			&i.Architecture,
			&i.EnvironmentVariables,
			&i.OperatingSystem,
			&i.InstanceMetadata,
			&i.ResourceMetadata,
			&i.Directory,
			&i.Version,
			&i.LastConnectedReplicaID,
			&i.ConnectionTimeoutSeconds,
			&i.TroubleshootingURL,
			&i.MOTDFile,
			&i.LifecycleState,
			&i.ExpandedDirectory,
			&i.LogsLength,
			&i.LogsOverflowed,
			&i.StartedAt,
			&i.ReadyAt,
			pq.Array(&i.Subsystems),
			pq.Array(&i.DisplayApps),
			&i.APIVersion,
			&i.DisplayOrder,
			&i.ParentID,
			&i.APIKeyScope,
			&i.Deleted,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many
SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted FROM workspace_agents
WHERE
	created_at > $1
	-- Filter out deleted sub agents.
	AND deleted = FALSE
`

func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsCreatedAfter, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgent
	for rows.Next() {
		var i WorkspaceAgent
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.FirstConnectedAt,
			&i.LastConnectedAt,
			&i.DisconnectedAt,
			&i.ResourceID,
			&i.AuthToken,
			&i.AuthInstanceID,
			&i.Architecture,
			&i.EnvironmentVariables,
			&i.OperatingSystem,
			&i.InstanceMetadata,
			&i.ResourceMetadata,
			&i.Directory,
			&i.Version,
			&i.LastConnectedReplicaID,
			&i.ConnectionTimeoutSeconds,
			&i.TroubleshootingURL,
			&i.MOTDFile,
			&i.LifecycleState,
			&i.ExpandedDirectory,
			&i.LogsLength,
			&i.LogsOverflowed,
			&i.StartedAt,
			&i.ReadyAt,
			pq.Array(&i.Subsystems),
			pq.Array(&i.DisplayApps),
			&i.APIVersion,
			&i.DisplayOrder,
			&i.ParentID,
			&i.APIKeyScope,
			&i.Deleted,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentsForMetrics = `-- name: GetWorkspaceAgentsForMetrics :many
SELECT
    w.id as workspace_id,
    w.name as workspace_name,
    u.username as owner_username,
    t.name as template_name,
    tv.name as template_version_name,
    workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
FROM workspaces w
JOIN users u ON w.owner_id = u.id
JOIN templates t ON w.template_id = t.id
JOIN workspace_builds wb ON w.id = wb.workspace_id
LEFT JOIN template_versions tv ON wb.template_version_id = tv.id
JOIN workspace_resources wr ON wb.job_id = wr.job_id
JOIN workspace_agents ON wr.id = workspace_agents.resource_id
WHERE w.deleted = false
AND wb.build_number = (
    SELECT MAX(wb2.build_number)
    FROM workspace_builds wb2
    WHERE wb2.workspace_id = w.id
)
AND workspace_agents.deleted = FALSE
`

type GetWorkspaceAgentsForMetricsRow struct {
	WorkspaceID         uuid.UUID      `db:"workspace_id" json:"workspace_id"`
	WorkspaceName       string         `db:"workspace_name" json:"workspace_name"`
	OwnerUsername       string         `db:"owner_username" json:"owner_username"`
	TemplateName        string         `db:"template_name" json:"template_name"`
	TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"`
	WorkspaceAgent      WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
}

func (q *sqlQuerier) GetWorkspaceAgentsForMetrics(ctx context.Context) ([]GetWorkspaceAgentsForMetricsRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsForMetrics)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceAgentsForMetricsRow
	for rows.Next() {
		var i GetWorkspaceAgentsForMetricsRow
		if err := rows.Scan(
			&i.WorkspaceID,
			&i.WorkspaceName,
			&i.OwnerUsername,
			&i.TemplateName,
			&i.TemplateVersionName,
			&i.WorkspaceAgent.ID,
			&i.WorkspaceAgent.CreatedAt,
			&i.WorkspaceAgent.UpdatedAt,
			&i.WorkspaceAgent.Name,
			&i.WorkspaceAgent.FirstConnectedAt,
			&i.WorkspaceAgent.LastConnectedAt,
			&i.WorkspaceAgent.DisconnectedAt,
			&i.WorkspaceAgent.ResourceID,
			&i.WorkspaceAgent.AuthToken,
			&i.WorkspaceAgent.AuthInstanceID,
			&i.WorkspaceAgent.Architecture,
			&i.WorkspaceAgent.EnvironmentVariables,
			&i.WorkspaceAgent.OperatingSystem,
			&i.WorkspaceAgent.InstanceMetadata,
			&i.WorkspaceAgent.ResourceMetadata,
			&i.WorkspaceAgent.Directory,
			&i.WorkspaceAgent.Version,
			&i.WorkspaceAgent.LastConnectedReplicaID,
			&i.WorkspaceAgent.ConnectionTimeoutSeconds,
			&i.WorkspaceAgent.TroubleshootingURL,
			&i.WorkspaceAgent.MOTDFile,
			&i.WorkspaceAgent.LifecycleState,
			&i.WorkspaceAgent.ExpandedDirectory,
			&i.WorkspaceAgent.LogsLength,
			&i.WorkspaceAgent.LogsOverflowed,
			&i.WorkspaceAgent.StartedAt,
			&i.WorkspaceAgent.ReadyAt,
			pq.Array(&i.WorkspaceAgent.Subsystems),
			pq.Array(&i.WorkspaceAgent.DisplayApps),
			&i.WorkspaceAgent.APIVersion,
			&i.WorkspaceAgent.DisplayOrder,
			&i.WorkspaceAgent.ParentID,
			&i.WorkspaceAgent.APIKeyScope,
			&i.WorkspaceAgent.Deleted,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many
SELECT
	workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted
FROM
	workspace_agents
JOIN
	workspace_resources ON workspace_agents.resource_id = workspace_resources.id
JOIN
	workspace_builds ON workspace_resources.job_id = workspace_builds.job_id
WHERE
	workspace_builds.workspace_id = $1 :: uuid AND
	workspace_builds.build_number = (
    	SELECT
			MAX(build_number)
    	FROM
			workspace_builds AS wb
    	WHERE
			wb.workspace_id = $1 :: uuid
	)
	-- Filter out deleted sub agents.
	AND workspace_agents.deleted = FALSE
`

func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsInLatestBuildByWorkspaceID, workspaceID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgent
	for rows.Next() {
		var i WorkspaceAgent
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.Name,
			&i.FirstConnectedAt,
			&i.LastConnectedAt,
			&i.DisconnectedAt,
			&i.ResourceID,
			&i.AuthToken,
			&i.AuthInstanceID,
			&i.Architecture,
			&i.EnvironmentVariables,
			&i.OperatingSystem,
			&i.InstanceMetadata,
			&i.ResourceMetadata,
			&i.Directory,
			&i.Version,
			&i.LastConnectedReplicaID,
			&i.ConnectionTimeoutSeconds,
			&i.TroubleshootingURL,
			&i.MOTDFile,
			&i.LifecycleState,
			&i.ExpandedDirectory,
			&i.LogsLength,
			&i.LogsOverflowed,
			&i.StartedAt,
			&i.ReadyAt,
			pq.Array(&i.Subsystems),
			pq.Array(&i.DisplayApps),
			&i.APIVersion,
			&i.DisplayOrder,
			&i.ParentID,
			&i.APIKeyScope,
			&i.Deleted,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceBuildAgentsByInstanceID = `-- name: GetWorkspaceBuildAgentsByInstanceID :many
SELECT
	workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
	workspace_builds.id AS workspace_build_id,
	workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl
FROM
	workspace_agents
JOIN
	workspace_resources
ON
	workspace_resources.id = workspace_agents.resource_id
JOIN
	workspace_builds
ON
	workspace_builds.job_id = workspace_resources.job_id
JOIN
	provisioner_jobs
ON
	provisioner_jobs.id = workspace_builds.job_id
JOIN
	workspaces
ON
	workspaces.id = workspace_builds.workspace_id
WHERE
	workspace_agents.auth_instance_id = $1 :: TEXT
	AND workspace_agents.deleted = FALSE
	AND workspace_agents.parent_id IS NULL
	AND provisioner_jobs.type = 'workspace_build'::provisioner_job_type
	AND workspaces.deleted = FALSE
ORDER BY
	workspace_agents.created_at DESC
`

type GetWorkspaceBuildAgentsByInstanceIDRow struct {
	WorkspaceAgent   WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"`
	WorkspaceBuildID uuid.UUID      `db:"workspace_build_id" json:"workspace_build_id"`
	WorkspaceTable   WorkspaceTable `db:"workspace_table" json:"workspace_table"`
}

func (q *sqlQuerier) GetWorkspaceBuildAgentsByInstanceID(ctx context.Context, authInstanceID string) ([]GetWorkspaceBuildAgentsByInstanceIDRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceBuildAgentsByInstanceID, authInstanceID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceBuildAgentsByInstanceIDRow
	for rows.Next() {
		var i GetWorkspaceBuildAgentsByInstanceIDRow
		if err := rows.Scan(
			&i.WorkspaceAgent.ID,
			&i.WorkspaceAgent.CreatedAt,
			&i.WorkspaceAgent.UpdatedAt,
			&i.WorkspaceAgent.Name,
			&i.WorkspaceAgent.FirstConnectedAt,
			&i.WorkspaceAgent.LastConnectedAt,
			&i.WorkspaceAgent.DisconnectedAt,
			&i.WorkspaceAgent.ResourceID,
			&i.WorkspaceAgent.AuthToken,
			&i.WorkspaceAgent.AuthInstanceID,
			&i.WorkspaceAgent.Architecture,
			&i.WorkspaceAgent.EnvironmentVariables,
			&i.WorkspaceAgent.OperatingSystem,
			&i.WorkspaceAgent.InstanceMetadata,
			&i.WorkspaceAgent.ResourceMetadata,
			&i.WorkspaceAgent.Directory,
			&i.WorkspaceAgent.Version,
			&i.WorkspaceAgent.LastConnectedReplicaID,
			&i.WorkspaceAgent.ConnectionTimeoutSeconds,
			&i.WorkspaceAgent.TroubleshootingURL,
			&i.WorkspaceAgent.MOTDFile,
			&i.WorkspaceAgent.LifecycleState,
			&i.WorkspaceAgent.ExpandedDirectory,
			&i.WorkspaceAgent.LogsLength,
			&i.WorkspaceAgent.LogsOverflowed,
			&i.WorkspaceAgent.StartedAt,
			&i.WorkspaceAgent.ReadyAt,
			pq.Array(&i.WorkspaceAgent.Subsystems),
			pq.Array(&i.WorkspaceAgent.DisplayApps),
			&i.WorkspaceAgent.APIVersion,
			&i.WorkspaceAgent.DisplayOrder,
			&i.WorkspaceAgent.ParentID,
			&i.WorkspaceAgent.APIKeyScope,
			&i.WorkspaceAgent.Deleted,
			&i.WorkspaceBuildID,
			&i.WorkspaceTable.ID,
			&i.WorkspaceTable.CreatedAt,
			&i.WorkspaceTable.UpdatedAt,
			&i.WorkspaceTable.OwnerID,
			&i.WorkspaceTable.OrganizationID,
			&i.WorkspaceTable.TemplateID,
			&i.WorkspaceTable.Deleted,
			&i.WorkspaceTable.Name,
			&i.WorkspaceTable.AutostartSchedule,
			&i.WorkspaceTable.Ttl,
			&i.WorkspaceTable.LastUsedAt,
			&i.WorkspaceTable.DormantAt,
			&i.WorkspaceTable.DeletingAt,
			&i.WorkspaceTable.AutomaticUpdates,
			&i.WorkspaceTable.Favorite,
			&i.WorkspaceTable.NextStartAt,
			&i.WorkspaceTable.GroupACL,
			&i.WorkspaceTable.UserACL,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceAgent = `-- name: InsertWorkspaceAgent :one
INSERT INTO
	workspace_agents (
		id,
		parent_id,
		created_at,
		updated_at,
		name,
		resource_id,
		auth_token,
		auth_instance_id,
		architecture,
		environment_variables,
		operating_system,
		directory,
		instance_metadata,
		resource_metadata,
		connection_timeout_seconds,
		troubleshooting_url,
		motd_file,
		display_apps,
		display_order,
		api_key_scope
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted
`

type InsertWorkspaceAgentParams struct {
	ID                       uuid.UUID             `db:"id" json:"id"`
	ParentID                 uuid.NullUUID         `db:"parent_id" json:"parent_id"`
	CreatedAt                time.Time             `db:"created_at" json:"created_at"`
	UpdatedAt                time.Time             `db:"updated_at" json:"updated_at"`
	Name                     string                `db:"name" json:"name"`
	ResourceID               uuid.UUID             `db:"resource_id" json:"resource_id"`
	AuthToken                uuid.UUID             `db:"auth_token" json:"auth_token"`
	AuthInstanceID           sql.NullString        `db:"auth_instance_id" json:"auth_instance_id"`
	Architecture             string                `db:"architecture" json:"architecture"`
	EnvironmentVariables     pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"`
	OperatingSystem          string                `db:"operating_system" json:"operating_system"`
	Directory                string                `db:"directory" json:"directory"`
	InstanceMetadata         pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"`
	ResourceMetadata         pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"`
	ConnectionTimeoutSeconds int32                 `db:"connection_timeout_seconds" json:"connection_timeout_seconds"`
	TroubleshootingURL       string                `db:"troubleshooting_url" json:"troubleshooting_url"`
	MOTDFile                 string                `db:"motd_file" json:"motd_file"`
	DisplayApps              []DisplayApp          `db:"display_apps" json:"display_apps"`
	DisplayOrder             int32                 `db:"display_order" json:"display_order"`
	APIKeyScope              AgentKeyScopeEnum     `db:"api_key_scope" json:"api_key_scope"`
}

func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspaceAgentParams) (WorkspaceAgent, error) {
	row := q.db.QueryRowContext(ctx, insertWorkspaceAgent,
		arg.ID,
		arg.ParentID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.Name,
		arg.ResourceID,
		arg.AuthToken,
		arg.AuthInstanceID,
		arg.Architecture,
		arg.EnvironmentVariables,
		arg.OperatingSystem,
		arg.Directory,
		arg.InstanceMetadata,
		arg.ResourceMetadata,
		arg.ConnectionTimeoutSeconds,
		arg.TroubleshootingURL,
		arg.MOTDFile,
		pq.Array(arg.DisplayApps),
		arg.DisplayOrder,
		arg.APIKeyScope,
	)
	var i WorkspaceAgent
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Name,
		&i.FirstConnectedAt,
		&i.LastConnectedAt,
		&i.DisconnectedAt,
		&i.ResourceID,
		&i.AuthToken,
		&i.AuthInstanceID,
		&i.Architecture,
		&i.EnvironmentVariables,
		&i.OperatingSystem,
		&i.InstanceMetadata,
		&i.ResourceMetadata,
		&i.Directory,
		&i.Version,
		&i.LastConnectedReplicaID,
		&i.ConnectionTimeoutSeconds,
		&i.TroubleshootingURL,
		&i.MOTDFile,
		&i.LifecycleState,
		&i.ExpandedDirectory,
		&i.LogsLength,
		&i.LogsOverflowed,
		&i.StartedAt,
		&i.ReadyAt,
		pq.Array(&i.Subsystems),
		pq.Array(&i.DisplayApps),
		&i.APIVersion,
		&i.DisplayOrder,
		&i.ParentID,
		&i.APIKeyScope,
		&i.Deleted,
	)
	return i, err
}

const insertWorkspaceAgentLogSources = `-- name: InsertWorkspaceAgentLogSources :many
INSERT INTO
		workspace_agent_log_sources (workspace_agent_id, created_at, id, display_name, icon)
	SELECT
		$1 :: uuid AS workspace_agent_id,
		$2 :: timestamptz AS created_at,
		unnest($3 :: uuid [ ]) AS id,
		unnest($4 :: VARCHAR(127) [ ]) AS display_name,
		unnest($5 :: text [ ]) AS icon
	RETURNING workspace_agent_log_sources.workspace_agent_id, workspace_agent_log_sources.id, workspace_agent_log_sources.created_at, workspace_agent_log_sources.display_name, workspace_agent_log_sources.icon
`

type InsertWorkspaceAgentLogSourcesParams struct {
	WorkspaceAgentID uuid.UUID   `db:"workspace_agent_id" json:"workspace_agent_id"`
	CreatedAt        time.Time   `db:"created_at" json:"created_at"`
	ID               []uuid.UUID `db:"id" json:"id"`
	DisplayName      []string    `db:"display_name" json:"display_name"`
	Icon             []string    `db:"icon" json:"icon"`
}

func (q *sqlQuerier) InsertWorkspaceAgentLogSources(ctx context.Context, arg InsertWorkspaceAgentLogSourcesParams) ([]WorkspaceAgentLogSource, error) {
	rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentLogSources,
		arg.WorkspaceAgentID,
		arg.CreatedAt,
		pq.Array(arg.ID),
		pq.Array(arg.DisplayName),
		pq.Array(arg.Icon),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentLogSource
	for rows.Next() {
		var i WorkspaceAgentLogSource
		if err := rows.Scan(
			&i.WorkspaceAgentID,
			&i.ID,
			&i.CreatedAt,
			&i.DisplayName,
			&i.Icon,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceAgentLogs = `-- name: InsertWorkspaceAgentLogs :many
WITH new_length AS (
	UPDATE workspace_agents SET
	logs_length = logs_length + $6 WHERE workspace_agents.id = $1
)
INSERT INTO
		workspace_agent_logs (agent_id, created_at, output, level, log_source_id)
	SELECT
		$1 :: uuid AS agent_id,
		$2 :: timestamptz AS created_at,
		unnest($3 :: VARCHAR(1024) [ ]) AS output,
		unnest($4 :: log_level [ ]) AS level,
		$5 :: uuid AS log_source_id
	RETURNING workspace_agent_logs.agent_id, workspace_agent_logs.created_at, workspace_agent_logs.output, workspace_agent_logs.id, workspace_agent_logs.level, workspace_agent_logs.log_source_id
`

type InsertWorkspaceAgentLogsParams struct {
	AgentID      uuid.UUID  `db:"agent_id" json:"agent_id"`
	CreatedAt    time.Time  `db:"created_at" json:"created_at"`
	Output       []string   `db:"output" json:"output"`
	Level        []LogLevel `db:"level" json:"level"`
	LogSourceID  uuid.UUID  `db:"log_source_id" json:"log_source_id"`
	OutputLength int32      `db:"output_length" json:"output_length"`
}

func (q *sqlQuerier) InsertWorkspaceAgentLogs(ctx context.Context, arg InsertWorkspaceAgentLogsParams) ([]WorkspaceAgentLog, error) {
	rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentLogs,
		arg.AgentID,
		arg.CreatedAt,
		pq.Array(arg.Output),
		pq.Array(arg.Level),
		arg.LogSourceID,
		arg.OutputLength,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentLog
	for rows.Next() {
		var i WorkspaceAgentLog
		if err := rows.Scan(
			&i.AgentID,
			&i.CreatedAt,
			&i.Output,
			&i.ID,
			&i.Level,
			&i.LogSourceID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceAgentMetadata = `-- name: InsertWorkspaceAgentMetadata :exec
INSERT INTO
	workspace_agent_metadata (
		workspace_agent_id,
		display_name,
		key,
		script,
		timeout,
		interval,
		display_order
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7)
`

type InsertWorkspaceAgentMetadataParams struct {
	WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
	DisplayName      string    `db:"display_name" json:"display_name"`
	Key              string    `db:"key" json:"key"`
	Script           string    `db:"script" json:"script"`
	Timeout          int64     `db:"timeout" json:"timeout"`
	Interval         int64     `db:"interval" json:"interval"`
	DisplayOrder     int32     `db:"display_order" json:"display_order"`
}

func (q *sqlQuerier) InsertWorkspaceAgentMetadata(ctx context.Context, arg InsertWorkspaceAgentMetadataParams) error {
	_, err := q.db.ExecContext(ctx, insertWorkspaceAgentMetadata,
		arg.WorkspaceAgentID,
		arg.DisplayName,
		arg.Key,
		arg.Script,
		arg.Timeout,
		arg.Interval,
		arg.DisplayOrder,
	)
	return err
}

const insertWorkspaceAgentScriptTimings = `-- name: InsertWorkspaceAgentScriptTimings :one
INSERT INTO
    workspace_agent_script_timings (
        script_id,
        started_at,
        ended_at,
        exit_code,
        stage,
        status
    )
VALUES
    ($1, $2, $3, $4, $5, $6)
RETURNING workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status
`

type InsertWorkspaceAgentScriptTimingsParams struct {
	ScriptID  uuid.UUID                        `db:"script_id" json:"script_id"`
	StartedAt time.Time                        `db:"started_at" json:"started_at"`
	EndedAt   time.Time                        `db:"ended_at" json:"ended_at"`
	ExitCode  int32                            `db:"exit_code" json:"exit_code"`
	Stage     WorkspaceAgentScriptTimingStage  `db:"stage" json:"stage"`
	Status    WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
}

func (q *sqlQuerier) InsertWorkspaceAgentScriptTimings(ctx context.Context, arg InsertWorkspaceAgentScriptTimingsParams) (WorkspaceAgentScriptTiming, error) {
	row := q.db.QueryRowContext(ctx, insertWorkspaceAgentScriptTimings,
		arg.ScriptID,
		arg.StartedAt,
		arg.EndedAt,
		arg.ExitCode,
		arg.Stage,
		arg.Status,
	)
	var i WorkspaceAgentScriptTiming
	err := row.Scan(
		&i.ScriptID,
		&i.StartedAt,
		&i.EndedAt,
		&i.ExitCode,
		&i.Stage,
		&i.Status,
	)
	return i, err
}

const softDeletePriorWorkspaceAgents = `-- name: SoftDeletePriorWorkspaceAgents :exec
UPDATE workspace_agents
SET deleted = TRUE
WHERE id IN (
    SELECT wa.id
    FROM workspace_agents wa
    JOIN workspace_resources wr ON wr.id = wa.resource_id
    JOIN workspace_builds wb ON wb.job_id = wr.job_id
    WHERE wb.workspace_id = $1
      AND wb.id <> $2
      AND wa.deleted = FALSE
)
`

type SoftDeletePriorWorkspaceAgentsParams struct {
	WorkspaceID    uuid.UUID `db:"workspace_id" json:"workspace_id"`
	CurrentBuildID uuid.UUID `db:"current_build_id" json:"current_build_id"`
}

// Marks agents from all prior builds of this workspace as deleted,
// preserving only agents belonging to @current_build_id. Called from
// provisionerdserver when a workspace build completes, after the new
// build's agents have been inserted, so running agents are not
// deleted while a build is still queued or provisioning.
func (q *sqlQuerier) SoftDeletePriorWorkspaceAgents(ctx context.Context, arg SoftDeletePriorWorkspaceAgentsParams) error {
	_, err := q.db.ExecContext(ctx, softDeletePriorWorkspaceAgents, arg.WorkspaceID, arg.CurrentBuildID)
	return err
}

const softDeleteWorkspaceAgentsByWorkspaceID = `-- name: SoftDeleteWorkspaceAgentsByWorkspaceID :exec
UPDATE workspace_agents
SET deleted = TRUE
WHERE id IN (
    SELECT wa.id
    FROM workspace_agents wa
    JOIN workspace_resources wr ON wr.id = wa.resource_id
    JOIN workspace_builds wb ON wb.job_id = wr.job_id
    WHERE wb.workspace_id = $1
      AND wa.deleted = FALSE
)
`

// Marks every non-deleted agent belonging to the given workspace as
// deleted. Called alongside UpdateWorkspaceDeletedByID when a workspace
// itself is soft-deleted, so the agent instance-identity auth path
// (which filters on workspace_agents.deleted) doesn't keep seeing
// orphaned rows.
func (q *sqlQuerier) SoftDeleteWorkspaceAgentsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, softDeleteWorkspaceAgentsByWorkspaceID, workspaceID)
	return err
}

const updateWorkspaceAgentConnectionByID = `-- name: UpdateWorkspaceAgentConnectionByID :exec
UPDATE
	workspace_agents
SET
	first_connected_at = $2,
	last_connected_at = $3,
	last_connected_replica_id = $4,
	disconnected_at = $5,
	updated_at = $6
WHERE
	id = $1
`

type UpdateWorkspaceAgentConnectionByIDParams struct {
	ID                     uuid.UUID     `db:"id" json:"id"`
	FirstConnectedAt       sql.NullTime  `db:"first_connected_at" json:"first_connected_at"`
	LastConnectedAt        sql.NullTime  `db:"last_connected_at" json:"last_connected_at"`
	LastConnectedReplicaID uuid.NullUUID `db:"last_connected_replica_id" json:"last_connected_replica_id"`
	DisconnectedAt         sql.NullTime  `db:"disconnected_at" json:"disconnected_at"`
	UpdatedAt              time.Time     `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAgentConnectionByID,
		arg.ID,
		arg.FirstConnectedAt,
		arg.LastConnectedAt,
		arg.LastConnectedReplicaID,
		arg.DisconnectedAt,
		arg.UpdatedAt,
	)
	return err
}

const updateWorkspaceAgentDirectoryByID = `-- name: UpdateWorkspaceAgentDirectoryByID :exec
UPDATE
	workspace_agents
SET
	directory = $2, updated_at = $3
WHERE
	id = $1
`

type UpdateWorkspaceAgentDirectoryByIDParams struct {
	ID        uuid.UUID `db:"id" json:"id"`
	Directory string    `db:"directory" json:"directory"`
	UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateWorkspaceAgentDirectoryByID(ctx context.Context, arg UpdateWorkspaceAgentDirectoryByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAgentDirectoryByID, arg.ID, arg.Directory, arg.UpdatedAt)
	return err
}

const updateWorkspaceAgentDisplayAppsByID = `-- name: UpdateWorkspaceAgentDisplayAppsByID :exec
UPDATE
	workspace_agents
SET
	display_apps = $2, updated_at = $3
WHERE
	id = $1
`

type UpdateWorkspaceAgentDisplayAppsByIDParams struct {
	ID          uuid.UUID    `db:"id" json:"id"`
	DisplayApps []DisplayApp `db:"display_apps" json:"display_apps"`
	UpdatedAt   time.Time    `db:"updated_at" json:"updated_at"`
}

func (q *sqlQuerier) UpdateWorkspaceAgentDisplayAppsByID(ctx context.Context, arg UpdateWorkspaceAgentDisplayAppsByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAgentDisplayAppsByID, arg.ID, pq.Array(arg.DisplayApps), arg.UpdatedAt)
	return err
}

const updateWorkspaceAgentLifecycleStateByID = `-- name: UpdateWorkspaceAgentLifecycleStateByID :exec
UPDATE
	workspace_agents
SET
	lifecycle_state = $2,
	started_at = $3,
	ready_at = $4
WHERE
	id = $1
`

type UpdateWorkspaceAgentLifecycleStateByIDParams struct {
	ID             uuid.UUID                    `db:"id" json:"id"`
	LifecycleState WorkspaceAgentLifecycleState `db:"lifecycle_state" json:"lifecycle_state"`
	StartedAt      sql.NullTime                 `db:"started_at" json:"started_at"`
	ReadyAt        sql.NullTime                 `db:"ready_at" json:"ready_at"`
}

func (q *sqlQuerier) UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAgentLifecycleStateByID,
		arg.ID,
		arg.LifecycleState,
		arg.StartedAt,
		arg.ReadyAt,
	)
	return err
}

const updateWorkspaceAgentLogOverflowByID = `-- name: UpdateWorkspaceAgentLogOverflowByID :exec
UPDATE
	workspace_agents
SET
	logs_overflowed = $2
WHERE
	id = $1
`

type UpdateWorkspaceAgentLogOverflowByIDParams struct {
	ID             uuid.UUID `db:"id" json:"id"`
	LogsOverflowed bool      `db:"logs_overflowed" json:"logs_overflowed"`
}

func (q *sqlQuerier) UpdateWorkspaceAgentLogOverflowByID(ctx context.Context, arg UpdateWorkspaceAgentLogOverflowByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAgentLogOverflowByID, arg.ID, arg.LogsOverflowed)
	return err
}

const updateWorkspaceAgentMetadata = `-- name: UpdateWorkspaceAgentMetadata :exec
WITH metadata AS (
	SELECT
		unnest($2::text[]) AS key,
		unnest($3::text[]) AS value,
		unnest($4::text[]) AS error,
		unnest($5::timestamptz[]) AS collected_at
)
UPDATE
	workspace_agent_metadata wam
SET
	value = m.value,
	error = m.error,
	collected_at = m.collected_at
FROM
	metadata m
WHERE
	wam.workspace_agent_id = $1
	AND wam.key = m.key
`

type UpdateWorkspaceAgentMetadataParams struct {
	WorkspaceAgentID uuid.UUID   `db:"workspace_agent_id" json:"workspace_agent_id"`
	Key              []string    `db:"key" json:"key"`
	Value            []string    `db:"value" json:"value"`
	Error            []string    `db:"error" json:"error"`
	CollectedAt      []time.Time `db:"collected_at" json:"collected_at"`
}

func (q *sqlQuerier) UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAgentMetadata,
		arg.WorkspaceAgentID,
		pq.Array(arg.Key),
		pq.Array(arg.Value),
		pq.Array(arg.Error),
		pq.Array(arg.CollectedAt),
	)
	return err
}

const updateWorkspaceAgentStartupByID = `-- name: UpdateWorkspaceAgentStartupByID :exec
UPDATE
	workspace_agents
SET
	version = $2,
	expanded_directory = $3,
	subsystems = $4,
	api_version = $5
WHERE
	id = $1
`

type UpdateWorkspaceAgentStartupByIDParams struct {
	ID                uuid.UUID                 `db:"id" json:"id"`
	Version           string                    `db:"version" json:"version"`
	ExpandedDirectory string                    `db:"expanded_directory" json:"expanded_directory"`
	Subsystems        []WorkspaceAgentSubsystem `db:"subsystems" json:"subsystems"`
	APIVersion        string                    `db:"api_version" json:"api_version"`
}

func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAgentStartupByID,
		arg.ID,
		arg.Version,
		arg.ExpandedDirectory,
		pq.Array(arg.Subsystems),
		arg.APIVersion,
	)
	return err
}

const deleteOldWorkspaceAgentStats = `-- name: DeleteOldWorkspaceAgentStats :exec
DELETE FROM
	workspace_agent_stats
WHERE
	created_at < (
		SELECT
			COALESCE(
				-- When generating initial template usage stats, all the
				-- raw agent stats are needed, after that only ~30 mins
				-- from last rollup is needed. Deployment stats seem to
				-- use between 15 mins and 1 hour of data. We keep a
				-- little bit more (1 day) just in case.
				MAX(start_time) - '1 days'::interval,
				-- Fall back to ~6 months ago if there are no template
				-- usage stats so that we don't delete the data before
				-- it's rolled up.
				NOW() - '180 days'::interval
			)
		FROM
			template_usage_stats
	)
	AND created_at < (
		-- Delete at most in batches of 4 hours (with this batch size, assuming
		-- 1 iteration / 10 minutes, we can clear out the previous 6 months of
		-- data in 7.5 days) whilst keeping the DB load low.
		SELECT
			COALESCE(MIN(created_at) + '4 hours'::interval, NOW())
		FROM
			workspace_agent_stats
	)
`

func (q *sqlQuerier) DeleteOldWorkspaceAgentStats(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, deleteOldWorkspaceAgentStats)
	return err
}

const getDeploymentWorkspaceAgentStats = `-- name: GetDeploymentWorkspaceAgentStats :one
WITH stats AS (
    SELECT
        agent_id,
        created_at,
        rx_bytes,
        tx_bytes,
        connection_median_latency_ms,
        session_count_vscode,
        session_count_ssh,
        session_count_jetbrains,
        session_count_reconnecting_pty,
        ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY created_at DESC) AS rn
    FROM workspace_agent_stats
    WHERE created_at > $1
)
SELECT
    coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
    coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
	-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
    coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_50,
    coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_95,
    coalesce(SUM(session_count_vscode) FILTER (WHERE rn = 1), 0)::bigint AS session_count_vscode,
    coalesce(SUM(session_count_ssh) FILTER (WHERE rn = 1), 0)::bigint AS session_count_ssh,
    coalesce(SUM(session_count_jetbrains) FILTER (WHERE rn = 1), 0)::bigint AS session_count_jetbrains,
    coalesce(SUM(session_count_reconnecting_pty) FILTER (WHERE rn = 1), 0)::bigint AS session_count_reconnecting_pty
FROM stats
`

type GetDeploymentWorkspaceAgentStatsRow struct {
	WorkspaceRxBytes             int64   `db:"workspace_rx_bytes" json:"workspace_rx_bytes"`
	WorkspaceTxBytes             int64   `db:"workspace_tx_bytes" json:"workspace_tx_bytes"`
	WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
	WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
	SessionCountVSCode           int64   `db:"session_count_vscode" json:"session_count_vscode"`
	SessionCountSSH              int64   `db:"session_count_ssh" json:"session_count_ssh"`
	SessionCountJetBrains        int64   `db:"session_count_jetbrains" json:"session_count_jetbrains"`
	SessionCountReconnectingPTY  int64   `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
}

func (q *sqlQuerier) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) {
	row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceAgentStats, createdAt)
	var i GetDeploymentWorkspaceAgentStatsRow
	err := row.Scan(
		&i.WorkspaceRxBytes,
		&i.WorkspaceTxBytes,
		&i.WorkspaceConnectionLatency50,
		&i.WorkspaceConnectionLatency95,
		&i.SessionCountVSCode,
		&i.SessionCountSSH,
		&i.SessionCountJetBrains,
		&i.SessionCountReconnectingPTY,
	)
	return i, err
}

const getDeploymentWorkspaceAgentUsageStats = `-- name: GetDeploymentWorkspaceAgentUsageStats :one
WITH agent_stats AS (
	SELECT
		coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
		coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
		coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
		coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
	 FROM workspace_agent_stats
	 	-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
		WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
),
minute_buckets AS (
	SELECT
		agent_id,
		date_trunc('minute', created_at) AS minute_bucket,
		coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
		coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
		coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
		coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
	FROM
		workspace_agent_stats
	WHERE
		created_at >= $1
		AND created_at < date_trunc('minute', now())  -- Exclude current partial minute
		AND usage = true
	GROUP BY
		agent_id,
		minute_bucket
),
latest_buckets AS (
	SELECT DISTINCT ON (agent_id)
		agent_id,
		minute_bucket,
		session_count_vscode,
		session_count_jetbrains,
		session_count_reconnecting_pty,
		session_count_ssh
	FROM
		minute_buckets
	ORDER BY
		agent_id,
		minute_bucket DESC
),
latest_agent_stats AS (
    SELECT
		coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
		coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
		coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
		coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
    FROM
        latest_buckets
)
SELECT workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats, latest_agent_stats
`

type GetDeploymentWorkspaceAgentUsageStatsRow struct {
	WorkspaceRxBytes             int64   `db:"workspace_rx_bytes" json:"workspace_rx_bytes"`
	WorkspaceTxBytes             int64   `db:"workspace_tx_bytes" json:"workspace_tx_bytes"`
	WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
	WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
	SessionCountVSCode           int64   `db:"session_count_vscode" json:"session_count_vscode"`
	SessionCountSSH              int64   `db:"session_count_ssh" json:"session_count_ssh"`
	SessionCountJetBrains        int64   `db:"session_count_jetbrains" json:"session_count_jetbrains"`
	SessionCountReconnectingPTY  int64   `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
}

func (q *sqlQuerier) GetDeploymentWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentUsageStatsRow, error) {
	row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceAgentUsageStats, createdAt)
	var i GetDeploymentWorkspaceAgentUsageStatsRow
	err := row.Scan(
		&i.WorkspaceRxBytes,
		&i.WorkspaceTxBytes,
		&i.WorkspaceConnectionLatency50,
		&i.WorkspaceConnectionLatency95,
		&i.SessionCountVSCode,
		&i.SessionCountSSH,
		&i.SessionCountJetBrains,
		&i.SessionCountReconnectingPTY,
	)
	return i, err
}

const getWorkspaceAgentStats = `-- name: GetWorkspaceAgentStats :many
WITH agent_stats AS (
	SELECT
		user_id,
		agent_id,
		workspace_id,
		template_id,
		MIN(created_at)::timestamptz AS aggregated_from,
		coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
		coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
		coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
		coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
	 FROM workspace_agent_stats
	-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
	WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
	GROUP BY user_id, agent_id, workspace_id, template_id
), latest_agent_stats AS (
	SELECT
		a.agent_id,
		coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
		coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
		coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
		coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
	 FROM (
		SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn
		FROM workspace_agent_stats WHERE created_at > $1
	) AS a WHERE a.rn = 1 GROUP BY a.user_id, a.agent_id, a.workspace_id, a.template_id
)
SELECT user_id, agent_stats.agent_id, workspace_id, template_id, aggregated_from, workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, latest_agent_stats.agent_id, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats JOIN latest_agent_stats ON agent_stats.agent_id = latest_agent_stats.agent_id
`

type GetWorkspaceAgentStatsRow struct {
	UserID                       uuid.UUID `db:"user_id" json:"user_id"`
	AgentID                      uuid.UUID `db:"agent_id" json:"agent_id"`
	WorkspaceID                  uuid.UUID `db:"workspace_id" json:"workspace_id"`
	TemplateID                   uuid.UUID `db:"template_id" json:"template_id"`
	AggregatedFrom               time.Time `db:"aggregated_from" json:"aggregated_from"`
	WorkspaceRxBytes             int64     `db:"workspace_rx_bytes" json:"workspace_rx_bytes"`
	WorkspaceTxBytes             int64     `db:"workspace_tx_bytes" json:"workspace_tx_bytes"`
	WorkspaceConnectionLatency50 float64   `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
	WorkspaceConnectionLatency95 float64   `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
	AgentID_2                    uuid.UUID `db:"agent_id_2" json:"agent_id_2"`
	SessionCountVSCode           int64     `db:"session_count_vscode" json:"session_count_vscode"`
	SessionCountSSH              int64     `db:"session_count_ssh" json:"session_count_ssh"`
	SessionCountJetBrains        int64     `db:"session_count_jetbrains" json:"session_count_jetbrains"`
	SessionCountReconnectingPTY  int64     `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
}

func (q *sqlQuerier) GetWorkspaceAgentStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentStats, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceAgentStatsRow
	for rows.Next() {
		var i GetWorkspaceAgentStatsRow
		if err := rows.Scan(
			&i.UserID,
			&i.AgentID,
			&i.WorkspaceID,
			&i.TemplateID,
			&i.AggregatedFrom,
			&i.WorkspaceRxBytes,
			&i.WorkspaceTxBytes,
			&i.WorkspaceConnectionLatency50,
			&i.WorkspaceConnectionLatency95,
			&i.AgentID_2,
			&i.SessionCountVSCode,
			&i.SessionCountSSH,
			&i.SessionCountJetBrains,
			&i.SessionCountReconnectingPTY,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentStatsAndLabels = `-- name: GetWorkspaceAgentStatsAndLabels :many
WITH agent_stats AS (
	SELECT
		user_id,
		agent_id,
		workspace_id,
		coalesce(SUM(rx_bytes), 0)::bigint AS rx_bytes,
		coalesce(SUM(tx_bytes), 0)::bigint AS tx_bytes
	 FROM workspace_agent_stats
		WHERE workspace_agent_stats.created_at > $1
		GROUP BY user_id, agent_id, workspace_id
), latest_agent_stats AS (
	SELECT
		a.agent_id,
		coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
		coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
		coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
		coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty,
		coalesce(SUM(connection_count), 0)::bigint AS connection_count,
		coalesce(MAX(connection_median_latency_ms), 0)::float AS connection_median_latency_ms
	 FROM (
		SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn
		FROM workspace_agent_stats
		-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
		WHERE created_at > $1 AND connection_median_latency_ms > 0
	) AS a
	WHERE a.rn = 1
	GROUP BY a.user_id, a.agent_id, a.workspace_id
)
SELECT
	users.username, workspace_agents.name AS agent_name, workspaces.name AS workspace_name, rx_bytes, tx_bytes,
	session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty,
	connection_count, connection_median_latency_ms
FROM
	agent_stats
JOIN
	latest_agent_stats
ON
	agent_stats.agent_id = latest_agent_stats.agent_id
JOIN
	users
ON
	users.id = agent_stats.user_id
JOIN
	workspace_agents
ON
	workspace_agents.id = agent_stats.agent_id
JOIN
	workspaces
ON
	workspaces.id = agent_stats.workspace_id
`

type GetWorkspaceAgentStatsAndLabelsRow struct {
	Username                    string  `db:"username" json:"username"`
	AgentName                   string  `db:"agent_name" json:"agent_name"`
	WorkspaceName               string  `db:"workspace_name" json:"workspace_name"`
	RxBytes                     int64   `db:"rx_bytes" json:"rx_bytes"`
	TxBytes                     int64   `db:"tx_bytes" json:"tx_bytes"`
	SessionCountVSCode          int64   `db:"session_count_vscode" json:"session_count_vscode"`
	SessionCountSSH             int64   `db:"session_count_ssh" json:"session_count_ssh"`
	SessionCountJetBrains       int64   `db:"session_count_jetbrains" json:"session_count_jetbrains"`
	SessionCountReconnectingPTY int64   `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
	ConnectionCount             int64   `db:"connection_count" json:"connection_count"`
	ConnectionMedianLatencyMS   float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
}

func (q *sqlQuerier) GetWorkspaceAgentStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentStatsAndLabelsRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentStatsAndLabels, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceAgentStatsAndLabelsRow
	for rows.Next() {
		var i GetWorkspaceAgentStatsAndLabelsRow
		if err := rows.Scan(
			&i.Username,
			&i.AgentName,
			&i.WorkspaceName,
			&i.RxBytes,
			&i.TxBytes,
			&i.SessionCountVSCode,
			&i.SessionCountSSH,
			&i.SessionCountJetBrains,
			&i.SessionCountReconnectingPTY,
			&i.ConnectionCount,
			&i.ConnectionMedianLatencyMS,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentUsageStats = `-- name: GetWorkspaceAgentUsageStats :many
WITH agent_stats AS (
	SELECT
		user_id,
		agent_id,
		workspace_id,
		template_id,
		MIN(created_at)::timestamptz AS aggregated_from,
		coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
		coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
		coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
		coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
	FROM workspace_agent_stats
	-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
	WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
	GROUP BY user_id, agent_id, workspace_id, template_id
),
minute_buckets AS (
	SELECT
		agent_id,
		date_trunc('minute', created_at) AS minute_bucket,
		coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
		coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
		coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
		coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
	FROM
		workspace_agent_stats
	WHERE
		created_at >= $1
		AND created_at < date_trunc('minute', now())  -- Exclude current partial minute
		AND usage = true
	GROUP BY
		agent_id,
		minute_bucket,
		user_id,
		agent_id,
		workspace_id,
		template_id
),
latest_buckets AS (
	SELECT DISTINCT ON (agent_id)
		agent_id,
		session_count_vscode,
		session_count_ssh,
		session_count_jetbrains,
		session_count_reconnecting_pty
	FROM
		minute_buckets
	ORDER BY
		agent_id,
		minute_bucket DESC
)
SELECT user_id,
agent_stats.agent_id,
workspace_id,
template_id,
aggregated_from,
workspace_rx_bytes,
workspace_tx_bytes,
workspace_connection_latency_50,
workspace_connection_latency_95,
coalesce(latest_buckets.agent_id,agent_stats.agent_id) AS agent_id,
coalesce(session_count_vscode, 0)::bigint AS session_count_vscode,
coalesce(session_count_ssh, 0)::bigint AS session_count_ssh,
coalesce(session_count_jetbrains, 0)::bigint AS session_count_jetbrains,
coalesce(session_count_reconnecting_pty, 0)::bigint AS session_count_reconnecting_pty
FROM agent_stats LEFT JOIN latest_buckets ON agent_stats.agent_id = latest_buckets.agent_id
`

type GetWorkspaceAgentUsageStatsRow struct {
	UserID                       uuid.UUID `db:"user_id" json:"user_id"`
	AgentID                      uuid.UUID `db:"agent_id" json:"agent_id"`
	WorkspaceID                  uuid.UUID `db:"workspace_id" json:"workspace_id"`
	TemplateID                   uuid.UUID `db:"template_id" json:"template_id"`
	AggregatedFrom               time.Time `db:"aggregated_from" json:"aggregated_from"`
	WorkspaceRxBytes             int64     `db:"workspace_rx_bytes" json:"workspace_rx_bytes"`
	WorkspaceTxBytes             int64     `db:"workspace_tx_bytes" json:"workspace_tx_bytes"`
	WorkspaceConnectionLatency50 float64   `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
	WorkspaceConnectionLatency95 float64   `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
	AgentID_2                    uuid.UUID `db:"agent_id_2" json:"agent_id_2"`
	SessionCountVSCode           int64     `db:"session_count_vscode" json:"session_count_vscode"`
	SessionCountSSH              int64     `db:"session_count_ssh" json:"session_count_ssh"`
	SessionCountJetBrains        int64     `db:"session_count_jetbrains" json:"session_count_jetbrains"`
	SessionCountReconnectingPTY  int64     `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
}

// `minute_buckets` could return 0 rows if there are no usage stats since `created_at`.
func (q *sqlQuerier) GetWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentUsageStats, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceAgentUsageStatsRow
	for rows.Next() {
		var i GetWorkspaceAgentUsageStatsRow
		if err := rows.Scan(
			&i.UserID,
			&i.AgentID,
			&i.WorkspaceID,
			&i.TemplateID,
			&i.AggregatedFrom,
			&i.WorkspaceRxBytes,
			&i.WorkspaceTxBytes,
			&i.WorkspaceConnectionLatency50,
			&i.WorkspaceConnectionLatency95,
			&i.AgentID_2,
			&i.SessionCountVSCode,
			&i.SessionCountSSH,
			&i.SessionCountJetBrains,
			&i.SessionCountReconnectingPTY,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAgentUsageStatsAndLabels = `-- name: GetWorkspaceAgentUsageStatsAndLabels :many
WITH agent_stats AS (
	SELECT
		user_id,
		agent_id,
		workspace_id,
		coalesce(SUM(rx_bytes), 0)::bigint AS rx_bytes,
		coalesce(SUM(tx_bytes), 0)::bigint AS tx_bytes,
		coalesce(MAX(connection_median_latency_ms), 0)::float AS connection_median_latency_ms
	FROM workspace_agent_stats
	-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
	WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
	GROUP BY user_id, agent_id, workspace_id
), latest_agent_stats AS (
	SELECT
		agent_id,
		coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
		coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
		coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
		coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty,
		coalesce(SUM(connection_count), 0)::bigint AS connection_count
	FROM workspace_agent_stats
	-- We only want the latest stats, but those stats might be
	-- spread across multiple rows.
	WHERE usage = true AND created_at > now() - '1 minute'::interval
	GROUP BY user_id, agent_id, workspace_id
)
SELECT
	users.username, workspace_agents.name AS agent_name, workspaces.name AS workspace_name, rx_bytes, tx_bytes,
	coalesce(session_count_vscode, 0)::bigint AS session_count_vscode,
	coalesce(session_count_ssh, 0)::bigint AS session_count_ssh,
	coalesce(session_count_jetbrains, 0)::bigint AS session_count_jetbrains,
	coalesce(session_count_reconnecting_pty, 0)::bigint AS session_count_reconnecting_pty,
	coalesce(connection_count, 0)::bigint AS connection_count,
	connection_median_latency_ms
FROM
	agent_stats
LEFT JOIN
	latest_agent_stats
ON
	agent_stats.agent_id = latest_agent_stats.agent_id
JOIN
	users
ON
	users.id = agent_stats.user_id
JOIN
	workspace_agents
ON
	workspace_agents.id = agent_stats.agent_id
JOIN
	workspaces
ON
	workspaces.id = agent_stats.workspace_id
`

type GetWorkspaceAgentUsageStatsAndLabelsRow struct {
	Username                    string  `db:"username" json:"username"`
	AgentName                   string  `db:"agent_name" json:"agent_name"`
	WorkspaceName               string  `db:"workspace_name" json:"workspace_name"`
	RxBytes                     int64   `db:"rx_bytes" json:"rx_bytes"`
	TxBytes                     int64   `db:"tx_bytes" json:"tx_bytes"`
	SessionCountVSCode          int64   `db:"session_count_vscode" json:"session_count_vscode"`
	SessionCountSSH             int64   `db:"session_count_ssh" json:"session_count_ssh"`
	SessionCountJetBrains       int64   `db:"session_count_jetbrains" json:"session_count_jetbrains"`
	SessionCountReconnectingPTY int64   `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
	ConnectionCount             int64   `db:"connection_count" json:"connection_count"`
	ConnectionMedianLatencyMS   float64 `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
}

func (q *sqlQuerier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, createdAt time.Time) ([]GetWorkspaceAgentUsageStatsAndLabelsRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentUsageStatsAndLabels, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceAgentUsageStatsAndLabelsRow
	for rows.Next() {
		var i GetWorkspaceAgentUsageStatsAndLabelsRow
		if err := rows.Scan(
			&i.Username,
			&i.AgentName,
			&i.WorkspaceName,
			&i.RxBytes,
			&i.TxBytes,
			&i.SessionCountVSCode,
			&i.SessionCountSSH,
			&i.SessionCountJetBrains,
			&i.SessionCountReconnectingPTY,
			&i.ConnectionCount,
			&i.ConnectionMedianLatencyMS,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceAgentStats = `-- name: InsertWorkspaceAgentStats :exec
INSERT INTO
	workspace_agent_stats (
		id,
		created_at,
		user_id,
		workspace_id,
		template_id,
		agent_id,
		connections_by_proto,
		connection_count,
		rx_packets,
		rx_bytes,
		tx_packets,
		tx_bytes,
		session_count_vscode,
		session_count_jetbrains,
		session_count_reconnecting_pty,
		session_count_ssh,
		connection_median_latency_ms,
		usage
	)
SELECT
	unnest($1 :: uuid[]) AS id,
	unnest($2 :: timestamptz[]) AS created_at,
	unnest($3 :: uuid[]) AS user_id,
	unnest($4 :: uuid[]) AS workspace_id,
	unnest($5 :: uuid[]) AS template_id,
	unnest($6 :: uuid[]) AS agent_id,
	jsonb_array_elements($7 :: jsonb) AS connections_by_proto,
	unnest($8 :: bigint[]) AS connection_count,
	unnest($9 :: bigint[]) AS rx_packets,
	unnest($10 :: bigint[]) AS rx_bytes,
	unnest($11 :: bigint[]) AS tx_packets,
	unnest($12 :: bigint[]) AS tx_bytes,
	unnest($13 :: bigint[]) AS session_count_vscode,
	unnest($14 :: bigint[]) AS session_count_jetbrains,
	unnest($15 :: bigint[]) AS session_count_reconnecting_pty,
	unnest($16 :: bigint[]) AS session_count_ssh,
	unnest($17 :: double precision[]) AS connection_median_latency_ms,
	unnest($18 :: boolean[]) AS usage
`

type InsertWorkspaceAgentStatsParams struct {
	ID                          []uuid.UUID     `db:"id" json:"id"`
	CreatedAt                   []time.Time     `db:"created_at" json:"created_at"`
	UserID                      []uuid.UUID     `db:"user_id" json:"user_id"`
	WorkspaceID                 []uuid.UUID     `db:"workspace_id" json:"workspace_id"`
	TemplateID                  []uuid.UUID     `db:"template_id" json:"template_id"`
	AgentID                     []uuid.UUID     `db:"agent_id" json:"agent_id"`
	ConnectionsByProto          json.RawMessage `db:"connections_by_proto" json:"connections_by_proto"`
	ConnectionCount             []int64         `db:"connection_count" json:"connection_count"`
	RxPackets                   []int64         `db:"rx_packets" json:"rx_packets"`
	RxBytes                     []int64         `db:"rx_bytes" json:"rx_bytes"`
	TxPackets                   []int64         `db:"tx_packets" json:"tx_packets"`
	TxBytes                     []int64         `db:"tx_bytes" json:"tx_bytes"`
	SessionCountVSCode          []int64         `db:"session_count_vscode" json:"session_count_vscode"`
	SessionCountJetBrains       []int64         `db:"session_count_jetbrains" json:"session_count_jetbrains"`
	SessionCountReconnectingPTY []int64         `db:"session_count_reconnecting_pty" json:"session_count_reconnecting_pty"`
	SessionCountSSH             []int64         `db:"session_count_ssh" json:"session_count_ssh"`
	ConnectionMedianLatencyMS   []float64       `db:"connection_median_latency_ms" json:"connection_median_latency_ms"`
	Usage                       []bool          `db:"usage" json:"usage"`
}

func (q *sqlQuerier) InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error {
	_, err := q.db.ExecContext(ctx, insertWorkspaceAgentStats,
		pq.Array(arg.ID),
		pq.Array(arg.CreatedAt),
		pq.Array(arg.UserID),
		pq.Array(arg.WorkspaceID),
		pq.Array(arg.TemplateID),
		pq.Array(arg.AgentID),
		arg.ConnectionsByProto,
		pq.Array(arg.ConnectionCount),
		pq.Array(arg.RxPackets),
		pq.Array(arg.RxBytes),
		pq.Array(arg.TxPackets),
		pq.Array(arg.TxBytes),
		pq.Array(arg.SessionCountVSCode),
		pq.Array(arg.SessionCountJetBrains),
		pq.Array(arg.SessionCountReconnectingPTY),
		pq.Array(arg.SessionCountSSH),
		pq.Array(arg.ConnectionMedianLatencyMS),
		pq.Array(arg.Usage),
	)
	return err
}

const upsertWorkspaceAppAuditSession = `-- name: UpsertWorkspaceAppAuditSession :one
INSERT INTO
	workspace_app_audit_sessions (
		id,
		agent_id,
		app_id,
		user_id,
		ip,
		user_agent,
		slug_or_port,
		status_code,
		started_at,
		updated_at
	)
VALUES
	(
		$1,
		$2,
		$3,
		$4,
		$5,
		$6,
		$7,
		$8,
		$9,
		$10
	)
ON CONFLICT
	(agent_id, app_id, user_id, ip, user_agent, slug_or_port, status_code)
DO
	UPDATE
	SET
		-- ID is used to know if session was reset on upsert.
		id = CASE
			WHEN workspace_app_audit_sessions.updated_at > NOW() - ($11::bigint || ' ms')::interval
			THEN workspace_app_audit_sessions.id
			ELSE EXCLUDED.id
		END,
		started_at = CASE
			WHEN workspace_app_audit_sessions.updated_at > NOW() - ($11::bigint || ' ms')::interval
			THEN workspace_app_audit_sessions.started_at
			ELSE EXCLUDED.started_at
		END,
		updated_at = EXCLUDED.updated_at
RETURNING
	id = $1 AS new_or_stale
`

type UpsertWorkspaceAppAuditSessionParams struct {
	ID              uuid.UUID `db:"id" json:"id"`
	AgentID         uuid.UUID `db:"agent_id" json:"agent_id"`
	AppID           uuid.UUID `db:"app_id" json:"app_id"`
	UserID          uuid.UUID `db:"user_id" json:"user_id"`
	Ip              string    `db:"ip" json:"ip"`
	UserAgent       string    `db:"user_agent" json:"user_agent"`
	SlugOrPort      string    `db:"slug_or_port" json:"slug_or_port"`
	StatusCode      int32     `db:"status_code" json:"status_code"`
	StartedAt       time.Time `db:"started_at" json:"started_at"`
	UpdatedAt       time.Time `db:"updated_at" json:"updated_at"`
	StaleIntervalMS int64     `db:"stale_interval_ms" json:"stale_interval_ms"`
}

// The returned boolean, new_or_stale, can be used to deduce if a new session
// was started. This means that a new row was inserted (no previous session) or
// the updated_at is older than stale interval.
func (q *sqlQuerier) UpsertWorkspaceAppAuditSession(ctx context.Context, arg UpsertWorkspaceAppAuditSessionParams) (bool, error) {
	row := q.db.QueryRowContext(ctx, upsertWorkspaceAppAuditSession,
		arg.ID,
		arg.AgentID,
		arg.AppID,
		arg.UserID,
		arg.Ip,
		arg.UserAgent,
		arg.SlugOrPort,
		arg.StatusCode,
		arg.StartedAt,
		arg.UpdatedAt,
		arg.StaleIntervalMS,
	)
	var new_or_stale bool
	err := row.Scan(&new_or_stale)
	return new_or_stale, err
}

const getLatestWorkspaceAppStatusByAppID = `-- name: GetLatestWorkspaceAppStatusByAppID :one
SELECT id, created_at, agent_id, app_id, workspace_id, state, message, uri
FROM workspace_app_statuses
WHERE app_id = $1::uuid
ORDER BY created_at DESC, id DESC
LIMIT 1
`

func (q *sqlQuerier) GetLatestWorkspaceAppStatusByAppID(ctx context.Context, appID uuid.UUID) (WorkspaceAppStatus, error) {
	row := q.db.QueryRowContext(ctx, getLatestWorkspaceAppStatusByAppID, appID)
	var i WorkspaceAppStatus
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.AgentID,
		&i.AppID,
		&i.WorkspaceID,
		&i.State,
		&i.Message,
		&i.Uri,
	)
	return i, err
}

const getLatestWorkspaceAppStatusesByWorkspaceIDs = `-- name: GetLatestWorkspaceAppStatusesByWorkspaceIDs :many
SELECT DISTINCT ON (workspace_id)
  id, created_at, agent_id, app_id, workspace_id, state, message, uri
FROM workspace_app_statuses
WHERE workspace_id = ANY($1 :: uuid[])
ORDER BY workspace_id, created_at DESC
`

func (q *sqlQuerier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) {
	rows, err := q.db.QueryContext(ctx, getLatestWorkspaceAppStatusesByWorkspaceIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAppStatus
	for rows.Next() {
		var i WorkspaceAppStatus
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.AgentID,
			&i.AppID,
			&i.WorkspaceID,
			&i.State,
			&i.Message,
			&i.Uri,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAppByAgentIDAndSlug = `-- name: GetWorkspaceAppByAgentIDAndSlug :one
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip FROM workspace_apps WHERE agent_id = $1 AND slug = $2
`

type GetWorkspaceAppByAgentIDAndSlugParams struct {
	AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
	Slug    string    `db:"slug" json:"slug"`
}

func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceAppByAgentIDAndSlug, arg.AgentID, arg.Slug)
	var i WorkspaceApp
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.AgentID,
		&i.DisplayName,
		&i.Icon,
		&i.Command,
		&i.Url,
		&i.HealthcheckUrl,
		&i.HealthcheckInterval,
		&i.HealthcheckThreshold,
		&i.Health,
		&i.Subdomain,
		&i.SharingLevel,
		&i.Slug,
		&i.External,
		&i.DisplayOrder,
		&i.Hidden,
		&i.OpenIn,
		&i.DisplayGroup,
		&i.Tooltip,
	)
	return i, err
}

const getWorkspaceAppStatusesByAppIDs = `-- name: GetWorkspaceAppStatusesByAppIDs :many
SELECT id, created_at, agent_id, app_id, workspace_id, state, message, uri FROM workspace_app_statuses WHERE app_id = ANY($1 :: uuid [ ])
ORDER BY created_at DESC, id DESC
`

func (q *sqlQuerier) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAppStatusesByAppIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAppStatus
	for rows.Next() {
		var i WorkspaceAppStatus
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.AgentID,
			&i.AppID,
			&i.WorkspaceID,
			&i.State,
			&i.Message,
			&i.Uri,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip FROM workspace_apps WHERE agent_id = $1 ORDER BY slug ASC
`

func (q *sqlQuerier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAppsByAgentID, agentID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceApp
	for rows.Next() {
		var i WorkspaceApp
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.AgentID,
			&i.DisplayName,
			&i.Icon,
			&i.Command,
			&i.Url,
			&i.HealthcheckUrl,
			&i.HealthcheckInterval,
			&i.HealthcheckThreshold,
			&i.Health,
			&i.Subdomain,
			&i.SharingLevel,
			&i.Slug,
			&i.External,
			&i.DisplayOrder,
			&i.Hidden,
			&i.OpenIn,
			&i.DisplayGroup,
			&i.Tooltip,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAppsByAgentIDs = `-- name: GetWorkspaceAppsByAgentIDs :many
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip FROM workspace_apps WHERE agent_id = ANY($1 :: uuid [ ]) ORDER BY slug ASC
`

func (q *sqlQuerier) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAppsByAgentIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceApp
	for rows.Next() {
		var i WorkspaceApp
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.AgentID,
			&i.DisplayName,
			&i.Icon,
			&i.Command,
			&i.Url,
			&i.HealthcheckUrl,
			&i.HealthcheckInterval,
			&i.HealthcheckThreshold,
			&i.Health,
			&i.Subdomain,
			&i.SharingLevel,
			&i.Slug,
			&i.External,
			&i.DisplayOrder,
			&i.Hidden,
			&i.OpenIn,
			&i.DisplayGroup,
			&i.Tooltip,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceAppsCreatedAfter = `-- name: GetWorkspaceAppsCreatedAfter :many
SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip FROM workspace_apps WHERE created_at > $1 ORDER BY slug ASC
`

func (q *sqlQuerier) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAppsCreatedAfter, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceApp
	for rows.Next() {
		var i WorkspaceApp
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.AgentID,
			&i.DisplayName,
			&i.Icon,
			&i.Command,
			&i.Url,
			&i.HealthcheckUrl,
			&i.HealthcheckInterval,
			&i.HealthcheckThreshold,
			&i.Health,
			&i.Subdomain,
			&i.SharingLevel,
			&i.Slug,
			&i.External,
			&i.DisplayOrder,
			&i.Hidden,
			&i.OpenIn,
			&i.DisplayGroup,
			&i.Tooltip,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceAppStatus = `-- name: InsertWorkspaceAppStatus :one
INSERT INTO workspace_app_statuses (id, created_at, workspace_id, agent_id, app_id, state, message, uri)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, agent_id, app_id, workspace_id, state, message, uri
`

type InsertWorkspaceAppStatusParams struct {
	ID          uuid.UUID               `db:"id" json:"id"`
	CreatedAt   time.Time               `db:"created_at" json:"created_at"`
	WorkspaceID uuid.UUID               `db:"workspace_id" json:"workspace_id"`
	AgentID     uuid.UUID               `db:"agent_id" json:"agent_id"`
	AppID       uuid.UUID               `db:"app_id" json:"app_id"`
	State       WorkspaceAppStatusState `db:"state" json:"state"`
	Message     string                  `db:"message" json:"message"`
	Uri         sql.NullString          `db:"uri" json:"uri"`
}

func (q *sqlQuerier) InsertWorkspaceAppStatus(ctx context.Context, arg InsertWorkspaceAppStatusParams) (WorkspaceAppStatus, error) {
	row := q.db.QueryRowContext(ctx, insertWorkspaceAppStatus,
		arg.ID,
		arg.CreatedAt,
		arg.WorkspaceID,
		arg.AgentID,
		arg.AppID,
		arg.State,
		arg.Message,
		arg.Uri,
	)
	var i WorkspaceAppStatus
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.AgentID,
		&i.AppID,
		&i.WorkspaceID,
		&i.State,
		&i.Message,
		&i.Uri,
	)
	return i, err
}

const updateWorkspaceAppHealthByID = `-- name: UpdateWorkspaceAppHealthByID :exec
UPDATE
	workspace_apps
SET
	health = $2
WHERE
	id = $1
`

type UpdateWorkspaceAppHealthByIDParams struct {
	ID     uuid.UUID          `db:"id" json:"id"`
	Health WorkspaceAppHealth `db:"health" json:"health"`
}

func (q *sqlQuerier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAppHealthByID, arg.ID, arg.Health)
	return err
}

const upsertWorkspaceApp = `-- name: UpsertWorkspaceApp :one
INSERT INTO
    workspace_apps (
        id,
        created_at,
        agent_id,
        slug,
        display_name,
        icon,
        command,
        url,
        external,
        subdomain,
        sharing_level,
        healthcheck_url,
        healthcheck_interval,
        healthcheck_threshold,
        health,
        display_order,
        hidden,
        open_in,
        display_group,
        tooltip
    )
VALUES
    ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
ON CONFLICT (id) DO UPDATE SET
    display_name = EXCLUDED.display_name,
    icon = EXCLUDED.icon,
    command = EXCLUDED.command,
    url = EXCLUDED.url,
    external = EXCLUDED.external,
    subdomain = EXCLUDED.subdomain,
    sharing_level = EXCLUDED.sharing_level,
    healthcheck_url = EXCLUDED.healthcheck_url,
    healthcheck_interval = EXCLUDED.healthcheck_interval,
    healthcheck_threshold = EXCLUDED.healthcheck_threshold,
    health = EXCLUDED.health,
    display_order = EXCLUDED.display_order,
    hidden = EXCLUDED.hidden,
    open_in = EXCLUDED.open_in,
    display_group = EXCLUDED.display_group,
    agent_id = EXCLUDED.agent_id,
    slug = EXCLUDED.slug,
    tooltip = EXCLUDED.tooltip
RETURNING id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in, display_group, tooltip
`

type UpsertWorkspaceAppParams struct {
	ID                   uuid.UUID          `db:"id" json:"id"`
	CreatedAt            time.Time          `db:"created_at" json:"created_at"`
	AgentID              uuid.UUID          `db:"agent_id" json:"agent_id"`
	Slug                 string             `db:"slug" json:"slug"`
	DisplayName          string             `db:"display_name" json:"display_name"`
	Icon                 string             `db:"icon" json:"icon"`
	Command              sql.NullString     `db:"command" json:"command"`
	Url                  sql.NullString     `db:"url" json:"url"`
	External             bool               `db:"external" json:"external"`
	Subdomain            bool               `db:"subdomain" json:"subdomain"`
	SharingLevel         AppSharingLevel    `db:"sharing_level" json:"sharing_level"`
	HealthcheckUrl       string             `db:"healthcheck_url" json:"healthcheck_url"`
	HealthcheckInterval  int32              `db:"healthcheck_interval" json:"healthcheck_interval"`
	HealthcheckThreshold int32              `db:"healthcheck_threshold" json:"healthcheck_threshold"`
	Health               WorkspaceAppHealth `db:"health" json:"health"`
	DisplayOrder         int32              `db:"display_order" json:"display_order"`
	Hidden               bool               `db:"hidden" json:"hidden"`
	OpenIn               WorkspaceAppOpenIn `db:"open_in" json:"open_in"`
	DisplayGroup         sql.NullString     `db:"display_group" json:"display_group"`
	Tooltip              string             `db:"tooltip" json:"tooltip"`
}

func (q *sqlQuerier) UpsertWorkspaceApp(ctx context.Context, arg UpsertWorkspaceAppParams) (WorkspaceApp, error) {
	row := q.db.QueryRowContext(ctx, upsertWorkspaceApp,
		arg.ID,
		arg.CreatedAt,
		arg.AgentID,
		arg.Slug,
		arg.DisplayName,
		arg.Icon,
		arg.Command,
		arg.Url,
		arg.External,
		arg.Subdomain,
		arg.SharingLevel,
		arg.HealthcheckUrl,
		arg.HealthcheckInterval,
		arg.HealthcheckThreshold,
		arg.Health,
		arg.DisplayOrder,
		arg.Hidden,
		arg.OpenIn,
		arg.DisplayGroup,
		arg.Tooltip,
	)
	var i WorkspaceApp
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.AgentID,
		&i.DisplayName,
		&i.Icon,
		&i.Command,
		&i.Url,
		&i.HealthcheckUrl,
		&i.HealthcheckInterval,
		&i.HealthcheckThreshold,
		&i.Health,
		&i.Subdomain,
		&i.SharingLevel,
		&i.Slug,
		&i.External,
		&i.DisplayOrder,
		&i.Hidden,
		&i.OpenIn,
		&i.DisplayGroup,
		&i.Tooltip,
	)
	return i, err
}

const insertWorkspaceAppStats = `-- name: InsertWorkspaceAppStats :exec
INSERT INTO
	workspace_app_stats (
		user_id,
		workspace_id,
		agent_id,
		access_method,
		slug_or_port,
		session_id,
		session_started_at,
		session_ended_at,
		requests
	)
SELECT
	unnest($1::uuid[]) AS user_id,
	unnest($2::uuid[]) AS workspace_id,
	unnest($3::uuid[]) AS agent_id,
	unnest($4::text[]) AS access_method,
	unnest($5::text[]) AS slug_or_port,
	unnest($6::uuid[]) AS session_id,
	unnest($7::timestamptz[]) AS session_started_at,
	unnest($8::timestamptz[]) AS session_ended_at,
	unnest($9::int[]) AS requests
ON CONFLICT
	(user_id, agent_id, session_id)
DO
	UPDATE SET
		session_ended_at = EXCLUDED.session_ended_at,
		requests = EXCLUDED.requests
	WHERE
		workspace_app_stats.user_id = EXCLUDED.user_id
		AND workspace_app_stats.agent_id = EXCLUDED.agent_id
		AND workspace_app_stats.session_id = EXCLUDED.session_id
		-- Since stats are updated in place as time progresses, we only
		-- want to update this row if it's fresh.
		AND workspace_app_stats.session_ended_at <= EXCLUDED.session_ended_at
		AND workspace_app_stats.requests <= EXCLUDED.requests
`

type InsertWorkspaceAppStatsParams struct {
	UserID           []uuid.UUID `db:"user_id" json:"user_id"`
	WorkspaceID      []uuid.UUID `db:"workspace_id" json:"workspace_id"`
	AgentID          []uuid.UUID `db:"agent_id" json:"agent_id"`
	AccessMethod     []string    `db:"access_method" json:"access_method"`
	SlugOrPort       []string    `db:"slug_or_port" json:"slug_or_port"`
	SessionID        []uuid.UUID `db:"session_id" json:"session_id"`
	SessionStartedAt []time.Time `db:"session_started_at" json:"session_started_at"`
	SessionEndedAt   []time.Time `db:"session_ended_at" json:"session_ended_at"`
	Requests         []int32     `db:"requests" json:"requests"`
}

func (q *sqlQuerier) InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error {
	_, err := q.db.ExecContext(ctx, insertWorkspaceAppStats,
		pq.Array(arg.UserID),
		pq.Array(arg.WorkspaceID),
		pq.Array(arg.AgentID),
		pq.Array(arg.AccessMethod),
		pq.Array(arg.SlugOrPort),
		pq.Array(arg.SessionID),
		pq.Array(arg.SessionStartedAt),
		pq.Array(arg.SessionEndedAt),
		pq.Array(arg.Requests),
	)
	return err
}

const getUserWorkspaceBuildParameters = `-- name: GetUserWorkspaceBuildParameters :many
SELECT name, value
FROM (
    SELECT DISTINCT ON (tvp.name)
        tvp.name,
        wbp.value,
        wb.created_at
    FROM
        workspace_build_parameters wbp
    JOIN
        workspace_builds wb ON wb.id = wbp.workspace_build_id
    JOIN
        workspaces w ON w.id = wb.workspace_id
    JOIN
        template_version_parameters tvp ON tvp.template_version_id = wb.template_version_id
    WHERE
		w.owner_id = $1
		AND wb.transition = 'start'
		AND w.template_id = $2
		AND tvp.ephemeral = false
		AND tvp.name = wbp.name
    ORDER BY
        tvp.name, wb.created_at DESC
) q1
ORDER BY created_at DESC, name
LIMIT 100
`

type GetUserWorkspaceBuildParametersParams struct {
	OwnerID    uuid.UUID `db:"owner_id" json:"owner_id"`
	TemplateID uuid.UUID `db:"template_id" json:"template_id"`
}

type GetUserWorkspaceBuildParametersRow struct {
	Name  string `db:"name" json:"name"`
	Value string `db:"value" json:"value"`
}

func (q *sqlQuerier) GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) {
	rows, err := q.db.QueryContext(ctx, getUserWorkspaceBuildParameters, arg.OwnerID, arg.TemplateID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetUserWorkspaceBuildParametersRow
	for rows.Next() {
		var i GetUserWorkspaceBuildParametersRow
		if err := rows.Scan(&i.Name, &i.Value); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceBuildParameters = `-- name: GetWorkspaceBuildParameters :many
SELECT
    workspace_build_id, name, value
FROM
    workspace_build_parameters
WHERE
    workspace_build_id = $1
`

func (q *sqlQuerier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceBuildParameters, workspaceBuildID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceBuildParameter
	for rows.Next() {
		var i WorkspaceBuildParameter
		if err := rows.Scan(&i.WorkspaceBuildID, &i.Name, &i.Value); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceBuildParameters = `-- name: InsertWorkspaceBuildParameters :exec
INSERT INTO
    workspace_build_parameters (workspace_build_id, name, value)
SELECT
    $1 :: uuid AS workspace_build_id,
    unnest($2 :: text[]) AS name,
    unnest($3 :: text[]) AS value
RETURNING workspace_build_id, name, value
`

type InsertWorkspaceBuildParametersParams struct {
	WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"`
	Name             []string  `db:"name" json:"name"`
	Value            []string  `db:"value" json:"value"`
}

func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error {
	_, err := q.db.ExecContext(ctx, insertWorkspaceBuildParameters, arg.WorkspaceBuildID, pq.Array(arg.Name), pq.Array(arg.Value))
	return err
}

const getActiveWorkspaceBuildsByTemplateID = `-- name: GetActiveWorkspaceBuildsByTemplateID :many
SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.has_external_agent, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name
FROM (
    SELECT
        workspace_id, MAX(build_number) as max_build_number
    FROM
		workspace_build_with_user AS workspace_builds
    WHERE
        workspace_id IN (
			SELECT
				id
			FROM
				workspaces
			WHERE
				template_id = $1
		)
    GROUP BY
        workspace_id
) m
JOIN
	workspace_build_with_user AS wb
	ON m.workspace_id = wb.workspace_id AND m.max_build_number = wb.build_number
JOIN
	provisioner_jobs AS pj
	ON wb.job_id = pj.id
WHERE
	wb.transition = 'start'::workspace_transition
AND
	pj.completed_at IS NOT NULL
`

func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) {
	rows, err := q.db.QueryContext(ctx, getActiveWorkspaceBuildsByTemplateID, templateID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceBuild
	for rows.Next() {
		var i WorkspaceBuild
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.WorkspaceID,
			&i.TemplateVersionID,
			&i.BuildNumber,
			&i.Transition,
			&i.InitiatorID,
			&i.JobID,
			&i.Deadline,
			&i.Reason,
			&i.DailyCost,
			&i.MaxDeadline,
			&i.TemplateVersionPresetID,
			&i.HasAITask,
			&i.HasExternalAgent,
			&i.InitiatorByAvatarUrl,
			&i.InitiatorByUsername,
			&i.InitiatorByName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getFailedWorkspaceBuildsByTemplateID = `-- name: GetFailedWorkspaceBuildsByTemplateID :many
SELECT
	tv.name AS template_version_name,
	u.username AS workspace_owner_username,
	w.name AS workspace_name,
	w.id AS workspace_id,
	wb.build_number AS workspace_build_number
FROM
	workspace_build_with_user AS wb
JOIN
	workspaces AS w
ON
	wb.workspace_id = w.id
JOIN
    users AS u
ON
    w.owner_id = u.id
JOIN
	provisioner_jobs AS pj
ON
	wb.job_id = pj.id
JOIN
	templates AS t
ON
	w.template_id = t.id
JOIN
	template_versions AS tv
ON
	wb.template_version_id = tv.id
WHERE
	w.template_id = $1
	AND wb.created_at >= $2
	AND pj.completed_at IS NOT NULL
	AND pj.job_status = 'failed'
ORDER BY
	tv.name ASC, wb.build_number DESC
`

type GetFailedWorkspaceBuildsByTemplateIDParams struct {
	TemplateID uuid.UUID `db:"template_id" json:"template_id"`
	Since      time.Time `db:"since" json:"since"`
}

type GetFailedWorkspaceBuildsByTemplateIDRow struct {
	TemplateVersionName    string    `db:"template_version_name" json:"template_version_name"`
	WorkspaceOwnerUsername string    `db:"workspace_owner_username" json:"workspace_owner_username"`
	WorkspaceName          string    `db:"workspace_name" json:"workspace_name"`
	WorkspaceID            uuid.UUID `db:"workspace_id" json:"workspace_id"`
	WorkspaceBuildNumber   int32     `db:"workspace_build_number" json:"workspace_build_number"`
}

func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) {
	rows, err := q.db.QueryContext(ctx, getFailedWorkspaceBuildsByTemplateID, arg.TemplateID, arg.Since)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetFailedWorkspaceBuildsByTemplateIDRow
	for rows.Next() {
		var i GetFailedWorkspaceBuildsByTemplateIDRow
		if err := rows.Scan(
			&i.TemplateVersionName,
			&i.WorkspaceOwnerUsername,
			&i.WorkspaceName,
			&i.WorkspaceID,
			&i.WorkspaceBuildNumber,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
SELECT
	id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
	workspace_build_with_user AS workspace_builds
WHERE
	workspace_id = $1
ORDER BY
    build_number desc
LIMIT
	1
`

func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) {
	row := q.db.QueryRowContext(ctx, getLatestWorkspaceBuildByWorkspaceID, workspaceID)
	var i WorkspaceBuild
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.BuildNumber,
		&i.Transition,
		&i.InitiatorID,
		&i.JobID,
		&i.Deadline,
		&i.Reason,
		&i.DailyCost,
		&i.MaxDeadline,
		&i.TemplateVersionPresetID,
		&i.HasAITask,
		&i.HasExternalAgent,
		&i.InitiatorByAvatarUrl,
		&i.InitiatorByUsername,
		&i.InitiatorByName,
	)
	return i, err
}

const getLatestWorkspaceBuildWithStatusByWorkspaceID = `-- name: GetLatestWorkspaceBuildWithStatusByWorkspaceID :one
SELECT
	workspace_builds.transition, workspace_builds.build_number, provisioner_jobs.job_status,
	workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl -- Used for dbauthz fetch() checks
FROM
	workspace_builds
INNER JOIN
	provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id
INNER JOIN
	workspaces ON workspace_builds.workspace_id = workspaces.id
WHERE
	workspace_builds.workspace_id = $1 AND
	workspaces.deleted = false
ORDER BY
	workspace_builds.build_number desc
	LIMIT
	1
`

type GetLatestWorkspaceBuildWithStatusByWorkspaceIDRow struct {
	Transition     WorkspaceTransition  `db:"transition" json:"transition"`
	BuildNumber    int32                `db:"build_number" json:"build_number"`
	JobStatus      ProvisionerJobStatus `db:"job_status" json:"job_status"`
	WorkspaceTable WorkspaceTable       `db:"workspace_table" json:"workspace_table"`
}

func (q *sqlQuerier) GetLatestWorkspaceBuildWithStatusByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (GetLatestWorkspaceBuildWithStatusByWorkspaceIDRow, error) {
	row := q.db.QueryRowContext(ctx, getLatestWorkspaceBuildWithStatusByWorkspaceID, workspaceID)
	var i GetLatestWorkspaceBuildWithStatusByWorkspaceIDRow
	err := row.Scan(
		&i.Transition,
		&i.BuildNumber,
		&i.JobStatus,
		&i.WorkspaceTable.ID,
		&i.WorkspaceTable.CreatedAt,
		&i.WorkspaceTable.UpdatedAt,
		&i.WorkspaceTable.OwnerID,
		&i.WorkspaceTable.OrganizationID,
		&i.WorkspaceTable.TemplateID,
		&i.WorkspaceTable.Deleted,
		&i.WorkspaceTable.Name,
		&i.WorkspaceTable.AutostartSchedule,
		&i.WorkspaceTable.Ttl,
		&i.WorkspaceTable.LastUsedAt,
		&i.WorkspaceTable.DormantAt,
		&i.WorkspaceTable.DeletingAt,
		&i.WorkspaceTable.AutomaticUpdates,
		&i.WorkspaceTable.Favorite,
		&i.WorkspaceTable.NextStartAt,
		&i.WorkspaceTable.GroupACL,
		&i.WorkspaceTable.UserACL,
	)
	return i, err
}

const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many
SELECT
	DISTINCT ON (workspace_id)
	id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
	workspace_build_with_user AS workspace_builds
WHERE
	workspace_id = ANY($1 :: uuid [ ])
ORDER BY
	workspace_id, build_number DESC -- latest first
`

func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) {
	rows, err := q.db.QueryContext(ctx, getLatestWorkspaceBuildsByWorkspaceIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceBuild
	for rows.Next() {
		var i WorkspaceBuild
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.WorkspaceID,
			&i.TemplateVersionID,
			&i.BuildNumber,
			&i.Transition,
			&i.InitiatorID,
			&i.JobID,
			&i.Deadline,
			&i.Reason,
			&i.DailyCost,
			&i.MaxDeadline,
			&i.TemplateVersionPresetID,
			&i.HasAITask,
			&i.HasExternalAgent,
			&i.InitiatorByAvatarUrl,
			&i.InitiatorByUsername,
			&i.InitiatorByName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one
SELECT
	id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
	workspace_build_with_user AS workspace_builds
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceBuildByID, id)
	var i WorkspaceBuild
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.BuildNumber,
		&i.Transition,
		&i.InitiatorID,
		&i.JobID,
		&i.Deadline,
		&i.Reason,
		&i.DailyCost,
		&i.MaxDeadline,
		&i.TemplateVersionPresetID,
		&i.HasAITask,
		&i.HasExternalAgent,
		&i.InitiatorByAvatarUrl,
		&i.InitiatorByUsername,
		&i.InitiatorByName,
	)
	return i, err
}

const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one
SELECT
	id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
	workspace_build_with_user AS workspace_builds
WHERE
	job_id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceBuildByJobID, jobID)
	var i WorkspaceBuild
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.BuildNumber,
		&i.Transition,
		&i.InitiatorID,
		&i.JobID,
		&i.Deadline,
		&i.Reason,
		&i.DailyCost,
		&i.MaxDeadline,
		&i.TemplateVersionPresetID,
		&i.HasAITask,
		&i.HasExternalAgent,
		&i.InitiatorByAvatarUrl,
		&i.InitiatorByUsername,
		&i.InitiatorByName,
	)
	return i, err
}

const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one
SELECT
	id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
	workspace_build_with_user AS workspace_builds
WHERE
	workspace_id = $1
	AND build_number = $2
`

type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct {
	WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
	BuildNumber int32     `db:"build_number" json:"build_number"`
}

func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceBuildByWorkspaceIDAndBuildNumber, arg.WorkspaceID, arg.BuildNumber)
	var i WorkspaceBuild
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.WorkspaceID,
		&i.TemplateVersionID,
		&i.BuildNumber,
		&i.Transition,
		&i.InitiatorID,
		&i.JobID,
		&i.Deadline,
		&i.Reason,
		&i.DailyCost,
		&i.MaxDeadline,
		&i.TemplateVersionPresetID,
		&i.HasAITask,
		&i.HasExternalAgent,
		&i.InitiatorByAvatarUrl,
		&i.InitiatorByUsername,
		&i.InitiatorByName,
	)
	return i, err
}

const getWorkspaceBuildMetricsByResourceID = `-- name: GetWorkspaceBuildMetricsByResourceID :one
SELECT
    wb.created_at,
    wb.transition,
    t.name AS template_name,
    o.name AS organization_name,
    (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0') AS is_prebuild,
    -- All agents must have ready_at set (terminal startup state)
    COUNT(*) FILTER (WHERE wa.ready_at IS NULL) = 0 AS all_agents_ready,
    -- Latest ready_at across all agents (for duration calculation)
	MAX(wa.ready_at)::timestamptz AS last_agent_ready_at,
    -- Worst status: error > timeout > ready
    CASE
        WHEN bool_or(wa.lifecycle_state = 'start_error') THEN 'error'
        WHEN bool_or(wa.lifecycle_state = 'start_timeout') THEN 'timeout'
        ELSE 'success'
    END AS worst_status
FROM workspace_builds wb
JOIN workspaces w ON wb.workspace_id = w.id
JOIN templates t ON w.template_id = t.id
JOIN organizations o ON t.organization_id = o.id
JOIN workspace_resources wr ON wr.job_id = wb.job_id
JOIN workspace_agents wa ON wa.resource_id = wr.id AND wa.parent_id IS NULL
WHERE wb.job_id = (SELECT job_id FROM workspace_resources WHERE workspace_resources.id = $1)
GROUP BY wb.created_at, wb.transition, t.name, o.name, w.owner_id
`

type GetWorkspaceBuildMetricsByResourceIDRow struct {
	CreatedAt        time.Time           `db:"created_at" json:"created_at"`
	Transition       WorkspaceTransition `db:"transition" json:"transition"`
	TemplateName     string              `db:"template_name" json:"template_name"`
	OrganizationName string              `db:"organization_name" json:"organization_name"`
	IsPrebuild       bool                `db:"is_prebuild" json:"is_prebuild"`
	AllAgentsReady   bool                `db:"all_agents_ready" json:"all_agents_ready"`
	LastAgentReadyAt time.Time           `db:"last_agent_ready_at" json:"last_agent_ready_at"`
	WorstStatus      string              `db:"worst_status" json:"worst_status"`
}

// Returns build metadata for e2e workspace build duration metrics.
// Also checks if all agents are ready and returns the worst status.
func (q *sqlQuerier) GetWorkspaceBuildMetricsByResourceID(ctx context.Context, id uuid.UUID) (GetWorkspaceBuildMetricsByResourceIDRow, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceBuildMetricsByResourceID, id)
	var i GetWorkspaceBuildMetricsByResourceIDRow
	err := row.Scan(
		&i.CreatedAt,
		&i.Transition,
		&i.TemplateName,
		&i.OrganizationName,
		&i.IsPrebuild,
		&i.AllAgentsReady,
		&i.LastAgentReadyAt,
		&i.WorstStatus,
	)
	return i, err
}

const getWorkspaceBuildProvisionerStateByID = `-- name: GetWorkspaceBuildProvisionerStateByID :one
SELECT
	workspace_builds.provisioner_state,
	templates.id AS template_id,
	templates.organization_id AS template_organization_id,
	templates.user_acl,
	templates.group_acl
FROM
	workspace_builds
INNER JOIN
	workspaces ON workspaces.id = workspace_builds.workspace_id
INNER JOIN
	templates ON templates.id = workspaces.template_id
WHERE
	workspace_builds.id = $1
`

type GetWorkspaceBuildProvisionerStateByIDRow struct {
	ProvisionerState       []byte      `db:"provisioner_state" json:"provisioner_state"`
	TemplateID             uuid.UUID   `db:"template_id" json:"template_id"`
	TemplateOrganizationID uuid.UUID   `db:"template_organization_id" json:"template_organization_id"`
	UserACL                TemplateACL `db:"user_acl" json:"user_acl"`
	GroupACL               TemplateACL `db:"group_acl" json:"group_acl"`
}

// Fetches the provisioner state of a workspace build, joined through to the
// template so that dbauthz can enforce policy.ActionUpdate on the template.
// Provisioner state contains sensitive Terraform state and should only be
// accessible to template administrators.
func (q *sqlQuerier) GetWorkspaceBuildProvisionerStateByID(ctx context.Context, workspaceBuildID uuid.UUID) (GetWorkspaceBuildProvisionerStateByIDRow, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceBuildProvisionerStateByID, workspaceBuildID)
	var i GetWorkspaceBuildProvisionerStateByIDRow
	err := row.Scan(
		&i.ProvisionerState,
		&i.TemplateID,
		&i.TemplateOrganizationID,
		&i.UserACL,
		&i.GroupACL,
	)
	return i, err
}

const getWorkspaceBuildStatsByTemplates = `-- name: GetWorkspaceBuildStatsByTemplates :many
SELECT
    w.template_id,
	t.name AS template_name,
	t.display_name AS template_display_name,
	t.organization_id AS template_organization_id,
    COUNT(*) AS total_builds,
    COUNT(CASE WHEN pj.job_status = 'failed' THEN 1 END) AS failed_builds
FROM
    workspace_build_with_user AS wb
JOIN
    workspaces AS w ON
    wb.workspace_id = w.id
JOIN
    provisioner_jobs AS pj ON
    wb.job_id = pj.id
JOIN
    templates AS t ON
	w.template_id = t.id
WHERE
    wb.created_at >= $1
    AND pj.completed_at IS NOT NULL
GROUP BY
    w.template_id, template_name, template_display_name, template_organization_id
ORDER BY
    template_name ASC
`

type GetWorkspaceBuildStatsByTemplatesRow struct {
	TemplateID             uuid.UUID `db:"template_id" json:"template_id"`
	TemplateName           string    `db:"template_name" json:"template_name"`
	TemplateDisplayName    string    `db:"template_display_name" json:"template_display_name"`
	TemplateOrganizationID uuid.UUID `db:"template_organization_id" json:"template_organization_id"`
	TotalBuilds            int64     `db:"total_builds" json:"total_builds"`
	FailedBuilds           int64     `db:"failed_builds" json:"failed_builds"`
}

func (q *sqlQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceBuildStatsByTemplates, since)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceBuildStatsByTemplatesRow
	for rows.Next() {
		var i GetWorkspaceBuildStatsByTemplatesRow
		if err := rows.Scan(
			&i.TemplateID,
			&i.TemplateName,
			&i.TemplateDisplayName,
			&i.TemplateOrganizationID,
			&i.TotalBuilds,
			&i.FailedBuilds,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many
SELECT
	id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
	workspace_build_with_user AS workspace_builds
WHERE
	workspace_builds.workspace_id = $1
	AND workspace_builds.created_at > $2
    AND CASE
		-- This allows using the last element on a page as effectively a cursor.
		-- This is an important option for scripts that need to paginate without
		-- duplicating or missing data.
		WHEN $3 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
			-- The pagination cursor is the last ID of the previous page.
			-- The query is ordered by the build_number field, so select all
			-- rows after the cursor.
			build_number > (
				SELECT
					build_number
				FROM
					workspace_builds
				WHERE
					id = $3
			)
		)
		ELSE true
END
ORDER BY
    build_number desc OFFSET $4
LIMIT
    -- A null limit means "no limit", so 0 means return all
    NULLIF($5 :: int, 0)
`

type GetWorkspaceBuildsByWorkspaceIDParams struct {
	WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
	Since       time.Time `db:"since" json:"since"`
	AfterID     uuid.UUID `db:"after_id" json:"after_id"`
	OffsetOpt   int32     `db:"offset_opt" json:"offset_opt"`
	LimitOpt    int32     `db:"limit_opt" json:"limit_opt"`
}

func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceBuildsByWorkspaceID,
		arg.WorkspaceID,
		arg.Since,
		arg.AfterID,
		arg.OffsetOpt,
		arg.LimitOpt,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceBuild
	for rows.Next() {
		var i WorkspaceBuild
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.WorkspaceID,
			&i.TemplateVersionID,
			&i.BuildNumber,
			&i.Transition,
			&i.InitiatorID,
			&i.JobID,
			&i.Deadline,
			&i.Reason,
			&i.DailyCost,
			&i.MaxDeadline,
			&i.TemplateVersionPresetID,
			&i.HasAITask,
			&i.HasExternalAgent,
			&i.InitiatorByAvatarUrl,
			&i.InitiatorByUsername,
			&i.InitiatorByName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many
SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1
`

func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceBuildsCreatedAfter, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceBuild
	for rows.Next() {
		var i WorkspaceBuild
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.WorkspaceID,
			&i.TemplateVersionID,
			&i.BuildNumber,
			&i.Transition,
			&i.InitiatorID,
			&i.JobID,
			&i.Deadline,
			&i.Reason,
			&i.DailyCost,
			&i.MaxDeadline,
			&i.TemplateVersionPresetID,
			&i.HasAITask,
			&i.HasExternalAgent,
			&i.InitiatorByAvatarUrl,
			&i.InitiatorByUsername,
			&i.InitiatorByName,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceBuild = `-- name: InsertWorkspaceBuild :exec
INSERT INTO
	workspace_builds (
		id,
		created_at,
		updated_at,
		workspace_id,
		template_version_id,
		"build_number",
		transition,
		initiator_id,
		job_id,
		provisioner_state,
		deadline,
		max_deadline,
		reason,
		template_version_preset_id
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
`

type InsertWorkspaceBuildParams struct {
	ID                      uuid.UUID           `db:"id" json:"id"`
	CreatedAt               time.Time           `db:"created_at" json:"created_at"`
	UpdatedAt               time.Time           `db:"updated_at" json:"updated_at"`
	WorkspaceID             uuid.UUID           `db:"workspace_id" json:"workspace_id"`
	TemplateVersionID       uuid.UUID           `db:"template_version_id" json:"template_version_id"`
	BuildNumber             int32               `db:"build_number" json:"build_number"`
	Transition              WorkspaceTransition `db:"transition" json:"transition"`
	InitiatorID             uuid.UUID           `db:"initiator_id" json:"initiator_id"`
	JobID                   uuid.UUID           `db:"job_id" json:"job_id"`
	ProvisionerState        []byte              `db:"provisioner_state" json:"provisioner_state"`
	Deadline                time.Time           `db:"deadline" json:"deadline"`
	MaxDeadline             time.Time           `db:"max_deadline" json:"max_deadline"`
	Reason                  BuildReason         `db:"reason" json:"reason"`
	TemplateVersionPresetID uuid.NullUUID       `db:"template_version_preset_id" json:"template_version_preset_id"`
}

func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error {
	_, err := q.db.ExecContext(ctx, insertWorkspaceBuild,
		arg.ID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.WorkspaceID,
		arg.TemplateVersionID,
		arg.BuildNumber,
		arg.Transition,
		arg.InitiatorID,
		arg.JobID,
		arg.ProvisionerState,
		arg.Deadline,
		arg.MaxDeadline,
		arg.Reason,
		arg.TemplateVersionPresetID,
	)
	return err
}

const updateWorkspaceBuildCostByID = `-- name: UpdateWorkspaceBuildCostByID :exec
UPDATE
	workspace_builds
SET
	daily_cost = $2
WHERE
	id = $1
`

type UpdateWorkspaceBuildCostByIDParams struct {
	ID        uuid.UUID `db:"id" json:"id"`
	DailyCost int32     `db:"daily_cost" json:"daily_cost"`
}

func (q *sqlQuerier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceBuildCostByID, arg.ID, arg.DailyCost)
	return err
}

const updateWorkspaceBuildDeadlineByID = `-- name: UpdateWorkspaceBuildDeadlineByID :exec
UPDATE
	workspace_builds
SET
	deadline = $1::timestamptz,
	max_deadline = $2::timestamptz,
	updated_at = $3::timestamptz
FROM
	workspaces
WHERE
	workspace_builds.id = $4::uuid
	AND workspace_builds.workspace_id = workspaces.id
	-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
	-- are managed by the reconciliation loop, not the lifecycle executor which handles
	-- deadline and max_deadline
	AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`

type UpdateWorkspaceBuildDeadlineByIDParams struct {
	Deadline    time.Time `db:"deadline" json:"deadline"`
	MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
	UpdatedAt   time.Time `db:"updated_at" json:"updated_at"`
	ID          uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg UpdateWorkspaceBuildDeadlineByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceBuildDeadlineByID,
		arg.Deadline,
		arg.MaxDeadline,
		arg.UpdatedAt,
		arg.ID,
	)
	return err
}

const updateWorkspaceBuildFlagsByID = `-- name: UpdateWorkspaceBuildFlagsByID :exec
UPDATE
	workspace_builds
SET
	has_ai_task = $1,
	has_external_agent = $2,
	updated_at = $3::timestamptz
WHERE id = $4::uuid
`

type UpdateWorkspaceBuildFlagsByIDParams struct {
	HasAITask        sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
	HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
	UpdatedAt        time.Time    `db:"updated_at" json:"updated_at"`
	ID               uuid.UUID    `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateWorkspaceBuildFlagsByID(ctx context.Context, arg UpdateWorkspaceBuildFlagsByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceBuildFlagsByID,
		arg.HasAITask,
		arg.HasExternalAgent,
		arg.UpdatedAt,
		arg.ID,
	)
	return err
}

const updateWorkspaceBuildProvisionerStateByID = `-- name: UpdateWorkspaceBuildProvisionerStateByID :exec
UPDATE
	workspace_builds
SET
	provisioner_state = $1::bytea,
	updated_at = $2::timestamptz
WHERE id = $3::uuid
`

type UpdateWorkspaceBuildProvisionerStateByIDParams struct {
	ProvisionerState []byte    `db:"provisioner_state" json:"provisioner_state"`
	UpdatedAt        time.Time `db:"updated_at" json:"updated_at"`
	ID               uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateWorkspaceBuildProvisionerStateByID(ctx context.Context, arg UpdateWorkspaceBuildProvisionerStateByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceBuildProvisionerStateByID, arg.ProvisionerState, arg.UpdatedAt, arg.ID)
	return err
}

const getWorkspaceModulesByJobID = `-- name: GetWorkspaceModulesByJobID :many
SELECT
	id, job_id, transition, source, version, key, created_at
FROM
	workspace_modules
WHERE
	job_id = $1
`

func (q *sqlQuerier) GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceModule, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceModulesByJobID, jobID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceModule
	for rows.Next() {
		var i WorkspaceModule
		if err := rows.Scan(
			&i.ID,
			&i.JobID,
			&i.Transition,
			&i.Source,
			&i.Version,
			&i.Key,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceModulesCreatedAfter = `-- name: GetWorkspaceModulesCreatedAfter :many
SELECT id, job_id, transition, source, version, key, created_at FROM workspace_modules WHERE created_at > $1
`

func (q *sqlQuerier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceModule, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceModulesCreatedAfter, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceModule
	for rows.Next() {
		var i WorkspaceModule
		if err := rows.Scan(
			&i.ID,
			&i.JobID,
			&i.Transition,
			&i.Source,
			&i.Version,
			&i.Key,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceModule = `-- name: InsertWorkspaceModule :one
INSERT INTO
	workspace_modules (id, job_id, transition, source, version, key, created_at)
VALUES
	($1, $2, $3, $4, $5, $6, $7) RETURNING id, job_id, transition, source, version, key, created_at
`

type InsertWorkspaceModuleParams struct {
	ID         uuid.UUID           `db:"id" json:"id"`
	JobID      uuid.UUID           `db:"job_id" json:"job_id"`
	Transition WorkspaceTransition `db:"transition" json:"transition"`
	Source     string              `db:"source" json:"source"`
	Version    string              `db:"version" json:"version"`
	Key        string              `db:"key" json:"key"`
	CreatedAt  time.Time           `db:"created_at" json:"created_at"`
}

func (q *sqlQuerier) InsertWorkspaceModule(ctx context.Context, arg InsertWorkspaceModuleParams) (WorkspaceModule, error) {
	row := q.db.QueryRowContext(ctx, insertWorkspaceModule,
		arg.ID,
		arg.JobID,
		arg.Transition,
		arg.Source,
		arg.Version,
		arg.Key,
		arg.CreatedAt,
	)
	var i WorkspaceModule
	err := row.Scan(
		&i.ID,
		&i.JobID,
		&i.Transition,
		&i.Source,
		&i.Version,
		&i.Key,
		&i.CreatedAt,
	)
	return i, err
}

const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one
SELECT
	id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
	workspace_resources
WHERE
	id = $1
`

func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) (WorkspaceResource, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceResourceByID, id)
	var i WorkspaceResource
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.JobID,
		&i.Transition,
		&i.Type,
		&i.Name,
		&i.Hide,
		&i.Icon,
		&i.InstanceType,
		&i.DailyCost,
		&i.ModulePath,
	)
	return i, err
}

const getWorkspaceResourceMetadataByResourceIDs = `-- name: GetWorkspaceResourceMetadataByResourceIDs :many
SELECT
	workspace_resource_id, key, value, sensitive, id
FROM
	workspace_resource_metadata
WHERE
	workspace_resource_id = ANY($1 :: uuid [ ]) ORDER BY id ASC
`

func (q *sqlQuerier) GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceResourceMetadataByResourceIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceResourceMetadatum
	for rows.Next() {
		var i WorkspaceResourceMetadatum
		if err := rows.Scan(
			&i.WorkspaceResourceID,
			&i.Key,
			&i.Value,
			&i.Sensitive,
			&i.ID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceResourceMetadataCreatedAfter = `-- name: GetWorkspaceResourceMetadataCreatedAfter :many
SELECT workspace_resource_id, key, value, sensitive, id FROM workspace_resource_metadata WHERE workspace_resource_id = ANY(
	SELECT id FROM workspace_resources WHERE created_at > $1
)
`

func (q *sqlQuerier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceResourceMetadataCreatedAfter, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceResourceMetadatum
	for rows.Next() {
		var i WorkspaceResourceMetadatum
		if err := rows.Scan(
			&i.WorkspaceResourceID,
			&i.Key,
			&i.Value,
			&i.Sensitive,
			&i.ID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many
SELECT
	id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
	workspace_resources
WHERE
	job_id = $1
`

func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesByJobID, jobID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceResource
	for rows.Next() {
		var i WorkspaceResource
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.JobID,
			&i.Transition,
			&i.Type,
			&i.Name,
			&i.Hide,
			&i.Icon,
			&i.InstanceType,
			&i.DailyCost,
			&i.ModulePath,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceResourcesByJobIDs = `-- name: GetWorkspaceResourcesByJobIDs :many
SELECT
	id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
	workspace_resources
WHERE
	job_id = ANY($1 :: uuid [ ])
`

func (q *sqlQuerier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesByJobIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceResource
	for rows.Next() {
		var i WorkspaceResource
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.JobID,
			&i.Transition,
			&i.Type,
			&i.Name,
			&i.Hide,
			&i.Icon,
			&i.InstanceType,
			&i.DailyCost,
			&i.ModulePath,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceResourcesCreatedAfter = `-- name: GetWorkspaceResourcesCreatedAfter :many
SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path FROM workspace_resources WHERE created_at > $1
`

func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesCreatedAfter, createdAt)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceResource
	for rows.Next() {
		var i WorkspaceResource
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.JobID,
			&i.Transition,
			&i.Type,
			&i.Name,
			&i.Hide,
			&i.Icon,
			&i.InstanceType,
			&i.DailyCost,
			&i.ModulePath,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceResource = `-- name: InsertWorkspaceResource :one
INSERT INTO
	workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
`

type InsertWorkspaceResourceParams struct {
	ID           uuid.UUID           `db:"id" json:"id"`
	CreatedAt    time.Time           `db:"created_at" json:"created_at"`
	JobID        uuid.UUID           `db:"job_id" json:"job_id"`
	Transition   WorkspaceTransition `db:"transition" json:"transition"`
	Type         string              `db:"type" json:"type"`
	Name         string              `db:"name" json:"name"`
	Hide         bool                `db:"hide" json:"hide"`
	Icon         string              `db:"icon" json:"icon"`
	InstanceType sql.NullString      `db:"instance_type" json:"instance_type"`
	DailyCost    int32               `db:"daily_cost" json:"daily_cost"`
	ModulePath   sql.NullString      `db:"module_path" json:"module_path"`
}

func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) {
	row := q.db.QueryRowContext(ctx, insertWorkspaceResource,
		arg.ID,
		arg.CreatedAt,
		arg.JobID,
		arg.Transition,
		arg.Type,
		arg.Name,
		arg.Hide,
		arg.Icon,
		arg.InstanceType,
		arg.DailyCost,
		arg.ModulePath,
	)
	var i WorkspaceResource
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.JobID,
		&i.Transition,
		&i.Type,
		&i.Name,
		&i.Hide,
		&i.Icon,
		&i.InstanceType,
		&i.DailyCost,
		&i.ModulePath,
	)
	return i, err
}

const insertWorkspaceResourceMetadata = `-- name: InsertWorkspaceResourceMetadata :many
INSERT INTO
	workspace_resource_metadata
SELECT
	$1 :: uuid AS workspace_resource_id,
	unnest($2 :: text [ ]) AS key,
	unnest($3 :: text [ ]) AS value,
	unnest($4 :: boolean [ ]) AS sensitive RETURNING workspace_resource_id, key, value, sensitive, id
`

type InsertWorkspaceResourceMetadataParams struct {
	WorkspaceResourceID uuid.UUID `db:"workspace_resource_id" json:"workspace_resource_id"`
	Key                 []string  `db:"key" json:"key"`
	Value               []string  `db:"value" json:"value"`
	Sensitive           []bool    `db:"sensitive" json:"sensitive"`
}

func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) {
	rows, err := q.db.QueryContext(ctx, insertWorkspaceResourceMetadata,
		arg.WorkspaceResourceID,
		pq.Array(arg.Key),
		pq.Array(arg.Value),
		pq.Array(arg.Sensitive),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceResourceMetadatum
	for rows.Next() {
		var i WorkspaceResourceMetadatum
		if err := rows.Scan(
			&i.WorkspaceResourceID,
			&i.Key,
			&i.Value,
			&i.Sensitive,
			&i.ID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const batchUpdateWorkspaceLastUsedAt = `-- name: BatchUpdateWorkspaceLastUsedAt :exec
UPDATE
	workspaces
SET
	last_used_at = $1
WHERE
	id = ANY($2 :: uuid[])
AND
  -- Do not overwrite with older data
  last_used_at < $1
`

type BatchUpdateWorkspaceLastUsedAtParams struct {
	LastUsedAt time.Time   `db:"last_used_at" json:"last_used_at"`
	IDs        []uuid.UUID `db:"ids" json:"ids"`
}

func (q *sqlQuerier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error {
	_, err := q.db.ExecContext(ctx, batchUpdateWorkspaceLastUsedAt, arg.LastUsedAt, pq.Array(arg.IDs))
	return err
}

const batchUpdateWorkspaceNextStartAt = `-- name: BatchUpdateWorkspaceNextStartAt :exec
UPDATE
	workspaces
SET
	next_start_at = CASE
		WHEN batch.next_start_at = '0001-01-01 00:00:00+00'::timestamptz THEN NULL
		ELSE batch.next_start_at
	END
FROM (
	SELECT
		unnest($1::uuid[]) AS id,
		unnest($2::timestamptz[]) AS next_start_at
) AS batch
WHERE
	workspaces.id = batch.id
`

type BatchUpdateWorkspaceNextStartAtParams struct {
	IDs          []uuid.UUID `db:"ids" json:"ids"`
	NextStartAts []time.Time `db:"next_start_ats" json:"next_start_ats"`
}

func (q *sqlQuerier) BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg BatchUpdateWorkspaceNextStartAtParams) error {
	_, err := q.db.ExecContext(ctx, batchUpdateWorkspaceNextStartAt, pq.Array(arg.IDs), pq.Array(arg.NextStartAts))
	return err
}

const deleteWorkspaceACLByID = `-- name: DeleteWorkspaceACLByID :exec
UPDATE
	workspaces
SET
	group_acl = '{}'::json,
	user_acl = '{}'::json
WHERE
	id = $1
`

func (q *sqlQuerier) DeleteWorkspaceACLByID(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, deleteWorkspaceACLByID, id)
	return err
}

const deleteWorkspaceACLsByOrganization = `-- name: DeleteWorkspaceACLsByOrganization :exec
UPDATE
	workspaces
SET
	group_acl = '{}'::jsonb,
	user_acl = '{}'::jsonb
WHERE
	organization_id = $1
	AND (
		NOT $2::boolean
		OR owner_id NOT IN (
			SELECT id FROM users WHERE is_service_account = true
		)
	)
`

type DeleteWorkspaceACLsByOrganizationParams struct {
	OrganizationID         uuid.UUID `db:"organization_id" json:"organization_id"`
	ExcludeServiceAccounts bool      `db:"exclude_service_accounts" json:"exclude_service_accounts"`
}

func (q *sqlQuerier) DeleteWorkspaceACLsByOrganization(ctx context.Context, arg DeleteWorkspaceACLsByOrganizationParams) error {
	_, err := q.db.ExecContext(ctx, deleteWorkspaceACLsByOrganization, arg.OrganizationID, arg.ExcludeServiceAccounts)
	return err
}

const favoriteWorkspace = `-- name: FavoriteWorkspace :exec
UPDATE workspaces SET favorite = true WHERE id = $1
`

func (q *sqlQuerier) FavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, favoriteWorkspace, id)
	return err
}

const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one
WITH workspaces_with_jobs AS (
	SELECT
	latest_build.transition, latest_build.provisioner_job_id, latest_build.started_at, latest_build.updated_at, latest_build.canceled_at, latest_build.completed_at, latest_build.error FROM workspaces
	LEFT JOIN LATERAL (
		SELECT
			workspace_builds.transition,
			provisioner_jobs.id AS provisioner_job_id,
			provisioner_jobs.started_at,
			provisioner_jobs.updated_at,
			provisioner_jobs.canceled_at,
			provisioner_jobs.completed_at,
			provisioner_jobs.error
		FROM
			workspace_builds
		LEFT JOIN
			provisioner_jobs
		ON
			provisioner_jobs.id = workspace_builds.job_id
		WHERE
			workspace_builds.workspace_id = workspaces.id
		ORDER BY
			build_number DESC
		LIMIT
			1
	) latest_build ON TRUE WHERE deleted = false
), pending_workspaces AS (
	SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
		started_at IS NULL
), building_workspaces AS (
	SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
		started_at IS NOT NULL AND
		canceled_at IS NULL AND
		completed_at IS NULL AND
		updated_at - INTERVAL '30 seconds' < NOW()
), running_workspaces AS (
	SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
		completed_at IS NOT NULL AND
		canceled_at IS NULL AND
		error IS NULL AND
		transition = 'start'::workspace_transition
), failed_workspaces AS (
	SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
		(canceled_at IS NOT NULL AND
			error IS NOT NULL) OR
		(completed_at IS NOT NULL AND
			error IS NOT NULL)
), stopped_workspaces AS (
	SELECT COUNT(*) AS count FROM workspaces_with_jobs WHERE
		completed_at IS NOT NULL AND
		canceled_at IS NULL AND
		error IS NULL AND
		transition = 'stop'::workspace_transition
)
SELECT
	pending_workspaces.count AS pending_workspaces,
	building_workspaces.count AS building_workspaces,
	running_workspaces.count AS running_workspaces,
	failed_workspaces.count AS failed_workspaces,
	stopped_workspaces.count AS stopped_workspaces
FROM pending_workspaces, building_workspaces, running_workspaces, failed_workspaces, stopped_workspaces
`

type GetDeploymentWorkspaceStatsRow struct {
	PendingWorkspaces  int64 `db:"pending_workspaces" json:"pending_workspaces"`
	BuildingWorkspaces int64 `db:"building_workspaces" json:"building_workspaces"`
	RunningWorkspaces  int64 `db:"running_workspaces" json:"running_workspaces"`
	FailedWorkspaces   int64 `db:"failed_workspaces" json:"failed_workspaces"`
	StoppedWorkspaces  int64 `db:"stopped_workspaces" json:"stopped_workspaces"`
}

func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) {
	row := q.db.QueryRowContext(ctx, getDeploymentWorkspaceStats)
	var i GetDeploymentWorkspaceStatsRow
	err := row.Scan(
		&i.PendingWorkspaces,
		&i.BuildingWorkspaces,
		&i.RunningWorkspaces,
		&i.FailedWorkspaces,
		&i.StoppedWorkspaces,
	)
	return i, err
}

const getRegularWorkspaceCreateMetrics = `-- name: GetRegularWorkspaceCreateMetrics :many
WITH first_success_build AS (
	-- Earliest successful 'start' build per workspace
	SELECT DISTINCT ON (wb.workspace_id)
		wb.workspace_id,
		wb.template_version_preset_id,
		wb.initiator_id
	FROM workspace_builds wb
	JOIN provisioner_jobs pj ON pj.id = wb.job_id
	WHERE
		wb.transition = 'start'::workspace_transition
  		AND pj.job_status = 'succeeded'::provisioner_job_status
	ORDER BY wb.workspace_id, wb.build_number, wb.id
)
SELECT
	t.name AS template_name,
	COALESCE(tvp.name, '') AS preset_name,
	o.name AS organization_name,
	COUNT(*) AS created_count
FROM first_success_build fsb
	JOIN workspaces w ON w.id = fsb.workspace_id
	JOIN templates t ON t.id = w.template_id
	LEFT JOIN template_version_presets tvp ON tvp.id = fsb.template_version_preset_id
	JOIN organizations o ON o.id = w.organization_id
WHERE
	NOT t.deleted
	-- Exclude workspaces whose first successful start was the prebuilds system user
	AND fsb.initiator_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
GROUP BY t.name, COALESCE(tvp.name, ''), o.name
ORDER BY t.name, preset_name, o.name
`

type GetRegularWorkspaceCreateMetricsRow struct {
	TemplateName     string `db:"template_name" json:"template_name"`
	PresetName       string `db:"preset_name" json:"preset_name"`
	OrganizationName string `db:"organization_name" json:"organization_name"`
	CreatedCount     int64  `db:"created_count" json:"created_count"`
}

// Count regular workspaces: only those whose first successful 'start' build
// was not initiated by the prebuild system user.
func (q *sqlQuerier) GetRegularWorkspaceCreateMetrics(ctx context.Context) ([]GetRegularWorkspaceCreateMetricsRow, error) {
	rows, err := q.db.QueryContext(ctx, getRegularWorkspaceCreateMetrics)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetRegularWorkspaceCreateMetricsRow
	for rows.Next() {
		var i GetRegularWorkspaceCreateMetricsRow
		if err := rows.Scan(
			&i.TemplateName,
			&i.PresetName,
			&i.OrganizationName,
			&i.CreatedCount,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaceACLByID = `-- name: GetWorkspaceACLByID :one
SELECT
	group_acl as groups,
	user_acl as users
FROM
	workspaces
WHERE
	id = $1
`

type GetWorkspaceACLByIDRow struct {
	Groups WorkspaceACL `db:"groups" json:"groups"`
	Users  WorkspaceACL `db:"users" json:"users"`
}

func (q *sqlQuerier) GetWorkspaceACLByID(ctx context.Context, id uuid.UUID) (GetWorkspaceACLByIDRow, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceACLByID, id)
	var i GetWorkspaceACLByIDRow
	err := row.Scan(&i.Groups, &i.Users)
	return i, err
}

const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one
SELECT
	id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
	workspaces_expanded as workspaces
WHERE
	workspaces.id = (
		SELECT
			workspace_id
		FROM
			workspace_builds
		WHERE
			workspace_builds.job_id = (
				SELECT
					job_id
				FROM
					workspace_resources
				WHERE
					workspace_resources.id = (
						SELECT
							resource_id
						FROM
							workspace_agents
						WHERE
							workspace_agents.id = $1
					)
			)
	)
`

func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUID) (Workspace, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceByAgentID, agentID)
	var i Workspace
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OwnerID,
		&i.OrganizationID,
		&i.TemplateID,
		&i.Deleted,
		&i.Name,
		&i.AutostartSchedule,
		&i.Ttl,
		&i.LastUsedAt,
		&i.DormantAt,
		&i.DeletingAt,
		&i.AutomaticUpdates,
		&i.Favorite,
		&i.NextStartAt,
		&i.GroupACL,
		&i.UserACL,
		&i.OwnerAvatarUrl,
		&i.OwnerUsername,
		&i.OwnerName,
		&i.OrganizationName,
		&i.OrganizationDisplayName,
		&i.OrganizationIcon,
		&i.OrganizationDescription,
		&i.TemplateName,
		&i.TemplateDisplayName,
		&i.TemplateIcon,
		&i.TemplateDescription,
		&i.TaskID,
		&i.GroupACLDisplayInfo,
		&i.UserACLDisplayInfo,
	)
	return i, err
}

const getWorkspaceByID = `-- name: GetWorkspaceByID :one
SELECT
	id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
	workspaces_expanded
WHERE
	id = $1
LIMIT
	1
`

func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceByID, id)
	var i Workspace
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OwnerID,
		&i.OrganizationID,
		&i.TemplateID,
		&i.Deleted,
		&i.Name,
		&i.AutostartSchedule,
		&i.Ttl,
		&i.LastUsedAt,
		&i.DormantAt,
		&i.DeletingAt,
		&i.AutomaticUpdates,
		&i.Favorite,
		&i.NextStartAt,
		&i.GroupACL,
		&i.UserACL,
		&i.OwnerAvatarUrl,
		&i.OwnerUsername,
		&i.OwnerName,
		&i.OrganizationName,
		&i.OrganizationDisplayName,
		&i.OrganizationIcon,
		&i.OrganizationDescription,
		&i.TemplateName,
		&i.TemplateDisplayName,
		&i.TemplateIcon,
		&i.TemplateDescription,
		&i.TaskID,
		&i.GroupACLDisplayInfo,
		&i.UserACLDisplayInfo,
	)
	return i, err
}

const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
SELECT
	id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
	workspaces_expanded as workspaces
WHERE
	owner_id = $1
	AND deleted = $2
	AND LOWER("name") = LOWER($3)
ORDER BY created_at DESC
`

type GetWorkspaceByOwnerIDAndNameParams struct {
	OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
	Deleted bool      `db:"deleted" json:"deleted"`
	Name    string    `db:"name" json:"name"`
}

func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceByOwnerIDAndName, arg.OwnerID, arg.Deleted, arg.Name)
	var i Workspace
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OwnerID,
		&i.OrganizationID,
		&i.TemplateID,
		&i.Deleted,
		&i.Name,
		&i.AutostartSchedule,
		&i.Ttl,
		&i.LastUsedAt,
		&i.DormantAt,
		&i.DeletingAt,
		&i.AutomaticUpdates,
		&i.Favorite,
		&i.NextStartAt,
		&i.GroupACL,
		&i.UserACL,
		&i.OwnerAvatarUrl,
		&i.OwnerUsername,
		&i.OwnerName,
		&i.OrganizationName,
		&i.OrganizationDisplayName,
		&i.OrganizationIcon,
		&i.OrganizationDescription,
		&i.TemplateName,
		&i.TemplateDisplayName,
		&i.TemplateIcon,
		&i.TemplateDescription,
		&i.TaskID,
		&i.GroupACLDisplayInfo,
		&i.UserACLDisplayInfo,
	)
	return i, err
}

const getWorkspaceByResourceID = `-- name: GetWorkspaceByResourceID :one
SELECT
	id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
	workspaces_expanded as workspaces
WHERE
	workspaces.id = (
		SELECT
			workspace_id
		FROM
			workspace_builds
		WHERE
			workspace_builds.job_id = (
				SELECT
					job_id
				FROM
					workspace_resources
				WHERE
					workspace_resources.id = $1
			)
	)
LIMIT
	1
`

func (q *sqlQuerier) GetWorkspaceByResourceID(ctx context.Context, resourceID uuid.UUID) (Workspace, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceByResourceID, resourceID)
	var i Workspace
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OwnerID,
		&i.OrganizationID,
		&i.TemplateID,
		&i.Deleted,
		&i.Name,
		&i.AutostartSchedule,
		&i.Ttl,
		&i.LastUsedAt,
		&i.DormantAt,
		&i.DeletingAt,
		&i.AutomaticUpdates,
		&i.Favorite,
		&i.NextStartAt,
		&i.GroupACL,
		&i.UserACL,
		&i.OwnerAvatarUrl,
		&i.OwnerUsername,
		&i.OwnerName,
		&i.OrganizationName,
		&i.OrganizationDisplayName,
		&i.OrganizationIcon,
		&i.OrganizationDescription,
		&i.TemplateName,
		&i.TemplateDisplayName,
		&i.TemplateIcon,
		&i.TemplateDescription,
		&i.TaskID,
		&i.GroupACLDisplayInfo,
		&i.UserACLDisplayInfo,
	)
	return i, err
}

const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one
SELECT
	id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info
FROM
	workspaces_expanded as workspaces
WHERE
		workspaces.id = (
		SELECT
			workspace_id
		FROM
			workspace_builds
		WHERE
				workspace_builds.job_id = (
				SELECT
					job_id
				FROM
					workspace_resources
				WHERE
						workspace_resources.id = (
						SELECT
							resource_id
						FROM
							workspace_agents
						WHERE
								workspace_agents.id = (
								SELECT
									agent_id
								FROM
									workspace_apps
								WHERE
									workspace_apps.id = $1
								)
					)
			)
	)
`

func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error) {
	row := q.db.QueryRowContext(ctx, getWorkspaceByWorkspaceAppID, workspaceAppID)
	var i Workspace
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OwnerID,
		&i.OrganizationID,
		&i.TemplateID,
		&i.Deleted,
		&i.Name,
		&i.AutostartSchedule,
		&i.Ttl,
		&i.LastUsedAt,
		&i.DormantAt,
		&i.DeletingAt,
		&i.AutomaticUpdates,
		&i.Favorite,
		&i.NextStartAt,
		&i.GroupACL,
		&i.UserACL,
		&i.OwnerAvatarUrl,
		&i.OwnerUsername,
		&i.OwnerName,
		&i.OrganizationName,
		&i.OrganizationDisplayName,
		&i.OrganizationIcon,
		&i.OrganizationDescription,
		&i.TemplateName,
		&i.TemplateDisplayName,
		&i.TemplateIcon,
		&i.TemplateDescription,
		&i.TaskID,
		&i.GroupACLDisplayInfo,
		&i.UserACLDisplayInfo,
	)
	return i, err
}

const getWorkspaceUniqueOwnerCountByTemplateIDs = `-- name: GetWorkspaceUniqueOwnerCountByTemplateIDs :many
SELECT templates.id AS template_id, COUNT(DISTINCT workspaces.owner_id) AS unique_owners_sum
FROM templates
LEFT JOIN workspaces ON workspaces.template_id = templates.id AND workspaces.deleted = false
WHERE templates.id = ANY($1 :: uuid[])
GROUP BY templates.id
`

type GetWorkspaceUniqueOwnerCountByTemplateIDsRow struct {
	TemplateID      uuid.UUID `db:"template_id" json:"template_id"`
	UniqueOwnersSum int64     `db:"unique_owners_sum" json:"unique_owners_sum"`
}

func (q *sqlQuerier) GetWorkspaceUniqueOwnerCountByTemplateIDs(ctx context.Context, templateIds []uuid.UUID) ([]GetWorkspaceUniqueOwnerCountByTemplateIDsRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceUniqueOwnerCountByTemplateIDs, pq.Array(templateIds))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceUniqueOwnerCountByTemplateIDsRow
	for rows.Next() {
		var i GetWorkspaceUniqueOwnerCountByTemplateIDsRow
		if err := rows.Scan(&i.TemplateID, &i.UniqueOwnersSum); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspaces = `-- name: GetWorkspaces :many
WITH
build_params AS (
SELECT
	LOWER(unnest($1 :: text[])) AS name,
	LOWER(unnest($2 :: text[])) AS value
),
filtered_workspaces AS (
SELECT
	workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.owner_name, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description, workspaces.task_id, workspaces.group_acl_display_info, workspaces.user_acl_display_info,
	latest_build.template_version_id,
	latest_build.template_version_name,
	latest_build.completed_at as latest_build_completed_at,
	latest_build.canceled_at as latest_build_canceled_at,
	latest_build.error as latest_build_error,
	latest_build.transition as latest_build_transition,
	latest_build.job_status as latest_build_status,
	latest_build.has_external_agent as latest_build_has_external_agent
FROM
	workspaces_expanded as workspaces
JOIN
    users
ON
    workspaces.owner_id = users.id
LEFT JOIN LATERAL (
	SELECT
		workspace_builds.id,
		workspace_builds.transition,
		workspace_builds.template_version_id,
		workspace_builds.has_ai_task,
		workspace_builds.has_external_agent,
		template_versions.name AS template_version_name,
		provisioner_jobs.id AS provisioner_job_id,
		provisioner_jobs.started_at,
		provisioner_jobs.updated_at,
		provisioner_jobs.canceled_at,
		provisioner_jobs.completed_at,
		provisioner_jobs.error,
		provisioner_jobs.job_status
	FROM
		workspace_builds
	JOIN
		provisioner_jobs
	ON
		provisioner_jobs.id = workspace_builds.job_id
	LEFT JOIN
		template_versions
	ON
		template_versions.id = workspace_builds.template_version_id
	WHERE
		workspace_builds.workspace_id = workspaces.id
	ORDER BY
		build_number DESC
	LIMIT
		1
) latest_build ON TRUE
LEFT JOIN LATERAL (
	SELECT
		id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache
	FROM
		templates
	WHERE
		templates.id = workspaces.template_id
) template ON true
WHERE
	-- Optionally include deleted workspaces
	workspaces.deleted = $3
	AND CASE
		WHEN $4 :: text != '' THEN
			CASE
			    -- Some workspace specific status refer to the transition
			    -- type. By default, the standard provisioner job status
			    -- search strings are supported.
			    -- 'running' states
				WHEN $4 = 'starting' THEN
				    latest_build.job_status = 'running'::provisioner_job_status AND
					latest_build.transition = 'start'::workspace_transition
				WHEN $4 = 'stopping' THEN
					latest_build.job_status = 'running'::provisioner_job_status AND
					latest_build.transition = 'stop'::workspace_transition
				WHEN $4 = 'deleting' THEN
					latest_build.job_status = 'running' AND
					latest_build.transition = 'delete'::workspace_transition

			    -- 'succeeded' states
			    WHEN $4 = 'deleted' THEN
			    	latest_build.job_status = 'succeeded'::provisioner_job_status AND
			    	latest_build.transition = 'delete'::workspace_transition
				WHEN $4 = 'stopped' THEN
					latest_build.job_status = 'succeeded'::provisioner_job_status AND
					latest_build.transition = 'stop'::workspace_transition
				WHEN $4 = 'started' THEN
					latest_build.job_status = 'succeeded'::provisioner_job_status AND
					latest_build.transition = 'start'::workspace_transition

			    -- Special case where the provisioner status and workspace status
			    -- differ. A workspace is "running" if the job is "succeeded" and
			    -- the transition is "start". This is because a workspace starts
			    -- running when a job is complete.
			    WHEN $4 = 'running' THEN
					latest_build.job_status = 'succeeded'::provisioner_job_status AND
					latest_build.transition = 'start'::workspace_transition

				WHEN $4 != '' THEN
				    -- By default just match the job status exactly
			    	latest_build.job_status = $4::provisioner_job_status
				ELSE
					true
			END
		ELSE true
	END
	-- Filter by owner_id
	AND CASE
		WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			workspaces.owner_id = $5
		ELSE true
	END
  	-- Filter by organization_id
  	AND CASE
		  WHEN $6 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			  workspaces.organization_id = $6
		  ELSE true
	END
	-- Filter by build parameter
   	-- @has_param will match any build that includes the parameter.
	AND CASE WHEN array_length($7 :: text[], 1) > 0  THEN
		EXISTS (
			SELECT
				1
			FROM
				workspace_build_parameters
			WHERE
				workspace_build_parameters.workspace_build_id = latest_build.id AND
				-- ILIKE is case insensitive
				workspace_build_parameters.name ILIKE ANY($7)
		)
		ELSE true
	END
	-- @param_value will match param name an value.
  	-- requires 2 arrays, @param_names and @param_values to be passed in.
  	-- Array index must match between the 2 arrays for name=value
  	AND CASE WHEN array_length($1 :: text[], 1) > 0  THEN
		EXISTS (
			SELECT
				1
			FROM
				workspace_build_parameters
			INNER JOIN
				build_params
			ON
				LOWER(workspace_build_parameters.name) = build_params.name AND
				LOWER(workspace_build_parameters.value) = build_params.value AND
				workspace_build_parameters.workspace_build_id = latest_build.id
		)
		ELSE true
	END

	-- Filter by owner_name
	AND CASE
		WHEN $8 :: text != '' THEN
			workspaces.owner_id = (SELECT id FROM users WHERE lower(users.username) = lower($8) AND deleted = false)
		ELSE true
	END
	-- Filter by template_name
	-- There can be more than 1 template with the same name across organizations.
	-- Use the organization filter to restrict to 1 org if needed.
	AND CASE
		WHEN $9 :: text != '' THEN
			workspaces.template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($9) AND deleted = false)
		ELSE true
	END
	-- Filter by template_ids
	AND CASE
		WHEN array_length($10 :: uuid[], 1) > 0 THEN
			workspaces.template_id = ANY($10)
		ELSE true
	END
  	-- Filter by workspace_ids
  	AND CASE
		  WHEN array_length($11 :: uuid[], 1) > 0 THEN
			  workspaces.id = ANY($11)
		  ELSE true
	END
	-- Filter by name, matching on substring
	AND CASE
		WHEN $12 :: text != '' THEN
			workspaces.name ILIKE '%' || $12 || '%'
		ELSE true
	END
	-- Filter by agent status
	-- has-agent: is only applicable for workspaces in "start" transition. Stopped and deleted workspaces don't have agents.
	AND CASE
		WHEN array_length($13 :: text[], 1) > 0 THEN
			(
				SELECT COUNT(*)
				FROM
					workspace_resources
				JOIN
					workspace_agents
				ON
					workspace_agents.resource_id = workspace_resources.id
				WHERE
					workspace_resources.job_id = latest_build.provisioner_job_id AND
					latest_build.transition = 'start'::workspace_transition AND
					-- Filter out deleted sub agents.
					workspace_agents.deleted = FALSE AND
					(
						CASE
							WHEN workspace_agents.first_connected_at IS NULL THEN
								CASE
									WHEN workspace_agents.connection_timeout_seconds > 0 AND NOW() - workspace_agents.created_at > workspace_agents.connection_timeout_seconds * INTERVAL '1 second' THEN
										'timeout'
									ELSE
										'connecting'
								END
							WHEN workspace_agents.disconnected_at > workspace_agents.last_connected_at THEN
								'disconnected'
							WHEN NOW() - workspace_agents.last_connected_at > INTERVAL '1 second' * $14 :: bigint THEN
								'disconnected'
							WHEN workspace_agents.last_connected_at IS NOT NULL THEN
								'connected'
							ELSE
								NULL
						END
					) = ANY($13 :: text[])
			) > 0
		ELSE true
	END
	-- Filter by dormant workspaces.
	AND CASE
		WHEN $15 :: boolean != 'false' THEN
			dormant_at IS NOT NULL
		ELSE true
	END
	-- Filter by last_used
	AND CASE
		  WHEN $16 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
				  workspaces.last_used_at <= $16
		  ELSE true
	END
	AND CASE
		  WHEN $17 :: timestamp with time zone > '0001-01-01 00:00:00Z' THEN
				  workspaces.last_used_at >= $17
		  ELSE true
	END
  	AND CASE
		  WHEN $18 :: boolean IS NOT NULL THEN
			  (latest_build.template_version_id = template.active_version_id) = $18 :: boolean
		  ELSE true
	END
	-- Filter by has_ai_task, checks if this is a task workspace.
	AND CASE
		WHEN $19::boolean IS NOT NULL
		THEN $19::boolean = EXISTS (
			SELECT
				1
			FROM
				tasks
			WHERE
				-- Consider all tasks, deleting a task does not turn the
				-- workspace into a non-task workspace.
				tasks.workspace_id = workspaces.id
		)
		ELSE true
	END
	-- Filter by has_external_agent in latest build
	AND CASE
		WHEN $20 :: boolean IS NOT NULL THEN
			latest_build.has_external_agent = $20 :: boolean
		ELSE true
	END
	-- Filter by shared status
	AND CASE
		WHEN $21 :: boolean IS NOT NULL THEN
			(workspaces.user_acl != '{}'::jsonb OR workspaces.group_acl != '{}'::jsonb) = $21 :: boolean
		ELSE true
	END
	-- Filter by shared_with_user_id
	AND CASE
		WHEN $22 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			workspaces.user_acl ? ($22 :: uuid) :: text
		ELSE true
	END
	-- Filter by shared_with_group_id
	AND CASE
		WHEN $23 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
			workspaces.group_acl ? ($23 :: uuid) :: text
		ELSE true
	END

	-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
	-- @authorize_filter
), filtered_workspaces_order AS (
	SELECT
		fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.group_acl_display_info, fw.user_acl_display_info, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_external_agent
	FROM
		filtered_workspaces fw
	ORDER BY
		-- To ensure that 'favorite' workspaces show up first in the list only for their owner.
		CASE WHEN favorite AND owner_username = (SELECT users.username FROM users WHERE users.id = $24) THEN 0 ELSE 1 END ASC,
		(latest_build_completed_at IS NOT NULL AND
			latest_build_canceled_at IS NULL AND
			latest_build_error IS NULL AND
			latest_build_transition = 'start'::workspace_transition) DESC,
		LOWER(owner_username) ASC,
		LOWER(name) ASC
	LIMIT
		CASE
			WHEN $26 :: integer > 0 THEN
				$26
		END
	OFFSET
		$25
), filtered_workspaces_order_with_summary AS (
	SELECT
		fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.group_acl_display_info, fwo.user_acl_display_info, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent
	FROM
		filtered_workspaces_order fwo
	-- Return a technical summary row with total count of workspaces.
	-- It is used to present the correct count if pagination goes beyond the offset.
	UNION ALL
	SELECT
		'00000000-0000-0000-0000-000000000000'::uuid, -- id
		'0001-01-01 00:00:00+00'::timestamptz, -- created_at
		'0001-01-01 00:00:00+00'::timestamptz, -- updated_at
		'00000000-0000-0000-0000-000000000000'::uuid, -- owner_id
		'00000000-0000-0000-0000-000000000000'::uuid, -- organization_id
		'00000000-0000-0000-0000-000000000000'::uuid, -- template_id
		false, -- deleted
		'**TECHNICAL_ROW**', -- name
		'', -- autostart_schedule
		0, -- ttl
		'0001-01-01 00:00:00+00'::timestamptz, -- last_used_at
		'0001-01-01 00:00:00+00'::timestamptz, -- dormant_at
		'0001-01-01 00:00:00+00'::timestamptz, -- deleting_at
		'never'::automatic_updates, -- automatic_updates
		false, -- favorite
		'0001-01-01 00:00:00+00'::timestamptz, -- next_start_at
		'{}'::jsonb, -- group_acl
		'{}'::jsonb, -- user_acl
		'', -- owner_avatar_url
		'', -- owner_username
		'', -- owner_name
		'', -- organization_name
		'', -- organization_display_name
		'', -- organization_icon
		'', -- organization_description
		'', -- template_name
		'', -- template_display_name
		'', -- template_icon
		'', -- template_description
		'00000000-0000-0000-0000-000000000000'::uuid, -- task_id
		'{}'::jsonb, -- group_acl_display_info
		'{}'::jsonb, -- user_acl_display_info
		-- Extra columns added to ` + "`" + `filtered_workspaces` + "`" + `
		'00000000-0000-0000-0000-000000000000'::uuid, -- template_version_id
		'', -- template_version_name
		'0001-01-01 00:00:00+00'::timestamptz, -- latest_build_completed_at,
		'0001-01-01 00:00:00+00'::timestamptz, -- latest_build_canceled_at,
		'', -- latest_build_error
		'start'::workspace_transition, -- latest_build_transition
		'unknown'::provisioner_job_status, -- latest_build_status
		false -- latest_build_has_external_agent
	WHERE
		$27 :: boolean = true
), total_count AS (
	SELECT
		count(*) AS count
    FROM
		filtered_workspaces
)
SELECT
	fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.group_acl_display_info, fwos.user_acl_display_info, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent,
	tc.count
FROM
	filtered_workspaces_order_with_summary fwos
CROSS JOIN
	total_count tc
`

type GetWorkspacesParams struct {
	ParamNames                            []string     `db:"param_names" json:"param_names"`
	ParamValues                           []string     `db:"param_values" json:"param_values"`
	Deleted                               bool         `db:"deleted" json:"deleted"`
	Status                                string       `db:"status" json:"status"`
	OwnerID                               uuid.UUID    `db:"owner_id" json:"owner_id"`
	OrganizationID                        uuid.UUID    `db:"organization_id" json:"organization_id"`
	HasParam                              []string     `db:"has_param" json:"has_param"`
	OwnerUsername                         string       `db:"owner_username" json:"owner_username"`
	TemplateName                          string       `db:"template_name" json:"template_name"`
	TemplateIDs                           []uuid.UUID  `db:"template_ids" json:"template_ids"`
	WorkspaceIds                          []uuid.UUID  `db:"workspace_ids" json:"workspace_ids"`
	Name                                  string       `db:"name" json:"name"`
	HasAgentStatuses                      []string     `db:"has_agent_statuses" json:"has_agent_statuses"`
	AgentInactiveDisconnectTimeoutSeconds int64        `db:"agent_inactive_disconnect_timeout_seconds" json:"agent_inactive_disconnect_timeout_seconds"`
	Dormant                               bool         `db:"dormant" json:"dormant"`
	LastUsedBefore                        time.Time    `db:"last_used_before" json:"last_used_before"`
	LastUsedAfter                         time.Time    `db:"last_used_after" json:"last_used_after"`
	UsingActive                           sql.NullBool `db:"using_active" json:"using_active"`
	HasAITask                             sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
	HasExternalAgent                      sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
	Shared                                sql.NullBool `db:"shared" json:"shared"`
	SharedWithUserID                      uuid.UUID    `db:"shared_with_user_id" json:"shared_with_user_id"`
	SharedWithGroupID                     uuid.UUID    `db:"shared_with_group_id" json:"shared_with_group_id"`
	RequesterID                           uuid.UUID    `db:"requester_id" json:"requester_id"`
	Offset                                int32        `db:"offset_" json:"offset_"`
	Limit                                 int32        `db:"limit_" json:"limit_"`
	WithSummary                           bool         `db:"with_summary" json:"with_summary"`
}

type GetWorkspacesRow struct {
	ID                          uuid.UUID            `db:"id" json:"id"`
	CreatedAt                   time.Time            `db:"created_at" json:"created_at"`
	UpdatedAt                   time.Time            `db:"updated_at" json:"updated_at"`
	OwnerID                     uuid.UUID            `db:"owner_id" json:"owner_id"`
	OrganizationID              uuid.UUID            `db:"organization_id" json:"organization_id"`
	TemplateID                  uuid.UUID            `db:"template_id" json:"template_id"`
	Deleted                     bool                 `db:"deleted" json:"deleted"`
	Name                        string               `db:"name" json:"name"`
	AutostartSchedule           sql.NullString       `db:"autostart_schedule" json:"autostart_schedule"`
	Ttl                         sql.NullInt64        `db:"ttl" json:"ttl"`
	LastUsedAt                  time.Time            `db:"last_used_at" json:"last_used_at"`
	DormantAt                   sql.NullTime         `db:"dormant_at" json:"dormant_at"`
	DeletingAt                  sql.NullTime         `db:"deleting_at" json:"deleting_at"`
	AutomaticUpdates            AutomaticUpdates     `db:"automatic_updates" json:"automatic_updates"`
	Favorite                    bool                 `db:"favorite" json:"favorite"`
	NextStartAt                 sql.NullTime         `db:"next_start_at" json:"next_start_at"`
	GroupACL                    json.RawMessage      `db:"group_acl" json:"group_acl"`
	UserACL                     json.RawMessage      `db:"user_acl" json:"user_acl"`
	OwnerAvatarUrl              string               `db:"owner_avatar_url" json:"owner_avatar_url"`
	OwnerUsername               string               `db:"owner_username" json:"owner_username"`
	OwnerName                   string               `db:"owner_name" json:"owner_name"`
	OrganizationName            string               `db:"organization_name" json:"organization_name"`
	OrganizationDisplayName     string               `db:"organization_display_name" json:"organization_display_name"`
	OrganizationIcon            string               `db:"organization_icon" json:"organization_icon"`
	OrganizationDescription     string               `db:"organization_description" json:"organization_description"`
	TemplateName                string               `db:"template_name" json:"template_name"`
	TemplateDisplayName         string               `db:"template_display_name" json:"template_display_name"`
	TemplateIcon                string               `db:"template_icon" json:"template_icon"`
	TemplateDescription         string               `db:"template_description" json:"template_description"`
	TaskID                      uuid.NullUUID        `db:"task_id" json:"task_id"`
	GroupACLDisplayInfo         interface{}          `db:"group_acl_display_info" json:"group_acl_display_info"`
	UserACLDisplayInfo          interface{}          `db:"user_acl_display_info" json:"user_acl_display_info"`
	TemplateVersionID           uuid.UUID            `db:"template_version_id" json:"template_version_id"`
	TemplateVersionName         sql.NullString       `db:"template_version_name" json:"template_version_name"`
	LatestBuildCompletedAt      sql.NullTime         `db:"latest_build_completed_at" json:"latest_build_completed_at"`
	LatestBuildCanceledAt       sql.NullTime         `db:"latest_build_canceled_at" json:"latest_build_canceled_at"`
	LatestBuildError            sql.NullString       `db:"latest_build_error" json:"latest_build_error"`
	LatestBuildTransition       WorkspaceTransition  `db:"latest_build_transition" json:"latest_build_transition"`
	LatestBuildStatus           ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"`
	LatestBuildHasExternalAgent sql.NullBool         `db:"latest_build_has_external_agent" json:"latest_build_has_external_agent"`
	Count                       int64                `db:"count" json:"count"`
}

// build_params is used to filter by build parameters if present.
// It has to be a CTE because the set returning function 'unnest' cannot
// be used in a WHERE clause.
func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaces,
		pq.Array(arg.ParamNames),
		pq.Array(arg.ParamValues),
		arg.Deleted,
		arg.Status,
		arg.OwnerID,
		arg.OrganizationID,
		pq.Array(arg.HasParam),
		arg.OwnerUsername,
		arg.TemplateName,
		pq.Array(arg.TemplateIDs),
		pq.Array(arg.WorkspaceIds),
		arg.Name,
		pq.Array(arg.HasAgentStatuses),
		arg.AgentInactiveDisconnectTimeoutSeconds,
		arg.Dormant,
		arg.LastUsedBefore,
		arg.LastUsedAfter,
		arg.UsingActive,
		arg.HasAITask,
		arg.HasExternalAgent,
		arg.Shared,
		arg.SharedWithUserID,
		arg.SharedWithGroupID,
		arg.RequesterID,
		arg.Offset,
		arg.Limit,
		arg.WithSummary,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspacesRow
	for rows.Next() {
		var i GetWorkspacesRow
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.OwnerID,
			&i.OrganizationID,
			&i.TemplateID,
			&i.Deleted,
			&i.Name,
			&i.AutostartSchedule,
			&i.Ttl,
			&i.LastUsedAt,
			&i.DormantAt,
			&i.DeletingAt,
			&i.AutomaticUpdates,
			&i.Favorite,
			&i.NextStartAt,
			&i.GroupACL,
			&i.UserACL,
			&i.OwnerAvatarUrl,
			&i.OwnerUsername,
			&i.OwnerName,
			&i.OrganizationName,
			&i.OrganizationDisplayName,
			&i.OrganizationIcon,
			&i.OrganizationDescription,
			&i.TemplateName,
			&i.TemplateDisplayName,
			&i.TemplateIcon,
			&i.TemplateDescription,
			&i.TaskID,
			&i.GroupACLDisplayInfo,
			&i.UserACLDisplayInfo,
			&i.TemplateVersionID,
			&i.TemplateVersionName,
			&i.LatestBuildCompletedAt,
			&i.LatestBuildCanceledAt,
			&i.LatestBuildError,
			&i.LatestBuildTransition,
			&i.LatestBuildStatus,
			&i.LatestBuildHasExternalAgent,
			&i.Count,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspacesAndAgentsByOwnerID = `-- name: GetWorkspacesAndAgentsByOwnerID :many
SELECT
	workspaces.id as id,
	workspaces.name as name,
	job_status,
	transition,
	(array_agg(ROW(agent_id, agent_name)::agent_id_name_pair) FILTER (WHERE agent_id IS NOT NULL))::agent_id_name_pair[] as agents
FROM workspaces
LEFT JOIN LATERAL (
	SELECT
		workspace_id,
		job_id,
		transition,
		job_status
	FROM workspace_builds
	JOIN provisioner_jobs ON provisioner_jobs.id = workspace_builds.job_id
	WHERE workspace_builds.workspace_id = workspaces.id
	ORDER BY build_number DESC
	LIMIT 1
) latest_build ON true
LEFT JOIN LATERAL (
	SELECT
		workspace_agents.id as agent_id,
		workspace_agents.name as agent_name,
		job_id
	FROM workspace_resources
	JOIN workspace_agents ON (
		workspace_agents.resource_id = workspace_resources.id
		-- Filter out deleted sub agents.
		AND workspace_agents.deleted = FALSE
	)
	WHERE job_id = latest_build.job_id
) resources ON true
WHERE
	-- Filter by owner_id
	workspaces.owner_id = $1 :: uuid
	AND workspaces.deleted = false
	-- Authorize Filter clause will be injected below in GetAuthorizedWorkspacesAndAgentsByOwnerID
	-- @authorize_filter
GROUP BY workspaces.id, workspaces.name, latest_build.job_status, latest_build.job_id, latest_build.transition
`

type GetWorkspacesAndAgentsByOwnerIDRow struct {
	ID         uuid.UUID            `db:"id" json:"id"`
	Name       string               `db:"name" json:"name"`
	JobStatus  ProvisionerJobStatus `db:"job_status" json:"job_status"`
	Transition WorkspaceTransition  `db:"transition" json:"transition"`
	Agents     []AgentIDNamePair    `db:"agents" json:"agents"`
}

func (q *sqlQuerier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspacesAndAgentsByOwnerID, ownerID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspacesAndAgentsByOwnerIDRow
	for rows.Next() {
		var i GetWorkspacesAndAgentsByOwnerIDRow
		if err := rows.Scan(
			&i.ID,
			&i.Name,
			&i.JobStatus,
			&i.Transition,
			pq.Array(&i.Agents),
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspacesByTemplateID = `-- name: GetWorkspacesByTemplateID :many
SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl FROM workspaces WHERE template_id = $1 AND deleted = false
`

func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspacesByTemplateID, templateID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceTable
	for rows.Next() {
		var i WorkspaceTable
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.OwnerID,
			&i.OrganizationID,
			&i.TemplateID,
			&i.Deleted,
			&i.Name,
			&i.AutostartSchedule,
			&i.Ttl,
			&i.LastUsedAt,
			&i.DormantAt,
			&i.DeletingAt,
			&i.AutomaticUpdates,
			&i.Favorite,
			&i.NextStartAt,
			&i.GroupACL,
			&i.UserACL,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many
SELECT
	workspaces.id,
	workspaces.name,
	workspace_builds.template_version_id as build_template_version_id
FROM
	workspaces
LEFT JOIN
	workspace_builds ON workspace_builds.workspace_id = workspaces.id
INNER JOIN
	provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id
INNER JOIN
	templates ON workspaces.template_id = templates.id
INNER JOIN
	users ON workspaces.owner_id = users.id
WHERE
	workspace_builds.build_number = (
		SELECT
			MAX(build_number)
		FROM
			workspace_builds
		WHERE
			workspace_builds.workspace_id = workspaces.id
	) AND

	(
		-- A workspace may be eligible for autostop if the following are true:
		--   * The provisioner job has not failed.
		--   * The workspace is not dormant.
		--   * The workspace build was a start transition.
		--   * The workspace's owner is suspended OR the workspace build deadline has passed.
		(
			provisioner_jobs.job_status != 'failed'::provisioner_job_status AND
			workspaces.dormant_at IS NULL AND
			workspace_builds.transition = 'start'::workspace_transition AND (
				users.status = 'suspended'::user_status OR (
					workspace_builds.deadline != '0001-01-01 00:00:00+00'::timestamptz AND
					workspace_builds.deadline < $1 :: timestamptz
				)
			)
		) OR

		-- A workspace may be eligible for autostart if the following are true:
		--   * The workspace's owner is active.
		--   * The provisioner job did not fail.
		--   * The workspace build was a stop transition.
		--   * The workspace is not dormant
		--   * The workspace has an autostart schedule.
		--   * It is after the workspace's next start time.
		(
			users.status = 'active'::user_status AND
			provisioner_jobs.job_status != 'failed'::provisioner_job_status AND
			workspace_builds.transition = 'stop'::workspace_transition AND
			workspaces.dormant_at IS NULL AND
			workspaces.autostart_schedule IS NOT NULL AND
			(
				-- next_start_at might be null in these two scenarios:
				--   * A coder instance was updated and we haven't updated next_start_at yet.
				--   * A database trigger made it null because of an update to a related column.
				--
				-- When this occurs, we return the workspace so the Coder server can
				-- compute a valid next start at and update it.
				workspaces.next_start_at IS NULL OR
				workspaces.next_start_at <= $1 :: timestamptz
			)
		) OR

		-- A workspace may be eligible for dormant stop if the following are true:
		--   * The workspace is not dormant.
		--   * The template has set a time 'til dormant.
		--   * The workspace has been unused for longer than the time 'til dormancy.
		(
			workspaces.dormant_at IS NULL AND
			templates.time_til_dormant > 0 AND
			($1 :: timestamptz) - workspaces.last_used_at > (INTERVAL '1 millisecond' * (templates.time_til_dormant / 1000000))
		) OR

		-- A workspace may be eligible for deletion if the following are true:
		--   * The workspace is dormant.
		--   * The workspace is scheduled to be deleted.
		--   * If there was a prior attempt to delete the workspace that failed:
		--      * This attempt was at least 24 hours ago.
		(
			workspaces.dormant_at IS NOT NULL AND
			workspaces.deleting_at IS NOT NULL AND
			workspaces.deleting_at < $1 :: timestamptz AND
			templates.time_til_dormant_autodelete > 0 AND
			CASE
				WHEN (
					workspace_builds.transition = 'delete'::workspace_transition AND
					provisioner_jobs.job_status = 'failed'::provisioner_job_status
				) THEN (
					(
						provisioner_jobs.canceled_at IS NOT NULL OR
						provisioner_jobs.completed_at IS NOT NULL
					) AND (
						($1 :: timestamptz) - (CASE
							WHEN provisioner_jobs.canceled_at IS NOT NULL THEN provisioner_jobs.canceled_at
							ELSE provisioner_jobs.completed_at
						END) > INTERVAL '24 hours'
					)
				)
				ELSE true
			END
		) OR

		-- A workspace may be eligible for failed stop if the following are true:
		--   * The template has a failure ttl set.
		--   * The workspace build was a start transition.
		--   * The provisioner job failed.
		--   * The provisioner job had completed.
		--   * The provisioner job has been completed for longer than the failure ttl.
		(
			templates.failure_ttl > 0 AND
			workspace_builds.transition = 'start'::workspace_transition AND
			provisioner_jobs.job_status = 'failed'::provisioner_job_status AND
			provisioner_jobs.completed_at IS NOT NULL AND
			($1 :: timestamptz) - provisioner_jobs.completed_at > (INTERVAL '1 millisecond' * (templates.failure_ttl / 1000000))
		)
	)
  	AND workspaces.deleted = 'false'
  	-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
	-- should not be considered by the lifecycle executor, as they are handled by the
	-- prebuilds reconciliation loop.
  	AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`

type GetWorkspacesEligibleForTransitionRow struct {
	ID                     uuid.UUID     `db:"id" json:"id"`
	Name                   string        `db:"name" json:"name"`
	BuildTemplateVersionID uuid.NullUUID `db:"build_template_version_id" json:"build_template_version_id"`
}

func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspacesEligibleForTransition, now)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspacesEligibleForTransitionRow
	for rows.Next() {
		var i GetWorkspacesEligibleForTransitionRow
		if err := rows.Scan(&i.ID, &i.Name, &i.BuildTemplateVersionID); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getWorkspacesForWorkspaceMetrics = `-- name: GetWorkspacesForWorkspaceMetrics :many
SELECT
    u.username as owner_username,
    t.name as template_name,
    tv.name as template_version_name,
    pj.job_status as latest_build_status,
    wb.transition as latest_build_transition
FROM workspaces w
JOIN users u ON w.owner_id = u.id
JOIN templates t ON w.template_id = t.id
JOIN workspace_builds wb ON w.id = wb.workspace_id
JOIN provisioner_jobs pj ON wb.job_id = pj.id
LEFT JOIN template_versions tv ON wb.template_version_id = tv.id
WHERE w.deleted = false
AND wb.build_number = (
    SELECT MAX(wb2.build_number)
    FROM workspace_builds wb2
    WHERE wb2.workspace_id = w.id
)
`

type GetWorkspacesForWorkspaceMetricsRow struct {
	OwnerUsername         string               `db:"owner_username" json:"owner_username"`
	TemplateName          string               `db:"template_name" json:"template_name"`
	TemplateVersionName   sql.NullString       `db:"template_version_name" json:"template_version_name"`
	LatestBuildStatus     ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"`
	LatestBuildTransition WorkspaceTransition  `db:"latest_build_transition" json:"latest_build_transition"`
}

func (q *sqlQuerier) GetWorkspacesForWorkspaceMetrics(ctx context.Context) ([]GetWorkspacesForWorkspaceMetricsRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspacesForWorkspaceMetrics)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspacesForWorkspaceMetricsRow
	for rows.Next() {
		var i GetWorkspacesForWorkspaceMetricsRow
		if err := rows.Scan(
			&i.OwnerUsername,
			&i.TemplateName,
			&i.TemplateVersionName,
			&i.LatestBuildStatus,
			&i.LatestBuildTransition,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspace = `-- name: InsertWorkspace :one
INSERT INTO
	workspaces (
		id,
		created_at,
		updated_at,
		owner_id,
		organization_id,
		template_id,
		name,
		autostart_schedule,
		ttl,
		last_used_at,
		automatic_updates,
		next_start_at
	)
VALUES
	($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl
`

type InsertWorkspaceParams struct {
	ID                uuid.UUID        `db:"id" json:"id"`
	CreatedAt         time.Time        `db:"created_at" json:"created_at"`
	UpdatedAt         time.Time        `db:"updated_at" json:"updated_at"`
	OwnerID           uuid.UUID        `db:"owner_id" json:"owner_id"`
	OrganizationID    uuid.UUID        `db:"organization_id" json:"organization_id"`
	TemplateID        uuid.UUID        `db:"template_id" json:"template_id"`
	Name              string           `db:"name" json:"name"`
	AutostartSchedule sql.NullString   `db:"autostart_schedule" json:"autostart_schedule"`
	Ttl               sql.NullInt64    `db:"ttl" json:"ttl"`
	LastUsedAt        time.Time        `db:"last_used_at" json:"last_used_at"`
	AutomaticUpdates  AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
	NextStartAt       sql.NullTime     `db:"next_start_at" json:"next_start_at"`
}

func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (WorkspaceTable, error) {
	row := q.db.QueryRowContext(ctx, insertWorkspace,
		arg.ID,
		arg.CreatedAt,
		arg.UpdatedAt,
		arg.OwnerID,
		arg.OrganizationID,
		arg.TemplateID,
		arg.Name,
		arg.AutostartSchedule,
		arg.Ttl,
		arg.LastUsedAt,
		arg.AutomaticUpdates,
		arg.NextStartAt,
	)
	var i WorkspaceTable
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OwnerID,
		&i.OrganizationID,
		&i.TemplateID,
		&i.Deleted,
		&i.Name,
		&i.AutostartSchedule,
		&i.Ttl,
		&i.LastUsedAt,
		&i.DormantAt,
		&i.DeletingAt,
		&i.AutomaticUpdates,
		&i.Favorite,
		&i.NextStartAt,
		&i.GroupACL,
		&i.UserACL,
	)
	return i, err
}

const unfavoriteWorkspace = `-- name: UnfavoriteWorkspace :exec
UPDATE workspaces SET favorite = false WHERE id = $1
`

func (q *sqlQuerier) UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
	_, err := q.db.ExecContext(ctx, unfavoriteWorkspace, id)
	return err
}

const updateTemplateWorkspacesLastUsedAt = `-- name: UpdateTemplateWorkspacesLastUsedAt :exec
UPDATE workspaces
SET
	last_used_at = $1::timestamptz
WHERE
	template_id = $2
`

type UpdateTemplateWorkspacesLastUsedAtParams struct {
	LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
	TemplateID uuid.UUID `db:"template_id" json:"template_id"`
}

func (q *sqlQuerier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error {
	_, err := q.db.ExecContext(ctx, updateTemplateWorkspacesLastUsedAt, arg.LastUsedAt, arg.TemplateID)
	return err
}

const updateWorkspace = `-- name: UpdateWorkspace :one
UPDATE
	workspaces
SET
	name = $2
WHERE
	id = $1
	AND deleted = false
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl
`

type UpdateWorkspaceParams struct {
	ID   uuid.UUID `db:"id" json:"id"`
	Name string    `db:"name" json:"name"`
}

func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error) {
	row := q.db.QueryRowContext(ctx, updateWorkspace, arg.ID, arg.Name)
	var i WorkspaceTable
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OwnerID,
		&i.OrganizationID,
		&i.TemplateID,
		&i.Deleted,
		&i.Name,
		&i.AutostartSchedule,
		&i.Ttl,
		&i.LastUsedAt,
		&i.DormantAt,
		&i.DeletingAt,
		&i.AutomaticUpdates,
		&i.Favorite,
		&i.NextStartAt,
		&i.GroupACL,
		&i.UserACL,
	)
	return i, err
}

const updateWorkspaceACLByID = `-- name: UpdateWorkspaceACLByID :exec
UPDATE
	workspaces
SET
	group_acl = $1,
	user_acl = $2
WHERE
	id = $3
`

type UpdateWorkspaceACLByIDParams struct {
	GroupACL WorkspaceACL `db:"group_acl" json:"group_acl"`
	UserACL  WorkspaceACL `db:"user_acl" json:"user_acl"`
	ID       uuid.UUID    `db:"id" json:"id"`
}

func (q *sqlQuerier) UpdateWorkspaceACLByID(ctx context.Context, arg UpdateWorkspaceACLByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceACLByID, arg.GroupACL, arg.UserACL, arg.ID)
	return err
}

const updateWorkspaceAutomaticUpdates = `-- name: UpdateWorkspaceAutomaticUpdates :exec
UPDATE
	workspaces
SET
	automatic_updates = $2
WHERE
		id = $1
`

type UpdateWorkspaceAutomaticUpdatesParams struct {
	ID               uuid.UUID        `db:"id" json:"id"`
	AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
}

func (q *sqlQuerier) UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg UpdateWorkspaceAutomaticUpdatesParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAutomaticUpdates, arg.ID, arg.AutomaticUpdates)
	return err
}

const updateWorkspaceAutostart = `-- name: UpdateWorkspaceAutostart :exec
UPDATE
	workspaces
SET
	autostart_schedule = $2,
	next_start_at = $3
WHERE
	id = $1
	-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
  	-- are managed by the reconciliation loop, not the lifecycle executor which handles
  	-- autostart_schedule and next_start_at
  	AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`

type UpdateWorkspaceAutostartParams struct {
	ID                uuid.UUID      `db:"id" json:"id"`
	AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
	NextStartAt       sql.NullTime   `db:"next_start_at" json:"next_start_at"`
}

func (q *sqlQuerier) UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceAutostart, arg.ID, arg.AutostartSchedule, arg.NextStartAt)
	return err
}

const updateWorkspaceDeletedByID = `-- name: UpdateWorkspaceDeletedByID :exec
UPDATE
	workspaces
SET
	deleted = $2
WHERE
	id = $1
`

type UpdateWorkspaceDeletedByIDParams struct {
	ID      uuid.UUID `db:"id" json:"id"`
	Deleted bool      `db:"deleted" json:"deleted"`
}

func (q *sqlQuerier) UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceDeletedByID, arg.ID, arg.Deleted)
	return err
}

const updateWorkspaceDormantDeletingAt = `-- name: UpdateWorkspaceDormantDeletingAt :one
UPDATE
    workspaces
SET
    dormant_at = $2,
    -- When a workspace is active we want to update the last_used_at to avoid the workspace going
    -- immediately dormant. If we're transition the workspace to dormant then we leave it alone.
    last_used_at = CASE WHEN $2::timestamptz IS NULL THEN
        now() at time zone 'utc'
    ELSE
        last_used_at
    END,
    -- If dormant_at is null (meaning active) or the template-defined time_til_dormant_autodelete is 0 we should set
    -- deleting_at to NULL else set it to the dormant_at + time_til_dormant_autodelete duration.
    deleting_at = CASE WHEN $2::timestamptz IS NULL OR templates.time_til_dormant_autodelete = 0 THEN
        NULL
    ELSE
        $2::timestamptz + (INTERVAL '1 millisecond' * (templates.time_til_dormant_autodelete / 1000000))
    END
FROM
    templates
WHERE
    workspaces.id = $1
    AND templates.id = workspaces.template_id
	-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
	-- are managed by the reconciliation loop, not the lifecycle executor which handles
	-- dormant_at and deleting_at
	AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
RETURNING
    workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl
`

type UpdateWorkspaceDormantDeletingAtParams struct {
	ID        uuid.UUID    `db:"id" json:"id"`
	DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
}

func (q *sqlQuerier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (WorkspaceTable, error) {
	row := q.db.QueryRowContext(ctx, updateWorkspaceDormantDeletingAt, arg.ID, arg.DormantAt)
	var i WorkspaceTable
	err := row.Scan(
		&i.ID,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.OwnerID,
		&i.OrganizationID,
		&i.TemplateID,
		&i.Deleted,
		&i.Name,
		&i.AutostartSchedule,
		&i.Ttl,
		&i.LastUsedAt,
		&i.DormantAt,
		&i.DeletingAt,
		&i.AutomaticUpdates,
		&i.Favorite,
		&i.NextStartAt,
		&i.GroupACL,
		&i.UserACL,
	)
	return i, err
}

const updateWorkspaceLastUsedAt = `-- name: UpdateWorkspaceLastUsedAt :exec
UPDATE
	workspaces
SET
	last_used_at = $2
WHERE
	id = $1
`

type UpdateWorkspaceLastUsedAtParams struct {
	ID         uuid.UUID `db:"id" json:"id"`
	LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
}

func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceLastUsedAt, arg.ID, arg.LastUsedAt)
	return err
}

const updateWorkspaceNextStartAt = `-- name: UpdateWorkspaceNextStartAt :exec
UPDATE
	workspaces
SET
	next_start_at = $2
WHERE
	id = $1
	-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
	-- are managed by the reconciliation loop, not the lifecycle executor which handles
	-- next_start_at
	AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`

type UpdateWorkspaceNextStartAtParams struct {
	ID          uuid.UUID    `db:"id" json:"id"`
	NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
}

func (q *sqlQuerier) UpdateWorkspaceNextStartAt(ctx context.Context, arg UpdateWorkspaceNextStartAtParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceNextStartAt, arg.ID, arg.NextStartAt)
	return err
}

const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec
UPDATE
	workspaces
SET
	ttl = $2
WHERE
	id = $1
	-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
	-- are managed by the reconciliation loop, not the lifecycle executor which handles
	-- ttl
	AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`

type UpdateWorkspaceTTLParams struct {
	ID  uuid.UUID     `db:"id" json:"id"`
	Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
}

func (q *sqlQuerier) UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspaceTTL, arg.ID, arg.Ttl)
	return err
}

const updateWorkspacesDormantDeletingAtByTemplateID = `-- name: UpdateWorkspacesDormantDeletingAtByTemplateID :many
UPDATE workspaces
SET
    deleting_at = CASE
        WHEN $1::bigint = 0 THEN NULL
        WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN ($2::timestamptz) + interval '1 milliseconds' * $1::bigint
        ELSE dormant_at + interval '1 milliseconds' * $1::bigint
    END,
    dormant_at = CASE WHEN $2::timestamptz > '0001-01-01 00:00:00+00'::timestamptz THEN $2::timestamptz ELSE dormant_at END
WHERE
    template_id = $3
	AND dormant_at IS NOT NULL
	AND deleted = false
	-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
	-- should not have their dormant or deleting at set, as these are handled by the
    -- prebuilds reconciliation loop.
	AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl
`

type UpdateWorkspacesDormantDeletingAtByTemplateIDParams struct {
	TimeTilDormantAutodeleteMs int64     `db:"time_til_dormant_autodelete_ms" json:"time_til_dormant_autodelete_ms"`
	DormantAt                  time.Time `db:"dormant_at" json:"dormant_at"`
	TemplateID                 uuid.UUID `db:"template_id" json:"template_id"`
}

func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) ([]WorkspaceTable, error) {
	rows, err := q.db.QueryContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceTable
	for rows.Next() {
		var i WorkspaceTable
		if err := rows.Scan(
			&i.ID,
			&i.CreatedAt,
			&i.UpdatedAt,
			&i.OwnerID,
			&i.OrganizationID,
			&i.TemplateID,
			&i.Deleted,
			&i.Name,
			&i.AutostartSchedule,
			&i.Ttl,
			&i.LastUsedAt,
			&i.DormantAt,
			&i.DeletingAt,
			&i.AutomaticUpdates,
			&i.Favorite,
			&i.NextStartAt,
			&i.GroupACL,
			&i.UserACL,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const updateWorkspacesTTLByTemplateID = `-- name: UpdateWorkspacesTTLByTemplateID :exec
UPDATE
	workspaces
SET
	ttl = $2
WHERE
	template_id = $1
	-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
	-- should not have their TTL updated, as they are handled by the prebuilds
	-- reconciliation loop.
	AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`

type UpdateWorkspacesTTLByTemplateIDParams struct {
	TemplateID uuid.UUID     `db:"template_id" json:"template_id"`
	Ttl        sql.NullInt64 `db:"ttl" json:"ttl"`
}

func (q *sqlQuerier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg UpdateWorkspacesTTLByTemplateIDParams) error {
	_, err := q.db.ExecContext(ctx, updateWorkspacesTTLByTemplateID, arg.TemplateID, arg.Ttl)
	return err
}

const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many
SELECT
	DISTINCT ON (workspace_agent_scripts.id) workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds, workspace_agent_scripts.display_name, workspace_agent_scripts.id,
	workspace_agent_script_timings.exit_code,
	workspace_agent_script_timings.status
	FROM workspace_agent_scripts
	LEFT JOIN workspace_agent_script_timings
		ON workspace_agent_script_timings.script_id = workspace_agent_scripts.id
	WHERE workspace_agent_scripts.workspace_agent_id = ANY($1 :: uuid [ ])
	ORDER BY workspace_agent_scripts.id, workspace_agent_script_timings.started_at
	DESC NULLS LAST
`

type GetWorkspaceAgentScriptsByAgentIDsRow struct {
	WorkspaceAgentID uuid.UUID                            `db:"workspace_agent_id" json:"workspace_agent_id"`
	LogSourceID      uuid.UUID                            `db:"log_source_id" json:"log_source_id"`
	LogPath          string                               `db:"log_path" json:"log_path"`
	CreatedAt        time.Time                            `db:"created_at" json:"created_at"`
	Script           string                               `db:"script" json:"script"`
	Cron             string                               `db:"cron" json:"cron"`
	StartBlocksLogin bool                                 `db:"start_blocks_login" json:"start_blocks_login"`
	RunOnStart       bool                                 `db:"run_on_start" json:"run_on_start"`
	RunOnStop        bool                                 `db:"run_on_stop" json:"run_on_stop"`
	TimeoutSeconds   int32                                `db:"timeout_seconds" json:"timeout_seconds"`
	DisplayName      string                               `db:"display_name" json:"display_name"`
	ID               uuid.UUID                            `db:"id" json:"id"`
	ExitCode         sql.NullInt32                        `db:"exit_code" json:"exit_code"`
	Status           NullWorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
}

func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]GetWorkspaceAgentScriptsByAgentIDsRow, error) {
	rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetWorkspaceAgentScriptsByAgentIDsRow
	for rows.Next() {
		var i GetWorkspaceAgentScriptsByAgentIDsRow
		if err := rows.Scan(
			&i.WorkspaceAgentID,
			&i.LogSourceID,
			&i.LogPath,
			&i.CreatedAt,
			&i.Script,
			&i.Cron,
			&i.StartBlocksLogin,
			&i.RunOnStart,
			&i.RunOnStop,
			&i.TimeoutSeconds,
			&i.DisplayName,
			&i.ID,
			&i.ExitCode,
			&i.Status,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many
INSERT INTO
	workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds, display_name, id)
SELECT
	$1 :: uuid AS workspace_agent_id,
	$2 :: timestamptz AS created_at,
	unnest($3 :: uuid [ ]) AS log_source_id,
	unnest($4 :: text [ ]) AS log_path,
	unnest($5 :: text [ ]) AS script,
	unnest($6 :: text [ ]) AS cron,
	unnest($7 :: boolean [ ]) AS start_blocks_login,
	unnest($8 :: boolean [ ]) AS run_on_start,
	unnest($9 :: boolean [ ]) AS run_on_stop,
	unnest($10 :: integer [ ]) AS timeout_seconds,
	unnest($11 :: text [ ]) AS display_name,
	unnest($12 :: uuid [ ]) AS id
RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds, workspace_agent_scripts.display_name, workspace_agent_scripts.id
`

type InsertWorkspaceAgentScriptsParams struct {
	WorkspaceAgentID uuid.UUID   `db:"workspace_agent_id" json:"workspace_agent_id"`
	CreatedAt        time.Time   `db:"created_at" json:"created_at"`
	LogSourceID      []uuid.UUID `db:"log_source_id" json:"log_source_id"`
	LogPath          []string    `db:"log_path" json:"log_path"`
	Script           []string    `db:"script" json:"script"`
	Cron             []string    `db:"cron" json:"cron"`
	StartBlocksLogin []bool      `db:"start_blocks_login" json:"start_blocks_login"`
	RunOnStart       []bool      `db:"run_on_start" json:"run_on_start"`
	RunOnStop        []bool      `db:"run_on_stop" json:"run_on_stop"`
	TimeoutSeconds   []int32     `db:"timeout_seconds" json:"timeout_seconds"`
	DisplayName      []string    `db:"display_name" json:"display_name"`
	ID               []uuid.UUID `db:"id" json:"id"`
}

func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) {
	rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts,
		arg.WorkspaceAgentID,
		arg.CreatedAt,
		pq.Array(arg.LogSourceID),
		pq.Array(arg.LogPath),
		pq.Array(arg.Script),
		pq.Array(arg.Cron),
		pq.Array(arg.StartBlocksLogin),
		pq.Array(arg.RunOnStart),
		pq.Array(arg.RunOnStop),
		pq.Array(arg.TimeoutSeconds),
		pq.Array(arg.DisplayName),
		pq.Array(arg.ID),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []WorkspaceAgentScript
	for rows.Next() {
		var i WorkspaceAgentScript
		if err := rows.Scan(
			&i.WorkspaceAgentID,
			&i.LogSourceID,
			&i.LogPath,
			&i.CreatedAt,
			&i.Script,
			&i.Cron,
			&i.StartBlocksLogin,
			&i.RunOnStart,
			&i.RunOnStop,
			&i.TimeoutSeconds,
			&i.DisplayName,
			&i.ID,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}
