/**
 * auto-updater.js — POS 收银台自动更新模块
 *
 * 功能：
 * 1. 启动时后台检查 version.json（5s 超时，不阻塞）
 * 2. 50KB/s 限速下载 web.zip，支持断点续传 (HTTP Range)
 * 3. 下载完成后解压到 pending/ 目录
 * 4. 下次启动弹原生对话框 → 用户确认后切换版本
 * 5. 强制更新：force_update=true → 无"稍后"按钮
 * 6. 崩溃检测：crash_flag 残留 → 自动回退到 previous/
 *
 * 目录结构 (%APPDATA%/ALD-POS/)：
 *   current/  ← 当前使用的 web 文件（HTTP Server 从这里读）
 *   pending/  ← 已下载，待用户确认的应用
 *   previous/ ← 上一个版本，回退用
 *   downloading/web.zip.part ← 下载中的临时文件（断点续传）
 *   current_version.txt
 *   pending_version.txt
 *   crash_flag          ← 启动时创建，正常退出时删除
 */

const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const crypto = require('crypto');
const { app, dialog, session } = require('electron');
const { execSync } = require('child_process');

// ── 配置 ──────────────────────────────────────────
const UPDATE_URL = 'https://dl.adlaldost.online/pos-updates/version.json';
const THROTTLE_BPS = 0; // 设为0则不限速
const VERSION_TIMEOUT = 15000; // version.json 请求超时 15s（新疆网络）
const DOWNLOAD_TIMEOUT = 600000; // 下载 10 分钟超时

// ── 路径 ──────────────────────────────────────────
const DATA_DIR = path.join(app.getPath('appData'), 'ALD-POS');
const CURRENT_DIR = path.join(DATA_DIR, 'current');
const PENDING_DIR = path.join(DATA_DIR, 'pending');
const PREVIOUS_DIR = path.join(DATA_DIR, 'previous');
const DOWNLOAD_DIR = path.join(DATA_DIR, 'downloading');
const CURRENT_VERSION_FILE = path.join(DATA_DIR, 'current_version.txt');
const PENDING_VERSION_FILE = path.join(DATA_DIR, 'pending_version.txt');
const CRASH_FLAG = path.join(DATA_DIR, 'crash_flag');
const PART_FILE = path.join(DOWNLOAD_DIR, 'web.zip.part');

// ── 工具函数 ──────────────────────────────────────

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

function readVersionFile(filePath) {
  try { return fs.readFileSync(filePath, 'utf8').trim(); } catch { return null; }
}

function writeVersionFile(filePath, version) {
  ensureDir(path.dirname(filePath));
  fs.writeFileSync(filePath, version, 'utf8');
}

function deleteDir(dir) {
  if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
}

function copyDir(src, dst) {
  deleteDir(dst);
  ensureDir(dst);
  copyRecursive(src, dst);
}

// 解压后递归定位 web 根（容忍嵌套结构，如 win-unpacked/resources/app/build/web/index.html）
function findFileDir(root, filename, maxDepth = 8) {
  const queue = [{ dir: root, depth: 0 }];
  while (queue.length) {
    const { dir, depth } = queue.shift();
    let entries;
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
    catch { continue; }
    if (entries.some(e => e.isFile() && e.name === filename)) return dir;
    if (depth < maxDepth) {
      for (const e of entries) {
        if (e.isDirectory()) queue.push({ dir: path.join(dir, e.name), depth: depth + 1 });
      }
    }
  }
  return null;
}

// 把 srcDir 内所有条目上移到 dstDir（遇同名目录则合并），清空 srcDir 链
function moveContentsUp(srcDir, dstDir) {
  if (!fs.existsSync(srcDir)) return;
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
  for (const e of entries) {
    const from = path.join(srcDir, e.name);
    const to = path.join(dstDir, e.name);
    if (fs.existsSync(to)) {
      if (e.isDirectory()) { moveContentsUp(from, to); continue; }
      try { fs.rmSync(to, { force: true }); } catch {}
    }
    try { fs.renameSync(from, to); }
    catch (err) { console.error('[Updater] moveContentsUp failed:', from, err.message); }
  }
}

// Recursive copy using readdirSync + readFileSync (works inside ASAR)
function copyRecursive(src, dst) {
  if (!fs.existsSync(src)) {
    console.error('[Updater] copyRecursive: source not found:', src);
    return;
  }
  ensureDir(dst);
  const entries = fs.readdirSync(src, { withFileTypes: true });
  for (const entry of entries) {
    const srcPath = path.join(src, entry.name);
    const dstPath = path.join(dst, entry.name);
    if (entry.isDirectory()) {
      copyRecursive(srcPath, dstPath);
    } else if (entry.isFile()) {
      try {
        fs.writeFileSync(dstPath, fs.readFileSync(srcPath));
      } catch (e) {
        console.error('[Updater] copyRecursive: failed to copy', srcPath, e.message);
      }
    }
  }
}

function fileSize(filePath) {
  try { return fs.statSync(filePath).size; } catch { return 0; }
}

// ── HTTP GET (returns Promise<{status, body, headers}>) ──

function httpGet(url, opts = {}) {
  const { timeout = 10000, headers = {} } = opts;
  const mod = url.startsWith('https') ? https : http;

  return new Promise((resolve, reject) => {
    const req = mod.get(url, { timeout, headers }, (res) => {
      // Follow redirects (up to 3)
      if ([301, 302, 307, 308].includes(res.statusCode)) {
        const loc = res.headers.location;
        if (loc && opts._redirectCount < 3) {
          return resolve(httpGet(loc, { ...opts, _redirectCount: (opts._redirectCount || 0) + 1 }));
        }
      }

      const chunks = [];
      res.on('data', (c) => chunks.push(c));
      res.on('end', () => {
        resolve({
          status: res.statusCode,
          body: Buffer.concat(chunks),
          headers: res.headers,
        });
      });
      res.on('error', reject);
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
  });
}

// ── MD5 文件校验 ──────────────────────────────────

function md5File(filePath) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash('md5');
    const stream = fs.createReadStream(filePath);
    stream.on('data', (d) => hash.update(d));
    stream.on('end', () => resolve(hash.digest('hex')));
    stream.on('error', reject);
  });
}

// ── 限速下载 + 断点续传 ───────────────────────────

function downloadWithResume(url, savePath, expectedSize) {
  return new Promise((resolve, reject) => {
    const mod = url.startsWith('https') ? https : http;
    const downloaded = fileSize(savePath);

    const headers = {};
    if (downloaded > 0) {
      headers['Range'] = `bytes=${downloaded}-`;
    }

    let totalDownloaded = downloaded;
    let lastTick = Date.now();
    let bytesThisTick = 0;
    let resolved = false;

    const req = mod.get(url, {
      headers,
      timeout: DOWNLOAD_TIMEOUT,
    }, (res) => {
      // Handle 206 Partial Content (resume) or 200 OK (fresh)
      if (res.statusCode === 200 && downloaded > 0) {
        // Server doesn't support resume — restart
        totalDownloaded = 0;
      } else if (res.statusCode !== 206 && res.statusCode !== 200) {
        if (!resolved) { resolved = true; reject(new Error(`HTTP ${res.statusCode}`)); }
        return;
      }

      const writeStream = fs.createWriteStream(savePath, {
        flags: totalDownloaded === 0 ? 'w' : 'a',
      });

      res.on('data', (chunk) => {
        // Throttle (skip if THROTTLE_BPS is 0)
        if (THROTTLE_BPS > 0) {
          bytesThisTick += chunk.length;
          const elapsed = Date.now() - lastTick;
          if (elapsed >= 100) {
            const rate = bytesThisTick / (elapsed / 1000);
            bytesThisTick = 0;
            lastTick = Date.now();
            if (rate > THROTTLE_BPS) {
              res.pause();
              const waitMs = Math.ceil((rate / THROTTLE_BPS - 1) * elapsed);
              setTimeout(() => { res.resume(); }, Math.min(waitMs, 1000));
            }
          }
        }
      });

      res.pipe(writeStream);

      writeStream.on('finish', () => {
        if (!resolved) {
          resolved = true;
          totalDownloaded = fileSize(savePath);
          if (expectedSize && totalDownloaded !== expectedSize) {
            reject(new Error(`Size mismatch: got ${totalDownloaded}, expected ${expectedSize}`));
          } else {
            resolve(totalDownloaded);
          }
        }
      });

      writeStream.on('error', (e) => {
        if (!resolved) { resolved = true; reject(e); }
      });
    });

    req.on('error', (e) => {
      if (!resolved) { resolved = true; reject(e); }
    });
    req.on('timeout', () => {
      req.destroy();
      if (!resolved) { resolved = true; reject(new Error('download timeout')); }
    });
  });
}

// ── 解压 ZIP ──────────────────────────────────────

function extractZip(zipPath, destDir) {
  deleteDir(destDir);
  ensureDir(destDir);

  // Expand-Archive 拒绝非 .zip 扩展名（如 web.zip.part），
  // 先复制成临时 .zip 文件再解压，避免 "不是支持的存档文件格式" 错误。
  let effectivePath = zipPath;
  let tempZip = null;
  if (path.extname(zipPath) !== '.zip') {
    tempZip = zipPath + '.tmp.zip';
    try {
      fs.copyFileSync(zipPath, tempZip);
      effectivePath = tempZip;
      console.log('[Updater] Copied', path.basename(zipPath), '-> temp .zip for Expand-Archive');
    } catch (copyErr) {
      console.error('[Updater] Failed to copy to temp .zip:', copyErr.message);
      // 继续用原路径，靠 tar fallback
    }
  }

  try {
    execSync(`powershell -Command "Expand-Archive -Path '${effectivePath}' -DestinationPath '${destDir}' -Force"`, {
      stdio: 'pipe',
      timeout: 60000,
    });
  } catch (e) {
    // Fallback: try tar (Windows 10 1803+ built-in)
    try {
      execSync(`tar -xf "${effectivePath}" -C "${destDir}"`, { stdio: 'pipe', timeout: 60000 });
    } catch (e2) {
      // Clean up temp
      if (tempZip) { try { fs.unlinkSync(tempZip); } catch {} }
      throw new Error(`Unzip failed: ${e.message}; tar fallback: ${e2.message}`);
    }
  }

  // Clean up temp .zip
  if (tempZip) { try { fs.unlinkSync(tempZip); } catch {} }

  // 容忍嵌套结构：若 index.html 不在根目录，把真正的 web 根上移到 destDir。
  // 这样无论上传的是 web 结构包（index.html 在根）还是全量嵌套包
  // （win-unpacked/resources/app/build/web/index.html）都能正确解压，避免"更新失败"。
  const webRoot = findFileDir(destDir, 'index.html');
  if (webRoot && webRoot !== destDir) {
    console.log('[Updater] Hoisting web root from', webRoot, '->', destDir);
    moveContentsUp(webRoot, destDir);
  }

  // Verify index.html exists at root
  if (!fs.existsSync(path.join(destDir, 'index.html'))) {
    throw new Error('Extracted archive missing index.html — corrupt or wrong file');
  }
}

// ═══════════════════════════════════════════════════
//  公开 API
// ═══════════════════════════════════════════════════

const AutoUpdater = {
  /**
   * 初始化：创建目录、写 crash_flag
   * 应在 app.whenReady() 最开始调用
   */
  init() {
    ensureDir(CURRENT_DIR);
    ensureDir(path.dirname(PART_FILE));
    // Write crash flag — removed on normal exit
    fs.writeFileSync(CRASH_FLAG, Date.now().toString(), 'utf8');
  },

  /**
   * 正常退出时清理 crash_flag
   */
  clearCrashFlag() {
    try { if (fs.existsSync(CRASH_FLAG)) fs.unlinkSync(CRASH_FLAG); } catch {}
  },

  /**
   * 检查上次是否崩溃。如果崩溃 → 回退到 previous/
   * @returns {string|null} 回退信息，无崩溃返回 null
   */
  checkCrashRecovery() {
    if (!fs.existsSync(CRASH_FLAG)) return null;
    if (!fs.existsSync(PREVIOUS_DIR)) {
      // No previous version to fallback to — delete crash flag
      try { fs.unlinkSync(CRASH_FLAG); } catch {}
      return null;
    }

    console.log('[Updater] Crash detected — rolling back to previous version');
    deleteDir(CURRENT_DIR);
    copyDir(PREVIOUS_DIR, CURRENT_DIR);

    // Copy previous version file
    const prevVer = path.join(PREVIOUS_DIR, '.version');
    const prevVerTxt = readVersionFile(prevVer);
    if (prevVerTxt) {
      writeVersionFile(CURRENT_VERSION_FILE, prevVerTxt);
    }

    // Delete crash flag
    try { fs.unlinkSync(CRASH_FLAG); } catch {}
    return prevVerTxt || 'previous';
  },

  /**
   * 仅检查版本号（不下载）
   * @returns {Promise<object|null>} manifest or null
   */
  async checkUpdateOnly() {
    const currentVer = readVersionFile(CURRENT_VERSION_FILE);
    let manifest;
    try {
      const res = await httpGet(UPDATE_URL, { timeout: VERSION_TIMEOUT });
      manifest = JSON.parse(res.body.toString('utf8'));
    } catch (e) {
      console.log('[Updater] checkUpdateOnly failed:', e.message);
      return null;
    }
    if (!manifest.version) return null;
    if (currentVer === manifest.version) {
      if (fs.existsSync(PENDING_VERSION_FILE)) {
        const pendVer = readVersionFile(PENDING_VERSION_FILE);
        if (pendVer === manifest.version) {
          deleteDir(PENDING_DIR);
          try { fs.unlinkSync(PENDING_VERSION_FILE); } catch {}
        }
      }
      return null;
    }
    // Already pending?
    const pendVer = readVersionFile(PENDING_VERSION_FILE);
    if (pendVer === manifest.version && fs.existsSync(path.join(PENDING_DIR, 'index.html'))) {
      return { ...manifest, alreadyDownloaded: true };
    }
    return manifest;
  },

  /**
   * 后台检查更新（不阻塞启动）
   * - 下载完成后解压到 pending/
   * - 写 pending_version.txt
   * @returns {Promise<object|null>} 更新信息 或 null
   */
  async checkForUpdate() {
    const currentVer = readVersionFile(CURRENT_VERSION_FILE);

    let manifest;
    try {
      const res = await httpGet(UPDATE_URL, { timeout: VERSION_TIMEOUT });
      manifest = JSON.parse(res.body.toString('utf8'));
    } catch (e) {
      console.log('[Updater] version.json fetch failed (will retry next launch):', e.message);
      return null;
    }

    if (!manifest.version) {
      console.log('[Updater] No version published yet');
      return null;
    }

    if (currentVer === manifest.version) {
      console.log('[Updater] Already up to date:', currentVer);
      // Clean up pending if already applied
      if (fs.existsSync(PENDING_VERSION_FILE)) {
        const pendVer = readVersionFile(PENDING_VERSION_FILE);
        if (pendVer === manifest.version) {
          deleteDir(PENDING_DIR);
          try { fs.unlinkSync(PENDING_VERSION_FILE); } catch {}
        }
      }
      return null;
    }

    // Check if already downloaded
    const pendVer = readVersionFile(PENDING_VERSION_FILE);
    if (pendVer === manifest.version && fs.existsSync(path.join(PENDING_DIR, 'index.html'))) {
      console.log('[Updater] Update already pending:', manifest.version);
      return { ...manifest, alreadyDownloaded: true };
    }

    console.log('[Updater] New version available:', manifest.version);

    // Download in background
    try {
      ensureDir(DOWNLOAD_DIR);

      // Use COS CDN URL from manifest for max download speed, fallback to nginx
      const zipUrl = manifest.download_url || `https://dl.adlaldost.online/pos-updates/${manifest.zip}`;
      console.log('[Updater] Downloading from COS:', zipUrl);

      await downloadWithResume(zipUrl, PART_FILE, manifest.size);

      // MD5 verify
      const actualMd5 = await md5File(PART_FILE);
      if (manifest.md5 && actualMd5 !== manifest.md5) {
        console.error('[Updater] MD5 mismatch — deleting download');
        try { fs.unlinkSync(PART_FILE); } catch {}
        return null;
      }

      console.log('[Updater] Download complete, extracting...');
      extractZip(PART_FILE, PENDING_DIR);

      // Clean up zip
      try { fs.unlinkSync(PART_FILE); } catch {}

      // Write pending version info
      writeVersionFile(PENDING_VERSION_FILE, manifest.version);
      // Also save metadata in pending dir
      ensureDir(PENDING_DIR);
      fs.writeFileSync(path.join(PENDING_DIR, '.version'), manifest.version, 'utf8');
      fs.writeFileSync(path.join(PENDING_DIR, '.meta.json'), JSON.stringify({
        version: manifest.version,
        changes: manifest.changes || [],
        force_update: manifest.force_update || false,
        downloaded_at: new Date().toISOString(),
      }), 'utf8');

      console.log('[Updater] Update ready for next launch:', manifest.version);
      return { ...manifest, alreadyDownloaded: false };

    } catch (e) {
      console.error('[Updater] Download/extract failed:', e.message);
      // Don't leave partial downloads
      try { fs.unlinkSync(PART_FILE); } catch {}
      return null;
    }
  },

  /**
   * 显示更新对话框。
   * 应在启动加载 POS 页面之前调用。
   * @param {BrowserWindow} parentWindow — 父窗口（用于 dialog 模态）
   * @returns {'updated'|'skipped'|'none'} 结果
   */
  async showUpdateDialog(parentWindow) {
    if (!fs.existsSync(PENDING_VERSION_FILE)) return 'none';
    if (!fs.existsSync(path.join(PENDING_DIR, 'index.html'))) return 'none';

    const pendVer = readVersionFile(PENDING_VERSION_FILE);
    const currentVer = readVersionFile(CURRENT_VERSION_FILE) || '(首次安装)';

    let meta = { changes: [], force_update: false };
    try {
      meta = JSON.parse(fs.readFileSync(path.join(PENDING_DIR, '.meta.json'), 'utf8'));
    } catch {}

    const changesText = meta.changes && meta.changes.length > 0
      ? '\n' + meta.changes.map((c) => `  • ${c}`).join('\n')
      : '\n  (无更新说明)';

    if (meta.force_update) {
      // Force update — no skip button
      await dialog.showMessageBox(parentWindow, {
        type: 'warning',
        title: 'ALD POS — 必须更新',
        message: `检测到重要更新，必须更新后才能继续使用。`,
        detail: `当前版本：${currentVer}\n最新版本：${pendVer}\n\n更新内容：${changesText}\n\n系统将在点击确定后自动重启。`,
        buttons: ['立即更新'],
        defaultId: 0,
        cancelId: 0,
      });
      this.applyUpdate();
      return 'updated';
    }

    // Optional update
    const { response } = await dialog.showMessageBox(parentWindow, {
      type: 'info',
      title: 'ALD POS — 新版本可用',
      message: '是否更新到最新版本？',
      detail: `当前版本：${currentVer}\n最新版本：${pendVer}\n\n更新内容：${changesText}\n\n选择"立即更新"将自动重启应用。`,
      buttons: ['立即更新', '稍后提醒'],
      defaultId: 0,
      cancelId: 1,
    });

    if (response === 0) {
      this.applyUpdate();
      return 'updated';
    }
    return 'skipped';
  },

  /**
   * 应用更新：previous ← current, current ← pending, 重启
   */
  applyUpdate() {
    console.log('[Updater] Applying update...');

    // Move current → previous
    if (fs.existsSync(CURRENT_DIR)) {
      deleteDir(PREVIOUS_DIR);
      fs.renameSync(CURRENT_DIR, PREVIOUS_DIR);
    }

    // Move pending → current
    if (fs.existsSync(PENDING_DIR)) {
      fs.renameSync(PENDING_DIR, CURRENT_DIR);
    }

    // Update version files
    const pendVer = readVersionFile(PENDING_VERSION_FILE);
    if (pendVer) {
      writeVersionFile(CURRENT_VERSION_FILE, pendVer);
    }
    try { fs.unlinkSync(PENDING_VERSION_FILE); } catch {}

    // Clear crash flag before restart
    this.clearCrashFlag();

    // 清除浏览器缓存（Service Worker / Cache Storage / IndexedDB 等），
    // 避免旧 SW 缓存把新 main.dart.js 替换为旧版本 → 更新后 UI 不生效。
    try {
      session.defaultSession.clearCache();
      session.defaultSession.clearStorageData({
        storages: ['caches', 'serviceworkers', 'indexdb', 'localstorage'],
      });
      console.log('[Updater] Browser caches cleared for clean update');
    } catch(e) {
      console.error('[Updater] Cache clear failed:', e.message);
    }

    // Restart
    console.log('[Updater] Restarting...');
    setImmediate(() => {
      app.relaunch();
      app.exit(0);
    });
  },

  /**
   * 实时进度下载（不限速 — 用户正在等待）
   * @param {object} manifest
   * @param {function} sendProgress - ({stage, percent, message}) => void
   */
  async downloadWithProgress(manifest, sendProgress) {
    const zipUrl = manifest.download_url || `https://dl.adlaldost.online/pos-updates/${manifest.zip}`;
    const mod = zipUrl.startsWith('https') ? https : http;
    const totalSize = manifest.size || 0;

    sendProgress({ stage: 'downloading', percent: 0, message: 'Connecting...' });

    return new Promise((resolve, reject) => {
      ensureDir(DOWNLOAD_DIR);
      let downloaded = fileSize(PART_FILE);

      // 如果旧 part 文件比新文件还大（换了不同大小的 zip），删掉重新下载
      if (totalSize > 0 && downloaded > totalSize) {
        console.log('[Updater] Stale part file (' + downloaded + 'B) > expected (' + totalSize + 'B), deleting');
        try { fs.unlinkSync(PART_FILE); } catch {}
        downloaded = 0;
      }

      const headers = {};
      if (downloaded > 0) headers['Range'] = `bytes=${downloaded}-`;

      let totalDownloaded = downloaded;
      let resolved = false;

      const req = mod.get(zipUrl, { headers, timeout: DOWNLOAD_TIMEOUT }, (res) => {
        if (res.statusCode === 200 && downloaded > 0) totalDownloaded = 0;
        else if (res.statusCode !== 206 && res.statusCode !== 200) {
          if (!resolved) { resolved = true; reject(new Error(`HTTP ${res.statusCode}`)); }
          return;
        }

        const contentLength = parseInt(res.headers['content-length'] || '0', 10);
        const effectiveSize = contentLength > 0
          ? contentLength + (res.statusCode === 206 ? downloaded : 0)
          : totalSize;

        const flags = totalDownloaded === 0 ? 'w' : 'a';
        const ws = fs.createWriteStream(PART_FILE, { flags });
        let lastReport = Date.now();

        res.on('data', (chunk) => {
          totalDownloaded += chunk.length;
          const now = Date.now();
          if (now - lastReport > 200) {
            lastReport = now;
            const pct = effectiveSize > 0 ? Math.round((totalDownloaded / effectiveSize) * 100) : 0;
            const mbDown = (totalDownloaded / 1024 / 1024).toFixed(1);
            const mbTotal = effectiveSize > 0 ? (effectiveSize / 1024 / 1024).toFixed(1) : '?';
            sendProgress({
              stage: 'downloading',
              percent: Math.min(pct, 99),
              message: `${mbDown}MB / ${mbTotal}MB`,
            });
          }
        });

        res.pipe(ws);

        ws.on('finish', () => {
          if (!resolved) {
            resolved = true;
            sendProgress({ stage: 'downloading', percent: 100, message: 'Download complete' });
            resolve(fileSize(PART_FILE));
          }
        });
        ws.on('error', (e) => { if (!resolved) { resolved = true; reject(e); } });
      });

      req.on('error', (e) => { if (!resolved) { resolved = true; reject(e); } });
      req.on('timeout', () => { req.destroy(); if (!resolved) { resolved = true; reject(new Error('download timeout')); } });
    });
  },

  /**
   * 完整更新流程（登录页触发）：
   * check → download(progress) → verify → extract → apply → restart
   * @param {BrowserWindow} win — main window for sending progress
   */
  async runUpdateFlow(win) {
    const send = (data) => {
      try {
        if (win && !win.isDestroyed()) {
          win.webContents.send('update:progress', data);
        }
      } catch {}
    };

    try {
      // 1. Check
      send({ stage: 'checking', percent: 0, message: 'Checking for updates...' });
      const manifest = await this.checkUpdateOnly();

      if (!manifest) {
        send({ stage: 'uptodate', percent: 100, message: 'Already up to date' });
        return;
      }

      if (manifest.alreadyDownloaded) {
        send({ stage: 'installing', percent: 100, message: 'Installing update...', version: manifest.version });
        await new Promise(r => setTimeout(r, 500));
        this.applyUpdate();
        return;
      }

      // 2. Download
      const forceUpdate = manifest.force_update || false;
      send({
        stage: 'downloading',
        percent: 0,
        message: 'Starting download...',
        forceUpdate,
        version: manifest.version,
        changes: manifest.changes || [],
      });
      await this.downloadWithProgress(manifest, (p) => send({ ...p, forceUpdate, version: manifest.version }));

      // 3. Verify MD5
      send({ stage: 'verifying', percent: 100, message: 'Verifying...', forceUpdate, version: manifest.version });
      const actualMd5 = await md5File(PART_FILE);
      if (manifest.md5 && actualMd5 !== manifest.md5) {
        send({ stage: 'error', percent: 0, message: 'Download verification failed', forceUpdate, version: manifest.version });
        try { fs.unlinkSync(PART_FILE); } catch {}
        return;
      }

      // 4. Extract
      send({ stage: 'extracting', percent: 100, message: 'Extracting...', forceUpdate, version: manifest.version });
      extractZip(PART_FILE, PENDING_DIR);
      try { fs.unlinkSync(PART_FILE); } catch {}

      // 5. Write pending metadata
      writeVersionFile(PENDING_VERSION_FILE, manifest.version);
      ensureDir(PENDING_DIR);
      fs.writeFileSync(path.join(PENDING_DIR, '.version'), manifest.version, 'utf8');
      fs.writeFileSync(path.join(PENDING_DIR, '.meta.json'), JSON.stringify({
        version: manifest.version,
        changes: manifest.changes || [],
        force_update: manifest.force_update || false,
        downloaded_at: new Date().toISOString(),
      }), 'utf8');

      // 6. Install & restart
      send({ stage: 'installing', percent: 100, message: 'Installing...', forceUpdate, version: manifest.version });
      await new Promise(r => setTimeout(r, 300));
      this.applyUpdate();

    } catch (e) {
      console.error('[Updater] runUpdateFlow error:', e);
      // 清理残留 part 文件，避免旧下载卡住下次尝试
      try { fs.unlinkSync(PART_FILE); } catch {}
      send({ stage: 'error', percent: 0, message: e.message });
    }
  },

  /**
   * 清除待安装的更新（用户选择跳过）
   */
  clearPending() {
    try { if (fs.existsSync(PENDING_VERSION_FILE)) fs.unlinkSync(PENDING_VERSION_FILE); } catch {}
    deleteDir(PENDING_DIR);
    try { if (fs.existsSync(PART_FILE)) fs.unlinkSync(PART_FILE); } catch {}
  },

  /**
   * 首次运行：如果 current/ 为空，从 packaged app 内置 web 文件初始化
   * @param {string} builtinWebRoot — packaged web 文件路径
   */
  initFirstRun(builtinWebRoot) {
    if (!fs.existsSync(path.join(CURRENT_DIR, 'index.html'))) {
      console.log('[Updater] First run — copying builtin web files');
      ensureDir(CURRENT_DIR);
      copyDir(builtinWebRoot, CURRENT_DIR);
      if (!readVersionFile(CURRENT_VERSION_FILE)) {
        writeVersionFile(CURRENT_VERSION_FILE, app.getVersion());
      }
      // 首次运行清空浏览器缓存，防止旧 SW 残留
      try {
        session.defaultSession.clearCache();
        session.defaultSession.clearStorageData({
          storages: ['caches', 'serviceworkers', 'indexdb', 'localstorage'],
        });
        console.log('[Updater] Browser caches cleared for first run');
      } catch(e) {
        console.error('[Updater] First-run cache clear failed:', e.message);
      }
    }
  },
};

module.exports = AutoUpdater;
