import { c as normalizeOptionalString } from "./string-coerce-Bje8XVt9.js";
import { a as formatErrorMessage } from "./errors-D58q7G6U.js";
import { t as createSubsystemLogger } from "./subsystem-esWnFs2r.js";
import { i as getRuntimeConfig } from "./io-D06zwDAe.js";
import { n as authorizeOperatorScopesForMethod } from "./method-scopes-DRFyG5Gd.js";
import { m as MAX_IMAGE_BYTES, r as extensionForMime } from "./mime-CPby_ck-.js";
import { u as saveMediaBuffer } from "./store-BafHTEIM.js";
import { a as isRequestBodyLimitError, s as readRequestBodyBufferWithLimit } from "./http-body-DgbTSDrs.js";
import { a as sendMethodNotAllowed, i as sendJson } from "./http-common-ChpZezSU.js";
import { l as resolveOpenAiCompatibleHttpOperatorScopes, t as authorizeGatewayHttpRequestOrReply } from "./http-auth-utils-BB0rSA0A.js";
import "./http-utils-BWa3N16q.js";
import { i as resolveChatAttachmentMaxBytes } from "./chat-attachments-DCGX9sG6.js";
//#region src/gateway/media-upload-http.ts
const log = createSubsystemLogger("gateway/media-upload");
const MEDIA_UPLOAD_ROUTE = "/__openclaw__/chat/media/upload";
/** Header carrying the original filename so the saved media keeps a useful label. */
const FILENAME_HEADER = "x-openclaw-filename";
function isMediaUploadPath(pathname) {
	return pathname === MEDIA_UPLOAD_ROUTE;
}
function resolveUploadMaxBytes(contentType) {
	const configuredMax = resolveChatAttachmentMaxBytes(getRuntimeConfig());
	if (contentType && contentType.toLowerCase().startsWith("image/gif")) return Math.min(configuredMax, MAX_IMAGE_BYTES);
	return configuredMax;
}
function sanitizeFilename(raw) {
	const trimmed = normalizeOptionalString(raw);
	if (!trimmed) return;
	const cleaned = trimmed.replace(/[\r\n\\/\0]+/g, "_").trim();
	return cleaned.length > 0 ? cleaned.slice(0, 255) : void 0;
}
/**
* Handles `POST /__openclaw__/chat/media/upload` — accepts a single binary
* attachment body, persists it via the media store, and returns an opaque
* `media://inbound/<id>` claim-check reference for the client to embed in the
* subsequent `chat.send` frame. This keeps large images off the WebSocket,
* which enforces a 25MB single-frame limit (oversized frames close the
* connection with 1009).
*/
async function handleMediaUploadHttpRequest(req, res, opts) {
	if (!isMediaUploadPath(new URL(req.url ?? "/", "http://localhost").pathname)) return false;
	if (req.method !== "POST") {
		sendMethodNotAllowed(res, "POST");
		return true;
	}
	const requestAuth = await authorizeGatewayHttpRequestOrReply({
		req,
		res,
		auth: opts.auth,
		trustedProxies: opts.trustedProxies,
		allowRealIpFallback: opts.allowRealIpFallback,
		rateLimiter: opts.rateLimiter
	});
	if (!requestAuth) return true;
	const scopeAuth = authorizeOperatorScopesForMethod("chat.send", resolveOpenAiCompatibleHttpOperatorScopes(req, requestAuth));
	if (!scopeAuth.allowed) {
		sendJson(res, 403, {
			ok: false,
			error: {
				type: "forbidden",
				message: `missing scope: ${scopeAuth.missingScope}`
			}
		});
		return true;
	}
	const rawContentType = normalizeOptionalString(Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"]);
	const contentType = rawContentType ? rawContentType.split(";")[0].trim() : void 0;
	const fileNameHeader = Array.isArray(req.headers[FILENAME_HEADER]) ? req.headers[FILENAME_HEADER]?.[0] : req.headers[FILENAME_HEADER];
	const fileName = sanitizeFilename(typeof fileNameHeader === "string" ? decodeHeaderFilename(fileNameHeader) : void 0);
	const maxBytes = resolveUploadMaxBytes(contentType);
	const declaredLength = parseDeclaredContentLength(req);
	if (declaredLength !== null && declaredLength > maxBytes) {
		drainRequest(req);
		sendJson(res, 413, {
			ok: false,
			error: {
				type: "payload_too_large",
				message: `attachment exceeds size limit (${maxBytes} bytes)`
			}
		});
		return true;
	}
	let body;
	try {
		body = await readRequestBodyBufferWithLimit(req, { maxBytes });
	} catch (err) {
		if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
			sendJson(res, 413, {
				ok: false,
				error: {
					type: "payload_too_large",
					message: `attachment exceeds size limit (${maxBytes} bytes)`
				}
			});
			return true;
		}
		if (isRequestBodyLimitError(err, "REQUEST_BODY_TIMEOUT")) {
			sendJson(res, 408, {
				ok: false,
				error: {
					type: "request_timeout",
					message: "request body timeout"
				}
			});
			return true;
		}
		sendJson(res, 400, {
			ok: false,
			error: {
				type: "invalid_request",
				message: "failed to read upload body"
			}
		});
		return true;
	}
	if (body.byteLength === 0) {
		sendJson(res, 400, {
			ok: false,
			error: {
				type: "invalid_request",
				message: "empty upload body"
			}
		});
		return true;
	}
	const labelWithExt = ensureExtension(fileName, contentType);
	try {
		const saved = await saveMediaBuffer(body, contentType, "inbound", maxBytes, labelWithExt);
		sendJson(res, 200, {
			ok: true,
			mediaRef: `media://inbound/${saved.id}`,
			mimeType: saved.contentType ?? contentType ?? "application/octet-stream",
			sizeBytes: saved.size,
			fileName: fileName ?? null
		});
	} catch (err) {
		log.warn(`media upload failed to persist: ${formatErrorMessage(err)}`);
		sendJson(res, 500, {
			ok: false,
			error: {
				type: "internal_error",
				message: "failed to persist upload"
			}
		});
	}
	return true;
}
function ensureExtension(label, mime) {
	if (!label) return;
	if (/\.[a-zA-Z0-9]+$/.test(label)) return label;
	const ext = mime ? extensionForMime(mime) : void 0;
	return ext ? `${label}${ext}` : label;
}
function decodeHeaderFilename(value) {
	try {
		return decodeURIComponent(value);
	} catch {
		return value;
	}
}
function parseDeclaredContentLength(req) {
	const header = req.headers["content-length"];
	const raw = Array.isArray(header) ? header[0] : header;
	if (typeof raw !== "string") return null;
	const parsed = Number.parseInt(raw, 10);
	if (!Number.isFinite(parsed) || parsed < 0) return null;
	return parsed;
}
/**
* Consumes and discards a request body we are about to reject. Without draining,
* responding while the client is still streaming a large upload resets the
* socket (ECONNRESET) instead of delivering the error response cleanly.
*/
function drainRequest(req) {
	req.on("error", () => {});
	req.resume();
}
//#endregion
export { MEDIA_UPLOAD_ROUTE, handleMediaUploadHttpRequest, isMediaUploadPath };
