import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { createServer } from 'node:http';
import { createServer as createPortProbeServer } from 'node:net';
import { networkInterfaces } from 'node:os';
import { resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { FmuxDatabase } from './database.js';
import { GitService } from './git-service.js';
import { PtyManager } from './pty-manager.js';
import { AgentManager } from './agent-manager.js';
import { CommitMessageGenerator } from './commit-message-generator.js';
import { ProviderUsageService } from './provider-usage.js';
import { ApiHandler } from './api.js';
import { ExtensionManager } from './extension-manager.js';
import { errorDetails, logger, requestDetails, socketDetails } from './logging.js';
import { WebSocketHub } from './websocket.js';
const PORT_FALLBACK_ATTEMPTS = 20;
function parseArgs() {
    const dev = process.argv.includes('--dev');
    let port = Number.parseInt(process.env.PORT || (dev ? '3001' : '4173'), 10);
    let host = process.env.FATMUX_HOST || process.env.HOST || '0.0.0.0';
    let webBuild = resolve(fileURLToPath(new URL('../../web/build', import.meta.url)));
    for (let i = 2; i < process.argv.length; i += 1) {
        const arg = process.argv[i];
        if (arg === '--port' && process.argv[i + 1]) {
            port = Number.parseInt(process.argv[++i], 10);
        }
        else if (arg === '--host' && process.argv[i + 1]) {
            host = process.argv[++i];
        }
        else if (arg === '--web-build' && process.argv[i + 1]) {
            webBuild = resolve(process.argv[++i]);
        }
        else if (arg === '--help' || arg === '-h') {
            console.log('Usage: fatmux [--host <host>] [--port <port>] [--web-build <path>] [--dev]');
            process.exit(0);
        }
    }
    return { port, host, webBuild, dev };
}
function isLoopbackHost(host) {
    return host === '127.0.0.1' || host === '::1' || host === 'localhost';
}
function localUrls(host, port) {
    if (isLoopbackHost(host)) {
        return [`http://${host === 'localhost' ? '127.0.0.1' : host}:${port}`];
    }
    if (host === '0.0.0.0' || host === '::') {
        const urls = ['http://127.0.0.1:' + port];
        for (const entries of Object.values(networkInterfaces())) {
            for (const entry of entries ?? []) {
                if (entry.internal || entry.family !== 'IPv4')
                    continue;
                urls.push(`http://${entry.address}:${port}`);
            }
        }
        return [...new Set(urls)];
    }
    return [`http://${host}:${port}`];
}
function portUsage(port) {
    const patterns = [new RegExp(`:${port}\\b`)];
    for (const command of [
        { bin: 'ss', args: ['-ltnp'] },
        { bin: 'lsof', args: ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN'] }
    ]) {
        try {
            const output = execFileSync(command.bin, command.args, { encoding: 'utf8' });
            const lines = output
                .split('\n')
                .map((line) => line.trim())
                .filter((line) => line.length > 0 && patterns.some((pattern) => pattern.test(line)));
            if (lines.length > 0)
                return lines;
        }
        catch {
            // Best-effort diagnostics only.
        }
    }
    return [];
}
async function canBind(host, port) {
    return new Promise((resolve, reject) => {
        const probe = createPortProbeServer();
        probe.once('error', (error) => {
            if (error.code === 'EADDRINUSE') {
                resolve(false);
                return;
            }
            reject(error);
        });
        probe.once('listening', () => {
            probe.close(() => resolve(true));
        });
        probe.listen({ host, port, exclusive: true });
    });
}
async function resolveListenPort(host, requestedPort) {
    if (requestedPort === 0) {
        return { port: requestedPort, collisions: [] };
    }
    const collisions = [];
    for (let offset = 0; offset < PORT_FALLBACK_ATTEMPTS; offset += 1) {
        const candidate = requestedPort + offset;
        if (candidate > 65535)
            break;
        if (await canBind(host, candidate)) {
            return { port: candidate, collisions };
        }
        collisions.push({ port: candidate, usage: portUsage(candidate) });
    }
    throw new Error(`Unable to find an open port starting at ${requestedPort} on ${host} after ${PORT_FALLBACK_ATTEMPTS} attempts`);
}
async function startListening(server, host, port) {
    return new Promise((resolvePromise, reject) => {
        const onError = (error) => {
            server.off('listening', onListening);
            reject(error);
        };
        const onListening = () => {
            server.off('error', onError);
            const address = server.address();
            if (!address || typeof address === 'string') {
                reject(new Error('Unexpected server address information'));
                return;
            }
            resolvePromise(address);
        };
        server.once('error', onError);
        server.once('listening', onListening);
        server.listen(port, host);
    });
}
const options = parseArgs();
logger.info('startup.begin', {
    pid: process.pid,
    nodeVersion: process.version,
    host: options.host,
    port: options.port,
    dev: options.dev,
    webBuild: options.webBuild
});
if (!options.dev) {
    const handlerFile = resolve(options.webBuild, 'handler.js');
    if (!existsSync(handlerFile)) {
        console.error(`Web build not found at ${handlerFile}. Run npm run build first.`);
        process.exit(1);
    }
    logger.info('startup.web_build.ready', { handlerFile });
}
const handler = options.dev
    ? null
    : (await import(pathToFileURL(resolve(options.webBuild, 'handler.js')).href)).handler;
const db = new FmuxDatabase();
logger.info('startup.database.ready', { dataHome: db.home, databaseFile: db.file });
const git = new GitService();
const pty = new PtyManager(db);
let hub;
const extensions = new ExtensionManager(db);
const agents = new AgentManager(db, (message) => hub.broadcast(message), extensions);
const commitMessages = new CommitMessageGenerator();
const usage = new ProviderUsageService();
hub = new WebSocketHub({ db, git, pty, agents });
const api = new ApiHandler({
    db,
    git,
    pty,
    agents,
    commitMessages,
    usage,
    extensions,
    broadcastGitStatus: (projectId) => hub.broadcastGitStatus(projectId)
});
let nextRequestId = 1;
const server = createServer(async (req, res) => {
    const requestId = nextRequestId++;
    const startedAt = Date.now();
    let routeTarget = 'unresolved';
    let finished = false;
    logger.info('http.request.start', { requestId, ...requestDetails(req) });
    res.on('finish', () => {
        finished = true;
        logger.info('http.request.finish', {
            requestId,
            routeTarget,
            statusCode: res.statusCode,
            durationMs: Date.now() - startedAt,
            ...requestDetails(req)
        });
    });
    res.on('close', () => {
        if (finished)
            return;
        logger.warn('http.request.closed', {
            requestId,
            routeTarget,
            durationMs: Date.now() - startedAt,
            ...requestDetails(req)
        });
    });
    try {
        if (await api.handle(req, res)) {
            routeTarget = 'api';
            return;
        }
        if (handler) {
            routeTarget = 'web';
            await handler(req, res);
            return;
        }
        routeTarget = 'not_found';
        res.writeHead(404).end();
    }
    catch (error) {
        routeTarget = 'error';
        logger.error('http.request.error', { requestId, ...requestDetails(req), ...errorDetails(error) });
        if (!res.headersSent) {
            res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
            res.end('Internal Server Error');
            return;
        }
        res.destroy(error instanceof Error ? error : undefined);
    }
});
server.on('connection', (socket) => {
    logger.info('tcp.connection.open', socketDetails(socket));
    socket.on('close', (hadError) => {
        logger.info('tcp.connection.close', { hadError, ...socketDetails(socket) });
    });
    socket.on('error', (error) => {
        logger.warn('tcp.connection.error', { ...socketDetails(socket), ...errorDetails(error) });
    });
});
server.on('clientError', (error, socket) => {
    logger.warn('tcp.client_error', { ...socketDetails(socket), ...errorDetails(error) });
});
server.on('upgrade', (req, socket, head) => {
    logger.info('ws.upgrade.request', { ...requestDetails(req), headBytes: head.length });
    if (!hub.upgrade(req, socket, head)) {
        logger.warn('ws.upgrade.rejected', { ...requestDetails(req) });
        socket.destroy();
    }
});
const listenPlan = await resolveListenPort(options.host, options.port);
for (const collision of listenPlan.collisions) {
    logger.warn('startup.port.occupied', {
        host: options.host,
        port: collision.port,
        usage: collision.usage.length > 0 ? collision.usage : ['port is already in use']
    });
}
if (listenPlan.port !== options.port) {
    logger.warn('startup.port.fallback', {
        requestedPort: options.port,
        selectedPort: listenPlan.port
    });
}
const address = await startListening(server, options.host, listenPlan.port);
server.on('error', (error) => {
    logger.error('server.error', errorDetails(error));
});
const urls = localUrls(options.host, address.port);
logger.info('startup.listen.ready', {
    host: address.address,
    port: address.port,
    family: address.family,
    urls
});
if (isLoopbackHost(options.host)) {
    logger.warn('startup.loopback_only', {
        host: options.host,
        hint: 'Remote browsers cannot reach loopback. In Docker/LXC/VM environments start fatmux with --host 0.0.0.0.'
    });
}
console.log(`fatmux running at ${urls[0]}`);
let shuttingDown = false;
function shutdown(signal) {
    if (shuttingDown)
        return;
    shuttingDown = true;
    logger.info('shutdown.begin', { signal });
    pty.disposeAll();
    hub.close();
    db.close();
    server.close(() => {
        logger.info('shutdown.complete', { signal });
        process.exit(0);
    });
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('uncaughtException', (error) => {
    logger.error('process.uncaught_exception', errorDetails(error));
});
process.on('unhandledRejection', (reason) => {
    logger.error('process.unhandled_rejection', errorDetails(reason));
});
//# sourceMappingURL=index.js.map