// Onboarding module - friendly first-run experience
// Handles messenger config checks, agent availability, and interactive setup
import { createInterface } from 'node:readline';
import { join } from 'node:path';
import { chmod, readFile, writeFile, mkdir } from 'node:fs/promises';
import { registry, isPlatformBlacklisted } from './registry.js';
import { AGIM_HOME } from './agim-paths.js';
const CONFIG_DIR = AGIM_HOME;
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
// ============================================
// Constants
// ============================================
// R12 / 2026-05-22 update — Telegram and Discord are kept in the source
// for IMHUB_ENABLE_GLOBAL_IM=1 deployments but hidden from the default
// onboarding flow (mainland-China baseline). DingTalk is added so the
// legacy prompt matches what the modern wizard surfaces.
const ALL_MESSENGERS = [
    { id: 'wechat-ilink', displayName: 'WeChat', description: 'Scan QR to login' },
    { id: 'feishu', displayName: 'Feishu/Lark', description: 'App ID and Secret' },
    { id: 'dingtalk', displayName: 'DingTalk', description: 'ClientID + ClientSecret (Stream mode)' },
    { id: 'telegram', displayName: 'Telegram', description: 'Bot token from @BotFather' },
    { id: 'discord', displayName: 'Discord', description: 'Bot token from Developer Portal' },
];
function getAvailableMessengers() {
    const globalSet = process.env.IMHUB_ENABLE_GLOBAL_IM === '1';
    // v1.2.69 — IMHUB_PLATFORM_BLACKLIST hides platforms from the
    // wizard / web admin entirely. Default is empty (no-op). Layered on
    // top of the GLOBAL_IM filter so an operator can still ban any
    // platform regardless of the global-IM mode.
    const base = globalSet
        ? ALL_MESSENGERS
        : ALL_MESSENGERS.filter((m) => m.id !== 'telegram' && m.id !== 'discord');
    return base.filter((m) => !isPlatformBlacklisted(m.id));
}
const AVAILABLE_MESSENGERS = ALL_MESSENGERS; // re-exported for tests; runtime uses getAvailableMessengers()
const AGENT_PACKAGES = {
    'claude-code': { agent: 'claude-code', npmPackage: '@anthropic-ai/claude-code', command: 'claude' },
    'codex': { agent: 'codex', npmPackage: '@openai/codex', command: 'codex' },
    'opencode': { agent: 'opencode', npmPackage: 'opencode-ai', command: 'opencode' },
    'antigravity': {
        agent: 'antigravity',
        npmPackage: '',
        command: 'agy',
        // Google's official Antigravity CLI is distributed via an install
        // script, not npm. Surface that one-liner so the user-facing hint
        // doesn't tell people to `npm i -g antigravity` (which would 404
        // or install some unrelated package).
        installCmd: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
    },
    // D+ Stage 2 sub-PR #4 — native (in-process LLM) agent. No CLI to
    // install; "availability" means an LLM backend with role native-chat
    // (or cheap fallback) is configured. The hint string steers operators
    // at the right docs page instead of suggesting an npm install.
    'native': {
        agent: 'native',
        npmPackage: '',
        command: '',
        installCmd: 'Configure a native LLM backend in ~/.agim/config.json (llmBackends + llmRoles) — see docs/llm-backends.md',
    },
};
// ============================================
// ============================================
// Agent availability cache (TTL-based)
// ============================================
// ============================================
// Config helpers (extracted for reuse)
// ============================================
export async function loadConfig() {
    try {
        const data = await readFile(CONFIG_FILE, 'utf-8');
        return JSON.parse(data);
    }
    catch {
        return {
            messengers: [],
            agents: [],
            defaultAgent: 'claude-code',
        };
    }
}
export async function saveConfig(config) {
    await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
    await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
    await chmod(CONFIG_FILE, 0o600);
}
// ============================================
// Readline helper
// ============================================
function createPromptInterface() {
    return createInterface({
        input: process.stdin,
        output: process.stdout,
    });
}
async function prompt(rl, question) {
    return new Promise((resolve) => {
        rl.question(question, (answer) => {
            resolve(answer.trim());
        });
    });
}
// ============================================
// 1. checkMessengerConfig (SYNC)
// ============================================
/**
 * Check if messengers are configured.
 * IMPORTANT: This checks the config.messengers array BEFORE any default fill.
 */
export function checkMessengerConfig(config) {
    if (config.messengers.length === 0) {
        return {
            needsOnboarding: true,
            availableMessengers: getAvailableMessengers(),
        };
    }
    return {
        needsOnboarding: false,
        availableMessengers: [],
        warnings: [],
    };
}
// ============================================
// 2. checkAgentAvailability (ASYNC, TTL-cached)
// ============================================
// Cache with TTL so agent install/uninstall is reflected within 2 minutes
let agentCheckCache = null;
const AGENT_CHECK_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
/** Per-agent cached `isAvailable()` results — fall-through cache for
 * `isAgentAvailableCached` when an agent isn't represented in the bulk
 * snapshot (e.g. registered after the last refresh, or the bulk hasn't
 * run yet). The bulk cache is consulted first; this Map only matters for
 * the fallback probe path. */
const perAgentCache = new Map();
/**
 * Check which agents are available.
 * Result is cached with a 2-minute TTL.
 */
export async function checkAgentAvailability() {
    if (agentCheckCache && Date.now() - agentCheckCache.timestamp < AGENT_CHECK_CACHE_TTL) {
        return agentCheckCache.result;
    }
    const agentNames = registry.listAgents();
    const available = [];
    const missing = [];
    for (const name of agentNames) {
        const agent = registry.findAgent(name);
        if (agent) {
            try {
                const isAvail = await agent.isAvailable();
                if (isAvail) {
                    available.push(name);
                }
                else {
                    missing.push(name);
                }
                perAgentCache.set(name, { ok: isAvail, timestamp: Date.now() });
            }
            catch {
                missing.push(name);
                perAgentCache.set(name, { ok: false, timestamp: Date.now() });
            }
        }
    }
    const result = {
        available,
        missing,
        allMissing: available.length === 0,
    };
    agentCheckCache = { result, timestamp: Date.now() };
    return result;
}
/**
 * Check if a specific agent is available.
 *
 * Uses the bulk cache when present, but falls through to a one-off probe
 * if the agent name isn't represented (e.g. agent registered after the
 * last bulk refresh). The probe result is itself cached for the TTL.
 */
export async function isAgentAvailableCached(agentName) {
    const bulk = await checkAgentAvailability();
    if (bulk.available.includes(agentName))
        return true;
    if (bulk.missing.includes(agentName))
        return false;
    // Not represented in bulk — probe directly with a per-agent cache.
    const cached = perAgentCache.get(agentName);
    if (cached && Date.now() - cached.timestamp < AGENT_CHECK_CACHE_TTL) {
        return cached.ok;
    }
    const agent = registry.findAgent(agentName);
    if (!agent)
        return false;
    try {
        const ok = await agent.isAvailable();
        perAgentCache.set(agentName, { ok, timestamp: Date.now() });
        return ok;
    }
    catch {
        perAgentCache.set(agentName, { ok: false, timestamp: Date.now() });
        return false;
    }
}
/**
 * Clear the agent availability caches. Useful for tests that change the
 * registry state, and for the future `/router refresh` ops command.
 */
export function clearAgentCache() {
    agentCheckCache = null;
    perAgentCache.clear();
}
// ============================================
// 3. formatAgentInstallHint
// ============================================
/**
 * Generate friendly install message for missing agents
 */
export function formatAgentInstallHint(missing) {
    if (missing.length === 0)
        return '';
    const getInstallHint = (name) => {
        const hint = AGENT_PACKAGES[name];
        if (!hint)
            return `npm i -g ${name}`;
        if (hint.installCmd)
            return hint.installCmd;
        return `npm i -g ${hint.npmPackage}`;
    };
    if (missing.length === 1) {
        return `Install ${missing[0]}: ${getInstallHint(missing[0])}`;
    }
    const hints = missing.map((name) => `  - ${name}: ${getInstallHint(name)}`);
    return `Install agents:\n${hints.join('\n')}`;
}
/**
 * Format error message when agent is not available
 */
export function formatAgentNotAvailableError(agentName) {
    let installHint;
    if (agentName === 'antigravity') {
        installHint = `安装方式:
  curl -fsSL https://antigravity.google/cli/install.sh | bash
然后跑一次 \`agy\` 走 Google 账号 OAuth 完成首次登录。`;
    }
    else {
        const hint = AGENT_PACKAGES[agentName];
        if (hint?.installCmd)
            installHint = hint.installCmd;
        else
            installHint = hint ? `npm i -g ${hint.npmPackage}` : `npm i -g ${agentName}`;
    }
    return `❌ ${agentName} is not installed or not available.\n\n${installHint}\n\nOr try another agent:\n  /cc - Claude Code\n  /cx - Codex\n  /oc - OpenCode\n  /agy - Antigravity`;
}
// ============================================
// 4. runMessengerOnboarding
// ============================================
/**
 * Interactive onboarding flow.
 * Returns updated config on success, null on failure/cancel.
 */
export async function runMessengerOnboarding(config) {
    // Non-interactive fallback — cli.ts handles this branch directly now
    // (web-console-only deployments shouldn't exit). Keep this guard so
    // direct callers of runMessengerOnboarding still get a sane no-op.
    if (!process.stdin.isTTY) {
        console.log('No TTY; skip messenger onboarding. Run interactively:');
        console.log('   agim config wechat');
        console.log('   agim config feishu');
        console.log('   agim config dingtalk');
        return null;
    }
    const rl = createPromptInterface();
    try {
        const messengers = getAvailableMessengers();
        while (true) {
            // Show menu
            console.log('\n📱 Let\'s set up a messenger:\n');
            messengers.forEach((m, i) => {
                console.log(`  ${i + 1}. ${m.displayName} - ${m.description}`);
            });
            console.log(`  Q. Quit and configure manually\n`);
            const choice = await prompt(rl, `Select a messenger (1-${messengers.length} or Q): `);
            // Handle quit
            if (choice.toLowerCase() === 'q' || choice === '') {
                console.log('\n❌ Onboarding cancelled.');
                console.log('Run "agim config <messenger>" to configure manually.');
                return null;
            }
            const index = parseInt(choice, 10) - 1;
            if (index < 0 || index >= messengers.length) {
                console.log(`Invalid choice. Please enter 1-${messengers.length} or Q.`);
                continue;
            }
            const selected = messengers[index];
            // Run the config flow
            const result = await configureMessenger(selected.id, config, rl);
            if (result) {
                rl.close();
                return result;
            }
            // Config failed - show retry menu
            console.log(`\n❌ Failed to configure ${selected.displayName}.`);
            const retry = await prompt(rl, '[R] Retry, [S] Select different, [Q] Quit: ');
            if (retry.toLowerCase() === 'q') {
                rl.close();
                return null;
            }
            else if (retry.toLowerCase() === 's') {
            }
            // 'r' or anything else = retry same messenger (loop continues)
        }
    }
    catch (error) {
        const errMsg = error instanceof Error ? error.message : String(error);
        console.error(`\n❌ Onboarding error: ${errMsg}`);
        rl.close();
        return null;
    }
}
// ============================================
// Messenger config flows
// ============================================
async function configureMessenger(messengerId, config, rl) {
    switch (messengerId) {
        case 'wechat-ilink':
            return configureWeChat(config, rl);
        case 'telegram':
            return configureTelegram(config, rl);
        case 'feishu':
            return configureFeishu(config, rl);
        case 'discord':
            return configureDiscord(config, rl);
        default:
            console.log(`Unknown messenger: ${messengerId}`);
            return null;
    }
}
async function configureWeChat(config, _rl) {
    console.log('\n📱 Configuring WeChat...');
    console.log('Fetching QR code...\n');
    try {
        const { ILinkWeChatAdapter } = await import('../plugins/messengers/wechat/ilink-adapter.js');
        const adapter = new ILinkWeChatAdapter();
        const { qrUrl, qrToken } = await adapter.startQRLogin();
        console.log('📱 Scan this QR code with WeChat:\n');
        console.log(qrUrl);
        console.log('\n');
        const credentials = await adapter.waitForQRLogin(qrToken, (status) => {
            console.log(`[${new Date().toLocaleTimeString()}] ${status}`);
        });
        if (credentials) {
            console.log(`\n✅ Logged in as ${credentials.userId}`);
            console.log(`   Bot ID: ${credentials.accountId}`);
            if (!config.messengers.includes('wechat-ilink')) {
                config.messengers.push('wechat-ilink');
            }
            return config;
        }
        else {
            console.log('\n❌ Login failed or timed out');
            return null;
        }
    }
    catch (error) {
        const errMsg = error instanceof Error ? error.message : String(error);
        console.error(`\n❌ Failed to configure WeChat: ${errMsg}`);
        return null;
    }
}
async function configureTelegram(config, rl) {
    console.log('\n📱 Configuring Telegram...');
    console.log('To get a bot token:');
    console.log('1. Open Telegram and search for @BotFather');
    console.log('2. Send /newbot and follow instructions');
    console.log('3. Copy the bot token\n');
    const token = await prompt(rl, 'Enter your bot token: ');
    if (!token) {
        console.log('❌ Bot token is required');
        return null;
    }
    const channelId = await prompt(rl, 'Enter channel ID (optional, press Enter for default): ');
    config.telegram = { botToken: token, channelId: channelId || 'default' };
    if (!config.messengers.includes('telegram')) {
        config.messengers.push('telegram');
    }
    console.log('✅ Telegram bot token saved');
    console.log(`   Channel ID: ${config.telegram.channelId}`);
    return config;
}
async function configureFeishu(config, rl) {
    console.log('\n📱 Configuring Feishu (WebSocket long polling mode)...');
    console.log('To create a Feishu bot:');
    console.log('1. Go to https://open.feishu.cn/app');
    console.log('2. Create a custom bot app');
    console.log('3. Enable Bot capability');
    console.log('4. Configure event subscriptions (Subscribe to "Receive Message" event)');
    console.log('5. Copy App ID and App Secret\n');
    const appId = await prompt(rl, 'Enter App ID: ');
    const appSecret = await prompt(rl, 'Enter App Secret: ');
    if (!appId || !appSecret) {
        console.log('❌ App ID and App Secret are required');
        return null;
    }
    config.feishu = { appId, appSecret };
    if (!config.messengers.includes('feishu')) {
        config.messengers.push('feishu');
    }
    console.log('✅ Feishu bot credentials saved');
    console.log('\n✅ Using WebSocket long polling mode - no webhook configuration needed!');
    console.log('   The bot will automatically connect to Feishu servers.');
    return config;
}
async function configureDiscord(config, rl) {
    console.log('\n📱 Configuring Discord...');
    console.log('To create a Discord bot:');
    console.log('1. Go to https://discord.com/developers/applications');
    console.log('2. Click "New Application" and give it a name');
    console.log('3. Go to "Bot" tab and click "Add Bot"');
    console.log('4. IMPORTANT: Enable "MESSAGE CONTENT INTENT" under Privileged Gateway Intents');
    console.log('5. Click "Reset Token" to get your bot token');
    console.log('6. Go to OAuth2 > URL Generator:');
    console.log('   - Scopes: bot');
    console.log('   - Bot Permissions: Send Messages, Read Message History, View Channels');
    console.log('7. Use the generated URL to invite the bot to your server\n');
    const token = await prompt(rl, 'Enter your bot token: ');
    if (!token) {
        console.log('❌ Bot token is required');
        return null;
    }
    const channelId = await prompt(rl, 'Enter channel ID (optional, press Enter for default): ');
    const allowedGuilds = await prompt(rl, 'Allowed guild IDs (comma-separated, optional): ');
    const allowedChannels = await prompt(rl, 'Allowed channel IDs (comma-separated, optional): ');
    config.discord = {
        botToken: token,
        channelId: channelId || 'default',
        allowedGuilds: allowedGuilds ? allowedGuilds.split(',').map(s => s.trim()).filter(Boolean) : undefined,
        allowedChannels: allowedChannels ? allowedChannels.split(',').map(s => s.trim()).filter(Boolean) : undefined,
    };
    if (!config.messengers.includes('discord')) {
        config.messengers.push('discord');
    }
    console.log('✅ Discord bot token saved');
    console.log(`   Channel ID: ${config.discord.channelId}`);
    if (config.discord.allowedGuilds?.length) {
        console.log(`   Allowed guilds: ${config.discord.allowedGuilds.join(', ')}`);
    }
    if (config.discord.allowedChannels?.length) {
        console.log(`   Allowed channels: ${config.discord.allowedChannels.join(', ')}`);
    }
    return config;
}
// ============================================
// Error message formatter for messenger.start()
// ============================================
/**
 * Format a user-friendly error message for messenger startup failures
 */
export function formatMessengerStartError(messengerName, error) {
    const errMsg = error instanceof Error ? error.message : String(error);
    // Check for credential/auth errors
    if (errMsg.toLowerCase().includes('credentials') || errMsg.toLowerCase().includes('auth')) {
        const configName = messengerName.replace('-ilink', '');
        return `Run "agim config ${configName}" to reconfigure.`;
    }
    // Generic error
    return errMsg;
}
//# sourceMappingURL=onboarding.js.map