/**
 * Extracts the text content from an assistant message in a Response.
 * Equivalent to OpenAI Python SDK's `response.output_text`.
 *
 * @param response - The Response object.
 * @returns The concatenated text from the first assistant message, or an empty string.
 */
export function getOutputText(response) {
    for (const item of response.output) {
        if (item.type === 'message' && item.role === 'assistant') {
            const content = item.content;
            if (typeof content === 'string')
                return content;
            if (Array.isArray(content)) {
                return content
                    .filter((p) => 'text' in p)
                    .map((p) => p.text)
                    .join('');
            }
        }
    }
    return '';
}
/**
 * Configuration settings for the Responses API client.
 * Properties use camelCase in JS and are serialized to snake_case for the API.
 */
export class ResponsesClientSettings {
    /** System-level instructions to guide the model. */
    instructions;
    temperature;
    topP;
    maxOutputTokens;
    frequencyPenalty;
    presencePenalty;
    toolChoice;
    truncation;
    parallelToolCalls;
    store;
    metadata;
    reasoning;
    text;
    seed;
    /**
     * Serializes settings into an OpenAI Responses API-compatible request object.
     * @internal
     */
    _serialize() {
        const filterUndefined = (obj) => Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined));
        const result = {
            instructions: this.instructions,
            temperature: this.temperature,
            top_p: this.topP,
            max_output_tokens: this.maxOutputTokens,
            frequency_penalty: this.frequencyPenalty,
            presence_penalty: this.presencePenalty,
            tool_choice: this.toolChoice,
            truncation: this.truncation,
            parallel_tool_calls: this.parallelToolCalls,
            store: this.store,
            metadata: this.metadata,
            reasoning: this.reasoning ? filterUndefined(this.reasoning) : undefined,
            text: this.text ? filterUndefined(this.text) : undefined,
            seed: this.seed,
        };
        // Filter out undefined properties
        return filterUndefined(result);
    }
}
/**
 * Client for the OpenAI Responses API served by Foundry Local's embedded web service.
 *
 * Unlike ChatClient/AudioClient (which use FFI via CoreInterop), the Responses API
 * is HTTP-only. This client uses fetch() for all operations and parses Server-Sent Events
 * for streaming.
 *
 * Create via `FoundryLocalManager.createResponsesClient()` or
 * `model.createResponsesClient(baseUrl)`.
 *
 * @example
 * ```typescript
 * const manager = FoundryLocalManager.create({ appName: 'MyApp' });
 * manager.startWebService();
 * const client = manager.createResponsesClient('my-model-id');
 *
 * // Non-streaming
 * const response = await client.create('Hello, world!');
 * console.log(response.output);
 *
 * // Streaming
 * await client.createStreaming('Tell me a story', (event) => {
 *     if (event.type === 'response.output_text.delta') {
 *         process.stdout.write(event.delta);
 *     }
 * });
 * ```
 */
export class ResponsesClient {
    baseUrl;
    modelId;
    /**
     * Configuration settings for responses.
     */
    settings = new ResponsesClientSettings();
    /**
     * @param baseUrl - The base URL of the Foundry Local web service (e.g. "http://127.0.0.1:5273").
     * @param modelId - Optional default model ID. Can be overridden per-request via options.
     */
    constructor(baseUrl, modelId) {
        if (!baseUrl || typeof baseUrl !== 'string' || baseUrl.trim() === '') {
            throw new Error('baseUrl must be a non-empty string.');
        }
        // Strip trailing slashes for consistent URL construction
        let url = baseUrl;
        while (url.endsWith('/')) {
            url = url.slice(0, -1);
        }
        this.baseUrl = url;
        this.modelId = modelId;
    }
    // ========================================================================
    // Public API
    // ========================================================================
    /**
     * Creates a model response (non-streaming).
     * @param input - A string prompt or array of input items.
     * @param options - Additional request parameters that override client settings.
     *   The `model` field is optional here if a default model was set in the constructor.
     * @returns The completed Response object. Check `response.status` and `response.error`
     *   even on success — the server returns HTTP 200 for model-level failures too.
     */
    async create(input, options) {
        this.validateInput(input);
        if (options?.tools) {
            this.validateTools(options.tools);
        }
        const body = this.buildRequest(input, { ...options, stream: false });
        const response = await this.fetchJson('/v1/responses', { method: 'POST', body: JSON.stringify(body) });
        return response;
    }
    /**
     * Creates a model response with streaming via Server-Sent Events.
     * @param input - A string prompt or array of input items.
     * @param callback - Called for each streaming event received.
     * @param options - Additional request parameters that override client settings.
     */
    async createStreaming(input, callback, options) {
        this.validateInput(input);
        if (options?.tools) {
            this.validateTools(options.tools);
        }
        if (!callback || typeof callback !== 'function') {
            throw new Error('Callback must be a valid function.');
        }
        const body = this.buildRequest(input, { ...options, stream: true });
        const res = await this.doFetch('/v1/responses', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
            body: JSON.stringify(body),
        });
        if (!res.body) {
            throw new Error('Streaming response has no body.');
        }
        let error = null;
        await this.parseSSEStream(res.body, (event) => {
            if (error)
                return;
            try {
                callback(event);
            }
            catch (e) {
                error = new Error(`User callback threw an error: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
            }
        });
        if (error) {
            throw error;
        }
    }
    /**
     * Retrieves a stored response by ID.
     * @param responseId - The ID of the response to retrieve.
     * @returns The Response object, or throws if not found.
     */
    async get(responseId) {
        this.validateId(responseId, 'responseId');
        return this.fetchJson(`/v1/responses/${encodeURIComponent(responseId)}`, { method: 'GET' });
    }
    /**
     * Deletes a stored response by ID.
     * @param responseId - The ID of the response to delete.
     * @returns The deletion result.
     */
    async delete(responseId) {
        this.validateId(responseId, 'responseId');
        return this.fetchJson(`/v1/responses/${encodeURIComponent(responseId)}`, { method: 'DELETE' });
    }
    /**
     * Cancels an in-progress response.
     * @param responseId - The ID of the response to cancel.
     * @returns The cancelled Response object.
     */
    async cancel(responseId) {
        this.validateId(responseId, 'responseId');
        return this.fetchJson(`/v1/responses/${encodeURIComponent(responseId)}/cancel`, { method: 'POST' });
    }
    /**
     * Retrieves input items for a stored response.
     * @param responseId - The ID of the response.
     * @returns The list of input items.
     */
    async getInputItems(responseId) {
        this.validateId(responseId, 'responseId');
        return this.fetchJson(`/v1/responses/${encodeURIComponent(responseId)}/input_items`, { method: 'GET' });
    }
    // ========================================================================
    // Internal helpers
    // ========================================================================
    /**
     * Builds the full request body by merging input, settings, and per-call options.
     */
    buildRequest(input, options) {
        const model = options?.model ?? this.modelId;
        if (!model || typeof model !== 'string' || model.trim() === '') {
            throw new Error('Model must be specified either in the constructor, via createResponsesClient(modelId), or in options.model.');
        }
        const serializedSettings = this.settings._serialize();
        // Merge order: model+input → settings defaults → per-call overrides
        return {
            model,
            input,
            ...serializedSettings,
            ...options,
        };
    }
    /**
     * Validates that input is a non-empty string or a non-empty array of items.
     */
    validateInput(input) {
        if (input === null || input === undefined) {
            throw new Error('Input cannot be null or undefined.');
        }
        if (typeof input === 'string') {
            if (input.trim() === '') {
                throw new Error('Input string cannot be empty.');
            }
            return;
        }
        if (Array.isArray(input)) {
            if (input.length === 0) {
                throw new Error('Input items array cannot be empty.');
            }
            for (const item of input) {
                if (!item || typeof item !== 'object') {
                    throw new Error('Each input item must be a non-null object.');
                }
                if (typeof item.type !== 'string' || item.type.trim() === '') {
                    throw new Error('Each input item must have a "type" property that is a non-empty string.');
                }
            }
            return;
        }
        throw new Error('Input must be a string or an array of input items.');
    }
    /**
     * Validates that tools array is properly formed.
     * Follows the same pattern as ChatClient.validateTools.
     */
    validateTools(tools) {
        if (!Array.isArray(tools)) {
            throw new Error('Tools must be an array if provided.');
        }
        for (const tool of tools) {
            if (!tool || typeof tool !== 'object' || Array.isArray(tool)) {
                throw new Error('Each tool must be a non-null object with a valid "type" and "name".');
            }
            if (tool.type !== 'function') {
                throw new Error('Each tool must have type "function".');
            }
            if (typeof tool.name !== 'string' || tool.name.trim() === '') {
                throw new Error('Each tool must have a "name" property that is a non-empty string.');
            }
        }
    }
    /**
     * Validates that a string ID parameter is non-empty and within length bounds.
     */
    validateId(id, paramName) {
        if (!id || typeof id !== 'string' || id.trim() === '') {
            throw new Error(`${paramName} must be a non-empty string.`);
        }
        if (id.length > 1024) {
            throw new Error(`${paramName} exceeds maximum length (1024).`);
        }
    }
    /**
     * Performs a fetch and parses the JSON response, handling errors.
     */
    async fetchJson(path, init) {
        const res = await this.doFetch(path, {
            ...init,
            headers: {
                'Content-Type': 'application/json',
                ...(init.headers || {}),
            },
        });
        const text = await res.text();
        try {
            return JSON.parse(text);
        }
        catch {
            throw new Error(`Failed to parse response JSON: ${text.substring(0, 200)}`);
        }
    }
    /**
     * Low-level fetch wrapper with error handling.
     */
    async doFetch(path, init) {
        const url = `${this.baseUrl}${path}`;
        let res;
        try {
            res = await fetch(url, init);
        }
        catch (e) {
            throw new Error(`Network error calling ${init.method ?? 'GET'} ${path}: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
        }
        if (!res.ok) {
            const errorText = await res.text().catch(() => res.statusText);
            throw new Error(`Responses API error (${res.status}): ${errorText}`);
        }
        return res;
    }
    /**
     * Parses a Server-Sent Events stream from the fetch response body.
     * Format: "event: {type}\ndata: {json}\n\n"
     * Terminal signal: "data: [DONE]\n\n"
     * Per SSE spec, multiple data: lines within a single event are joined with \n.
     */
    async parseSSEStream(body, onEvent) {
        const reader = body.getReader();
        const decoder = new TextDecoder();
        const bufferParts = [];
        let parseError = null;
        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done)
                    break;
                bufferParts.push(decoder.decode(value, { stream: true }));
                const buffer = bufferParts.join('');
                // Process complete SSE blocks (separated by double newlines)
                const blocks = buffer.split('\n\n');
                // Keep the last (potentially incomplete) block for next iteration
                const incomplete = blocks.pop() ?? '';
                bufferParts.length = 0;
                if (incomplete)
                    bufferParts.push(incomplete);
                for (const block of blocks) {
                    if (parseError)
                        break;
                    const trimmed = block.trim();
                    if (!trimmed)
                        continue;
                    // Check for terminal signal
                    if (trimmed === 'data: [DONE]') {
                        return;
                    }
                    // Parse SSE fields — per spec, multiple data: lines are joined with \n
                    const dataLines = [];
                    for (const line of trimmed.split('\n')) {
                        if (line.startsWith('data: ')) {
                            dataLines.push(line.slice(6));
                        }
                        else if (line === 'data:') {
                            dataLines.push('');
                        }
                        // 'event:' field is informational; the type is inside the JSON data
                    }
                    const eventData = dataLines.length > 0 ? dataLines.join('\n') : undefined;
                    if (eventData) {
                        try {
                            const parsed = JSON.parse(eventData);
                            onEvent(parsed);
                        }
                        catch (e) {
                            parseError = new Error(`Failed to parse streaming event: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
                        }
                    }
                }
            }
        }
        finally {
            reader.releaseLock();
        }
        if (parseError) {
            throw parseError;
        }
    }
}
