/**
 * WeCom (企业微信) adapter — Bot WebSocket mode via @wecom/aibot-node-sdk.
 *
 * Supports:
 * - Inbound text, image, voice (STT), file, video, mixed (图文混排) messages
 * - Native streaming replies via replyStream (with 6-min timeout auto-fallback)
 * - Outbound markdown messages via sendMessage
 * - Permission confirmation through plain text replies
 *
 * Reference: wecom-openclaw-plugin (official Tencent WeCom plugin)
 */

import path from 'path'
import fs from 'fs'
import { ChannelAdapter } from '../base'
import { registerAdapter } from '../registry'
import type { ChannelType, IncomingMessage, OutboundMessage, ImPermissionRequest } from '../../types'
import type { PlatformContext } from '@/lib/types'
import type { DeliveryLayer } from '../../core/delivery'
import { extractTemplateCards } from './template-cards'
import { WecomStreamRenderer } from './stream-renderer'
import type { StreamRenderContext } from '../stream-renderer'
import { getForgeDataDir } from '@/lib/forge-data'
import {
    WSClient,
    generateReqId,
    WSAuthFailureError,
    WSReconnectExhaustedError,
} from '@wecom/aibot-node-sdk'
import type { WsFrame, Logger, WeComMediaType } from '@wecom/aibot-node-sdk'

// ---------------------------------------------------------------------------
// Message body type (from WsFrame.body — mirrors wecom-openclaw-plugin)
// ---------------------------------------------------------------------------

interface WeComMessageBody {
    msgid: string
    aibotid?: string
    chatid?: string
    chattype: 'single' | 'group'
    from: { userid: string; corpid?: string }
    response_url?: string
    msgtype: string
    text?: { content: string }
    image?: { url?: string; base64?: string; aeskey?: string }
    voice?: { content?: string }
    file?: { url?: string; aeskey?: string }
    video?: { url?: string; aeskey?: string }
    mixed?: {
        msg_item: Array<{
            msgtype: 'text' | 'image'
            text?: { content: string }
            image?: { url?: string; base64?: string; aeskey?: string }
        }>
    }
    quote?: {
        msgtype: string
        text?: { content: string }
        voice?: { content: string }
        image?: { url?: string; aeskey?: string }
        file?: { url?: string; aeskey?: string }
    }
    event?: {
        eventtype?: string
        template_card_event?: {
            card_type?: string
            event_key?: string
            task_id?: string
            selected_items?: {
                selected_item?: Array<{
                    question_key?: string
                    option_ids?: {
                        option_id?: string[]
                    }
                }>
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Active frame entry for streaming reply
// ---------------------------------------------------------------------------

interface ActiveFrameEntry {
    frame: WsFrame
    streamId: string
    started: boolean
}

// ---------------------------------------------------------------------------
// Adapter implementation
// ---------------------------------------------------------------------------

const DEFAULT_WS_URL = 'wss://openws.work.weixin.qq.com'
const CONNECTION_DEAD_MS = 5 * 60 * 1000

// ---------------------------------------------------------------------------
// Media limits (mirror wecom-openclaw-plugin)
// ---------------------------------------------------------------------------

const IMAGE_MAX_BYTES = 10 * 1024 * 1024
const VIDEO_MAX_BYTES = 10 * 1024 * 1024
const VOICE_MAX_BYTES = 2 * 1024 * 1024
const FILE_MAX_BYTES = 20 * 1024 * 1024
const ABSOLUTE_MAX_BYTES = FILE_MAX_BYTES
const VOICE_SUPPORTED_MIMES = new Set(['audio/amr'])

const THINKING_MESSAGE = '⏳ 正在思考…'

export class WecomAdapter extends ChannelAdapter {
    readonly channelType: ChannelType = 'wecom'
    readonly supportsMessageEditing = false

    private wsClient: WSClient | null = null
    private running = false
    private stopped = false
    private lastEventTime = 0
    private botId = ''
    /**
     * Cached WeCom CLI config dir for this adapter instance, lazily computed
     * from {@link botId} the first time it's requested. Refreshed by the
     * CLI init API endpoint when a new dir is provisioned.
     */
    private cliConfigDir: string | null | undefined

    private messageQueue: IncomingMessage[] = []
    private messageWaiter: ((msg: IncomingMessage | null) => void) | null = null

    private permissionCallback: ((requestId: string, decision: 'allow' | 'deny') => void) | null = null
    private pendingPermByChat = new Map<string, { requestIdPrefix: string; senderId: string; timer: ReturnType<typeof setTimeout> | null }>()
    private permissionTimers = new Map<string, ReturnType<typeof setTimeout>>()

    private seenMessageIds = new Set<string>()
    private seenMessageIdTimer: ReturnType<typeof setTimeout> | null = null

    private activeFrames = new Map<string, ActiveFrameEntry>()

    // ---------------------------------------------------------------------------
    // Lifecycle
    // ---------------------------------------------------------------------------

    async start(config: Record<string, string>): Promise<void> {
        this.stopped = false

        const botId = (config.bot_id || '').trim()
        const secret = (config.secret || '').trim()
        if (!botId || !secret) {
            throw new Error('WeCom bot_id and secret are required')
        }
        this.botId = botId
        this.cliConfigDir = undefined // force recompute on next request

        const wsUrl = (config.ws_url || '').trim() || DEFAULT_WS_URL

        const logger = this.createLogger()

        this.wsClient = new WSClient({
            botId,
            secret,
            wsUrl,
            logger,
            heartbeatInterval: 30_000,
            maxReconnectAttempts: 10,
            maxAuthFailureAttempts: 5,
        })

        this.wsClient.on('connected', () => {
            console.log('[WeCom SDK] WebSocket connected')
        })

        this.wsClient.on('authenticated', () => {
            console.log('[WeCom SDK] Authentication successful')
            this.running = true
            this.lastEventTime = Date.now()
        })

        this.wsClient.on('disconnected', (reason: string) => {
            console.log(`[WeCom SDK] WebSocket disconnected: ${reason}`)
            this.running = false
        })

        this.wsClient.on('error', (error: Error) => {
            console.error(`[WeCom SDK] WebSocket error: ${error.message}`)
            if (error instanceof WSAuthFailureError) {
                // Authentication failure: config error (bad bot_id/secret). Stop the adapter
                // and let the BridgeManager surface the error instead of retrying forever.
                console.error('[WeCom SDK] Authentication failed — stopping adapter')
                this.running = false
                this.cleanup()
                return
            }
            if (error instanceof WSReconnectExhaustedError) {
                console.error('[WeCom SDK] Reconnect attempts exhausted — connection marked as dead')
                this.running = false
                this.cleanup()
                return
            }
        })

        // Kicked by server: another connection with the same bot_id was established.
        // Do not auto-reconnect to avoid a mutual-kicking loop. The BridgeManager will
        // surface the error and the user must resolve the duplicate instance.
        this.wsClient.on('event.disconnected_event', () => {
            console.error('[WeCom SDK] Kicked by server: a new connection was established elsewhere. Auto-reconnect suppressed to avoid mutual kicking.')
            this.running = false
            this.cleanup()
        })

        this.wsClient.on('message', (frame: WsFrame) => {
            if (this.stopped) return
            this.lastEventTime = Date.now()
            this.handleIncomingMessage(frame).catch(err => {
                console.error('[WeCom SDK] Failed to process message:', err instanceof Error ? err.message : err)
            })
        })

        this.wsClient.on('event', (frame: WsFrame) => {
            if (this.stopped) return
            this.lastEventTime = Date.now()
            this.handleIncomingMessage(frame).catch(err => {
                console.error('[WeCom SDK] Failed to process event:', err instanceof Error ? err.message : err)
            })
        })

        this.wsClient.connect()
        this.running = true
        console.log(`[WeCom SDK] Connected via WSClient, botId=${botId}`)
    }

    private cleanup(): void {
        this.activeFrames.clear()
        this.pendingPermByChat.clear()
        // Clear all permission timers to prevent memory leaks
        for (const timer of this.permissionTimers.values()) {
            clearTimeout(timer)
        }
        this.permissionTimers.clear()
        this.lastEventTime = 0
        this.seenMessageIds.clear()
        if (this.seenMessageIdTimer) {
            clearTimeout(this.seenMessageIdTimer)
            this.seenMessageIdTimer = null
        }
    }

    async stop(): Promise<void> {
        this.running = false
        this.stopped = true

        if (this.wsClient) {
            try {
                this.wsClient.disconnect()
            } catch (err) {
                console.warn('[WeCom SDK] WSClient disconnect error:', err instanceof Error ? err.message : err)
            }
            this.wsClient = null
        }

        if (this.messageWaiter) {
            this.messageWaiter(null)
            this.messageWaiter = null
        }
        this.messageQueue = []
        this.cleanup()

        console.log('[WeCom SDK] Stopped')
    }

    isRunning(): boolean {
        return this.running
    }

    // ---------------------------------------------------------------------------
    // Credentials & CLI config access (used by BridgeManager to inject env
    // variables into the SDK subprocess without re-reading the DB).
    // ---------------------------------------------------------------------------

    /** Return this adapter's bot_id. Empty string if not yet started. */
    getBotId(): string {
        return this.botId
    }

    /**
     * Return the WeCom CLI config dir for this bot, or undefined if the
     * dir has not been initialized yet (mcp_config.enc is missing). The
     * result is cached for the lifetime of the adapter — call
     * {@link invalidateCliConfigDirCache} when the dir is provisioned.
     */
    getCliConfigDir(): string | undefined {
        if (!this.botId) return undefined
        if (this.cliConfigDir === undefined) {
            const dir = path.join(getForgeDataDir(), 'wecom-cli', `bot_${this.botId}`)
            this.cliConfigDir = fs.existsSync(path.join(dir, 'mcp_config.enc')) ? dir : null
        }
        return this.cliConfigDir ?? undefined
    }

    /** Return WeCom CLI config dir for platformContext (was inline in bridge-manager). */
    async ensureChannelAuth(_msg: IncomingMessage): Promise<Partial<PlatformContext> | undefined> {
        const cliConfigDir = this.getCliConfigDir()
        return cliConfigDir ? { wecom: { cliConfigDir } } : undefined
    }

    /** 企微 replyStream 流式渲染（C-2）。 */
    createStreamRenderer(ctx: StreamRenderContext) {
        return new WecomStreamRenderer(ctx)
    }

    // ---------------------------------------------------------------------------
    // Platform capability hooks (C-3)
    // ---------------------------------------------------------------------------

    /** 企微 replyStream 自处理错误投递（对应原 bridge 行 595：不发额外错误文本）。 */
    handlesErrorDeliveryInRenderer(): boolean {
        return true
    }

    /**
     * 平台特化最终投递：提取模板卡 + 剩余文本（对应原 bridge 行 745-759）。
     * 永远返回 true（已处理），bridge 不走默认文本。
     */
    async deliverPlatformFinal(chatId: string, text: string, delivery: DeliveryLayer): Promise<boolean> {
        const { cards, remainingText } = extractTemplateCards(text)
        for (const card of cards) {
            try {
                await delivery.deliver(this, chatId, '', { templateCard: card, skipDedup: true })
            } catch (err) {
                console.warn('[WeCom SDK] template card delivery failed:', err instanceof Error ? err.message : err)
            }
        }
        const finalText = remainingText || (cards.length > 0 ? '📋 卡片消息已发送。' : '')
        if (finalText) {
            await delivery.deliver(this, chatId, finalText, { skipDedup: true })
        }
        return true
    }

    /** Invalidate the CLI config dir cache (call after the dir is provisioned). */
    invalidateCliConfigDirCache(): void {
        this.cliConfigDir = undefined
    }

    // ---------------------------------------------------------------------------
    // Message consumption (pull model)
    // ---------------------------------------------------------------------------

    async consumeOne(signal: AbortSignal): Promise<IncomingMessage | null> {
        if (this.messageQueue.length > 0) {
            return this.messageQueue.shift()!
        }
        if (signal.aborted) return null

        return new Promise<IncomingMessage | null>((resolve) => {
            let resolved = false

            const cleanup = () => {
                if (resolved) return
                resolved = true
                this.messageWaiter = null
            }

            const healthCheck = setInterval(() => {
                const sinceLastEvent = Date.now() - this.lastEventTime
                if (this.lastEventTime > 0 && sinceLastEvent >= CONNECTION_DEAD_MS) {
                    console.warn(`[WeCom SDK] No events for ${Math.round(sinceLastEvent / 1000)}s — connection likely dead`)
                    this.running = false
                    clearInterval(healthCheck)
                    cleanup()
                    resolve(null)
                }
            }, 60_000)

            this.messageWaiter = (msg) => {
                signal.removeEventListener('abort', onAbort)
                clearInterval(healthCheck)
                cleanup()
                resolve(msg)
            }

            const onAbort = () => {
                clearInterval(healthCheck)
                cleanup()
                resolve(null)
            }
            signal.addEventListener('abort', onAbort, { once: true })
        })
    }

    // ---------------------------------------------------------------------------
    // Outbound messaging (non-stream)
    // ---------------------------------------------------------------------------

    async send(msg: OutboundMessage): Promise<string | undefined> {
        if (msg.deleteMessageId || msg.editMessageId) {
            return undefined
        }
        if (!msg.text.trim()) return undefined
        if (!this.wsClient) throw new Error('WeCom client not initialized')

        await this.wsClient.sendMessage(msg.chatId, {
            msgtype: 'markdown',
            markdown: { content: msg.text },
        })
        return undefined
    }

    async sendImage(chatId: string, imageBuffer: Buffer, caption?: string): Promise<void> {
        if (!this.wsClient) {
            if (caption) await this.send({ chatId, text: `[图片] ${caption}` })
            return
        }

        const result = await this.uploadAndSendMedia(chatId, imageBuffer, 'image.png', 'image/png')

        if (result.ok) {
            if (caption) await this.send({ chatId, text: caption })
            if (result.downgradeNote) {
                await this.send({ chatId, text: `ℹ️ ${result.downgradeNote}` })
            }
            return
        }

        if (result.rejectReason) {
            await this.send({ chatId, text: `⚠️ ${result.rejectReason}` })
            return
        }

        const fallback = caption ? `[图片] ${caption}` : '[图片]'
        await this.send({ chatId, text: fallback })
    }

    async sendFile(chatId: string, fileBuffer: Buffer, filename: string, caption?: string): Promise<void> {
        if (!this.wsClient) {
            const fallback = caption ? `${caption}\n📎 ${filename}` : `📎 ${filename}`
            await this.send({ chatId, text: fallback })
            return
        }

        const mimeType = this.guessMimeType(filename)
        const result = await this.uploadAndSendMedia(chatId, fileBuffer, filename, mimeType)

        if (result.ok) {
            if (caption) {
                await this.send({ chatId, text: caption })
            }
            if (result.downgradeNote) {
                await this.send({ chatId, text: `ℹ️ ${result.downgradeNote}` })
            }
            return
        }

        if (result.rejectReason) {
            await this.send({ chatId, text: `⚠️ ${result.rejectReason}` })
            return
        }

        // Final fallback: text + filename
        const fallback = caption ? `${caption}\n📎 ${filename}` : `📎 ${filename}`
        await this.send({ chatId, text: fallback })
    }

    async sendTypingIndicator(_chatId: string): Promise<void> {
        // WeCom doesn't have a typing indicator API; do nothing.
        // The BridgeManager uses replyStreamDraft for live progress updates.
    }

    async sendTemplateCard(chatId: string, card: Record<string, unknown>): Promise<void> {
        if (!this.wsClient) throw new Error('WeCom client not initialized')
        await this.wsClient.sendMessage(chatId, {
            msgtype: 'template_card',
            template_card: card as Record<string, unknown> & { card_type: string },
        })
    }

    // ---------------------------------------------------------------------------
    // Internal: Media helpers
    // ---------------------------------------------------------------------------

    private guessMimeType(filename: string): string {
        const ext = filename.split('.').pop()?.toLowerCase() || ''
        const map: Record<string, string> = {
            jpg: 'image/jpeg',
            jpeg: 'image/jpeg',
            png: 'image/png',
            gif: 'image/gif',
            webp: 'image/webp',
            bmp: 'image/bmp',
            mp4: 'video/mp4',
            mov: 'video/quicktime',
            avi: 'video/x-msvideo',
            webm: 'video/webm',
            amr: 'audio/amr',
            mp3: 'audio/mpeg',
            wav: 'audio/wav',
            m4a: 'audio/mp4',
            ogg: 'audio/ogg',
            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',
            ppt: 'application/vnd.ms-powerpoint',
            pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            txt: 'text/plain',
            md: 'text/markdown',
            csv: 'text/csv',
            json: 'application/json',
            zip: 'application/zip',
            rar: 'application/vnd.rar',
            '7z': 'application/x-7z-compressed',
        }
        return map[ext] || 'application/octet-stream'
    }

    private detectWeComMediaType(mimeType: string): WeComMediaType {
        const mime = mimeType.toLowerCase()
        if (mime.startsWith('image/')) return 'image'
        if (mime.startsWith('video/')) return 'video'
        if (mime.startsWith('audio/') || mime === 'application/ogg') return 'voice'
        return 'file'
    }

    private applyFileSizeLimits(
        fileSize: number,
        detectedType: WeComMediaType,
        contentType?: string,
    ): { finalType: WeComMediaType; shouldReject: boolean; rejectReason?: string; downgraded: boolean; downgradeNote?: string } {
        const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2)

        if (fileSize > ABSOLUTE_MAX_BYTES) {
            return {
                finalType: detectedType,
                shouldReject: true,
                rejectReason: `文件大小 ${fileSizeMB}MB 超过了企业微信允许的最大限制 20MB，无法发送。请尝试压缩文件或减小文件大小。`,
                downgraded: false,
            }
        }

        switch (detectedType) {
            case 'image':
                if (fileSize > IMAGE_MAX_BYTES) {
                    return {
                        finalType: 'file',
                        shouldReject: false,
                        downgraded: true,
                        downgradeNote: `图片大小 ${fileSizeMB}MB 超过 10MB 限制，已转为文件格式发送`,
                    }
                }
                break
            case 'video':
                if (fileSize > VIDEO_MAX_BYTES) {
                    return {
                        finalType: 'file',
                        shouldReject: false,
                        downgraded: true,
                        downgradeNote: `视频大小 ${fileSizeMB}MB 超过 10MB 限制，已转为文件格式发送`,
                    }
                }
                break
            case 'voice':
                if (contentType && !VOICE_SUPPORTED_MIMES.has(contentType.toLowerCase())) {
                    return {
                        finalType: 'file',
                        shouldReject: false,
                        downgraded: true,
                        downgradeNote: `语音格式 ${contentType} 不支持，企微仅支持 AMR 格式，已转为文件格式发送`,
                    }
                }
                if (fileSize > VOICE_MAX_BYTES) {
                    return {
                        finalType: 'file',
                        shouldReject: false,
                        downgraded: true,
                        downgradeNote: `语音大小 ${fileSizeMB}MB 超过 2MB 限制，已转为文件格式发送`,
                    }
                }
                break
            case 'file':
                break
        }

        return { finalType: detectedType, shouldReject: false, downgraded: false }
    }

    private async uploadAndSendMedia(
        chatId: string,
        buffer: Buffer,
        filename: string,
        mimeType: string,
    ): Promise<{ ok: boolean; mediaId?: string; rejectReason?: string; error?: string; downgraded?: boolean; downgradeNote?: string }> {
        if (!this.wsClient) {
            return { ok: false, error: 'WeCom client not initialized' }
        }

        const detectedType = this.detectWeComMediaType(mimeType)
        const sizeCheck = this.applyFileSizeLimits(buffer.length, detectedType, mimeType)

        if (sizeCheck.shouldReject) {
            return { ok: false, rejectReason: sizeCheck.rejectReason }
        }

        try {
            const uploadResult = await this.wsClient.uploadMedia(buffer, {
                type: sizeCheck.finalType,
                filename,
            })
            const sendResult = await this.wsClient.sendMediaMessage(chatId, sizeCheck.finalType, uploadResult.media_id)
            return {
                ok: true,
                mediaId: uploadResult.media_id,
                downgraded: sizeCheck.downgraded,
                downgradeNote: sizeCheck.downgradeNote,
            }
        } catch (err) {
            const errMsg = err instanceof Error ? err.message : String(err)
            console.warn('[WeCom SDK] uploadAndSendMedia failed:', errMsg)
            return { ok: false, error: errMsg }
        }
    }

    // ---------------------------------------------------------------------------
    // Streaming reply (native replyStream)
    // ---------------------------------------------------------------------------

    async replyStreamDraft(chatId: string, text: string): Promise<void> {
        const entry = this.activeFrames.get(chatId)
        if (!entry || !this.wsClient?.isConnected) return
        await this.wsClient.replyStream(entry.frame, entry.streamId, text, false)
        entry.started = true
    }

    /**
     * Finalize streaming reply. Returns a result indicating how the message was sent.
     * - 'stream': sent via replyStream (success)
     * - 'fallback': sent via sendMessage (stream expired or unavailable)
     * - 'failed': could not send (wsClient unavailable)
     * - 'error': threw an unexpected error (caller should fallback)
     */
    async replyStreamFinal(chatId: string, text: string): Promise<'stream' | 'fallback' | 'failed' | 'error'> {
        const entry = this.activeFrames.get(chatId)
        if (!entry || !this.wsClient?.isConnected) {
            if (this.wsClient?.isConnected) {
                await this.wsClient.sendMessage(chatId, {
                    msgtype: 'markdown',
                    markdown: { content: text },
                })
                this.activeFrames.delete(chatId)
                return 'fallback'
            }
            this.activeFrames.delete(chatId)
            return 'failed'
        }
        try {
            await this.wsClient.replyStream(entry.frame, entry.streamId, text, true)
            return 'stream'
        } catch (err: any) {
            const errMsg = err?.errmsg || err?.message || String(err)
            if (err?.errcode === 846608 || errMsg.includes('846608')) {
                console.log('[WeCom SDK] Stream expired, sending final text via sendMessage')
                await this.wsClient.sendMessage(chatId, {
                    msgtype: 'markdown',
                    markdown: { content: text },
                })
                return 'fallback'
            }
            // Unexpected error — let caller handle fallback
            return 'error'
        } finally {
            this.activeFrames.delete(chatId)
        }
    }

    cleanupActiveFrame(chatId: string): void {
        this.activeFrames.delete(chatId)
    }

    // ---------------------------------------------------------------------------
    // Permission UI
    // ---------------------------------------------------------------------------

    async sendPermissionPrompt(req: ImPermissionRequest): Promise<void> {
        const inputSummary = Object.entries(req.toolInput)
            .map(([k, v]) => `${k}: ${String(v).slice(0, 80)}`)
            .join('\n')
        const prefix = req.requestId.slice(0, 8)

        // If a previous timer for this chat is still pending, cancel it before
        // scheduling a new one (covers the "second prompt while the first is
        // still active" edge case).
        const existingTimer = this.permissionTimers.get(req.chatId)
        if (existingTimer) clearTimeout(existingTimer)

        const timer = setTimeout(() => {
            this.permissionTimers.delete(req.chatId)
            const current = this.pendingPermByChat.get(req.chatId)
            if (current?.requestIdPrefix === prefix) {
                this.pendingPermByChat.delete(req.chatId)
            }
        }, 120_000)
        this.permissionTimers.set(req.chatId, timer)
        this.pendingPermByChat.set(req.chatId, { requestIdPrefix: prefix, senderId: req.senderId, timer })

        await this.send({
            chatId: req.chatId,
            text: `需要授权\n\nForge 想要使用 ${req.toolName}：\n${inputSummary}\n\n回复"允许"或"拒绝"`,
        })
    }

    onPermissionResponse(callback: (requestId: string, decision: 'allow' | 'deny') => void): void {
        this.permissionCallback = callback
    }

    // ---------------------------------------------------------------------------
    // Config validation
    // ---------------------------------------------------------------------------

    validateConfig(config: Record<string, string>): { valid: boolean; error?: string } {
        const botId = (config.bot_id || '').trim()
        const secret = (config.secret || '').trim()
        if (!botId) return { valid: false, error: 'WeCom bot_id is required' }
        if (!secret) return { valid: false, error: 'WeCom secret is required' }
        return { valid: true }
    }

    // ---------------------------------------------------------------------------
    // Internal: Incoming message handler
    // ---------------------------------------------------------------------------

    private async handleIncomingMessage(frame: WsFrame): Promise<void> {
        if (this.stopped) return

        const body = frame.body as WeComMessageBody
        if (!body || !body.from?.userid) return

        const msgId = body.msgid
        if (msgId && this.isDuplicate(msgId)) return

        const chatId = body.chatid || body.from.userid
        const isDm = body.chattype === 'single'
        const senderId = body.from.userid

        if (body.msgtype === 'event') {
            const eventType = body.event?.eventtype
            if (eventType === 'template_card_event') {
                const eventText = this.buildTemplateCardEventText(body)
                if (eventText) {
                    const incoming: IncomingMessage = {
                        channelType: 'wecom',
                        channelId: 'wecom',
                        chatId,
                        messageId: msgId,
                        senderId,
                        senderName: 'User',
                        text: eventText,
                        isDm,
                        isGroupMention: !isDm,
                        wecomFrame: frame,
                    }
                    this.enqueueMessage(incoming)
                }
                return
            }
            console.log(`[WeCom SDK] Skipping event message: eventtype=${eventType}`)
            return
        }

        const { text, images, files, quoteContent } = await this.parseMessage(body)

        if (!text && (!images || images.length === 0) && (!files || files.length === 0)) {
            console.log('[WeCom SDK] Skipping empty message')
            return
        }

        if (this.handlePermCommand(text, chatId, senderId)) return
        if (this.handleSimplePermResponse(text, chatId, senderId)) return

        const streamId = generateReqId('stream')
        // Check for existing active frame to detect potential race condition.
        // If a previous stream is still in-flight, close it out so the user
        // doesn't see a draft that never completes (would otherwise hang
        // until the SDK's 6-min stream TTL).
        const existingFrame = this.activeFrames.get(chatId)
        if (existingFrame) {
            console.warn(`[WeCom SDK] Overwriting active frame for chatId=${chatId}, existingStreamId=${existingFrame.streamId}, newStreamId=${streamId}`)
            if (existingFrame.started && this.wsClient?.isConnected) {
                try {
                    await this.wsClient.replyStream(existingFrame.frame, existingFrame.streamId, '(replaced)', true)
                } catch (err) {
                    console.warn('[WeCom SDK] Failed to finalize replaced stream:', err instanceof Error ? err.message : err)
                }
            }
        }
        this.activeFrames.set(chatId, { frame, streamId, started: false })

        let finalText = text
        if (quoteContent && !text) {
            finalText = quoteContent
        } else if (quoteContent && text) {
            finalText = `> ${quoteContent}\n\n${text}`
        }

        const incoming: IncomingMessage = {
            channelType: 'wecom',
            channelId: 'wecom',
            chatId,
            messageId: msgId,
            senderId,
            senderName: 'User',
            text: finalText,
            isDm,
            isGroupMention: !isDm,
            ...(images && images.length > 0 && { images }),
            ...(files && files.length > 0 && { files }),
            wecomFrame: frame,
        }

        console.log(`[WeCom SDK] Incoming: type=${body.msgtype}, chat=${chatId}, isDm=${isDm}, sender=${senderId}, text="${finalText.slice(0, 50)}"`)
        this.enqueueMessage(incoming)
    }

    private buildTemplateCardEventText(body: WeComMessageBody): string | undefined {
        const ev = body.event?.template_card_event
        if (!ev) return undefined

        const selectedItems = ev.selected_items?.selected_item ?? []
        const selectedLines = selectedItems.map((item) => {
            const questionKey = item.question_key?.trim() || 'unknown_question'
            const optionIds = item.option_ids?.option_id?.filter(Boolean) ?? []
            return `- ${questionKey}: ${optionIds.length > 0 ? optionIds.join(', ') : '(未选择)'}`
        })

        const senderUserId = body.from?.userid || ''
        const chatId = body.chatid || senderUserId

        return [
            '[企业微信模板卡片回调]',
            `event_type(事件类型): template_card_event`,
            body.msgid ? `msgid(消息 id): ${body.msgid}` : undefined,
            body.aibotid ? `aibotid(机器人 id): ${body.aibotid}` : undefined,
            body.chattype ? `chat_type(会话类型): ${body.chattype}` : undefined,
            chatId ? `chat_id(会话 id): ${chatId}` : undefined,
            senderUserId ? `from.userid(发送人 id): ${senderUserId}` : undefined,
            ev.card_type ? `card_type(卡片类型): ${ev.card_type}` : undefined,
            ev.event_key ? `event_key(事件 key): ${ev.event_key}` : undefined,
            ev.task_id ? `task_id(任务 id): ${ev.task_id}` : undefined,
            selectedLines.length > 0 ? 'selected_items(选择项):' : 'selected_items(选择项): []',
            ...selectedLines,
        ]
            .filter((line): line is string => Boolean(line))
            .join('\n')
    }

    // ---------------------------------------------------------------------------
    // Internal: Message parsing
    // ---------------------------------------------------------------------------

    private async parseMessage(body: WeComMessageBody): Promise<{
        text: string
        images: Array<{ data: string; mimeType: string; name?: string }> | undefined
        files: Array<{ data: Buffer; name: string; mimeType: string; size: number }> | undefined
        quoteContent: string | undefined
    }> {
        const textParts: string[] = []
        const images: Array<{ data: string; mimeType: string; name?: string }> = []
        const files: Array<{ data: Buffer; name: string; mimeType: string; size: number }> = []
        let quoteContent: string | undefined

        if (body.text?.content) {
            let content = body.text.content
            // Strip leading @bot mention in group chats (best-effort until bot name config is added)
            if (body.chattype === 'group') {
                content = content.replace(/^\s*@\S+\s*/, '').trim()
            }
            if (content) textParts.push(content)
        }

        if (body.msgtype === 'mixed' && body.mixed?.msg_item) {
            // Process items in parallel — independent downloads, no shared state.
            await Promise.all(body.mixed.msg_item.map(async (item) => {
                if (item.msgtype === 'text' && item.text?.content) {
                    let content = item.text.content
                    if (body.chattype === 'group') {
                        content = content.replace(/^\s*@\S+\s*/, '').trim()
                    }
                    if (content) textParts.push(content)
                } else if (item.msgtype === 'image' && item.image?.url) {
                    await this.fetchAndPushImage(item.image.url, item.image.aeskey, 'image.jpg', images)
                }
            }))
        } else {
            if (body.msgtype === 'voice' && body.voice?.content) {
                textParts.push(body.voice.content)
            }

            if (body.image?.url) {
                await this.fetchAndPushImage(body.image.url, body.image.aeskey, 'image.jpg', images)
            } else if (body.image?.base64) {
                images.push({ data: body.image.base64, mimeType: 'image/jpeg', name: 'image.jpg' })
            }

            if (body.msgtype === 'file' && body.file?.url) {
                await this.fetchAndPushFile(body.file.url, body.file.aeskey, files)
            }

            if (body.msgtype === 'video' && body.video?.url) {
                await this.fetchAndPushFile(body.video.url, body.video.aeskey, files)
            }
        }

        if (body.quote) {
            if (body.quote.msgtype === 'text' && body.quote.text?.content) {
                quoteContent = body.quote.text.content
            } else if (body.quote.msgtype === 'voice' && body.quote.voice?.content) {
                quoteContent = body.quote.voice.content
            } else if (body.quote.msgtype === 'image' && body.quote.image?.url) {
                await this.fetchAndPushImage(body.quote.image.url, body.quote.image.aeskey, 'quote_image.jpg', images)
            }
        }

        return {
            text: textParts.join('\n').trim(),
            images: images.length > 0 ? images : undefined,
            files: files.length > 0 ? files : undefined,
            quoteContent,
        }
    }

    private extractFileNameFromUrl(url: string): string {
        // Try URL parsing first; fall back to splitting the path if the input
        // is not a valid absolute URL. The basename is only used when it looks
        // like a real filename (contains a dot), so we don't return bogus ids.
        let basename: string | undefined
        try {
            basename = new URL(url).pathname.split('/').pop()
        } catch {
            basename = url.split('/').pop()
        }
        if (basename && basename.includes('.')) {
            try {
                return decodeURIComponent(basename)
            } catch {
                return basename
            }
        }
        return 'file'
    }

    // ---------------------------------------------------------------------------
    // Internal: Media download
    // ---------------------------------------------------------------------------

    private async downloadMedia(url: string, aesKey: string | undefined, kind: 'image' | 'file'): Promise<Buffer | null> {
        if (!this.wsClient) return null
        try {
            const { buffer } = await this.wsClient.downloadFile(url, aesKey)
            return buffer
        } catch (err) {
            console.warn(`[WeCom SDK] ${kind} download failed:`, err instanceof Error ? err.message : err)
            return null
        }
    }

    private async fetchAndPushImage(
        url: string,
        aesKey: string | undefined,
        defaultName: string,
        images: Array<{ data: string; mimeType: string; name?: string }>,
    ): Promise<void> {
        const buf = await this.downloadMedia(url, aesKey, 'image')
        if (!buf) return
        const name = this.extractFileNameFromUrl(url) || defaultName
        images.push({ data: buf.toString('base64'), mimeType: 'image/jpeg', name })
    }

    private async fetchAndPushFile(
        url: string,
        aesKey: string | undefined,
        files: Array<{ data: Buffer; name: string; mimeType: string; size: number }>,
    ): Promise<void> {
        const buf = await this.downloadMedia(url, aesKey, 'file')
        if (!buf) return
        const name = this.extractFileNameFromUrl(url)
        const mimeType = this.guessMimeType(name) || (url.includes('video') ? 'video/mp4' : 'application/octet-stream')
        files.push({ data: buf, name, mimeType, size: buf.length })
    }

    // ---------------------------------------------------------------------------
    // Internal: Permission handling
    // ---------------------------------------------------------------------------

    private cancelPermissionTimer(chatId: string): void {
        const timer = this.permissionTimers.get(chatId)
        if (timer) {
            clearTimeout(timer)
            this.permissionTimers.delete(chatId)
        }
    }

    private handlePermCommand(text: string, chatId: string, senderId: string): boolean {
        if (!text.startsWith('/perm ')) return false
        const parts = text.trim().split(/\s+/)
        if (parts.length < 3) return false

        let requestIdPrefix = ''
        let decision: 'allow' | 'deny' | null = null
        if (parts[1] === 'allow' || parts[1] === 'deny') {
            decision = parts[1]
            requestIdPrefix = parts[2]
        } else if (parts[2] === 'allow' || parts[2] === 'deny') {
            requestIdPrefix = parts[1]
            decision = parts[2]
        }
        if (!decision || !requestIdPrefix) return false

        const pending = this.pendingPermByChat.get(chatId)
        if (pending && pending.senderId !== senderId) return true

        this.pendingPermByChat.delete(chatId)
        this.cancelPermissionTimer(chatId)
        this.permissionCallback?.(requestIdPrefix, decision)
        return true
    }

    private handleSimplePermResponse(text: string, chatId: string, senderId: string): boolean {
        const pending = this.pendingPermByChat.get(chatId)
        if (!pending) return false
        if (pending.senderId !== senderId) return false

        const normalized = text.trim().toLowerCase().replace(/[。.!！?？,，;；]$/g, '')
        const allowWords = ['allow', 'y', 'yes', 'ok', '允许', '好', '好的', '是', '可以', '同意']
        const denyWords = ['deny', 'n', 'no', '拒绝', '不', '不行', '否', '取消']

        let decision: 'allow' | 'deny' | null = null
        if (allowWords.includes(normalized)) decision = 'allow'
        if (denyWords.includes(normalized)) decision = 'deny'
        if (!decision) return false

        this.pendingPermByChat.delete(chatId)
        this.cancelPermissionTimer(chatId)
        this.permissionCallback?.(pending.requestIdPrefix, decision)
        return true
    }

    // ---------------------------------------------------------------------------
    // Internal: Utils
    // ---------------------------------------------------------------------------

    private isDuplicate(messageId: string): boolean {
        if (this.seenMessageIds.has(messageId)) return true
        this.seenMessageIds.add(messageId)
        if (!this.seenMessageIdTimer) {
            this.seenMessageIdTimer = setTimeout(() => {
                this.seenMessageIds.clear()
                this.seenMessageIdTimer = null
            }, 300_000)
        }
        return false
    }

    private enqueueMessage(msg: IncomingMessage): void {
        if (this.messageWaiter) {
            const waiter = this.messageWaiter
            this.messageWaiter = null
            waiter(msg)
        } else {
            this.messageQueue.push(msg)
        }
    }

    private createLogger(): Logger {
        return {
            debug: (message: string) => { /* suppress debug */ },
            info: (message: string) => { console.log(`[WeCom SDK] ${message}`) },
            warn: (message: string) => { console.warn(`[WeCom SDK] ${message}`) },
            error: (message: string) => { console.error(`[WeCom SDK] ${message}`) },
        }
    }
}

registerAdapter('wecom', WecomAdapter)
