/**
 * Conversation Engine (Layer 4).
 *
 * Routes messages to the active Agent engine via EngineRegistry. Phase 0 obtains
 * the adapter with EngineRegistry.get(session.backend).createQuery(); the stream
 * is still consumed here in SDK-specific form (Phase 0.5 will unify output via
 * process(request, callbacks)). Forwards events to the delivery layer via
 * callbacks (onTyping → onDraft → onFinal).
 *
 * Handles:
 *   - User message persistence
 *   - SDK streaming consumption
 *   - Permission callbacks during streaming
 *   - Assistant message persistence
 *   - Session title/timestamp update
 */

import { sessionRepo } from '@/lib/database/repositories/session.repo'
import { messageRepo } from '@/lib/database/repositories/message.repo'
import crypto from 'crypto'
import { EngineRegistry } from '@/lib/engine'
import type { PlatformContext } from '@/lib/engine'
import { createImAggregator } from './agent-callback-aggregator'
import type { ForgeAttachment } from '@/lib/engine/adapters/claude/client'
import { getUploadsDir } from '@/lib/forge-data'
import fs from 'fs'
import path from 'path'
import { archiveOldMemories } from '@/lib/workspace-fs'
import { maybeFlushMemory } from '@/lib/memory-flush'
import { getSetting } from '@/lib/settings-store'
import { resolveProvider } from '@/lib/provider'
import type { ConversationCallbacks, ConversationResult, ImPermissionRequest, IncomingMessage } from '../types'

let lastArchiveTime = 0

export class ConversationEngine {
  /**
   * Process an incoming IM message through the Claude Agent SDK.
   *
   * The callbacks enable streaming preview:
   *   onTyping  — called once when the first SDK event arrives
   *   onDraft   — called periodically with accumulated partial text
   *   onFinal   — called once when the full response is ready
   *   onPermissionRequest — called when the SDK needs tool permission
   */
  async processMessage(
    msg: IncomingMessage,
    sessionId: string,
    workspace: string,
    callbacks: ConversationCallbacks,
    userId?: string,
    platformContext?: PlatformContext,
  ): Promise<ConversationResult> {

    // Get IM permission mode from global settings
    const permissionMode = getSetting('im_permission_mode', userId) || 'confirm'

    // Save images/files to uploads dir ONCE, then use for both DB content and SDK attachments
    const uploadsDir = getUploadsDir()
    const savedImages: Array<{ filename: string; filePath: string; name: string; mimeType: string }> = []
    // All non-image files are treated uniformly — no tier classification.
    // They are written to disk and the Agent is prompted to use its Read
    // tool, which gives the Agent full autonomy to handle any format.
    const savedFiles: Array<{ filename: string; filePath: string; name: string; mimeType: string; size: number }> = []
    const uploadNamePrefix = this.buildImUploadNamePrefix(msg)

    if (msg.images && msg.images.length > 0) {
      for (const img of msg.images) {
        const ext = img.mimeType === 'image/png' ? '.png' : img.mimeType === 'image/gif' ? '.gif' : img.mimeType === 'image/webp' ? '.webp' : '.jpg'
        const baseName = this.sanitizeUploadName(path.basename(img.name || 'image', path.extname(img.name || '')), 40)
        const filename = `${uploadNamePrefix}_${Math.random().toString(36).slice(2, 6)}_${baseName}${ext}`
        const filePath = path.join(uploadsDir, filename)
        fs.writeFileSync(filePath, Buffer.from(img.data, 'base64'))
        savedImages.push({ filename, filePath, name: img.name || 'image', mimeType: img.mimeType })
      }
    }
    if (msg.files && msg.files.length > 0) {
      for (const file of msg.files) {
        const filename = `${uploadNamePrefix}_${Math.random().toString(36).slice(2, 6)}_${this.sanitizeUploadName(file.name, 80)}`
        const filePath = path.join(uploadsDir, filename)
        fs.writeFileSync(filePath, file.data)
        savedFiles.push({ filename, filePath, name: file.name, mimeType: file.mimeType, size: file.size })
      }
    }

    // Save user message with attachment blocks so desktop ChatView can render them
    const userMsgId = crypto.randomUUID()
    const hasAttachments = savedImages.length > 0 || savedFiles.length > 0
    let userContent: string
    if (hasAttachments) {
      const blocks: Array<Record<string, unknown>> = []
      if (msg.text) blocks.push({ type: 'text', text: msg.text })
      for (const img of savedImages) {
        blocks.push({ type: 'image_attachment', url: `/api/upload/${img.filename}`, name: img.name })
      }
      for (const file of savedFiles) {
        blocks.push({ type: 'file_attachment', url: `/api/upload/${file.filename}`, name: file.name, size: file.size, mimeType: file.mimeType })
      }
      userContent = JSON.stringify(blocks)
    } else {
      userContent = msg.text
    }
    messageRepo.create({ id: userMsgId, sessionId, role: 'user', content: userContent })

    // Throttle archiving: max once per hour
    const now = Date.now()
    if (now - lastArchiveTime > 3600_000) {
      lastArchiveTime = now
      const retentionDays = parseInt(getSetting('memory_retention_days', userId) || '7', 10)
      try {
        archiveOldMemories(workspace, retentionDays, userId)
      } catch { /* ignore archive errors */ }
    }

    // Get session model/backend and check if this is a continuation
    const session = sessionRepo.findById(sessionId)
    const model = session?.model || 'claude-sonnet-4-6'
    const engine = EngineRegistry.get(session?.backend)
    const resolved = resolveProvider(model, userId)
    const providerType = resolved.provider

    // 从 platformContext 解构渠道凭证（取代原先尾部参数，消除参数泄漏）。
    // 变量名与下方两处 createQuery 调用保持一致，调用点无需改动。
    const feishuAuth = platformContext?.feishu
    const isFeishuChannel = platformContext?.channelFlags?.feishu
    const isWeComChannel = platformContext?.channelFlags?.wecom
    const wecomCliConfigDir = platformContext?.wecom?.cliConfigDir
    const isDingtalkChannel = platformContext?.channelFlags?.dingtalk
    const dingtalkAuth = platformContext?.dingtalk

    // Check if the session already has messages — if so, resume the conversation
    const existingMsgCount = messageRepo.countBySession(sessionId)
    const isResume = existingMsgCount > 1
    // Create abort controller for the SDK query
    // Combines the external abort signal (from /stop or timeout) with a local controller
    const localAbort = new AbortController()
    if (callbacks.abortSignal) {
      callbacks.abortSignal.addEventListener('abort', () => localAbort.abort(), { once: true })
    }

    // Build ForgeAttachment[] from already-saved files (no duplicate writes)
    // Images use tier 'image' (base64 vision block in SDK).
    // All non-image files use tier 'file' — the Agent decides how to handle
    // them (Read tool, Bash, etc.) rather than us pre-classifying into
    // pdf/text/binary with limited delivery.
    const attachments: ForgeAttachment[] = [
      ...savedImages.map(img => ({ name: img.name, serverPath: img.filePath, mimeType: img.mimeType, tier: 'image' as const })),
      ...savedFiles.map(f => ({ name: f.name, serverPath: f.filePath, mimeType: f.mimeType, tier: 'file' as const })),
    ]

    // Build prompt: for non-image attachments, tell the Agent the disk path
    // so it can use its Read tool — dual-path insurance with images (base64
    // vision + disk path fallback) and full Agent autonomy for other files.
    const filePathRefs = savedFiles
      .map(f => `[User attached file: ${f.filePath} (${f.name})]`)
      .join('\n')
    const imagePathRefs = savedImages
      .map(img => `[User attached image saved at: ${img.filePath} (${img.name})]`)
      .join('\n')
    const attachmentHints = [
      imagePathRefs && `${imagePathRefs}\n(The image is also sent via vision above; use Read tool on this path if vision fails)`,
      filePathRefs && `${filePathRefs}\nPlease read the attached file(s) using your Read tool, then respond.`,
    ].filter(Boolean).join('\n\n')

    const promptWithHints = attachmentHints
      ? `${attachmentHints}\n\n${msg.text || (attachments.length > 0 ? attachments.map(a => `[${a.name}]`).join(' ') : '')}`
      : (msg.text || (attachments.length > 0 ? attachments.map(a => `[${a.name}]`).join(' ') : ''))

    // Create SDK query
    // Skip MCP servers for IM to reduce subprocess startup overhead (~2-3s savings)
    const t0 = Date.now()
    let sdkSessionId = sessionId
    console.log(`[ConversationEngine] Creating SDK query: model=${model}, isResume=${isResume}, permissionMode=${permissionMode}, msgCount=${existingMsgCount}, attachments=${attachments.length}`)

    let result: ConversationResult
    // B-D: 引擎流通过统一 process + IM 聚合适配器消费。canUseTool 不传——process 内部从
    // aggregator.callbacks.onPermissionRequest 合成（消除原 canUseTool 包装）。
    const aggregator = createImAggregator({ convCallbacks: callbacks, msg })
    const baseRequest = {
      prompt: promptWithHints,
      attachments: attachments.length > 0 ? attachments : undefined,
      sessionId,
      model,
      workspaceId: workspace,
      userId,
      bypassPermissions: permissionMode === 'full',
      resumeSession: isResume,
      abortController: localAbort,
      skipMcpServers: true,
      useImPrompt: true,
      thinkingMode: getSetting('thinking_mode', userId) || 'auto',
      providerType,
      feishuAuth,
      isFeishuChannel,
      isWeComChannel,
      wecomCliConfigDir,
      isDingtalkChannel,
      dingtalkAuth,
    }
    try {
      await engine.process(baseRequest, aggregator.callbacks)
      result = await aggregator.finalize()
    } catch (err) {
      // Fallback: if "Session ID already in use", retry with a fresh SDK session
      const errMsg = err instanceof Error ? err.message : String(err)
      if (errMsg.includes('already in use')) {
        console.log(`[ConversationEngine] Session ID conflict, retrying with fresh SDK session`)
        const localAbort2 = new AbortController()
        if (callbacks.abortSignal) {
          callbacks.abortSignal.addEventListener('abort', () => localAbort2.abort(), { once: true })
        }
        await engine.process(
          { ...baseRequest, sessionId: crypto.randomUUID(), resumeSession: false, abortController: localAbort2 },
          aggregator.callbacks,
        )
        result = await aggregator.finalize()
      } else {
        throw err
      }
    }
    console.log(`[ConversationEngine] Stream consumed (+${Date.now() - t0}ms), text=${result.text.length} chars, tools=${result.toolsUsed.join(',') || 'none'}`)

    // Save assistant message
    const assistantMsgId = crypto.randomUUID()
    messageRepo.create({ id: assistantMsgId, sessionId, role: 'assistant', content: JSON.stringify(result.blocks) })

    // Update session title + timestamp
    const msgCount = messageRepo.countBySession(sessionId)
    if (msgCount <= 2) {
      const title = `[${msg.channelType}] ${msg.text.length > 40 ? msg.text.slice(0, 37) + '...' : msg.text}`
      sessionRepo.updateTitle(sessionId, title)
    } else {
      sessionRepo.touch(sessionId)
    }

    // Background memory flush — remind Agent to write memory if needed (every N messages)
    // Fire-and-forget: user doesn't wait for this
    maybeFlushMemory(sessionId, workspace, model, msg.text, result.text.slice(0, 200), userId)

    return result
  }

  private buildImUploadNamePrefix(msg: IncomingMessage): string {
    const chatType = msg.isDm ? 'dm' : 'group'
    const chatHash = crypto.createHash('sha256').update(msg.chatId).digest('hex').slice(0, 8)
    const stamp = new Date()
      .toISOString()
      .replaceAll('-', '')
      .replaceAll(':', '')
      .replaceAll('.', '')
      .slice(0, 15) + 'Z'
    return `im_${msg.channelType}_in_${chatType}_${chatHash}_${stamp}`
  }

  private sanitizeUploadName(name: string, maxLength: number): string {
    const sanitized = name.replace(/[^a-zA-Z0-9._-]/g, '_').replace(/^_+$/, '')
    return (sanitized || 'file').slice(0, maxLength)
  }

  // ---------------------------------------------------------------------------
  // Stream consumption & block collection 迁移至 agent-callback-aggregator.ts（B-D）：
  // processMessage 现在通过 engine.process + createImAggregator 消费引擎流。
  // ---------------------------------------------------------------------------

}

// ---------------------------------------------------------------------------
// Attachment detection — 迁至 ./shared/attachment-parsing（消除循环依赖）。
// re-export 保持原有外部引用（src/app/api/chat/route.ts）零变化。
// ---------------------------------------------------------------------------
export { parseMediaProtocol, extractFilePaths, resolveFileAttachments } from '../shared/attachment-parsing'
