/**
 * feishu-engine.js — 消息路由引擎
 *
 * 职责：
 *   - 接收来自 LarkClient 的消息事件和卡片交互事件
 *   - 按优先级路由：音频转写 → 斜杠命令 → 卡片回调 → 权限文本响应 → 普通对话
 *   - 维护 pendingApprovals（工具审批挂起状态）
 *   - 维护 processingLocks（防止单用户并发）
 */

import { feishuDb } from './feishu-db.js';
import { transcribeAudio } from './speech.js';
import { CommandHandler } from './command-handler.js';
import { runQuery } from './sdk-bridge.js';
import { resolveToolApproval } from '../../claude-sdk.js';
import { feishuBindingGuide } from '../../../shared/brand.js';
import {
  buildToolApprovalResultCardDirect,
  buildModeSelectCardDirect,
  buildModelSelectCardDirect,
  buildStatusCardDirect,
  buildListCardDirect,
  buildDeleteResultCard,
  buildAskUserQuestionResultCard,
  buildCreateProjectResultCard,
  stripProjectPrefix,
} from './card-builder.js';
import { getProjects, addProjectManually, deleteProject, deleteSession } from '../../projects.js';
import { getUserPaths } from '../../services/user-directories.js';
import { promises as fs } from 'fs';
import path from 'path';
import { spawn } from 'child_process';

/**
 * 飞书机器人菜单 event_key → 斜杠命令映射
 *
 * 通过环境变量 FEISHU_MENU_KEY_MAP（JSON 字符串）配置，示例：
 *   FEISHU_MENU_KEY_MAP='{"key_help":"/help","key_new":"/new","key_project":"/project","key_mode":"/mode","key_list":"/list","key_stop":"/stop"}'
 */
const MENU_KEY_MAP = (() => {
  try {
    console.log('[Feishu:engine] Loaded MENU_KEY_MAP:', process.env.FEISHU_MENU_KEY_MAP);
    return JSON.parse(process.env.FEISHU_MENU_KEY_MAP || '{}');
  } catch {
    return {};
  }
})();

// 文本权限响应关键词（不区分大小写）
const ALLOW_KEYWORDS    = new Set(['允许', '同意', '可以', '好', 'y', 'yes', 'allow', 'ok']);
const DENY_KEYWORDS     = new Set(['拒绝', '不允许', '否', '不', 'n', 'no', 'deny', 'reject']);
const ALLOW_ALL_KEYWORDS = new Set(['允许所有', '全部允许', 'allow all', 'allow_all', 'allowall']);

/**
 * 将卡片 JSON 字符串转为飞书 WS 回调同步响应格式
 */
function toCardResponse(cardStr) {
  return { card: { type: 'raw', data: JSON.parse(cardStr) } };
}

export class FeishuEngine {
  /**
   * @param {Object} larkClient  LarkClient 实例（由 index.js 注入）
   */
  constructor(larkClient) {
    this.larkClient = larkClient;

    // Map<feishuOpenId, { requestId, cardMessageId, toolName }>
    this.pendingApprovals = new Map();

    // Map<feishuOpenId, true>  — 防并发处理锁
    this.processingLocks = new Map();

    // Map<feishuOpenId, Array<{chatId, messageId, content, images}>>  — 消息队列
    this.messageQueues = new Map();

    this.commandHandler = new CommandHandler(larkClient);
  }

  // ─── 主入口（普通消息） ───────────────────────────────────────────────────────

  /**
   * @param {Object} params
   * @param {string} params.feishuOpenId
   * @param {string} params.chatId
   * @param {string} params.messageId
   * @param {string} params.content
   * @param {Array|null}  params.images   [{data: Buffer, mimeType: string}]
   * @param {Object|null} params.audio    {data: Buffer, format: string, mimeType: string}
   * @param {Array|null}  params.files       [{data: Buffer, filename: string, fileKey: string}]
   * @param {Array|null}  params.quotedFiles  [{messageId: string, fileKey: string, filename: string}]
   */
  async handleMessage({ feishuOpenId, chatId, messageId, content, images, audio, files, quotedFiles }) {
    // 0. 消息去重（飞书 at-least-once 投递，同一 messageId 可能多次到达；持久化到 DB 防重启后重复）
    if (messageId && feishuDb.isProcessed(messageId)) {
      console.log(`[Feishu:engine] Duplicate messageId ${messageId}, skipping`);
      return;
    }
    if (messageId) {
      feishuDb.markProcessed(messageId);
    }

    // 每次收到消息时更新 chat_id，供菜单点击等无消息 ID 的场景使用
    if (chatId) {
      feishuDb.updateSessionState(feishuOpenId, { chat_id: chatId });
    }

    // 1. 检查绑定（/auth 命令本身不需要绑定）
    const rawText = content?.trim() || '';
    const isAuthCmd = rawText.toLowerCase().startsWith('/auth');

    if (!isAuthCmd) {
      const binding = feishuDb.getBinding(feishuOpenId);
      if (!binding) {
        await this.larkClient.replyText(messageId,
          feishuBindingGuide()
        );
        return;
      }
    }

    // 添加"处理中"表情回复
    let reactionId = null;
    if (messageId) {
      reactionId = await this.larkClient.addReaction(messageId, 'OneSecond').catch(err => {
        console.warn('[Feishu:engine] addReaction failed:', err.message);
        return null;
      });
    }

    try {
      // 2. 音频转文字
      if (audio) {
        await this.larkClient.replyText(messageId, '🎙️ 正在转写语音…');
        let transcribed;
        try {
          transcribed = await transcribeAudio(audio.data, audio.format);
        } catch (err) {
          console.error('[Feishu:engine] transcribeAudio error:', err.message);
          await this.larkClient.sendText(chatId, '❌ 语音转写失败，请检查 Whisper API 配置或重新发送。');
          return;
        }
        if (!transcribed) {
          await this.larkClient.sendText(chatId, '🔇 未识别到语音内容，请重新发送语音或改用文字输入。');
          return;
        }
        await this.larkClient.sendText(chatId, `📝 已转写：「${transcribed}」`);
        // 以转写文字继续处理
        content = transcribed;
      }

      const normalizedText = (content || '').trim();

      // 3. 斜杠命令
      if (!images && normalizedText.startsWith('/')) {
        await this.commandHandler.dispatch(feishuOpenId, chatId, messageId, normalizedText);
        return;
      }

      // 4. 文本权限响应（兼容不点卡片的用户）
      if (this.pendingApprovals.has(feishuOpenId) && !images) {
        const handled = await this._handleTextPermissionResponse(feishuOpenId, messageId, normalizedText);
        if (handled) return;
      }

      // 5. 普通对话 → 队列处理
      if (this.processingLocks.get(feishuOpenId)) {
        const queue = this.messageQueues.get(feishuOpenId) || [];
        const MAX_QUEUE = 5;
        if (queue.length >= MAX_QUEUE) {
          await this.larkClient.replyText(messageId, `⚠️ 队列已满（最多 ${MAX_QUEUE} 条），请等待处理完成后再发送。`);
          return;
        }
        queue.push({ chatId, messageId, content: normalizedText, images, files, quotedFiles });
        this.messageQueues.set(feishuOpenId, queue);
        await this.larkClient.replyText(messageId, `⏳ 已排队（第 ${queue.length} 条），稍后处理…`);
        return;
      }

      this.processingLocks.set(feishuOpenId, true);
      try {
        await this._runChat(feishuOpenId, chatId, messageId, normalizedText, images, files, quotedFiles);
      } finally {
        // 有排队消息则继续处理（保持锁），否则释放锁
        const queue = this.messageQueues.get(feishuOpenId);
        if (queue && queue.length > 0) {
          this._drainQueue(feishuOpenId).catch(err =>
            console.error('[Feishu:engine] drainQueue error:', err.message)
          );
        } else {
          this.processingLocks.delete(feishuOpenId);
        }
      }
    } finally {
      // 处理完成后删除"处理中"表情
      if (reactionId && messageId) {
        this.larkClient.deleteReaction(messageId, reactionId).catch(err => {
          console.warn('[Feishu:engine] deleteReaction failed:', err.message);
        });
      }
    }
  }

  // ─── 菜单点击回调入口 ─────────────────────────────────────────────────────────

  /**
   * 处理飞书机器人菜单点击事件（application.bot.menu_v6）
   * event_key → 斜杠命令的映射通过 FEISHU_MENU_KEY_MAP 环境变量配置（JSON 字符串）
   *
   * @param {Object} params
   * @param {string} params.feishuOpenId  点击者 open_id（p2p 单聊中同时作为 chat_id）
   * @param {string} params.eventKey      飞书菜单配置的 event_key
   */
  async handleMenuAction({ feishuOpenId, eventKey }) {
    const commandText = MENU_KEY_MAP[eventKey];
    if (!commandText) {
      console.warn(`[Feishu:engine] Unknown menu event_key: "${eventKey}". Configure via FEISHU_MENU_KEY_MAP env var.`);
      return;
    }

    console.log(`[Feishu:engine] Menu: eventKey="${eventKey}" → command="${commandText}" for ${feishuOpenId}`);

    const binding = feishuDb.getBinding(feishuOpenId);
    if (!binding && !commandText.toLowerCase().startsWith('/auth')) {
      await this.larkClient.sendTextToUser(feishuOpenId,
        feishuBindingGuide()
      );
      return;
    }

    // 优先使用 DB 中存储的 chat_id（由历史消息写入），保证 sendMessage 时 receive_id_type 可用 chat_id
    // 若用户从未发过消息（chat_id 尚未入库），则回退到 feishuOpenId（open_id），
    // command-handler 的 _reply/_replyCard 会自动检测 ou_ 前缀并使用 sendTextToUser
    const state = feishuDb.getSessionState(feishuOpenId);
    const chatId = state?.chat_id || feishuOpenId;

    await this.commandHandler.dispatch(feishuOpenId, chatId, null, commandText);
  }

  // ─── 卡片交互回调入口 ─────────────────────────────────────────────────────────

  /**
   * @param {Object} params
   * @param {string} params.feishuOpenId
   * @param {Object} params.action        按钮 value 对象
   * @param {string} params.messageId     卡片消息 ID（用于更新卡片）
   * @param {Object} params.formValue     输入框值
   */
  async handleCardAction({ feishuOpenId, action, messageId, formValue }) {
    console.log('[Feishu:engine] handleCardAction called, action:', JSON.stringify(action), 'messageId:', messageId);
    const actionType = action?.action;
    if (!actionType) {
      console.log('[Feishu:engine] handleCardAction: no actionType, returning');
      return {};
    }

    const binding = feishuDb.getBinding(feishuOpenId);
    if (!binding) {
      console.log('[Feishu:engine] handleCardAction: no binding for', feishuOpenId);
      return {};
    }

    switch (actionType) {
      case 'tool_approve':
        return await this._handleToolApproveAction(feishuOpenId, action);

      case 'set_mode':
        return await this._handleSetModeAction(feishuOpenId, action);

      case 'show_projects':
        await this._handleShowProjectsAction(feishuOpenId, binding.user_uuid);
        return {};

      case 'show_modes': {
        const state = feishuDb.getSessionState(feishuOpenId) || {};
        const cardStr = buildModeSelectCardDirect(state.permission_mode || 'default');
        return toCardResponse(cardStr);
      }

      case 'show_model_select': {
        const state = feishuDb.getSessionState(feishuOpenId) || {};
        const cardStr = buildModelSelectCardDirect(state.claude_model || 'sonnet');
        return toCardResponse(cardStr);
      }

      case 'set_claude_model': {
        const model = action.model;
        const validModels = new Set(['sonnet', 'opus', 'haiku']);
        if (!validModels.has(model)) return {};
        feishuDb.updateSessionState(feishuOpenId, { claude_model: model });
        const cardStr = buildModelSelectCardDirect(model);
        return toCardResponse(cardStr);
      }

      case 'show_sessions':
        await this.commandHandler._handleList(feishuOpenId, feishuOpenId, null);
        return {};

      case 'new_session': {
        feishuDb.clearSession(feishuOpenId);
        const state = feishuDb.getSessionState(feishuOpenId) || {};
        const cardStr = buildStatusCardDirect({ ...state, claude_session_id: null });
        if (messageId) {
          this.larkClient.replyText(messageId, '✅ 新建会话成功，直接发消息即可开始新对话。').catch(() => {});
        }
        return toCardResponse(cardStr);
      }

      case 'list_select': {
        console.log('[Feishu:engine] list_select formValue:', JSON.stringify(formValue), 'action:', JSON.stringify(action));
        const index = parseInt(formValue?.list_select, 10);
        return await this._handleListSelect(feishuOpenId, action.type, index);
      }

      case 'ask_user_submit':
        return await this._handleAskUserSubmit(feishuOpenId, action, formValue);

      case 'ask_user_skip':
        return await this._handleAskUserSkip(feishuOpenId, action);

      case 'create_project_submit':
        return await this._handleCreateProjectSubmit(feishuOpenId, messageId, binding.user_uuid, formValue);

      case 'create_project_cancel': {
        const state = feishuDb.getSessionState(feishuOpenId) || {};
        return toCardResponse(buildStatusCardDirect(state));
      }

      case 'delete_project':
        return await this._handleDeleteProject(feishuOpenId, messageId, binding.user_uuid, formValue, action);

      case 'delete_session':
        return await this._handleDeleteSession(feishuOpenId, messageId, binding.user_uuid, formValue);

      case 'delete_cancel': {
        const state = feishuDb.getSessionState(feishuOpenId) || {};
        return toCardResponse(buildStatusCardDirect(state));
      }

      default:
        console.log(`[Feishu:engine] Unknown card action: ${actionType}`);
        return {};
    }
  }

  // ─── 工具审批 ─────────────────────────────────────────────────────────────────

  async _handleToolApproveAction(feishuOpenId, action) {
    const { decision, request_id: requestId, tool_name: toolName } = action;
    const pending = this.pendingApprovals.get(feishuOpenId);

    // 用 action 中的 requestId 或 pendingApprovals 中的 requestId
    const resolveId = requestId || pending?.requestId;
    if (!resolveId) return {};

    let result;
    if (decision === 'allow_remember') {
      result = { allow: true, rememberEntry: toolName || pending?.toolName };
    } else if (decision === 'allow') {
      result = { allow: true };
    } else {
      result = { allow: false, message: '用户拒绝' };
    }

    resolveToolApproval(resolveId, result);
    this.pendingApprovals.delete(feishuOpenId);

    // 通过 WS 同步响应更新卡片
    const updatedCardStr = buildToolApprovalResultCardDirect(decision === 'deny' ? 'deny' : 'allow');
    return toCardResponse(updatedCardStr);
  }

  // ─── AskUserQuestion 卡片提交处理 ─────────────────────────────────────────────

  async _handleAskUserSubmit(feishuOpenId, action, formValue) {
    const { request_id: requestId } = action;
    const pending = this.pendingApprovals.get(feishuOpenId);
    const resolveId = requestId || pending?.requestId;
    if (!resolveId) return {};

    const questions = pending?.questions || [];
    const answers = {};
    questions.forEach((q, qi) => {
      const fieldValue = formValue?.[`q_${qi}`];
      // multi_select_static 返回数组，select_static 返回字符串
      answers[q.question] = Array.isArray(fieldValue)
        ? fieldValue.join(', ')
        : (fieldValue || '');
    });

    resolveToolApproval(resolveId, {
      allow: true,
      updatedInput: { ...(pending?.originalInput || {}), answers },
    });
    this.pendingApprovals.delete(feishuOpenId);

    const resultCardStr = buildAskUserQuestionResultCard(answers, false);
    return toCardResponse(resultCardStr);
  }

  async _handleAskUserSkip(feishuOpenId, action) {
    const { request_id: requestId } = action;
    const pending = this.pendingApprovals.get(feishuOpenId);
    const resolveId = requestId || pending?.requestId;
    if (!resolveId) return {};

    resolveToolApproval(resolveId, { allow: false, message: '用户跳过问题' });
    this.pendingApprovals.delete(feishuOpenId);

    const resultCardStr = buildAskUserQuestionResultCard({}, true);
    return toCardResponse(resultCardStr);
  }

  async _handleTextPermissionResponse(feishuOpenId, messageId, text) {
    const lower = text.toLowerCase().trim();
    const pending = this.pendingApprovals.get(feishuOpenId);
    if (!pending) return false;

    // AskUserQuestion：解析选项字母/数字回复
    if (pending.toolName === 'AskUserQuestion') {
      return await this._handleAskUserQuestionResponse(feishuOpenId, messageId, text, pending);
    }

    let decision = null;
    if (ALLOW_ALL_KEYWORDS.has(lower)) {
      decision = 'allow_remember';
    } else if (ALLOW_KEYWORDS.has(lower)) {
      decision = 'allow';
    } else if (DENY_KEYWORDS.has(lower)) {
      decision = 'deny';
    }

    if (!decision) return false;

    let result;
    if (decision === 'allow_remember') {
      result = { allow: true, rememberEntry: pending.toolName };
    } else if (decision === 'allow') {
      result = { allow: true };
    } else {
      result = { allow: false, message: '用户拒绝' };
    }

    resolveToolApproval(pending.requestId, result);
    this.pendingApprovals.delete(feishuOpenId);

    // 更新挂起的卡片
    if (pending.cardMessageId) {
      const updatedCard = buildToolApprovalResultCardDirect(decision === 'deny' ? 'deny' : 'allow');
      await this.larkClient.updateCard(pending.cardMessageId, updatedCard).catch(() => {});
    } else {
      const label = decision === 'deny' ? '❌ 已拒绝' : '✅ 已允许';
      await this.larkClient.replyText(messageId, label);
    }

    return true;
  }

  async _handleAskUserQuestionResponse(feishuOpenId, messageId, text, pending) {
    const lower = text.toLowerCase().trim();
    const questions = pending.questions || [];
    const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';

    // 跳过
    if (DENY_KEYWORDS.has(lower) || lower === '跳过' || lower === 'skip') {
      resolveToolApproval(pending.requestId, { allow: false, message: '用户跳过问题' });
      this.pendingApprovals.delete(feishuOpenId);
      if (pending.cardMessageId) {
        const resultCard = buildAskUserQuestionResultCard({}, true);
        await this.larkClient.updateCard(pending.cardMessageId, resultCard).catch(() => {});
      } else {
        await this.larkClient.replyText(messageId, '⏭️ 已跳过问卷').catch(() => {});
      }
      return true;
    }

    // 解析：多题用逗号分隔，多选用 + 连接；同时支持字母（A）和数字（1）
    const rawParts = text.trim().toUpperCase().split(',').map(s => s.trim());

    // 将数字转换为字母（"1" → "A"，"2" → "B" …）
    const normalize = (token) => {
      const n = parseInt(token, 10);
      return (!isNaN(n) && n >= 1 && n <= 26) ? LETTERS[n - 1] : token;
    };

    const isValidLetter = (t) => /^[A-Z]$/.test(t);
    const parsePart = (part) => part.split('+').map(t => normalize(t.trim()));
    const allValid = rawParts.every(p => parsePart(p).every(isValidLetter));

    if (!allValid || (questions.length > 1 && rawParts.length !== questions.length)) {
      const hint = questions.length > 1
        ? `请按题目顺序输入选项，多题用逗号分隔（如 A,B,A），多选用 + 连接`
        : `请输入选项字母（如 A）或数字（如 1），多选用 + 连接（如 A+B）`;
      await this.larkClient.replyText(messageId, `❌ 格式不正确，${hint}，或回复"跳过"跳过此问卷。`).catch(() => {});
      return true; // 消耗此消息，等待用户重新输入
    }

    // 构建 answers 对象
    const answers = {};
    questions.forEach((q, qi) => {
      const part = rawParts.length === 1 ? rawParts[0] : rawParts[qi];
      const selectedLetters = parsePart(part);
      const options = q.options || [];
      const selectedLabels = selectedLetters
        .map(l => options[LETTERS.indexOf(l)])
        .filter(Boolean)
        .map(opt => opt.label);
      answers[q.question] = selectedLabels.join(', ');
    });

    resolveToolApproval(pending.requestId, {
      allow: true,
      updatedInput: { ...pending.originalInput, answers },
    });
    this.pendingApprovals.delete(feishuOpenId);

    // 更新卡片（若通过文字回复了已有的交互卡片）
    if (pending.cardMessageId) {
      const resultCard = buildAskUserQuestionResultCard(answers, false);
      await this.larkClient.updateCard(pending.cardMessageId, resultCard).catch(() => {});
    } else {
      const summary = Object.entries(answers)
        .map(([q, a]) => `• ${q}：${a || '（未选中）'}`)
        .join('\n');
      await this.larkClient.replyText(messageId, `✅ 已提交\n${summary}`).catch(() => {});
    }
    return true;
  }

  // ─── 卡片按钮 action 处理 ─────────────────────────────────────────────────────

  async _handleSetModeAction(feishuOpenId, action) {
    const mode = action.mode;
    const validModes = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions']);
    if (!validModes.has(mode)) return {};

    feishuDb.updateSessionState(feishuOpenId, { permission_mode: mode });

    const cardStr = buildModeSelectCardDirect(mode);
    return toCardResponse(cardStr);
  }

  // ─── 创建项目 ─────────────────────────────────────────────────────────────────

  async _handleCreateProjectSubmit(feishuOpenId, messageId, userUuid, formValue) {
    const PROJECT_NAME_REGEX = /^[a-zA-Z0-9_-]{1,100}$/;
    const TRUSTED_GIT_HOSTS = ['github.com', 'gitlab.com', 'bitbucket.org'];

    const projectName = (formValue?.project_name || '').trim();
    const githubUrl   = (formValue?.github_url   || '').trim();

    // —— 同步校验 ——
    if (!projectName) {
      return toCardResponse(buildCreateProjectResultCard(false, '项目名称不能为空'));
    }
    if (!PROJECT_NAME_REGEX.test(projectName)) {
      return toCardResponse(buildCreateProjectResultCard(false,
        '项目名称只能包含字母、数字、连字符（`-`）和下划线（`_`），长度 1-100 位'));
    }
    if (githubUrl) {
      let parsedUrl;
      try { parsedUrl = new URL(githubUrl); } catch {
        return toCardResponse(buildCreateProjectResultCard(false, 'GitHub 仓库地址格式不正确'));
      }
      if (parsedUrl.protocol !== 'https:' || !TRUSTED_GIT_HOSTS.includes(parsedUrl.hostname.toLowerCase())) {
        return toCardResponse(buildCreateProjectResultCard(false,
          `仅支持 ${TRUSTED_GIT_HOSTS.join(', ')} 的 HTTPS 地址`));
      }
    }

    // —— 立即返回"处理中"卡片，异步完成创建后更新卡片 ——
    const processingCard = buildCreateProjectResultCard(true,
      githubUrl
        ? `正在创建项目 **${projectName}** 并克隆仓库，请稍候…`
        : `正在创建项目 **${projectName}**…`
    );

    setImmediate(async () => {
      try {
        const { projectsDir } = getUserPaths(userUuid);
        const absolutePath = path.join(projectsDir, projectName);

        // 检查文件夹是否已存在（仅删记录保留文件夹的情况允许重新注册）
        let folderExists = false;
        try {
          await fs.access(absolutePath);
          folderExists = true;
        } catch (err) {
          if (err.code !== 'ENOENT') throw err;
        }

        if (!folderExists) {
          if (githubUrl) {
            // 让 git clone 自行创建目录
            await new Promise((resolve, reject) => {
              const git = spawn('git', ['clone', '--depth', '1', githubUrl, absolutePath], {
                env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
                stdio: 'pipe',
              });
              let stderr = '';
              git.stderr.on('data', d => { stderr += d.toString(); });
              git.on('close', code => code === 0 ? resolve() : reject(new Error(stderr.trim() || 'git clone 失败')));
              git.on('error', reject);
              setTimeout(() => { git.kill(); reject(new Error('git clone 超时（60s）')); }, 60000);
            });
          } else {
            await fs.mkdir(absolutePath, { recursive: true });
          }
        }

        try {
          await addProjectManually(absolutePath, projectName, userUuid);
        } catch (err) {
          if (err.message?.includes('already configured')) {
            await this.larkClient.updateCard(messageId,
              buildCreateProjectResultCard(false, `项目 **${projectName}** 已存在，请换个名称`)
            ).catch(() => {});
            return;
          }
          throw err;
        }

        // 切换到新项目并清空会话
        feishuDb.updateSessionState(feishuOpenId, { cwd: absolutePath, claude_session_id: null });
        const newState = feishuDb.getSessionState(feishuOpenId) || {};
        await this.larkClient.updateCard(messageId,
          buildStatusCardDirect({ ...newState, cwd: absolutePath })
        ).catch(() => {});
      } catch (err) {
        console.error('[Feishu:engine] createProject error:', err.message);
        await this.larkClient.updateCard(messageId,
          buildCreateProjectResultCard(false, `创建失败：${err.message}`)
        ).catch(() => {});
      }
    });

    return toCardResponse(processingCard);
  }

  async _handleShowProjectsAction(feishuOpenId, userUuid) {
    let projects = [];
    try {
      projects = await getProjects(userUuid);
    } catch (err) {
      console.error('[Feishu:engine] getProjects error:', err.message);
    }

    const sliced = projects.slice(0, 20);
    const items = sliced.map(p => ({
      label: p.displayName || p.name,
      sub: `\`${stripProjectPrefix(p.path || p.name)}\``,
    }));

    this.commandHandler._storeProjectList(feishuOpenId, sliced);

    const card = buildListCardDirect({
      title: '📁 项目列表',
      items,
      actionType: 'project',
    });
    await this.larkClient.sendMessageToUser(feishuOpenId, card, 'interactive').catch(() =>
      this.larkClient.sendTextToUser(feishuOpenId, sliced.map((p, i) => `${i + 1}. ${p.displayName || p.name}`).join('\n'))
    );
  }

  // ─── 删除项目 ──────────────────────────────────────────────────────────────────

  async _handleDeleteProject(feishuOpenId, messageId, userUuid, formValue, action) {
    const index = parseInt(formValue?.delete_select, 10);
    const projects = this.commandHandler.getListCache(feishuOpenId, 'projects');
    const project = !isNaN(index) && index >= 1 ? projects[index - 1] : null;

    if (!project) {
      return toCardResponse(buildDeleteResultCard(false, '请先从下拉列表中选择要删除的项目。'));
    }

    try {
      const deleteFolder = action?.delete_folder === 'true';
      await deleteProject(project.name, userUuid, { deleteFolder });

      // 若删除的是当前项目，清空 cwd 和会话
      const state = feishuDb.getSessionState(feishuOpenId) || {};
      if (state.cwd && (state.cwd === project.path || state.cwd === project.name)) {
        feishuDb.updateSessionState(feishuOpenId, { cwd: '', claude_session_id: null });
      }

      const displayName = project.displayName || project.name;
      const extra = deleteFolder ? '（含文件夹）' : '';
      if (messageId) {
        this.larkClient.replyText(messageId, `✅ 项目 **${displayName}** 已删除${extra}`).catch(() => {});
      }
      return toCardResponse(buildDeleteResultCard(true, `项目 **${displayName}** 已删除${extra}`));
    } catch (err) {
      console.error('[Feishu:engine] deleteProject error:', err.message);
      return toCardResponse(buildDeleteResultCard(false, `删除失败：${err.message}`));
    }
  }

  // ─── 删除会话 ──────────────────────────────────────────────────────────────────

  async _handleDeleteSession(feishuOpenId, messageId, userUuid, formValue) {
    const index = parseInt(formValue?.delete_select, 10);
    const sessions = this.commandHandler.getListCache(feishuOpenId, 'sessions');
    const session = !isNaN(index) && index >= 1 ? sessions[index - 1] : null;

    if (!session) {
      return toCardResponse(buildDeleteResultCard(false, '请先从下拉列表中选择要删除的会话。'));
    }

    const state = feishuDb.getSessionState(feishuOpenId) || {};
    const cwd = state.cwd;
    if (!cwd) {
      return toCardResponse(buildDeleteResultCard(false, '未设置当前项目，无法删除会话。'));
    }

    const projectName = cwd.replace(/\//g, '-');

    try {
      await deleteSession(projectName, session.id, userUuid);

      // 若删除的是当前会话，清空 session id
      if (state.claude_session_id === session.id) {
        feishuDb.clearSession(feishuOpenId);
      }

      const shortId = session.id?.slice(0, 8) || session.id;
      if (messageId) {
        this.larkClient.replyText(messageId, `✅ 会话 \`${shortId}\` 已删除`).catch(() => {});
      }
      return toCardResponse(buildDeleteResultCard(true, `会话 \`${shortId}\` 已删除`));
    } catch (err) {
      console.error('[Feishu:engine] deleteSession error:', err.message);
      return toCardResponse(buildDeleteResultCard(false, `删除失败：${err.message}`));
    }
  }

  async _handleListSelect(feishuOpenId, type, index) {
    const idx = typeof index === 'number' ? index : parseInt(index, 10);

    if (type === 'project') {
      const projects = this.commandHandler.getListCache(feishuOpenId, 'projects');
      const project = !isNaN(idx) && idx >= 1 ? projects[idx - 1] : null;
      if (project) {
        feishuDb.updateSessionState(feishuOpenId, { cwd: project.path || project.name, claude_session_id: null });
      }
    } else if (type === 'session') {
      const sessions = this.commandHandler.getListCache(feishuOpenId, 'sessions');
      const session = !isNaN(idx) && idx >= 1 ? sessions[idx - 1] : null;
      if (session) {
        feishuDb.updateSessionState(feishuOpenId, { claude_session_id: session.id });
      }
    }

    // 通过 WS 同步响应返回状态卡片
    const state = feishuDb.getSessionState(feishuOpenId) || {};
    const cardStr = buildStatusCardDirect(state);
    return toCardResponse(cardStr);
  }

  // ─── 普通对话 ─────────────────────────────────────────────────────────────────

  async _runChat(feishuOpenId, chatId, messageId, content, images, files, quotedFiles) {
    const binding = feishuDb.getBinding(feishuOpenId);
    if (!binding) return;

    const state = feishuDb.getSessionState(feishuOpenId) || {};

    // 未选择项目路径时，拒绝对话并提示用户
    if (!state.cwd) {
      await this.larkClient.sendText(chatId,
        '📁 请先选择一个项目再开始对话。\n\n发送 `/project` 查看并选择项目，或发送 `/project create` 创建新项目。'
      );
      return;
    }

    let finalContent = content;
    let finalImages  = images ? [...images] : null;

    // ─── 直接发送的文件附件：保存到 {cwd}/lark-files/ ────────────────────────
    if (files && files.length > 0) {
      const larkFilesDir = path.join(state.cwd, 'lark-files');
      try {
        await fs.mkdir(larkFilesDir, { recursive: true });
        const filePaths = [];
        for (const file of files) {
          const safeName = `${file.fileKey || Date.now()}_${path.basename(file.filename) || 'attachment'}`;
          const filePath = path.join(larkFilesDir, safeName);
          await fs.writeFile(filePath, file.data);
          filePaths.push(filePath);
          console.log(`[Feishu:engine] Saved file: ${filePath}`);
        }
        const refs = filePaths.map(p => `@${p}`).join('\n');
        finalContent = finalContent ? `${finalContent}\n\n${refs}` : refs;
      } catch (err) {
        console.error('[Feishu:engine] Failed to save file attachment:', err.message);
        await this.larkClient.sendText(chatId, `❌ 文件保存失败：${err.message}`);
        return;
      }
    }

    // ─── 引用消息中的附件：检查是否已在本地，按需下载 ───────────────────────
    if (quotedFiles && quotedFiles.length > 0) {
      const larkFilesDir = path.join(state.cwd, 'lark-files');
      try {
        await fs.mkdir(larkFilesDir, { recursive: true });
        const filePaths = [];
        for (const qf of quotedFiles) {
          if (qf.type === 'image') {
            // 引用的图片：下载后追加到 images 列表，让 Claude 直接"看"到
            const buf = await this.larkClient.downloadFile(qf.messageId, qf.fileKey, 'image');
            finalImages = finalImages || [];
            finalImages.push({ data: buf, mimeType: 'image/jpeg' });
            console.log(`[Feishu:engine] Loaded quoted image: ${qf.fileKey}`);
          } else if (qf.type === 'audio') {
            // 引用的音频：转写后追加到 prompt
            const buf = await this.larkClient.downloadFile(qf.messageId, qf.fileKey);
            const transcribed = await transcribeAudio(buf, 'ogg').catch(err => {
              console.error('[Feishu:engine] transcribeAudio for quoted audio failed:', err.message);
              return null;
            });
            if (transcribed) {
              finalContent = finalContent
                ? `${finalContent}\n\n（引用语音转写：「${transcribed}」）`
                : `（引用语音转写：「${transcribed}」）`;
            }
          } else {
            // 引用的文件/视频：保存到 lark-files/ 并加 @ 引用
            const safeName = `${qf.fileKey}_${path.basename(qf.filename) || 'attachment'}`;
            const filePath = path.join(larkFilesDir, safeName);
            let exists = false;
            try { await fs.access(filePath); exists = true; } catch {}
            if (!exists) {
              const buf = await this.larkClient.downloadFile(qf.messageId, qf.fileKey);
              await fs.writeFile(filePath, buf);
              console.log(`[Feishu:engine] Downloaded quoted file: ${filePath}`);
            } else {
              console.log(`[Feishu:engine] Quoted file already cached: ${filePath}`);
            }
            filePaths.push(filePath);
          }
        }
        if (filePaths.length > 0) {
          const refs = filePaths.map(p => `@${p}`).join('\n');
          finalContent = finalContent ? `${finalContent}\n\n${refs}` : refs;
        }
      } catch (err) {
        console.error('[Feishu:engine] Failed to process quoted attachment:', err.message);
        await this.larkClient.sendText(chatId, `⚠️ 引用附件处理失败：${err.message}，将忽略附件继续处理。`);
        // 不中断，继续对话
      }
    }

    await runQuery({
      feishuOpenId,
      chatId,
      messageId,
      content: finalContent,
      images:  finalImages,
      userUuid:        binding.user_uuid,
      state,
      larkClient:      this.larkClient,
      pendingApprovals: this.pendingApprovals,
    });
  }

  /**
   * 处理排队消息。调用时 processingLocks 必须已被持有，
   * 方法内部循环处理完所有队列项后统一释放锁。
   */
  async _drainQueue(feishuOpenId) {
    try {
      while (true) {
        const queue = this.messageQueues.get(feishuOpenId);
        if (!queue || queue.length === 0) break;

        const next = queue.shift();
        if (queue.length === 0) this.messageQueues.delete(feishuOpenId);

        // 给排队消息添加"处理中"表情
        let reactionId = null;
        if (next.messageId) {
          reactionId = await this.larkClient.addReaction(next.messageId, 'OneSecond').catch(() => null);
        }

        try {
          await this._runChat(feishuOpenId, next.chatId, next.messageId, next.content, next.images, next.files, next.quotedFiles);
        } catch (err) {
          console.error('[Feishu:engine] drainQueue _runChat error:', err.message);
        } finally {
          if (reactionId && next.messageId) {
            this.larkClient.deleteReaction(next.messageId, reactionId).catch(() => {});
          }
        }
      }
    } finally {
      // 无论正常还是异常，最终释放锁
      this.processingLocks.delete(feishuOpenId);
    }
  }

  // 辅助：通过 feishuOpenId 获取 chatId（单聊中 open_id 即 chat_id）
  async _getChatIdForUser(feishuOpenId) {
    return feishuOpenId;
  }
}
