/**
 * @module queue
 *
 * Queue service for publishing messages to Agentuity queues.
 *
 * Used internally by `@agentuity/queue`'s `QueueClient`. App code should
 * use `QueueClient` directly rather than reaching for these primitives.
 *
 * @example
 * ```typescript
 * import { QueueClient } from '@agentuity/queue';
 *
 * const queue = new QueueClient();
 * const result = await queue.publish('order-queue', {
 *   orderId: 123,
 *   action: 'process',
 * });
 * console.log(`Published message ${result.id}`);
 * ```
 */
import { buildUrl, toPayload, toServiceException } from '@agentuity/adapter';
import { StructuredError } from '@agentuity/adapter';
import { z } from 'zod';
/**
 * Parameters for publishing a message to a queue.
 *
 * @example
 * ```typescript
 * const params: QueuePublishParams = {
 *   metadata: { priority: 'high' },
 *   partitionKey: 'customer-123',
 *   idempotencyKey: 'order-456-v1',
 *   ttl: 3600, // 1 hour
 * };
 * ```
 */
export const QueuePublishParamsSchema = z.object({
    /**
     * Optional metadata to attach to the message.
     * Can contain any JSON-serializable data for message routing or filtering.
     */
    metadata: z
        .record(z.string(), z.unknown())
        .optional()
        .describe('Optional metadata to attach to the message.'),
    /**
     * Optional partition key for message ordering.
     * Messages with the same partition key are guaranteed to be processed in order.
     */
    partitionKey: z.string().optional().describe('Optional partition key for message ordering.'),
    /**
     * Optional idempotency key for deduplication.
     * If a message with the same key was recently published, it will be deduplicated.
     */
    idempotencyKey: z.string().optional().describe('Optional idempotency key for deduplication.'),
    /**
     * Optional time-to-live in seconds.
     * Messages will expire and be removed after this duration.
     */
    ttl: z.number().optional().describe('Optional time-to-live in seconds.'),
    /**
     * Optional project ID for cross-project publishing.
     * If not specified, uses the current project context.
     */
    projectId: z.string().optional().describe('Optional project ID for cross-project publishing.'),
    /**
     * Optional agent ID for attribution.
     * If not specified, uses the current agent context.
     */
    agentId: z.string().optional().describe('Optional agent ID for attribution.'),
    /**
     * Whether to publish synchronously.
     * When true, the API waits for the message to be fully persisted before returning.
     * When false (default), the API returns immediately with a pending message.
     */
    sync: z.boolean().optional().describe('Whether to publish synchronously.'),
});
/**
 * Result of publishing a message to a queue.
 *
 * @example
 * ```typescript
 * const result = await queue.publish('my-queue', payload);
 * console.log(`Message ${result.id} published at offset ${result.offset}`);
 * ```
 */
export const QueuePublishResultSchema = z.object({
    /**
     * The unique message ID (prefixed with msg_).
     * Use this ID to track, acknowledge, or delete the message.
     */
    id: z.string().describe('The unique message ID (prefixed with msg_).'),
    /**
     * The sequential offset of the message in the queue.
     * Offsets are monotonically increasing and can be used for log-style consumption.
     */
    offset: z.number().describe('The sequential offset of the message in the queue.'),
    /**
     * ISO 8601 timestamp when the message was published.
     */
    publishedAt: z.string().describe('ISO 8601 timestamp when the message was published.'),
});
/**
 * Parameters for creating a queue.
 *
 * @example
 * ```typescript
 * const result = await queue.createQueue('my-queue', {
 *   queueType: 'pubsub',
 *   settings: { defaultTtlSeconds: 86400 },
 * });
 * ```
 */
export const QueueCreateParamsSchema = z.object({
    /**
     * Type of queue to create.
     * - `worker`: Messages are consumed by exactly one consumer with acknowledgment.
     * - `pubsub`: Messages are broadcast to all subscribers.
     * @default 'worker'
     */
    queueType: z.enum(['worker', 'pubsub']).optional().describe('Type of queue to create.'),
    /**
     * Optional description of the queue's purpose.
     */
    description: z.string().optional().describe("Optional description of the queue's purpose."),
    /**
     * Optional settings to customize queue behavior.
     * Only provided fields are applied; others use server defaults.
     */
    settings: z
        .object({
        /** Default time-to-live for messages in seconds. Null means no expiration. */
        defaultTtlSeconds: z
            .number()
            .nullable()
            .optional()
            .describe('Default time-to-live for messages in seconds. Null means no expiration.'),
        /** Time in seconds a message is invisible after being received. */
        defaultVisibilityTimeoutSeconds: z
            .number()
            .optional()
            .describe('Time in seconds a message is invisible after being received.'),
        /** Maximum number of delivery attempts before moving to DLQ. */
        defaultMaxRetries: z
            .number()
            .optional()
            .describe('Maximum number of delivery attempts before moving to DLQ.'),
        /** Maximum number of messages a single client can process concurrently. */
        maxInFlightPerClient: z
            .number()
            .optional()
            .describe('Maximum number of messages a single client can process concurrently.'),
        /** Retention period for acknowledged messages in seconds. */
        retentionSeconds: z
            .number()
            .optional()
            .describe('Retention period for acknowledged messages in seconds.'),
    })
        .optional()
        .describe('Optional settings to customize queue behavior.'),
});
/**
 * Result of creating a queue.
 */
export const QueueCreateResultSchema = z.object({
    /** The queue name. */
    name: z.string().describe('The queue name.'),
    /** The queue type ('worker' or 'pubsub'). */
    queueType: z.string().describe("The queue type ('worker' or 'pubsub')."),
});
// ============================================================================
// Errors
// ============================================================================
/**
 * Error thrown when a publish operation fails.
 *
 * This is a general error for publish failures that aren't specifically
 * validation or not-found errors.
 */
export const QueuePublishError = StructuredError('QueuePublishError');
/**
 * Error thrown when a queue creation operation fails.
 *
 * This is a general error for create failures that aren't specifically
 * validation, not-found, or conflict (409) errors.
 */
export const QueueCreateError = StructuredError('QueueCreateError');
/**
 * Error thrown when a queue is not found.
 *
 * @example
 * ```typescript
 * try {
 *   await queue.publish('non-existent', 'payload');
 * } catch (error) {
 *   if (error instanceof QueueNotFoundError) {
 *     console.error('Queue does not exist');
 *   }
 * }
 * ```
 */
export const QueueNotFoundError = StructuredError('QueueNotFoundError');
/**
 * Error thrown when validation fails.
 *
 * Contains the field name and optionally the invalid value for debugging.
 */
export const QueueValidationError = StructuredError('QueueValidationError')();
// ============================================================================
// Internal Validation
// ============================================================================
const MAX_QUEUE_NAME_LENGTH = 256;
const MAX_PAYLOAD_SIZE = 1048576;
const MAX_PARTITION_KEY_LENGTH = 256;
const MAX_IDEMPOTENCY_KEY_LENGTH = 256;
const VALID_QUEUE_NAME_REGEX = /^[a-z_][a-z0-9_-]*$/;
/** @internal */
function validateQueueNameInternal(name) {
    if (!name || name.length === 0) {
        throw new QueueValidationError({
            message: 'Queue name cannot be empty',
            field: 'queueName',
            value: name,
        });
    }
    if (name.length > MAX_QUEUE_NAME_LENGTH) {
        throw new QueueValidationError({
            message: `Queue name must not exceed ${MAX_QUEUE_NAME_LENGTH} characters`,
            field: 'queueName',
            value: name,
        });
    }
    if (!VALID_QUEUE_NAME_REGEX.test(name)) {
        throw new QueueValidationError({
            message: 'Queue name must start with a letter or underscore and contain only lowercase letters, digits, underscores, and hyphens',
            field: 'queueName',
            value: name,
        });
    }
}
/** @internal */
function validatePayloadInternal(payload) {
    if (!payload || payload.length === 0) {
        throw new QueueValidationError({
            message: 'Payload cannot be empty',
            field: 'payload',
        });
    }
    if (payload.length > MAX_PAYLOAD_SIZE) {
        throw new QueueValidationError({
            message: `Payload size exceeds ${MAX_PAYLOAD_SIZE} byte limit (${payload.length} bytes)`,
            field: 'payload',
            value: payload.length,
        });
    }
}
/**
 * Unwraps the standard API response envelope.
 *
 * The queue server returns responses wrapped in:
 *   { success: true, data: { [key]: { ...actual data... } } }
 *
 * Since `fromResponse()` parses the raw JSON body, `res.data` from the
 * adapter is the full envelope. This helper extracts the nested data by key.
 *
 * Falls back to treating the input as already-unwrapped data (e.g., in tests
 * using mock adapters that provide flat data directly).
 *
 * @internal
 */
function unwrapApiResponse(data, key) {
    if (data !== null && typeof data === 'object') {
        const body = data;
        if (body.success === true && typeof body.data === 'object' && body.data !== null) {
            const envelope = body.data;
            if (key in envelope) {
                return envelope[key];
            }
        }
    }
    return data;
}
// ============================================================================
// QueueStorageService Implementation
// ============================================================================
/**
 * HTTP-based implementation of the {@link QueueService} interface.
 *
 * This service communicates with the Agentuity Queue API to publish
 * messages. App code should use `QueueClient` from `@agentuity/queue`
 * rather than instantiating this class directly.
 */
export class QueueStorageService {
    #adapter;
    #baseUrl;
    #knownQueues = new Set();
    /**
     * Creates a new QueueStorageService.
     *
     * @param baseUrl - The base URL of the Queue API
     * @param adapter - The fetch adapter for making HTTP requests
     */
    constructor(baseUrl, adapter) {
        this.#adapter = adapter;
        this.#baseUrl = baseUrl;
    }
    /**
     * @inheritdoc
     */
    async publish(queueName, payload, params) {
        // Validate inputs before sending to API
        validateQueueNameInternal(queueName);
        const [body] = await toPayload(payload);
        const payloadStr = typeof payload === 'string' ? payload : body;
        validatePayloadInternal(payloadStr);
        // Validate optional params
        if (params?.partitionKey && params.partitionKey.length > MAX_PARTITION_KEY_LENGTH) {
            throw new QueueValidationError({
                message: `Partition key must not exceed ${MAX_PARTITION_KEY_LENGTH} characters`,
                field: 'partitionKey',
                value: params.partitionKey.length,
            });
        }
        if (params?.idempotencyKey && params.idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
            throw new QueueValidationError({
                message: `Idempotency key must not exceed ${MAX_IDEMPOTENCY_KEY_LENGTH} characters`,
                field: 'idempotencyKey',
                value: params.idempotencyKey.length,
            });
        }
        if (params?.ttl !== undefined && params.ttl < 0) {
            throw new QueueValidationError({
                message: 'TTL cannot be negative',
                field: 'ttl',
                value: params.ttl,
            });
        }
        const basePath = `/queue/messages/publish/${encodeURIComponent(queueName)}`;
        const url = buildUrl(this.#baseUrl, params?.sync ? `${basePath}?sync=true` : basePath);
        const requestBody = {
            payload: typeof payload === 'string' ? payload : body,
        };
        if (params?.metadata) {
            requestBody.metadata = params.metadata;
        }
        if (params?.partitionKey) {
            requestBody.partition_key = params.partitionKey;
        }
        if (params?.idempotencyKey) {
            requestBody.idempotency_key = params.idempotencyKey;
        }
        if (params?.ttl !== undefined) {
            requestBody.ttl_seconds = params.ttl;
        }
        if (params?.projectId) {
            requestBody.project_id = params.projectId;
        }
        if (params?.agentId) {
            requestBody.agent_id = params.agentId;
        }
        const signal = AbortSignal.timeout(30_000);
        const res = await this.#adapter.invoke(url, {
            method: 'POST',
            signal,
            body: JSON.stringify(requestBody),
            contentType: 'application/json',
            telemetry: {
                name: 'agentuity.queue.publish',
                attributes: {
                    queueName,
                },
            },
        });
        if (res.ok) {
            const data = unwrapApiResponse(res.data, 'message');
            const result = QueuePublishResultSchema.safeParse({
                id: data.id,
                offset: data.offset,
                publishedAt: data.published_at,
            });
            if (result.success) {
                return result.data;
            }
            throw new QueuePublishError({
                message: `Queue publish returned an unexpected response format: ${result.error.message}`,
            });
        }
        if (res.response.status === 404) {
            throw new QueueNotFoundError({
                message: `Queue not found: ${queueName}`,
            });
        }
        throw await toServiceException('POST', url, res.response);
    }
    /**
     * @inheritdoc
     */
    async createQueue(queueName, params) {
        validateQueueNameInternal(queueName);
        if (this.#knownQueues.has(queueName)) {
            return {
                name: queueName,
                queueType: params?.queueType ?? 'worker',
            };
        }
        const url = buildUrl(this.#baseUrl, '/queue/create');
        const requestBody = {
            name: queueName,
            queue_type: params?.queueType ?? 'worker',
        };
        if (params?.description !== undefined) {
            requestBody.description = params.description;
        }
        if (params?.settings) {
            const settings = {};
            if (params.settings.defaultTtlSeconds !== undefined) {
                settings.default_ttl_seconds = params.settings.defaultTtlSeconds;
            }
            if (params.settings.defaultVisibilityTimeoutSeconds !== undefined) {
                settings.default_visibility_timeout_seconds =
                    params.settings.defaultVisibilityTimeoutSeconds;
            }
            if (params.settings.defaultMaxRetries !== undefined) {
                settings.default_max_retries = params.settings.defaultMaxRetries;
            }
            if (params.settings.maxInFlightPerClient !== undefined) {
                settings.max_in_flight_per_client = params.settings.maxInFlightPerClient;
            }
            if (params.settings.retentionSeconds !== undefined) {
                settings.retention_seconds = params.settings.retentionSeconds;
            }
            if (Object.keys(settings).length > 0) {
                requestBody.settings = settings;
            }
        }
        const signal = AbortSignal.timeout(30_000);
        const res = await this.#adapter.invoke(url, {
            method: 'POST',
            signal,
            body: JSON.stringify(requestBody),
            contentType: 'application/json',
            telemetry: {
                name: 'agentuity.queue.create',
                attributes: {
                    queueName,
                },
            },
        });
        if (res.ok) {
            const data = unwrapApiResponse(res.data, 'queue');
            const result = QueueCreateResultSchema.safeParse({
                name: data.name,
                queueType: data.queue_type,
            });
            if (result.success) {
                this.#knownQueues.add(queueName);
                return result.data;
            }
            throw new QueueCreateError({
                message: `Queue create returned an unexpected response format: ${result.error.message}`,
            });
        }
        if (res.response.status === 409) {
            this.#knownQueues.add(queueName);
            return {
                name: queueName,
                queueType: params?.queueType ?? 'worker',
            };
        }
        throw await toServiceException('POST', url, res.response);
    }
    /**
     * @inheritdoc
     */
    async deleteQueue(queueName) {
        validateQueueNameInternal(queueName);
        const url = buildUrl(this.#baseUrl, `/queue/delete/${encodeURIComponent(queueName)}`);
        const signal = AbortSignal.timeout(30_000);
        const res = await this.#adapter.invoke(url, {
            method: 'DELETE',
            signal,
            telemetry: {
                name: 'agentuity.queue.delete',
                attributes: {
                    queueName,
                },
            },
        });
        if (res.ok) {
            this.#knownQueues.delete(queueName);
            return;
        }
        if (res.response.status === 404) {
            throw new QueueNotFoundError({
                message: `Queue not found: ${queueName}`,
            });
        }
        throw await toServiceException('DELETE', url, res.response);
    }
}
//# sourceMappingURL=service.js.map