"use strict";
/**
 * CLIProxy Service Manager
 *
 * Manages CLIProxyAPI as a background service for the CCS dashboard.
 * Ensures the proxy is running when needed for:
 * - Control Panel integration (management.html)
 * - Stats fetching
 * - OAuth flows
 *
 * Unlike cliproxy-executor.ts which runs proxy per-session,
 * this module manages a persistent background instance.
 */
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.getTokenRefreshStatus = exports.isTokenRefreshWorkerRunning = exports.getServiceStatus = exports.stopCliproxyService = exports.ensureCliproxyService = void 0;
const child_process_1 = require("child_process");
const net = __importStar(require("net"));
const binary_manager_1 = require("./binary-manager");
const config_generator_1 = require("./config/config-generator");
const session_tracker_1 = require("./session-tracker");
const proxy_detector_1 = require("./proxy/proxy-detector");
const startup_lock_1 = require("./services/startup-lock");
const stats_fetcher_1 = require("./services/stats-fetcher");
const token_refresh_worker_1 = require("./auth/token-refresh-worker");
const token_refresh_config_1 = require("./auth/token-refresh-config");
/** Background proxy process reference */
let proxyProcess = null;
/** Token refresh worker instance */
let tokenRefreshWorker = null;
/** Cleanup registered flag */
let cleanupRegistered = false;
/**
 * Wait for TCP port to become available
 */
async function waitForPort(port, timeout = 5000, pollInterval = 100) {
    const start = Date.now();
    while (Date.now() - start < timeout) {
        try {
            await new Promise((resolve, reject) => {
                const socket = net.createConnection({ port, host: '127.0.0.1' }, () => {
                    socket.destroy();
                    resolve();
                });
                socket.on('error', (err) => {
                    socket.destroy();
                    reject(err);
                });
                socket.setTimeout(500, () => {
                    socket.destroy();
                    reject(new Error('Connection timeout'));
                });
            });
            return true; // Connection successful
        }
        catch {
            await new Promise((r) => setTimeout(r, pollInterval));
        }
    }
    return false;
}
/**
 * Register process-exit cleanup handlers for in-process workers.
 */
function registerCleanup() {
    if (cleanupRegistered)
        return;
    const cleanup = () => {
        // Stop token refresh worker first
        if (tokenRefreshWorker && tokenRefreshWorker.isActive()) {
            tokenRefreshWorker.stop();
            tokenRefreshWorker = null;
        }
        // Do not stop detached CLIProxy on parent exit.
        // Persistence is expected across CCS command lifecycles.
    };
    process.once('exit', cleanup);
    process.once('SIGTERM', cleanup);
    process.once('SIGINT', cleanup);
    cleanupRegistered = true;
}
/**
 * Start token refresh worker if configured
 * @param verbose Enable verbose logging
 */
function startTokenRefreshWorker(verbose) {
    // Skip if already running
    if (tokenRefreshWorker && tokenRefreshWorker.isActive()) {
        return;
    }
    // Load config
    const config = (0, token_refresh_config_1.getTokenRefreshConfig)();
    if (!config) {
        // Not configured or disabled
        return;
    }
    // Create and start worker
    tokenRefreshWorker = new token_refresh_worker_1.TokenRefreshWorker({
        refreshInterval: config.interval_minutes ?? 30,
        preemptiveTime: config.preemptive_minutes ?? 45,
        maxRetries: config.max_retries ?? 3,
        verbose: config.verbose || verbose,
    });
    tokenRefreshWorker.start();
    if (verbose) {
        console.error('[i] Token refresh worker started');
    }
}
/**
 * Ensure CLIProxy service is running
 *
 * If proxy is already running, returns immediately.
 * If not, spawns a new background instance.
 *
 * @param port CLIProxy port (default: 8317)
 * @param verbose Show debug output
 * @param deps Test-only dependency overrides (see EnsureCliproxyServiceDeps)
 * @returns Result indicating success and whether it was already running
 */
async function ensureCliproxyService(port = config_generator_1.CLIPROXY_DEFAULT_PORT, verbose = false, deps = {}) {
    const ensureBinaryFn = deps.ensureBinaryFn ?? binary_manager_1.ensureCLIProxyBinary;
    const detectRunningProxyFn = deps.detectRunningProxyFn ?? proxy_detector_1.detectRunningProxy;
    const configNeedsRegenerationFn = deps.configNeedsRegenerationFn ?? config_generator_1.configNeedsRegeneration;
    const withStartupLockFn = deps.withStartupLockFn ?? startup_lock_1.withStartupLock;
    const log = (msg) => {
        if (verbose) {
            console.error(`[cliproxy-service] ${msg}`);
        }
    };
    // Check if config needs update (even if running)
    let configRegenerated = false;
    if (configNeedsRegenerationFn(port)) {
        log('Config outdated, regenerating...');
        (0, config_generator_1.regenerateConfig)(port);
        configRegenerated = true;
    }
    // Use startup lock to coordinate with other CCS processes (ccs agy, ccs config, etc.)
    return await withStartupLockFn(async () => {
        // Use unified detection (HTTP check + session-lock + port-process)
        log(`Checking if CLIProxy is running on port ${port}...`);
        const proxyStatus = await detectRunningProxyFn(port);
        log(`Proxy detection: ${JSON.stringify(proxyStatus)}`);
        if (proxyStatus.running && proxyStatus.verified) {
            // Already running and healthy
            log('CLIProxy already running');
            if (configRegenerated) {
                log('Config was updated - running instance will use new config on next restart');
            }
            return { started: true, alreadyRunning: true, port, configRegenerated };
        }
        if (proxyStatus.running && !proxyStatus.verified) {
            // Proxy detected but not ready yet (another process is starting it)
            log(`Proxy starting up (detected via ${proxyStatus.method}), waiting...`);
            const becameHealthy = await (0, proxy_detector_1.waitForProxyHealthy)(port, 5000);
            if (becameHealthy) {
                log('Proxy became healthy');
                return { started: true, alreadyRunning: true, port, configRegenerated };
            }
            // Proxy didn't become healthy - will try to start fresh below
            log('Proxy detected but not responding, will start fresh');
        }
        if (proxyStatus.blocked) {
            // Port blocked by non-CLIProxy process - try HTTP as last resort
            const isActuallyOurs = await (0, proxy_detector_1.waitForProxyHealthy)(port, 1000);
            if (isActuallyOurs) {
                log('Reclaimed CLIProxy with unrecognized process name');
                return { started: true, alreadyRunning: true, port, configRegenerated };
            }
            // Truly blocked
            return {
                started: false,
                alreadyRunning: false,
                port,
                error: `Port ${port} is blocked by ${proxyStatus.blocker?.processName}`,
            };
        }
        // Need to start new instance
        log('CLIProxy not running, starting background instance...');
        // 1. Ensure binary exists
        let binaryPath;
        try {
            binaryPath = await ensureBinaryFn(verbose, {
                allowInstall: false,
                skipAutoUpdate: true,
            });
            log(`Binary ready: ${binaryPath}`);
        }
        catch (error) {
            const err = error;
            return {
                started: false,
                alreadyRunning: false,
                port,
                error: `Failed to prepare binary: ${err.message}`,
            };
        }
        // 2. Ensure/regenerate config if needed
        let configPath;
        if ((0, config_generator_1.configNeedsRegeneration)(port)) {
            log('Config needs regeneration, updating...');
            configPath = (0, config_generator_1.regenerateConfig)(port);
        }
        else {
            configPath = (0, config_generator_1.generateConfig)('gemini', port);
        }
        log(`Config ready: ${configPath}`);
        // 3. Spawn background process
        const proxyArgs = ['--config', configPath];
        log(`Spawning: ${binaryPath} ${proxyArgs.join(' ')}`);
        proxyProcess = (0, child_process_1.spawn)(binaryPath, proxyArgs, {
            stdio: ['ignore', verbose ? 'pipe' : 'ignore', verbose ? 'pipe' : 'ignore'],
            detached: true,
            env: {
                ...process.env,
                WRITABLE_PATH: (0, config_generator_1.getCliproxyWritablePath)(),
            },
        });
        if (verbose) {
            proxyProcess.stdout?.on('data', (data) => {
                process.stderr.write(`[cliproxy] ${data.toString()}`);
            });
            proxyProcess.stderr?.on('data', (data) => {
                process.stderr.write(`[cliproxy-err] ${data.toString()}`);
            });
        }
        proxyProcess.unref();
        proxyProcess.on('error', (error) => {
            log(`Spawn error: ${error.message}`);
        });
        registerCleanup();
        // 4. Wait for proxy to be ready
        log(`Waiting for CLIProxy on port ${port}...`);
        const ready = await waitForPort(port, 5000);
        if (!ready) {
            if (proxyProcess && !proxyProcess.killed) {
                proxyProcess.kill('SIGTERM');
                proxyProcess = null;
            }
            // Get backend label for error message
            const { loadOrCreateUnifiedConfig } = await Promise.resolve().then(() => __importStar(require('../config/unified-config-loader')));
            const { DEFAULT_BACKEND } = await Promise.resolve().then(() => __importStar(require('./binary/platform-detector')));
            const config = loadOrCreateUnifiedConfig();
            const backendLabel = (config.cliproxy?.backend ?? DEFAULT_BACKEND) === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
            return {
                started: false,
                alreadyRunning: false,
                port,
                error: `${backendLabel} failed to start within 5s on port ${port}`,
            };
        }
        log(`CLIProxy service started on port ${port}`);
        // 5. Register session
        if (proxyProcess.pid) {
            (0, session_tracker_1.registerSession)(port, proxyProcess.pid);
            log(`Session registered for PID ${proxyProcess.pid}`);
        }
        // 6. Start token refresh worker if configured
        startTokenRefreshWorker(verbose);
        return { started: true, alreadyRunning: false, port };
    });
}
exports.ensureCliproxyService = ensureCliproxyService;
/**
 * Stop the managed CLIProxy service
 */
function stopCliproxyService() {
    // Stop token refresh worker first
    if (tokenRefreshWorker && tokenRefreshWorker.isActive()) {
        tokenRefreshWorker.stop();
        tokenRefreshWorker = null;
    }
    // Then stop proxy process
    if (proxyProcess && !proxyProcess.killed) {
        proxyProcess.kill('SIGTERM');
        proxyProcess = null;
        return true;
    }
    return false;
}
exports.stopCliproxyService = stopCliproxyService;
/**
 * Get service status
 */
async function getServiceStatus(port = config_generator_1.CLIPROXY_DEFAULT_PORT) {
    const running = await (0, stats_fetcher_1.isCliproxyRunning)(port);
    const managedByUs = proxyProcess !== null && !proxyProcess.killed;
    return { running, managedByUs, port };
}
exports.getServiceStatus = getServiceStatus;
/**
 * Check if token refresh worker is running
 */
function isTokenRefreshWorkerRunning() {
    return tokenRefreshWorker !== null && tokenRefreshWorker.isActive();
}
exports.isTokenRefreshWorkerRunning = isTokenRefreshWorkerRunning;
/**
 * Get token refresh worker status
 */
function getTokenRefreshStatus() {
    if (!tokenRefreshWorker) {
        return { running: false, lastResults: null };
    }
    return {
        running: tokenRefreshWorker.isActive(),
        lastResults: tokenRefreshWorker.getLastRefreshResults(),
    };
}
exports.getTokenRefreshStatus = getTokenRefreshStatus;
//# sourceMappingURL=service-manager.js.map