import { s as normalizeOptionalLowercaseString } from "./string-coerce-Bje8XVt9.js";
import { a as formatErrorMessage } from "./errors-D58q7G6U.js";
import { l as mimeTypeFromFilePath, m as MAX_IMAGE_BYTES, r as extensionForMime } from "./mime-BvnFJLWs.js";
import { n as estimateBase64DecodedBytes } from "./base64--h3aMrdJ.js";
import { t as sniffMimeFromBase64 } from "./sniff-mime-from-base64-Dp0TZasx.js";
import { i as deleteMediaBuffer, u as saveMediaBuffer } from "./store-BMDm-hWh.js";
import { i as resolveInboundMediaReference } from "./media-reference-CnUKq0Vo.js";
import { stat } from "node:fs/promises";
//#region src/gateway/chat-attachments.ts
const OFFLOAD_THRESHOLD_BYTES = 2e6;
const TEXT_ONLY_OFFLOAD_LIMIT = 10;
function resolveChatAttachmentMaxBytes(cfg) {
	const configured = cfg.agents?.defaults?.mediaMaxMb;
	return Math.floor((typeof configured === "number" && Number.isFinite(configured) && configured > 0 ? configured : 20) * 1024 * 1024);
}
var UnsupportedAttachmentError = class extends Error {
	constructor(reason, message) {
		super(message);
		this.name = "UnsupportedAttachmentError";
		this.reason = reason;
	}
};
var MediaOffloadError = class extends Error {
	constructor(message, options) {
		super(message, options);
		this.name = "MediaOffloadError";
		this.cause = options?.cause;
	}
};
function normalizeMime(mime) {
	if (!mime) return;
	return normalizeOptionalLowercaseString(mime.split(";")[0]) || void 0;
}
function isImageMime(mime) {
	return typeof mime === "string" && mime.startsWith("image/");
}
function isGenericContainerMime(mime) {
	return mime === "application/zip" || mime === "application/octet-stream";
}
function shouldIgnoreProvidedImageMime(params) {
	return isGenericContainerMime(params.sniffedMime) && isImageMime(params.providedMime);
}
function isValidBase64(value) {
	if (value.length === 0 || value.length % 4 !== 0) return false;
	return /^[A-Za-z0-9+/]+={0,2}$/.test(value);
}
function verifyDecodedSize(buffer, estimatedBytes, label) {
	if (Math.abs(buffer.byteLength - estimatedBytes) > 3) throw new Error(`attachment ${label}: base64 contains invalid characters (expected ~${estimatedBytes} bytes decoded, got ${buffer.byteLength})`);
}
function ensureExtension(label, mime) {
	if (/\.[a-zA-Z0-9]+$/.test(label)) return label;
	const ext = extensionForMime(mime) ?? "";
	return ext ? `${label}${ext}` : label;
}
function assertSavedMedia(value, label) {
	if (value === null || typeof value !== "object" || !("id" in value) || typeof value.id !== "string") throw new Error(`attachment ${label}: saveMediaBuffer returned an unexpected shape`);
	const id = value.id;
	if (id.length === 0) throw new Error(`attachment ${label}: saveMediaBuffer returned an empty media ID`);
	if (id.includes("/") || id.includes("\\") || id.includes("\0")) throw new Error(`attachment ${label}: saveMediaBuffer returned an unsafe media ID (contains path separator or null byte)`);
	const path = value.path;
	if (typeof path !== "string" || path.length === 0) throw new Error(`attachment ${label}: saveMediaBuffer returned no on-disk path`);
	return {
		id,
		path
	};
}
function normalizeAttachment(att, idx, opts) {
	const mime = att.mimeType ?? "";
	const content = att.content;
	const label = att.fileName || att.type || `attachment-${idx + 1}`;
	if (typeof content !== "string") throw new Error(`attachment ${label}: content must be base64 string`);
	if (opts.requireImageMime && !mime.startsWith("image/")) throw new Error(`attachment ${label}: only image/* supported`);
	let base64 = content.trim();
	if (opts.stripDataUrlPrefix) {
		const dataUrlMatch = /^data:[^;]+;base64,(.*)$/.exec(base64);
		if (dataUrlMatch) base64 = dataUrlMatch[1];
	}
	return {
		label,
		mime,
		base64
	};
}
async function handleUploadedMediaRefAttachment(params) {
	const { att, idx, maxBytes, shouldForceImageOffload, supportsInlineImages, acceptNonImage } = params;
	const label = att.fileName || att.type || `attachment-${idx + 1}`;
	const mediaRefRaw = String(att.mediaRef);
	let resolved;
	try {
		resolved = await resolveInboundMediaReference(mediaRefRaw);
	} catch (err) {
		throw new Error(`attachment ${label}: ${formatErrorMessage(err)}`);
	}
	if (!resolved) throw new Error(`attachment ${label}: unresolved media reference (${mediaRefRaw})`);
	let sizeBytes;
	try {
		sizeBytes = (await stat(resolved.physicalPath)).size;
	} catch (err) {
		throw new Error(`attachment ${label}: ${formatErrorMessage(err)}`);
	}
	if (sizeBytes <= 0) throw new UnsupportedAttachmentError("empty-payload", `attachment ${label}: empty payload`);
	if (sizeBytes > maxBytes) throw new Error(`attachment ${label}: exceeds size limit (${sizeBytes} > ${maxBytes} bytes)`);
	const declaredMime = normalizeMime(att.mimeType);
	const labelMime = normalizeMime(mimeTypeFromFilePath(resolved.id) ?? mimeTypeFromFilePath(label));
	const finalMime = declaredMime && !isGenericContainerMime(declaredMime) && declaredMime || labelMime && !isGenericContainerMime(labelMime) && labelMime || declaredMime || labelMime || "application/octet-stream";
	const isImage = isImageMime(finalMime);
	if (isImage && !supportsInlineImages && !shouldForceImageOffload) throw new UnsupportedAttachmentError("text-only-image", `attachment ${label}: active model does not accept image inputs`);
	if (!isImage && !acceptNonImage) throw new UnsupportedAttachmentError("unsupported-non-image", `attachment ${label}: non-image attachments (${finalMime}) are not supported on this entrypoint`);
	if (isImage && finalMime === "image/gif" && sizeBytes > 6291456) throw new Error(`attachment ${label}: GIF exceeds size limit (${sizeBytes} > ${MAX_IMAGE_BYTES} bytes)`);
	return {
		isImage,
		ref: {
			mediaRef: resolved.normalizedSource,
			id: resolved.id,
			path: resolved.physicalPath,
			mimeType: finalMime,
			label,
			sizeBytes
		}
	};
}
async function parseMessageWithAttachments(message, attachments, opts) {
	const maxBytes = opts?.maxBytes ?? 20 * 1024 * 1024;
	const log = opts?.log;
	const shouldForceImageOffload = opts?.supportsImages === false;
	const supportsInlineImages = opts?.supportsInlineImages !== false;
	const acceptNonImage = opts?.acceptNonImage !== false;
	if (!attachments || attachments.length === 0) return {
		message,
		images: [],
		imageOrder: [],
		offloadedRefs: []
	};
	const images = [];
	const imageOrder = [];
	const offloadedRefs = [];
	let updatedMessage = message;
	let textOnlyImageOffloadCount = 0;
	const savedMediaIds = [];
	try {
		for (const [idx, att] of attachments.entries()) {
			if (!att) continue;
			if (typeof att.mediaRef === "string" && att.mediaRef.trim().length > 0) {
				const result = await handleUploadedMediaRefAttachment({
					att,
					idx,
					maxBytes,
					shouldForceImageOffload,
					supportsInlineImages,
					acceptNonImage
				});
				const ref = result.ref;
				if (shouldForceImageOffload && result.isImage && textOnlyImageOffloadCount >= TEXT_ONLY_OFFLOAD_LIMIT) {
					log?.warn(`attachment ${ref.label}: dropping image because text-only offload limit ${TEXT_ONLY_OFFLOAD_LIMIT} was reached`);
					updatedMessage += "\n[image attachment omitted: text-only attachment limit reached]";
					continue;
				}
				if (result.isImage) {
					updatedMessage += `\n[media attached: ${ref.mediaRef}]`;
					imageOrder.push("offloaded");
					if (shouldForceImageOffload) textOnlyImageOffloadCount++;
				}
				log?.info?.(`[Gateway] Using uploaded attachment (${ref.mimeType}). Ref: ${ref.mediaRef}`);
				offloadedRefs.push(ref);
				continue;
			}
			const { base64: b64, label, mime } = normalizeAttachment(att, idx, {
				stripDataUrlPrefix: true,
				requireImageMime: false
			});
			if (b64.length === 0) throw new UnsupportedAttachmentError("empty-payload", `attachment ${label}: empty payload`);
			if (!isValidBase64(b64)) throw new Error(`attachment ${label}: invalid base64 content`);
			const sizeBytes = estimateBase64DecodedBytes(b64);
			if (sizeBytes > maxBytes) throw new Error(`attachment ${label}: exceeds size limit (${sizeBytes} > ${maxBytes} bytes)`);
			const providedMime = normalizeMime(mime);
			const sniffedMime = normalizeMime(await sniffMimeFromBase64(b64));
			const labelMime = normalizeMime(mimeTypeFromFilePath(label));
			const trustedProvidedMime = shouldIgnoreProvidedImageMime({
				sniffedMime,
				providedMime
			}) ? void 0 : providedMime;
			const finalMime = sniffedMime && !isGenericContainerMime(sniffedMime) && sniffedMime || trustedProvidedMime && !isGenericContainerMime(trustedProvidedMime) && trustedProvidedMime || labelMime && !isGenericContainerMime(labelMime) && labelMime || sniffedMime || trustedProvidedMime || labelMime || "application/octet-stream";
			if (sniffedMime && providedMime && !isGenericContainerMime(providedMime) && sniffedMime !== providedMime) {
				const usedSource = finalMime === sniffedMime ? "sniffed" : finalMime === providedMime ? "provided" : "label-derived";
				log?.warn(`attachment ${label}: mime mismatch (${providedMime} -> ${sniffedMime}), using ${usedSource}`);
			}
			const isImage = isImageMime(finalMime);
			if (isImage && !supportsInlineImages && !shouldForceImageOffload) throw new UnsupportedAttachmentError("text-only-image", `attachment ${label}: active model does not accept image inputs`);
			if (!isImage && !acceptNonImage) throw new UnsupportedAttachmentError("unsupported-non-image", `attachment ${label}: non-image attachments (${finalMime}) are not supported on this entrypoint`);
			if (isImage && sizeBytes > 6291456) throw new Error(`attachment ${label}: image exceeds size limit (${sizeBytes} > ${MAX_IMAGE_BYTES} bytes)`);
			if (shouldForceImageOffload && isImage && textOnlyImageOffloadCount >= TEXT_ONLY_OFFLOAD_LIMIT) {
				log?.warn(`attachment ${label}: dropping image because text-only offload limit ${TEXT_ONLY_OFFLOAD_LIMIT} was reached`);
				updatedMessage += "\n[image attachment omitted: text-only attachment limit reached]";
				continue;
			}
			if (!(shouldForceImageOffload || !isImage || sizeBytes > OFFLOAD_THRESHOLD_BYTES)) {
				images.push({
					type: "image",
					data: b64,
					mimeType: finalMime
				});
				imageOrder.push("inline");
				continue;
			}
			const buffer = Buffer.from(b64, "base64");
			verifyDecodedSize(buffer, sizeBytes, label);
			let savedMedia;
			try {
				savedMedia = assertSavedMedia(await saveMediaBuffer(buffer, finalMime, "inbound", maxBytes, ensureExtension(label, finalMime)), label);
			} catch (err) {
				throw new MediaOffloadError(`[Gateway Error] Failed to save intercepted media to disk: ${formatErrorMessage(err)}`, { cause: err });
			}
			savedMediaIds.push(savedMedia.id);
			const mediaRef = `media://inbound/${savedMedia.id}`;
			if (isImage) updatedMessage += `\n[media attached: ${mediaRef}]`;
			log?.info?.(shouldForceImageOffload && isImage ? `[Gateway] Offloaded image for text-only model. Saved: ${mediaRef}` : `[Gateway] Offloaded attachment (${finalMime}). Saved: ${mediaRef}`);
			offloadedRefs.push({
				mediaRef,
				id: savedMedia.id,
				path: savedMedia.path,
				mimeType: finalMime,
				label,
				sizeBytes
			});
			if (isImage) {
				imageOrder.push("offloaded");
				if (shouldForceImageOffload) textOnlyImageOffloadCount++;
			}
		}
	} catch (err) {
		if (savedMediaIds.length > 0) await Promise.allSettled(savedMediaIds.map((id) => deleteMediaBuffer(id, "inbound")));
		throw err;
	}
	return {
		message: updatedMessage !== message ? updatedMessage.trimEnd() : message,
		images,
		imageOrder,
		offloadedRefs
	};
}
//#endregion
export { resolveChatAttachmentMaxBytes as i, UnsupportedAttachmentError as n, parseMessageWithAttachments as r, MediaOffloadError as t };
