import { ConfigurationError } from '@pellux/goodvibes-errors';
import { logger } from '../utils/logger.js';
import net from 'node:net';
import { summarizeError } from '../utils/error-display.js';
const DEFAULT_SERVICE_START_TIMEOUT_MS = 1500;
const TCP_PORT_PROBE_TIMEOUT_MS = 250;
const DAEMON_IDENTITY_PROBE_TIMEOUT_MS = 750;
function readBooleanSetting(config, key) {
    const value = config.get(key);
    if (typeof value !== 'boolean') {
        throw new ConfigurationError(`Expected ${key} to be a boolean, got ${typeof value}.`);
    }
    return value;
}
function readHostSetting(config, key, fallback) {
    const value = config.get(key);
    if (value === undefined || value === null)
        return fallback;
    if (typeof value !== 'string') {
        throw new ConfigurationError(`Expected ${key} to be a host string, got ${typeof value}.`);
    }
    const trimmed = value.trim();
    if (!trimmed) {
        throw new ConfigurationError(`Expected ${key} to be a non-empty host string.`);
    }
    return trimmed;
}
function readPortSetting(config, key, fallback) {
    const value = config.get(key);
    if (value === undefined || value === null)
        return fallback;
    const port = typeof value === 'number'
        ? value
        : typeof value === 'string' && value.trim() !== ''
            ? Number(value)
            : NaN;
    if (!Number.isInteger(port) || port < 1 || port > 65535) {
        throw new ConfigurationError(`Expected ${key} to be an integer TCP port between 1 and 65535.`);
    }
    return port;
}
function isBindConflictMessage(message) {
    return message.includes('EADDRINUSE') || message.includes('Address already in use');
}
async function stopAfterFailedStart(label, service) {
    try {
        await service.stop();
    }
    catch (error) {
        logger.warn(`${label} cleanup after failed startup failed`, { error: summarizeError(error) });
    }
}
async function startGuardedService(options) {
    try {
        const result = await startWithTimeout(options.label, () => options.service.start(), options.timeoutMs, () => options.service.stop());
        if (result === 'timed_out') {
            return { service: null, status: options.timedOutStatus() };
        }
        return { service: options.service, status: options.startedStatus() };
    }
    catch (error) {
        const message = summarizeError(error);
        if (!isBindConflictMessage(message))
            throw error;
        await stopAfterFailedStart(options.label, options.service);
        return { service: null, status: await options.bindConflictStatus(message) };
    }
}
async function isTcpPortInUse(host, port, timeoutMs = TCP_PORT_PROBE_TIMEOUT_MS) {
    return await new Promise((resolve) => {
        const socket = new net.Socket();
        let settled = false;
        const finish = (result) => {
            if (settled)
                return;
            settled = true;
            socket.destroy();
            resolve(result);
        };
        socket.setTimeout(timeoutMs);
        socket.once('connect', () => finish(true));
        socket.once('timeout', () => finish(false));
        socket.once('error', () => finish(false));
        socket.connect(port, host);
    });
}
function normalizeProbeHost(host) {
    if (host === '0.0.0.0' || host === '::')
        return '127.0.0.1';
    return host;
}
function formatBaseUrl(host, port) {
    const normalized = normalizeProbeHost(host);
    const urlHost = normalized.includes(':') && !normalized.startsWith('[') ? `[${normalized}]` : normalized;
    return `http://${urlHost}:${port}`;
}
function createServiceStatus(mode, host, port, details = {}) {
    return {
        mode,
        host,
        port,
        baseUrl: formatBaseUrl(host, port),
        ...details,
    };
}
async function probeGoodVibesDaemonIdentity(host, port, token) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), DAEMON_IDENTITY_PROBE_TIMEOUT_MS);
    timeout.unref?.();
    try {
        const headers = new Headers();
        if (token?.trim())
            headers.set('Authorization', `Bearer ${token.trim()}`);
        const response = await fetch(`${formatBaseUrl(host, port)}/status`, {
            headers,
            signal: controller.signal,
        });
        if (response.status === 401 || response.status === 403) {
            return {
                kind: 'unauthorized',
                reason: token?.trim()
                    ? 'GoodVibes daemon identity probe was rejected by the configured token'
                    : 'GoodVibes daemon identity probe needs an auth token',
            };
        }
        if (!response.ok) {
            return { kind: 'unknown', reason: `Identity probe returned HTTP ${response.status}` };
        }
        let body = null;
        try {
            body = await response.json();
        }
        catch (error) {
            return {
                kind: 'unknown',
                reason: `Identity probe response was not valid JSON: ${summarizeError(error)}`,
            };
        }
        if (body?.status === 'running' && typeof body.version === 'string') {
            return { kind: 'goodvibes', status: body.status, version: body.version };
        }
        return { kind: 'unknown', reason: 'Identity probe response did not match GoodVibes daemon status shape' };
    }
    catch (error) {
        return { kind: 'unknown', reason: summarizeError(error) };
    }
    finally {
        clearTimeout(timeout);
    }
}
async function startWithTimeout(label, start, timeoutMs, cleanup) {
    let timer = null;
    const startPromise = start().then(() => 'started');
    try {
        const result = await Promise.race([
            startPromise,
            new Promise((resolve) => {
                timer = setTimeout(() => resolve('timed_out'), timeoutMs);
                timer.unref?.();
            }),
        ]);
        if (result === 'timed_out') {
            logger.warn(`${label} startup timed out; continuing without it in this host instance`, { timeoutMs });
            if (cleanup) {
                void cleanup().catch((error) => {
                    logger.warn(`${label} cleanup after startup timeout failed`, { error: summarizeError(error) });
                });
                void startPromise.then(async () => {
                    try {
                        await cleanup();
                    }
                    catch (error) {
                        logger.warn(`${label} delayed cleanup after startup timeout failed`, { error: summarizeError(error) });
                    }
                }, (error) => {
                    logger.warn(`${label} startup rejected after timeout`, { error: summarizeError(error) });
                });
            }
        }
        return result;
    }
    finally {
        if (timer !== null)
            clearTimeout(timer);
    }
}
async function createDefaultDaemonServer(runtimeBus, userAuth, runtimeServices) {
    const { DaemonServer } = await import('../daemon/server.js');
    return new DaemonServer({ runtimeBus, userAuth, runtimeServices });
}
async function createDefaultHttpListener(hookDispatcher, userAuth, configManager) {
    const { HttpListener } = await import('../daemon/http-listener.js');
    return new HttpListener({ hookDispatcher, userAuth, configManager });
}
export async function startHostServices(config, runtimeBus, hookDispatcher, runtimeServices, factories = {}) {
    const sharedUserAuth = runtimeServices.localUserAuthManager;
    const startupTimeoutMs = factories.startupTimeoutMs ?? DEFAULT_SERVICE_START_TIMEOUT_MS;
    const daemonHost = readHostSetting(config, 'controlPlane.host', '127.0.0.1');
    const daemonPort = readPortSetting(config, 'controlPlane.port', 3421);
    const httpListenerHost = readHostSetting(config, 'httpListener.host', '127.0.0.1');
    const httpListenerPort = readPortSetting(config, 'httpListener.port', 3422);
    const probeDaemonPortInUse = factories.probeDaemonPortInUse ?? ((host, port) => isTcpPortInUse(host, port));
    const probeDaemonIdentity = factories.probeDaemonIdentity ?? probeGoodVibesDaemonIdentity;
    const probeHttpListenerPortInUse = factories.probeHttpListenerPortInUse ?? ((host, port) => isTcpPortInUse(host, port));
    let embeddedDaemonServer = null;
    let embeddedHttpListener = null;
    let daemonStatus = createServiceStatus('disabled', daemonHost, daemonPort, { reason: 'danger.daemon is disabled' });
    let httpListenerStatus = createServiceStatus('disabled', httpListenerHost, httpListenerPort, {
        reason: 'danger.httpListener is disabled',
    });
    if (readBooleanSetting(config, 'danger.daemon')) {
        if (await probeDaemonPortInUse(daemonHost, daemonPort)) {
            const identity = await probeDaemonIdentity(daemonHost, daemonPort, factories.sharedDaemonToken);
            if (identity.kind === 'goodvibes') {
                daemonStatus = createServiceStatus('external', daemonHost, daemonPort, {
                    authenticated: true,
                    status: identity.status,
                    version: identity.version,
                    reason: 'Existing GoodVibes daemon verified on configured host/port',
                });
                logger.info('Existing GoodVibes daemon detected; continuing without embedded daemon in this host instance', {
                    host: daemonHost,
                    port: daemonPort,
                    version: identity.version,
                });
            }
            else {
                daemonStatus = createServiceStatus('blocked', daemonHost, daemonPort, {
                    authenticated: identity.kind !== 'unauthorized' ? undefined : false,
                    reason: identity.reason ?? 'Configured daemon port is occupied by an unverified process',
                });
                logger.warn('Daemon server port already in use by an unverified process; continuing without embedded daemon in this host instance', {
                    host: daemonHost,
                    port: daemonPort,
                    reason: identity.reason,
                });
            }
        }
        else {
            const pendingDaemonServer = factories.createDaemonServer
                ? factories.createDaemonServer(runtimeBus, sharedUserAuth, runtimeServices)
                : await createDefaultDaemonServer(runtimeBus, sharedUserAuth, runtimeServices);
            const service = pendingDaemonServer;
            service.enable({ daemon: true }, factories.sharedDaemonToken);
            const started = await startGuardedService({
                label: 'Daemon server',
                service,
                timeoutMs: startupTimeoutMs,
                timedOutStatus: () => createServiceStatus('unavailable', daemonHost, daemonPort, {
                    reason: 'Daemon server startup timed out',
                }),
                startedStatus: () => createServiceStatus('embedded', daemonHost, daemonPort, {
                    reason: 'Embedded daemon started in this host instance',
                }),
                bindConflictStatus: async (message) => {
                    const identity = await probeDaemonIdentity(daemonHost, daemonPort, factories.sharedDaemonToken);
                    if (identity.kind === 'goodvibes') {
                        logger.info('Existing GoodVibes daemon detected after bind conflict; continuing without embedded daemon in this host instance', {
                            host: daemonHost,
                            port: daemonPort,
                            version: identity.version,
                        });
                        return createServiceStatus('external', daemonHost, daemonPort, {
                            authenticated: true,
                            status: identity.status,
                            version: identity.version,
                            reason: 'Existing GoodVibes daemon verified after bind conflict',
                        });
                    }
                    logger.warn('Daemon server port already in use; continuing without local daemon in this host instance', { error: message });
                    return createServiceStatus('blocked', daemonHost, daemonPort, {
                        authenticated: identity.kind !== 'unauthorized' ? undefined : false,
                        reason: identity.reason ?? message,
                    });
                },
            });
            embeddedDaemonServer = started.service;
            daemonStatus = started.status;
        }
    }
    if (readBooleanSetting(config, 'danger.httpListener')) {
        if (await probeHttpListenerPortInUse(httpListenerHost, httpListenerPort)) {
            httpListenerStatus = createServiceStatus('blocked', httpListenerHost, httpListenerPort, {
                reason: 'Configured HTTP listener port is already in use',
            });
            logger.warn('HTTP listener port already in use; continuing without local listener in this host instance', {
                host: httpListenerHost,
                port: httpListenerPort,
            });
        }
        else {
            const pendingHttpListener = factories.createHttpListener
                ? factories.createHttpListener(hookDispatcher, sharedUserAuth, runtimeServices.configManager)
                : await createDefaultHttpListener(hookDispatcher, sharedUserAuth, runtimeServices.configManager);
            const service = pendingHttpListener;
            service.enable({ httpListener: true }, factories.sharedHttpListenerToken);
            const started = await startGuardedService({
                label: 'HTTP listener',
                service,
                timeoutMs: startupTimeoutMs,
                timedOutStatus: () => createServiceStatus('unavailable', httpListenerHost, httpListenerPort, {
                    reason: 'HTTP listener startup timed out',
                }),
                startedStatus: () => createServiceStatus('embedded', httpListenerHost, httpListenerPort, {
                    reason: 'Embedded HTTP listener started in this host instance',
                }),
                bindConflictStatus: (message) => {
                    logger.warn('HTTP listener port already in use; continuing without local listener in this host instance', { error: message });
                    return createServiceStatus('blocked', httpListenerHost, httpListenerPort, { reason: message });
                },
            });
            embeddedHttpListener = started.service;
            httpListenerStatus = started.status;
        }
    }
    return {
        daemonServer: embeddedDaemonServer,
        httpListener: embeddedHttpListener,
        daemonStatus,
        httpListenerStatus,
        listRecentControlPlaneEvents(limit) {
            return embeddedDaemonServer?.listRecentControlPlaneEvents(limit) ?? [];
        },
        async stop() {
            await Promise.allSettled([
                embeddedDaemonServer?.stop(),
                embeddedHttpListener?.stop(),
            ]);
        },
    };
}
