import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, extname, join } from "node:path";
import { z } from "zod";
import { transcribeMistralAudio } from "../utils/mistral_transcription.js";
import { formatZodError } from "../utils/zod.js";
import { AsyncSessionManagerError, createScopedAsyncSessionManager, } from "./session_manager.js";
const DEFAULT_POLL_INTERVAL_MS = 1000;
const DEFAULT_REQUEST_TIMEOUT_SECONDS = 30;
const MAX_COMMAND_PREVIEW_CHARS = 128;
const DEFAULT_TELEGRAM_VOICE_MIME_TYPE = "audio/ogg";
const DEFAULT_TELEGRAM_VOICE_FILE_NAME = "voice.ogg";
const DEFAULT_TELEGRAM_AUDIO_MIME_TYPE = "audio/mpeg";
const DEFAULT_TELEGRAM_AUDIO_FILE_NAME = "audio.mp3";
const DEFAULT_TELEGRAM_PHOTO_MIME_TYPE = "image/jpeg";
const DEFAULT_TELEGRAM_DOCUMENT_MIME_TYPE = "application/octet-stream";
const MESSAGE_QUEUED_REACTION_EMOJI = "👀";
const MESSAGE_QUEUED_REACTION_DELAY_MS = 1000;
const TELEGRAM_MAX_MESSAGE_BYTES = 4096;
const TELEGRAM_MESSAGE_BYTE_BUFFER_RATIO = 0.95;
const TELEGRAM_SAFE_MESSAGE_BYTES = Math.floor(TELEGRAM_MAX_MESSAGE_BYTES * TELEGRAM_MESSAGE_BYTE_BUFFER_RATIO);
const TELEGRAM_MESSAGE_SPLIT_DELAY_MS = 1000;
const ABORTED = Symbol("aborted");
const CALLBACK_ACTION_PREFIX = "tau:action:";
const CALLBACK_USE_PREFIX = "tau:use:";
const MAX_SESSION_PREVIEW_CHARS = 64;
const MAX_TELEGRAM_ATTACHMENTS_PER_TURN = 10;
const MAX_TELEGRAM_ATTACHMENT_FILE_BYTES = 20 * 1024 * 1024;
const MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES = 50 * 1024 * 1024;
const TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX = "tau-telegram-attachments-";
const NO_ACTIVE_SESSION_MESSAGE = "no active session. use /new or /sessions";
const SUPPORTED_TEXT_ATTACHMENT_EXTENSIONS = new Set([
    ".txt",
    ".md",
    ".json",
    ".csv",
    ".yaml",
    ".yml",
]);
const SUPPORTED_IMAGE_ATTACHMENT_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif"]);
const SUPPORTED_TEXT_ATTACHMENT_MIME_TYPES = new Set([
    "text/plain",
    "text/markdown",
    "application/json",
    "text/csv",
    "application/csv",
    "text/yaml",
    "application/yaml",
    "application/x-yaml",
    "text/x-yaml",
]);
const MIME_EXTENSION_BY_TYPE = {
    "application/pdf": ".pdf",
    "application/json": ".json",
    "text/markdown": ".md",
    "text/csv": ".csv",
    "application/csv": ".csv",
    "text/plain": ".txt",
    "text/yaml": ".yaml",
    "application/yaml": ".yaml",
    "application/x-yaml": ".yaml",
    "text/x-yaml": ".yaml",
    "image/jpeg": ".jpg",
    "image/png": ".png",
    "image/webp": ".webp",
    "image/gif": ".gif",
};
const MIME_TYPE_BY_EXTENSION = {
    ".pdf": "application/pdf",
    ".txt": "text/plain",
    ".md": "text/markdown",
    ".json": "application/json",
    ".csv": "text/csv",
    ".yaml": "text/yaml",
    ".yml": "text/yaml",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
    ".webp": "image/webp",
    ".gif": "image/gif",
};
async function sweepStaleTelegramAttachmentTempDirs() {
    const systemTmpDir = tmpdir();
    try {
        const entries = await readdir(systemTmpDir, { withFileTypes: true, encoding: "utf8" });
        for (const entry of entries) {
            if (!entry.isDirectory() || !entry.name.startsWith(TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX)) {
                continue;
            }
            try {
                await rm(join(systemTmpDir, entry.name), { recursive: true, force: true });
            }
            catch { }
        }
    }
    catch {
        return;
    }
}
const QUICK_ACTION_ROWS = [
    ["new", "sessions", "status"],
    ["interrupt", "close"],
    ["quiet", "verbose"],
];
function splitCommandText(text) {
    return text
        .trim()
        .split(/\s+/)
        .map((part) => part.trim())
        .filter(Boolean);
}
function stripCommandMention(command) {
    const mentionIndex = command.indexOf("@");
    if (mentionIndex === -1) {
        return command;
    }
    return command.slice(0, mentionIndex);
}
function truncateText(text, maxChars) {
    const trimmed = text.trim();
    if (trimmed.length <= maxChars) {
        return trimmed;
    }
    if (maxChars <= 1) {
        return "…";
    }
    return `${trimmed.slice(0, maxChars - 1)}…`;
}
function splitTelegramMessage(text) {
    if (Buffer.byteLength(text, "utf8") <= TELEGRAM_SAFE_MESSAGE_BYTES) {
        return [text];
    }
    const chunks = [];
    let currentChunk = "";
    for (const character of text) {
        const nextChunk = `${currentChunk}${character}`;
        if (Buffer.byteLength(nextChunk, "utf8") <= TELEGRAM_SAFE_MESSAGE_BYTES) {
            currentChunk = nextChunk;
            continue;
        }
        if (currentChunk) {
            chunks.push(currentChunk);
            currentChunk = character;
            continue;
        }
        chunks.push(character);
    }
    if (currentChunk) {
        chunks.push(currentChunk);
    }
    return chunks;
}
function normalizeSizeBytes(value) {
    if (typeof value !== "number" || !Number.isFinite(value)) {
        return undefined;
    }
    const rounded = Math.floor(value);
    if (rounded <= 0) {
        return undefined;
    }
    return rounded;
}
function selectLargestPhotoVariant(variants) {
    if (!variants || variants.length === 0) {
        return undefined;
    }
    let selected;
    let selectedScore = -1;
    for (const variant of variants) {
        const fileId = variant.file_id?.trim();
        if (!fileId) {
            continue;
        }
        const fileSize = normalizeSizeBytes(variant.file_size);
        const width = normalizeSizeBytes(variant.width) ?? 0;
        const height = normalizeSizeBytes(variant.height) ?? 0;
        const resolutionScore = width * height;
        const sizeScore = fileSize ?? 0;
        const score = Math.max(sizeScore, resolutionScore);
        if (!selected || score >= selectedScore) {
            selected = variant;
            selectedScore = score;
        }
    }
    const selectedFileId = selected?.file_id?.trim();
    if (!selectedFileId) {
        return undefined;
    }
    return {
        fileId: selectedFileId,
        sizeBytes: normalizeSizeBytes(selected?.file_size),
    };
}
function inferExtensionFromMimeType(mimeType) {
    const trimmed = mimeType.trim().toLowerCase();
    if (!trimmed) {
        return undefined;
    }
    return MIME_EXTENSION_BY_TYPE[trimmed];
}
function sanitizeAttachmentFileName(fileName, fallback) {
    const trimmed = basename(fileName.trim());
    const normalized = trimmed
        .replace(/\s+/g, "-")
        .replace(/[^a-zA-Z0-9._-]/g, "-")
        .replace(/-+/g, "-")
        .replace(/^[-.]+/, "")
        .replace(/[-.]+$/, "");
    if (!normalized) {
        return fallback;
    }
    return normalized;
}
function inferMimeTypeFromFileName(fileName) {
    const extension = extname(fileName.trim()).toLowerCase();
    if (!extension) {
        return undefined;
    }
    return MIME_TYPE_BY_EXTENSION[extension];
}
function isSupportedDocumentAttachment(mimeType, fileName) {
    const normalizedMimeType = mimeType.trim().toLowerCase();
    if (normalizedMimeType.startsWith("image/")) {
        return true;
    }
    if (normalizedMimeType === "application/pdf") {
        return true;
    }
    if (SUPPORTED_TEXT_ATTACHMENT_MIME_TYPES.has(normalizedMimeType)) {
        return true;
    }
    const extension = extname(fileName.trim()).toLowerCase();
    if (SUPPORTED_TEXT_ATTACHMENT_EXTENSIONS.has(extension)) {
        return true;
    }
    if (extension === ".pdf") {
        return true;
    }
    return SUPPORTED_IMAGE_ATTACHMENT_EXTENSIONS.has(extension);
}
function describeAttachmentLimitBytes(sizeBytes) {
    const megabytes = sizeBytes / (1024 * 1024);
    if (Number.isInteger(megabytes)) {
        return `${megabytes} MB`;
    }
    return `${megabytes.toFixed(1)} MB`;
}
function describeAttachment(fileName, mimeType) {
    const trimmedFileName = fileName.trim();
    const trimmedMimeType = mimeType.trim();
    if (trimmedFileName) {
        return `'${trimmedFileName}'`;
    }
    if (trimmedMimeType) {
        return `'${trimmedMimeType}'`;
    }
    return "attachment";
}
function describeSession(session, details = {}) {
    return [
        formatSessionHeadline(session.id, "status"),
        `project: ${session.projectId}`,
        `state: ${session.state}`,
        `verbosity: ${details.verbosity ?? "quiet"}`,
        ...(session.error ? [`error: ${session.error}`] : []),
        ...(details.lastCommand
            ? [`last command: ${truncateText(details.lastCommand, MAX_COMMAND_PREVIEW_CHARS)}`]
            : []),
        ...(details.lastAssistantMessage
            ? ["last assistant message:", details.lastAssistantMessage]
            : []),
    ].join("\n");
}
function formatSessionHeadline(sessionId, label) {
    return `(${sessionId}) ${label}`;
}
const telegramObject = (shape) => z.object(shape).passthrough();
const telegramPartialObject = (shape) => telegramObject(shape).partial();
function parseOrThrow(schema, raw, message) {
    const parsed = schema.safeParse(raw);
    if (!parsed.success) {
        throw new Error(`${message}: ${formatZodError(parsed.error)}`);
    }
    return parsed.data;
}
const TelegramEnvelopeSchema = z.discriminatedUnion("ok", [
    z.object({
        ok: z.literal(true),
        description: z.string().optional(),
        result: z.unknown(),
    }),
    z.object({
        ok: z.literal(false),
        description: z.string().optional(),
        result: z.unknown().optional(),
    }),
]);
const TelegramChatSchema = telegramObject({ id: z.number(), type: z.string() });
const TelegramUserSchema = telegramObject({ id: z.number() });
const TELEGRAM_FILE_SHAPE = {
    file_id: z.string(),
    file_name: z.string(),
    mime_type: z.string(),
    file_size: z.number(),
};
const TelegramFileSchema = telegramPartialObject(TELEGRAM_FILE_SHAPE);
const TelegramPhotoVariantSchema = telegramPartialObject({
    file_id: z.string(),
    file_size: z.number(),
    width: z.number(),
    height: z.number(),
});
const TelegramVoiceSchema = TelegramFileSchema.pick({ file_id: true, mime_type: true });
const TelegramAudioSchema = TelegramFileSchema.pick({
    file_id: true,
    mime_type: true,
    file_name: true,
});
const TelegramMessageSchema = telegramPartialObject({
    message_id: z.number(),
    chat: TelegramChatSchema,
    from: TelegramUserSchema,
    text: z.string(),
    caption: z.string(),
    photo: z.array(TelegramPhotoVariantSchema),
    document: TelegramFileSchema,
    voice: TelegramVoiceSchema,
    audio: TelegramAudioSchema,
});
const TelegramCallbackQuerySchema = telegramPartialObject({
    id: z.string(),
    from: TelegramUserSchema,
    data: z.string(),
    message: telegramPartialObject({ chat: TelegramChatSchema }),
});
const TelegramUpdateSchema = telegramPartialObject({
    update_id: z.number(),
    message: TelegramMessageSchema,
    callback_query: TelegramCallbackQuerySchema,
});
const TelegramGetUpdatesResultSchema = z.array(TelegramUpdateSchema);
const TelegramGetFileResultSchema = z.object({ file_path: z.string() });
const TelegramAckResultSchema = z.literal(true);
function createTelegramApi(botToken) {
    const apiUrl = `https://api.telegram.org/bot${botToken}`;
    async function callTelegramMethod(method, payload, resultSchema) {
        const response = await fetch(`${apiUrl}/${method}`, {
            method: "POST",
            headers: {
                "content-type": "application/json",
            },
            body: JSON.stringify(payload),
        });
        if (!response.ok) {
            const detail = (await response.text()).trim();
            throw new Error(detail || `telegram ${method} failed: HTTP ${response.status}`);
        }
        let raw;
        try {
            raw = await response.json();
        }
        catch {
            throw new Error(`telegram ${method} returned invalid JSON`);
        }
        const envelope = parseOrThrow(TelegramEnvelopeSchema, raw, `telegram ${method} returned an invalid response payload`);
        if (!envelope.ok) {
            const detail = envelope.description?.trim() ?? "";
            throw new Error(detail || `telegram ${method} request failed`);
        }
        return parseOrThrow(resultSchema, envelope.result, `telegram ${method} returned an invalid result`);
    }
    return {
        async getUpdates(args) {
            return callTelegramMethod("getUpdates", {
                offset: args.offset,
                timeout: args.timeoutSeconds,
                allowed_updates: args.allowedUpdates,
            }, TelegramGetUpdatesResultSchema);
        },
        async sendMessage(chatId, text, options) {
            await callTelegramMethod("sendMessage", {
                chat_id: chatId,
                text,
                ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
            }, z.unknown());
        },
        async downloadFile(fileId) {
            const parsed = await callTelegramMethod("getFile", {
                file_id: fileId,
            }, TelegramGetFileResultSchema);
            const filePath = parsed.file_path?.trim();
            if (!filePath) {
                throw new Error("telegram file path is missing");
            }
            const response = await fetch(`https://api.telegram.org/file/bot${botToken}/${filePath}`);
            if (!response.ok) {
                const detail = (await response.text()).trim();
                throw new Error(detail || `telegram file download failed: HTTP ${response.status}`);
            }
            const bytes = await response.arrayBuffer();
            return Buffer.from(bytes);
        },
        async setCommands(commands) {
            await callTelegramMethod("setMyCommands", {
                commands,
            }, TelegramAckResultSchema);
        },
        async setMessageReaction(chatId, messageId) {
            await callTelegramMethod("setMessageReaction", {
                chat_id: chatId,
                message_id: messageId,
                reaction: [{ type: "emoji", emoji: MESSAGE_QUEUED_REACTION_EMOJI }],
            }, TelegramAckResultSchema);
        },
        async answerCallbackQuery(callbackQueryId, text) {
            await callTelegramMethod("answerCallbackQuery", {
                callback_query_id: callbackQueryId,
                ...(text ? { text } : {}),
            }, TelegramAckResultSchema);
        },
    };
}
class AsyncTelegramAdapterImpl {
    projects;
    defaultProjectId;
    systemMessage;
    allowedUserIds;
    allowedChatIds;
    pollIntervalMs;
    requestTimeoutSeconds;
    mistralApiKey;
    sessionManager;
    enforceChatOwnership;
    botOwnerPrefix;
    allowedProjectIds;
    api;
    fetchImpl;
    onLog;
    commandDefinitions;
    commandHandlers;
    callbackActionHandlers;
    abortController = new AbortController();
    activeSessionsByChat = new Map();
    sessionsByChat = new Map();
    chatsBySession = new Map();
    sessionVerbosityBySession = new Map();
    lastCommandBySession = new Map();
    lastAssistantMessageBySession = new Map();
    latestAssistantMessageByRun = new Map();
    pendingAttachmentsBySession = new Map();
    pendingAttachmentTempDirBySession = new Map();
    attachmentTempDirsBySession = new Map();
    updateQueueTailByKey = new Map();
    inFlightUpdateTasks = new Set();
    unsubscribeSessionEvents;
    loopPromise;
    nextUpdateOffset = 0;
    closed = false;
    constructor(options) {
        const botId = options.botId.trim();
        if (!botId) {
            throw new Error("telegram bot id must be a non-empty string");
        }
        this.projects = options.projects;
        this.defaultProjectId = options.defaultProjectId;
        this.systemMessage = options.systemMessage?.trim() || undefined;
        this.allowedUserIds =
            options.allowedUserIds && options.allowedUserIds.length > 0
                ? new Set(options.allowedUserIds)
                : undefined;
        this.allowedChatIds =
            options.allowedChatIds && options.allowedChatIds.length > 0
                ? new Set(options.allowedChatIds)
                : undefined;
        this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
        this.requestTimeoutSeconds = options.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS;
        this.mistralApiKey = options.mistralApiKey?.trim() || undefined;
        this.sessionManager = options.sessionManager;
        this.enforceChatOwnership = true;
        this.botOwnerPrefix = `telegram:${botId}`;
        this.allowedProjectIds = Object.keys(options.projects);
        this.api = options.api ?? createTelegramApi(options.botToken);
        this.fetchImpl = options.fetchImpl;
        this.onLog = options.onLog;
        this.commandDefinitions = this.createCommandDefinitions();
        this.commandHandlers = new Map();
        this.callbackActionHandlers = new Map();
        for (const definition of this.commandDefinitions) {
            this.commandHandlers.set(definition.command, definition.handler);
            if (definition.callbackAction) {
                this.callbackActionHandlers.set(definition.callbackAction, definition.handler);
            }
        }
        this.unsubscribeSessionEvents = this.sessionManager.onEvent((event) => {
            this.onSessionEvent(event);
        });
        void this.syncCommands();
        this.loopPromise = this.runLoop();
        this.log("info", "telegram adapter started", {
            pollIntervalMs: this.pollIntervalMs,
            requestTimeoutSeconds: this.requestTimeoutSeconds,
        });
    }
    async close() {
        if (this.closed) {
            return;
        }
        this.closed = true;
        this.abortController.abort();
        this.unsubscribeSessionEvents();
        try {
            await this.loopPromise;
            await this.waitForInFlightUpdateTasks();
            for (const sessionId of Array.from(this.attachmentTempDirsBySession.keys())) {
                await this.cleanupSessionAttachmentTempDirs(sessionId);
            }
            this.pendingAttachmentsBySession.clear();
            this.pendingAttachmentTempDirBySession.clear();
        }
        finally {
            this.log("info", "telegram adapter stopped");
        }
    }
    log(level, message, data) {
        this.onLog?.({ level, message, ...(data === undefined ? {} : { data }) });
    }
    createCommandDefinitions() {
        return [
            {
                command: "/help",
                description: "show supported commands",
                usage: "/help",
                handler: async (chatId) => this.handleHelp(chatId),
            },
            {
                command: "/new",
                description: "start a new session",
                usage: "/new [projectId]",
                callbackAction: "new",
                handler: async (chatId, args) => this.handleNew(chatId, args),
            },
            {
                command: "/projects",
                description: "list configured projects",
                usage: "/projects",
                handler: async (chatId) => this.handleProjects(chatId),
            },
            {
                command: "/use",
                description: "switch active session",
                usage: "/use <sessionId|prefix|index>",
                handler: async (chatId, args) => this.handleUse(chatId, args),
            },
            {
                command: "/sessions",
                description: "list sessions",
                usage: "/sessions",
                callbackAction: "sessions",
                handler: async (chatId) => this.handleSessions(chatId),
            },
            {
                command: "/status",
                description: "show active session status",
                usage: "/status",
                callbackAction: "status",
                handler: async (chatId) => this.handleStatus(chatId),
            },
            {
                command: "/interrupt",
                description: "interrupt active run",
                usage: "/interrupt",
                callbackAction: "interrupt",
                handler: async (chatId) => this.handleInterrupt(chatId),
            },
            {
                command: "/close",
                description: "close session(s)",
                usage: "/close [<sessionId>|all]",
                callbackAction: "close",
                handler: async (chatId, args) => this.handleClose(chatId, args),
            },
            {
                command: "/verbose",
                description: "stream progress updates",
                usage: "/verbose",
                callbackAction: "verbose",
                handler: async (chatId) => this.handleVerbosityCommand(chatId, "verbose"),
            },
            {
                command: "/quiet",
                description: "only send final assistant message",
                usage: "/quiet",
                callbackAction: "quiet",
                handler: async (chatId) => this.handleVerbosityCommand(chatId, "quiet"),
            },
        ];
    }
    async syncCommands() {
        if (!this.api.setCommands) {
            return;
        }
        const commands = this.commandDefinitions.map((definition) => ({
            command: definition.command.slice(1),
            description: definition.description,
        }));
        try {
            await this.api.setCommands(commands);
            this.log("info", "telegram commands synced", { count: commands.length });
        }
        catch (error) {
            this.log("warn", "failed to sync telegram commands", {
                cause: error instanceof Error ? error.message : String(error),
            });
        }
    }
    async runLoop() {
        while (!this.abortController.signal.aborted) {
            try {
                const updates = await this.getUpdates();
                for (const update of updates) {
                    if (this.abortController.signal.aborted) {
                        return;
                    }
                    const updateId = update.update_id;
                    if (typeof updateId === "number" && Number.isFinite(updateId)) {
                        this.nextUpdateOffset = Math.max(this.nextUpdateOffset, updateId + 1);
                    }
                    this.enqueueUpdate(update);
                }
            }
            catch (error) {
                if (this.abortController.signal.aborted) {
                    return;
                }
                this.log("warn", "telegram poll failed", {
                    cause: error instanceof Error ? error.message : String(error),
                });
                await this.wait(this.pollIntervalMs);
            }
        }
    }
    enqueueUpdate(update) {
        const queueKey = this.getUpdateQueueKey(update);
        const previousTask = this.updateQueueTailByKey.get(queueKey) ?? Promise.resolve();
        const queuedTask = previousTask
            .then(async () => {
            if (this.abortController.signal.aborted) {
                return;
            }
            await this.handleUpdate(update);
        })
            .catch((error) => {
            if (this.abortController.signal.aborted) {
                return;
            }
            const updateId = typeof update.update_id === "number" && Number.isFinite(update.update_id)
                ? update.update_id
                : undefined;
            this.log("warn", "telegram update handling failed", {
                ...(updateId === undefined ? {} : { updateId }),
                cause: error instanceof Error ? error.message : String(error),
            });
        });
        const trackedTask = queuedTask.finally(() => {
            this.inFlightUpdateTasks.delete(trackedTask);
            if (this.updateQueueTailByKey.get(queueKey) === trackedTask) {
                this.updateQueueTailByKey.delete(queueKey);
            }
        });
        this.updateQueueTailByKey.set(queueKey, trackedTask);
        this.inFlightUpdateTasks.add(trackedTask);
    }
    getUpdateQueueKey(update) {
        const messageChatId = update.message?.chat?.id;
        if (typeof messageChatId === "number" && Number.isFinite(messageChatId)) {
            return `chat:${messageChatId}`;
        }
        const callbackChatId = update.callback_query?.message?.chat?.id;
        if (typeof callbackChatId === "number" && Number.isFinite(callbackChatId)) {
            return `chat:${callbackChatId}`;
        }
        return "global";
    }
    async waitForInFlightUpdateTasks() {
        if (this.inFlightUpdateTasks.size === 0) {
            return;
        }
        await Promise.allSettled(Array.from(this.inFlightUpdateTasks));
    }
    async getUpdates() {
        const updates = await this.raceWithAbort(this.api.getUpdates({
            offset: this.nextUpdateOffset,
            timeoutSeconds: this.requestTimeoutSeconds,
            allowedUpdates: ["message", "callback_query"],
        }));
        if (updates === undefined) {
            return [];
        }
        return updates;
    }
    async raceWithAbort(promise) {
        if (this.abortController.signal.aborted) {
            return undefined;
        }
        let abortListener;
        const abortPromise = new Promise((resolve) => {
            abortListener = () => resolve(ABORTED);
            this.abortController.signal.addEventListener("abort", abortListener, { once: true });
        });
        const settled = promise.then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
        const raceResult = await Promise.race([settled, abortPromise]);
        if (abortListener) {
            this.abortController.signal.removeEventListener("abort", abortListener);
        }
        if (raceResult === ABORTED) {
            return undefined;
        }
        if (!raceResult.ok) {
            throw raceResult.error;
        }
        return raceResult.value;
    }
    async handleUpdate(update) {
        const message = update.message;
        if (message) {
            const chat = message.chat;
            if (!chat || chat.type !== "private") {
                return;
            }
            const chatId = chat.id;
            if (!this.isChatAllowed(chatId)) {
                return;
            }
            const userId = message.from?.id;
            if (!this.isUserAllowed(userId)) {
                return;
            }
            const text = typeof message.text === "string" ? message.text.trim() : "";
            const isCommand = text.startsWith("/");
            if (!isCommand) {
                await this.queueMessageAttachments(chatId, message);
            }
            if (text) {
                if (isCommand) {
                    await this.handleCommand(chatId, text);
                    return;
                }
                await this.handleMessage(chatId, text, message.message_id);
                return;
            }
            const audioMessage = this.parseAudioMessage(message);
            if (!audioMessage) {
                return;
            }
            await this.handleAudioMessage(chatId, audioMessage, message.message_id);
            return;
        }
        const callbackQuery = update.callback_query;
        if (!callbackQuery) {
            return;
        }
        const chat = callbackQuery.message?.chat;
        if (!chat || chat.type !== "private") {
            await this.answerCallbackQuery(callbackQuery.id);
            return;
        }
        const chatId = chat.id;
        if (!this.isChatAllowed(chatId)) {
            await this.answerCallbackQuery(callbackQuery.id);
            return;
        }
        if (!this.isUserAllowed(callbackQuery.from?.id)) {
            await this.answerCallbackQuery(callbackQuery.id);
            return;
        }
        const callbackData = callbackQuery.data?.trim();
        if (!callbackData) {
            await this.answerCallbackQuery(callbackQuery.id);
            return;
        }
        const callbackHandled = await this.handleCallback(chatId, callbackData);
        await this.answerCallbackQuery(callbackQuery.id, callbackHandled ? "done" : undefined);
    }
    parseAudioMessage(message) {
        const voiceFileId = message.voice?.file_id?.trim();
        if (voiceFileId) {
            return {
                fileId: voiceFileId,
                mimeType: message.voice?.mime_type?.trim() || DEFAULT_TELEGRAM_VOICE_MIME_TYPE,
                fileName: DEFAULT_TELEGRAM_VOICE_FILE_NAME,
            };
        }
        const audioFileId = message.audio?.file_id?.trim();
        if (!audioFileId) {
            return undefined;
        }
        return {
            fileId: audioFileId,
            mimeType: message.audio?.mime_type?.trim() || DEFAULT_TELEGRAM_AUDIO_MIME_TYPE,
            fileName: message.audio?.file_name?.trim() || DEFAULT_TELEGRAM_AUDIO_FILE_NAME,
        };
    }
    async queueMessageAttachments(chatId, message) {
        const caption = typeof message.caption === "string" ? message.caption.trim() : "";
        const attachmentCaption = caption || undefined;
        const parsedAttachments = [];
        const photo = selectLargestPhotoVariant(message.photo);
        if (photo) {
            const extension = inferExtensionFromMimeType(DEFAULT_TELEGRAM_PHOTO_MIME_TYPE) ?? ".jpg";
            const fileName = sanitizeAttachmentFileName(`photo${extension}`, `photo${extension}`);
            parsedAttachments.push({
                fileId: photo.fileId,
                fileName,
                mimeType: DEFAULT_TELEGRAM_PHOTO_MIME_TYPE,
                sizeBytes: photo.sizeBytes,
                caption: attachmentCaption,
            });
        }
        const documentFileId = message.document?.file_id?.trim();
        if (documentFileId) {
            const rawMimeType = message.document?.mime_type?.trim().toLowerCase();
            const mimeTypeForExtension = rawMimeType && rawMimeType !== DEFAULT_TELEGRAM_DOCUMENT_MIME_TYPE
                ? rawMimeType
                : undefined;
            const inferredExtension = inferExtensionFromMimeType(mimeTypeForExtension ?? "") ?? "";
            const fallbackFileName = `attachment${inferredExtension}`;
            const rawFileName = message.document?.file_name?.trim() || fallbackFileName;
            let fileName = sanitizeAttachmentFileName(rawFileName, fallbackFileName || "attachment");
            if (!extname(fileName) && inferredExtension) {
                fileName = `${fileName}${inferredExtension}`;
            }
            const mimeType = mimeTypeForExtension ??
                inferMimeTypeFromFileName(fileName) ??
                rawMimeType ??
                DEFAULT_TELEGRAM_DOCUMENT_MIME_TYPE;
            if (!isSupportedDocumentAttachment(mimeType, fileName)) {
                await this.reply(chatId, `skipped attachment ${describeAttachment(fileName, mimeType)}: unsupported file type`);
            }
            else {
                parsedAttachments.push({
                    fileId: documentFileId,
                    fileName,
                    mimeType,
                    sizeBytes: normalizeSizeBytes(message.document?.file_size),
                    caption: attachmentCaption,
                });
            }
        }
        if (parsedAttachments.length === 0) {
            return;
        }
        const session = await this.requireActiveOrSingleSession(chatId);
        if (!session) {
            return;
        }
        const pending = this.pendingAttachmentsBySession.get(session.id) ?? [];
        let totalSizeBytes = pending.reduce((total, attachment) => {
            return total + (attachment.materialized?.sizeBytes ?? attachment.declaredSizeBytes ?? 0);
        }, 0);
        for (const attachment of parsedAttachments) {
            const attachmentLabel = describeAttachment(attachment.fileName, attachment.mimeType);
            if (pending.length >= MAX_TELEGRAM_ATTACHMENTS_PER_TURN) {
                await this.replySkippedAttachment(chatId, attachmentLabel, `exceeds attachment limit (${MAX_TELEGRAM_ATTACHMENTS_PER_TURN} files per turn)`);
                continue;
            }
            const declaredFileLimitReason = typeof attachment.sizeBytes === "number"
                ? this.getAttachmentPerFileLimitReason(attachment.sizeBytes)
                : undefined;
            if (declaredFileLimitReason) {
                await this.replySkippedAttachment(chatId, attachmentLabel, declaredFileLimitReason);
                continue;
            }
            const declaredTotalLimitReason = typeof attachment.sizeBytes === "number"
                ? this.getAttachmentTotalLimitReason(totalSizeBytes, attachment.sizeBytes)
                : undefined;
            if (declaredTotalLimitReason) {
                await this.replySkippedAttachment(chatId, attachmentLabel, declaredTotalLimitReason);
                continue;
            }
            let bytes;
            try {
                bytes = await this.api.downloadFile(attachment.fileId);
            }
            catch (error) {
                await this.reply(chatId, `failed to download attachment ${attachmentLabel}: ${this.formatManagerError(error)}`);
                continue;
            }
            const sizeBytes = bytes.byteLength;
            const fileLimitReason = this.getAttachmentPerFileLimitReason(sizeBytes);
            if (fileLimitReason) {
                await this.replySkippedAttachment(chatId, attachmentLabel, fileLimitReason);
                continue;
            }
            const totalLimitReason = this.getAttachmentTotalLimitReason(totalSizeBytes, sizeBytes);
            if (totalLimitReason) {
                await this.replySkippedAttachment(chatId, attachmentLabel, totalLimitReason);
                continue;
            }
            const tempDirPath = await this.getOrCreatePendingAttachmentTempDir(session.id);
            const indexedFileName = `${String(pending.length + 1).padStart(2, "0")}-${attachment.fileName}`;
            const filePath = join(tempDirPath, indexedFileName);
            try {
                await writeFile(filePath, bytes);
            }
            catch (error) {
                await this.reply(chatId, `failed to materialize attachment ${attachmentLabel}: ${this.formatManagerError(error)}`);
                continue;
            }
            pending.push({
                fileId: attachment.fileId,
                fileName: attachment.fileName,
                mimeType: attachment.mimeType,
                declaredSizeBytes: attachment.sizeBytes,
                caption: attachment.caption,
                materialized: {
                    path: filePath,
                    sizeBytes,
                },
            });
            totalSizeBytes += sizeBytes;
        }
        if (pending.length > 0) {
            this.pendingAttachmentsBySession.set(session.id, pending);
        }
    }
    async buildMessageTextWithAttachments(sessionId, text, chatId) {
        const attachments = await this.materializePendingAttachments(sessionId, chatId);
        if (attachments.length === 0) {
            return text;
        }
        return [this.formatAttachmentBlock(attachments), text].join("\n\n");
    }
    formatAttachmentBlock(attachments) {
        const lines = ["attachments:"];
        for (const attachment of attachments) {
            lines.push(`- path: ${attachment.path}`);
            lines.push(`  mime: ${attachment.mimeType}`);
            lines.push(`  size_bytes: ${attachment.sizeBytes}`);
            if (attachment.caption) {
                lines.push(`  caption: ${JSON.stringify(attachment.caption)}`);
            }
        }
        return lines.join("\n");
    }
    async materializePendingAttachments(sessionId, chatId) {
        const pending = this.pendingAttachmentsBySession.get(sessionId);
        if (!pending || pending.length === 0) {
            return [];
        }
        let totalSizeBytes = 0;
        const nextPending = [];
        const readyAttachments = [];
        for (const attachment of pending) {
            const attachmentLabel = describeAttachment(attachment.fileName, attachment.mimeType);
            const materialized = attachment.materialized;
            if (!materialized) {
                await this.reply(chatId, `skipped attachment ${attachmentLabel}: local temp file is missing`);
                continue;
            }
            const fileLimitReason = this.getAttachmentPerFileLimitReason(materialized.sizeBytes);
            if (fileLimitReason) {
                await this.replySkippedAttachment(chatId, attachmentLabel, fileLimitReason);
                continue;
            }
            const totalLimitReason = this.getAttachmentTotalLimitReason(totalSizeBytes, materialized.sizeBytes);
            if (totalLimitReason) {
                await this.replySkippedAttachment(chatId, attachmentLabel, totalLimitReason);
                continue;
            }
            totalSizeBytes += materialized.sizeBytes;
            nextPending.push(attachment);
            readyAttachments.push({
                path: materialized.path,
                mimeType: attachment.mimeType,
                sizeBytes: materialized.sizeBytes,
                caption: attachment.caption,
            });
        }
        if (nextPending.length === 0) {
            this.pendingAttachmentsBySession.delete(sessionId);
        }
        else {
            this.pendingAttachmentsBySession.set(sessionId, nextPending);
        }
        return readyAttachments;
    }
    async getOrCreatePendingAttachmentTempDir(sessionId) {
        const existingPath = this.pendingAttachmentTempDirBySession.get(sessionId);
        if (existingPath) {
            return existingPath;
        }
        const directoryPath = await mkdtemp(join(tmpdir(), TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX));
        this.pendingAttachmentTempDirBySession.set(sessionId, directoryPath);
        const directories = this.attachmentTempDirsBySession.get(sessionId) ?? new Set();
        directories.add(directoryPath);
        this.attachmentTempDirsBySession.set(sessionId, directories);
        return directoryPath;
    }
    resetPendingAttachmentQueue(sessionId) {
        this.pendingAttachmentsBySession.delete(sessionId);
        this.pendingAttachmentTempDirBySession.delete(sessionId);
    }
    clearSessionAttachments(sessionId) {
        this.resetPendingAttachmentQueue(sessionId);
        void this.cleanupSessionAttachmentTempDirs(sessionId);
    }
    async cleanupSessionAttachmentTempDirs(sessionId) {
        const directories = this.attachmentTempDirsBySession.get(sessionId);
        if (!directories || directories.size === 0) {
            return;
        }
        this.attachmentTempDirsBySession.delete(sessionId);
        for (const directoryPath of directories) {
            try {
                await rm(directoryPath, { recursive: true, force: true });
            }
            catch (error) {
                this.log("warn", "failed to clean up telegram attachment temp directory", {
                    sessionId,
                    directoryPath,
                    cause: this.formatManagerError(error),
                });
            }
        }
    }
    isChatAllowed(chatId) {
        if (!this.allowedChatIds) {
            return true;
        }
        return this.allowedChatIds.has(chatId);
    }
    isUserAllowed(userId) {
        if (!this.allowedUserIds) {
            return true;
        }
        if (typeof userId !== "number") {
            return false;
        }
        return this.allowedUserIds.has(userId);
    }
    ownerIdForChat(chatId) {
        if (!this.enforceChatOwnership) {
            return this.botOwnerPrefix;
        }
        return `${this.botOwnerPrefix}:chat:${chatId}`;
    }
    getSessionManagerForChat(chatId) {
        if (!this.enforceChatOwnership) {
            return this.sessionManager;
        }
        return createScopedAsyncSessionManager({
            sessionManager: this.sessionManager,
            ownerId: this.ownerIdForChat(chatId),
            allowedProjectIds: this.allowedProjectIds,
        });
    }
    async handleCommand(chatId, text) {
        const parts = splitCommandText(text);
        const command = stripCommandMention(parts[0] ?? "");
        const args = parts.slice(1);
        const handler = this.commandHandlers.get(command);
        if (!handler) {
            await this.reply(chatId, "unsupported command. use /help");
            return;
        }
        await handler(chatId, args);
    }
    async handleCallback(chatId, callbackData) {
        if (callbackData.startsWith(CALLBACK_USE_PREFIX)) {
            const sessionId = callbackData.slice(CALLBACK_USE_PREFIX.length).trim();
            if (!sessionId) {
                return false;
            }
            await this.handleUse(chatId, [sessionId]);
            return true;
        }
        if (!callbackData.startsWith(CALLBACK_ACTION_PREFIX)) {
            return false;
        }
        const action = callbackData.slice(CALLBACK_ACTION_PREFIX.length).trim();
        const handler = this.callbackActionHandlers.get(action);
        if (!handler) {
            return false;
        }
        await handler(chatId, []);
        return true;
    }
    async handleHelp(chatId) {
        const lines = [
            "commands:",
            ...this.commandDefinitions.map((definition) => definition.usage),
            "",
            "tip: use /sessions and tap a session button to switch quickly",
        ];
        await this.reply(chatId, lines.join("\n"), {
            replyMarkup: this.buildQuickActionsKeyboard(),
        });
    }
    resolveNewCommand(args) {
        const projectIds = Object.keys(this.projects);
        const resolveFallbackProjectId = () => {
            if (this.defaultProjectId) {
                if (!this.projects[this.defaultProjectId]) {
                    return {
                        error: `telegram.<botId>.defaultProjectId '${this.defaultProjectId}' is not configured`,
                    };
                }
                return { projectId: this.defaultProjectId };
            }
            if (projectIds.length === 1) {
                return { projectId: projectIds[0] };
            }
            if (projectIds.length === 0) {
                return { error: "no async projects configured" };
            }
            return {
                error: "missing project id. usage: /new [projectId]. use /projects to list options",
            };
        };
        if (args.length > 1) {
            return { error: "usage: /new [projectId]" };
        }
        if (args.length === 1) {
            const projectId = args[0];
            if (!projectId) {
                return { error: "usage: /new [projectId]" };
            }
            if (!this.projects[projectId]) {
                return {
                    error: `unknown project '${projectId}'. usage: /new [projectId]. use /projects to list options`,
                };
            }
            return { projectId };
        }
        const fallback = resolveFallbackProjectId();
        if (!fallback.projectId) {
            return {
                error: fallback.error ?? "unable to resolve project id",
            };
        }
        return {
            projectId: fallback.projectId,
        };
    }
    async handleProjects(chatId) {
        const entries = Object.entries(this.projects).sort(([left], [right]) => left.localeCompare(right));
        if (entries.length === 0) {
            await this.reply(chatId, "no async projects configured");
            return;
        }
        const lines = entries.map(([projectId, project]) => {
            if (!project.description) {
                return projectId;
            }
            return `${projectId}: ${project.description}`;
        });
        await this.reply(chatId, [`projects:`, ...lines].join("\n"));
    }
    async handleNew(chatId, args) {
        const parsed = this.resolveNewCommand(args);
        if ("error" in parsed) {
            await this.reply(chatId, parsed.error);
            return;
        }
        try {
            const sessionManager = this.getSessionManagerForChat(chatId);
            const session = await sessionManager.createSession({
                projectId: parsed.projectId,
            });
            this.setActiveSession(chatId, session.id);
            this.sessionVerbosityBySession.set(session.id, "quiet");
            await this.reply(chatId, this.formatSessionPreparing(session.projectId));
        }
        catch (error) {
            await this.reply(chatId, this.formatManagerError(error));
        }
    }
    async handleUse(chatId, args) {
        const selector = args[0]?.trim();
        if (!selector) {
            await this.reply(chatId, "usage: /use <sessionId|prefix|index>");
            return;
        }
        const sessionManager = this.getSessionManagerForChat(chatId);
        const sessions = sessionManager.listSessions();
        const selectedSession = this.resolveSessionSelector(selector, sessions, sessionManager);
        if ("error" in selectedSession) {
            await this.reply(chatId, selectedSession.error);
            return;
        }
        this.setActiveSession(chatId, selectedSession.session.id);
        await this.reply(chatId, formatSessionHeadline(selectedSession.session.id, `using session (${selectedSession.session.state})`));
    }
    resolveSessionSelector(selector, sessions, sessionManager) {
        const exactSession = sessionManager.getSession(selector);
        if (exactSession) {
            return { session: exactSession };
        }
        if (/^\d+$/.test(selector)) {
            const index = Number.parseInt(selector, 10);
            if (index <= 0 || index > sessions.length) {
                return {
                    error: sessions.length === 0
                        ? "no sessions"
                        : `session index '${selector}' is out of range (1-${sessions.length})`,
                };
            }
            const indexedSession = sessions[index - 1];
            if (!indexedSession) {
                return { error: "session index lookup failed" };
            }
            return { session: indexedSession };
        }
        const prefixMatches = sessions.filter((session) => session.id.startsWith(selector));
        if (prefixMatches.length === 1) {
            const [prefixMatch] = prefixMatches;
            if (!prefixMatch) {
                return { error: "session prefix lookup failed" };
            }
            return { session: prefixMatch };
        }
        if (prefixMatches.length > 1) {
            return {
                error: `session prefix '${selector}' is ambiguous: ${prefixMatches.map((session) => session.id).join(", ")}`,
            };
        }
        return { error: `session '${selector}' not found` };
    }
    async handleSessions(chatId) {
        const sessionManager = this.getSessionManagerForChat(chatId);
        const sessions = sessionManager.listSessions();
        const lines = this.formatSessions(chatId, sessions);
        await this.reply(chatId, lines.join("\n"), {
            replyMarkup: this.buildSessionsKeyboard(chatId, sessions),
        });
    }
    formatSessions(chatId, sessions) {
        if (sessions.length === 0) {
            return ["no sessions"];
        }
        const activeSessionId = this.activeSessionsByChat.get(chatId);
        return [
            "sessions:",
            ...sessions.map((session, index) => {
                const marker = session.id === activeSessionId ? "*" : "-";
                const preview = this.formatSessionPreview(session.id);
                return `${index + 1}. ${marker} ${session.id} ${session.projectId} ${session.state}${preview ? ` · ${preview}` : ""}`;
            }),
        ];
    }
    formatSessionPreview(sessionId) {
        const previews = [];
        const lastCommand = this.lastCommandBySession.get(sessionId);
        if (lastCommand) {
            previews.push(`$ ${truncateText(lastCommand, MAX_SESSION_PREVIEW_CHARS)}`);
        }
        const lastAssistantMessage = this.lastAssistantMessageBySession.get(sessionId);
        if (lastAssistantMessage) {
            previews.push(truncateText(lastAssistantMessage, MAX_SESSION_PREVIEW_CHARS));
        }
        if (previews.length === 0) {
            return undefined;
        }
        return previews.join(" | ");
    }
    buildSessionsKeyboard(chatId, sessions) {
        const activeSessionId = this.activeSessionsByChat.get(chatId);
        const inlineKeyboard = sessions.map((session, index) => [
            {
                text: `${index + 1}. ${session.id}${session.id === activeSessionId ? " *" : ""}`,
                callback_data: `${CALLBACK_USE_PREFIX}${session.id}`,
            },
        ]);
        inlineKeyboard.push(...this.buildQuickActionsKeyboard().inline_keyboard);
        return {
            inline_keyboard: inlineKeyboard,
        };
    }
    buildQuickActionsKeyboard() {
        return {
            inline_keyboard: QUICK_ACTION_ROWS.map((row) => row.map((action) => ({
                text: `/${action}`,
                callback_data: `${CALLBACK_ACTION_PREFIX}${action}`,
            }))),
        };
    }
    async handleStatus(chatId) {
        const session = await this.requireActiveSession(chatId);
        if (!session) {
            return;
        }
        await this.reply(chatId, describeSession(session, {
            verbosity: this.getSessionVerbosity(session.id),
            lastCommand: this.lastCommandBySession.get(session.id),
            lastAssistantMessage: this.lastAssistantMessageBySession.get(session.id),
        }), {
            replyMarkup: this.buildQuickActionsKeyboard(),
        });
    }
    async handleInterrupt(chatId) {
        const session = await this.requireActiveSession(chatId);
        if (!session) {
            return;
        }
        try {
            const sessionManager = this.getSessionManagerForChat(chatId);
            const result = await sessionManager.interruptSession(session.id);
            if (!result.interrupted) {
                await this.reply(chatId, formatSessionHeadline(result.session.id, "no run in progress"));
                return;
            }
            await this.reply(chatId, formatSessionHeadline(result.session.id, "interrupt requested"));
        }
        catch (error) {
            await this.reply(chatId, this.formatManagerError(error));
        }
    }
    async handleClose(chatId, args) {
        if (args.length > 1) {
            await this.reply(chatId, "usage: /close [<sessionId>|all]");
            return;
        }
        const target = args[0]?.trim();
        if (target === "all") {
            try {
                const sessionManager = this.getSessionManagerForChat(chatId);
                const closed = await sessionManager.closeInactiveSessions();
                for (const session of closed) {
                    this.clearClosedSession(session.id);
                }
                if (closed.length === 0) {
                    await this.reply(chatId, "no sessions to close");
                    return;
                }
                const label = closed.length === 1 ? "session" : "sessions";
                await this.reply(chatId, [`closed ${closed.length} ${label}`, closed.map((session) => session.id).join(", ")].join("\n"));
            }
            catch (error) {
                await this.reply(chatId, this.formatManagerError(error));
            }
            return;
        }
        const sessionId = target ?? this.getActiveSession(chatId)?.id;
        if (!sessionId) {
            await this.reply(chatId, NO_ACTIVE_SESSION_MESSAGE);
            return;
        }
        try {
            const sessionManager = this.getSessionManagerForChat(chatId);
            const closed = await sessionManager.closeSession(sessionId);
            this.clearClosedSession(closed.id);
            await this.reply(chatId, formatSessionHeadline(closed.id, "closed"));
        }
        catch (error) {
            await this.reply(chatId, this.formatManagerError(error));
        }
    }
    async handleVerbosityCommand(chatId, verbosity) {
        const session = await this.requireActiveSession(chatId);
        if (!session) {
            return;
        }
        this.sessionVerbosityBySession.set(session.id, verbosity);
        await this.reply(chatId, formatSessionHeadline(session.id, `verbosity set to ${verbosity}`));
    }
    async handleMessage(chatId, text, sourceMessageId) {
        const session = await this.requireActiveOrSingleSession(chatId);
        if (!session) {
            return;
        }
        try {
            await this.submitSessionMessage(chatId, session.id, text, sourceMessageId);
        }
        catch (error) {
            await this.reply(chatId, this.formatManagerError(error));
        }
    }
    async handleAudioMessage(chatId, message, sourceMessageId) {
        const session = await this.requireActiveOrSingleSession(chatId);
        if (!session) {
            return;
        }
        if (!this.mistralApiKey) {
            await this.reply(chatId, "set MISTRAL_API_KEY or apiKeys.mistral to transcribe Telegram audio");
            return;
        }
        let transcript = "";
        try {
            const audio = await this.api.downloadFile(message.fileId);
            transcript = (await transcribeMistralAudio({
                apiKey: this.mistralApiKey,
                audio,
                fileName: message.fileName,
                mimeType: message.mimeType,
                fetchImpl: this.fetchImpl,
            })).trim();
        }
        catch (error) {
            await this.reply(chatId, `audio transcription failed: ${this.formatManagerError(error)}`);
            return;
        }
        if (!transcript) {
            await this.reply(chatId, "audio transcription failed: transcription result was empty");
            return;
        }
        try {
            await this.submitSessionMessage(chatId, session.id, transcript, sourceMessageId);
        }
        catch (error) {
            await this.reply(chatId, this.formatManagerError(error));
        }
    }
    async requireActiveSession(chatId) {
        const session = this.getActiveSession(chatId);
        if (!session) {
            await this.reply(chatId, NO_ACTIVE_SESSION_MESSAGE);
            return undefined;
        }
        return session;
    }
    async requireActiveOrSingleSession(chatId) {
        const session = this.getActiveOrSingleSession(chatId);
        if (!session) {
            await this.reply(chatId, NO_ACTIVE_SESSION_MESSAGE);
            return undefined;
        }
        return session;
    }
    async submitSessionMessage(chatId, sessionId, text, sourceMessageId) {
        const textWithAttachments = await this.buildMessageTextWithAttachments(sessionId, text, chatId);
        const sessionManager = this.getSessionManagerForChat(chatId);
        await sessionManager.sendMessage(sessionId, textWithAttachments, this.systemMessage ? { additionalSystemMessage: this.systemMessage } : undefined);
        this.resetPendingAttachmentQueue(sessionId);
        await this.reactToQueuedMessage(chatId, sourceMessageId);
        if (this.isVerboseSession(sessionId)) {
            await this.reply(chatId, this.formatMessageQueued(sessionId));
        }
    }
    getAttachmentPerFileLimitReason(sizeBytes) {
        if (sizeBytes <= MAX_TELEGRAM_ATTACHMENT_FILE_BYTES) {
            return undefined;
        }
        return `exceeds per-file limit (${describeAttachmentLimitBytes(MAX_TELEGRAM_ATTACHMENT_FILE_BYTES)})`;
    }
    getAttachmentTotalLimitReason(totalSizeBytes, nextSizeBytes) {
        if (totalSizeBytes + nextSizeBytes <= MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES) {
            return undefined;
        }
        return `exceeds per-turn total limit (${describeAttachmentLimitBytes(MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES)})`;
    }
    async replySkippedAttachment(chatId, attachmentLabel, reason) {
        await this.reply(chatId, `skipped attachment ${attachmentLabel}: ${reason}`);
    }
    getActiveSession(chatId) {
        const sessionId = this.activeSessionsByChat.get(chatId);
        if (!sessionId) {
            return undefined;
        }
        const sessionManager = this.getSessionManagerForChat(chatId);
        const session = sessionManager.getSession(sessionId);
        if (!session) {
            this.clearActiveSession(chatId);
            this.clearSessionAttachments(sessionId);
            return undefined;
        }
        return session;
    }
    getActiveOrSingleSession(chatId) {
        const activeSession = this.getActiveSession(chatId);
        if (activeSession) {
            return activeSession;
        }
        const sessionManager = this.getSessionManagerForChat(chatId);
        const sessions = sessionManager.listSessions();
        if (sessions.length !== 1) {
            return undefined;
        }
        const [singleSession] = sessions;
        if (!singleSession) {
            return undefined;
        }
        this.setActiveSession(chatId, singleSession.id);
        return singleSession;
    }
    setActiveSession(chatId, sessionId) {
        this.activeSessionsByChat.set(chatId, sessionId);
        this.linkChatToSession(chatId, sessionId);
    }
    clearActiveSession(chatId) {
        const sessionId = this.activeSessionsByChat.get(chatId);
        if (!sessionId) {
            return;
        }
        this.activeSessionsByChat.delete(chatId);
        this.unlinkChatFromSession(chatId, sessionId);
    }
    linkChatToSession(chatId, sessionId) {
        const sessions = this.sessionsByChat.get(chatId) ?? new Set();
        sessions.add(sessionId);
        this.sessionsByChat.set(chatId, sessions);
        const chats = this.chatsBySession.get(sessionId) ?? new Set();
        chats.add(chatId);
        this.chatsBySession.set(sessionId, chats);
    }
    unlinkChatFromSession(chatId, sessionId) {
        const sessions = this.sessionsByChat.get(chatId);
        if (sessions) {
            sessions.delete(sessionId);
            if (sessions.size === 0) {
                this.sessionsByChat.delete(chatId);
            }
        }
        const chats = this.chatsBySession.get(sessionId);
        if (!chats) {
            return;
        }
        chats.delete(chatId);
        if (chats.size === 0) {
            this.chatsBySession.delete(sessionId);
        }
    }
    clearClosedSession(sessionId) {
        for (const [chatId, activeSessionId] of this.activeSessionsByChat) {
            if (activeSessionId === sessionId) {
                this.activeSessionsByChat.delete(chatId);
            }
        }
        const chatIds = Array.from(this.chatsBySession.get(sessionId) ?? []);
        for (const chatId of chatIds) {
            this.unlinkChatFromSession(chatId, sessionId);
        }
        this.sessionVerbosityBySession.delete(sessionId);
        this.lastCommandBySession.delete(sessionId);
        this.lastAssistantMessageBySession.delete(sessionId);
        this.latestAssistantMessageByRun.delete(sessionId);
        this.clearSessionAttachments(sessionId);
    }
    getSessionVerbosity(sessionId) {
        return this.sessionVerbosityBySession.get(sessionId) ?? "quiet";
    }
    isVerboseSession(sessionId) {
        return this.getSessionVerbosity(sessionId) === "verbose";
    }
    onSessionEvent(event) {
        if (event.type === "session-progress") {
            if (!this.chatsBySession.has(event.sessionId)) {
                return;
            }
            this.handleSessionProgress(event);
            return;
        }
        if (event.type !== "session-state-changed") {
            return;
        }
        if (!this.chatsBySession.has(event.sessionId)) {
            return;
        }
        if (event.state === "running") {
            this.latestAssistantMessageByRun.delete(event.sessionId);
            if (this.isVerboseSession(event.sessionId)) {
                this.notifyLifecycle(event.sessionId, event.projectId, "started");
            }
            return;
        }
        if (event.state === "failed") {
            this.latestAssistantMessageByRun.delete(event.sessionId);
            this.notifyLifecycle(event.sessionId, event.projectId, "failed");
            return;
        }
        if (event.state === "waiting-input" && event.previousState === "preparing-workspace") {
            this.notifySession(event.sessionId, this.formatSessionReady(event.sessionId, event.projectId));
            return;
        }
        if (event.state === "waiting-input" && event.previousState === "running") {
            if (this.isVerboseSession(event.sessionId)) {
                this.notifyLifecycle(event.sessionId, event.projectId, "finished");
                return;
            }
            const message = this.latestAssistantMessageByRun.get(event.sessionId);
            this.latestAssistantMessageByRun.delete(event.sessionId);
            if (message) {
                this.notifySession(event.sessionId, message);
            }
        }
    }
    handleSessionProgress(event) {
        const isVerbose = this.isVerboseSession(event.sessionId);
        if (event.progress.type === "bash-command") {
            this.lastCommandBySession.set(event.sessionId, event.progress.command);
            if (!isVerbose) {
                return;
            }
            this.notifySession(event.sessionId, [
                formatSessionHeadline(event.sessionId, "bash command"),
                `$ ${truncateText(event.progress.command, MAX_COMMAND_PREVIEW_CHARS)}`,
            ].join("\n"));
            return;
        }
        if (event.progress.type === "edited-file") {
            if (!isVerbose) {
                return;
            }
            this.notifySession(event.sessionId, [formatSessionHeadline(event.sessionId, "edited file"), event.progress.path].join("\n"));
            return;
        }
        if (event.progress.type === "wrote-file") {
            if (!isVerbose) {
                return;
            }
            this.notifySession(event.sessionId, [formatSessionHeadline(event.sessionId, "wrote file"), event.progress.path].join("\n"));
            return;
        }
        this.lastAssistantMessageBySession.set(event.sessionId, event.progress.text);
        this.latestAssistantMessageByRun.set(event.sessionId, event.progress.text);
        if (!isVerbose) {
            return;
        }
        this.notifySession(event.sessionId, [formatSessionHeadline(event.sessionId, "assistant message"), event.progress.text].join("\n"));
    }
    notifyLifecycle(sessionId, projectId, state) {
        const stateLabel = {
            started: "run started",
            finished: "run finished",
            failed: "run failed",
        }[state];
        this.notifySession(sessionId, [formatSessionHeadline(sessionId, stateLabel), `project: ${projectId}`].join("\n"));
    }
    notifySession(sessionId, text) {
        const chatIds = this.chatsBySession.get(sessionId);
        if (!chatIds || chatIds.size === 0) {
            return;
        }
        for (const chatId of chatIds) {
            void this.reply(chatId, text);
        }
    }
    async reactToQueuedMessage(chatId, messageId) {
        if (!this.api.setMessageReaction) {
            return;
        }
        if (typeof messageId !== "number" || !Number.isInteger(messageId) || messageId <= 0) {
            return;
        }
        await this.wait(MESSAGE_QUEUED_REACTION_DELAY_MS);
        if (this.abortController.signal.aborted) {
            return;
        }
        try {
            await this.api.setMessageReaction(chatId, messageId);
        }
        catch (error) {
            this.log("warn", "failed to set telegram message reaction", {
                chatId,
                messageId,
                cause: error instanceof Error ? error.message : String(error),
            });
        }
    }
    async answerCallbackQuery(callbackQueryId, text) {
        if (!this.api.answerCallbackQuery) {
            return;
        }
        const trimmedCallbackQueryId = callbackQueryId?.trim();
        if (!trimmedCallbackQueryId) {
            return;
        }
        try {
            await this.api.answerCallbackQuery(trimmedCallbackQueryId, text);
        }
        catch (error) {
            this.log("warn", "failed to answer telegram callback query", {
                callbackQueryId: trimmedCallbackQueryId,
                cause: error instanceof Error ? error.message : String(error),
            });
        }
    }
    formatSessionPreparing(projectId) {
        return ["session is being prepared", `project: ${projectId}`].join("\n");
    }
    formatSessionReady(sessionId, projectId) {
        return [formatSessionHeadline(sessionId, "session is ready"), `project: ${projectId}`].join("\n");
    }
    formatMessageQueued(sessionId) {
        return formatSessionHeadline(sessionId, "message queued");
    }
    formatManagerError(error) {
        if (error instanceof AsyncSessionManagerError) {
            return error.message;
        }
        return error instanceof Error ? error.message : String(error);
    }
    async reply(chatId, text, options) {
        const chunks = splitTelegramMessage(text);
        for (const [index, chunk] of chunks.entries()) {
            try {
                await this.api.sendMessage(chatId, chunk, {
                    replyMarkup: index === chunks.length - 1 ? options?.replyMarkup : undefined,
                });
            }
            catch (error) {
                this.log("warn", "failed to send telegram message", {
                    chatId,
                    cause: error instanceof Error ? error.message : String(error),
                });
                return;
            }
            if (index < chunks.length - 1) {
                await this.wait(TELEGRAM_MESSAGE_SPLIT_DELAY_MS);
            }
        }
    }
    async wait(durationMs) {
        if (durationMs <= 0 || this.abortController.signal.aborted) {
            return;
        }
        await new Promise((resolve) => {
            const timeout = setTimeout(() => {
                this.abortController.signal.removeEventListener("abort", onAbort);
                resolve();
            }, durationMs);
            timeout.unref?.();
            const onAbort = () => {
                clearTimeout(timeout);
                resolve();
            };
            this.abortController.signal.addEventListener("abort", onAbort, { once: true });
        });
    }
}
export async function startAsyncTelegramAdapter(options) {
    await sweepStaleTelegramAttachmentTempDirs();
    const adapter = new AsyncTelegramAdapterImpl(options);
    return {
        close: () => adapter.close(),
    };
}
//# sourceMappingURL=telegram.js.map