import { EXTERNAL_FETCH_TIMEOUT_MS, MAX_EXTERNAL_IMAGE_BYTES, MAX_EXTERNAL_REDIRECTS } from "./image-security-constants.js";
import { BlockedConnectionToPrivateIpError, BlockedExternalImageExceedingError, BlockedExternalImageLargerThanError, ExternalImageResponseBodyEmptyError, FailedToDownloadError, ImageSecurityError, RedirectMissingLocationError, TimedOutDownloadingError, TooManyRedirectsError, UnableToResolveHostError, UnexpectedRedirectStateError } from "./image-security-errors.js";
import { resolveDownloadedImageMimeType } from "./image-content-security.js";
import { isBlockedIpAddress, validateExternalImageUrl, validateUrlResolvesToPublicIp } from "./image-url-security.js";
import { retryWithBackoff } from "@inkeep/agents-core";
import { lookup } from "node:dns";
import { Agent } from "undici";

//#region src/domains/run/services/blob-storage/external-image-downloader.ts
const externalImageDispatcher = new Agent({ connect: { lookup(hostname, options, callback) {
	lookup(hostname, options, (error, address, family) => {
		if (error) {
			callback(new UnableToResolveHostError(hostname, { cause: error }), "", 0);
			return;
		}
		if (Array.isArray(address)) {
			const selected = address[0];
			if (!selected) {
				callback(new UnableToResolveHostError(hostname), "", 0);
				return;
			}
			if (isBlockedIpAddress(selected.address)) {
				callback(new BlockedConnectionToPrivateIpError(selected.address), "", 0);
				return;
			}
			callback(null, selected.address, selected.family);
			return;
		}
		if (isBlockedIpAddress(address)) {
			callback(new BlockedConnectionToPrivateIpError(address), "", 0);
			return;
		}
		callback(null, address, family);
	});
} } });
const MAX_EXTERNAL_FETCH_ATTEMPTS = 3;
async function downloadExternalImage(url) {
	let currentUrl = validateExternalImageUrl(url);
	await validateUrlResolvesToPublicIp(currentUrl);
	for (let redirectCount = 0; redirectCount <= MAX_EXTERNAL_REDIRECTS; redirectCount++) {
		const response = await fetchWithRetry(currentUrl);
		if (isRedirectStatus(response.status)) {
			const location = response.headers.get("location");
			if (!location) throw new RedirectMissingLocationError(toSanitizedUrl(currentUrl));
			if (redirectCount === MAX_EXTERNAL_REDIRECTS) throw new TooManyRedirectsError(toSanitizedUrl(url));
			currentUrl = validateExternalImageUrl(new URL(location, currentUrl).toString());
			await validateUrlResolvesToPublicIp(currentUrl);
			continue;
		}
		if (!response.ok) throw new FailedToDownloadError(toSanitizedUrl(currentUrl), `${response.status} ${response.statusText}`);
		const headerContentType = (response.headers.get("content-type") || "").split(";")[0].trim().toLowerCase();
		const contentLength = response.headers.get("content-length");
		if (contentLength && Number.isFinite(Number(contentLength)) && Number(contentLength) > MAX_EXTERNAL_IMAGE_BYTES) throw new BlockedExternalImageLargerThanError(MAX_EXTERNAL_IMAGE_BYTES, contentLength);
		const data = await readResponseBytesWithLimit(response, MAX_EXTERNAL_IMAGE_BYTES);
		return {
			data,
			mimeType: await resolveDownloadedImageMimeType(data, headerContentType)
		};
	}
	throw new UnexpectedRedirectStateError(toSanitizedUrl(url));
}
async function fetchWithRetry(url) {
	return retryWithBackoff(async () => {
		let response;
		try {
			response = await fetchWithConnectionIpValidation(url);
		} catch (error) {
			if (error instanceof TimedOutDownloadingError || error instanceof FailedToDownloadError) error.status = 502;
			throw error;
		}
		if (isRetryableStatus(response.status)) {
			const err = new FailedToDownloadError(toSanitizedUrl(url), `${response.status} ${response.statusText}`);
			err.status = response.status;
			throw err;
		}
		return response;
	}, {
		maxAttempts: MAX_EXTERNAL_FETCH_ATTEMPTS,
		maxDelayMs: 2e3,
		label: `image-download ${toSanitizedUrl(url)}`
	});
}
function isRedirectStatus(status) {
	return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
}
function isRetryableStatus(status) {
	return status === 429 || status >= 500 && status <= 599;
}
async function readResponseBytesWithLimit(response, maxBytes) {
	if (!response.body) throw new ExternalImageResponseBodyEmptyError();
	const reader = response.body.getReader();
	const chunks = [];
	let totalBytes = 0;
	try {
		while (true) {
			const { done, value } = await reader.read();
			if (done) break;
			if (!value) continue;
			totalBytes += value.byteLength;
			if (totalBytes > maxBytes) throw new BlockedExternalImageExceedingError(maxBytes);
			chunks.push(value);
		}
	} finally {
		reader.releaseLock();
	}
	const merged = new Uint8Array(totalBytes);
	let offset = 0;
	for (const chunk of chunks) {
		merged.set(chunk, offset);
		offset += chunk.byteLength;
	}
	return merged;
}
async function fetchWithConnectionIpValidation(url) {
	try {
		return await fetch(url.toString(), {
			redirect: "manual",
			signal: AbortSignal.timeout(EXTERNAL_FETCH_TIMEOUT_MS),
			dispatcher: externalImageDispatcher
		});
	} catch (error) {
		const imageSecurityError = extractImageSecurityError(error);
		if (imageSecurityError instanceof BlockedConnectionToPrivateIpError || imageSecurityError instanceof UnableToResolveHostError) throw imageSecurityError;
		if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) throw new TimedOutDownloadingError(toSanitizedUrl(url));
		throw new FailedToDownloadError(toSanitizedUrl(url), void 0, { cause: error });
	}
}
function extractImageSecurityError(error) {
	if (error instanceof ImageSecurityError) return error;
	if (error instanceof Error && error.cause) return extractImageSecurityError(error.cause);
	return null;
}
function toSanitizedUrl(url) {
	const parsed = typeof url === "string" ? new URL(url) : new URL(url.toString());
	parsed.search = "";
	parsed.hash = "";
	return parsed.toString();
}

//#endregion
export { downloadExternalImage };