/**
 * Base HTTP Client
 *
 * Shared request logic for IndexerClient and AuthClient.
 * Includes automatic retry with exponential backoff for transient failures.
 */
export class ApiError extends Error {
    status;
    body;
    constructor(message, status, body) {
        super(message);
        this.status = status;
        this.body = body;
        this.name = 'ApiError';
    }
    /** Whether this error is retryable based on HTTP status or error type. */
    get retryable() {
        // Network errors (status 0) and server errors are retryable
        if (this.status === 0 || this.status === 429 || this.status === 502 || this.status === 503 || this.status === 504) {
            return true;
        }
        // Check for server-provided retryable flag in the response body
        if (this.body) {
            try {
                const parsed = JSON.parse(this.body);
                if (parsed?.error?.retryable === true)
                    return true;
            }
            catch { /* not JSON */ }
        }
        return false;
    }
    /** Extract retry_after_ms hint from server error response, if present. */
    get retryAfterMs() {
        if (this.body) {
            try {
                const parsed = JSON.parse(this.body);
                return parsed?.error?.details?.retry_after_ms;
            }
            catch { /* not JSON */ }
        }
        return undefined;
    }
}
const DEFAULT_RETRY_CONFIG = {
    maxRetries: 3,
    baseDelayMs: 200,
    maxDelayMs: 10_000,
};
function computeRetryDelay(config, attempt, retryAfterMs) {
    const exponential = config.baseDelayMs * Math.pow(2, Math.min(attempt, 10));
    const capped = Math.min(exponential, config.maxDelayMs);
    // Apply jitter: multiply by a random factor in [0.5, 1.0) to prevent thundering herd
    const jitter = 0.5 + Math.random() * 0.5;
    const delay = Math.floor(capped * jitter);
    return retryAfterMs != null ? Math.max(delay, retryAfterMs) : delay;
}
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
export class BaseClient {
    baseUrl;
    getToken;
    _proxyFetch = null;
    _retryConfig = DEFAULT_RETRY_CONFIG;
    constructor(baseUrl, getToken) {
        this.baseUrl = baseUrl;
        this.getToken = getToken;
    }
    /**
     * Set an HTTP proxy function. When set, all requests are routed through
     * the proxy instead of direct fetch(). Used in iframe mode to route
     * requests through the WASM encryption layer.
     */
    setProxy(proxyFetch) {
        this._proxyFetch = proxyFetch;
    }
    /** Override the default retry configuration. */
    setRetryConfig(config) {
        this._retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
    }
    async request(path, options) {
        // If proxy is configured, route through it (iframe mode).
        // Retry is handled by the Rust HttpClient in the WASM layer.
        if (this._proxyFetch) {
            return this.proxyRequest(path, options);
        }
        // Direct fetch with retry
        return this.fetchWithRetry(path, options);
    }
    /** Execute a direct fetch with automatic retry for retryable errors. */
    async fetchWithRetry(path, options) {
        let lastError;
        for (let attempt = 0; attempt <= this._retryConfig.maxRetries; attempt++) {
            if (attempt > 0 && lastError) {
                const delay = computeRetryDelay(this._retryConfig, attempt - 1, lastError.retryAfterMs);
                await sleep(delay);
            }
            try {
                return await this.directFetch(path, options);
            }
            catch (err) {
                const apiError = err instanceof ApiError ? err : new ApiError(String(err), 0);
                if (apiError.retryable && attempt < this._retryConfig.maxRetries) {
                    lastError = apiError;
                    continue;
                }
                throw apiError;
            }
        }
        // Unreachable, but satisfies TypeScript
        throw lastError;
    }
    /** Single-attempt direct fetch (no retry). */
    async directFetch(path, options) {
        const url = `${this.baseUrl}${path}`;
        const headers = {};
        const token = this.getToken();
        if (token) {
            headers['Authorization'] = `Bearer ${token}`;
        }
        if (options?.method === 'POST') {
            headers['Content-Type'] = 'application/json';
        }
        const response = await fetch(url, {
            ...options,
            headers: { ...headers, ...options?.headers },
        });
        if (!response.ok) {
            const body = await response.text().catch(() => '');
            throw new ApiError(`API request failed: ${response.status} ${response.statusText}`, response.status, body);
        }
        try {
            return await response.json();
        }
        catch {
            throw new ApiError(`API request returned non-JSON response: ${response.status} ${response.statusText}`, response.status, await response.text().catch(() => ''));
        }
    }
    /**
     * Route a request through the iframe proxy.
     * The proxy goes through WASM EncryptedHttpClient which handles encryption.
     * Retry is handled by the Rust HttpClient, so no retry here.
     */
    async proxyRequest(path, options) {
        const method = options?.method ?? 'GET';
        let body;
        if (options?.body) {
            try {
                body = typeof options.body === 'string'
                    ? JSON.parse(options.body)
                    : options.body;
            }
            catch {
                body = options.body;
            }
        }
        // Extract query string from path so the WASM proxy can forward it
        let actualPath = path;
        let query;
        const qIdx = path.indexOf('?');
        if (qIdx !== -1) {
            actualPath = path.slice(0, qIdx);
            query = {};
            for (const [k, v] of new URLSearchParams(path.slice(qIdx + 1))) {
                query[k] = v;
            }
        }
        try {
            const result = await this._proxyFetch(actualPath, method, body, query, this.baseUrl, this.getToken());
            return result;
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            throw new ApiError(message, 0);
        }
    }
}
//# sourceMappingURL=base-client.js.map