/**
 * WeChat (Weixin iLink) adapter — HTTP long-polling.
 *
 * One adapter instance per WeChat account (one im_channels row).
 * Uses native peer_user_id as chatId (no synthetic format needed —
 * channel_bindings.channel_id already disambiguates accounts).
 */

import { ChannelAdapter } from '../base'
import { registerAdapter } from '../registry'
import { weixinContextTokenRepo } from '@/lib/database/repositories/weixin-context-token.repo'
import { weixinSyncCursorRepo } from '@/lib/database/repositories/weixin-sync-cursor.repo'
import { getUpdates, sendTextMessage, sendImageMessage, sendFileMessage, sendTyping, getConfig } from './weixin-api'
import type { WeixinCredentials, WeixinMessage, MessageItem, GetUpdatesResponse } from './weixin-types'
import { MessageItemType, TypingStatus, ERRCODE_SESSION_EXPIRED, DEFAULT_BASE_URL, DEFAULT_CDN_BASE_URL } from './weixin-types'
import { downloadMediaFromItem, uploadMediaToCdn } from './weixin-media'
import { isPaused, setPaused, clearAllPauses, clearPause } from './weixin-session-guard'
import { transcribeAudio } from '@/lib/im/shared/stt'
import type { ChannelType, IncomingMessage, OutboundMessage, ImPermissionRequest } from '../../types'
const DEDUP_MAX = 500
const BACKOFF_BASE_MS = 2_000
const BACKOFF_MAX_MS = 30_000

export class WeixinAdapter extends ChannelAdapter {
    readonly channelType: ChannelType = 'weixin'
    readonly supportsMessageEditing = false

    private token = ''
    private appid = ''
    private channelId = ''
    private running = false

    // Internal message queue: pollLoop fills, consumeOne drains
    private messageQueue: IncomingMessage[] = []
    private messageWaiter: ((msg: IncomingMessage | null) => void) | null = null

    // Permission response callback (set by Bridge Manager)
    private permissionCallback: ((requestId: string, decision: 'allow' | 'deny') => void) | null = null

    // Polling internals
    private pollAbortController: AbortController | null = null
    private getUpdatesBuf = ''
    private seenMessageIds = new Set<string>()
    private consecutiveFailures = 0
    private typingTickets = new Map<string, string>() // peerUserId → typingTicket

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

    async start(config: Record<string, string>): Promise<void> {
        const token = config.token
        if (!token) throw new Error('WeChat token is required. Please login via QR code first.')

        this.token = token
        this.appid = config.appid || ''
        this.channelId = config.channel_id || ''
        this.running = true
        this.pollAbortController = new AbortController()
        this.seenMessageIds.clear()
        this.consecutiveFailures = 0
        this.typingTickets.clear()
        clearPause(this.channelId)

        // Restore persisted long-poll cursor so we don't miss/duplicate
        // messages across process restarts.
        this.getUpdatesBuf = this.loadSyncCursor()
        if (this.getUpdatesBuf) {
            console.log(
                `[Weixin] Restored sync cursor for channel ${this.channelId} (len=${this.getUpdatesBuf.length})`,
            )
        }

        // Start background polling loop
        this.pollLoop()
    }

    async stop(): Promise<void> {
        this.running = false
        this.pollAbortController?.abort()
        this.pollAbortController = null

        // Drain waiters
        if (this.messageWaiter) {
            this.messageWaiter(null)
            this.messageWaiter = null
        }
        this.messageQueue = []
        this.seenMessageIds.clear()
        this.typingTickets.clear()
    }

    isRunning(): boolean {
        return this.running
    }

    // ---------------------------------------------------------------------------
    // 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

            this.messageWaiter = (msg) => {
                if (resolved) return
                resolved = true
                signal.removeEventListener('abort', onAbort)
                this.messageWaiter = null
                resolve(msg)
            }

            const onAbort = () => {
                if (resolved) return
                resolved = true
                this.messageWaiter = null
                resolve(null)
            }
            signal.addEventListener('abort', onAbort, { once: true })
        })
    }

    // ---------------------------------------------------------------------------
    // Outbound messaging
    // ---------------------------------------------------------------------------

    async send(msg: OutboundMessage): Promise<string | undefined> {
        const creds = this.buildCreds()
        const peerUserId = msg.chatId
        const contextToken = this.getContextToken(peerUserId)

        if (!contextToken) {
            console.warn(`[Weixin] No context_token for peer ${peerUserId}, message may fail`)
        }

        // WeChat text messages are plain text — markdown syntax passes through
        // as-is, which is more readable than a broken filter stripping visual
        // markers (the previous StreamingMarkdownFilter had bugs that left most
        // syntax unfiltered while removing useful cues like code fences and
        // blockquote markers).
        const { clientId } = await sendTextMessage(creds, peerUserId, msg.text, contextToken || '')
        return clientId
    }

    async sendImage(chatId: string, imageBuffer: Buffer, caption?: string): Promise<void> {
        const creds = this.buildCreds()
        const peerUserId = chatId
        const contextToken = this.getContextToken(peerUserId) || ''

        // Upload encrypted image to CDN
        const { encryptQueryParam, aesKeyBase64 } = await uploadMediaToCdn(
            creds, imageBuffer, 'image.png', 1, // IMAGE = 1
        )

        // Send image message
        await sendImageMessage(creds, peerUserId, encryptQueryParam, aesKeyBase64, contextToken)

        // Send caption as separate text message if present
        if (caption) {
            await sendTextMessage(creds, peerUserId, caption, contextToken)
        }
    }

    async sendFile(chatId: string, fileBuffer: Buffer, filename: string, caption?: string): Promise<void> {
        const creds = this.buildCreds()
        const peerUserId = chatId
        const contextToken = this.getContextToken(peerUserId) || ''

        // Upload encrypted file to CDN
        const { encryptQueryParam, aesKeyBase64 } = await uploadMediaToCdn(
            creds, fileBuffer, filename, 3, // FILE = 3
        )

        // Send file message
        await sendFileMessage(creds, peerUserId, encryptQueryParam, aesKeyBase64, filename, fileBuffer.length, contextToken)

        // Send caption as separate text message if present
        if (caption) {
            await sendTextMessage(creds, peerUserId, caption, contextToken)
        }
    }

    async sendTypingIndicator(chatId: string): Promise<void> {
        const creds = this.buildCreds()
        const peerUserId = chatId
        const contextToken = this.getContextToken(peerUserId)
        if (!contextToken) return

        // Get or fetch typing ticket
        let ticket = this.typingTickets.get(peerUserId)
        if (!ticket) {
            try {
                const config = await getConfig(creds, peerUserId, contextToken)
                if (config.typing_ticket) {
                    ticket = config.typing_ticket
                    this.typingTickets.set(peerUserId, ticket)
                }
            } catch {
                return // Best effort
            }
        }
        if (!ticket) return

        await sendTyping(creds, peerUserId, ticket, TypingStatus.TYPING)
    }

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

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

        const text = `⚠️ Permission Required\n\nTool: \`${req.toolName}\`\n${inputSummary}\n\nReply /allow or /deny`

        const creds = this.buildCreds()
        const contextToken = this.getContextToken(req.chatId) || ''
        await sendTextMessage(creds, req.chatId, text, contextToken)
    }

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

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

    validateConfig(config: Record<string, string>): { valid: boolean; error?: string } {
        if (!config.token) return { valid: false, error: 'WeChat token is required. Please login via QR code first.' }
        return { valid: true }
    }

    // ---------------------------------------------------------------------------
    // Internal: Polling loop
    // ---------------------------------------------------------------------------

    private async pollLoop(): Promise<void> {
        const creds = this.buildCreds()
        let consecutiveErrors = 0

        while (this.running) {
            // Check pause state
            if (isPaused(this.channelId)) {
                await sleep(10_000)
                continue
            }

            try {
                const resp: GetUpdatesResponse = await getUpdates(
                    creds,
                    this.getUpdatesBuf,
                    35_000,
                )

                // Check for session expiry
                if (resp.errcode === ERRCODE_SESSION_EXPIRED) {
                    setPaused(this.channelId, 'Session expired (errcode -14)')
                    console.warn(`[Weixin] Session expired for ${this.channelId}, pausing`)
                    consecutiveErrors = 0
                    continue
                }

                // Check for other API errors
                if (resp.errcode && resp.errcode !== 0) {
                    throw new Error(`API error: ${resp.errcode} ${resp.errmsg || ''}`)
                }

                // Process messages (sequentially awaited so media downloads
                // complete before the message is enqueued for the conversation
                // engine — otherwise the engine sees an empty prompt with no
                // attachments and the upstream model rejects with
                // "未正常接收到prompt参数").
                if (resp.msgs && resp.msgs.length > 0) {
                    for (const msg of resp.msgs) {
                        try {
                            await this.processMessage(msg)
                        } catch (err) {
                            console.error(
                                '[Weixin] processMessage error:',
                                err instanceof Error ? err.message : err,
                            )
                        }
                    }
                }

                // Update cursor (in-memory + persisted to survive restarts)
                if (resp.get_updates_buf && resp.get_updates_buf !== this.getUpdatesBuf) {
                    this.getUpdatesBuf = resp.get_updates_buf
                    this.saveSyncCursor(this.getUpdatesBuf)
                }

                consecutiveErrors = 0 // Reset on success
            } catch (err) {
                if (err instanceof Error && err.name === 'AbortError') break

                consecutiveErrors++
                const backoff = Math.min(
                    BACKOFF_BASE_MS * Math.pow(2, consecutiveErrors - 1),
                    BACKOFF_MAX_MS,
                )

                console.error(
                    `[Weixin] Poll error for ${this.channelId} (failure ${consecutiveErrors}):`,
                    err instanceof Error ? err.message : err,
                )

                // Too many consecutive errors — mark as disconnected for Bridge Manager reconnection
                if (consecutiveErrors >= 5) {
                    console.error(`[Weixin] Too many consecutive poll errors, stopping adapter`)
                    this.running = false
                    if (this.messageWaiter) {
                        this.messageWaiter(null)
                        this.messageWaiter = null
                    }
                    break
                }

                await sleep(backoff)
            }
        }
    }

    private async processMessage(msg: WeixinMessage): Promise<void> {
        if (!msg.from_user_id) return

        // Log incoming item types so we can diagnose voice/file/image flow.
        const itemTypeNames: Record<number, string> = {
            1: 'TEXT', 2: 'IMAGE', 3: 'VOICE', 4: 'FILE', 5: 'VIDEO',
        }
        const types = (msg.item_list || []).map(it => itemTypeNames[it.type] || `T${it.type}`)
        console.log(
            `[Weixin] processMessage from=${msg.from_user_id} item_types=[${types.join(',')}] msg_id=${msg.message_id || msg.seq}`,
        )

        // Dedup by message_id or seq
        const msgKey = msg.message_id || `seq_${msg.seq}`
        if (this.seenMessageIds.has(msgKey)) return
        this.seenMessageIds.add(msgKey)

        // Trim dedup set
        if (this.seenMessageIds.size > DEDUP_MAX) {
            const arr = Array.from(this.seenMessageIds)
            for (let i = 0; i < arr.length - DEDUP_MAX; i++) {
                this.seenMessageIds.delete(arr[i])
            }
        }

        // Persist context_token
        if (msg.context_token) {
            this.setContextToken(msg.from_user_id, msg.context_token)
        }

        // Extract text from item_list
        let text = ''
        const items = msg.item_list || []
        for (const item of items) {
            if (item.type === MessageItemType.TEXT && item.text_item?.text) {
                text += item.text_item.text
            }
        }

        // Handle quoted/referenced messages
        if (msg.ref_message) {
            const refParts: string[] = []
            if (msg.ref_message.title) refParts.push(msg.ref_message.title)
            if (msg.ref_message.content) refParts.push(msg.ref_message.content)
            if (refParts.length > 0) {
                text = `[引用: ${refParts.join(' | ')}]\n${text}`
            }
        }

        // Use native peer_user_id as chatId
        const chatId = msg.from_user_id

        // Download media attachments. We await all downloads BEFORE enqueueing
        // the message — fire-and-forget would race the conversation engine and
        // produce an empty prompt with no attachments (upstream model then
        // rejects with "未正常接收到prompt参数").
        const images: Array<{ data: string; mimeType: string; name?: string }> = []
        const files: Array<{ data: Buffer; name: string; mimeType: string; size: number }> = []
        const mediaItems = items.filter(it => it.type !== MessageItemType.TEXT)

        if (mediaItems.length > 0) {
            const downloads = await Promise.all(
                mediaItems.map(async (item) => {
                    try {
                        const media = await this.downloadMediaItem(item)
                        return { item, media }
                    } catch (err) {
                        console.error(
                            '[Weixin] Media download failed:',
                            err instanceof Error ? err.message : err,
                        )
                        return { item, media: null }
                    }
                }),
            )

            for (const { item, media } of downloads) {
                if (!media) continue
                if (item.type === MessageItemType.IMAGE) {
                    images.push({
                        data: media.data.toString('base64'),
                        mimeType: media.mimeType,
                        name: media.filename,
                    })
                } else {
                    files.push({
                        data: media.data,
                        name: media.filename,
                        mimeType: media.mimeType,
                        size: media.data.length,
                    })
                }
            }
        }

        // STT: transcribe voice files before passing to the main model.
        // Most chat models don't support raw audio; convert to text first.
        const voiceFiles = files.filter(f => f.mimeType === 'audio/wav' || f.mimeType === 'audio/silk')
        if (voiceFiles.length > 0) {
            const transcripts: string[] = []
            for (const vf of voiceFiles) {
                const transcript = await transcribeAudio(vf.data as Buffer)
                if (transcript) {
                    transcripts.push(transcript)
                    // Remove this WAV from files — the main model gets text instead
                    const idx = files.indexOf(vf)
                    if (idx !== -1) files.splice(idx, 1)
                }
            }
            if (transcripts.length > 0) {
                const base = text.trim()
                const transcriptBlock = transcripts.join('\n')
                text = base ? `${base}\n[语音转写]: ${transcriptBlock}` : transcriptBlock
                console.log(`[Weixin] STT transcribed voice: "${transcriptBlock.slice(0, 80)}"`)
            }
        }

        // Fallback prompt for media-only messages so the SDK never receives an
        // empty prompt. Voice messages always have empty text; image/file
        // messages may also arrive without a caption.
        let promptText = text.trim()
        if (!promptText && (images.length > 0 || files.length > 0)) {
            const hasVoice = mediaItems.some(it => it.type === MessageItemType.VOICE)
            if (hasVoice) {
                promptText = '请基于这条语音消息回复用户。'
            } else if (images.length > 0) {
                promptText = '请描述这张图片，或根据图片内容回答用户。'
            } else {
                promptText = '请处理用户发送的文件。'
            }
        }

        // Check for permission responses (/allow, /deny)
        const trimmedText = text.trim().toLowerCase()
        if (trimmedText === '/allow' || trimmedText === '/deny') {
            // Permission responses are handled by the permission broker via
            // onPermissionResponse callback. We don't enqueue them as normal messages.
            // The permission broker tracks pending requests by chatId.
            // However, we need to forward this to the permission callback.
            // Since we don't have the requestId here, we handle this differently:
            // The permission broker in Bridge Manager will need to match by chatId.
            // For now, we just enqueue it as a normal message and let the command
            // handler or permission broker deal with it.
        }

        // Skip messages with no text AND no media (e.g. unknown event types
        // we don't know how to surface).
        if (!promptText && images.length === 0 && files.length === 0) {
            console.log(
                `[Weixin] Skipping message with no usable content (msg_id=${msg.message_id})`,
            )
            return
        }

        const incoming: IncomingMessage = {
            channelType: 'weixin',
            channelId: this.channelId || 'weixin',
            chatId,
            senderId: msg.from_user_id,
            senderName: msg.from_user_id.slice(0, 8),
            text: promptText,
            isDm: true, // WeChat bot API is DM-only
            isGroupMention: false,
            images: images.length > 0 ? images : undefined,
            files: files.length > 0 ? files : undefined,
            rawEvent: msg,
        }

        this.enqueueMessage(incoming)
    }

    private async downloadMediaItem(
        item: MessageItem,
    ): Promise<{ data: Buffer; mimeType: string; filename: string } | null> {
        const creds = this.buildCreds()
        return downloadMediaFromItem(item, creds.cdnBaseUrl)
    }

    // ---------------------------------------------------------------------------
    // Internal: Message queue
    // ---------------------------------------------------------------------------

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

    // ---------------------------------------------------------------------------
    // Internal: Context token persistence
    // ---------------------------------------------------------------------------

    private getContextToken(peerUserId: string): string | null {
        try {
            // 完全等价于 `SELECT context_token FROM weixin_context_tokens
            // WHERE channel_id = ? AND peer_user_id = ?`——repo 返回的 contextToken
            // 由 drizzle 推断为 camelCase 字段。原 race-condition 容忍语义
            // (catch swallow) 保留。
            const row = weixinContextTokenRepo.findToken(this.channelId, peerUserId)
            return row?.contextToken || null
        } catch {
            return null
        }
    }

    private setContextToken(peerUserId: string, token: string): void {
        try {
            // 等价于原 `INSERT INTO weixin_context_tokens (...) ON CONFLICT
            // (channel_id, peer_user_id) DO UPDATE SET context_token =
            // excluded.context_token, updated_at = datetime('now')`——repo.upsert
            // 在 db.transaction 中按 (channel_id, peer_user_id) 判存在后分支
            // insert/update，与原 ON CONFLICT 行为完全一致。原 race-condition
            // 容忍语义 (catch swallow) 保留。
            weixinContextTokenRepo.upsert(this.channelId, peerUserId, token)
        } catch (err) {
            console.error('[Weixin] Failed to persist context_token:', err instanceof Error ? err.message : err)
        }
    }

    // ---------------------------------------------------------------------------
    // Internal: Long-poll cursor persistence (survives process restart)
    // ---------------------------------------------------------------------------

    private loadSyncCursor(): string {
        if (!this.channelId) return ''
        try {
            // 等价于 `SELECT get_updates_buf FROM weixin_sync_cursors
            // WHERE channel_id = ?`——repo 返回的 getUpdatesBuf 由 drizzle
            // 推断为 camelCase 字段。
            const row = weixinSyncCursorRepo.findByChannel(this.channelId)
            return row?.getUpdatesBuf || ''
        } catch (err) {
            console.error('[Weixin] Failed to load sync cursor:', err instanceof Error ? err.message : err)
            return ''
        }
    }

    private saveSyncCursor(buf: string): void {
        if (!this.channelId) return
        try {
            // 等价于原 `INSERT INTO weixin_sync_cursors (...) ON CONFLICT
            // (channel_id) DO UPDATE SET get_updates_buf = excluded.get_updates_buf,
            // updated_at = datetime('now')`——repo.upsert 在 db.transaction 中按
            // channel_id 判存在后分支 insert/update，与原 ON CONFLICT 行为
            // 完全一致。原 race-condition 容忍语义 (catch swallow) 保留。
            weixinSyncCursorRepo.upsert(this.channelId, buf)
        } catch (err) {
            console.error('[Weixin] Failed to persist sync cursor:', err instanceof Error ? err.message : err)
        }
    }

    // ---------------------------------------------------------------------------
    // Internal: Helpers
    // ---------------------------------------------------------------------------

    private buildCreds(): WeixinCredentials {
        return {
            botToken: this.token,
            ilinkBotId: this.appid || this.channelId,
            baseUrl: DEFAULT_BASE_URL,
            cdnBaseUrl: DEFAULT_CDN_BASE_URL,
        }
    }
}

// Self-register
registerAdapter('weixin', WeixinAdapter)

// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------

function sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms))
}
