import { assert, unreachable } from "../../../shared/src/asserts.js";
import { sleep } from "../../../shared/src/sleep.js";
import "../../../shared/src/valita.js";
import { getErrorMessage } from "../../../shared/src/error.js";
import { must } from "../../../shared/src/must.js";
import { PushFailed, TransformFailed } from "../../../zero-protocol/src/error-kind-enum.js";
import { ZeroCache } from "../../../zero-protocol/src/error-origin-enum.js";
import { HTTP, Internal, Parse } from "../../../zero-protocol/src/error-reason-enum.js";
import { isProtocolError } from "../../../zero-protocol/src/error.js";
import { upstreamSchema } from "../types/shards.js";
import { randInt } from "../../../shared/src/rand.js";
import { ProtocolErrorWithLevel } from "../types/error-with-level.js";
import { context, propagation } from "@opentelemetry/api";
import "urlpattern-polyfill";
//#region ../zero-cache/src/custom/fetch.ts
var reservedParams = ["schema", "appID"];
/**
* Compiles and validates a URLPattern from configuration.
*
* Patterns must be full URLs (e.g., "https://api.example.com/endpoint").
* URLPattern automatically sets search and hash to wildcard ('*'),
* which means query parameters and fragments are ignored during matching.
*
* @throws Error if the pattern is an invalid URLPattern
*/
function compileUrlPattern(pattern) {
	try {
		return new URLPattern(pattern);
	} catch (e) {
		throw new Error(`Invalid URLPattern in URL configuration: "${pattern}". Error: ${e instanceof Error ? e.message : String(e)}`);
	}
}
/**
* Filters custom headers based on the allowed headers list.
* If no allowedHeaders list is specified, no custom headers are forwarded (secure by default).
* Header names are compared case-insensitively.
*/
function filterCustomHeaders(headers, allowedHeaders) {
	if (!headers) return {};
	if (!allowedHeaders || allowedHeaders.length === 0) return {};
	const allowed = new Set(allowedHeaders.map((h) => h.toLowerCase()));
	const filtered = {};
	for (const [key, value] of Object.entries(headers)) if (allowed.has(key.toLowerCase())) filtered[key] = value;
	return filtered;
}
var getBodyPreview = async (res, lc) => {
	try {
		const body = await res.clone().text();
		if (body.length > 512) return body.slice(0, 512) + "...";
		return body;
	} catch (e) {
		lc.warn?.("failed to get body preview", { url: res.url }, e);
	}
};
var MAX_ATTEMPTS = 4;
async function fetchFromAPIServer(validator, source, lc, ctx, shard, body) {
	const fetchFromAPIServerID = randInt(1, Number.MAX_SAFE_INTEGER).toString(36);
	lc = lc.withContext("fetchFromAPIServerID", fetchFromAPIServerID).withContext("source", source);
	const fetchConfig = source === "push" ? ctx.pushContext : ctx.queryContext;
	const url = must(fetchConfig.url, `Fetch config for ${source} is missing URL`);
	const headerOptions = fetchConfig.headerOptions;
	lc.debug?.("fetchFromAPIServer called", { url });
	if (!urlMatch(url, fetchConfig.allowedUrlPatterns ?? [])) throw new ProtocolErrorWithLevel(source === "push" ? {
		kind: PushFailed,
		origin: ZeroCache,
		reason: Internal,
		message: `URL "${url}" is not allowed by the ZERO_MUTATE_URL configuration`,
		mutationIDs: []
	} : {
		kind: TransformFailed,
		origin: ZeroCache,
		reason: Internal,
		message: `URL "${url}" is not allowed by the ZERO_QUERY_URL configuration`,
		queryIDs: []
	}, "warn");
	const headers = { "Content-Type": "application/json" };
	if (headerOptions.apiKey) headers["X-Api-Key"] = headerOptions.apiKey;
	Object.assign(headers, filterCustomHeaders(headerOptions.customHeaders, headerOptions.allowedClientHeaders));
	if (ctx.auth?.raw) headers["Authorization"] = `Bearer ${ctx.auth.raw}`;
	if (headerOptions.cookie) headers["Cookie"] = headerOptions.cookie;
	if (headerOptions.origin) headers["Origin"] = headerOptions.origin;
	propagation.inject(context.active(), headers);
	const urlObj = new URL(url);
	const params = new URLSearchParams(urlObj.search);
	for (const reserved of reservedParams) assert(!params.has(reserved), `The push URL cannot contain the reserved query param "${reserved}"`);
	params.append("schema", upstreamSchema(shard));
	params.append("appID", shard.appID);
	urlObj.search = params.toString();
	const finalUrl = urlObj.toString();
	for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
		lc = lc.withContext("fetchFromAPIServerAttempt", attempt);
		lc.debug?.("fetch from API server attempt");
		const shouldRetry = async () => {
			if (attempt < MAX_ATTEMPTS) {
				const delayMs = getBackoffDelayMs(attempt);
				lc.debug?.(`fetch from API server retrying in ${delayMs} ms`);
				await sleep(delayMs);
				return true;
			}
			lc.debug?.("fetch from API server reached max attempts, not retrying");
			return false;
		};
		try {
			const response = await fetch(finalUrl, {
				method: "POST",
				headers,
				body: JSON.stringify(body)
			});
			if (!response.ok) {
				const bodyPreview = await getBodyPreview(response, lc);
				lc.warn?.("fetch from API server returned non-OK status", {
					url: finalUrl,
					status: response.status,
					bodyPreview
				});
				if ((response.status === 502 || response.status === 504) && await shouldRetry()) continue;
				throw new ProtocolErrorWithLevel(source === "push" ? {
					kind: PushFailed,
					origin: ZeroCache,
					reason: HTTP,
					status: response.status,
					bodyPreview,
					message: `Fetch from API server returned non-OK status ${response.status}`,
					mutationIDs: []
				} : {
					kind: TransformFailed,
					origin: ZeroCache,
					reason: HTTP,
					status: response.status,
					bodyPreview,
					message: `Fetch from API server returned non-OK status ${response.status}`,
					queryIDs: []
				}, "warn");
			}
			try {
				const json = await response.json();
				const result = validator.parse(json);
				lc.debug?.("fetch from API server succeeded");
				return result;
			} catch (error) {
				lc.warn?.("failed to parse response", { url: finalUrl }, error);
				throw new ProtocolErrorWithLevel(source === "push" ? {
					kind: PushFailed,
					origin: ZeroCache,
					reason: Parse,
					message: `Failed to parse response from API server: ${getErrorMessage(error)}`,
					mutationIDs: []
				} : {
					kind: TransformFailed,
					origin: ZeroCache,
					reason: Parse,
					message: `Failed to parse response from API server: ${getErrorMessage(error)}`,
					queryIDs: []
				}, "warn", { cause: error });
			}
		} catch (error) {
			if (isProtocolError(error)) throw error;
			const isFetchFailed = error instanceof TypeError && error.message === "fetch failed";
			let logLevel = isFetchFailed ? "warn" : "error";
			lc[logLevel]?.("fetch from API server threw error", { url: finalUrl }, error);
			if (isFetchFailed && await shouldRetry()) continue;
			throw new ProtocolErrorWithLevel(source === "push" ? {
				kind: PushFailed,
				origin: ZeroCache,
				reason: Internal,
				message: `Fetch from API server threw error: ${getErrorMessage(error)}`,
				mutationIDs: []
			} : {
				kind: TransformFailed,
				origin: ZeroCache,
				reason: Internal,
				message: `Fetch from API server threw error: ${getErrorMessage(error)}`,
				queryIDs: []
			}, logLevel, { cause: error });
		}
	}
	unreachable();
}
/**
* Returns true if the url matches one of the allowedUrlPatterns.
*
* URLPattern automatically ignores query parameters and hash fragments during matching
* because it sets search and hash to wildcard ('*') by default.
*
* Example URLPattern patterns:
* - "https://api.example.com/endpoint" - Exact match for a specific URL
* - "https://*.example.com/endpoint" - Matches any single subdomain (e.g., "https://api.example.com/endpoint")
* - "https://*.*.example.com/endpoint" - Matches two subdomains (e.g., "https://api.v1.example.com/endpoint")
* - "https://api.example.com/*" - Matches any path under /
* - "https://api.example.com/:version/endpoint" - Matches with named parameter (e.g., "https://api.example.com/v1/endpoint")
*/
function urlMatch(url, allowedUrlPatterns) {
	for (const pattern of allowedUrlPatterns) if (pattern.test(url)) return true;
	return false;
}
/**
* Returns the delay in milliseconds for the next retry attempt using exponential backoff with jitter.
*
* The delay assumes the first retry is attempt 1.
* The formula is: `min(1000, 100 * 2^(attempt - 1) + jitter)` where jitter is between 0 and 100ms.
*/
function getBackoffDelayMs(attempt) {
	return Math.min(1e3, 100 * Math.pow(2, attempt - 1) + Math.random() * 100);
}
//#endregion
export { compileUrlPattern, fetchFromAPIServer };

//# sourceMappingURL=fetch.js.map