/**
 * Bun Dev Server
 *
 * Spawns Bun with --hot as a subprocess. Bun's --hot mode re-evaluates changed
 * modules and hot-swaps the default export's `fetch` handler on the running
 * server — no process restart, no port rebind, no dropped connections.
 *
 * The user's app.ts exports the result of createApp() which includes `fetch`
 * and `port` properties that Bun uses to manage the server lifecycle.
 *
 * Key requirements for bun --hot:
 * - app.ts MUST have `export default` with { fetch, port } properties
 * - Without export default, Bun runs the code but never starts an HTTP server
 */
import { getAgentEnv } from '../../../agent-detection';
import { createServer as createNetServer } from 'node:net';
/**
 * Check if a port is available for binding.
 * Returns true if the port is free, false if in use.
 */
function isPortAvailable(port, host = '127.0.0.1') {
    return new Promise((resolve) => {
        const server = createNetServer();
        server.once('error', () => {
            resolve(false);
        });
        server.listen(port, host, () => {
            server.close(() => {
                resolve(true);
            });
        });
    });
}
/**
 * Kill any process listening on the specified port.
 * Uses lsof on Unix systems to find and kill the process.
 */
async function killProcessOnPort(port, logger) {
    if (process.platform === 'win32') {
        // Windows: use netstat to find PID, then taskkill
        // This is more complex and less reliable, skip for now
        return false;
    }
    try {
        // Find PIDs listening on the port
        const result = Bun.spawnSync(['lsof', '-t', '-i', `:${port}`], {
            stdout: 'pipe',
            stderr: 'ignore',
        });
        if (result.exitCode !== 0 || !result.stdout) {
            return false;
        }
        const pids = new TextDecoder()
            .decode(result.stdout)
            .trim()
            .split('\n')
            .filter((line) => line && /^\d+$/.test(line));
        if (pids.length === 0) {
            return false;
        }
        // Kill each process
        for (const pid of pids) {
            try {
                // Use SIGKILL to ensure cleanup
                const killResult = Bun.spawnSync(['kill', '-9', pid], {
                    stdout: 'ignore',
                    stderr: 'ignore',
                });
                if (killResult.exitCode === 0) {
                    logger.debug('Killed orphan process %s on port %d', pid, port);
                }
            }
            catch {
                // Ignore kill errors
            }
        }
        // Brief pause to let the port be released
        await new Promise((resolve) => setTimeout(resolve, 100));
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Ensure the port is available, cleaning up any orphan processes if needed.
 */
async function ensurePortAvailable(port, logger) {
    const available = await isPortAvailable(port);
    if (available) {
        return;
    }
    logger.debug('Port %d is in use, attempting to clean up orphan process...', port);
    const killed = await killProcessOnPort(port, logger);
    if (killed) {
        // Verify the port is now free
        const nowAvailable = await isPortAvailable(port);
        if (!nowAvailable) {
            throw new Error(`Port ${port} is still in use after cleanup. Another process may be holding it.\n` +
                `Run 'lsof -i :${port}' to identify the process.`);
        }
        logger.debug('Port %d is now available', port);
    }
    else {
        throw new Error(`Port ${port} is already in use.\n` +
            `Run 'lsof -i :${port}' to identify the process, or kill it manually.`);
    }
}
/**
 * Validate app.ts for common issues that prevent Bun --hot from starting.
 *
 * Bun --hot requires `export default { fetch, port }` to start a server.
 * Common mistakes:
 * - No `export default` (v1 pattern: destructuring result without exporting)
 * - Calling createApp() but not exporting it
 *
 * @internal Exported for testing only
 */
export async function validateAppTs(appPath) {
    const result = {
        hasDefaultExport: false,
        hasCreateApp: false,
        isV1Pattern: false,
        hints: [],
    };
    const file = Bun.file(appPath);
    if (!(await file.exists())) {
        return result;
    }
    const content = await file.text();
    // Strip comments to avoid false positives from commented-out code
    // Simple approach: remove single-line and multi-line comments
    const codeWithoutComments = content
        .replace(/\/\/.*$/gm, '') // Single-line comments
        .replace(/\/\*[\s\S]*?\*\//g, ''); // Multi-line comments
    // Check for default export patterns (only in actual code, not comments)
    // Matches: export default createApp(...), export default await createApp(...),
    // export default { fetch, port }, const { ... } = await createApp(...) then export default result
    result.hasDefaultExport = /\bexport\s+default\b/.test(codeWithoutComments);
    // Check for createApp call
    result.hasCreateApp = /\bcreateApp\s*\(/.test(content);
    // Detect v1 pattern: destructuring createApp result without export default
    // e.g., const { server, logger } = await createApp({...});
    const hasDestructuring = /const\s*\{[^}]*\}\s*=\s*(?:await\s+)?createApp/.test(content);
    if (hasDestructuring && !result.hasDefaultExport) {
        result.isV1Pattern = true;
        result.hints.push('app.ts calls createApp() but does not export it. Bun --hot requires `export default` to start a server.', '', 'Fix: Change your app.ts to export the createApp() result:', '', '  import { createApp } from "@agentuity/runtime";', '  import agents from "@agent/index";', '', '  export default createApp({', '    agents,', '    router: { path: "/api", router: api },', '  });', '', 'Or if you need the logger:', '', '  const app = await createApp({ agents });', '  app.logger.debug("Running %s", app.server.url);', '  export default app;');
    }
    // Check for missing createApp entirely
    if (!result.hasCreateApp && !content.includes('Bun.serve')) {
        result.hints.push('app.ts does not call createApp(). This is required for Agentuity apps.', '', 'Example:', '', '  import { createApp } from "@agentuity/runtime";', '  export default createApp({ agents });');
    }
    return result;
}
/**
 * Build a detailed error message with validation hints and captured output.
 *
 * @internal Exported for testing only
 */
export function buildStartupErrorMessage(port, timeoutMs, stderr, validation, stdout) {
    const lines = [];
    lines.push(`Bun server failed to start on port ${port} after ${timeoutMs}ms`);
    lines.push('');
    // Show captured stderr if any
    if (stderr.trim()) {
        lines.push('Bun stderr:');
        lines.push('');
        // Indent stderr lines for readability
        for (const line of stderr.trim().split('\n').slice(0, 20)) {
            lines.push(`  ${line}`);
        }
        if (stderr.split('\n').length > 20) {
            lines.push('  ... (truncated)');
        }
        lines.push('');
    }
    // Show captured stdout if any (runtime errors, uncaught exceptions, etc.)
    if (stdout?.trim()) {
        lines.push('Bun stdout:');
        lines.push('');
        for (const line of stdout.trim().split('\n').slice(0, 20)) {
            lines.push(`  ${line}`);
        }
        if (stdout.split('\n').length > 20) {
            lines.push('  ... (truncated)');
        }
        lines.push('');
    }
    // Show validation hints
    if (validation.hints.length > 0) {
        lines.push('Possible issue:');
        lines.push('');
        for (const hint of validation.hints) {
            lines.push(`  ${hint}`);
        }
        lines.push('');
    }
    // Generic troubleshooting if no specific hints
    if (validation.hints.length === 0) {
        lines.push('Troubleshooting:');
        lines.push('');
        lines.push('  1. Check app.ts exports `export default createApp({...})`');
        lines.push('  2. Check for TypeScript/syntax errors in your code');
        lines.push(`  3. Check if port ${port} is already in use: lsof -i :${port}`);
        lines.push('  4. Try running manually: bun run --hot app.ts');
        lines.push('');
    }
    return lines.join('\n');
}
/**
 * Start Bun dev server with --hot (Vite asset server must already be running).
 *
 * Uses `bun --hot` so Bun watches all imported files and hot-swaps the fetch
 * handler on the running server. The server stays up — only the changed modules
 * are re-evaluated.
 */
export async function startBunDevServer(options) {
    const { rootDir, port = 3500, logger, vitePort: _vitePort, inspect, inspectWait, inspectBrk, } = options;
    logger.debug('Starting Bun dev server (port %d)...', port);
    const appPath = `${rootDir}/app.ts`;
    const appFile = Bun.file(appPath);
    if (!(await appFile.exists())) {
        throw new Error(`App entry not found at ${appPath}.`);
    }
    // Pre-validate app.ts for common issues
    const validation = await validateAppTs(appPath);
    if (validation.isV1Pattern) {
        logger.warn('');
        logger.warn('⚠️  app.ts may have a v1-style pattern that prevents Bun --hot from starting.');
        for (const hint of validation.hints) {
            logger.warn('   %s', hint);
        }
        logger.warn('');
    }
    // Ensure the port is available, cleaning up any orphan processes
    await ensurePortAvailable(port, logger);
    process.env.PORT = String(port);
    const args = ['bun'];
    // --hot: in-process hot reload — re-evaluates changed modules and swaps
    // the default export's fetch handler without restarting the process.
    // --no-clear-screen: don't clear terminal on reload (CLI manages output)
    args.push('--hot', '--no-clear-screen');
    if (inspectBrk) {
        args.push('--inspect-brk');
    }
    else if (inspectWait) {
        args.push('--inspect-wait');
    }
    else if (inspect) {
        args.push('--inspect');
    }
    args.push('run', appPath);
    logger.debug('Spawning bun subprocess: %s', args.join(' '));
    // Capture stderr for error reporting while still showing it in real-time
    const stderrChunks = [];
    const stdoutChunks = [];
    // Helper to read a stream, capture output, and forward to parent
    const captureStream = async (stream, chunks, output) => {
        const reader = stream.getReader();
        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done)
                    break;
                const str = new TextDecoder().decode(value);
                chunks.push(str);
                output.write(value); // Forward to parent for real-time visibility
            }
        }
        catch {
            // Stream may be closed unexpectedly when process exits
        }
    };
    const bunProcess = Bun.spawn(args, {
        cwd: rootDir,
        stdout: 'pipe',
        stderr: 'pipe',
        env: {
            ...process.env,
            ...getAgentEnv(),
            PORT: String(port),
            FORCE_COLOR: '1', // Enable colors even though stdout is piped
        },
        // Make the child a process-group leader so the CLI's procManager /
        // killBunSubprocess() can signal the whole tree via process.kill(-pid, ...).
        // Without detached:true, bun --hot and any workers it spawns share our
        // process group, process.kill(-pid) fails with EPERM (not a group leader),
        // and we fall back to a direct PID kill that leaves children orphaned.
        //
        // We intentionally do NOT call .unref() here — the parent still tracks
        // and drives the child's lifecycle, and piped stdio is unaffected.
        detached: true,
    });
    // Start capturing streams in the background (don't await, we need to check server readiness)
    if (bunProcess.stdout) {
        captureStream(bunProcess.stdout, stdoutChunks, process.stdout).catch(() => { });
    }
    if (bunProcess.stderr) {
        captureStream(bunProcess.stderr, stderrChunks, process.stderr).catch(() => { });
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    globalThis.__AGENTUITY_BUN_SUBPROCESS__ = bunProcess;
    // Notify caller IMMEDIATELY so the subprocess can be registered with the
    // process manager before the readiness wait below. Without this, a SIGINT
    // during the up-to-5s readiness wait would leave the subprocess unmanaged
    // (only the synchronous exit-handler safety net would catch it).
    if (options.onSpawn) {
        try {
            options.onSpawn(bunProcess);
        }
        catch (err) {
            logger.debug('onSpawn callback threw, killing subprocess: %s', err);
            const pid = bunProcess.pid;
            try {
                if (typeof pid === 'number' && pid > 1 && process.platform !== 'win32') {
                    try {
                        process.kill(-pid, 'SIGKILL');
                    }
                    catch {
                        bunProcess.kill('SIGKILL');
                    }
                }
                else {
                    bunProcess.kill('SIGKILL');
                }
            }
            catch {
                // Best effort
            }
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            globalThis.__AGENTUITY_BUN_SUBPROCESS__ = undefined;
            throw err;
        }
    }
    // Wait for server to start listening
    const maxRetries = 50;
    const retryDelay = 100;
    const timeoutMs = maxRetries * retryDelay;
    let serverReady = false;
    for (let i = 0; i < maxRetries; i++) {
        if (bunProcess.exitCode !== null) {
            // Brief pause to let stream capture finish flushing
            await new Promise((resolve) => setTimeout(resolve, 50));
            const stderr = stderrChunks.join('');
            const stdout = stdoutChunks.join('');
            const output = [stderr, stdout].filter(Boolean).join('\n');
            throw new Error(`Bun subprocess exited with code ${bunProcess.exitCode} during startup\n\n${output}`);
        }
        try {
            await fetch(`http://127.0.0.1:${port}/`, {
                method: 'HEAD',
                signal: AbortSignal.timeout(1000),
            });
            serverReady = true;
            break;
        }
        catch {
            // Not ready yet
        }
        await new Promise((resolve) => setTimeout(resolve, retryDelay));
    }
    if (!serverReady) {
        // The subprocess is spawned with detached:true and is therefore the
        // leader of its own process group. bunProcess.kill() only targets the
        // direct PID, which would leave any workers/grandchildren orphaned.
        // Signal the whole process group with SIGKILL and fall back to the
        // handle's kill() if the group-kill fails (EPERM, not-group-leader,
        // or Windows where negative PIDs aren't supported).
        const pid = bunProcess.pid;
        try {
            if (typeof pid === 'number' && pid > 1 && process.platform !== 'win32') {
                try {
                    process.kill(-pid, 'SIGKILL');
                }
                catch (groupErr) {
                    logger.debug('Process-group SIGKILL failed for pid %d (%s), falling back to direct kill', pid, groupErr.code ?? groupErr);
                    bunProcess.kill('SIGKILL');
                }
            }
            else {
                bunProcess.kill('SIGKILL');
            }
        }
        catch (err) {
            logger.debug('Error killing subprocess during startup failure: %s', err);
        }
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        globalThis.__AGENTUITY_BUN_SUBPROCESS__ = undefined;
        // Brief pause to let stream capture finish flushing
        await new Promise((resolve) => setTimeout(resolve, 50));
        const stderr = stderrChunks.join('');
        const stdout = stdoutChunks.join('');
        throw new Error(buildStartupErrorMessage(port, timeoutMs, stderr, validation, stdout));
    }
    logger.debug(`Bun dev server started on http://127.0.0.1:${port} (--hot mode)`);
    return { bunServerPort: port };
}
//# sourceMappingURL=bun-dev-server.js.map