import { createLogger } from "../../../src/utils/logger/index.js";
import { init_logger } from "../../../src/utils/logger.js";
import { normalizeTelegramCommandName, parseSlashCommand } from "../../../src/chat-commands/command-parse.js";
import { checkMentionInTranscription } from "../../../src/voice/stt/preflight.js";
import { tryConsumeTelegramClarifyFreeText } from "../../../src/gateway/clarify-runtime.js";
import { buildTelegramUpdateKey, telegramUpdateDedupe } from "./dedupe.js";
//#region extensions/telegram/src/inbound-processor.ts
init_logger();
const log = createLogger("TelegramInboundProcessor");
/** Must bypass the per-chat queue so /abort can cancel an in-flight turn. */
function isAbortSlashCommand(text) {
	const parsed = parseSlashCommand(text);
	if (!parsed) return false;
	const cmd = normalizeTelegramCommandName(parsed.command);
	return cmd === "abort" || cmd === "stop" || cmd === "cancel";
}
/** Maximum voice message duration for STT in seconds */
const STT_MAX_VOICE_DURATION_SECONDS = 60;
/**
* Extract media items from message
*/
function extractMediaItems(message) {
	const media = [];
	if (message.photo?.length) media.push({
		type: "photo",
		fileId: message.photo[message.photo.length - 1].file_id
	});
	if (message.document) media.push({
		type: "document",
		fileId: message.document.file_id
	});
	if (message.video) media.push({
		type: "video",
		fileId: message.video.file_id
	});
	if (message.audio) media.push({
		type: "audio",
		fileId: message.audio.file_id
	});
	if (message.voice) media.push({
		type: "voice",
		fileId: message.voice.file_id
	});
	return media;
}
/**
* Download + STT a Telegram voice message (for mention probe or media pipeline).
*/
async function downloadAndTranscribeTelegramVoice(params) {
	const { voice, bot, botToken, accountApiRoot, sttService, sttConfig } = params;
	if ((voice.duration || 0) > STT_MAX_VOICE_DURATION_SECONDS) return "";
	if (!sttService.isSTTAvailable(sttConfig)) return "";
	try {
		const downloadUrl = `${accountApiRoot}/file/bot${botToken}/${(await bot.api.getFile(voice.file_id)).file_path}`;
		const response = await fetch(downloadUrl);
		if (!response.ok) return "";
		const buffer = Buffer.from(await response.arrayBuffer());
		if (buffer.length === 0) return "";
		return (await sttService.transcribe(buffer, sttConfig, { language: sttConfig?.provider === "alibaba" ? "zh" : void 0 })).text?.trim() ?? "";
	} catch {
		return "";
	}
}
/**
* Process a single media item (download + optional STT)
*/
async function processMediaItem(item, bot, botToken, accountApiRoot, message, sttService, mediaUtils, sttConfig, reuseVoiceTranscript) {
	try {
		const file = await bot.api.getFile(item.fileId);
		const downloadUrl = `${accountApiRoot}/file/bot${botToken}/${file.file_path}`;
		const response = await fetch(downloadUrl);
		log.info({
			type: item.type,
			fileId: item.fileId,
			downloadUrl,
			responseStatus: response.status
		}, "Media download response");
		if (!response.ok) throw new Error(`Failed to download: ${response.status}`);
		const buffer = await response.arrayBuffer();
		log.info({
			type: item.type,
			fileId: item.fileId,
			bufferSize: buffer.byteLength,
			filePath: file.file_path
		}, "Media buffer downloaded");
		if (buffer.byteLength === 0) log.warn({
			type: item.type,
			fileId: item.fileId,
			filePath: file.file_path
		}, "Media buffer is empty, may cause issues");
		let transcribedText = "";
		if (item.type === "voice" && sttService.isSTTAvailable(sttConfig)) if (reuseVoiceTranscript !== void 0 && reuseVoiceTranscript !== "") transcribedText = reuseVoiceTranscript;
		else if ((message.voice?.duration || 0) <= STT_MAX_VOICE_DURATION_SECONDS) try {
			transcribedText = (await sttService.transcribe(Buffer.from(buffer), sttConfig, { language: sttConfig?.provider === "alibaba" ? "zh" : void 0 })).text;
		} catch (sttError) {
			log.error({ sttError }, "STT transcription failed");
			transcribedText = "[STT failed]";
		}
		else transcribedText = `[Voice message too long (>${STT_MAX_VOICE_DURATION_SECONDS}s)]`;
		const base64 = Buffer.from(buffer).toString("base64");
		const mimeType = mediaUtils.getMimeType(item.type, file.file_path);
		log.info({
			type: item.type,
			mimeType,
			base64Length: base64.length,
			size: buffer.byteLength,
			name: file.file_path?.split("/").pop()
		}, "Attachment created");
		return {
			attachment: {
				type: item.type,
				mimeType,
				data: base64,
				name: file.file_path?.split("/").pop(),
				size: buffer.byteLength
			},
			transcribedText
		};
	} catch (err) {
		log.error({
			type: item.type,
			fileId: item.fileId,
			err
		}, "Failed to download media");
		return {
			attachment: null,
			transcribedText: ""
		};
	}
}
/**
* Process all media items
*/
async function processAllMedia(media, bot, botToken, accountApiRoot, message, sttService, mediaUtils, sttConfig, reuseVoiceTranscript) {
	const attachments = [];
	let transcribedText = "";
	let voiceReuseConsumed = false;
	for (const item of media) {
		const reuse = item.type === "voice" && reuseVoiceTranscript && !voiceReuseConsumed ? reuseVoiceTranscript : void 0;
		if (reuse) voiceReuseConsumed = true;
		const result = await processMediaItem(item, bot, botToken, accountApiRoot, message, sttService, mediaUtils, sttConfig, reuse);
		if (result.attachment) attachments.push(result.attachment);
		if (result.transcribedText) transcribedText = result.transcribedText;
	}
	return {
		attachments,
		transcribedText
	};
}
/**
* Create inbound message processor
*/
function createInboundProcessor(deps) {
	const { bus, config, accountManager, accessControl, sessionKeyService, sttService, mediaUtils } = deps;
	const messageQueues = /* @__PURE__ */ new Map();
	const processingLocks = /* @__PURE__ */ new Map();
	const getChatKey = (accountId, chatId) => `${accountId}:${chatId}`;
	const processNextMessage = async (chatKey) => {
		if (processingLocks.has(chatKey)) return;
		const queue = messageQueues.get(chatKey);
		if (!queue || queue.length === 0) return;
		let lockResolve;
		const lockPromise = new Promise((resolve) => {
			lockResolve = resolve;
		});
		processingLocks.set(chatKey, lockPromise);
		const { ctx, accountId, resolve, reject } = queue.shift();
		try {
			await processMessageInternal(ctx, accountId);
			resolve();
		} catch (err) {
			reject(err instanceof Error ? err : new Error(String(err)));
		} finally {
			lockResolve?.();
			processingLocks.delete(chatKey);
			if (queue.length > 0) processNextMessage(chatKey);
			else messageQueues.delete(chatKey);
		}
	};
	const processMessageInternal = async (ctx, accountId) => {
		const updateKey = buildTelegramUpdateKey(ctx);
		if (updateKey && telegramUpdateDedupe.checkAndAdd(updateKey)) {
			log.debug({
				updateKey,
				accountId
			}, "Duplicate update detected, skipping");
			return;
		}
		const account = accountManager.getAccount(accountId);
		if (!account) {
			log.warn({ accountId }, "Account not found for message processing");
			return;
		}
		const botUsername = accountManager.getBotUsername(accountId);
		if (!botUsername) {
			log.warn({ accountId }, "Bot username not available");
			return;
		}
		const message = ctx.message;
		if (!message) return;
		const chatId = String(ctx.chat?.id);
		const senderId = String(ctx.from?.id);
		const senderUsername = ctx.from?.username;
		const content = message.text ?? message.caption ?? "";
		const isGroup = ctx.chat?.type === "group" || ctx.chat?.type === "supergroup";
		const threadId = message.message_thread_id;
		const effectiveAllowFrom = accessControl.normalizeAllowFromWithStore({ allowFrom: isGroup ? account.groupAllowFrom : account.allowFrom });
		const baseAccess = accessControl.evaluateGroupBaseAccess({
			isGroup,
			groupConfig: account.groups?.[chatId],
			topicConfig: threadId ? account.groups?.[chatId]?.topics?.[String(threadId)] : void 0,
			hasGroupAllowOverride: !!(account.groups?.[chatId]?.allowFrom || threadId && account.groups?.[chatId]?.topics?.[String(threadId)]?.allowFrom),
			effectiveGroupAllow: effectiveAllowFrom,
			senderId,
			senderUsername
		});
		if (!baseAccess.allowed) {
			log.debug({
				accountId,
				chatId,
				reason: baseAccess.reason
			}, "Message blocked by base access");
			return;
		}
		const botEarly = accountManager.getBot(accountId);
		const botTokenEarly = account.botToken;
		const accountApiRootEarly = account.apiRoot?.replace(/\/$/, "") || "https://api.telegram.org";
		let voiceProbeText = "";
		if (isGroup && message.voice && botEarly && botTokenEarly && sttService.isSTTAvailable(config?.tools?.media?.audio)) voiceProbeText = await downloadAndTranscribeTelegramVoice({
			voice: message.voice,
			bot: botEarly,
			botToken: botTokenEarly,
			accountApiRoot: accountApiRootEarly,
			sttService,
			sttConfig: config?.tools?.media?.audio
		});
		if (isGroup) {
			const requireMention = accessControl.resolveRequireMention({
				topicConfig: threadId ? account.groups?.[chatId]?.topics?.[String(threadId)] : void 0,
				groupConfig: account.groups?.[chatId],
				defaultRequireMention: true
			});
			const hasMedia = !!(message.photo || message.document || message.video || message.audio || message.voice);
			const captionEntities = message.caption_entities;
			const voiceMention = voiceProbeText.trim();
			const hasMention = accessControl.hasBotMention({
				botUsername,
				text: content,
				entities: message.entities
			}) || hasMedia && accessControl.hasBotMention({
				botUsername,
				text: message.caption ?? "",
				entities: captionEntities
			}) || voiceMention.length > 0 && (accessControl.hasBotMention({
				botUsername,
				text: voiceMention
			}) || checkMentionInTranscription(voiceMention, [botUsername]));
			if (requireMention && !hasMention) {
				if (!!message.voice && !message.photo && !message.document && !message.video && !message.audio && !(message.caption ?? "").trim() && !content.trim()) {
					log.debug({
						accountId,
						chatId
					}, "Group voice without mention after STT probe, ignored");
					return;
				}
				if (!hasMedia || content.trim().length > 0) {
					log.debug({
						accountId,
						chatId
					}, "Group message without mention ignored");
					return;
				}
				log.debug({
					accountId,
					chatId
				}, "Group media message without mention - processing anyway");
			}
		}
		const cleanContent = isGroup ? accessControl.removeBotMention(content, botUsername) : content;
		const sessionKey = sessionKeyService.generateSessionKey({
			source: "telegram",
			chatId,
			senderId,
			isGroup,
			threadId: threadId ? String(threadId) : void 0,
			accountId
		});
		const media = extractMediaItems(message);
		const bot = accountManager.getBot(accountId);
		const botToken = account.botToken;
		let attachments = [];
		let transcribedText = "";
		if (bot && botToken && media.length > 0) {
			const mediaResult = await processAllMedia(media, bot, botToken, account.apiRoot?.replace(/\/$/, "") || "https://api.telegram.org", message, sttService, mediaUtils, config?.tools?.media?.audio, voiceProbeText || void 0);
			attachments = mediaResult.attachments;
			transcribedText = mediaResult.transcribedText;
		}
		const captionIsCommand = cleanContent.trim().startsWith("/");
		const finalContent = transcribedText ? captionIsCommand && cleanContent ? cleanContent.trim() + (transcribedText ? `\n\n${transcribedText}` : "") : transcribedText + (cleanContent ? "\n\n" + cleanContent : "") : cleanContent;
		const isCommand = cleanContent.trim().startsWith("/");
		log.info({
			accountId,
			chatId,
			senderId,
			isGroup,
			sessionKey,
			contentLength: finalContent.length,
			attachmentCount: attachments.length,
			isCommand
		}, "Processing Telegram message");
		if (finalContent.trim().length > 0 && !isCommand && tryConsumeTelegramClarifyFreeText(sessionKey, finalContent.trim())) {
			log.debug({ sessionKey }, "Telegram: consumed message as clarify reply");
			return;
		}
		await bus.publishInbound({
			channel: "telegram",
			sender_id: senderId,
			chat_id: chatId,
			content: finalContent,
			metadata: {
				accountId,
				sessionKey,
				senderUsername,
				messageId: String(message.message_id),
				isGroup,
				isCommand,
				threadId: threadId ? String(threadId) : void 0,
				media: media.length > 0 ? media : void 0,
				transcribedVoice: !!transcribedText || void 0
			},
			attachments: attachments.length > 0 ? attachments : void 0
		});
	};
	return (ctx, accountId) => {
		const message = ctx.message;
		if (message) {
			const raw = message.text ?? message.caption ?? "";
			if (raw.trim() && isAbortSlashCommand(raw.trim())) return processMessageInternal(ctx, accountId);
		}
		const chatKey = getChatKey(accountId, String(ctx.chat?.id));
		return new Promise((resolve, reject) => {
			const queue = messageQueues.get(chatKey) || [];
			queue.push({
				ctx,
				accountId,
				resolve,
				reject
			});
			messageQueues.set(chatKey, queue);
			processNextMessage(chatKey);
		});
	};
}
//#endregion
export { createInboundProcessor };

//# sourceMappingURL=inbound-processor.js.map