import { EventEmitter } from "node:events";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { getUpdates, sendTyping, } from "@tencent-weixin/openclaw-weixin/src/api/api.js";
import { WeixinConfigManager } from "@tencent-weixin/openclaw-weixin/src/api/config-cache.js";
import { SESSION_EXPIRED_ERRCODE, pauseSession, getRemainingPauseMs, } from "@tencent-weixin/openclaw-weixin/src/api/session-guard.js";
import { MessageItemType, TypingStatus, } from "@tencent-weixin/openclaw-weixin/src/api/types.js";
import { listWeixinAccountIds, resolveWeixinAccount, } from "@tencent-weixin/openclaw-weixin/src/auth/accounts.js";
import { downloadRemoteImageToTemp } from "@tencent-weixin/openclaw-weixin/src/cdn/upload.js";
import { downloadMediaFromItem } from "@tencent-weixin/openclaw-weixin/src/media/media-download.js";
import { setContextToken, getContextToken, restoreContextTokens } from "@tencent-weixin/openclaw-weixin/src/messaging/inbound.js";
import { sendMessageWeixin, StreamingMarkdownFilter } from "@tencent-weixin/openclaw-weixin/src/messaging/send.js";
import { sendWeixinMediaFile } from "@tencent-weixin/openclaw-weixin/src/messaging/send-media.js";
import { getSyncBufFilePath, loadGetUpdatesBuf, saveGetUpdatesBuf, } from "@tencent-weixin/openclaw-weixin/src/storage/sync-buf.js";
import { logger } from "../logger.js";
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
const MAX_CONSECUTIVE_FAILURES = 3;
const BACKOFF_DELAY_MS = 30_000;
const RETRY_DELAY_MS = 2_000;
function summarizeError(err) {
    if (err instanceof Error) {
        return {
            name: err.name,
            message: err.message,
            stack: err.stack?.split("\n").slice(0, 3).join("\n"),
        };
    }
    return { message: String(err) };
}
function summarizeAttachment(att) {
    return {
        type: att.type,
        mimeType: att.mimeType,
        hasLocalPath: !!att.localPath,
        hasUrl: !!att.url,
        filename: att.filename,
    };
}
function extractSendResultMetadata(result) {
    if (result && typeof result === "object") {
        const obj = result;
        return {
            messageId: obj.messageId,
            clientId: obj.clientId,
            ret: obj.ret,
            errcode: obj.errcode,
            errmsg: obj.errmsg,
        };
    }
    return {};
}
function extractMessagePreview(text) {
    const normalized = text.replace(/\s+/g, " ").trim();
    return normalized.length > 160 ? `${normalized.slice(0, 160)}…` : normalized;
}
function extractTokenMetadata(token) {
    return {
        hasContextToken: !!token,
        contextTokenLength: token?.length ?? 0,
    };
}
function extractTextBody(itemList) {
    if (!itemList?.length)
        return "";
    for (const item of itemList) {
        if (item.type === MessageItemType.TEXT && item.text_item?.text !== undefined) {
            return item.text_item.text;
        }
        if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
            return item.voice_item.text;
        }
    }
    return "";
}
function resolveMediaTempDir() {
    return path.join(os.tmpdir(), "visionclaw", "weixin", "media");
}
async function saveMediaBuffer(buffer, contentType, subdir, _maxBytes, originalFilename) {
    const dir = path.join(resolveMediaTempDir(), subdir ?? "inbound");
    await fs.promises.mkdir(dir, { recursive: true });
    let ext = ".bin";
    if (originalFilename) {
        const dotIdx = originalFilename.lastIndexOf(".");
        if (dotIdx > 0)
            ext = originalFilename.slice(dotIdx);
    }
    else if (contentType) {
        const mapping = {
            "image/jpeg": ".jpg",
            "image/png": ".png",
            "image/gif": ".gif",
            "image/webp": ".webp",
            "audio/wav": ".wav",
            "audio/silk": ".silk",
            "video/mp4": ".mp4",
        };
        ext = mapping[contentType] ?? ".bin";
    }
    const name = `wx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`;
    const filePath = path.join(dir, name);
    await fs.promises.writeFile(filePath, buffer);
    return { path: filePath };
}
/**
 * WeChat (Weixin) channel adapter for VisionClaw.
 *
 * Wraps the transport modules from `@tencent-weixin/openclaw-weixin`
 * into VisionClaw's ChannelAdapter interface. Runs the getUpdates
 * long-poll loop directly rather than going through OpenClaw's
 * plugin runtime.
 */
export class WeixinAdapter extends EventEmitter {
    name = "weixin";
    config;
    abortController = null;
    contextTokens = new Map();
    typingTickets = new Map();
    constructor(config) {
        super();
        this.config = config;
    }
    start() {
        const weixinConfig = this.config.channels.weixin;
        if (!weixinConfig?.enabled)
            return Promise.resolve();
        const openClawCfg = {};
        const accountIds = listWeixinAccountIds(openClawCfg);
        const targetAccountId = weixinConfig.accountId ?? accountIds[0];
        if (!targetAccountId) {
            logger.warn("Weixin: No account configured. Run QR login first.");
            return Promise.resolve();
        }
        let account;
        try {
            account = resolveWeixinAccount(openClawCfg, targetAccountId);
        }
        catch (err) {
            logger.warn(`Weixin: Failed to resolve account "${targetAccountId}": ${err instanceof Error ? err.message : String(err)}`);
            return Promise.resolve();
        }
        if (!account.configured) {
            logger.warn(`Weixin: Account "${targetAccountId}" not configured (no token). Run QR login.`);
            return Promise.resolve();
        }
        const baseUrl = weixinConfig.baseUrl;
        const cdnBaseUrl = weixinConfig.cdnBaseUrl;
        restoreContextTokens(account.accountId);
        this.abortController = new AbortController();
        void this.runPollLoop({
            accountId: account.accountId,
            baseUrl,
            cdnBaseUrl,
            token: account.token,
            signal: this.abortController.signal,
        });
        return Promise.resolve();
    }
    stop() {
        if (this.abortController) {
            this.abortController.abort();
            this.abortController = null;
        }
        return Promise.resolve();
    }
    async sendMessage(to, message, attachments) {
        const weixinConfig = this.config.channels.weixin;
        if (!weixinConfig)
            throw new Error("Weixin channel not configured");
        const openClawCfg = {};
        const accountId = weixinConfig.accountId ?? listWeixinAccountIds(openClawCfg)[0];
        if (!accountId)
            throw new Error("No Weixin account configured");
        const account = resolveWeixinAccount(openClawCfg, accountId);
        const baseUrl = weixinConfig.baseUrl;
        const cdnBaseUrl = weixinConfig.cdnBaseUrl;
        // Look up contextToken: try the runtime Map first (exact key, then with/without
        // @im.wechat suffix), then fall back to the persistent module-level store which
        // survives process restarts.
        const altKey = to.endsWith("@im.wechat")
            ? to.replace("@im.wechat", "")
            : `${to}@im.wechat`;
        const contextToken = this.contextTokens.get(to) ??
            this.contextTokens.get(altKey) ??
            getContextToken(accountId, to) ??
            getContextToken(accountId, altKey);
        if (!contextToken) {
            logger.warn(`Weixin: No contextToken for "${to}", sending without context`);
        }
        const opts = { baseUrl, token: account.token, contextToken };
        const startedAt = Date.now();
        const sendMeta = {
            accountId,
            to,
            baseUrl,
            messageLength: message.length,
            messagePreview: extractMessagePreview(message),
            attachmentCount: attachments?.length ?? 0,
            attachments: attachments?.map(summarizeAttachment) ?? [],
            ...extractTokenMetadata(contextToken),
        };
        logger.log("debug", "weixin", "sendMessage start", sendMeta);
        let sentParts = 0;
        if (attachments && attachments.length > 0) {
            for (const [index, att] of attachments.entries()) {
                let filePath = att.localPath;
                // If no local path but we have a URL, download to temp file first
                if (!filePath && att.url) {
                    try {
                        filePath = await downloadRemoteImageToTemp(att.url, resolveMediaTempDir());
                    }
                    catch (err) {
                        logger.warn(`Weixin: Failed to download remote ${att.type} from ${att.url}: ${err instanceof Error ? err.message : String(err)}`);
                        continue;
                    }
                }
                if (!filePath)
                    continue;
                try {
                    logger.log("debug", "weixin", "sendMessage media part start", {
                        ...sendMeta,
                        attachmentIndex: index,
                        attachment: summarizeAttachment(att),
                        resolvedLocalPath: filePath,
                    });
                    const mediaStartedAt = Date.now();
                    const result = await sendWeixinMediaFile({
                        filePath,
                        to,
                        text: message,
                        opts,
                        cdnBaseUrl,
                    });
                    sentParts += 1;
                    logger.log("debug", "weixin", "sendMessage media part success", {
                        ...sendMeta,
                        attachmentIndex: index,
                        elapsedMs: Date.now() - mediaStartedAt,
                        ...extractSendResultMetadata(result),
                    });
                    message = "";
                }
                catch (err) {
                    logger.log("error", "weixin", "sendMessage media part failed", {
                        ...sendMeta,
                        attachmentIndex: index,
                        error: summarizeError(err),
                    });
                    logger.err(`Weixin: Failed to send ${att.type}: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
        }
        if (message.trim()) {
            const filter = new StreamingMarkdownFilter();
            const plainText = filter.feed(message) + filter.flush();
            logger.log("debug", "weixin", "sendMessage text part start", {
                ...sendMeta,
                plainTextLength: plainText.length,
                plainTextPreview: extractMessagePreview(plainText),
            });
            const textStartedAt = Date.now();
            try {
                const result = await sendMessageWeixin({ to, text: plainText, opts });
                sentParts += 1;
                logger.log("debug", "weixin", "sendMessage text part success", {
                    ...sendMeta,
                    elapsedMs: Date.now() - textStartedAt,
                    ...extractSendResultMetadata(result),
                });
            }
            catch (err) {
                logger.log("error", "weixin", "sendMessage text part failed", {
                    ...sendMeta,
                    elapsedMs: Date.now() - textStartedAt,
                    error: summarizeError(err),
                });
                throw err;
            }
        }
        logger.log("debug", "weixin", "sendMessage complete", {
            ...sendMeta,
            sentParts,
            elapsedMs: Date.now() - startedAt,
        });
    }
    async sendTypingIndicator(to) {
        const weixinConfig = this.config.channels.weixin;
        if (!weixinConfig)
            return;
        const openClawCfg = {};
        const accountId = weixinConfig.accountId ?? listWeixinAccountIds(openClawCfg)[0];
        if (!accountId)
            return;
        const account = resolveWeixinAccount(openClawCfg, accountId);
        const baseUrl = weixinConfig.baseUrl;
        const ticket = this.typingTickets.get(to);
        if (!ticket)
            return;
        try {
            await sendTyping({
                baseUrl,
                token: account.token,
                body: {
                    ilink_user_id: to,
                    typing_ticket: ticket,
                    status: TypingStatus.TYPING,
                },
            });
        }
        catch {
            // best-effort
        }
    }
    async runPollLoop(opts) {
        const { accountId, baseUrl, cdnBaseUrl, token, signal } = opts;
        const syncFilePath = getSyncBufFilePath(accountId);
        const previousBuf = loadGetUpdatesBuf(syncFilePath);
        let getUpdatesBuf = previousBuf ?? "";
        const configManager = new WeixinConfigManager({ baseUrl, token }, (msg) => logger.log("debug", "weixin", msg));
        let nextTimeoutMs = DEFAULT_LONG_POLL_TIMEOUT_MS;
        let consecutiveFailures = 0;
        logger.channel("weixin", `Poll loop started (${baseUrl}, account=${accountId})`);
        while (!signal.aborted) {
            try {
                const resp = await getUpdates({
                    baseUrl,
                    token,
                    get_updates_buf: getUpdatesBuf,
                    timeoutMs: nextTimeoutMs,
                });
                if (resp.longpolling_timeout_ms !== undefined && resp.longpolling_timeout_ms > 0) {
                    nextTimeoutMs = resp.longpolling_timeout_ms;
                }
                const isApiError = (resp.ret !== undefined && resp.ret !== 0) ||
                    (resp.errcode !== undefined && resp.errcode !== 0);
                if (isApiError) {
                    const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
                    if (isSessionExpired) {
                        pauseSession(accountId);
                        const pauseMs = getRemainingPauseMs(accountId);
                        logger.err(`[Weixin] Session expired, pausing for ${Math.ceil(pauseMs / 60_000)} min`);
                        await this.sleep(pauseMs, signal);
                        consecutiveFailures = 0;
                        continue;
                    }
                    consecutiveFailures += 1;
                    logger.warn(`Weixin getUpdates failed: ret=${resp.ret} errcode=${resp.errcode} (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`);
                    if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
                        consecutiveFailures = 0;
                        await this.sleep(BACKOFF_DELAY_MS, signal);
                    }
                    else {
                        await this.sleep(RETRY_DELAY_MS, signal);
                    }
                    continue;
                }
                consecutiveFailures = 0;
                if (resp.get_updates_buf !== undefined && resp.get_updates_buf !== "") {
                    saveGetUpdatesBuf(syncFilePath, resp.get_updates_buf);
                    getUpdatesBuf = resp.get_updates_buf;
                }
                const messages = resp.msgs ?? [];
                for (const msg of messages) {
                    await this.processInboundMessage(msg, {
                        accountId,
                        baseUrl,
                        cdnBaseUrl,
                        token,
                        configManager,
                    });
                }
            }
            catch (err) {
                // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal.aborted can change after await
                if (signal.aborted)
                    return;
                consecutiveFailures += 1;
                logger.warn(`Weixin getUpdates error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${err instanceof Error ? err.message : String(err)}`);
                if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
                    consecutiveFailures = 0;
                    await this.sleep(BACKOFF_DELAY_MS, signal);
                }
                else {
                    await this.sleep(RETRY_DELAY_MS, signal);
                }
            }
        }
        logger.channel("weixin", "Poll loop ended");
    }
    async processInboundMessage(full, deps) {
        const fromUserId = full.from_user_id ?? "";
        if (!fromUserId)
            return;
        if (full.context_token) {
            setContextToken(deps.accountId, fromUserId, full.context_token);
            this.contextTokens.set(fromUserId, full.context_token);
        }
        const cachedConfig = await deps.configManager.getForUser(fromUserId, full.context_token);
        if (cachedConfig.typingTicket) {
            this.typingTickets.set(fromUserId, cachedConfig.typingTicket);
        }
        const textBody = extractTextBody(full.item_list);
        const attachments = [];
        const mediaItem = this.findMediaItem(full.item_list);
        if (mediaItem) {
            try {
                const downloaded = await downloadMediaFromItem(mediaItem, {
                    cdnBaseUrl: deps.cdnBaseUrl,
                    saveMedia: saveMediaBuffer,
                    log: (msg) => logger.log("debug", "weixin", msg),
                    errLog: (msg) => logger.warn(`[Weixin] ${msg}`),
                    label: "inbound",
                });
                if (downloaded.decryptedPicPath) {
                    attachments.push({
                        type: "image",
                        localPath: downloaded.decryptedPicPath,
                        mimeType: "image/jpeg",
                    });
                }
                if (downloaded.decryptedVoicePath) {
                    attachments.push({
                        type: "audio",
                        localPath: downloaded.decryptedVoicePath,
                        mimeType: downloaded.voiceMediaType ?? "audio/wav",
                    });
                }
                if (downloaded.decryptedFilePath) {
                    attachments.push({
                        type: "file",
                        localPath: downloaded.decryptedFilePath,
                        mimeType: downloaded.fileMediaType ?? "application/octet-stream",
                    });
                }
                if (downloaded.decryptedVideoPath) {
                    attachments.push({
                        type: "video",
                        localPath: downloaded.decryptedVideoPath,
                        mimeType: "video/mp4",
                    });
                }
            }
            catch (err) {
                logger.warn(`Weixin: Media download failed: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
        if (!textBody && attachments.length === 0)
            return;
        const commandMessage = {
            id: `weixin-${String(full.message_id ?? Date.now())}-${fromUserId}`,
            channel: "weixin",
            sender: fromUserId,
            text: textBody,
            attachments,
            timestamp: full.create_time_ms
                ? new Date(full.create_time_ms).toISOString()
                : new Date().toISOString(),
            meta: {
                accountId: deps.accountId,
                contextToken: full.context_token,
                sessionId: full.session_id,
            },
        };
        this.emit("message", commandMessage);
    }
    findMediaItem(itemList) {
        if (!itemList?.length)
            return undefined;
        const hasMedia = (m) => !!(m?.encrypt_query_param ?? m?.full_url);
        return (itemList.find((i) => i.type === MessageItemType.IMAGE && hasMedia(i.image_item?.media)) ??
            itemList.find((i) => i.type === MessageItemType.VIDEO && hasMedia(i.video_item?.media)) ??
            itemList.find((i) => i.type === MessageItemType.FILE && hasMedia(i.file_item?.media)) ??
            itemList.find((i) => i.type === MessageItemType.VOICE &&
                hasMedia(i.voice_item?.media) &&
                !i.voice_item?.text));
    }
    sleep(ms, signal) {
        return new Promise((resolve, reject) => {
            const t = setTimeout(resolve, ms);
            signal.addEventListener("abort", () => {
                clearTimeout(t);
                reject(new Error("aborted"));
            }, { once: true });
        });
    }
}
//# sourceMappingURL=weixin.js.map