/**
 * API Client for Quickback Compiler Service
 *
 * Handles communication with the remote compiler API.
 */
import { getStoredToken } from './auth.js';
const API_URL = process.env.QUICKBACK_API_URL || 'https://compiler.quickback.dev';
/**
 * Exponential backoff schedule for retrying transient 500 proxy errors
 * (cold-start race with the compiler container). Values chosen to recover
 * quickly on the hot path while still smoothing out sub-second proxy hiccups.
 */
const RETRY_BACKOFF_MS = [500, 1000, 2000];
async function readErrorPayload(response) {
    const contentType = response.headers.get('content-type') || '';
    if (contentType.includes('application/json')) {
        try {
            return await response.json();
        }
        catch {
            return { error: 'Failed to parse JSON error response' };
        }
    }
    try {
        const text = await response.text();
        return { error: 'Request failed', raw: text };
    }
    catch {
        return { error: 'Request failed' };
    }
}
/**
 * Fetch available templates from the server
 */
export async function fetchTemplates() {
    const response = await fetch(`${API_URL}/templates`);
    if (!response.ok) {
        throw new Error('Failed to fetch templates');
    }
    return response.json();
}
/**
 * Call the compiler API.
 * Automatically ensures the container is ready before sending the request.
 * Pass `onStatus` to receive progress updates (e.g. for spinner text).
 */
export async function callCompiler(input, onStatus) {
    // Ensure the cloud container is warm before sending the compile payload
    await waitForReady(12, 5000, onStatus);
    onStatus?.('Compiling...');
    const apiKey = process.env.QUICKBACK_API_KEY;
    const headers = {
        'Content-Type': 'application/json',
    };
    if (apiKey) {
        // API keys use x-api-key header (Better Auth default)
        headers['x-api-key'] = apiKey;
    }
    else {
        // Session tokens (from device auth login) use Bearer header
        const token = await getStoredToken();
        if (token) {
            headers['Authorization'] = `Bearer ${token}`;
        }
    }
    const body = JSON.stringify(input);
    const maxRetries = 3;
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        // Abort if the request takes longer than 120 seconds
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 120_000);
        let response;
        try {
            // Try SSE streaming first for live progress updates.
            // X-Quickback-Stream: v2 opts into chunked file-by-file delivery so the
            // server doesn't have to JSON.stringify multi-MB SPA output in one go.
            response = await fetch(`${API_URL}/compile`, {
                method: 'POST',
                headers: { ...headers, 'Accept': 'text/event-stream', 'X-Quickback-Stream': 'v2' },
                body,
                signal: controller.signal,
            });
        }
        catch (err) {
            clearTimeout(timeout);
            if (err.name === 'AbortError') {
                throw new Error('Compilation timed out after 120 seconds. The compiler may be overloaded — please try again.');
            }
            throw err;
        }
        clearTimeout(timeout);
        if (!response.ok) {
            const error = await readErrorPayload(response);
            // Retry on transient container proxy errors (cold start race).
            // Proxy errors return non-JSON (raw HTML/text) — real compiler errors
            // are JSON. Use exponential backoff (500ms → 1s → 2s) rather than a
            // flat 5s delay so we recover quickly on hot paths and tests don't
            // blow past their default timeout.
            if (response.status === 500 && attempt < maxRetries - 1 && error.raw) {
                const backoffMs = RETRY_BACKOFF_MS[attempt] ?? 2000;
                onStatus?.('Transient error, retrying...');
                await new Promise(r => setTimeout(r, backoffMs));
                onStatus?.('Compiling...');
                continue;
            }
            if ((response.status === 401 || response.status === 403) && error.hint) {
                throw new Error(`${error.error}\n${error.hint}`);
            }
            // Friendly message for transient infrastructure errors
            if (response.status === 502 || response.status === 503 || response.status === 504) {
                const raw = error.raw ? `\n${error.raw}` : '';
                throw new Error(`Compiler returned ${response.status} ${response.statusText} — please try again.${raw}`);
            }
            // Include detailed error info for debugging.
            //
            // The server sometimes returns both a category (`error`, e.g.
            // "Compilation failed") and a specific reason (`message`, e.g. the
            // actual failing assertion). Users want both — the category tells them
            // *what kind of failure*, the message tells them *why*. We join them
            // when distinct and fall back to whichever is present.
            const category = error.error?.trim();
            const specific = error.message?.trim();
            const errorMsg = category && specific && category !== specific
                ? `${category}: ${specific}`
                : specific || category || 'Compilation failed';
            const details = error.details ? `\nDetails: ${JSON.stringify(error.details, null, 2)}` : '';
            const stderr = error.stderr ? `\nStderr: ${error.stderr}` : '';
            const stdout = error.stdout ? `\nStdout: ${error.stdout}` : '';
            const raw = error.raw ? `\nResponse: ${error.raw}` : '';
            const status = `\nStatus: ${response.status} ${response.statusText}`;
            throw new Error(errorMsg + status + details + stderr + stdout + raw);
        }
        const contentType = response.headers.get('content-type') || '';
        // Handle SSE streaming response
        if (contentType.includes('text/event-stream') && response.body) {
            return consumeSSEStream(response.body, onStatus);
        }
        // Fallback: plain JSON response (server doesn't support streaming)
        return response.json();
    }
    throw new Error('Compilation failed after retries');
}
/**
 * Check API health
 */
export async function checkHealth() {
    const response = await fetch(`${API_URL}/health`);
    if (!response.ok) {
        throw new Error('Compiler service unavailable');
    }
    return response.json();
}
/**
 * Consume an SSE stream from the compiler, forwarding progress events to onStatus
 * and returning the final CompilerOutput when the 'result' event arrives.
 */
async function consumeSSEStream(body, onStatus) {
    const reader = body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    let currentEvent = '';
    // v1 path: a single `result` event arrives with everything.
    // v2 path: `meta` (optional) + N `file` events + a final `done`.
    // We accept either so a new CLI works against an old server and vice versa.
    let result = null;
    const v2Files = [];
    let v2Expected = 0;
    let v2Done = null;
    while (true) {
        const { done, value } = await reader.read();
        if (done)
            break;
        buffer += decoder.decode(value, { stream: true });
        // Parse SSE events from buffer
        const lines = buffer.split('\n');
        buffer = lines.pop() || ''; // Keep incomplete line in buffer
        for (const line of lines) {
            if (line.startsWith('event: ')) {
                currentEvent = line.slice(7).trim();
            }
            else if (line.startsWith('data: ')) {
                const data = line.slice(6);
                try {
                    const parsed = JSON.parse(data);
                    if (currentEvent === 'progress') {
                        const secs = (parsed.elapsedMs / 1000).toFixed(1);
                        onStatus?.(`${parsed.message} (${secs}s)`);
                    }
                    else if (currentEvent === 'meta') {
                        v2Expected = parsed.fileCount || 0;
                    }
                    else if (currentEvent === 'file') {
                        v2Files.push(parsed);
                        if (v2Expected > 0) {
                            onStatus?.(`Receiving files (${v2Files.length}/${v2Expected})...`);
                        }
                    }
                    else if (currentEvent === 'done') {
                        v2Done = parsed;
                    }
                    else if (currentEvent === 'result') {
                        result = parsed;
                    }
                    else if (currentEvent === 'error') {
                        const errorMsg = parsed.message || parsed.error || 'Compilation failed';
                        throw new Error(errorMsg);
                    }
                }
                catch (e) {
                    if (e instanceof Error && e.message !== 'Compilation failed')
                        throw e;
                    // Ignore JSON parse errors for malformed lines
                }
                currentEvent = '';
            }
        }
    }
    // Prefer v2 if we got a `done` event (server is v2-aware).
    if (v2Done) {
        return {
            files: v2Files,
            meta: v2Done.meta,
            commands: v2Done.commands,
            warnings: v2Done.warnings,
        };
    }
    if (!result) {
        throw new Error('Compilation stream ended without a result');
    }
    return result;
}
/**
 * Wait for the compiler container to be ready.
 * Cloud containers may need a cold start — this polls health until the
 * container reports "ready", retrying up to `maxAttempts` times.
 */
export async function waitForReady(maxAttempts = 12, intervalMs = 5000, onStatus) {
    for (let i = 0; i < maxAttempts; i++) {
        try {
            const health = await checkHealth();
            if (health.status === 'ready')
                return;
            onStatus?.(`Compiler warming up (${health.status})...`);
        }
        catch {
            onStatus?.('Connecting to compiler...');
        }
        if (i < maxAttempts - 1) {
            await new Promise(r => setTimeout(r, intervalMs));
        }
    }
    throw new Error('Compiler container did not become ready in time');
}
/**
 * Get the API URL (for debugging)
 */
export function getApiUrl() {
    return API_URL;
}
//# sourceMappingURL=api-client.js.map