import crypto, { randomUUID } from 'node:crypto';
import net from 'node:net';
import path from 'node:path';
import { launchDaemonDetached } from './launch.js';
import { getDaemonMetadataPath, getDaemonSocketPath } from './paths.js';
const DEFAULT_DAEMON_TIMEOUT_MS = 30_000;
export function resolveDaemonPaths(configPath) {
    const key = deriveConfigKey(configPath);
    return {
        key,
        socketPath: getDaemonSocketPath(key),
        metadataPath: getDaemonMetadataPath(key),
    };
}
export class DaemonClient {
    options;
    socketPath;
    metadataPath;
    startingPromise = null;
    constructor(options) {
        this.options = options;
        const paths = resolveDaemonPaths(options.configPath);
        this.socketPath = paths.socketPath;
        this.metadataPath = paths.metadataPath;
    }
    async callTool(params) {
        return this.invoke('callTool', params, params.timeoutMs);
    }
    async listTools(params) {
        return this.invoke('listTools', params);
    }
    async listResources(params) {
        return this.invoke('listResources', params);
    }
    async closeServer(params) {
        await this.invoke('closeServer', params);
    }
    async status() {
        try {
            return (await this.sendRequest('status', {}));
        }
        catch (error) {
            if (isTransportError(error)) {
                return null;
            }
            throw error;
        }
    }
    async stop() {
        try {
            await this.sendRequest('stop', {});
        }
        catch (error) {
            if (isTransportError(error)) {
                return;
            }
            throw error;
        }
    }
    async invoke(method, params, timeoutMs) {
        await this.ensureDaemon();
        try {
            return (await this.sendRequest(method, params, timeoutMs));
        }
        catch (error) {
            if (isTransportError(error)) {
                await this.restartDaemon();
                return (await this.sendRequest(method, params, timeoutMs));
            }
            throw error;
        }
    }
    async ensureDaemon() {
        const available = await this.isResponsive();
        if (available) {
            return;
        }
        await this.startDaemon();
        await this.waitForReady();
    }
    async restartDaemon() {
        await this.startDaemon();
        await this.waitForReady();
    }
    async startDaemon() {
        if (this.startingPromise) {
            await this.startingPromise;
            return;
        }
        this.startingPromise = Promise.resolve()
            .then(() => {
            launchDaemonDetached({
                configPath: this.options.configPath,
                rootDir: this.options.rootDir,
                metadataPath: this.metadataPath,
                socketPath: this.socketPath,
            });
        })
            .finally(() => {
            this.startingPromise = null;
        });
        await this.startingPromise;
    }
    async waitForReady() {
        const deadline = Date.now() + 10_000;
        while (Date.now() < deadline) {
            if (await this.isResponsive()) {
                return;
            }
            await delay(100);
        }
        throw new Error('Timeout while waiting for MCPorter daemon to start.');
    }
    async isResponsive() {
        try {
            await this.sendRequest('status', {});
            return true;
        }
        catch (error) {
            if (isTransportError(error)) {
                return false;
            }
            throw error;
        }
    }
    async sendRequest(method, params, timeoutOverrideMs) {
        const request = {
            id: randomUUID(),
            method,
            params,
        };
        const payload = JSON.stringify(request);
        const timeoutMs = resolveDaemonTimeout(timeoutOverrideMs);
        const response = await new Promise((resolve, reject) => {
            const socket = net.createConnection(this.socketPath);
            let settled = false;
            const finishReject = (error) => {
                if (settled) {
                    return;
                }
                settled = true;
                reject(error);
            };
            const finishResolve = (value) => {
                if (settled) {
                    return;
                }
                settled = true;
                resolve(value);
            };
            socket.setTimeout(timeoutMs, () => {
                // If the daemon doesn't answer in time we treat it as a transport error, destroy the socket,
                // and let invoke() restart the daemon so hung keep-alive servers get a fresh start.
                socket.destroy(Object.assign(new Error('Daemon request timed out.'), {
                    code: 'ETIMEDOUT',
                }));
            });
            let buffer = '';
            socket.on('connect', () => {
                socket.write(payload, (error) => {
                    if (error) {
                        finishReject(error);
                    }
                    // Do not end the socket here; allow the server to respond and close.
                });
            });
            socket.on('data', (chunk) => {
                buffer += chunk.toString();
            });
            socket.on('end', () => finishResolve(buffer));
            socket.on('error', (error) => {
                finishReject(error);
            });
        });
        const trimmed = response.trim();
        if (!trimmed) {
            const error = new Error('Empty daemon response.');
            error.code = 'ECONNRESET';
            throw error;
        }
        let parsed;
        try {
            parsed = JSON.parse(trimmed);
        }
        catch {
            const parseError = new Error('Failed to parse daemon response.');
            parseError.code = 'ECONNRESET';
            throw parseError;
        }
        if (!parsed.ok) {
            const error = new Error(parsed.error?.message ?? 'Daemon error');
            error.code = parsed.error?.code;
            throw error;
        }
        return parsed.result;
    }
}
function deriveConfigKey(configPath) {
    const absolute = path.resolve(configPath);
    return crypto.createHash('sha1').update(absolute).digest('hex').slice(0, 12);
}
function isTransportError(error) {
    if (!error || typeof error !== 'object') {
        return false;
    }
    const code = error.code;
    return code === 'ECONNREFUSED' || code === 'ENOENT' || code === 'ETIMEDOUT' || code === 'ECONNRESET';
}
function resolveDaemonTimeout(override) {
    if (typeof override === 'number' && Number.isFinite(override) && override > 0) {
        return override;
    }
    const raw = process.env.MCPORTER_DAEMON_TIMEOUT_MS;
    if (!raw) {
        return DEFAULT_DAEMON_TIMEOUT_MS;
    }
    const parsed = Number.parseInt(raw, 10);
    if (!Number.isFinite(parsed) || parsed <= 0) {
        return DEFAULT_DAEMON_TIMEOUT_MS;
    }
    return parsed;
}
function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}
//# sourceMappingURL=client.js.map