import type { Plugin, ViteDevServer } from 'vite';
import { WebSocketServer, WebSocket } from 'ws';
import type { IncomingMessage } from 'http';
import fs from 'fs';
import path from 'path';
import extractZip from 'extract-zip';
import { runCommand } from '../scripts/utils/command-runtime.mjs';

export interface WebSocketMessage {
  type: string;
  data?: any;
  payload?: any;
  client?: string;
  version?: string;
}

export interface ClientMeta {
  id: number;
  type: string;  // 'figma' | 'vscode' | 'browser' | 'unknown'
  version?: string;
  address?: string;
  connectedAt: number;
}

interface UploadSession {
  transferId: string;
  pageName: string;
  displayName?: string;
  outputRelativeDir: string;
  fileName: string;
  mode: 'zip' | 'files';
  totalChunks: number;
  totalBytes?: number;
  receivedChunks: number;
  receivedBytes: number;
  chunks: Map<number, Buffer>;
  filesRoot?: string;
  filesReceived: number;
  startedAt: number;
}

interface HandleMessageContext {
  clientMeta: Map<WebSocket, ClientMeta>;
  uploadSessions: Map<string, UploadSession>;
  projectRoot: string;
}

const IGNORED_EXTRACT_ENTRIES = new Set(['__MACOSX', '.DS_Store']);
const nodeCommand = process.execPath;

function ensureDir(dirPath: string) {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true });
  }
}

function inferExtractedRootFolder(extractDir: string) {
  if (!fs.existsSync(extractDir)) {
    return { entryCount: 0, hasRootFolder: false, rootFolderName: '' };
  }

  const entries = fs
    .readdirSync(extractDir, { withFileTypes: true })
    .filter(entry => !IGNORED_EXTRACT_ENTRIES.has(entry.name));

  if (entries.length === 1 && entries[0].isDirectory()) {
    return { entryCount: entries.length, hasRootFolder: true, rootFolderName: entries[0].name };
  }

  return { entryCount: entries.length, hasRootFolder: false, rootFolderName: '' };
}

function isSafeName(value: string) {
  return Boolean(value && value.trim() && !value.includes('..') && !/[\\/]/.test(value));
}

function isSafeRelativePath(value: string) {
  if (!value || typeof value !== 'string') return false;
  const normalized = value.replace(/\\/g, '/');
  if (normalized.startsWith('/') || normalized.startsWith('~')) return false;
  if (normalized.split('/').some(part => part === '..')) return false;
  return true;
}

function isValidDisplayName(value?: string) {
  if (value === undefined) return true;
  const text = String(value).trim();
  return text.length > 0 && text.length <= 200;
}

function normalizeRelativeDir(value: string) {
  return value
    .replace(/\\/g, '/')
    .split('/')
    .filter(Boolean)
    .join('/');
}

function resolveOutputRelativeDir(data: any, fallbackName: string) {
  const candidates = [
    data?.outputRelativeDir,
    data?.outputPath,
    data?.targetPath,
    data?.targetDir,
    data?.folderPath,
    data?.relativePath,
    data?.pagePath,
  ];

  for (const candidate of candidates) {
    if (typeof candidate !== 'string') continue;
    const normalized = normalizeRelativeDir(candidate.trim());
    if (!normalized) continue;
    if (!isSafeRelativePath(normalized)) return null;

    const segments = normalized.split('/');
    if (segments.length === 0 || segments.some(segment => !isSafeName(segment))) {
      return null;
    }

    return normalized;
  }

  return fallbackName;
}

function resolvePrototypeOutputDir(projectRoot: string, outputRelativeDir: string) {
  return path.join(projectRoot, 'src', 'prototypes', ...outputRelativeDir.split('/'));
}

function sendWsMessage(ws: WebSocket, payload: any) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(payload));
  }
}

/**
 * WebSocket 插件 - 在 Vite 开发服务器上添加 WebSocket 支持
 */
export function websocketPlugin(): Plugin {
  let wss: WebSocketServer | null = null;
  const clients = new Set<WebSocket>();
  const clientMeta = new Map<WebSocket, ClientMeta>();
  const uploadSessions = new Map<string, UploadSession>();
  let nextClientId = 1;
  const WS_PATH = '/ws';
  const projectRoot = process.cwd();

  return {
    name: 'vite-websocket',
    apply: 'serve',

    configureServer(server: ViteDevServer) {
      // 使用独立的 noServer 模式，避免抢占 Vite 自带的 HMR WebSocket 升级请求
      // （此前共享同一个 httpServer 且指定 path 会拦截非 /ws 升级，导致 HMR 报 Invalid frame header）
      wss = new WebSocketServer({ noServer: true });

      const handleUpgrade = (req: IncomingMessage, socket: any, head: Buffer) => {
        // 只处理 /ws 的升级请求，其余交给 Vite 自己的 HMR 逻辑
        const pathname = req.url ? new URL(req.url, 'http://localhost').pathname : '';
        if (pathname !== WS_PATH) {
          return;
        }

        wss?.handleUpgrade(req, socket, head, (ws) => {
          wss?.emit('connection', ws, req);
        });
      };

      server.httpServer?.on('upgrade', handleUpgrade);

      wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
        // 从 URL 查询参数中获取客户端类型
        const url = new URL(req.url || '', 'http://localhost');
        const clientType = url.searchParams.get('client') || 'unknown';
        const clientVersion = url.searchParams.get('version') || undefined;
        
        console.log('[WebSocket] 新客户端连接:', {
          type: clientType,
          version: clientVersion,
          address: req.socket.remoteAddress
        });
        
        clients.add(ws);
        const meta: ClientMeta = {
          id: nextClientId++,
          type: clientType,
          version: clientVersion,
          address: req.socket.remoteAddress,
          connectedAt: Date.now()
        };
        clientMeta.set(ws, meta);

        // 发送欢迎消息
        ws.send(JSON.stringify({ 
          type: 'connected', 
          message: 'WebSocket 连接成功' 
        }));

        // 处理客户端消息
        ws.on('message', (rawData) => {
          try {
            const payloadText = typeof rawData === 'string'
              ? rawData
              : Buffer.isBuffer(rawData)
                ? rawData.toString()
                : Array.isArray(rawData)
                  ? Buffer.concat(rawData).toString()
                  : Buffer.from(rawData as ArrayBuffer).toString();

            const message: WebSocketMessage = JSON.parse(payloadText);
            const client = clientMeta.get(ws);
            const bodyKeys = message && typeof message === 'object' ? Object.keys(message as any) : [];
            const dataKeys = message?.data && typeof message.data === 'object'
              ? Object.keys(message.data)
              : [];

            console.log('[WebSocket] 收到 WS 消息:', {
              clientId: client?.id,
              address: client?.address,
              type: message?.type,
              hasData: message?.data !== undefined,
              bodyKeys,
              dataKeys,
              bytes: Buffer.byteLength(payloadText, 'utf8')
            });

            // 处理不同类型的消息
            handleMessage(ws, message, clients, { clientMeta, uploadSessions, projectRoot });
          } catch (err) {
            console.error('[WebSocket] 解析消息失败:', err);
            ws.send(JSON.stringify({ 
              type: 'error', 
              message: '消息格式错误' 
            }));
          }
        });

        // 处理连接关闭
        ws.on('close', () => {
          console.log('[WebSocket] 客户端断开连接');
          clients.delete(ws);
          clientMeta.delete(ws);
        });

        // 处理错误
        ws.on('error', (err) => {
          console.error('[WebSocket] 连接错误:', err);
          clients.delete(ws);
          clientMeta.delete(ws);
        });
      });

      // HTTP API: 获取当前 WS 客户端信息
      server.middlewares.use('/api/ws/clients', (req, res) => {
        if (req.method !== 'GET') {
          res.statusCode = 405;
          res.end('Method Not Allowed');
          return;
        }

        const list = Array.from(clientMeta.values()).map((item) => ({
          id: item.id,
          type: item.type,
          version: item.version,
          address: item.address,
          connectedAt: item.connectedAt
        }));
        
        // 统计各类型客户端数量
        const stats = Array.from(clientMeta.values()).reduce((acc, item) => {
          acc[item.type] = (acc[item.type] || 0) + 1;
          return acc;
        }, {} as Record<string, number>);

        res.setHeader('Content-Type', 'application/json; charset=utf-8');
        res.end(JSON.stringify({ 
          clients: list,
          stats,
          total: list.length
        }));
      });

      // HTTP API: 发送消息给全部客户端
      server.middlewares.use('/api/ws/send', (req, res) => {
        if (req.method !== 'POST') {
          res.statusCode = 405;
          res.end('Method Not Allowed');
          return;
        }

        let body = '';
        req.on('data', (chunk) => {
          body += chunk;
        });

        req.on('end', () => {
          try {
            const parsedBody = JSON.parse(body || '{}');
            const { type } = parsedBody;
            const payload = (parsedBody as any).payload;
            const targetClientType = (parsedBody as any).targetClientType;
            const targetClientTypes = (parsedBody as any).targetClientTypes;
            // 兼容历史字段：payload / data
            const data = payload !== undefined
              ? payload
              : (parsedBody as any).data;

            // 验证 type
            if (!type || typeof type !== 'string') {
              res.statusCode = 400;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: 'type is required' }));
              return;
            }

            // 验证 data
            if (data === undefined || data === null) {
              res.statusCode = 400;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ error: 'data is required' }));
              return;
            }

            // 针对 sync-widget-content 的特殊验证
            if (type === 'sync-widget-content') {
              // 验证 widgetId（必需字段）
              if (!parsedBody.widgetId || typeof parsedBody.widgetId !== 'string') {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'widgetId is required for sync-widget-content' }));
                return;
              }

              // 验证 data.layers 不为空
              if (!data || typeof data !== 'object' || !data.layers) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'data.layers is required' }));
                return;
              }

              if (!Array.isArray(data.layers) || data.layers.length === 0) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'data.layers array is empty' }));
                return;
              }
            }

            // 针对 sync-page-content 的验证
            if (type === 'sync-page-content') {
              // 验证 data.layers 不为空
              if (!data || typeof data !== 'object' || !data.layers) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'data.layers is required' }));
                return;
              }

              if (!Array.isArray(data.layers) || data.layers.length === 0) {
                res.statusCode = 400;
                res.setHeader('Content-Type', 'application/json; charset=utf-8');
                res.end(JSON.stringify({ error: 'data.layers array is empty' }));
                return;
              }
            }

            const allClientCount = clients.size;
            const normalizedTargetClientTypes = Array.isArray(targetClientTypes)
              ? targetClientTypes.filter((v) => typeof v === 'string' && v)
              : typeof targetClientType === 'string' && targetClientType
                ? [targetClientType]
                : [];

            // 这里的 targetClientTypes 是插件层的“按客户端类型定向”机制。
            // 当前前端仅对 vscode/cursor 做 open-file 的稳定投递；其他 IDE 本轮通过系统命令打开，不走 WS 定向。
            const targetClients = normalizedTargetClientTypes.length === 0
              ? clients
              : new Set(
                Array.from(clients).filter((ws) => {
                  const meta = clientMeta.get(ws);
                  return meta?.type && normalizedTargetClientTypes.includes(meta.type);
                })
              );

            const targetClientCount = targetClients.size;
            
            // 如果没有客户端连接
            if (allClientCount === 0) {
              res.statusCode = 200;
              res.setHeader('Content-Type', 'application/json; charset=utf-8');
              res.end(JSON.stringify({ ok: true, sent: 0, warning: 'no clients connected' }));
              return;
            }

            console.log('[WebSocket] /api/ws/send 请求:', {
              type,
              hasPayload: data !== undefined,
              clientCount: allClientCount,
              targetClientCount,
              targetClientTypes: normalizedTargetClientTypes,
              bodyKeys: parsedBody && typeof parsedBody === 'object' ? Object.keys(parsedBody) : []
            });

            // 立即返回成功状态（不等待渲染完成）
            res.setHeader('Content-Type', 'application/json; charset=utf-8');
            res.end(JSON.stringify({
              ok: true,
              sent: targetClientCount,
              ...(normalizedTargetClientTypes.length > 0 && targetClientCount === 0
                ? { warning: 'no target clients connected' }
                : {})
            }));

            // 异步广播消息（不阻塞响应）
            setImmediate(() => {
              try {
                // 构建完整的消息对象，包含所有字段（type, widgetId/pageId, data, blurImages, metadata 等）
                const message: any = {
                  type,
                  ...(parsedBody.widgetId ? { widgetId: parsedBody.widgetId } : {}),
                  ...(parsedBody.pageId ? { pageId: parsedBody.pageId } : {}),
                  ...(payload !== undefined ? { payload } : {}),
                  data,
                  ...(parsedBody.blurImages !== undefined ? { blurImages: parsedBody.blurImages } : {}),
                  ...(parsedBody.metadata ? { metadata: parsedBody.metadata } : {})
                };
                const sentCount = broadcast(targetClients, message);
                console.log('[WebSocket] 消息已广播:', { type, sentCount });
              } catch (err) {
                console.error('[WebSocket] 广播消息失败:', err);
              }
            });

          } catch (err) {
            console.error('[WebSocket] /api/ws/send 解析失败:', err);
            res.statusCode = 400;
            res.setHeader('Content-Type', 'application/json; charset=utf-8');
            res.end(JSON.stringify({ error: 'invalid json body' }));
          }
        });
      });

      // 服务器关闭时清理
      server.httpServer?.on('close', () => {
        if (wss) {
          wss.close();
          clients.clear();
          console.log('[WebSocket] 服务器已关闭');
        }

        server.httpServer?.off?.('upgrade', handleUpgrade);
      });
    }
  };
}

/**
 * 处理 WebSocket 消息
 */
function handleMessage(
  ws: WebSocket, 
  message: WebSocketMessage, 
  clients: Set<WebSocket>,
  context: HandleMessageContext
) {
  switch (message.type) {
    case 'identify':
      // 客户端身份识别（支持连接后再发送身份信息）
      {
        const meta = Array.from(clients).find(c => c === ws);
        if (meta) {
          const clientInfo = context.clientMeta.get(ws);
          if (clientInfo) {
            clientInfo.type = message.client || clientInfo.type;
            clientInfo.version = message.version || clientInfo.version;
            context.clientMeta.set(ws, clientInfo);
            console.log('[WebSocket] 客户端已识别:', {
              id: clientInfo.id,
              type: clientInfo.type,
              version: clientInfo.version
            });
          }
        }
        ws.send(JSON.stringify({ 
          type: 'identified',
          message: '身份识别成功'
        }));
      }
      break;

    case 'ping':
      ws.send(JSON.stringify({ type: 'pong' }));
      break;

    case 'broadcast':
      // 广播消息给所有客户端
      broadcast(clients, {
        type: 'broadcast',
        data: message.data
      });
      break;

    case 'echo':
      // 回显消息
      ws.send(JSON.stringify({
        type: 'echo',
        data: message.data
      }));
      break;
    case 'chrome-export:init':
      {
        const data = (message.data ?? message.payload ?? {}) as any;
        const transferId = String(data.transferId || '').trim();
        const pageName = String(data.pageName || '').trim();
        const displayName = data.displayName !== undefined ? String(data.displayName).trim() : undefined;
        const mode = data.mode === 'files' ? 'files' : 'zip';
        const totalChunks = Number(data.totalChunks);
        const totalBytes = typeof data.totalBytes === 'number' ? data.totalBytes : undefined;
        const fileNameRaw = String(data.fileName || 'chrome-export.zip');
        const fileName = path.basename(fileNameRaw || 'chrome-export.zip');
        const outputRelativeDir = resolveOutputRelativeDir(data, pageName);

        if (!transferId || !isSafeName(transferId)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', message: 'transferId is invalid' });
        }
        if (!pageName || !isSafeName(pageName)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'pageName is invalid' });
        }
        if (!isValidDisplayName(displayName)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'displayName is invalid' });
        }
        if (!outputRelativeDir) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'output path is invalid' });
        }
        if (mode === 'zip' && (!Number.isFinite(totalChunks) || totalChunks <= 0)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'totalChunks is invalid' });
        }
        if (context.uploadSessions.has(transferId)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'transferId already exists' });
        }

        const transferDir = path.join(context.projectRoot, 'temp', 'chrome-export', transferId);
        const filesRoot = mode === 'files' ? path.join(transferDir, 'files') : undefined;
        if (filesRoot) {
          ensureDir(filesRoot);
        }

        const session: UploadSession = {
          transferId,
          pageName,
          displayName,
          outputRelativeDir,
          fileName,
          mode,
          totalChunks,
          totalBytes,
          receivedChunks: 0,
          receivedBytes: 0,
          chunks: new Map(),
          filesRoot,
          filesReceived: 0,
          startedAt: Date.now()
        };

        context.uploadSessions.set(transferId, session);
        sendWsMessage(ws, { type: 'chrome-export:ack', transferId });
      }
      break;
    case 'chrome-export:chunk':
      {
        const data = (message.data ?? message.payload ?? {}) as any;
        const transferId = String(data.transferId || '').trim();
        const chunkIndex = Number(data.index);
        const chunkData = data.data;

        if (!transferId || !context.uploadSessions.has(transferId)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'unknown transferId' });
        }
        const session = context.uploadSessions.get(transferId)!;
        if (session.mode !== 'zip') {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'chunk not allowed for files mode' });
        }
        if (!Number.isFinite(chunkIndex) || chunkIndex < 0) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'invalid chunk index' });
        }
        if (typeof chunkData !== 'string' || !chunkData) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'invalid chunk data' });
        }
        if (chunkIndex >= session.totalChunks) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'chunk index out of range' });
        }

        if (!session.chunks.has(chunkIndex)) {
          const buffer = Buffer.from(chunkData, 'base64');
          session.chunks.set(chunkIndex, buffer);
          session.receivedChunks = session.chunks.size;
          session.receivedBytes += buffer.byteLength;
        }

        sendWsMessage(ws, {
          type: 'chrome-export:progress',
          transferId,
          receivedChunks: session.receivedChunks,
          totalChunks: session.totalChunks,
          receivedBytes: session.receivedBytes,
          totalBytes: session.totalBytes
        });
      }
      break;
    case 'chrome-export:file':
      {
        const data = (message.data ?? message.payload ?? {}) as any;
        const transferId = String(data.transferId || '').trim();
        const relativePath = String(data.path || data.relativePath || '').trim();
        const fileData = data.data;

        if (!transferId || !context.uploadSessions.has(transferId)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'unknown transferId' });
        }
        const session = context.uploadSessions.get(transferId)!;
        if (session.mode !== 'files') {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'file not allowed for zip mode' });
        }
        if (!isSafeRelativePath(relativePath)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'invalid file path' });
        }
        if (typeof fileData !== 'string' || !fileData) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'invalid file data' });
        }
        if (!session.filesRoot) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'files root not ready' });
        }

        const targetPath = path.join(session.filesRoot, relativePath);
        ensureDir(path.dirname(targetPath));

        const buffer = Buffer.from(fileData, 'base64');
        fs.writeFileSync(targetPath, buffer);
        session.filesReceived += 1;

        sendWsMessage(ws, {
          type: 'chrome-export:progress',
          transferId,
          filesReceived: session.filesReceived
        });
      }
      break;
    case 'chrome-export:complete':
      {
        const data = (message.data ?? message.payload ?? {}) as any;
        const transferId = String(data.transferId || '').trim();

        if (!transferId || !context.uploadSessions.has(transferId)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'unknown transferId' });
        }

        const session = context.uploadSessions.get(transferId)!;
        const inboxRoot = path.join(context.projectRoot, 'temp', 'chrome-export');
        const transferDir = path.join(inboxRoot, transferId);
        const extractDir = path.join(transferDir, 'extract');

        if (session.mode === 'zip') {
          const missing: number[] = [];
          for (let i = 0; i < session.totalChunks; i += 1) {
            if (!session.chunks.has(i)) {
              missing.push(i);
            }
          }

          if (missing.length > 0) {
            return sendWsMessage(ws, {
              type: 'chrome-export:error',
              transferId,
              message: 'missing chunks',
              missing
            });
          }

          const orderedBuffers: Buffer[] = new Array(session.totalChunks);
          for (let i = 0; i < session.totalChunks; i += 1) {
            orderedBuffers[i] = session.chunks.get(i)!;
          }

          const zipBuffer = Buffer.concat(orderedBuffers);
          const zipPath = path.join(transferDir, session.fileName);

          ensureDir(transferDir);
          fs.writeFileSync(zipPath, zipBuffer);

          if (fs.existsSync(extractDir)) {
            fs.rmSync(extractDir, { recursive: true, force: true });
          }
          ensureDir(extractDir);

          sendWsMessage(ws, { type: 'chrome-export:status', transferId, stage: 'extracting' });

          extractZip(zipPath, { dir: extractDir })
          .then(() => {
            const inferred = inferExtractedRootFolder(extractDir);
            if (inferred.entryCount === 0) {
              throw new Error('empty zip');
            }
            const sourceDir = inferred.hasRootFolder
              ? path.join(extractDir, inferred.rootFolderName)
              : extractDir;

            const outputName = session.pageName;
            if (!isSafeName(outputName)) {
              throw new Error('invalid pageName');
            }

            const scriptPath = path.join(context.projectRoot, 'scripts', 'chrome-export-converter.mjs');
            const commandArgs = [scriptPath, sourceDir, outputName];
            if (session.displayName) {
              commandArgs.push('--display-name', session.displayName);
            }
            commandArgs.push('--target-dir', session.outputRelativeDir);

            sendWsMessage(ws, { type: 'chrome-export:status', transferId, stage: 'importing' });

            void runCommand({
              command: nodeCommand,
              args: commandArgs,
              cwd: context.projectRoot,
              capture: true,
            }).then((result) => {
              if (result.code !== 0) {
                sendWsMessage(ws, {
                  type: 'chrome-export:error',
                  transferId,
                  message: result.stderr || result.stdout || 'import failed'
                });
              } else {
                const outputDir = resolvePrototypeOutputDir(context.projectRoot, session.outputRelativeDir);
                sendWsMessage(ws, {
                  type: 'chrome-export:done',
                  transferId,
                  pageName: outputName,
                  displayName: session.displayName,
                  outputRelativeDir: session.outputRelativeDir,
                  sourceDir,
                  outputDir,
                  stdout: result.stdout ? String(result.stdout).trim() : undefined,
                  stderr: result.stderr ? String(result.stderr).trim() : undefined
                });
                if (fs.existsSync(transferDir)) {
                  fs.rmSync(transferDir, { recursive: true, force: true });
                }
                context.uploadSessions.delete(transferId);
              }
            }).catch((error: any) => {
              sendWsMessage(ws, {
                type: 'chrome-export:error',
                transferId,
                message: error?.message || 'import failed'
              });
            });
          })
          .catch((error: any) => {
            sendWsMessage(ws, {
              type: 'chrome-export:error',
              transferId,
              message: error?.message || 'extract failed'
            });
          });
        } else {
          if (!session.filesRoot) {
            return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'files root not ready' });
          }

          const sourceDir = session.filesRoot;
          const outputName = session.pageName;
          if (!isSafeName(outputName)) {
            return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'invalid pageName' });
          }

          const scriptPath = path.join(context.projectRoot, 'scripts', 'chrome-export-converter.mjs');
          const commandArgs = [scriptPath, sourceDir, outputName];
          if (session.displayName) {
            commandArgs.push('--display-name', session.displayName);
          }
          commandArgs.push('--target-dir', session.outputRelativeDir);

          sendWsMessage(ws, { type: 'chrome-export:status', transferId, stage: 'importing' });

          void runCommand({
            command: nodeCommand,
            args: commandArgs,
            cwd: context.projectRoot,
            capture: true,
          }).then((result) => {
            if (result.code !== 0) {
              sendWsMessage(ws, {
                type: 'chrome-export:error',
                transferId,
                message: result.stderr || result.stdout || 'import failed'
              });
            } else {
              const outputDir = resolvePrototypeOutputDir(context.projectRoot, session.outputRelativeDir);
              sendWsMessage(ws, {
                type: 'chrome-export:done',
                transferId,
                pageName: outputName,
                displayName: session.displayName,
                outputRelativeDir: session.outputRelativeDir,
                sourceDir,
                outputDir,
                stdout: result.stdout ? String(result.stdout).trim() : undefined,
                stderr: result.stderr ? String(result.stderr).trim() : undefined
              });
              if (fs.existsSync(transferDir)) {
                fs.rmSync(transferDir, { recursive: true, force: true });
              }
              context.uploadSessions.delete(transferId);
            }
          }).catch((error: any) => {
            sendWsMessage(ws, {
              type: 'chrome-export:error',
              transferId,
              message: error?.message || 'import failed'
            });
          });
        }
      }
      break;
    case 'chrome-export:abort':
      {
        const data = (message.data ?? message.payload ?? {}) as any;
        const transferId = String(data.transferId || '').trim();
        if (!transferId || !context.uploadSessions.has(transferId)) {
          return sendWsMessage(ws, { type: 'chrome-export:error', transferId, message: 'unknown transferId' });
        }
        context.uploadSessions.delete(transferId);
        sendWsMessage(ws, { type: 'chrome-export:aborted', transferId });
      }
      break;

    default:
      // 未知消息类型，可以在这里添加自定义处理逻辑
      console.log('[WebSocket] 未处理的消息类型:', message.type);
      ws.send(JSON.stringify({
        type: 'unknown',
        message: `未知的消息类型: ${message.type}`
      }));
  }
}

/**
 * 广播消息给所有连接的客户端
 */
function broadcast(clients: Set<WebSocket>, message: WebSocketMessage) {
  const data = JSON.stringify(message);
  let count = 0;
  clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
      count += 1;
    }
  });
  return count;
}
