import { createReplyPrefixOptions, readJsonBodyWithLimit, registerWebhookTarget, rejectNonPostWebhookRequest, requestBodyErrorToText, resolveSenderCommandAuthorization, resolveWebhookPath, resolveWebhookTargets } from "minion/plugin-sdk";
import { ProxyAgent, fetch as fetch$1 } from "undici";

//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
	let target = {};
	for (var name in all) {
		__defProp(target, name, {
			get: all[name],
			enumerable: true
		});
	}
	if (!no_symbols) {
		__defProp(target, Symbol.toStringTag, { value: "Module" });
	}
	return target;
};

//#endregion
//#region extensions/zalo/src/api.ts
/**
* Zalo Bot API client
* @see https://bot.zaloplatforms.com/docs
*/
const ZALO_API_BASE = "https://bot-api.zaloplatforms.com";
var ZaloApiError = class extends Error {
	constructor(message, errorCode, description) {
		super(message);
		this.errorCode = errorCode;
		this.description = description;
		this.name = "ZaloApiError";
	}
	/** True if this is a long-polling timeout (no updates available) */
	get isPollingTimeout() {
		return this.errorCode === 408;
	}
};
/**
* Call the Zalo Bot API
*/
async function callZaloApi(method, token, body, options) {
	const url = `${ZALO_API_BASE}/bot${token}/${method}`;
	const controller = new AbortController();
	const timeoutId = options?.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : void 0;
	const fetcher = options?.fetch ?? fetch;
	try {
		const data = await (await fetcher(url, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: body ? JSON.stringify(body) : void 0,
			signal: controller.signal
		})).json();
		if (!data.ok) throw new ZaloApiError(data.description ?? `Zalo API error: ${method}`, data.error_code, data.description);
		return data;
	} finally {
		if (timeoutId) clearTimeout(timeoutId);
	}
}
/**
* Validate bot token and get bot info
*/
async function getMe(token, timeoutMs, fetcher) {
	return callZaloApi("getMe", token, void 0, {
		timeoutMs,
		fetch: fetcher
	});
}
/**
* Send a text message
*/
async function sendMessage(token, params, fetcher) {
	return callZaloApi("sendMessage", token, params, { fetch: fetcher });
}
/**
* Send a photo message
*/
async function sendPhoto(token, params, fetcher) {
	return callZaloApi("sendPhoto", token, params, { fetch: fetcher });
}
/**
* Get updates using long polling (dev/testing only)
* Note: Zalo returns a single update per call, not an array like Telegram
*/
async function getUpdates(token, params, fetcher) {
	const pollTimeoutSec = params?.timeout ?? 30;
	const timeoutMs = (pollTimeoutSec + 5) * 1e3;
	return callZaloApi("getUpdates", token, { timeout: String(pollTimeoutSec) }, {
		timeoutMs,
		fetch: fetcher
	});
}
/**
* Set webhook URL for receiving updates
*/
async function setWebhook(token, params, fetcher) {
	return callZaloApi("setWebhook", token, params, { fetch: fetcher });
}
/**
* Delete webhook configuration
*/
async function deleteWebhook(token, fetcher) {
	return callZaloApi("deleteWebhook", token, void 0, { fetch: fetcher });
}

//#endregion
//#region extensions/zalo/src/proxy.ts
const proxyCache = /* @__PURE__ */ new Map();
function resolveZaloProxyFetch(proxyUrl) {
	const trimmed = proxyUrl?.trim();
	if (!trimmed) return;
	const cached = proxyCache.get(trimmed);
	if (cached) return cached;
	const agent = new ProxyAgent(trimmed);
	const fetcher = (input, init) => fetch$1(input, {
		...init,
		dispatcher: agent
	});
	proxyCache.set(trimmed, fetcher);
	return fetcher;
}

//#endregion
//#region extensions/zalo/src/runtime.ts
let runtime = null;
function setZaloRuntime(next) {
	runtime = next;
}
function getZaloRuntime() {
	if (!runtime) throw new Error("Zalo runtime not initialized");
	return runtime;
}

//#endregion
//#region extensions/zalo/src/monitor.ts
var monitor_exports = /* @__PURE__ */ __exportAll({
	handleZaloWebhookRequest: () => handleZaloWebhookRequest,
	monitorZaloProvider: () => monitorZaloProvider,
	registerZaloWebhookTarget: () => registerZaloWebhookTarget
});
const ZALO_TEXT_LIMIT = 2e3;
const DEFAULT_MEDIA_MAX_MB = 5;
function logVerbose(core, runtime, message) {
	if (core.logging.shouldLogVerbose()) runtime.log?.(`[zalo] ${message}`);
}
function isSenderAllowed(senderId, allowFrom) {
	if (allowFrom.includes("*")) return true;
	const normalizedSenderId = senderId.toLowerCase();
	return allowFrom.some((entry) => {
		return entry.toLowerCase().replace(/^(zalo|zl):/i, "") === normalizedSenderId;
	});
}
const webhookTargets = /* @__PURE__ */ new Map();
function registerZaloWebhookTarget(target) {
	return registerWebhookTarget(webhookTargets, target).unregister;
}
async function handleZaloWebhookRequest(req, res) {
	const resolved = resolveWebhookTargets(req, webhookTargets);
	if (!resolved) return false;
	const { targets } = resolved;
	if (rejectNonPostWebhookRequest(req, res)) return true;
	const headerToken = String(req.headers["x-bot-api-secret-token"] ?? "");
	const matching = targets.filter((entry) => entry.secret === headerToken);
	if (matching.length === 0) {
		res.statusCode = 401;
		res.end("unauthorized");
		return true;
	}
	if (matching.length > 1) {
		res.statusCode = 401;
		res.end("ambiguous webhook target");
		return true;
	}
	const target = matching[0];
	const body = await readJsonBodyWithLimit(req, {
		maxBytes: 1024 * 1024,
		timeoutMs: 3e4,
		emptyObjectOnEmpty: false
	});
	if (!body.ok) {
		res.statusCode = body.code === "PAYLOAD_TOO_LARGE" ? 413 : body.code === "REQUEST_BODY_TIMEOUT" ? 408 : 400;
		res.end(body.code === "REQUEST_BODY_TIMEOUT" ? requestBodyErrorToText("REQUEST_BODY_TIMEOUT") : body.error);
		return true;
	}
	const raw = body.value;
	const record = raw && typeof raw === "object" ? raw : null;
	const update = record && record.ok === true && record.result ? record.result : record ?? void 0;
	if (!update?.event_name) {
		res.statusCode = 400;
		res.end("invalid payload");
		return true;
	}
	target.statusSink?.({ lastInboundAt: Date.now() });
	processUpdate(update, target.token, target.account, target.config, target.runtime, target.core, target.mediaMaxMb, target.statusSink, target.fetcher).catch((err) => {
		target.runtime.error?.(`[${target.account.accountId}] Zalo webhook failed: ${String(err)}`);
	});
	res.statusCode = 200;
	res.end("ok");
	return true;
}
function startPollingLoop(params) {
	const { token, account, config, runtime, core, abortSignal, isStopped, mediaMaxMb, statusSink, fetcher } = params;
	const pollTimeout = 30;
	const poll = async () => {
		if (isStopped() || abortSignal.aborted) return;
		try {
			const response = await getUpdates(token, { timeout: pollTimeout }, fetcher);
			if (response.ok && response.result) {
				statusSink?.({ lastInboundAt: Date.now() });
				await processUpdate(response.result, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher);
			}
		} catch (err) {
			if (err instanceof ZaloApiError && err.isPollingTimeout) {} else if (!isStopped() && !abortSignal.aborted) {
				console.error(`[${account.accountId}] Zalo polling error:`, err);
				await new Promise((resolve) => setTimeout(resolve, 5e3));
			}
		}
		if (!isStopped() && !abortSignal.aborted) setImmediate(poll);
	};
	poll();
}
async function processUpdate(update, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher) {
	const { event_name, message } = update;
	if (!message) return;
	switch (event_name) {
		case "message.text.received":
			await handleTextMessage(message, token, account, config, runtime, core, statusSink, fetcher);
			break;
		case "message.image.received":
			await handleImageMessage(message, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher);
			break;
		case "message.sticker.received":
			console.log(`[${account.accountId}] Received sticker from ${message.from.id}`);
			break;
		case "message.unsupported.received":
			console.log(`[${account.accountId}] Received unsupported message type from ${message.from.id}`);
			break;
	}
}
async function handleTextMessage(message, token, account, config, runtime, core, statusSink, fetcher) {
	const { text } = message;
	if (!text?.trim()) return;
	await processMessageWithPipeline({
		message,
		token,
		account,
		config,
		runtime,
		core,
		text,
		mediaPath: void 0,
		mediaType: void 0,
		statusSink,
		fetcher
	});
}
async function handleImageMessage(message, token, account, config, runtime, core, mediaMaxMb, statusSink, fetcher) {
	const { photo, caption } = message;
	let mediaPath;
	let mediaType;
	if (photo) try {
		const maxBytes = mediaMaxMb * 1024 * 1024;
		const fetched = await core.channel.media.fetchRemoteMedia({ url: photo });
		const saved = await core.channel.media.saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", maxBytes);
		mediaPath = saved.path;
		mediaType = saved.contentType;
	} catch (err) {
		console.error(`[${account.accountId}] Failed to download Zalo image:`, err);
	}
	await processMessageWithPipeline({
		message,
		token,
		account,
		config,
		runtime,
		core,
		text: caption,
		mediaPath,
		mediaType,
		statusSink,
		fetcher
	});
}
async function processMessageWithPipeline(params) {
	const { message, token, account, config, runtime, core, text, mediaPath, mediaType, statusSink, fetcher } = params;
	const { from, chat, message_id, date } = message;
	const isGroup = chat.chat_type === "GROUP";
	const chatId = chat.id;
	const senderId = from.id;
	const senderName = from.name;
	const dmPolicy = account.config.dmPolicy ?? "pairing";
	const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
	const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
	const { senderAllowedForCommands, commandAuthorized } = await resolveSenderCommandAuthorization({
		cfg: config,
		rawBody,
		isGroup,
		dmPolicy,
		configuredAllowFrom: configAllowFrom,
		senderId,
		isSenderAllowed,
		readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalo"),
		shouldComputeCommandAuthorized: (body, cfg) => core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
		resolveCommandAuthorizedFromAuthorizers: (params) => core.channel.commands.resolveCommandAuthorizedFromAuthorizers(params)
	});
	if (!isGroup) {
		if (dmPolicy === "disabled") {
			logVerbose(core, runtime, `Blocked zalo DM from ${senderId} (dmPolicy=disabled)`);
			return;
		}
		if (dmPolicy !== "open") {
			if (!senderAllowedForCommands) {
				if (dmPolicy === "pairing") {
					const { code, created } = await core.channel.pairing.upsertPairingRequest({
						channel: "zalo",
						id: senderId,
						meta: { name: senderName ?? void 0 }
					});
					if (created) {
						logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
						try {
							await sendMessage(token, {
								chat_id: chatId,
								text: core.channel.pairing.buildPairingReply({
									channel: "zalo",
									idLine: `Your Zalo user id: ${senderId}`,
									code
								})
							}, fetcher);
							statusSink?.({ lastOutboundAt: Date.now() });
						} catch (err) {
							logVerbose(core, runtime, `zalo pairing reply failed for ${senderId}: ${String(err)}`);
						}
					}
				} else logVerbose(core, runtime, `Blocked unauthorized zalo sender ${senderId} (dmPolicy=${dmPolicy})`);
				return;
			}
		}
	}
	const route = core.channel.routing.resolveAgentRoute({
		cfg: config,
		channel: "zalo",
		accountId: account.accountId,
		peer: {
			kind: isGroup ? "group" : "direct",
			id: chatId
		}
	});
	if (isGroup && core.channel.commands.isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
		logVerbose(core, runtime, `zalo: drop control command from unauthorized sender ${senderId}`);
		return;
	}
	const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
	const storePath = core.channel.session.resolveStorePath(config.session?.store, { agentId: route.agentId });
	const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
	const previousTimestamp = core.channel.session.readSessionUpdatedAt({
		storePath,
		sessionKey: route.sessionKey
	});
	const body = core.channel.reply.formatAgentEnvelope({
		channel: "Zalo",
		from: fromLabel,
		timestamp: date ? date * 1e3 : void 0,
		previousTimestamp,
		envelope: envelopeOptions,
		body: rawBody
	});
	const ctxPayload = core.channel.reply.finalizeInboundContext({
		Body: body,
		BodyForAgent: rawBody,
		RawBody: rawBody,
		CommandBody: rawBody,
		From: isGroup ? `zalo:group:${chatId}` : `zalo:${senderId}`,
		To: `zalo:${chatId}`,
		SessionKey: route.sessionKey,
		AccountId: route.accountId,
		ChatType: isGroup ? "group" : "direct",
		ConversationLabel: fromLabel,
		SenderName: senderName || void 0,
		SenderId: senderId,
		CommandAuthorized: commandAuthorized,
		Provider: "zalo",
		Surface: "zalo",
		MessageSid: message_id,
		MediaPath: mediaPath,
		MediaType: mediaType,
		MediaUrl: mediaPath,
		OriginatingChannel: "zalo",
		OriginatingTo: `zalo:${chatId}`
	});
	await core.channel.session.recordInboundSession({
		storePath,
		sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
		ctx: ctxPayload,
		onRecordError: (err) => {
			runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
		}
	});
	const tableMode = core.channel.text.resolveMarkdownTableMode({
		cfg: config,
		channel: "zalo",
		accountId: account.accountId
	});
	const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
		cfg: config,
		agentId: route.agentId,
		channel: "zalo",
		accountId: account.accountId
	});
	await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
		ctx: ctxPayload,
		cfg: config,
		dispatcherOptions: {
			...prefixOptions,
			deliver: async (payload) => {
				await deliverZaloReply({
					payload,
					token,
					chatId,
					runtime,
					core,
					config,
					accountId: account.accountId,
					statusSink,
					fetcher,
					tableMode
				});
			},
			onError: (err, info) => {
				runtime.error?.(`[${account.accountId}] Zalo ${info.kind} reply failed: ${String(err)}`);
			}
		},
		replyOptions: { onModelSelected }
	});
}
async function deliverZaloReply(params) {
	const { payload, token, chatId, runtime, core, config, accountId, statusSink, fetcher } = params;
	const tableMode = params.tableMode ?? "code";
	const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
	const mediaList = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
	if (mediaList.length > 0) {
		let first = true;
		for (const mediaUrl of mediaList) {
			const caption = first ? text : void 0;
			first = false;
			try {
				await sendPhoto(token, {
					chat_id: chatId,
					photo: mediaUrl,
					caption
				}, fetcher);
				statusSink?.({ lastOutboundAt: Date.now() });
			} catch (err) {
				runtime.error?.(`Zalo photo send failed: ${String(err)}`);
			}
		}
		return;
	}
	if (text) {
		const chunkMode = core.channel.text.resolveChunkMode(config, "zalo", accountId);
		const chunks = core.channel.text.chunkMarkdownTextWithMode(text, ZALO_TEXT_LIMIT, chunkMode);
		for (const chunk of chunks) try {
			await sendMessage(token, {
				chat_id: chatId,
				text: chunk
			}, fetcher);
			statusSink?.({ lastOutboundAt: Date.now() });
		} catch (err) {
			runtime.error?.(`Zalo message send failed: ${String(err)}`);
		}
	}
}
async function monitorZaloProvider(options) {
	const { token, account, config, runtime, abortSignal, useWebhook, webhookUrl, webhookSecret, webhookPath, statusSink, fetcher: fetcherOverride } = options;
	const core = getZaloRuntime();
	const effectiveMediaMaxMb = account.config.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
	const fetcher = fetcherOverride ?? resolveZaloProxyFetch(account.config.proxy);
	let stopped = false;
	const stopHandlers = [];
	const stop = () => {
		stopped = true;
		for (const handler of stopHandlers) handler();
	};
	if (useWebhook) {
		if (!webhookUrl || !webhookSecret) throw new Error("Zalo webhookUrl and webhookSecret are required for webhook mode");
		if (!webhookUrl.startsWith("https://")) throw new Error("Zalo webhook URL must use HTTPS");
		if (webhookSecret.length < 8 || webhookSecret.length > 256) throw new Error("Zalo webhook secret must be 8-256 characters");
		const path = resolveWebhookPath({
			webhookPath,
			webhookUrl,
			defaultPath: null
		});
		if (!path) throw new Error("Zalo webhookPath could not be derived");
		await setWebhook(token, {
			url: webhookUrl,
			secret_token: webhookSecret
		}, fetcher);
		const unregister = registerZaloWebhookTarget({
			token,
			account,
			config,
			runtime,
			core,
			path,
			secret: webhookSecret,
			statusSink: (patch) => statusSink?.(patch),
			mediaMaxMb: effectiveMediaMaxMb,
			fetcher
		});
		stopHandlers.push(unregister);
		abortSignal.addEventListener("abort", () => {
			deleteWebhook(token, fetcher).catch(() => {});
		}, { once: true });
		return { stop };
	}
	try {
		await deleteWebhook(token, fetcher);
	} catch {}
	startPollingLoop({
		token,
		account,
		config,
		runtime,
		core,
		abortSignal,
		isStopped: () => stopped,
		mediaMaxMb: effectiveMediaMaxMb,
		statusSink,
		fetcher
	});
	return { stop };
}

//#endregion
export { ZaloApiError as a, sendPhoto as c, resolveZaloProxyFetch as i, monitor_exports as n, getMe as o, setZaloRuntime as r, sendMessage as s, handleZaloWebhookRequest as t };