// Synced from packages/transport-http/src/http-core.ts
// Extracted from legacy source: src/runtime/transports/http-json-transport.ts
import { sleepWithSignal } from './backoff.js';
import { mergeHeaders, resolveAuthToken, resolveHeaders } from './auth.js';
import { getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, resolveHttpRetryPolicy, } from './retry.js';
import { buildUrl, createTransportPaths } from './paths.js';
function isPlainObject(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isRecord(value) {
    return typeof value === 'object' && value !== null;
}
function applyHeaderSource(target, source) {
    if (!source)
        return;
    if (source instanceof Headers) {
        source.forEach((value, key) => {
            target[key] = value;
        });
        return;
    }
    if (Array.isArray(source)) {
        for (const [key, value] of source) {
            target[key] = value;
        }
        return;
    }
    for (const [key, value] of Object.entries(source)) {
        if (value !== undefined) {
            target[key] = value;
        }
    }
}
function mergeHeaderRecord(...sources) {
    const merged = {};
    for (const source of sources) {
        applyHeaderSource(merged, source);
    }
    return merged;
}
function readErrorMessage(status, url, body) {
    if (isRecord(body) && typeof body.error === 'string' && body.error.trim()) {
        return body.error.trim();
    }
    if (typeof body === 'string' && body.trim()) {
        return body.trim();
    }
    return `Transport request failed with status ${status} for ${url}`;
}
function createTransportError(status, url, method, body) {
    return Object.assign(new Error(readErrorMessage(status, url, body)), {
        transport: {
            status,
            body,
            url,
            method,
        },
    });
}
function createNetworkTransportError(error, url, method) {
    const message = error instanceof Error && error.message.trim()
        ? error.message.trim()
        : `Transport request failed before receiving a response for ${url}`;
    return Object.assign(new Error(message), {
        cause: error,
        transport: {
            status: 0,
            body: { error: message },
            url,
            method,
        },
    });
}
function toStringValue(value, key) {
    if (value === undefined || value === null) {
        throw new Error(`Missing required path parameter "${key}"`);
    }
    return String(value);
}
function addQueryValue(url, key, value) {
    if (value === undefined)
        return;
    if (value === null) {
        url.searchParams.append(key, 'null');
        return;
    }
    if (Array.isArray(value)) {
        for (const item of value) {
            addQueryValue(url, key, item);
        }
        return;
    }
    if (typeof value === 'object') {
        url.searchParams.append(key, JSON.stringify(value));
        return;
    }
    url.searchParams.append(key, String(value));
}
function splitContractInput(path, input = {}) {
    const remaining = { ...input };
    const interpolatedPath = path.replace(/\{([^}]+)\}/g, (_match, key) => {
        const value = toStringValue(remaining[key], key);
        delete remaining[key];
        return encodeURIComponent(value);
    });
    return { interpolatedPath, remaining };
}
export function createJsonRequestInit(token, body, method = 'GET', headers = {}, signal, defaultHeaders = {}) {
    return {
        method,
        credentials: 'include',
        signal,
        headers: mergeHeaderRecord(defaultHeaders, token ? { Authorization: `Bearer ${token}` } : undefined, body !== undefined ? { 'Content-Type': 'application/json' } : undefined, headers),
        ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
    };
}
export const createJsonInit = createJsonRequestInit;
export function createFetch(fetchImpl, fallbackFetch) {
    const resolved = fetchImpl ?? fallbackFetch ?? globalThis.fetch;
    if (typeof resolved !== 'function') {
        throw new Error('Fetch implementation is required');
    }
    return resolved.bind(globalThis);
}
export async function readJsonBody(response) {
    const text = await response.text();
    if (!text.trim())
        return null;
    try {
        return JSON.parse(text);
    }
    catch {
        return text;
    }
}
export async function requestJson(fetchImpl, url, init = {}) {
    let response;
    try {
        response = await fetchImpl(url, init);
    }
    catch (error) {
        throw createNetworkTransportError(error, url, init.method ?? 'GET');
    }
    const body = await readJsonBody(response);
    if (!response.ok) {
        throw createTransportError(response.status, url, init.method ?? 'GET', body);
    }
    return body;
}
export function createHttpJsonTransport(options) {
    const baseUrl = options.baseUrl.trim();
    const fetchImpl = createFetch(options.fetchImpl, options.fetch);
    const authToken = options.authToken ?? null;
    const defaultHeaders = options.headers;
    const retryPolicy = options.retry;
    const paths = createTransportPaths(baseUrl);
    const requestJsonForTransport = async (pathOrUrl, requestOptions = {}) => {
        const url = pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')
            ? pathOrUrl
            : buildUrl(baseUrl, pathOrUrl);
        const method = requestOptions.method ?? (requestOptions.body === undefined ? 'GET' : 'POST');
        const resolvedRetry = resolveHttpRetryPolicy(retryPolicy, requestOptions.retry);
        let attempt = 0;
        while (true) {
            attempt += 1;
            const token = await resolveAuthToken(authToken, options.getAuthToken);
            const headers = await resolveHeaders(defaultHeaders, options.getHeaders);
            try {
                return await requestJson(fetchImpl, url, createJsonRequestInit(token, requestOptions.body, method, mergeHeaders(headers, requestOptions.headers), requestOptions.signal));
            }
            catch (error) {
                const status = typeof error === 'object' && error !== null && 'transport' in error
                    ? error.transport?.status
                    : undefined;
                const shouldRetry = attempt < resolvedRetry.maxAttempts && ((typeof status === 'number' && status > 0 && isRetryableHttpStatus(method, status, resolvedRetry))
                    || (typeof status === 'number' && status === 0 && isRetryableNetworkError(method, resolvedRetry)));
                if (!shouldRetry) {
                    throw error;
                }
                await sleepWithSignal(getHttpRetryDelay(attempt + 1, resolvedRetry), requestOptions.signal);
            }
        }
    };
    const resolveContractRequest = (method, path, input = {}) => {
        const { interpolatedPath, remaining } = splitContractInput(path, input);
        const upperMethod = method.toUpperCase();
        const url = new URL(buildUrl(baseUrl, interpolatedPath));
        if (upperMethod === 'GET' || upperMethod === 'HEAD') {
            for (const [key, value] of Object.entries(remaining)) {
                addQueryValue(url, key, value);
            }
            return {
                url: url.toString(),
                method: upperMethod,
            };
        }
        const body = isPlainObject(remaining) && Object.keys(remaining).length > 0 ? remaining : undefined;
        return {
            url: url.toString(),
            method: upperMethod,
            ...(body ? { body } : {}),
        };
    };
    return {
        baseUrl: paths.baseUrl,
        authToken,
        fetchImpl,
        paths,
        buildUrl(path) {
            return buildUrl(baseUrl, path);
        },
        async getAuthToken() {
            return await resolveAuthToken(authToken, options.getAuthToken);
        },
        requestJson: requestJsonForTransport,
        resolveContractRequest,
    };
}
