#!/usr/bin/env node

/**
 * MindOS interactive setup script
 *
 * Usage: npm run setup  OR  mindos onboard
 *
 * Steps:
 *   1. Choose knowledge base path → default ~/MindOS/mind (same as GUI)
 *   2. Choose template (en / zh / empty / custom) → copy to knowledge base path
 *   3. Choose ports (web + mcp) — checked for conflicts upfront
 *   4. Auth token (auto-generated or passphrase-seeded)
 *   5. Web UI password (optional)
 *   6. Choose AI provider + API Key → write ~/.mindos/config.json
 *   7. Print next steps
 *
 * Language switching:
 *   ← → keys switch UI language (en/zh) at any prompt
 *   ↑ ↓ keys navigate select options
 *   Enter confirms
 */

import { existsSync, cpSync, writeFileSync, readFileSync, mkdirSync, createWriteStream, rmSync, openSync } from 'node:fs';
import { resolve, dirname, join, relative, isAbsolute, sep as pathSep } from 'node:path';
import { homedir, tmpdir, networkInterfaces } from 'node:os';
import { fileURLToPath } from 'node:url';
import { createInterface } from 'node:readline';
import { pipeline } from 'node:stream/promises';
import { execFileSync, spawn } from 'node:child_process';
import { randomBytes, createHash } from 'node:crypto';
import { createConnection } from 'node:net';
import http from 'node:http';
import { gunzipSync } from 'node:zlib';
import { MCP_AGENTS, SKILL_AGENT_REGISTRY, detectAgentPresence } from '../packages/mindos/bin/lib/mcp-agents.js';
import { resolveNpmInvocation, resolveNpxInvocation } from '../packages/mindos/bin/lib/npm-invocation.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
const MINDOS_DIR = resolve(homedir(), '.mindos');
const CONFIG_PATH = resolve(MINDOS_DIR, 'config.json');
const LOG_PATH = resolve(MINDOS_DIR, 'mindos.log');

// ── i18n ─────────────────────────────────────────────────────────────────────

const T = {
  title:          { en: '🧠 MindOS Setup', zh: '🧠 MindOS 初始化' },
  langHint:       { en: '  ← → 切换中文    ↑ ↓ navigate    Enter confirm', zh: '  ← → switch to English    ↑ ↓ 上下切换    Enter 确认' },

  // mode selection
  modePrompt:     { en: 'How would you like to set up?', zh: '选择配置方式' },
  modeOpts:       { en: ['Continue here in terminal (CLI)', 'Open browser to set up (recommended)'], zh: ['在终端继续配置（CLI）', '打开浏览器配置（推荐）'] },
  modeVals:       ['cli', 'gui'],
  guiStarting:    { en: '⏳ Starting MindOS, please wait...', zh: '⏳ 正在启动 MindOS，请稍候...' },
  guiReady:       { en: (url) => `🌐 Opening setup in browser: ${url}`, zh: (url) => `🌐 在浏览器中打开配置页面: ${url}` },
  guiOpenFailed:  { en: (url) => `  Could not open browser automatically. Open this URL manually:\n  ${url}`, zh: (url) => `  无法自动打开浏览器，请手动访问：\n  ${url}` },

  // step labels
  step:           { en: (n, total) => `Step ${n}/${total}`, zh: (n, total) => `步骤 ${n}/${total}` },
  stepTitles:     {
    en: ['Knowledge Base', 'AI Provider', 'Ports', 'Auth Token', 'Web Password', 'Start Mode', 'Connection Mode', 'Agent Connection'],
    zh: ['知识库',         'AI 服务商',    '端口',   'Auth Token', 'Web 密码',     '启动方式',    '连接模式',          'Agent 连接'],
  },

  // path
  pathPrompt:     { en: 'Folder name or absolute path', zh: '文件夹名或绝对路径' },
  pathHintInline: { en: (base) => `stored under ${base + '/'}`, zh: (base) => `存储在 ${base + '/'}` },
  pathResolved:   { en: (p) => `  → ${c.dim(p)}`, zh: (p) => `  → ${c.dim(p)}` },

  // existing kb
  kbExists:       { en: (p) => `  ${c.yellow('⚠')} Directory already exists: ${c.dim(p)}`, zh: (p) => `  ${c.yellow('⚠')} 目录已存在：${c.dim(p)}` },
  kbExistsFiles:  { en: 'Contents', zh: '目录内容' },
  kbExistsOpts:   { en: ['Use this directory', 'Choose a different path'], zh: ['使用此目录', '重新选择路径'] },
  kbExistsVals:   ['use', 'reselect'],
  kbCreated:      { en: '✔ Knowledge base initialized', zh: '✔ 知识库已初始化' },

  // template
  tplPrompt:      { en: 'Template', zh: '模板' },
  tplOptions:     { en: ['en — English template', 'zh — Chinese template', 'empty — blank files only', 'custom — local path or URL'], zh: ['en — 英文模板', 'zh — 中文模板', 'empty — 仅基础文件', 'custom — 本地路径或 URL'] },
  tplValues:      ['en', 'zh', 'empty', 'custom'],
  tplNotFound:    { en: '✘ Template not found', zh: '✘ 模板目录不存在' },
  customPrompt:   { en: 'Path or URL', zh: '路径或 URL' },
  customEmpty:    { en: '  Path cannot be empty, please try again', zh: '  路径不能为空，请重新输入' },
  downloading:    { en: '⏳ Downloading template...', zh: '⏳ 正在下载模板...' },
  dlDone:         { en: '✔ Template downloaded', zh: '✔ 模板下载完成' },

  // ports
  webPortPrompt:  { en: 'Web UI port', zh: 'Web UI 端口' },
  mcpPortPrompt:  { en: 'MCP server port', zh: 'MCP 服务端口' },
  portInUse:      { en: (p) => `  ⚠ Port ${p} is already in use, choose another`, zh: (p) => `  ⚠ 端口 ${p} 已被占用，请换一个` },
  portInvalid:    { en: (p) => `  ⚠ Invalid port "${p}", must be 1024–65535`, zh: (p) => `  ⚠ 端口 "${p}" 无效，需在 1024–65535 之间` },

  // auth
  authPrompt:     { en: 'Auth token seed (Enter to auto-generate)', zh: 'Auth token 种子（回车自动生成）' },
  tokenGenerated: { en: '✔ Auth token', zh: '✔ Auth token' },

  // web password
  webPassPrompt:  { en: 'Web UI password (leave empty = no password protection)', zh: 'Web UI 访问密码（留空 = 不设密码）' },
  webPassWarn:    { en: '  ⚠ No Web UI password — anyone on the network can access the UI', zh: '  ⚠ 未设置 Web UI 密码，局域网内任何人均可访问' },
  webPassSkip:    { en: 'Skip password protection anyway?', zh: '确认不设密码继续？' },

  // provider
  providerPrompt: { en: 'AI Provider', zh: 'AI 服务商' },
  providerOpts:   { en: ['anthropic', 'openai', 'skip — configure later via `mindos config set`'], zh: ['anthropic', 'openai', 'skip — 稍后通过 `mindos config set` 配置'] },
  providerVals:   ['anthropic', 'openai', 'skip'],
  providerSkip:   { en: '  → AI provider skipped. Run `mindos config set ai.provider <anthropic|openai>` later.', zh: '  → 已跳过 AI 配置，稍后运行 `mindos config set ai.provider <anthropic|openai>` 补填。' },
  anthropicKey:   { en: 'Anthropic API Key (sk-ant-...)', zh: 'Anthropic API Key (sk-ant-...)' },
  openaiKey:      { en: 'OpenAI API Key (sk-...)', zh: 'OpenAI API Key (sk-...)' },
  openaiBase:     { en: 'OpenAI Base URL (leave empty for default)', zh: 'OpenAI Base URL（留空使用默认）' },
  apiKeyWarn:     { en: '  ⚠ No API key entered — AI features will not work until you add one to ~/.mindos/config.json', zh: '  ⚠ 未填写 API Key，AI 功能将无法使用，可后续在 ~/.mindos/config.json 中补填' },

  // config
  cfgExists:      { en: (p) => `${p} already exists. Overwrite?`, zh: (p) => `${p} 已存在，是否覆盖？` },

  // start mode
  startModePrompt: { en: 'Start Mode', zh: '启动方式' },
  startModeOpts:   { en: ['Background service (recommended, auto-start on boot)', 'Foreground (manual start each time)'], zh: ['后台服务（推荐，开机自启）', '前台运行（每次手动启动）'] },
  startModeVals:   ['daemon', 'start'],
  startModeSkip:   { en: '  → Daemon not supported on this platform, using foreground mode', zh: '  → 当前平台不支持后台服务，使用前台模式' },
  cfgKept:        { en: '✔ Keeping existing config', zh: '✔ 保留现有配置' },
  cfgKeptNote:    { en: '  Settings from this session were not saved', zh: '  本次填写的设置未保存' },
  cfgSaved:       { en: '✔ Config saved', zh: '✔ 配置已保存' },
  cfgConfirm:     { en: 'Save this configuration?', zh: '保存此配置？' },
  cfgAborted:     { en: '✘ Setup cancelled. Run `mindos onboard` to try again.', zh: '✘ 设置已取消。运行 `mindos onboard` 重新开始。' },
  yesNo:          { en: '[y/N]', zh: '[y/N]' },
  yesNoDefault:   { en: '[Y/n]', zh: '[Y/n]' },
  startNow:       { en: 'Start MindOS now?', zh: '现在启动 MindOS？' },
  syncSetup:      { en: 'Set up cross-device sync via Git?', zh: '是否配置 Git 跨设备同步？' },
  syncLater:      { en: '  → Run `mindos sync init` anytime to set up sync later.', zh: '  → 随时运行 `mindos sync init` 配置同步。' },

  // mode selection step (Step 7)
  modeSelectHint:   { en: 'Connection mode (Space to toggle, Enter to confirm):', zh: '连接模式（空格切换，Enter 确认）：' },
  modeCli:          { en: 'CLI    Operate KB via command line', zh: 'CLI    通过命令行操作知识库' },
  modeCliHint:      { en: 'recommended, more token-efficient', zh: '推荐，更省 token' },
  modeMcp:          { en: 'MCP    Connect via MCP protocol', zh: 'MCP    通过 MCP 协议连接' },
  modeMcpHint:      { en: 'optional, may consume more tokens', zh: '可选，可能消耗更多 token' },
  modeNoneError:    { en: 'Please select at least one mode.', zh: '请至少选择一种模式。' },

  // agent config install (internal, Skill concept hidden from user)
  skillInstallFail: { en: (name, msg) => `  ${c.red('✘')} Configuration failed: ${msg}`, zh: (name, msg) => `  ${c.red('✘')} 配置失败：${msg}` },

  // restart prompts (re-onboard with config changes)
  restartRequired:   { en: 'Config changed. Service restart required.', zh: '配置已变更，需要重启服务。' },
  restartNow:        { en: 'Restart now?', zh: '立即重启？' },
  changesOnNextStart: { en: 'Changes will take effect on next start.', zh: '变更将在下次启动时生效。' },

  // next steps (onboard — keep it minimal, details shown on `mindos start`)
  nextSteps: {
    en: (cmd) => [
      '─────────────────────────────────────────────',
      '🚀 Setup complete! Start MindOS:\n',
      `     ${c.cyan(cmd)}`,
      `  ${c.dim('Connection details and auth token will be shown on startup.')}\n`,
      '─────────────────────────────────────────────',
    ],
    zh: (cmd) => [
      '─────────────────────────────────────────────',
      '🚀 初始化完成！启动 MindOS：\n',
      `     ${c.cyan(cmd)}`,
      `  ${c.dim('连接信息和 Auth token 将在启动后显示。')}\n`,
      '─────────────────────────────────────────────',
    ],
  },
};

// ── Terminal helpers ──────────────────────────────────────────────────────────

const ESC = '\x1b';
const CLEAR_LINE = '\r\x1b[K';
const CURSOR_UP = (n) => n > 0 ? `\x1b[${n}A` : '';
const HIDE_CURSOR = '\x1b[?25l';
const SHOW_CURSOR = '\x1b[?25h';

const c = process.stdout.isTTY ? {
  bold:   (s) => `\x1b[1m${s}\x1b[0m`,
  dim:    (s) => `\x1b[2m${s}\x1b[0m`,
  cyan:   (s) => `\x1b[36m${s}\x1b[0m`,
  green:  (s) => `\x1b[32m${s}\x1b[0m`,
  red:    (s) => `\x1b[31m${s}\x1b[0m`,
  yellow: (s) => `\x1b[33m${s}\x1b[0m`,
} : { bold: s=>s, dim: s=>s, cyan: s=>s, green: s=>s, red: s=>s, yellow: s=>s };

function write(s) { process.stdout.write(s); }

// ── State ─────────────────────────────────────────────────────────────────────

function detectSystemLang() {
  // Check env vars by precedence (LC_ALL > LC_MESSAGES > LANG > LANGUAGE)
  // For LANGUAGE, only use the first entry before ':' (it's a priority list)
  const candidates = [
    process.env.LC_ALL,
    process.env.LC_MESSAGES,
    process.env.LANG,
    ...(process.env.LANGUAGE ? [process.env.LANGUAGE.split(':')[0]] : []),
  ];
  const first = candidates.find(Boolean);
  if (first) {
    return first.toLowerCase().startsWith('zh') ? 'zh' : 'en';
  }

  // Fallback: Intl API (works on Windows where LANG is often unset)
  try {
    const locale = Intl.DateTimeFormat().resolvedOptions().locale;
    if (locale.toLowerCase().startsWith('zh')) return 'zh';
  } catch {}

  return 'en';
}

let uiLang = detectSystemLang();
const t = (key) => T[key]?.[uiLang] ?? T[key]?.en ?? '';
const tf = (key, ...args) => {
  const v = T[key]?.[uiLang] ?? T[key]?.en;
  return typeof v === 'function' ? v(...args) : v ?? '';
};

// ── Step header ───────────────────────────────────────────────────────────────

const TOTAL_STEPS = 8;
function stepHeader(n) {
  const title = T.stepTitles[uiLang][n - 1] ?? T.stepTitles.en[n - 1];
  const stepLabel = tf('step', n, TOTAL_STEPS);
  console.log(`\n${c.bold(title)}  ${c.dim(stepLabel)}`);
  console.log(c.dim('─'.repeat(44)));
}

// ── Raw-mode key reader ───────────────────────────────────────────────────────

function readKey() {
  return new Promise((resolve) => {
    const { stdin } = process;
    if (!stdin.isTTY) return resolve(null);
    stdin.setRawMode(true);
    stdin.resume();
    stdin.setEncoding('utf8');
    const onData = (chunk) => {
      stdin.setRawMode(false);
      stdin.pause();
      stdin.removeListener('data', onData);
      resolve(chunk);
    };
    stdin.on('data', onData);
  });
}

// ── Select prompt ─────────────────────────────────────────────────────────────

async function select(labelKey, optionsKey, valuesKey = null) {
  let idx = 0;
  let lastLineCount = 0;

  const render = (first = false) => {
    const opts = T[optionsKey][uiLang];
    const lines = [
      `${c.bold(t(labelKey) + ':')}`,
      ...opts.map((o, i) => {
        const [val, ...rest] = o.split(' — ');
        const desc = rest.join(' — ');
        const item = desc ? `${c.cyan(val)} ${c.dim('—')} ${c.dim(desc)}` : c.cyan(val);
        return i === idx
          ? `  ${c.cyan('❯')} ${item}`
          : `    ${c.dim(val)}${desc ? c.dim(' — ' + desc) : ''}`;
      }),
    ];
    if (!first && lastLineCount > 0) {
      write(`${CURSOR_UP(lastLineCount)}\r\x1b[J`);
    }
    lastLineCount = lines.length;
    write(lines.join('\n') + '\n');
  };

  write(HIDE_CURSOR);
  render(true);

  while (true) {
    const key = await readKey();
    if (key === null) {
      write(SHOW_CURSOR);
      return (valuesKey ? T[valuesKey] : T[optionsKey][uiLang])[0];
    }

    const opts = T[optionsKey][uiLang];

    if (key === '\r' || key === '\n') {
      write(SHOW_CURSOR);
      write(`${CURSOR_UP(lastLineCount)}\r\x1b[J`);
      const displayLabel = opts[idx].split(' — ')[0];
      write(`${c.bold(t(labelKey) + ':')} ${c.cyan(displayLabel)}\n`);
      return valuesKey ? T[valuesKey][idx] : opts[idx];
    }

    if (key === `${ESC}[A`) { idx = (idx - 1 + opts.length) % opts.length; render(); }
    else if (key === `${ESC}[B`) { idx = (idx + 1) % opts.length; render(); }
    else if (key === `${ESC}[C` || key === `${ESC}[D`) {
      uiLang = uiLang === 'en' ? 'zh' : 'en';
      idx = Math.min(idx, T[optionsKey][uiLang].length - 1);
      render();
    }
    else if (key === '\x03') { write(SHOW_CURSOR); process.exit(1); }
  }
}

// ── Text input prompt ─────────────────────────────────────────────────────────

async function askText(labelKey, defaultValue = '', hintKey = '', ...hintArgs) {
  const buildPrompt = () => {
    const label = c.bold(t(labelKey));
    const def   = defaultValue ? ` ${c.dim('[' + defaultValue + ']')}` : '';
    const hintStr = hintKey ? tf(hintKey, ...hintArgs) : '';
    const h     = hintStr ? `  ${c.dim('← ' + hintStr)}` : '';
    return `${label}${def}${h}: `;
  };

  if (!process.stdin.isTTY) {
    return new Promise((resolve) => {
      const rl = createInterface({ input: process.stdin, output: process.stdout });
      rl.question(buildPrompt(), (ans) => { rl.close(); resolve(ans.trim() || defaultValue); });
    });
  }

  const render = (buf, first = false) => {
    if (!first) write(`\r\x1b[K`);
    write(`${buildPrompt()}${c.cyan(buf)}`);
  };

  write(HIDE_CURSOR);
  let buf = '';
  render(buf, true);
  write(SHOW_CURSOR);

  process.stdin.setRawMode(true);
  process.stdin.resume();
  process.stdin.setEncoding('utf8');

  return new Promise((resolve) => {
    const onData = (chunk) => {
      if (chunk === '\x03') { write('\n'); process.exit(1); }

      if (chunk === '\r' || chunk === '\n') {
        process.stdin.setRawMode(false);
        process.stdin.pause();
        process.stdin.removeListener('data', onData);
        write('\n');
        resolve(buf || defaultValue);
        return;
      }

      if (chunk === `${ESC}[C` || chunk === `${ESC}[D`) {
        uiLang = uiLang === 'en' ? 'zh' : 'en';
        render(buf);
        return;
      }

      if (chunk === '\x7f' || chunk === '\b') {
        if (buf.length > 0) { buf = buf.slice(0, -1); render(buf); }
        return;
      }

      if (chunk.startsWith(ESC)) return;
      buf += chunk;
      render(buf);
    };
    process.stdin.on('data', onData);
  });
}

// ── Yes/No prompt ─────────────────────────────────────────────────────────────

async function askYesNo(labelKey, arg = '', defaultYes = false) {
  const label = typeof T[labelKey][uiLang] === 'function' ? T[labelKey][uiLang](arg) : t(labelKey);
  const hintKey = defaultYes ? 'yesNoDefault' : 'yesNo';
  if (!process.stdin.isTTY) {
    return new Promise((resolve) => {
      const rl = createInterface({ input: process.stdin, output: process.stdout });
      rl.question(`${c.bold(label)} ${c.dim(t(hintKey))}: `, (ans) => {
        const v = ans.trim().toLowerCase();
        rl.close(); resolve(defaultYes ? v !== 'n' : v === 'y');
      });
    });
  }

  write(HIDE_CURSOR);
  const render = (buf = '', first = false) => {
    if (!first) write(`\r\x1b[K`);
    write(`${c.bold(label)} ${c.dim(t(hintKey))}: ${c.cyan(buf)}`);
  };
  render('', true);
  write(SHOW_CURSOR);

  process.stdin.setRawMode(true);
  process.stdin.resume();
  process.stdin.setEncoding('utf8');

  return new Promise((resolve) => {
    let buf = '';
    const onData = (chunk) => {
      if (chunk === '\x03') { write('\n'); process.exit(1); }

      if (chunk === '\r' || chunk === '\n') {
        process.stdin.setRawMode(false);
        process.stdin.pause();
        process.stdin.removeListener('data', onData);
        write('\n');
        const v = buf.toLowerCase();
        resolve(defaultYes ? v !== 'n' : v === 'y');
        return;
      }

      if (chunk === `${ESC}[C` || chunk === `${ESC}[D`) {
        uiLang = uiLang === 'en' ? 'zh' : 'en';
        render(buf);
        return;
      }

      if (chunk === '\x7f' || chunk === '\b') {
        if (buf.length > 0) { buf = buf.slice(0, -1); render(buf); }
        return;
      }

      if (chunk.startsWith(ESC)) return;
      buf += chunk;
      render(buf);
    };
    process.stdin.on('data', onData);
  });
}

const askYesNoDefault = (labelKey, arg = '') => askYesNo(labelKey, arg, true);

// ── Port helpers ──────────────────────────────────────────────────────────────

function isPortInUse(port) {
  return new Promise((resolve) => {
    const sock = createConnection({ port, host: '127.0.0.1' });
    const cleanup = (result) => { sock.destroy(); resolve(result); };
    // On localhost, timeout means no response — treat as free (same as bin/lib/port.js)
    sock.setTimeout(500, () => cleanup(false));
    sock.once('connect', () => cleanup(true));
    sock.once('error', (err) => {
      // ECONNREFUSED = nothing listening → free; other errors = treat as in-use
      cleanup(err.code !== 'ECONNREFUSED');
    });
  });
}

async function isSelfPort(port) {
  try {
    return await new Promise((resolve) => {
      const req = http.get(`http://127.0.0.1:${port}/api/health`, { timeout: 800 }, (res) => {
        let body = '';
        res.on('data', chunk => { body += chunk; });
        res.on('end', () => {
          try {
            const data = JSON.parse(body);
            // 200 with service=mindos → definitely us.
            // 401 Unauthorized → also us (webPassword is set).
            resolve(data.service === 'mindos' || res.statusCode === 401);
          } catch {
            // Non-JSON but got a response on /api/health → likely us
            resolve(res.statusCode === 401 || res.statusCode === 200);
          }
        });
      });
      req.on('error', () => resolve(false));
      req.on('timeout', () => { req.destroy(); resolve(false); });
    });
  } catch { return false; }
}

async function findFreePort(from) {
  for (let p = from; p <= 65535; p++) {
    if (!await isPortInUse(p)) return p;
  }
  return from; // fallback (extremely unlikely)
}

async function askPort(labelKey, defaultPort) {
  let port = defaultPort;
  // If the default port is in use, check if it's our own service (self = ok to keep)
  if (await isPortInUse(port)) {
    if (await isSelfPort(port)) {
      // Already running on this port — keep it as default
    } else {
      port = await findFreePort(port + 1);
    }
  }
  while (true) {
    const input = (await askText(labelKey, String(port))).trim();
    const parsed = parseInt(input, 10);
    if (!parsed || parsed < 1024 || parsed > 65535) {
      write(c.yellow(tf('portInvalid', input) + '\n'));
      continue;
    }
    if (await isPortInUse(parsed)) {
      // Check if it's our own service — acceptable to keep
      if (await isSelfPort(parsed)) {
        return parsed;
      }
      const next = await findFreePort(parsed + 1);
      write(c.yellow(tf('portInUse', parsed) + '\n'));
      port = next;
      continue;
    }
    return parsed;
  }
}

// ── Token generation ──────────────────────────────────────────────────────────

function generateToken(passphrase = '') {
  let bytes;
  if (passphrase) {
    const salt = randomBytes(16).toString('hex');
    bytes = createHash('sha256').update(passphrase + salt).digest();
  } else {
    bytes = randomBytes(24);
  }
  const hex = bytes.toString('hex').slice(0, 24);
  return hex.match(/.{4}/g).join('-');
}

// ── Template handler ──────────────────────────────────────────────────────────

function parseGithubDir(url) {
  const m = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/?(.*)$/);
  if (!m) return null;
  const [, owner, repo, ref, subdir] = m;
  return { tarball: `https://api.github.com/repos/${owner}/${repo}/tarball/${ref}`, subdir: subdir || '' };
}

async function downloadAndExtract(url, destDir) {
  const tmp = join(tmpdir(), `mindos-tpl-${Date.now()}`);
  mkdirSync(tmp, { recursive: true });
  const tarPath = join(tmp, 'tpl.tar.gz');

  let fetchUrl = url;
  let subdir = '';
  const gh = parseGithubDir(url);
  if (gh) { fetchUrl = gh.tarball; subdir = gh.subdir; }

  const res = await fetch(fetchUrl, { headers: { 'User-Agent': 'mindos-init' }, redirect: 'follow' });
  if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
  await pipeline(res.body, createWriteStream(tarPath));

  const extractDir = join(tmp, 'extracted');
  mkdirSync(extractDir, { recursive: true });
  extractTarGzSafe(tarPath, extractDir);

  const { readdirSync, statSync } = await import('node:fs');
  let contentRoot = extractDir;
  const entries = readdirSync(extractDir);
  if (entries.length === 1 && statSync(join(extractDir, entries[0])).isDirectory()) {
    contentRoot = join(extractDir, entries[0]);
  }
  if (subdir) contentRoot = join(contentRoot, subdir);

  cpSync(contentRoot, destDir, { recursive: true, filter: (src) => !src.endsWith('.gitkeep') });
  rmSync(tmp, { recursive: true, force: true });
}

function extractTarGzSafe(tarPath, destDir) {
  const tar = gunzipSync(readFileSync(tarPath));
  let offset = 0;
  let longName = null;

  while (offset + 512 <= tar.length) {
    const header = tar.subarray(offset, offset + 512);
    offset += 512;
    if (isZeroBlock(header)) break;

    const nameRaw = readTarString(header, 0, 100);
    const size = readTarOctal(header, 124, 12);
    const typeflag = header[156];
    const prefix = readTarString(header, 345, 155);
    const dataEnd = offset + size;
    if (dataEnd > tar.length) throw new Error('Invalid template tar entry size');

    const paddedSize = Math.ceil(size / 512) * 512;
    if (typeflag === 0x4c) {
      longName = tar.subarray(offset, dataEnd).toString('utf-8').replace(/\0.*$/, '');
      offset += paddedSize;
      continue;
    }
    if (typeflag === 0x78) {
      const paxPath = readPaxPath(tar.subarray(offset, dataEnd).toString('utf-8'));
      if (paxPath) longName = paxPath;
      offset += paddedSize;
      continue;
    }
    if (typeflag === 0x67) {
      offset += paddedSize;
      continue;
    }

    const entryName = longName || (prefix ? `${prefix}/${nameRaw}` : nameRaw);
    longName = null;
    if (!entryName || entryName === '.' || entryName === './') {
      offset += paddedSize;
      continue;
    }

    const entryPath = resolveTarEntryPath(destDir, entryName);
    const isDir = typeflag === 0x35 || entryName.endsWith('/');
    const isFile = typeflag === 0 || typeflag === 0x30;
    if (isDir) {
      mkdirSync(entryPath, { recursive: true });
    } else if (isFile) {
      mkdirSync(dirname(entryPath), { recursive: true });
      writeFileSync(entryPath, tar.subarray(offset, dataEnd));
    }

    offset += paddedSize;
  }
}

function resolveTarEntryPath(destDir, entryName) {
  const normalizedEntry = entryName.replaceAll('\\', '/');
  if (
    normalizedEntry.startsWith('/')
    || normalizedEntry.startsWith('//')
    || /^[A-Za-z]:/.test(entryName)
    || /^[A-Za-z]:/.test(normalizedEntry)
    || normalizedEntry.split('/').includes('..')
  ) {
    throw new Error(`Template tar entry outside extraction directory: ${entryName}`);
  }

  const root = resolve(destDir);
  const target = resolve(root, normalizedEntry);
  const rel = relative(root, target);
  if (rel === '..' || rel.startsWith(`..${pathSep}`) || isAbsolute(rel)) {
    throw new Error(`Template tar entry outside extraction directory: ${entryName}`);
  }
  return target;
}

function readTarString(buffer, offset, length) {
  const slice = buffer.subarray(offset, offset + length);
  const nul = slice.indexOf(0);
  return slice.subarray(0, nul === -1 ? length : nul).toString('utf-8');
}

function readTarOctal(buffer, offset, length) {
  const raw = readTarString(buffer, offset, length).trim();
  if (!raw) return 0;
  const parsed = Number.parseInt(raw.replace(/\0/g, ''), 8);
  if (!Number.isFinite(parsed)) throw new Error('Invalid template tar entry size');
  return parsed;
}

function readPaxPath(content) {
  let cursor = 0;
  while (cursor < content.length) {
    const space = content.indexOf(' ', cursor);
    if (space === -1) return null;
    const length = Number.parseInt(content.slice(cursor, space), 10);
    if (!Number.isFinite(length) || length <= 0) return null;
    const record = content.slice(space + 1, cursor + length).replace(/\n$/, '');
    const eq = record.indexOf('=');
    if (eq > 0 && record.slice(0, eq) === 'path') return record.slice(eq + 1);
    cursor += length;
  }
  return null;
}

function isZeroBlock(buffer) {
  for (const byte of buffer) {
    if (byte !== 0) return false;
  }
  return true;
}

async function applyTemplate(tpl, mindDir) {
  if (tpl === 'custom') {
    let source = '';
    while (!source) {
      source = (await askText('customPrompt')).trim();
      if (!source) write(c.yellow(t('customEmpty') + '\n'));
    }

    const isUrl = source.startsWith('http://') || source.startsWith('https://');
    if (isUrl) {
      write(c.yellow(t('downloading') + '\n'));
      await downloadAndExtract(source, mindDir);
      console.log(`${c.green(t('dlDone'))}: ${c.dim(mindDir)}`);
    } else {
      const localPath = resolve(source);
      if (!existsSync(localPath)) {
        console.error(c.red(`${t('tplNotFound')}: ${localPath}`));
        process.exit(1);
      }
      cpSync(localPath, mindDir, { recursive: true, filter: (src) => !src.endsWith('.gitkeep') });
      console.log(`${c.green(t('kbCreated'))}: ${c.dim(mindDir)}`);
    }
  } else {
    const tplDir = resolve(ROOT, 'templates', tpl);
    if (!existsSync(tplDir)) {
      console.error(c.red(`${t('tplNotFound')}: ${tplDir}`));
      process.exit(1);
    }
    cpSync(tplDir, mindDir, { recursive: true, filter: (src) => !src.endsWith('.gitkeep') });
    console.log(`${c.green(t('kbCreated'))}: ${c.dim(mindDir)}`);
  }
}

// MCP_AGENTS imported from packages/mindos/bin/lib/mcp-agents.js

function expandHomePath(p) {
  return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
}

/** Parse JSONC: JSON plus comments (e.g. VS Code configs). Returns {} for empty input. */
function parseJsonc(text) {
  let stripped = text.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)/gm, (m, g) => g ? '' : m);
  stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '');
  if (!stripped.trim()) return {};
  return JSON.parse(stripped);
}

/** Detect if an agent already has mindos configured (for pre-selection). */
function isAgentInstalled(agentKey) {
  const agent = MCP_AGENTS[agentKey];
  if (!agent) return false;
  for (const cfgPath of [agent.global, agent.project]) {
    if (!cfgPath) continue;
    const abs = expandHomePath(cfgPath);
    if (!existsSync(abs)) continue;
    try {
      const config = parseJsonc(readFileSync(abs, 'utf-8'));
      if (config[agent.key]?.mindos) return true;
    } catch { /* ignore */ }
  }
  return false;
}

/**
 * Step 7: mode selection — CLI (default-on) vs MCP (default-off).
 * Returns { cli: boolean, mcp: boolean }.
 */
async function runModeSelect() {
  const modes = [
    { key: 'cli', label: t('modeCli'), hint: t('modeCliHint'), preselect: true },
    { key: 'mcp', label: t('modeMcp'), hint: t('modeMcpHint'), preselect: false },
  ];

  return new Promise((resolve) => {
    let cursor = 0;
    const chosen = new Set(modes.map((m, i) => m.preselect ? i : -1).filter(i => i >= 0));

    const render = (first = false, errMsg = '') => {
      if (!first) write(`\x1b[${modes.length + (errMsg ? 3 : 2)}A\x1b[J`);
      write(`${c.bold(t('modeSelectHint'))}\n`);
      for (let i = 0; i < modes.length; i++) {
        const m = modes[i];
        const check   = chosen.has(i) ? c.green('✔') : c.dim('○');
        const pointer = i === cursor  ? c.cyan('❯') : ' ';
        const label   = i === cursor  ? (chosen.has(i) ? c.green(m.label) : c.cyan(m.label)) : (chosen.has(i) ? c.green(m.label) : m.label);
        write(`  ${pointer} ${check} ${label}  ${c.dim('(' + m.hint + ')')}\n`);
      }
      if (errMsg) {
        write(`  ${c.yellow('⚠ ' + errMsg)}\n`);
      }
      write(c.dim(`  ${chosen.size} ${uiLang === 'zh' ? '已选' : 'selected'}\n`));
    };

    write('\n');
    render(true);

    process.stdin.setRawMode(true);
    process.stdin.resume();
    process.stdin.setEncoding('utf8');

    const onKey = (key) => {
      if (key === '\x03') { cleanup(); process.exit(1); }
      if (key === `${ESC}[A`) { cursor = (cursor - 1 + modes.length) % modes.length; render(); }
      else if (key === `${ESC}[B`) { cursor = (cursor + 1) % modes.length; render(); }
      else if (key === ' ') {
        if (chosen.has(cursor)) chosen.delete(cursor); else chosen.add(cursor);
        render();
      } else if (key === '\r' || key === '\n') {
        if (chosen.size === 0) {
          render(false, t('modeNoneError'));
          return;
        }
        cleanup();
        resolve({
          cli: chosen.has(0),
          mcp: chosen.has(1),
        });
      }
    };

    const cleanup = () => {
      process.stdin.removeListener('data', onKey);
      process.stdin.setRawMode(false);
      process.stdin.pause();
    };

    process.stdin.on('data', onKey);
  });
}

/**
 * Agent multi-select UI — shared by both CLI-only and MCP modes.
 * Returns array of selected agent keys.
 */
async function runAgentSelect() {
  const keys = Object.keys(MCP_AGENTS);

  const options = keys.map(k => {
    const installed = isAgentInstalled(k);
    const present = detectAgentPresence(k);
    return {
      label: MCP_AGENTS[k].name,
      hint:  installed ? (uiLang === 'zh' ? '已配置' : 'configured') : present ? (uiLang === 'zh' ? '已检测' : 'detected') : (uiLang === 'zh' ? '未找到' : 'not found'),
      value: k,
      preselect: installed || present,
    };
  });

  options.sort((a, b) => {
    const rank = (o) => o.hint.includes('configured') || o.hint.includes('已配置') ? 0 : o.preselect ? 1 : 2;
    return rank(a) - rank(b);
  });

  const selected = await new Promise((resolveSelected) => {
    let cursor = 0;
    const chosen = new Set(options.map((o, i) => o.preselect ? i : -1).filter(i => i >= 0));

    const render = (first = false) => {
      if (!first) write(`\x1b[${options.length + 2}A\x1b[J`);
      write(`${c.bold(uiLang === 'zh' ? '选择 Agent：' : 'Select agents:')}  ${c.dim(uiLang === 'zh' ? '(↑↓ 移动  空格 切换  D 已检测  A 全选  Enter 确认)' : '(↑↓ move  Space toggle  D detected  A all  Enter confirm)')}\n`);
      for (let i = 0; i < options.length; i++) {
        const o = options[i];
        const check   = chosen.has(i) ? c.green('✔') : c.dim('○');
        const pointer = i === cursor  ? c.cyan('❯') : ' ';
        const label   = i === cursor  ? (chosen.has(i) ? c.green(o.label) : c.cyan(o.label)) : (chosen.has(i) ? c.green(o.label) : o.label);
        write(`  ${pointer} ${check} ${label}  ${c.dim('(' + o.hint + ')')}\n`);
      }
      write(c.dim(`  ${chosen.size} ${uiLang === 'zh' ? '已选' : 'selected'}\n`));
    };

    write('\n');
    render(true);

    process.stdin.setRawMode(true);
    process.stdin.resume();
    process.stdin.setEncoding('utf8');

    const onKey = (key) => {
      if (key === '\x03') { cleanup(); process.exit(1); }
      if (key === `${ESC}[A`) { cursor = (cursor - 1 + options.length) % options.length; render(); }
      else if (key === `${ESC}[B`) { cursor = (cursor + 1) % options.length; render(); }
      else if (key === ' ') {
        if (chosen.has(cursor)) chosen.delete(cursor); else chosen.add(cursor);
        render();
      } else if (key === 'a' || key === 'A') {
        if (chosen.size === options.length) chosen.clear();
        else options.forEach((_, i) => chosen.add(i));
        render();
      } else if (key === 'd' || key === 'D') {
        chosen.clear();
        options.forEach((o, i) => { if (o.preselect) chosen.add(i); });
        render();
      } else if (key === '\r' || key === '\n') {
        cleanup();
        resolveSelected([...chosen].sort().map(i => options[i].value));
      }
    };

    const cleanup = () => {
      process.stdin.removeListener('data', onKey);
      process.stdin.setRawMode(false);
      process.stdin.pause();
    };

    process.stdin.on('data', onKey);
  });

  if (selected.length === 0) {
    write(c.dim(uiLang === 'zh' ? '  → 未选择 Agent。\n' : '  → No agents selected.\n'));
  }
  return selected;
}

/**
 * Write MCP config into selected agents' config files.
 * Agent selection is handled separately by runAgentSelect().
 */
function installMcpConfig(selected, mcpPort, authToken) {
  const entry = { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } };

  for (const agentKey of selected) {
    const agent = MCP_AGENTS[agentKey];
    const cfgPath = agent.global || agent.project;
    if (!cfgPath) continue;
    const abs = expandHomePath(cfgPath);
    try {
      let config = {};
      if (existsSync(abs)) config = parseJsonc(readFileSync(abs, 'utf-8'));
      if (!config[agent.key]) config[agent.key] = {};
      config[agent.key].mindos = entry;
      const dir = resolve(abs, '..');
      if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
      writeFileSync(abs, JSON.stringify(config, null, 2) + '\n', 'utf-8');
    } catch { /* logged by caller via agent count */ }
  }
}

/* ── Skill auto-install ────────────────────────────────────────────────────── */

const UNIVERSAL_AGENTS = new Set([
  'cline', 'codex', 'cursor', 'gemini-cli',
  'github-copilot', 'kimi-cli', 'opencode', 'warp',
]);
const SKILL_UNSUPPORTED = new Set([]);

/**
 * Install the appropriate MindOS Skill to selected agents via `npx skills add`.
 * @param {string} template - 'en' | 'zh' | 'empty' | 'custom'
 * @param {string[]} selectedAgents - MCP agent keys from the multi-select step
 */
function runSkillInstallStep(template, selectedAgents) {
  if (!selectedAgents || selectedAgents.length === 0) return true;

  const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
  const localSource = resolve(ROOT, 'skills');
  const githubSource = 'GeminiLight/MindOS';

  const additionalAgents = selectedAgents
    .flatMap((key) => {
      if (SKILL_UNSUPPORTED.has(key)) return [];
      if (UNIVERSAL_AGENTS.has(key)) return [];
      const reg = SKILL_AGENT_REGISTRY[key];
      if (!reg) return [key];
      if (reg.mode === 'unsupported') return [];
      if (reg.mode === 'universal') return [];
      return [reg.skillAgentName || key];
    });

  // Direct-copy skill for 'unsupported' agents (e.g., QClaw, WorkBuddy, Lingma)
  const unsupportedAgents = selectedAgents.filter((key) => {
    const reg = SKILL_AGENT_REGISTRY[key];
    return reg?.mode === 'unsupported';
  });
  for (const key of unsupportedAgents) {
    const agent = MCP_AGENTS[key];
    if (!agent) continue;
    const skillSourceDir = resolve(ROOT, 'skills', skillName);
    if (!existsSync(skillSourceDir)) continue;
    const targetDir = expandHomePath(agent.presenceDirs?.[0] ?? agent.global.replace(/\/[^/]+$/, '/'));
    const targetSkillDir = resolve(targetDir, 'skills', skillName);
    try {
      if (!existsSync(targetSkillDir)) {
        cpSync(skillSourceDir, targetSkillDir, { recursive: true });
      }
    } catch { /* best-effort copy for unsupported agents */ }
  }

  const agentArgs = additionalAgents.length > 0
    ? additionalAgents.flatMap(a => ['-a', a])
    : ['-a', 'universal'];

  const sources = [githubSource, localSource];

  for (const source of sources) {
    const args = ['skills', 'add', source, '--skill', skillName, ...agentArgs, '-g', '-y'];
    try {
      const invocation = resolveNpxInvocation(args);
      execFileSync(invocation.command, invocation.args, {
        encoding: 'utf-8',
        timeout: 30_000,
        env: { ...process.env, NODE_ENV: 'production' },
        stdio: 'pipe',
      });
      return true;
    } catch { /* try next source */ }
  }

  return false;
}

// ── GUI Setup ─────────────────────────────────────────────────────────────────

function openBrowser(url) {
  try {
    const platform = process.platform;
    if (platform === 'darwin') {
      execFileSync('open', [url], { stdio: 'ignore' });
    } else if (platform === 'linux') {
      // Check for WSL
      const isWSL = existsSync('/proc/version') &&
        readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft');
      if (isWSL) {
        execFileSync('cmd.exe', ['/c', 'start', '', url], { stdio: 'ignore' });
      } else {
        execFileSync('xdg-open', [url], { stdio: 'ignore' });
      }
    } else {
      execFileSync('cmd.exe', ['/c', 'start', '', url], { stdio: 'ignore' });
    }
    return true;
  } catch {
    return false;
  }
}

async function startGuiSetup() {
  // Ensure ~/.mindos directory exists
  mkdirSync(MINDOS_DIR, { recursive: true });

  // Read or create config, set setupPending
  let config = {};
  try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* ignore */ }

  const isFirstTime = !config.mindRoot;

  // Clean up zombie process from a previous abandoned setup session.
  if (config.setupPort) {
    try {
      const { killByPort } = await import('../packages/mindos/bin/lib/stop.js');
      killByPort(Number(config.setupPort));
      // Brief wait for port to free
      await new Promise(r => setTimeout(r, 500));
    } catch { /* best effort */ }
  }

  config.setupPending = true;
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');

  // Determine which port to use for the setup wizard
  let usePort;
  if (isFirstTime) {
    // First-time onboard: use a temporary port (scan from 9100) so the user's
    // chosen port in Step 3 can differ without a mid-setup restart.
    // 9100 is chosen to avoid conflicts with common services (5000=AirPlay, 3456/8080=dev).
    usePort = await findFreePort(9100);
  } else {
    // Re-onboard: service is already running on config.port — reuse it.
    const existingPort = config.port || 3456;
    if (await isSelfPort(existingPort)) {
      // Service already running — just open the setup page, no need to spawn.
      const url = `http://localhost:${existingPort}/setup`;
      console.log(`\n${c.green(tf('guiReady', url))}\n`);
      const opened = openBrowser(url);
      if (!opened) console.log(c.dim(tf('guiOpenFailed', url)));
      process.exit(0);
    }
    // Service not running — start on existing port
    if (await isPortInUse(existingPort)) {
      // Port occupied — try stopping leftover MindOS processes first
      try {
        const { stopMindos } = await import('../packages/mindos/bin/lib/stop.js');
        stopMindos();
        // stopMindos() sends SIGTERM synchronously — wait for both web and mcp
        // ports to free, since `start` will assertPortFree on both.
        const { waitForPortFree } = await import('../packages/mindos/bin/lib/gateway.js');
        const mcpPort = config.mcpPort || 8781;
        const [webFreed, mcpFreed] = await Promise.all([
          waitForPortFree(existingPort),
          waitForPortFree(mcpPort),
        ]);
        usePort = webFreed ? existingPort : await findFreePort(9100);
      } catch {
        usePort = await findFreePort(9100);
      }
    } else {
      usePort = existingPort;
    }
  }

  write(c.yellow(t('guiStarting') + '\n'));

  // Start the server in the background
  // Record the temporary setup port in config so stopMindos() can clean it up
  // if the user abandons setup mid-way or closes the browser without completing.
  config.setupPort = usePort;
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');

  // Pass MINDOS_WEB_PORT (not PORT) so loadConfig() won't override with the
  // config file port — this is critical when we need a temporary port.
  const cliPath = resolve(__dirname, '../packages/mindos/bin/cli.js');
  const logFd = openSync(LOG_PATH, 'a');
  const child = spawn(process.execPath, [cliPath, 'start'], {
    detached: true,
    stdio: ['ignore', logFd, logFd],
    env: { ...process.env, MINDOS_WEB_PORT: String(usePort) },
  });
  child.unref();

  // First-time install hint
  if (isFirstTime) {
    write(c.dim('  First run: installing dependencies and building app (may take a few minutes)...\n'));
  }

  // Wait for the server to be ready (10min timeout — first run involves npm install + build)
  const { waitForHttp } = await import('../packages/mindos/bin/lib/gateway.js');
  const ready = await waitForHttp(usePort, { retries: 600, intervalMs: 1000, label: 'MindOS', logFile: LOG_PATH });

  if (!ready) {
    write(c.red('\n✘ Server failed to start.\n'));
    if (existsSync(LOG_PATH)) {
      write(c.dim(`\n  Last log output (${LOG_PATH}):\n`));
      try {
        const lines = readFileSync(LOG_PATH, 'utf-8').trim().split('\n').slice(-15);
        for (const line of lines) write(c.dim(`  ${line}\n`));
      } catch {}
    }
    write(c.dim(`\n  Full logs: mindos logs\n`));
    write(c.dim(`  Manual start: mindos start\n\n`));
    process.exit(1);
  }

  const url = `http://localhost:${usePort}/setup`;
  console.log(`\n${c.green(tf('guiReady', url))}\n`);

  const opened = openBrowser(url);
  if (!opened) {
    console.log(c.dim(tf('guiOpenFailed', url)));
  }

  process.exit(0);
}

// ── Main ──────────────────────────────────────────────────────────────────────

async function main() {
  console.log(`\n${c.bold(t('title'))}\n\n${c.dim(t('langHint'))}\n`);

  // ── Mode selection: CLI or GUI ───────────────────────────────────────────
  const mode = await select('modePrompt', 'modeOpts', 'modeVals');

  if (mode === 'gui') {
    await startGuiSetup();
    return;
  }

  // ── CLI mode continues below ─────────────────────────────────────────────

  // ── Early overwrite check ─────────────────────────────────────────────────
  if (existsSync(CONFIG_PATH)) {
    let existing = {};
    try { existing = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}

    const mask = (s) => s ? s.slice(0, 4) + '••••••••' + s.slice(-2) : c.dim('(not set)');
    const row  = (label, val) => `  ${c.dim(label.padEnd(18))} ${val}`;
    const providers = existing.ai?.providers;
    const anthropicKey = providers?.anthropic?.apiKey || existing.ai?.anthropicApiKey || '';
    const openaiKey    = providers?.openai?.apiKey    || existing.ai?.openaiApiKey    || '';

    console.log(c.bold('\nExisting config:'));
    console.log(row('Knowledge base:', c.cyan(existing.mindRoot || '(not set)')));
    console.log(row('Web port:',       c.cyan(String(existing.port || '3456'))));
    console.log(row('MCP port:',       c.cyan(String(existing.mcpPort || '8781'))));
    console.log(row('Auth token:',     existing.authToken ? mask(existing.authToken) : c.dim('(not set)')));
    console.log(row('Web password:',   existing.webPassword ? '••••••••' : c.dim('(none)')));
    console.log(row('AI provider:',    c.cyan(existing.ai?.provider || '(not set)')));
    if (anthropicKey) console.log(row('Anthropic key:', mask(anthropicKey)));
    if (openaiKey)    console.log(row('OpenAI key:',    mask(openaiKey)));
    write('\n');

    const overwrite = await askYesNo('cfgExists', CONFIG_PATH);
    if (!overwrite) {
      const existingMode     = existing.startMode || 'start';
      const existingMcpPort  = existing.mcpPort   || 8781;
      const existingAuth     = existing.authToken || '';
      const existingMindRoot = existing.mindRoot  || resolve(homedir(), 'MindOS', 'mind');
      console.log(`\n${c.green(t('cfgKept'))}  ${c.dim(CONFIG_PATH)}`);
      write(c.dim(t('cfgKeptNote') + '\n'));
      const installDaemon = process.argv.includes('--install-daemon');
      finish(existingMindRoot, existingMode, existingMcpPort, existingAuth, installDaemon);
      return;
    }
  }

  // ── Step 1: Knowledge base path ───────────────────────────────────────────
  stepHeader(1);

  // Resume: read existing config to offer current values as defaults
  let resumeCfg = {};
  try { resumeCfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { /* first run */ }

  const { readdirSync } = await import('node:fs');
  let mindDir;
  let selectedTemplate = 'en'; // hoisted — set by template selection or inferred from existing config
  // Infer template from existing config's disabledSkills or UI language
  if (resumeCfg.disabledSkills?.includes('mindos')) {
    selectedTemplate = 'zh';
  } else if (resumeCfg.disabledSkills?.includes('mindos-zh')) {
    selectedTemplate = 'en';
  } else {
    selectedTemplate = uiLang; // fallback to UI language for first-time existing KB
  }

  // Default KB path: existing mindRoot if set, otherwise ~/MindOS (same as GUI default)
  const HOME = homedir();
  const kbDefault = resumeCfg.mindRoot || resolve(HOME, 'MindOS', 'mind');

  while (true) {
    const input = (await askText('pathPrompt', kbDefault)).trim();
    // Handle absolute paths for all platforms (Unix: /path, Windows: C:\path or C:/path)
    const isAbsolute = input.startsWith('/') || /^[A-Za-z]:[/\\]/.test(input);
    const resolved = input.startsWith('~/')
      ? resolve(HOME, input.slice(2))
      : isAbsolute ? resolve(input) : resolve(HOME, input);
    write(tf('pathResolved', resolved) + '\n');
    mindDir = resolved;

    if (existsSync(mindDir)) {
      // show contents
      let entries = [];
      try { entries = readdirSync(mindDir).filter(e => !e.startsWith('.')); } catch {}
      write('\n');
      write(tf('kbExists', mindDir) + '\n');
      if (entries.length) {
        const label = T.kbExistsFiles[uiLang] ?? T.kbExistsFiles.en;
        write(`  ${c.dim(label + ':')} ${entries.slice(0, 8).map(e => c.dim(e)).join('  ')}${entries.length > 8 ? c.dim('  …') : ''}\n`);
      } else {
        write(`  ${c.dim('(empty)')}\n`);
      }
      write('\n');

      const choice = await select('kbExistsFiles', 'kbExistsOpts', 'kbExistsVals');
      if (choice === 'reselect') { write('\n'); continue; }
      break;
    } else {
      // ── Template selection (part of Step 1) ─────────────────────────────
      write('\n');
      selectedTemplate = await select('tplPrompt', 'tplOptions', 'tplValues');
      mkdirSync(mindDir, { recursive: true });
      await applyTemplate(selectedTemplate, mindDir);
      break;
    }
  }

  // ── Step 2: AI Provider + API Key ─────────────────────────────────────────
  write('\n');
  stepHeader(2);

  const provider = await select('providerPrompt', 'providerOpts', 'providerVals');
  const isSkip = provider === 'skip';
  const isAnthropic = provider === 'anthropic';

  // preserve existing provider configs (use resumeCfg already read at top of main)
  let existingProviders = {
    anthropic: { apiKey: '', model: 'claude-sonnet-4-6' },
    openai:    { apiKey: '', model: 'gpt-5.4', baseUrl: '' },
  };
  let existingAiProvider = 'anthropic';
  if (resumeCfg.ai?.providers) {
    existingProviders = { ...existingProviders, ...resumeCfg.ai.providers };
  } else if (resumeCfg.ai?.anthropicApiKey) {
    existingProviders.anthropic = { apiKey: resumeCfg.ai.anthropicApiKey || '', model: resumeCfg.ai.anthropicModel || 'claude-sonnet-4-6' };
    existingProviders.openai    = { apiKey: resumeCfg.ai.openaiApiKey || '', model: resumeCfg.ai.openaiModel || 'gpt-5.4', baseUrl: resumeCfg.ai.openaiBaseUrl || '' };
  }
  if (resumeCfg.ai?.provider) existingAiProvider = resumeCfg.ai.provider;

  if (isSkip) {
    write(c.dim(t('providerSkip') + '\n'));
  } else {
    let apiKey = '';
    let baseUrl = '';
    if (isAnthropic) {
      apiKey = await askText('anthropicKey');
    } else {
      apiKey = await askText('openaiKey');
      baseUrl = await askText('openaiBase');
    }

    if (!apiKey) {
      write(c.yellow(t('apiKeyWarn') + '\n'));
    }

    if (isAnthropic) {
      existingProviders.anthropic = { apiKey, model: existingProviders.anthropic?.model || 'claude-sonnet-4-6' };
    } else {
      existingProviders.openai = { apiKey, model: existingProviders.openai?.model || 'gpt-5.4', baseUrl: baseUrl || '' };
    }
  }

  // ── Step 3: Ports ─────────────────────────────────────────────────────────
  write('\n');
  stepHeader(3);
  const existingCfg = resumeCfg;
  const defaultWebPort = typeof existingCfg.port === 'number' ? existingCfg.port : 3456;
  const defaultMcpPort = typeof existingCfg.mcpPort === 'number' ? existingCfg.mcpPort : (defaultWebPort === 8781 ? 8782 : 8781);
  let webPort, mcpPort;
  while (true) {
    webPort = await askPort('webPortPrompt', defaultWebPort);
    mcpPort = await askPort('mcpPortPrompt', defaultMcpPort);
    if (webPort !== mcpPort) break;
    write(c.yellow(`  ⚠ ${uiLang === 'zh' ? 'Web 端口和 MCP 端口不能相同，请重新选择' : 'Web port and MCP port must be different — please choose again'}\n`));
  }

  // ── Step 4: Auth token ────────────────────────────────────────────────────
  write('\n');
  stepHeader(4);
  // Resume: if config already has a token, offer it as the default (Enter = keep)
  const existingToken = existingCfg.authToken || '';
  let authToken;
  if (existingToken) {
    const masked = existingToken.length > 8 ? existingToken.slice(0, 8) + '····' : existingToken;
    write(c.dim(`  ${uiLang === 'zh' ? '现有 token：' : 'Current token:'} ${c.cyan(masked)}\n`));
    const keepToken = await askYesNoDefault('cfgConfirm');
    if (keepToken) {
      authToken = existingToken;
      console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(existingToken.slice(0, 8) + '····')}`);
    } else {
      const authSeed = await askText('authPrompt');
      authToken = generateToken(authSeed);
      console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(authToken)}`);
    }
  } else {
    const authSeed = await askText('authPrompt');
    authToken = generateToken(authSeed);
    console.log(`${c.green(t('tokenGenerated'))}: ${c.cyan(authToken)}`);
  }

  // ── Step 5: Web UI password ───────────────────────────────────────────────
  write('\n');
  stepHeader(5);
  let webPassword = '';
  const existingPassword = existingCfg.webPassword || '';
  if (existingPassword) {
    write(c.dim(`  ${uiLang === 'zh' ? '已设置密码（Enter 保留）' : 'Password is set (Enter to keep)'}\n`));
    const keepPass = await askYesNoDefault('cfgConfirm');
    if (keepPass) {
      webPassword = existingPassword;
    }
  }
  if (!webPassword) {
    while (true) {
      webPassword = await askText('webPassPrompt');
      if (webPassword) break;
      write(c.yellow(t('webPassWarn') + '\n'));
      const confirmed = await askYesNo('webPassSkip');
      if (confirmed) break;
    }
  }

  // ── Step 6: Start Mode ──────────────────────────────────────────────────
  write('\n');
  stepHeader(6);

  let startMode = 'start';
  const daemonPlatform = process.platform === 'darwin' || process.platform === 'linux';
  if (daemonPlatform) {
    startMode = await select('startModePrompt', 'startModeOpts', 'startModeVals');
  } else {
    write(c.dim(t('startModeSkip') + '\n'));
  }

  const config = {
    mindRoot:       mindDir,
    port:           webPort,
    mcpPort:        mcpPort,
    authToken:      authToken,
    webPassword:    webPassword || '',
    startMode:      startMode,
    disabledSkills: selectedTemplate === 'zh' ? ['mindos'] : ['mindos-zh'],
    ai: {
      provider:  isSkip ? existingAiProvider : (isAnthropic ? 'anthropic' : 'openai'),
      providers: existingProviders,
    },
  };

  // ── Configuration Summary & Confirmation ──────────────────────────────────
  const maskPw = (s) => s ? '•'.repeat(Math.min(s.length, 8)) : '';
  const maskTk = (s) => s && s.length > 8 ? s.slice(0, 8) + '····' : (s ? s.slice(0, 4) + '····' : '');
  const sep = '━'.repeat(40);
  write(`\n${sep}\n`);
  write(`${c.bold(uiLang === 'zh' ? '配置摘要' : 'Configuration Summary')}\n`);
  write(`${sep}\n`);
  write(`  ${c.dim('Knowledge base:')}  ${mindDir}\n`);
  write(`  ${c.dim('Web port:')}        ${webPort}\n`);
  write(`  ${c.dim('MCP port:')}        ${mcpPort}\n`);
  write(`  ${c.dim('Auth token:')}      ${maskTk(authToken)}\n`);
  if (webPassword) write(`  ${c.dim('Web password:')}    ${maskPw(webPassword)}\n`);
  write(`  ${c.dim('AI provider:')}     ${config.ai.provider}\n`);
  write(`  ${c.dim('Start mode:')}      ${startMode}\n`);
  write(`${sep}\n`);

  const confirmSave = await askYesNoDefault('cfgConfirm');
  if (!confirmSave) {
    console.log(c.red(t('cfgAborted')));
    process.exit(0);
  }

  const isResuming = Object.keys(resumeCfg).length > 0;
  const needsRestart = isResuming && (
    config.port        !== (resumeCfg.port        ?? 3456) ||
    config.mcpPort     !== (resumeCfg.mcpPort     ?? 8781) ||
    config.mindRoot    !== (resumeCfg.mindRoot     ?? '')   ||
    config.authToken   !== (resumeCfg.authToken   ?? '')   ||
    config.webPassword !== (resumeCfg.webPassword ?? '')
  );

  mkdirSync(MINDOS_DIR, { recursive: true });
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
  console.log(`\n${c.green(t('cfgSaved'))}: ${c.dim(CONFIG_PATH)}`);

  // ── Skill rules are now built into SKILL.md — no install needed ──────────
  // .mindos/user-preferences.md will be created on first preference capture or via `mindos init-skills`.

  // ── Step 7: Connection Mode ─────────────────────────────────────────────────
  write('\n');
  stepHeader(7);
  const modes = await runModeSelect();

  // ── Step 8: Agent Connection ────────────────────────────────────────────────
  write('\n');
  stepHeader(8);
  const selectedAgents = await runAgentSelect();

  if (selectedAgents.length > 0) {
    const agentCount = selectedAgents.length;
    write('\n' + c.dim(uiLang === 'zh'
      ? `  正在为 ${agentCount} 个 Agent 配置连接…`
      : `  Configuring connection for ${agentCount} agent${agentCount > 1 ? 's' : ''}…`) + '\n');

    if (modes.mcp) {
      installMcpConfig(selectedAgents, mcpPort, authToken);
    }
    const skillOk = runSkillInstallStep(selectedTemplate, selectedAgents);

    if (skillOk) {
      write(c.green(uiLang === 'zh'
        ? `  ✔ ${agentCount} 个 Agent 已配置完成\n`
        : `  ✔ ${agentCount} agent${agentCount > 1 ? 's' : ''} configured\n`));
    } else {
      write(c.yellow(uiLang === 'zh'
        ? `  ⚠ ${agentCount} 个 Agent 配置部分完成，CLI 指令文档安装失败\n`
        : `  ⚠ ${agentCount} agent${agentCount > 1 ? 's' : ''} partially configured — CLI guide install failed\n`));
    }

    if (!modes.mcp) {
      write(c.dim(uiLang === 'zh'
        ? '  → 如需启用 MCP 模式，随时运行 `mindos mcp install`。\n'
        : '  → To enable MCP mode later, run `mindos mcp install`.\n'));
    }
  }

  // ── Sync setup (optional) ──────────────────────────────────────────────────
  const wantSync = await askYesNo('syncSetup');
  if (wantSync) {
    const { initSync } = await import('../packages/mindos/bin/lib/sync.js');
    await initSync(mindDir);
  } else {
    console.log(c.dim(t('syncLater')));
  }

  // ── Register CLI globally if not already in PATH ────────────────────────────
  ensureCliInPath();

  const installDaemon = startMode === 'daemon' || process.argv.includes('--install-daemon');
  finish(mindDir, config.startMode, config.mcpPort, config.authToken, installDaemon, needsRestart, resumeCfg.port ?? 3456);
}

function ensureCliInPath() {
  try {
    execFileSync(process.platform === 'win32' ? 'where' : 'which', ['mindos'], { stdio: 'ignore' });
    return; // already in PATH (npm -g install or previous link)
  } catch { /* not found */ }

  // Only auto-register for source/dev installations.
  // Desktop (Electron) manages its own process; npx is ephemeral.
  const isDesktop = !!(process.env.ELECTRON_RUN_AS_NODE || process.env.MINDOS_DESKTOP);
  const isDevInstall = existsSync(resolve(ROOT, '.git'));
  if (isDesktop || !isDevInstall) return;

  write('\n');
  try {
    const invocation = resolveNpmInvocation(['link']);
    execFileSync(invocation.command, invocation.args, { cwd: ROOT, stdio: 'ignore' });
    write(c.green(uiLang === 'zh'
      ? '  ✔ mindos CLI 已注册到全局路径\n'
      : '  ✔ mindos CLI registered globally\n'));
  } catch {
    write(c.yellow(uiLang === 'zh'
      ? '  ⚠ 无法自动注册 CLI，请手动运行：npm link\n'
      : '  ⚠ Could not register CLI automatically. Run manually: npm link\n'));
  }
}

function getLocalIP() {
  for (const ifaces of Object.values(networkInterfaces())) {
    for (const iface of ifaces) {
      if (iface.family === 'IPv4' && !iface.internal) return iface.address;
    }
  }
  return null;
}

async function finish(mindDir, startMode = 'start', mcpPort = 8781, authToken = '', installDaemon = false, needsRestart = false, oldPort = 3456) {
  // startMode 'daemon' stored in config is equivalent to installDaemon flag
  if (startMode === 'daemon') {
    installDaemon = true;
    startMode = 'start';
  }
  if (needsRestart) {
    const isRunning = await isSelfPort(oldPort);
    if (isRunning) {
      write(c.yellow(t('restartRequired') + '\n'));
      const doRestart = await askYesNoDefault('restartNow');
      if (doRestart) {
        const cliPath = resolve(__dirname, '../packages/mindos/bin/cli.js');
        // Use 'restart' (stop → start) instead of bare 'start' which would
        // fail assertPortFree because the old process is still running.
        execFileSync(process.execPath, [cliPath, 'restart'], { stdio: 'inherit' });
      } else {
        write(c.dim(t('restartManual') + '\n'));
      }
      return;
    } else {
      write(c.dim(t('changesOnNextStart') + '\n'));
      // fall through to normal Start now? prompt
    }
  }

  const startCmd = installDaemon ? 'mindos start --daemon' : (startMode === 'dev' ? 'mindos dev' : 'mindos start');
  const lines = T.nextSteps[uiLang](startCmd);
  console.log('');
  lines.forEach((l) => console.log(l));

  const doStart = await askYesNoDefault('startNow');
  if (doStart) {
    const cliPath = resolve(__dirname, '../packages/mindos/bin/cli.js');
    if (installDaemon) {
      // Install and start as background service — returns immediately
      execFileSync(process.execPath, [cliPath, 'start', '--daemon'], { stdio: 'inherit' });
    } else {
      execFileSync(process.execPath, [cliPath, startMode], { stdio: 'inherit' });
    }
  }
}

main().catch((err) => {
  write(SHOW_CURSOR);
  console.error(err);
  process.exit(1);
});
