import fs from "node:fs";
import path from "node:path";
import { randomUUID } from "node:crypto";
import { spawn } from "node:child_process";
import { createRequire } from "node:module";
export class OpenClawWeixinClient {
    options;
    account;
    getUpdatesBuf = "";
    constructor(options) {
        this.options = options;
        this.account = this.loadPreferredAccount();
        this.getUpdatesBuf = this.readSyncBuf(this.account?.accountId);
    }
    status() {
        if (!this.options.config.enabled)
            return "disabled";
        if (!this.account)
            return "needs login";
        return `logged-in:${this.account.accountId}`;
    }
    currentAccount() {
        return this.account;
    }
    async login(signal) {
        const login = await loadLoginQrModule();
        const accountId = this.options.config.accountId;
        const started = await login.startWeixinLoginWithQr({
            accountId,
            apiBaseUrl: this.options.config.apiBaseUrl,
            force: true,
            verbose: false,
        });
        if (started.qrcodeUrl) {
            const qrPath = await writeLoginQrPng(this.options.dataDir, started.qrcodeUrl).catch(() => undefined);
            const opened = qrPath ? openExternal(qrPath) : false;
            const message = formatImageLoginStatus(started.qrcodeUrl, qrPath, opened);
            if (this.options.onStatus)
                this.options.onStatus(message);
            else
                process.stdout.write(`${message}\n`);
        }
        else {
            this.options.onStatus?.("OpenClaw login started, but no QR code was returned.");
        }
        this.options.onStatus?.("Waiting for WeChat scan confirmation...");
        const waited = await waitForLoginConfirmation({
            apiBaseUrl: this.options.config.apiBaseUrl,
            qrcodeUrl: started.qrcodeUrl,
            signal,
            timeoutMs: Math.max(300_000, this.options.config.qrPollIntervalMs * 60),
            statusTimeoutMs: this.options.config.longPollTimeoutMs,
            onStatus: this.options.onStatus,
        });
        if (!waited.connected && !waited.alreadyConnected) {
            throw new Error(waited.message || "WeChat OpenClaw login did not complete.");
        }
        if (!waited.botToken) {
            throw new Error("WeChat OpenClaw login completed without bot token.");
        }
        const now = Date.now();
        const stored = {
            accountId: safeAccountId(waited.accountId || accountId || waited.userId || randomUUID()),
            token: waited.botToken,
            baseUrl: waited.baseUrl || this.options.config.apiBaseUrl,
            userId: waited.userId,
            createdAtMs: now,
            updatedAtMs: now,
        };
        saveAccount(authRoot(this.options.dataDir), stored);
        this.account = stored;
        this.getUpdatesBuf = this.readSyncBuf(stored.accountId);
        return stored;
    }
    async poll(onMessage, signal) {
        const api = await loadApiModule();
        const account = this.requireAccount();
        this.options.onStatus?.(`polling account=${account.accountId}`);
        while (!signal?.aborted) {
            try {
                const response = await api.getUpdates({
                    baseUrl: account.baseUrl,
                    token: account.token,
                    get_updates_buf: this.getUpdatesBuf,
                    timeoutMs: this.options.config.longPollTimeoutMs,
                    abortSignal: signal,
                });
                if (signal?.aborted)
                    break;
                const isError = (response.ret !== undefined && response.ret !== 0) ||
                    (response.errcode !== undefined && response.errcode !== 0);
                if (isError) {
                    const code = response.errcode ?? response.ret;
                    this.options.onStatus?.(`getupdates error ${code}: ${response.errmsg ?? ""}`);
                    await delay(2000, signal);
                    continue;
                }
                if (response.longpolling_timeout_ms && response.longpolling_timeout_ms > 0) {
                    this.options.config.longPollTimeoutMs = response.longpolling_timeout_ms;
                }
                if (response.get_updates_buf) {
                    this.getUpdatesBuf = response.get_updates_buf;
                    this.writeSyncBuf(account.accountId, this.getUpdatesBuf);
                }
                for (const message of response.msgs ?? []) {
                    await onMessage(message);
                }
            }
            catch (error) {
                if (signal?.aborted)
                    break;
                this.options.onStatus?.(`getupdates failed: ${errorMessage(error)}`);
                await delay(3000, signal);
            }
        }
    }
    async sendText(toUserId, text, contextToken) {
        const api = await loadApiModule();
        const account = this.requireAccount();
        await api.sendMessage({
            baseUrl: account.baseUrl,
            token: account.token,
            body: buildTextMessage(toUserId, text, contextToken),
        });
    }
    async sendMediaFile(toUserId, filePath, text, contextToken) {
        const media = await loadSendMediaModule();
        const account = this.requireAccount();
        await media.sendWeixinMediaFile({
            filePath,
            to: toUserId,
            text,
            opts: {
                baseUrl: account.baseUrl,
                token: account.token,
                contextToken,
            },
            cdnBaseUrl: this.options.config.cdnBaseUrl,
        });
    }
    async saveInboundMedia(item, inboxDir, fallbackName) {
        const url = item.file_item?.full_url || item.file_item?.url ||
            item.image_item?.full_url || item.image_item?.url ||
            item.voice_item?.full_url || item.voice_item?.url ||
            item.video_item?.full_url || item.video_item?.url;
        const filename = safeFilename(item.file_item?.file_name ||
            item.image_item?.file_name ||
            item.voice_item?.file_name ||
            item.video_item?.file_name ||
            fallbackName);
        fs.mkdirSync(inboxDir, { recursive: true });
        if (!url) {
            const target = path.join(inboxDir, `${Date.now()}-${filename}.json`);
            fs.writeFileSync(target, JSON.stringify(item, null, 2), "utf-8");
            return { path: target, bytes: fs.statSync(target).size };
        }
        const response = await fetch(url);
        if (!response.ok)
            throw new Error(`download media failed ${response.status}: ${url}`);
        const bytes = Buffer.from(await response.arrayBuffer());
        const target = path.join(inboxDir, `${Date.now()}-${filename}`);
        fs.writeFileSync(target, bytes);
        return { path: target, bytes: bytes.length };
    }
    requireAccount() {
        if (!this.account) {
            throw new Error("WeChat OpenClaw is not logged in. Run deepseekcode --wechat-login first.");
        }
        return this.account;
    }
    loadPreferredAccount() {
        const accounts = listStoredAccounts(authRoot(this.options.dataDir));
        if (this.options.config.accountId) {
            return accounts.find((account) => account.accountId === this.options.config.accountId);
        }
        return accounts.at(-1);
    }
    readSyncBuf(accountId) {
        if (!accountId)
            return "";
        const file = syncBufPath(this.options.dataDir, accountId);
        if (!fs.existsSync(file))
            return "";
        try {
            const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
            return parsed.get_updates_buf ?? "";
        }
        catch {
            return "";
        }
    }
    writeSyncBuf(accountId, value) {
        const file = syncBufPath(this.options.dataDir, accountId);
        fs.mkdirSync(path.dirname(file), { recursive: true });
        fs.writeFileSync(file, JSON.stringify({ get_updates_buf: value }), "utf-8");
    }
}
export function authRoot(dataDir) {
    return path.join(dataDir, "remote", "wechat-openclaw", "auth");
}
export function inboxRoot(dataDir, chatId) {
    return path.join(dataDir, "remote", "wechat-openclaw", "inbox", safeFilename(chatId));
}
export function listStoredAccounts(root) {
    const dir = path.join(root, "accounts");
    if (!fs.existsSync(dir))
        return [];
    return fs.readdirSync(dir)
        .filter((file) => file.endsWith(".json"))
        .map((file) => {
        try {
            return JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8"));
        }
        catch {
            return undefined;
        }
    })
        .filter((account) => Boolean(account?.accountId && account.token));
}
function saveAccount(root, account) {
    const dir = path.join(root, "accounts");
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(path.join(dir, `${safeFilename(account.accountId)}.json`), JSON.stringify(account, null, 2), "utf-8");
}
function formatImageLoginStatus(qrcodeUrl, qrPath, opened) {
    const lines = [
        "OpenClaw WeChat QR image login",
        qrPath
            ? "DeepSeekCode generated a local QR image for WeChat login."
            : "DeepSeekCode could not generate a QR image. Use the backup URL only if you know it works on your network.",
        opened
            ? "The QR image was opened with your system image viewer. Scan it with WeChat, then return to DeepSeekCode."
            : "Open the QR image path below and scan it with WeChat.",
    ];
    if (qrPath)
        lines.push(`QR image: ${qrPath}`);
    lines.push(`Backup URL: ${qrcodeUrl}`);
    return lines.join("\n");
}
async function writeLoginQrPng(dataDir, qrcodeUrl) {
    const targetDir = path.join(dataDir, "remote", "wechat-openclaw", "qr");
    fs.mkdirSync(targetDir, { recursive: true });
    const target = path.join(targetDir, `openclaw-login-${Date.now()}.png`);
    const qr = loadQrCodeModule();
    await qr.toFile(target, qrcodeUrl, {
        errorCorrectionLevel: "M",
        margin: 2,
        scale: 10,
        type: "png",
    });
    return target;
}
function loadQrCodeModule() {
    const require = createRequire(import.meta.url);
    return require("qrcode");
}
function openExternal(target) {
    const platform = process.platform;
    const command = platform === "win32"
        ? "cmd.exe"
        : platform === "darwin"
            ? "open"
            : "xdg-open";
    const args = platform === "win32"
        ? ["/d", "/s", "/c", "start", "", target]
        : [target];
    try {
        const child = spawn(command, args, {
            detached: true,
            stdio: "ignore",
            windowsHide: true,
        });
        child.unref();
        return true;
    }
    catch {
        return false;
    }
}
async function waitForLoginConfirmation(input) {
    const qrcode = qrcodeFromUrl(input.qrcodeUrl);
    if (!qrcode) {
        return {
            connected: false,
            message: "OpenClaw did not return a usable qrcode token.",
        };
    }
    const deadline = Date.now() + input.timeoutMs;
    let currentBaseUrl = input.apiBaseUrl || "https://ilinkai.weixin.qq.com";
    let scanNotified = false;
    let lastWaitNoticeAt = 0;
    while (Date.now() < deadline) {
        throwIfAborted(input.signal);
        const endpoint = `${currentBaseUrl.replace(/\/+$/, "")}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
        const status = await fetchQrStatus(endpoint, input.statusTimeoutMs, input.signal);
        throwIfAborted(input.signal);
        switch (status.status) {
            case "confirmed":
                if (!status.bot_token || !status.ilink_bot_id) {
                    return {
                        connected: false,
                        message: "Login confirmed but OpenClaw did not return bot token/account id.",
                    };
                }
                return {
                    connected: true,
                    botToken: status.bot_token,
                    accountId: status.ilink_bot_id,
                    baseUrl: status.baseurl,
                    userId: status.ilink_user_id,
                    message: "WeChat OpenClaw connected.",
                };
            case "scaned":
                if (!scanNotified) {
                    input.onStatus?.("WeChat QR scanned. Waiting for confirmation...");
                    scanNotified = true;
                }
                break;
            case "scaned_but_redirect":
                if (status.redirect_host) {
                    currentBaseUrl = `https://${status.redirect_host}`;
                    input.onStatus?.(`WeChat login redirected to ${status.redirect_host}. Continuing...`);
                }
                break;
            case "binded_redirect":
                return {
                    alreadyConnected: true,
                    connected: false,
                    message: "This OpenClaw account is already connected.",
                };
            case "expired":
                return {
                    connected: false,
                    message: "QR code expired. Run /remote-control wechat start again.",
                };
            case "need_verifycode":
                input.onStatus?.("WeChat requires an extra verification code. OpenClaw verification-code input is not available inside TUI yet.");
                return {
                    connected: false,
                    message: "WeChat requires an extra verification code. Please retry or use the standalone OpenClaw login flow.",
                };
            case "verify_code_blocked":
                return {
                    connected: false,
                    message: "Verification code was blocked after too many attempts.",
                };
            default: {
                const now = Date.now();
                if (now - lastWaitNoticeAt > 15_000) {
                    input.onStatus?.("Waiting for WeChat scan confirmation...");
                    lastWaitNoticeAt = now;
                }
            }
        }
        await delay(1000, input.signal);
    }
    return {
        connected: false,
        message: "WeChat OpenClaw login timed out.",
    };
}
async function fetchQrStatus(url, timeoutMs, signal) {
    try {
        const response = await fetchWithTimeout(url, timeoutMs, signal);
        if (!response.ok) {
            return { status: "wait", errcode: response.status, errmsg: response.statusText };
        }
        return await response.json();
    }
    catch (error) {
        if (isAbortLike(error))
            throw error;
        return { status: "wait", errmsg: errorMessage(error) };
    }
}
async function fetchWithTimeout(url, timeoutMs, signal) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort("timeout"), Math.max(1000, timeoutMs));
    const abort = () => controller.abort(signal?.reason ?? "aborted");
    signal?.addEventListener("abort", abort, { once: true });
    try {
        return await fetch(url, { signal: controller.signal });
    }
    finally {
        clearTimeout(timeout);
        signal?.removeEventListener("abort", abort);
    }
}
function qrcodeFromUrl(qrcodeUrl) {
    if (!qrcodeUrl)
        return undefined;
    try {
        const parsed = new URL(qrcodeUrl);
        return parsed.searchParams.get("qrcode") ?? undefined;
    }
    catch {
        const match = /[?&]qrcode=([^&]+)/.exec(qrcodeUrl);
        return match ? decodeURIComponent(match[1] ?? "") : undefined;
    }
}
function throwIfAborted(signal) {
    if (signal?.aborted)
        throw new Error("WeChat OpenClaw login cancelled.");
}
function isAbortLike(error) {
    return error instanceof Error && error.name === "AbortError";
}
function syncBufPath(dataDir, accountId) {
    return path.join(dataDir, "remote", "wechat-openclaw", "sync", `${safeFilename(accountId)}.json`);
}
async function loadLoginQrModule() {
    return await import("@tencent-weixin/openclaw-weixin/dist/src/auth/login-qr.js");
}
async function loadApiModule() {
    return await import("@tencent-weixin/openclaw-weixin/dist/src/api/api.js");
}
async function loadSendMediaModule() {
    return await import("@tencent-weixin/openclaw-weixin/dist/src/messaging/send-media.js");
}
function buildTextMessage(toUserId, text, contextToken) {
    return {
        msg: {
            from_user_id: "",
            to_user_id: toUserId,
            client_id: `deepseekcode-${randomUUID()}`,
            message_type: 2,
            message_state: 2,
            item_list: [{ type: 1, text_item: { text } }],
            context_token: contextToken,
        },
    };
}
function safeAccountId(value) {
    return safeFilename(value) || randomUUID();
}
function safeFilename(value) {
    return value.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").slice(0, 120) || "file";
}
function delay(ms, signal) {
    return new Promise((resolve) => {
        if (signal?.aborted) {
            resolve();
            return;
        }
        const timer = setTimeout(resolve, ms);
        signal?.addEventListener("abort", () => {
            clearTimeout(timer);
            resolve();
        }, { once: true });
    });
}
function errorMessage(error) {
    return error instanceof Error ? error.message : String(error);
}
