"use strict";
/**
 * Session Tracker for CLIProxy Multi-Instance Support
 *
 * Manages reference counting for shared CLIProxy instances.
 * Multiple CCS sessions can share a single proxy on the same port.
 * Proxy only terminates when ALL sessions exit (count reaches 0).
 *
 * Lock file format: ~/.ccs/cliproxy/sessions.json
 * {
 *   "port": 8317,
 *   "pid": 12345,        // CLIProxy process PID
 *   "sessions": ["abc123", "def456"],  // Active session IDs
 *   "startedAt": "2024-01-01T00:00:00Z"
 * }
 */
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.getRunningProxyVersion = exports.getProxyStatus = exports.stopProxy = exports.cleanupOrphanedSessions = exports.hasActiveSessions = exports.getSessionCount = exports.unregisterSession = exports.registerSession = exports.getExistingProxy = exports.deleteSessionLockForPort = exports.getSessionLockPath = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const lockfile = __importStar(require("proper-lockfile"));
const config_generator_1 = require("./config/config-generator");
const port_utils_1 = require("../utils/port-utils");
const config_generator_2 = require("./config/config-generator");
/** Generate unique session ID */
function generateSessionId() {
    return crypto.randomBytes(8).toString('hex');
}
/** Get path to session lock file for specific port */
function getSessionLockPathForPort(port) {
    if (port === config_generator_2.CLIPROXY_DEFAULT_PORT) {
        return path.join((0, config_generator_1.getCliproxyDir)(), 'sessions.json');
    }
    return path.join((0, config_generator_1.getCliproxyDir)(), `sessions-${port}.json`);
}
function ensureCliproxyDir() {
    const dir = (0, config_generator_1.getCliproxyDir)();
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
    }
    return dir;
}
function withSessionTrackerLock(fn) {
    const dir = ensureCliproxyDir();
    let release;
    try {
        release = lockfile.lockSync(dir, {
            stale: 10000,
        });
        return fn();
    }
    finally {
        if (release) {
            try {
                release();
            }
            catch {
                // Best-effort release
            }
        }
    }
}
/** Get path to session lock file (default port) - kept for future use */
function _getSessionLockPath() {
    return getSessionLockPathForPort(config_generator_2.CLIPROXY_DEFAULT_PORT);
}
exports.getSessionLockPath = _getSessionLockPath;
/** Read session lock file for specific port (returns null if not exists or invalid) */
function readSessionLockForPort(port) {
    const lockPath = getSessionLockPathForPort(port);
    try {
        if (!fs.existsSync(lockPath)) {
            return null;
        }
        const content = fs.readFileSync(lockPath, 'utf-8');
        const lock = JSON.parse(content);
        // Validate structure
        if (typeof lock.port !== 'number' ||
            typeof lock.pid !== 'number' ||
            !Array.isArray(lock.sessions)) {
            return null;
        }
        return lock;
    }
    catch {
        return null;
    }
}
/** Read session lock file (default port, returns null if not exists or invalid) */
function readSessionLock() {
    return readSessionLockForPort(config_generator_2.CLIPROXY_DEFAULT_PORT);
}
function getTargetSummary(lock) {
    const targets = lock.sessionTargets ? Object.values(lock.sessionTargets).filter(Boolean) : [];
    if (targets.length === 0) {
        return lock.target;
    }
    const unique = new Set(targets);
    if (unique.size === 1) {
        return targets[0];
    }
    return 'mixed';
}
/** Write session lock file for specific port */
function writeSessionLockForPort(lock) {
    const lockPath = getSessionLockPathForPort(lock.port);
    ensureCliproxyDir();
    lock.target = getTargetSummary(lock);
    fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2), { mode: 0o600 });
}
/** Delete session lock file for specific port */
function deleteSessionLockForPort(port) {
    const lockPath = getSessionLockPathForPort(port);
    try {
        if (fs.existsSync(lockPath)) {
            fs.unlinkSync(lockPath);
        }
    }
    catch {
        // Ignore errors on cleanup
    }
}
exports.deleteSessionLockForPort = deleteSessionLockForPort;
/** Delete session lock file (default port) */
function deleteSessionLock() {
    deleteSessionLockForPort(config_generator_2.CLIPROXY_DEFAULT_PORT);
}
/** Check if a PID is still running */
function isProcessRunning(pid) {
    try {
        // Sending signal 0 checks if process exists without killing it
        process.kill(pid, 0);
        return true;
    }
    catch (err) {
        const e = err;
        // EPERM means process exists but we don't have permission to signal it
        if (e.code === 'EPERM') {
            return true;
        }
        // ESRCH means no such process
        return false;
    }
}
/**
 * Wait for a process to exit within a timeout.
 * @param pid Process ID to wait for
 * @param timeoutMs Maximum time to wait in milliseconds
 * @returns true if process exited, false if timeout
 */
async function waitForProcessExit(pid, timeoutMs) {
    const start = Date.now();
    while (Date.now() - start < timeoutMs) {
        if (!isProcessRunning(pid)) {
            return true; // Process exited
        }
        await new Promise((resolve) => setTimeout(resolve, 100));
    }
    return false; // Timeout
}
/**
 * Check if there's an existing proxy running that we can reuse.
 * Returns the existing lock if proxy is healthy, null otherwise.
 */
function getExistingProxy(port) {
    const lock = readSessionLockForPort(port);
    if (!lock) {
        return null;
    }
    // Verify port matches
    if (lock.port !== port) {
        return null;
    }
    // Verify proxy process is still running
    if (!isProcessRunning(lock.pid)) {
        // Proxy crashed - clean up stale lock
        deleteSessionLockForPort(port);
        return null;
    }
    return lock;
}
exports.getExistingProxy = getExistingProxy;
/**
 * Register a new session with the proxy.
 * Call this when starting a new CCS session that will use an existing proxy.
 * @param port Port the proxy is running on
 * @param proxyPid PID of the proxy process
 * @param version Optional CLIProxy version (stored when spawning new proxy)
 * @param backend Optional backend type (original vs plus)
 * @returns Session ID for this session
 */
function registerSession(port, proxyPid, version, backend, target) {
    const sessionId = generateSessionId();
    withSessionTrackerLock(() => {
        const existingLock = readSessionLockForPort(port);
        if (existingLock && existingLock.port === port && existingLock.pid === proxyPid) {
            // Add to existing sessions
            existingLock.sessions.push(sessionId);
            if (target) {
                existingLock.sessionTargets = existingLock.sessionTargets || {};
                existingLock.sessionTargets[sessionId] = target;
            }
            writeSessionLockForPort(existingLock);
            return;
        }
        // Create new lock (first session for this proxy)
        const newLock = {
            port,
            pid: proxyPid,
            sessions: [sessionId],
            startedAt: new Date().toISOString(),
            version,
            backend,
            target,
            sessionTargets: target ? { [sessionId]: target } : undefined,
        };
        writeSessionLockForPort(newLock);
    });
    return sessionId;
}
exports.registerSession = registerSession;
/**
 * Unregister a session from the proxy.
 * @param sessionId Session ID to unregister
 * @param port Port to unregister from (optional, searches default port if not provided)
 * @returns true if this was the last session (proxy should be killed)
 */
function unregisterSession(sessionId, port) {
    // If port provided, use port-specific lookup
    if (port !== undefined) {
        return withSessionTrackerLock(() => {
            const lock = readSessionLockForPort(port);
            if (!lock) {
                return true;
            }
            const index = lock.sessions.indexOf(sessionId);
            if (index !== -1) {
                lock.sessions.splice(index, 1);
            }
            if (lock.sessionTargets) {
                delete lock.sessionTargets[sessionId];
                if (Object.keys(lock.sessionTargets).length === 0) {
                    delete lock.sessionTargets;
                }
            }
            if (lock.sessions.length === 0) {
                deleteSessionLockForPort(port);
                return true;
            }
            writeSessionLockForPort(lock);
            return false;
        });
    }
    // Fallback: search default port (backward compat)
    return withSessionTrackerLock(() => {
        const lock = readSessionLock();
        if (!lock) {
            // No lock file - assume we're the only session
            return true;
        }
        // Remove this session from the list
        const index = lock.sessions.indexOf(sessionId);
        if (index !== -1) {
            lock.sessions.splice(index, 1);
        }
        if (lock.sessionTargets) {
            delete lock.sessionTargets[sessionId];
            if (Object.keys(lock.sessionTargets).length === 0) {
                delete lock.sessionTargets;
            }
        }
        // Check if any sessions remain
        if (lock.sessions.length === 0) {
            // Last session - clean up lock file
            deleteSessionLock();
            return true;
        }
        // Other sessions still active - keep proxy running
        writeSessionLockForPort(lock);
        return false;
    });
}
exports.unregisterSession = unregisterSession;
/**
 * Get current session count for the proxy.
 * @param port Port to check (defaults to CLIPROXY_DEFAULT_PORT)
 */
function getSessionCount(port = config_generator_2.CLIPROXY_DEFAULT_PORT) {
    const lock = readSessionLockForPort(port);
    if (!lock) {
        return 0;
    }
    return lock.sessions.length;
}
exports.getSessionCount = getSessionCount;
/**
 * Check if proxy has any active sessions.
 * Used to determine if a "zombie" proxy should be killed.
 * @param port Port to check (defaults to CLIPROXY_DEFAULT_PORT)
 */
function hasActiveSessions(port = config_generator_2.CLIPROXY_DEFAULT_PORT) {
    const lock = readSessionLockForPort(port);
    if (!lock) {
        return false;
    }
    // Verify proxy is still running
    if (!isProcessRunning(lock.pid)) {
        deleteSessionLockForPort(port);
        return false;
    }
    return lock.sessions.length > 0;
}
exports.hasActiveSessions = hasActiveSessions;
/**
 * Clean up orphaned sessions (when proxy crashes).
 * Called on startup to ensure clean state.
 */
function cleanupOrphanedSessions(port) {
    const lock = readSessionLockForPort(port);
    if (!lock) {
        return;
    }
    // If port doesn't match, this shouldn't happen with port-specific files
    if (lock.port !== port) {
        return;
    }
    // If proxy is dead, clean up lock
    if (!isProcessRunning(lock.pid)) {
        deleteSessionLockForPort(port);
    }
}
exports.cleanupOrphanedSessions = cleanupOrphanedSessions;
/**
 * Stop the CLIProxy process and clean up session lock.
 * Falls back to port-based detection if no session lock exists.
 * @param port Port to stop (defaults to CLIPROXY_DEFAULT_PORT)
 * @returns Object with success status and details
 */
async function stopProxy(port = config_generator_2.CLIPROXY_DEFAULT_PORT) {
    const lock = readSessionLockForPort(port);
    if (!lock) {
        // No session lock - try to find process by port (legacy/untracked proxy)
        const portProcess = await (0, port_utils_1.getPortProcess)(port);
        if (!portProcess) {
            return { stopped: false, port, error: 'No active CLIProxy session found' };
        }
        if (!(0, port_utils_1.isCLIProxyProcess)(portProcess)) {
            return {
                stopped: false,
                port,
                error: `Port ${port} is in use by ${portProcess.processName}, not CLIProxy`,
            };
        }
        // Found CLIProxy running without session lock - kill it
        try {
            process.kill(portProcess.pid, 'SIGTERM');
            // Wait for graceful shutdown
            const exited = await waitForProcessExit(portProcess.pid, 3000);
            if (!exited) {
                // Escalate to SIGKILL
                try {
                    process.kill(portProcess.pid, 'SIGKILL');
                    await waitForProcessExit(portProcess.pid, 1000);
                }
                catch {
                    // Process may have exited between check and kill
                }
            }
            return { stopped: true, port, pid: portProcess.pid, sessionCount: 0 };
        }
        catch (err) {
            const error = err;
            if (error.code === 'ESRCH') {
                return { stopped: false, port, error: 'CLIProxy process already terminated' };
            }
            return {
                stopped: false,
                port,
                pid: portProcess.pid,
                error: `Failed to stop: ${error.message}`,
            };
        }
    }
    // Check if proxy is running
    if (!isProcessRunning(lock.pid)) {
        deleteSessionLockForPort(port);
        return { stopped: false, port, error: 'CLIProxy was not running (cleaned up stale lock)' };
    }
    const sessionCount = lock.sessions.length;
    const pid = lock.pid;
    try {
        // Kill the proxy process
        process.kill(pid, 'SIGTERM');
        // Wait for graceful shutdown
        const exited = await waitForProcessExit(pid, 3000);
        if (!exited) {
            // Escalate to SIGKILL
            try {
                process.kill(pid, 'SIGKILL');
                await waitForProcessExit(pid, 1000);
            }
            catch {
                // Process may have exited between check and kill
            }
        }
        // Clean up session lock
        deleteSessionLockForPort(port);
        return { stopped: true, port, pid, sessionCount };
    }
    catch (err) {
        const error = err;
        if (error.code === 'ESRCH') {
            // Process already gone
            deleteSessionLockForPort(port);
            return { stopped: false, port, error: 'CLIProxy process already terminated' };
        }
        return { stopped: false, port, pid, error: `Failed to stop: ${error.message}` };
    }
}
exports.stopProxy = stopProxy;
/**
 * Get proxy status information for specific port.
 */
function getProxyStatus(port = config_generator_2.CLIPROXY_DEFAULT_PORT) {
    const lock = readSessionLockForPort(port);
    if (!lock) {
        return { running: false };
    }
    // Verify proxy is still running
    if (!isProcessRunning(lock.pid)) {
        deleteSessionLockForPort(port);
        return { running: false };
    }
    return {
        running: true,
        port: lock.port,
        pid: lock.pid,
        sessionCount: lock.sessions.length,
        startedAt: lock.startedAt,
        version: lock.version,
        target: lock.target || 'claude',
    };
}
exports.getProxyStatus = getProxyStatus;
/**
 * Get the version of the running proxy from session lock.
 * @param port Port to check (defaults to CLIPROXY_DEFAULT_PORT)
 * @returns Version string if available, null otherwise
 */
function getRunningProxyVersion(port = config_generator_2.CLIPROXY_DEFAULT_PORT) {
    const lock = readSessionLockForPort(port);
    if (!lock) {
        return null;
    }
    // Verify proxy is still running
    if (!isProcessRunning(lock.pid)) {
        deleteSessionLockForPort(port);
        return null;
    }
    return lock.version ?? null;
}
exports.getRunningProxyVersion = getRunningProxyVersion;
//# sourceMappingURL=session-tracker.js.map