import * as path from 'node:path';
import { ProxyAgent, setGlobalDispatcher } from 'undici';
import { normalizeProxyUrl, Storage } from '@hoptrendy/hopcode-core';
import { loadSettings } from '../../config/settings.js';
import { writeStderrLine, writeStdoutLine } from '../../utils/stdioHelpers.js';
import { AcpBridge, SessionRouter } from '@hoptrendy/channel-base';
import { getPlugin, registerPlugin } from './channel-registry.js';
import { findCliEntryPath, parseChannelConfig } from './config-utils.js';
import { readServiceInfo, writeServiceInfo, removeServiceInfo, } from './pidfile.js';
import { getExtensionManager } from '../extensions/utils.js';
const MAX_CRASH_RESTARTS = 3;
const CRASH_WINDOW_MS = 5 * 60 * 1000; // 5-minute window for counting crashes
const RESTART_DELAY_MS = 3000;
/**
 * Resolve and apply proxy settings for the channel service process.
 *
 * The normal CLI path applies proxy via loadCliConfig → Config constructor →
 * setGlobalDispatcher, but `channel start` never calls loadCliConfig. This
 * replicates the same resolution logic (--proxy flag → settings.proxy →
 * HTTPS_PROXY → HTTP_PROXY) and applies the global dispatcher for native
 * fetch() calls. The resolved URL is also passed to channels via
 * ChannelBaseOptions so adapters can configure their own HTTP clients (e.g.
 * grammy uses node-fetch which needs a separate agent).
 */
export function resolveProxy(cliProxy, settingsProxy) {
    const proxyUrl = normalizeProxyUrl(cliProxy ||
        settingsProxy ||
        process.env['HTTPS_PROXY'] ||
        process.env['https_proxy'] ||
        process.env['HTTP_PROXY'] ||
        process.env['http_proxy']);
    if (proxyUrl) {
        setGlobalDispatcher(new ProxyAgent(proxyUrl));
    }
    return proxyUrl;
}
function sessionsPath() {
    return path.join(Storage.getGlobalHopCodeDir(), 'channels', 'sessions.json');
}
function loadChannelsConfig() {
    const settings = loadSettings(process.cwd());
    const channels = settings.merged.channels;
    return channels || {};
}
/**
 * Load channel plugins from active extensions.
 * Extensions declare channels in their hopcode-extension.json manifest.
 */
async function loadChannelsFromExtensions() {
    let loaded = 0;
    try {
        const extensionManager = await getExtensionManager();
        const extensions = extensionManager
            .getLoadedExtensions()
            .filter((e) => e.isActive && e.channels);
        for (const ext of extensions) {
            for (const [channelType, channelDef] of Object.entries(ext.channels)) {
                if (await getPlugin(channelType)) {
                    writeStderrLine(`[Extensions] Skipping channel "${channelType}" from "${ext.name}": type already registered`);
                    continue;
                }
                const entryPath = path.join(ext.path, channelDef.entry);
                try {
                    const module = (await import(entryPath));
                    const plugin = module.plugin;
                    if (!plugin || typeof plugin.createChannel !== 'function') {
                        writeStderrLine(`[Extensions] "${ext.name}": channel entry point does not export a valid plugin object`);
                        continue;
                    }
                    if (plugin.channelType !== channelType) {
                        writeStderrLine(`[Extensions] "${ext.name}": channelType mismatch — manifest says "${channelType}", plugin says "${plugin.channelType}"`);
                        continue;
                    }
                    registerPlugin(plugin);
                    loaded++;
                    writeStdoutLine(`[Extensions] Loaded channel "${channelType}" from "${ext.name}"`);
                }
                catch (err) {
                    writeStderrLine(`[Extensions] Failed to load channel "${channelType}" from "${ext.name}": ${err instanceof Error ? err.message : String(err)}`);
                }
            }
        }
    }
    catch (err) {
        writeStderrLine(`[Extensions] Failed to load extensions: ${err instanceof Error ? err.message : String(err)}`);
    }
    return loaded;
}
async function createChannel(name, config, bridge, options) {
    const channelPlugin = await getPlugin(config.type);
    if (!channelPlugin) {
        throw new Error(`Unknown channel type: "${config.type}".`);
    }
    return channelPlugin.createChannel(name, config, bridge, options);
}
function registerToolCallDispatch(bridge, router, channels) {
    bridge.on('toolCall', (event) => {
        const target = router.getTarget(event.sessionId);
        if (target) {
            const channel = channels.get(target.channelName);
            if (channel) {
                channel.onToolCall(target.chatId, event);
            }
        }
    });
}
/** Check for duplicate instance and abort if one is already running. */
function checkDuplicateInstance() {
    const existing = readServiceInfo();
    if (existing) {
        writeStderrLine(`Error: Channel service is already running (PID ${existing.pid}, started ${existing.startedAt}).`);
        writeStderrLine('Use "HopCode channel stop" to stop it first.');
        process.exit(1);
    }
}
/** Start a single channel with its own bridge + crash recovery. */
async function startSingle(name, proxy) {
    checkDuplicateInstance();
    const channelsConfig = loadChannelsConfig();
    await loadChannelsFromExtensions();
    if (!channelsConfig[name]) {
        writeStderrLine(`Error: Channel "${name}" not found in settings. Add it to channels.${name} in settings.json.`);
        process.exit(1);
    }
    let config;
    try {
        config = await parseChannelConfig(name, channelsConfig[name]);
    }
    catch (err) {
        writeStderrLine(`Error: ${err instanceof Error ? err.message : String(err)}`);
        process.exit(1);
    }
    const cliEntryPath = findCliEntryPath();
    let shuttingDown = false;
    const crashTimestamps = [];
    const bridgeOpts = { cliEntryPath, cwd: config.cwd, model: config.model };
    let bridge = new AcpBridge(bridgeOpts);
    await bridge.start();
    const router = new SessionRouter(bridge, config.cwd, config.sessionScope, sessionsPath());
    const channels = new Map();
    const channel = await createChannel(name, config, bridge, { router, proxy });
    channels.set(name, channel);
    registerToolCallDispatch(bridge, router, channels);
    try {
        await channel.connect();
    }
    catch (err) {
        writeStderrLine(`Error: ${err instanceof Error ? err.message : String(err)}`);
        bridge.stop();
        process.exit(1);
    }
    writeServiceInfo([name]);
    writeStdoutLine(`[Channel] "${name}" is running. Press Ctrl+C to stop.`);
    const attachDisconnectHandler = (b) => {
        b.on('disconnected', async () => {
            if (shuttingDown)
                return;
            const now = Date.now();
            crashTimestamps.push(now);
            // Only count crashes within the recent window
            const recentCrashes = crashTimestamps.filter((ts) => now - ts < CRASH_WINDOW_MS);
            if (recentCrashes.length > MAX_CRASH_RESTARTS) {
                writeStderrLine(`[Channel] Bridge crashed ${recentCrashes.length} times in ${CRASH_WINDOW_MS / 1000}s. Giving up.`);
                channel.disconnect();
                router.clearAll();
                removeServiceInfo();
                process.exit(1);
            }
            writeStderrLine(`[Channel] Bridge crashed (${recentCrashes.length}/${MAX_CRASH_RESTARTS} in window). Restarting in ${RESTART_DELAY_MS / 1000}s...`);
            await new Promise((r) => setTimeout(r, RESTART_DELAY_MS));
            try {
                bridge = new AcpBridge(bridgeOpts);
                await bridge.start();
                router.setBridge(bridge);
                channel.setBridge(bridge);
                registerToolCallDispatch(bridge, router, channels);
                attachDisconnectHandler(bridge);
                const result = await router.restoreSessions();
                writeStdoutLine(`[Channel] Bridge restarted. Sessions restored: ${result.restored}, failed: ${result.failed}`);
            }
            catch (err) {
                writeStderrLine(`[Channel] Failed to restart bridge: ${err instanceof Error ? err.message : String(err)}`);
            }
        });
    };
    attachDisconnectHandler(bridge);
    const shutdown = () => {
        shuttingDown = true;
        writeStdoutLine('\n[Channel] Shutting down...');
        channel.disconnect();
        bridge.stop();
        router.clearAll();
        removeServiceInfo();
        process.exit(0);
    };
    process.on('SIGINT', shutdown);
    process.on('SIGTERM', shutdown);
    await new Promise(() => { });
}
/** Start all configured channels with a shared bridge + crash recovery. */
async function startAll(proxy) {
    checkDuplicateInstance();
    const channelsConfig = loadChannelsConfig();
    await loadChannelsFromExtensions();
    if (Object.keys(channelsConfig).length === 0) {
        writeStderrLine('Error: No channels configured in settings.json. Add entries under "channels".');
        process.exit(1);
    }
    // Parse all configs upfront — fail fast on bad config
    const parsed = [];
    for (const [name, raw] of Object.entries(channelsConfig)) {
        try {
            parsed.push({
                name,
                config: await parseChannelConfig(name, raw),
            });
        }
        catch (err) {
            writeStderrLine(`Error in channel "${name}": ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        }
    }
    const cliEntryPath = findCliEntryPath();
    const defaultCwd = process.cwd();
    let shuttingDown = false;
    const crashTimestamps = [];
    // All channels share one bridge process. Use the first channel's model.
    const models = [
        ...new Set(parsed.map((p) => p.config.model).filter(Boolean)),
    ];
    if (models.length > 1) {
        writeStderrLine(`[Channel] Warning: Multiple models configured (${models.join(', ')}). ` +
            `Shared bridge will use "${models[0]}".`);
    }
    const bridgeOpts = {
        cliEntryPath,
        cwd: defaultCwd,
        model: models[0],
    };
    let bridge = new AcpBridge(bridgeOpts);
    await bridge.start();
    const router = new SessionRouter(bridge, defaultCwd, 'user', sessionsPath());
    // Register per-channel scope overrides so each channel uses its own sessionScope
    for (const { name, config } of parsed) {
        router.setChannelScope(name, config.sessionScope);
    }
    const channels = new Map();
    writeStdoutLine(`[Channel] Starting ${parsed.length} channel(s): ${parsed.map((p) => p.name).join(', ')}`);
    for (const { name, config } of parsed) {
        channels.set(name, await createChannel(name, config, bridge, { router, proxy }));
    }
    registerToolCallDispatch(bridge, router, channels);
    // Connect all channels
    let connectedCount = 0;
    for (const [name, channel] of channels) {
        try {
            await channel.connect();
            connectedCount++;
            writeStdoutLine(`[Channel] "${name}" connected.`);
        }
        catch (err) {
            writeStderrLine(`[Channel] Failed to connect "${name}": ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    if (connectedCount === 0) {
        writeStderrLine('[Channel] No channels connected. Exiting.');
        bridge.stop();
        process.exit(1);
    }
    writeServiceInfo(parsed.map((p) => p.name));
    writeStdoutLine(`[Channel] Running ${connectedCount} channel(s). Press Ctrl+C to stop.`);
    const attachDisconnectHandler = (b) => {
        b.on('disconnected', async () => {
            if (shuttingDown)
                return;
            const now = Date.now();
            crashTimestamps.push(now);
            const recentCrashes = crashTimestamps.filter((ts) => now - ts < CRASH_WINDOW_MS);
            if (recentCrashes.length > MAX_CRASH_RESTARTS) {
                writeStderrLine(`[Channel] Bridge crashed ${recentCrashes.length} times in ${CRASH_WINDOW_MS / 1000}s. Giving up.`);
                for (const channel of channels.values()) {
                    try {
                        channel.disconnect();
                    }
                    catch {
                        // best-effort
                    }
                }
                router.clearAll();
                removeServiceInfo();
                process.exit(1);
            }
            writeStderrLine(`[Channel] Bridge crashed (${recentCrashes.length}/${MAX_CRASH_RESTARTS} in window). Restarting in ${RESTART_DELAY_MS / 1000}s...`);
            await new Promise((r) => setTimeout(r, RESTART_DELAY_MS));
            try {
                bridge = new AcpBridge(bridgeOpts);
                await bridge.start();
                router.setBridge(bridge);
                for (const channel of channels.values()) {
                    channel.setBridge(bridge);
                }
                registerToolCallDispatch(bridge, router, channels);
                attachDisconnectHandler(bridge);
                const result = await router.restoreSessions();
                writeStdoutLine(`[Channel] Bridge restarted. Sessions restored: ${result.restored}, failed: ${result.failed}`);
            }
            catch (err) {
                writeStderrLine(`[Channel] Failed to restart bridge: ${err instanceof Error ? err.message : String(err)}`);
            }
        });
    };
    attachDisconnectHandler(bridge);
    const shutdown = () => {
        shuttingDown = true;
        writeStdoutLine('\n[Channel] Shutting down...');
        for (const [name, channel] of channels) {
            try {
                channel.disconnect();
                writeStdoutLine(`[Channel] "${name}" disconnected.`);
            }
            catch {
                // best-effort
            }
        }
        bridge.stop();
        router.clearAll();
        removeServiceInfo();
        process.exit(0);
    };
    process.on('SIGINT', shutdown);
    process.on('SIGTERM', shutdown);
    await new Promise(() => { });
}
export const startCommand = {
    command: 'start [name]',
    describe: 'Start channels (all if no name given, or a single named channel)',
    builder: (yargs) => yargs.positional('name', {
        type: 'string',
        describe: 'Channel name (omit to start all configured channels)',
    }),
    handler: async (argv) => {
        const settings = loadSettings(process.cwd());
        const proxy = resolveProxy(argv['proxy'], settings.merged.proxy);
        if (argv.name) {
            await startSingle(argv.name, proxy);
        }
        else {
            await startAll(proxy);
        }
    },
};
//# sourceMappingURL=start.js.map