/**
 * WeCom Agent Webhook 处理器
 * 处理 XML 格式回调
 */

import { pathToFileURL } from "node:url";
import path from "node:path";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
import type { ResolvedAgentAccount } from "../types/index.js";
import {
    extractMsgType,
    extractFromUser,
    extractContent,
    extractChatId,
    extractMediaId,
    extractMsgId,
    extractFileName,
    extractAgentId,
} from "../shared/xml-parser.js";
import { sendText, downloadMedia, uploadMedia, sendMedia as sendAgentMedia } from "./api-client.js";
import { getWecomRuntime } from "../runtime.js";
import type { WecomAgentInboundMessage } from "../types/index.js";
import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../shared/command-auth.js";
import { resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "../config/index.js";
import { generateAgentId, shouldUseDynamicAgent, ensureDynamicAgentListed } from "../dynamic-agent.js";

/** 错误提示信息 */
const ERROR_HELP = "";

// Agent webhook 幂等去重池（防止企微回调重试导致重复回复）
// 注意：这是进程内内存去重，重启会清空；但足以覆盖企微的短周期重试。
const RECENT_MSGID_TTL_MS = 10 * 60 * 1000;
const recentAgentMsgIds = new Map<string, number>();

function rememberAgentMsgId(msgId: string): boolean {
    const now = Date.now();
    const existing = recentAgentMsgIds.get(msgId);
    if (existing && now - existing < RECENT_MSGID_TTL_MS) return false;
    recentAgentMsgIds.set(msgId, now);
    // 简单清理：只在写入时做一次线性 prune，避免无界增长
    for (const [k, ts] of recentAgentMsgIds) {
        if (now - ts >= RECENT_MSGID_TTL_MS) recentAgentMsgIds.delete(k);
    }
    return true;
}

function looksLikeTextFile(buffer: Buffer): boolean {
    const sampleSize = Math.min(buffer.length, 4096);
    if (sampleSize === 0) return true;
    let bad = 0;
    for (let i = 0; i < sampleSize; i++) {
        const b = buffer[i]!;
        const isWhitespace = b === 0x09 || b === 0x0a || b === 0x0d; // \t \n \r
        const isPrintable = b >= 0x20 && b !== 0x7f;
        if (!isWhitespace && !isPrintable) bad++;
    }
    // 非可打印字符占比太高，基本可判断为二进制
    return bad / sampleSize <= 0.02;
}

function analyzeTextHeuristic(buffer: Buffer): { sampleSize: number; badCount: number; badRatio: number } {
    const sampleSize = Math.min(buffer.length, 4096);
    if (sampleSize === 0) return { sampleSize: 0, badCount: 0, badRatio: 0 };
    let badCount = 0;
    for (let i = 0; i < sampleSize; i++) {
        const b = buffer[i]!;
        const isWhitespace = b === 0x09 || b === 0x0a || b === 0x0d;
        const isPrintable = b >= 0x20 && b !== 0x7f;
        if (!isWhitespace && !isPrintable) badCount++;
    }
    return { sampleSize, badCount, badRatio: badCount / sampleSize };
}

function previewHex(buffer: Buffer, maxBytes = 32): string {
    const n = Math.min(buffer.length, maxBytes);
    if (n <= 0) return "";
    return buffer
        .subarray(0, n)
        .toString("hex")
        .replace(/(..)/g, "$1 ")
        .trim();
}

function buildTextFilePreview(buffer: Buffer, maxChars: number): string | undefined {
    if (!looksLikeTextFile(buffer)) return undefined;
    const text = buffer.toString("utf8");
    if (!text.trim()) return undefined;
    const truncated = text.length > maxChars ? `${text.slice(0, maxChars)}\n…(已截断)` : text;
    return truncated;
}

/**
 * **AgentWebhookParams (Webhook 处理器参数)**
 * 
 * 传递给 Agent Webhook 处理函数的上下文参数集合。
 * @property req Node.js 原始请求对象
 * @property res Node.js 原始响应对象
 * @property agent 解析后的 Agent 账号信息
 * @property config 全局插件配置
 * @property core OpenClaw 插件运行时
 * @property log 可选日志输出函数
 * @property error 可选错误输出函数
 */
export type AgentWebhookParams = {
    req: IncomingMessage;
    res: ServerResponse;
    /**
     * 上游已完成验签/解密时传入，避免重复协议处理。
     * 仅用于 POST 消息回调流程。
     */
    verifiedPost?: {
        timestamp: string;
        nonce: string;
        signature: string;
        encrypted: string;
        decrypted: string;
        parsed: WecomAgentInboundMessage;
    };
    agent: ResolvedAgentAccount;
    config: OpenClawConfig;
    core: PluginRuntime;
    log?: (msg: string) => void;
    error?: (msg: string) => void;
};

export type AgentInboundProcessDecision = {
    shouldProcess: boolean;
    reason: string;
};

/**
 * 仅允许“用户意图消息”进入 AI 会话。
 * - event 回调（如 enter_agent/subscribe）不应触发会话与自动回复
 * - 系统发送者（sys）不应触发会话与自动回复
 * - 缺失发送者时默认丢弃，避免写入异常会话
 */
export function shouldProcessAgentInboundMessage(params: {
    msgType: string;
    fromUser: string;
    eventType?: string;
}): AgentInboundProcessDecision {
    const msgType = String(params.msgType ?? "").trim().toLowerCase();
    const fromUser = String(params.fromUser ?? "").trim();
    const normalizedFromUser = fromUser.toLowerCase();
    const eventType = String(params.eventType ?? "").trim().toLowerCase();

    if (msgType === "event") {
        return {
            shouldProcess: false,
            reason: `event:${eventType || "unknown"}`,
        };
    }

    if (!fromUser) {
        return {
            shouldProcess: false,
            reason: "missing_sender",
        };
    }

    if (normalizedFromUser === "sys") {
        return {
            shouldProcess: false,
            reason: "system_sender",
        };
    }

    return {
        shouldProcess: true,
        reason: "user_message",
    };
}

function normalizeAgentId(value: unknown): number | undefined {
    if (typeof value === "number" && Number.isFinite(value)) return value;
    const raw = String(value ?? "").trim();
    if (!raw) return undefined;
    const parsed = Number(raw);
    return Number.isFinite(parsed) ? parsed : undefined;
}

/**
 * **resolveQueryParams (解析查询参数)**
 * 
 * 辅助函数：从 IncomingMessage 中解析 URL 查询字符串，用于获取签名、时间戳等参数。
 */
function resolveQueryParams(req: IncomingMessage): URLSearchParams {
    const url = new URL(req.url ?? "/", "http://localhost");
    return url.searchParams;
}

/**
 * 处理消息回调 (POST)
 */
async function handleMessageCallback(params: AgentWebhookParams): Promise<boolean> {
    const { req, res, verifiedPost, agent, config, core, log, error } = params;

    try {
        if (!verifiedPost) {
            error?.("[wecom-agent] inbound: missing preverified envelope");
            res.statusCode = 400;
            res.setHeader("Content-Type", "text/plain; charset=utf-8");
            res.end(`invalid request - 缺少上游验签结果${ERROR_HELP}`);
            return true;
        }

        log?.(`[wecom-agent] inbound: method=${req.method ?? "UNKNOWN"} remote=${req.socket?.remoteAddress ?? "unknown"}`);
        const query = resolveQueryParams(req);
        const querySignature = query.get("msg_signature") ?? "";

        const encrypted = verifiedPost.encrypted;
        const decrypted = verifiedPost.decrypted;
        const msg = verifiedPost.parsed;
        const timestamp = verifiedPost.timestamp;
        const nonce = verifiedPost.nonce;
        const signature = verifiedPost.signature || querySignature;
        log?.(
            `[wecom-agent] inbound: using preverified envelope timestamp=${timestamp ? "yes" : "no"} nonce=${nonce ? "yes" : "no"} msg_signature=${signature ? "yes" : "no"} encryptLen=${encrypted.length}`,
        );

        log?.(`[wecom-agent] inbound: decryptedBytes=${Buffer.byteLength(decrypted, "utf8")}`);

        const inboundAgentId = normalizeAgentId(extractAgentId(msg));
        if (
            inboundAgentId !== undefined &&
            typeof agent.agentId === "number" &&
            Number.isFinite(agent.agentId) &&
            inboundAgentId !== agent.agentId
        ) {
            error?.(
                `[wecom-agent] inbound: agentId mismatch ignored expectedAgentId=${agent.agentId} actualAgentId=${String(extractAgentId(msg) ?? "")}`,
            );
        }
        const msgType = extractMsgType(msg);
        const fromUser = extractFromUser(msg);
        const chatId = extractChatId(msg);
        const msgId = extractMsgId(msg);
        const eventType = String((msg as Record<string, unknown>).Event ?? "").trim().toLowerCase();
        if (msgId) {
            const ok = rememberAgentMsgId(msgId);
            if (!ok) {
                log?.(`[wecom-agent] duplicate msgId=${msgId} from=${fromUser} chatId=${chatId ?? "N/A"} type=${msgType}; skipped`);
                res.statusCode = 200;
                res.setHeader("Content-Type", "text/plain; charset=utf-8");
                res.end("success");
                return true;
            }
        }
        const content = String(extractContent(msg) ?? "");

        const preview = content.length > 100 ? `${content.slice(0, 100)}…` : content;
        log?.(`[wecom-agent] ${msgType} from=${fromUser} chatId=${chatId ?? "N/A"} msgId=${msgId ?? "N/A"} content=${preview}`);

        // 先返回 success (Agent 模式使用 API 发送回复，不用被动回复)
        res.statusCode = 200;
        res.setHeader("Content-Type", "text/plain; charset=utf-8");
        res.end("success");

        const decision = shouldProcessAgentInboundMessage({
            msgType,
            fromUser,
            eventType,
        });
        if (!decision.shouldProcess) {
            log?.(
                `[wecom-agent] skip processing: type=${msgType || "unknown"} event=${eventType || "N/A"} from=${fromUser || "N/A"} reason=${decision.reason}`,
            );
            return true;
        }

        // 异步处理消息
        processAgentMessage({
            agent,
            config,
            core,
            fromUser,
            chatId,
            msgType,
            content,
            msg,
            log,
            error,
        }).catch((err) => {
            error?.(`[wecom-agent] process failed: ${String(err)}`);
        });

        return true;
    } catch (err) {
        error?.(`[wecom-agent] callback failed: ${String(err)}`);
        res.statusCode = 400;
        res.setHeader("Content-Type", "text/plain; charset=utf-8");
        res.end(`error - 回调处理失败${ERROR_HELP}`);
        return true;
    }
}

/**
 * **processAgentMessage (处理 Agent 消息)**
 * 
 * 异步处理解密后的消息内容，并触发 OpenClaw Agent。
 * 流程：
 * 1. 路由解析：根据 userid或群ID 确定 Agent 路由。
 * 2. 媒体处理：如果是图片/文件等，下载资源。
 * 3. 上下文构建：创建 Inbound Context。
 * 4. 会话记录：更新 Session 状态。
 * 5. 调度回复：将 Agent 的响应通过 `api-client` 发送回企业微信。
 */
async function processAgentMessage(params: {
    agent: ResolvedAgentAccount;
    config: OpenClawConfig;
    core: PluginRuntime;
    fromUser: string;
    chatId?: string;
    msgType: string;
    content: string;
    msg: WecomAgentInboundMessage;
    log?: (msg: string) => void;
    error?: (msg: string) => void;
}): Promise<void> {
    const { agent, config, core, fromUser, chatId, content, msg, msgType, log, error } = params;

    const isGroup = Boolean(chatId);
    const peerId = isGroup ? chatId! : fromUser;
    const mediaMaxBytes = resolveWecomMediaMaxBytes(config);

    // 处理媒体文件
    const attachments: any[] = []; // TODO: define specific type
    let finalContent = content;
    let mediaPath: string | undefined;
    let mediaType: string | undefined;

    if (["image", "voice", "video", "file"].includes(msgType)) {
        const mediaId = extractMediaId(msg);
        if (mediaId) {
            try {
                log?.(`[wecom-agent] downloading media: ${mediaId} (${msgType})`);
                const { buffer, contentType, filename: headerFileName } = await downloadMedia({ agent, mediaId, maxBytes: mediaMaxBytes });
                const xmlFileName = extractFileName(msg);
                const originalFileName = (xmlFileName || headerFileName || `${mediaId}.bin`).trim();
                const heuristic = analyzeTextHeuristic(buffer);

                // 推断文件名后缀
                const extMap: Record<string, string> = {
                    "image/jpeg": "jpg", "image/png": "png", "image/gif": "gif",
                    "audio/amr": "amr", "audio/speex": "speex", "video/mp4": "mp4",
                };
                const textPreview = msgType === "file" ? buildTextFilePreview(buffer, 12_000) : undefined;
                const looksText = Boolean(textPreview);
                const originalExt = path.extname(originalFileName).toLowerCase();
                const normalizedContentType =
                    looksText && originalExt === ".md" ? "text/markdown" :
                    looksText && (!contentType || contentType === "application/octet-stream")
                        ? "text/plain; charset=utf-8"
                        : contentType;

                const ext = extMap[normalizedContentType] || (looksText ? "txt" : "bin");
                const filename = `${mediaId}.${ext}`;

                log?.(
                    `[wecom-agent] file meta: msgType=${msgType} mediaId=${mediaId} size=${buffer.length} maxBytes=${mediaMaxBytes} ` +
                    `contentType=${contentType} normalizedContentType=${normalizedContentType} originalFileName=${originalFileName} ` +
                    `xmlFileName=${xmlFileName ?? "N/A"} headerFileName=${headerFileName ?? "N/A"} ` +
                    `textHeuristic(sample=${heuristic.sampleSize}, bad=${heuristic.badCount}, ratio=${heuristic.badRatio.toFixed(4)}) ` +
                    `headHex="${previewHex(buffer)}"`,
                );

                // 使用 Core SDK 保存媒体文件
                const saved = await core.channel.media.saveMediaBuffer(
                    buffer,
                    normalizedContentType,
                    "inbound", // context/scope
                    mediaMaxBytes, // limit
                    originalFileName
                );

                log?.(`[wecom-agent] media saved to: ${saved.path}`);
                mediaPath = saved.path;
                mediaType = normalizedContentType;

                // 构建附件
                attachments.push({
                    name: originalFileName,
                    mimeType: normalizedContentType,
                    url: pathToFileURL(saved.path).href, // 使用跨平台安全的文件 URL
                });

                // 更新文本提示
                if (textPreview) {
                    finalContent = [
                        content,
                        "",
                        "文件内容预览：",
                        "```",
                        textPreview,
                        "```",
                        `(已下载 ${buffer.length} 字节)`,
                    ].join("\n");
                } else {
                    if (msgType === "file") {
                        finalContent = [
                            content,
                            "",
                            `已收到文件：${originalFileName}`,
                            `文件类型：${normalizedContentType || contentType || "未知"}`,
                            "提示：当前仅对文本/Markdown/JSON/CSV/HTML/PDF（可选）做内容抽取；其他二进制格式请转为 PDF 或复制文本内容。",
                            `(已下载 ${buffer.length} 字节)`,
                        ].join("\n");
                    } else {
                        finalContent = `${content} (已下载 ${buffer.length} 字节)`;
                    }
                }
                log?.(`[wecom-agent] file preview: enabled=${looksText} finalContentLen=${finalContent.length} attachments=${attachments.length}`);
            } catch (err) {
                error?.(`[wecom-agent] media processing failed: ${String(err)}`);
                finalContent = [
                    content,
                    "",
                    `媒体处理失败：${String(err)}`,
                    `提示：可在 OpenClaw 配置中提高 channels.wecom.media.maxBytes（当前=${mediaMaxBytes}）`,
                    `例如：openclaw config set channels.wecom.media.maxBytes ${50 * 1024 * 1024}`,
                ].join("\n");
            }
        } else {
            const keys = Object.keys((msg as unknown as Record<string, unknown>) ?? {}).slice(0, 50).join(",");
            error?.(`[wecom-agent] mediaId not found for ${msgType}; keys=${keys}`);
        }
    }

    // 解析路由
    const route = core.channel.routing.resolveAgentRoute({
        cfg: config,
        channel: "wecom",
        accountId: agent.accountId,
        peer: { kind: isGroup ? "group" : "direct", id: peerId },
    });

    // ===== 动态 Agent 路由注入 =====
    const useDynamicAgent = shouldUseDynamicAgent({
        chatType: isGroup ? "group" : "dm",
        senderId: fromUser,
        config,
    });

    if (shouldRejectWecomDefaultRoute({ cfg: config, matchedBy: route.matchedBy, useDynamicAgent })) {
        const prompt =
            `当前账号（${agent.accountId}）未绑定 OpenClaw Agent，已拒绝回退到默认主智能体。` +
            `请在 bindings 中添加：{"agentId":"你的Agent","match":{"channel":"wecom","accountId":"${agent.accountId}"}}`;
        error?.(
            `[wecom-agent] routing guard: blocked default fallback accountId=${agent.accountId} matchedBy=${route.matchedBy} from=${fromUser}`,
        );
        try {
            await sendText({ agent, toUser: fromUser, chatId: undefined, text: prompt });
            log?.(`[wecom-agent] routing guard prompt delivered to ${fromUser}`);
        } catch (err: unknown) {
            error?.(`[wecom-agent] routing guard prompt failed: ${String(err)}`);
        }
        return;
    }

    if (useDynamicAgent) {
        const targetAgentId = generateAgentId(
            isGroup ? "group" : "dm",
            peerId,
            agent.accountId,
        );
        route.agentId = targetAgentId;
        route.sessionKey = `agent:${targetAgentId}:wecom:${agent.accountId}:${isGroup ? "group" : "dm"}:${peerId}`;
        // 异步添加到 agents.list（不阻塞）
        ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
        log?.(`[wecom-agent] dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
    }
    // ===== 动态 Agent 路由注入结束 =====

    // 构建上下文
    const fromLabel = isGroup ? `group:${peerId}` : `user:${fromUser}`;
    const storePath = core.channel.session.resolveStorePath(config.session?.store, {
        agentId: route.agentId,
    });
    const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
    const previousTimestamp = core.channel.session.readSessionUpdatedAt({
        storePath,
        sessionKey: route.sessionKey,
    });
    const body = core.channel.reply.formatAgentEnvelope({
        channel: "WeCom",
        from: fromLabel,
        previousTimestamp,
        envelope: envelopeOptions,
        body: finalContent,
    });

    const authz = await resolveWecomCommandAuthorization({
        core,
        cfg: config,
        // Agent 门禁应读取 channels.wecom.agent.dm（即 agent.config.dm），而不是 channels.wecom.dm（不存在）
        accountConfig: agent.config,
        rawBody: finalContent,
        senderUserId: fromUser,
    });
    log?.(`[wecom-agent] authz: dmPolicy=${authz.dmPolicy} shouldCompute=${authz.shouldComputeAuth} sender=${fromUser.toLowerCase()} senderAllowed=${authz.senderAllowed} authorizerConfigured=${authz.authorizerConfigured} commandAuthorized=${String(authz.commandAuthorized)}`);

    // 命令门禁：未授权时必须明确回复（Agent 侧用私信提示）
    if (authz.shouldComputeAuth && authz.commandAuthorized !== true) {
        const prompt = buildWecomUnauthorizedCommandPrompt({ senderUserId: fromUser, dmPolicy: authz.dmPolicy, scope: "agent" });
        try {
            await sendText({ agent, toUser: fromUser, chatId: undefined, text: prompt });
            log?.(`[wecom-agent] unauthorized command: replied via DM to ${fromUser}`);
        } catch (err: unknown) {
            error?.(`[wecom-agent] unauthorized command reply failed: ${String(err)}`);
        }
        return;
    }

    const ctxPayload = core.channel.reply.finalizeInboundContext({
        Body: body,
        RawBody: finalContent,
        CommandBody: finalContent,
        Attachments: attachments.length > 0 ? attachments : undefined,
        From: isGroup ? `wecom:group:${peerId}` : `wecom:${fromUser}`,
        // 使用 wecom-agent: 前缀标记 Agent 会话，确保 outbound 路由不会混入 Bot WS 发送路径。
        // resolveWecomTarget 已支持剥离 wecom-agent: 前缀（target.ts L41），解析结果不变。
        To: `wecom-agent:${fromUser}`,
        SessionKey: route.sessionKey,
        AccountId: route.accountId,
        ChatType: isGroup ? "group" : "direct",
        ConversationLabel: fromLabel,
        SenderName: fromUser,
        SenderId: fromUser,
        Provider: "wecom",
        Surface: "webchat",
        OriginatingChannel: "wecom",
        // 标记为 Agent 会话的回复路由目标，避免与 Bot 会话混淆：
        // - 用于让 /new /reset 这类命令回执不被 Bot 侧策略拦截
        // - 群聊场景也统一路由为私信触发者（与 deliver 策略一致）
        OriginatingTo: `wecom-agent:${fromUser}`,
        CommandAuthorized: authz.commandAuthorized ?? true,
        MediaPath: mediaPath,
        MediaType: mediaType,
        MediaUrl: mediaPath,
    });

    // 记录会话
    await core.channel.session.recordInboundSession({
        storePath,
        sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
        ctx: ctxPayload,
        onRecordError: (err: unknown) => {
            error?.(`[wecom-agent] session record failed: ${String(err)}`);
        },
    });

    // 调度回复
    await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
        ctx: ctxPayload,
        cfg: config,
        dispatcherOptions: {
            deliver: async (payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string }, info: { kind: string }) => {
                let text = payload.text ?? "";

                // ── 1. 解析 MEDIA: 指令（兜底处理核心 splitMediaFromOutput 未覆盖的边界情况）──
                const mediaDirectivePaths: string[] = [];
                const mediaDirectiveRe = /^MEDIA:\s*`?([^\n`]+?)`?\s*$/gm;
                let _mdMatch: RegExpExecArray | null;
                while ((_mdMatch = mediaDirectiveRe.exec(text)) !== null) {
                    let p = (_mdMatch[1] ?? "").trim();
                    if (!p) continue;
                    if (p.startsWith("~/") || p === "~") {
                        const home = process.env.HOME || "/root";
                        p = p.replace(/^~/, home);
                    }
                    if (!mediaDirectivePaths.includes(p)) mediaDirectivePaths.push(p);
                }
                // 从回复文本中移除 MEDIA: 指令行
                if (mediaDirectivePaths.length > 0) {
                    text = text.replace(/^MEDIA:\s*`?[^\n`]+?`?\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
                }

                // ── 2. 合并所有媒体 URL ──
                const mediaUrls = Array.from(new Set([
                    ...(payload.mediaUrls || []),
                    ...(payload.mediaUrl ? [payload.mediaUrl] : []),
                    ...mediaDirectivePaths,
                ]));

                // ── 3. 发送文本部分 ──
                if (text.trim()) {
                    try {
                        await sendText({ agent, toUser: fromUser, chatId: undefined, text });
                        log?.(`[wecom-agent] reply delivered (${info.kind}) to ${fromUser} (textLen=${text.length})`);
                    } catch (err: unknown) {
                        const message = err instanceof Error ? `${err.message}${err.cause ? ` (cause: ${String(err.cause)})` : ""}` : String(err);
                        error?.(`[wecom-agent] reply failed: ${message}`);
                    }
                }

                // ── 4. 逐个发送媒体文件（通过 Agent API 上传 + 发送）──
                for (const mediaPath of mediaUrls) {
                    try {
                        const isRemoteUrl = /^https?:\/\//i.test(mediaPath);
                        let buf: Buffer;
                        let contentType: string;
                        let filename: string;

                        if (isRemoteUrl) {
                            const res = await fetch(mediaPath, { signal: AbortSignal.timeout(30_000) });
                            if (!res.ok) throw new Error(`download failed: ${res.status}`);
                            buf = Buffer.from(await res.arrayBuffer());
                            contentType = res.headers.get("content-type") || "application/octet-stream";
                            filename = new URL(mediaPath).pathname.split("/").pop() || "media";
                        } else {
                            const fs = await import("node:fs/promises");
                            const pathModule = await import("node:path");
                            buf = await fs.readFile(mediaPath);
                            filename = pathModule.basename(mediaPath);
                            const ext = pathModule.extname(mediaPath).slice(1).toLowerCase();
                            const MIME_MAP: Record<string, string> = {
                                jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif",
                                webp: "image/webp", mp3: "audio/mpeg", wav: "audio/wav", amr: "audio/amr",
                                mp4: "video/mp4", mov: "video/quicktime", pdf: "application/pdf",
                                doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                                xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                                txt: "text/plain", csv: "text/csv", json: "application/json", zip: "application/zip",
                            };
                            contentType = MIME_MAP[ext] ?? "application/octet-stream";
                        }

                        // 确定企微媒体类型
                        let mediaType: "image" | "voice" | "video" | "file" = "file";
                        if (contentType.startsWith("image/")) mediaType = "image";
                        else if (contentType.startsWith("audio/")) mediaType = "voice";
                        else if (contentType.startsWith("video/")) mediaType = "video";

                        log?.(`[wecom-agent] uploading media: ${filename} (${mediaType}, ${contentType}, ${buf.length} bytes)`);

                        const mediaId = await uploadMedia({ agent, type: mediaType, buffer: buf, filename });

                        await sendAgentMedia({
                            agent,
                            toUser: fromUser,
                            mediaId,
                            mediaType,
                            ...(mediaType === "video" ? { title: filename, description: "" } : {}),
                        });

                        log?.(`[wecom-agent] media sent (${info.kind}) to ${fromUser}: ${filename} (${mediaType})`);
                    } catch (err: unknown) {
                        const message = err instanceof Error ? `${err.message}${err.cause ? ` (cause: ${String(err.cause)})` : ""}` : String(err);
                        error?.(`[wecom-agent] media send failed: ${mediaPath}: ${message}`);
                        // 降级：发文本通知用户
                        try {
                            await sendText({ agent, toUser: fromUser, chatId: undefined, text: `⚠️ 文件发送失败: ${mediaPath.split("/").pop() || mediaPath}\n${message}` });
                        } catch { /* ignore */ }
                    }
                }

                // 如果既没有文本也没有媒体，不做任何事（防止空回复）
            },
            onError: (err: unknown, info: { kind: string }) => {
                error?.(`[wecom-agent] ${info.kind} reply error: ${String(err)}`);
            },
        }
    });
}

/**
 * **handleAgentWebhook (Agent Webhook 入口)**
 * 
 * 统一处理 Agent 模式的 POST 消息回调请求。
 * URL 验证与验签/解密由 monitor 层统一处理后再调用本函数。
 */
export async function handleAgentWebhook(params: AgentWebhookParams): Promise<boolean> {
    const { req } = params;

    if (req.method === "POST") {
        return handleMessageCallback(params);
    }

    return false;
}
