"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startOpenAICompatProxy = exports.stopOpenAICompatProxy = exports.getOpenAICompatProxyStatus = exports.listOpenAICompatProxyStatuses = exports.isOpenAICompatProxyRunning = void 0;
const child_process_1 = require("child_process");
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const http = __importStar(require("http"));
const lockfile = __importStar(require("proper-lockfile"));
const daemon_process_ownership_1 = require("../cursor/daemon-process-ownership");
const proxy_daemon_paths_1 = require("./proxy-daemon-paths");
const proxy_daemon_state_1 = require("./proxy-daemon-state");
const proxy_port_resolver_1 = require("./proxy-port-resolver");
function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function verifyProxyProcessOwnership(pid, profileName) {
    const profilePattern = profileName
        ? new RegExp(`(^|\\s)--profile(?:\\s+|=)${escapeRegExp(profileName)}(?=\\s|$)`)
        : null;
    return (0, daemon_process_ownership_1.verifyProcessOwnership)(pid, (commandLine) => commandLine.includes('--ccs-openai-proxy-daemon') &&
        commandLine.includes('proxy-daemon-entry') &&
        (!profilePattern || profilePattern.test(commandLine)));
}
function generateProxyAuthToken() {
    return crypto.randomBytes(24).toString('hex');
}
async function withOpenAICompatProxyLock(operation) {
    const proxyDir = (0, proxy_daemon_paths_1.getOpenAICompatProxyDir)();
    await fs.promises.mkdir(proxyDir, { recursive: true, mode: 0o700 });
    await fs.promises.chmod(proxyDir, 0o700);
    let release;
    try {
        release = await lockfile.lock(proxyDir, {
            stale: 10000,
            retries: { retries: 20, minTimeout: 50, maxTimeout: 250 },
            realpath: false,
        });
    }
    catch (error) {
        throw new Error(`Failed to lock OpenAI-compatible proxy directory (${proxyDir}): ${error.message}`);
    }
    try {
        return await operation();
    }
    finally {
        if (release) {
            try {
                await release();
            }
            catch {
                // Best-effort release.
            }
        }
    }
}
async function terminateDaemonProcess(pid) {
    if (!pid) {
        return;
    }
    try {
        process.kill(pid, 'SIGTERM');
        let attempts = 0;
        while (attempts < 10) {
            await new Promise((resolve) => setTimeout(resolve, 200));
            try {
                process.kill(pid, 0);
                attempts += 1;
            }
            catch {
                return;
            }
        }
        process.kill(pid, 'SIGKILL');
    }
    catch {
        // Best-effort cleanup for a daemon we just spawned.
    }
}
function listOpenAICompatProxyCandidatePorts(profileName, preferredPort, exact, excludedPorts = new Set()) {
    if (exact) {
        return excludedPorts.has(preferredPort) ? [] : [preferredPort];
    }
    return (0, proxy_port_resolver_1.listOpenAICompatProxyCandidatePorts)(profileName, preferredPort, excludedPorts);
}
function isPortBindConflictMessage(message) {
    if (!message) {
        return false;
    }
    const normalized = message.toLowerCase();
    return (message.includes('EADDRINUSE') ||
        normalized.includes('address already in use') ||
        (normalized.includes('is port') && normalized.includes('in use')));
}
async function resolveDaemonEntrypoint() {
    for (const candidate of (0, proxy_daemon_state_1.resolveOpenAICompatProxyEntrypointCandidates)()) {
        try {
            await fs.promises.access(candidate, fs.constants.R_OK);
            return candidate;
        }
        catch {
            // Try next candidate.
        }
    }
    return null;
}
async function isOpenAICompatProxyRunning(port, expectedProfileName) {
    return new Promise((resolve) => {
        const req = http.request({ hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 3000 }, (res) => {
            let body = '';
            res.setEncoding('utf8');
            res.on('data', (chunk) => (body += chunk));
            res.on('end', () => {
                if (res.statusCode !== 200) {
                    resolve(false);
                    return;
                }
                try {
                    const payload = JSON.parse(body);
                    resolve(payload.service === proxy_daemon_paths_1.OPENAI_COMPAT_PROXY_SERVICE_NAME &&
                        (!expectedProfileName || payload.profile === expectedProfileName));
                }
                catch {
                    resolve(false);
                }
            });
        });
        req.on('error', () => resolve(false));
        req.on('timeout', () => {
            req.destroy();
            resolve(false);
        });
        req.end();
    });
}
exports.isOpenAICompatProxyRunning = isOpenAICompatProxyRunning;
async function getOpenAICompatProxyStatusForProfile(profileName) {
    const state = getOpenAICompatProxyStateForProfile(profileName);
    const session = state.session;
    if (!session) {
        return { running: false, profileName, pid: state.pid || undefined };
    }
    const pid = state.pid;
    if (!pid || verifyProxyProcessOwnership(pid, profileName) !== 'owned') {
        return { running: false, profileName, port: session.port };
    }
    const port = session.port;
    const running = await isOpenAICompatProxyRunning(port, profileName);
    return {
        running,
        pid: running ? pid : undefined,
        ...session,
    };
}
async function getLegacyOpenAICompatProxyStatus() {
    const session = (0, proxy_daemon_state_1.readLegacyOpenAICompatProxySession)();
    const pid = (0, proxy_daemon_state_1.getLegacyOpenAICompatProxyPid)();
    if (!session && !pid) {
        return null;
    }
    const profileName = session?.profileName;
    const hasOwnedPid = typeof pid === 'number' &&
        typeof profileName === 'string' &&
        profileName.length > 0 &&
        verifyProxyProcessOwnership(pid, profileName) === 'owned';
    const port = session?.port;
    const running = hasOwnedPid && typeof port === 'number'
        ? await isOpenAICompatProxyRunning(port, session?.profileName)
        : false;
    return {
        running,
        port,
        pid: running ? pid || undefined : undefined,
        ...session,
    };
}
function getOpenAICompatProxyStateForProfile(profileName) {
    const session = (0, proxy_daemon_state_1.readOpenAICompatProxySession)(profileName);
    if (session) {
        return {
            pid: (0, proxy_daemon_state_1.getOpenAICompatProxyPid)(profileName),
            session,
            source: 'profile',
        };
    }
    const legacySession = (0, proxy_daemon_state_1.readLegacyOpenAICompatProxySession)();
    if (legacySession?.profileName === profileName) {
        return {
            pid: (0, proxy_daemon_state_1.getLegacyOpenAICompatProxyPid)(),
            session: legacySession,
            source: 'legacy',
        };
    }
    return { pid: null, session: null, source: 'profile' };
}
async function listOpenAICompatProxyStatuses() {
    const profileNames = new Set((0, proxy_daemon_state_1.listOpenAICompatProxyProfileNames)());
    const legacySession = (0, proxy_daemon_state_1.readLegacyOpenAICompatProxySession)();
    if (legacySession?.profileName) {
        profileNames.add(legacySession.profileName);
    }
    const profileStatuses = await Promise.all([...profileNames].map((profileName) => getOpenAICompatProxyStatusForProfile(profileName)));
    const statuses = profileStatuses.filter((status) => status.profileName);
    if (!legacySession?.profileName) {
        const legacyStatus = await getLegacyOpenAICompatProxyStatus();
        if (legacyStatus) {
            statuses.push(legacyStatus);
        }
    }
    return statuses;
}
exports.listOpenAICompatProxyStatuses = listOpenAICompatProxyStatuses;
async function getOpenAICompatProxyStatus(profileName) {
    if (profileName) {
        return getOpenAICompatProxyStatusForProfile(profileName);
    }
    const statuses = await listOpenAICompatProxyStatuses();
    const running = statuses.filter((status) => status.running);
    if (running.length === 1) {
        return running[0];
    }
    if (running.length > 1) {
        return { running: true };
    }
    const latestKnown = statuses[0];
    return latestKnown ?? { running: false };
}
exports.getOpenAICompatProxyStatus = getOpenAICompatProxyStatus;
async function stopOpenAICompatProxyUnlocked(profileName) {
    const state = getOpenAICompatProxyStateForProfile(profileName);
    const pid = state.pid;
    if (!pid) {
        removeOpenAICompatProxyState(state, profileName);
        return { success: true };
    }
    const ownership = verifyProxyProcessOwnership(pid, profileName);
    if (ownership === 'not-owned') {
        removeOpenAICompatProxyState(state, profileName);
        return { success: true };
    }
    if (ownership === 'unknown') {
        return {
            success: false,
            error: `Refusing to stop PID ${pid}: unable to verify daemon ownership`,
        };
    }
    if (ownership === 'not-running') {
        removeOpenAICompatProxyState(state, profileName);
        return { success: true };
    }
    try {
        process.kill(pid, 'SIGTERM');
        let attempts = 0;
        while (attempts < 10) {
            await new Promise((resolve) => setTimeout(resolve, 500));
            try {
                process.kill(pid, 0);
                attempts += 1;
            }
            catch {
                break;
            }
        }
        if (attempts >= 10) {
            try {
                process.kill(pid, 'SIGKILL');
            }
            catch {
                // Already exited.
            }
        }
    }
    catch (error) {
        const err = error;
        if (err.code !== 'ESRCH') {
            return { success: false, error: `Failed to stop daemon: ${err.message}` };
        }
    }
    removeOpenAICompatProxyState(state, profileName);
    return { success: true };
}
async function stopLegacyOpenAICompatProxyUnlocked() {
    const legacySession = (0, proxy_daemon_state_1.readLegacyOpenAICompatProxySession)();
    if (legacySession?.profileName) {
        return stopOpenAICompatProxyUnlocked(legacySession.profileName);
    }
    const pid = (0, proxy_daemon_state_1.getLegacyOpenAICompatProxyPid)();
    if (!pid) {
        (0, proxy_daemon_state_1.removeLegacyOpenAICompatProxyPid)();
        (0, proxy_daemon_state_1.removeLegacyOpenAICompatProxySession)();
        return { success: true };
    }
    const ownership = verifyProxyProcessOwnership(pid);
    if (ownership === 'not-owned' || ownership === 'not-running') {
        (0, proxy_daemon_state_1.removeLegacyOpenAICompatProxyPid)();
        (0, proxy_daemon_state_1.removeLegacyOpenAICompatProxySession)();
        return { success: true };
    }
    if (ownership === 'unknown') {
        return {
            success: false,
            error: `Refusing to stop PID ${pid}: unable to verify daemon ownership`,
        };
    }
    try {
        process.kill(pid, 'SIGTERM');
        let attempts = 0;
        while (attempts < 10) {
            await new Promise((resolve) => setTimeout(resolve, 500));
            try {
                process.kill(pid, 0);
                attempts += 1;
            }
            catch {
                break;
            }
        }
        if (attempts >= 10) {
            try {
                process.kill(pid, 'SIGKILL');
            }
            catch {
                // Already exited.
            }
        }
    }
    catch (error) {
        const err = error;
        if (err.code !== 'ESRCH') {
            return { success: false, error: `Failed to stop daemon: ${err.message}` };
        }
    }
    (0, proxy_daemon_state_1.removeLegacyOpenAICompatProxyPid)();
    (0, proxy_daemon_state_1.removeLegacyOpenAICompatProxySession)();
    return { success: true };
}
function removeOpenAICompatProxyState(state, profileName) {
    if (state.source === 'legacy') {
        (0, proxy_daemon_state_1.removeLegacyOpenAICompatProxyPid)();
        (0, proxy_daemon_state_1.removeLegacyOpenAICompatProxySession)();
        return;
    }
    (0, proxy_daemon_state_1.removeOpenAICompatProxyPid)(profileName);
    (0, proxy_daemon_state_1.removeOpenAICompatProxySession)(profileName);
}
async function stopOpenAICompatProxy(profileName) {
    return withOpenAICompatProxyLock(async () => {
        if (profileName) {
            return stopOpenAICompatProxyUnlocked(profileName);
        }
        const statuses = await listOpenAICompatProxyStatuses();
        const failures = [];
        for (const status of statuses) {
            const stopped = status.profileName
                ? await stopOpenAICompatProxyUnlocked(status.profileName)
                : await stopLegacyOpenAICompatProxyUnlocked();
            if (!stopped.success) {
                failures.push(status.profileName
                    ? `${status.profileName}: ${stopped.error || 'failed to stop proxy'}`
                    : `legacy proxy: ${stopped.error || 'failed to stop proxy'}`);
            }
        }
        if (failures.length > 0) {
            return { success: false, error: `Failed to stop some proxies: ${failures.join('; ')}` };
        }
        return { success: true };
    });
}
exports.stopOpenAICompatProxy = stopOpenAICompatProxy;
async function startOpenAICompatProxy(profile, options = {}) {
    return withOpenAICompatProxyLock(async () => {
        const status = await getOpenAICompatProxyStatus(profile.profileName);
        const host = options.host?.trim() || status.host || '127.0.0.1';
        const portPreference = (0, proxy_port_resolver_1.resolveOpenAICompatProxyPortPreference)(profile.profileName);
        const explicitPort = typeof options.port === 'number' ? options.port : undefined;
        const requiresExactPort = explicitPort !== undefined || portPreference.source === 'profile';
        if (status.running &&
            explicitPort === undefined &&
            portPreference.source !== 'profile' &&
            status.port &&
            (status.host || '127.0.0.1') === host) {
            return {
                success: true,
                alreadyRunning: true,
                pid: status.pid,
                port: status.port,
                authToken: status.authToken,
            };
        }
        const preferredPort = explicitPort ?? portPreference.port;
        if (status.running && status.port === preferredPort && (status.host || '127.0.0.1') === host) {
            return {
                success: true,
                alreadyRunning: true,
                pid: status.pid,
                port: preferredPort,
                authToken: status.authToken,
            };
        }
        if (!Number.isInteger(preferredPort) || preferredPort < 1 || preferredPort > 65535) {
            return { success: false, port: preferredPort, error: `Invalid port: ${preferredPort}` };
        }
        if (status.pid && !status.port) {
            const stopped = await stopOpenAICompatProxyUnlocked(profile.profileName);
            if (!stopped.success) {
                return {
                    success: false,
                    port: preferredPort,
                    error: stopped.error || 'Failed to clear stale proxy state',
                };
            }
        }
        const daemonEntry = await resolveDaemonEntrypoint();
        if (!daemonEntry) {
            return {
                success: false,
                port: preferredPort,
                error: 'OpenAI proxy daemon entrypoint not found. Run `bun run build` and retry.',
            };
        }
        const launchOnPort = (port, persistState) => new Promise((resolve) => {
            let resolved = false;
            let timeout = null;
            let stderr = '';
            const authToken = generateProxyAuthToken();
            const authTokenFile = (0, proxy_daemon_state_1.writeOpenAICompatProxyAuthTokenFile)(profile.profileName, authToken);
            const commitState = () => {
                if (proc.pid) {
                    (0, proxy_daemon_state_1.writeOpenAICompatProxyPid)(profile.profileName, proc.pid);
                }
                (0, proxy_daemon_state_1.writeOpenAICompatProxySession)({
                    profileName: profile.profileName,
                    settingsPath: profile.settingsPath,
                    host,
                    port,
                    baseUrl: profile.baseUrl,
                    authToken,
                    model: profile.model,
                    insecure: options.insecure,
                });
            };
            const finish = (result) => {
                if (resolved)
                    return;
                resolved = true;
                if (timeout)
                    clearTimeout(timeout);
                if (!result.success) {
                    (0, proxy_daemon_state_1.removeOpenAICompatProxyAuthTokenFile)(authTokenFile);
                    if (persistState) {
                        (0, proxy_daemon_state_1.removeOpenAICompatProxyPid)(profile.profileName);
                        (0, proxy_daemon_state_1.removeOpenAICompatProxySession)(profile.profileName);
                    }
                }
                resolve(result);
            };
            const proc = (0, child_process_1.spawn)(process.execPath, [
                daemonEntry,
                '--port',
                String(port),
                '--host',
                host,
                '--profile',
                profile.profileName,
                '--settings-path',
                profile.settingsPath,
                '--auth-token-file',
                authTokenFile,
                ...(options.insecure ? ['--insecure'] : []),
                '--ccs-openai-proxy-daemon',
            ], { stdio: ['ignore', 'ignore', 'pipe'], detached: true });
            proc.unref();
            proc.stderr?.setEncoding('utf8');
            proc.stderr?.on('data', (chunk) => {
                stderr += chunk;
            });
            let attempts = 0;
            const poll = async () => {
                attempts += 1;
                if (await isOpenAICompatProxyRunning(port, profile.profileName)) {
                    if (persistState) {
                        commitState();
                    }
                    finish({
                        success: true,
                        pid: proc.pid,
                        port,
                        authToken,
                        ...(persistState
                            ? {}
                            : {
                                commitState,
                                stop: async () => {
                                    await terminateDaemonProcess(proc.pid);
                                },
                            }),
                    });
                    return;
                }
                if (attempts >= 30) {
                    finish({
                        success: false,
                        port,
                        error: `Proxy daemon did not start within 30 seconds on port ${port}`,
                    });
                    return;
                }
                timeout = setTimeout(poll, 1000);
            };
            timeout = setTimeout(poll, 1000);
            proc.on('error', (error) => {
                finish({
                    success: false,
                    port,
                    error: error.message,
                    bindConflict: isPortBindConflictMessage(error.message),
                });
            });
            proc.on('exit', (code, signal) => {
                const bindConflict = isPortBindConflictMessage(stderr);
                if (code === 0) {
                    finish({
                        success: false,
                        port,
                        error: 'Proxy daemon exited before becoming healthy',
                        bindConflict,
                    });
                    return;
                }
                if (code !== null) {
                    finish({
                        success: false,
                        port,
                        error: stderr.trim() || `Proxy daemon exited with code ${code}`,
                        bindConflict,
                    });
                    return;
                }
                finish({
                    success: false,
                    port,
                    error: `Proxy daemon was killed by signal ${signal}`,
                    bindConflict,
                });
            });
        });
        const launchProxy = async (persistState) => {
            const attemptedPorts = new Set();
            let lastResult = null;
            const candidates = listOpenAICompatProxyCandidatePorts(profile.profileName, preferredPort, requiresExactPort, attemptedPorts);
            if (candidates.length === 0) {
                return {
                    success: false,
                    port: preferredPort,
                    error: `No free proxy port found in adaptive range ${proxy_daemon_paths_1.OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START}-${proxy_daemon_paths_1.OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END}`,
                };
            }
            for (const port of candidates) {
                const result = await launchOnPort(port, persistState);
                if (result.success) {
                    return result;
                }
                if (requiresExactPort && result.bindConflict) {
                    return {
                        ...result,
                        error: `Requested proxy port ${preferredPort} is already in use`,
                    };
                }
                lastResult = result;
                attemptedPorts.add(port);
                if (!result.bindConflict) {
                    return result;
                }
            }
            return (lastResult ?? {
                success: false,
                port: preferredPort,
                error: requiresExactPort
                    ? `Requested proxy port ${preferredPort} is already in use`
                    : 'No free proxy port found in the adaptive proxy port range',
            });
        };
        if (status.running) {
            const launched = await launchProxy(false);
            if (!launched.success) {
                return launched;
            }
            const stopped = await stopOpenAICompatProxyUnlocked(profile.profileName);
            if (!stopped.success) {
                await launched.stop?.();
                return {
                    success: false,
                    port: launched.port,
                    error: stopped.error || 'Failed to replace the running proxy',
                };
            }
            launched.commitState?.();
            return launched;
        }
        return launchProxy(true);
    });
}
exports.startOpenAICompatProxy = startOpenAICompatProxy;
//# sourceMappingURL=proxy-daemon.js.map