"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runServiceInstallCommand = runServiceInstallCommand;
exports.runServiceInstallCommandWithoutCurrentRepoRegistration = runServiceInstallCommandWithoutCurrentRepoRegistration;
exports.runServiceEnableCommand = runServiceEnableCommand;
exports.runServiceRestartCommand = runServiceRestartCommand;
exports.runServiceDisableCommand = runServiceDisableCommand;
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const control_plane_config_1 = require("../../config/control-plane-config");
const branding_1 = require("../../core/branding");
const command_1 = require("../../core/command");
const file_readers_1 = require("../../core/file-readers");
const package_metadata_1 = require("../../core/package-metadata");
const paths_1 = require("../../core/paths");
const repo_registry_1 = require("../../core/repo-registry");
const service_drain_state_1 = require("../../core/service-drain-state");
const service_manager_1 = require("../../core/service-manager");
const service_state_1 = require("../../core/service-state");
const catalog_1 = require("../../i18n/catalog");
const locale_1 = require("../locale");
const definition_1 = require("./definition");
const environment_1 = require("./environment");
const SERVICE_STATE_WAIT_TIMEOUT_MS = 10_000;
const SERVICE_DRAIN_LEASE_MS = 30_000;
const SERVICE_DRAIN_POLL_MS = 1_000;
const LAUNCHD_BOOTSTRAP_GRACE_MS = 1_000;
const PATH_ENV_VAR = "PATH";
const SERVICE_DRAIN_CANCEL_SIGNAL = "SIGINT";
async function runServiceInstallCommand(homeDirectory, deps, options = {}) {
    return runServiceInstall(homeDirectory, deps, {
        registerCurrentRepo: true,
        ...options,
    });
}
async function runServiceInstallCommandWithoutCurrentRepoRegistration(homeDirectory, deps, options = {}) {
    return runServiceInstall(homeDirectory, deps, {
        registerCurrentRepo: false,
        ...options,
    });
}
async function runServiceInstall(homeDirectory, deps, options) {
    const cwd = resolveServiceCommandCwd(deps);
    const locale = await (0, locale_1.loadCommandLocale)(cwd);
    const sleep = deps.sleep ?? defaultSleep;
    const log = deps.quiet ? () => { } : console.log;
    const current = await (0, service_state_1.getLiveServiceState)(homeDirectory);
    const logPath = (0, paths_1.getServiceLogPath)(homeDirectory);
    await ensureServiceLogFile(logPath);
    if (options.registerCurrentRepo) {
        await registerCurrentRepoIfConfigured(homeDirectory, cwd);
    }
    const cliEntrypoint = resolveCliEntrypoint(deps);
    if (!(await (0, file_readers_1.pathExists)(cliEntrypoint))) {
        throw new Error((0, catalog_1.translate)(locale, "service.command.enable.missingBuild", {
            packageRoot: resolveServicePackageRoot(deps, cliEntrypoint),
        }));
    }
    const resolvedManager = await requireSupportedServiceManager(homeDirectory, deps);
    const definitionBeforeInstall = await (0, file_readers_1.readTextFileIfExists)(resolvedManager.definitionPath);
    const definitionForEnvPreservation = options.preservedDefinition === undefined
        ? definitionBeforeInstall
        : options.preservedDefinition;
    const installPlan = await buildServiceInstallPlan(homeDirectory, deps, {
        manager: resolvedManager,
        cliEntrypoint,
        definitionForEnvPreservation,
        refreshEnv: options.refreshEnv === true,
    });
    const packageUpdatePending = current
        ? await hasPendingServicePackageUpdate(current)
        : false;
    if (current &&
        !options.refreshEnv &&
        !packageUpdatePending &&
        serviceDefinitionMatchesInstallPlan(definitionBeforeInstall, installPlan)) {
        if (!deps.quiet) {
            console.log((0, catalog_1.translate)(locale, "service.command.enable.alreadyEnabled", {
                pid: current.pid,
            }));
            console.log((0, catalog_1.translate)(locale, "service.command.enable.log", {
                path: logPath,
            }));
        }
        return 0;
    }
    await (0, promises_1.mkdir)(node_path_1.default.dirname(installPlan.definitionPath), { recursive: true });
    const run = deps.runCommand ?? command_1.runCommand;
    if (current !== null) {
        await runServiceDisableCommand(homeDirectory, {
            ...deps,
            ...(deps.quiet !== undefined ? { quiet: deps.quiet } : {}),
        });
    }
    await (0, promises_1.writeFile)(installPlan.definitionPath, installPlan.definition, "utf8");
    await activateInstalledService(homeDirectory, installPlan, current === null &&
        definitionBeforeInstall !== null &&
        !serviceDefinitionMatchesInstallPlan(definitionBeforeInstall, installPlan), run, sleep);
    const liveService = await waitForServiceState(homeDirectory, SERVICE_STATE_WAIT_TIMEOUT_MS, sleep);
    if (!liveService) {
        throw new Error((0, catalog_1.translate)(locale, "service.command.enable.didNotCreatePidInTime", {
            pidPath: (0, paths_1.getServicePidPath)(homeDirectory),
            logPath,
        }));
    }
    log((0, catalog_1.translate)(locale, "service.command.enable.started", {
        pid: liveService.pid,
    }));
    log((0, catalog_1.translate)(locale, "service.command.enable.log", { path: logPath }));
    return 0;
}
async function runServiceEnableCommand(homeDirectory, deps, options = {}) {
    return runServiceInstallCommand(homeDirectory, deps, {
        refreshEnv: options.refreshEnv === true,
    });
}
async function registerCurrentRepoIfConfigured(homeDirectory, cwd) {
    try {
        await (0, control_plane_config_1.loadConfigWithExtends)(cwd);
    }
    catch (error) {
        if (isMissingConfigError(error)) {
            return;
        }
        throw error;
    }
    const result = await (0, repo_registry_1.registerRepo)({
        cwd,
    }, homeDirectory);
    void result;
}
function isMissingConfigError(error) {
    return (typeof error === "object" &&
        error !== null &&
        "code" in error &&
        error.code === "ENOENT");
}
async function runServiceRestartCommand(homeDirectory, deps, options = {}) {
    if (options.force) {
        return runImmediateServiceRestartCommand(homeDirectory, deps, options);
    }
    const drainWait = await waitForServiceDrain(homeDirectory, deps, service_drain_state_1.SERVICE_DRAIN_ACTIONS.restart);
    if (!drainWait.shouldAct) {
        return runServiceInstallCommand(homeDirectory, deps, {
            refreshEnv: options.refreshEnv === true,
        });
    }
    return runWithServiceDrainCleanup(homeDirectory, drainWait.requestId, async () => {
        const installOptions = await buildServiceRestartInstallOptions(homeDirectory, deps, options);
        await runImmediateServiceDisableCommand(homeDirectory, deps);
        return runServiceInstallCommand(homeDirectory, deps, installOptions);
    });
}
async function runImmediateServiceRestartCommand(homeDirectory, deps, options = {}) {
    if ((await (0, service_state_1.getLiveServiceState)(homeDirectory)) === null) {
        return runServiceInstallCommand(homeDirectory, deps, {
            refreshEnv: options.refreshEnv === true,
        });
    }
    const installOptions = await buildServiceRestartInstallOptions(homeDirectory, deps, options);
    await runImmediateServiceDisableCommand(homeDirectory, deps);
    return runServiceInstallCommand(homeDirectory, deps, installOptions);
}
async function buildServiceRestartInstallOptions(homeDirectory, deps, options) {
    if (options.refreshEnv) {
        return { refreshEnv: true };
    }
    return {
        preservedDefinition: await readInstalledServiceDefinition(homeDirectory, deps),
        refreshEnv: false,
    };
}
async function runServiceDisableCommand(homeDirectory, deps = {}, options = {}) {
    if (options.force) {
        return runImmediateServiceDisableCommand(homeDirectory, deps);
    }
    const drainWait = await waitForServiceDrain(homeDirectory, deps, service_drain_state_1.SERVICE_DRAIN_ACTIONS.disable);
    if (!drainWait.shouldAct) {
        return runImmediateServiceDisableCommand(homeDirectory, deps);
    }
    return runWithServiceDrainCleanup(homeDirectory, drainWait.requestId, () => runImmediateServiceDisableCommand(homeDirectory, deps));
}
async function runWithServiceDrainCleanup(homeDirectory, requestId, action) {
    try {
        return await action();
    }
    finally {
        await (0, service_drain_state_1.removeServiceDrainStateIfCurrent)(homeDirectory, requestId);
    }
}
async function runImmediateServiceDisableCommand(homeDirectory, deps = {}) {
    const locale = await (0, locale_1.loadCommandLocale)(resolveServiceCommandCwd(deps));
    const manager = await (0, service_manager_1.detectServiceManager)(buildDetectServiceManagerOptions(homeDirectory, deps, locale));
    const run = deps.runCommand ?? command_1.runCommand;
    const sleep = deps.sleep ?? defaultSleep;
    const current = await (0, service_state_1.getLiveServiceState)(homeDirectory);
    if (!manager.supported) {
        if (current) {
            throw new Error((0, catalog_1.translate)(locale, "service.command.disable.unsupportedWhileRunning", {
                pid: current.pid,
                reason: manager.reason,
            }));
        }
        await removeDefinitionFiles((0, service_manager_1.getKnownServiceDefinitionPaths)(homeDirectory));
        await (0, promises_1.rm)((0, paths_1.getServicePidPath)(homeDirectory), { force: true });
        if (!deps.quiet) {
            console.log((0, catalog_1.translate)(locale, "service.command.disable.disabled"));
        }
        return 0;
    }
    if (manager.kind === service_manager_1.SERVICE_MANAGER_KINDS.launchd) {
        await run({
            command: service_manager_1.SERVICE_MANAGER_COMMANDS.launchctl,
            args: ["bootout", manager.launchctlDomain, manager.definitionPath],
            allowNonZeroExit: true,
            timeoutMs: 30_000,
        });
    }
    else {
        await run({
            command: service_manager_1.SERVICE_MANAGER_COMMANDS.systemctl,
            args: ["--user", "disable", "--now", branding_1.SYSTEMD_UNIT_NAME],
            allowNonZeroExit: true,
            timeoutMs: 30_000,
        });
    }
    await (0, promises_1.rm)(manager.definitionPath, { force: true });
    if (manager.kind === service_manager_1.SERVICE_MANAGER_KINDS.systemdUser) {
        await run({
            command: service_manager_1.SERVICE_MANAGER_COMMANDS.systemctl,
            args: ["--user", "daemon-reload"],
            allowNonZeroExit: true,
            timeoutMs: 30_000,
        });
    }
    const stopped = await waitForServiceStop(homeDirectory, SERVICE_STATE_WAIT_TIMEOUT_MS, sleep);
    if (!stopped) {
        throw new Error((0, catalog_1.translate)(locale, "service.command.disable.didNotStopCleanly"));
    }
    await (0, promises_1.rm)((0, paths_1.getServicePidPath)(homeDirectory), { force: true });
    if (!deps.quiet) {
        if (current) {
            console.log((0, catalog_1.translate)(locale, "service.command.disable.stopped", {
                pid: current.pid,
            }));
        }
        else {
            console.log((0, catalog_1.translate)(locale, "service.command.disable.disabled"));
        }
    }
    return 0;
}
async function waitForServiceDrain(homeDirectory, deps, action) {
    const locale = await (0, locale_1.loadCommandLocale)(resolveServiceCommandCwd(deps));
    const sleep = deps.sleep ?? defaultSleep;
    const current = await (0, service_state_1.getLiveServiceState)(homeDirectory);
    if (!current) {
        return { shouldAct: false };
    }
    if (!current.capabilities?.includes(service_state_1.SERVICE_PROCESS_CAPABILITIES.serviceDrain)) {
        throw new Error((0, catalog_1.translate)(locale, "service.command.serviceDrain.unsupportedService", {
            pid: current.pid,
        }));
    }
    const request = (0, service_drain_state_1.createServiceDrainState)({
        action,
        targetPid: current.pid,
        targetStartedAt: current.startedAt,
        leaseMs: SERVICE_DRAIN_LEASE_MS,
    });
    await (0, service_drain_state_1.writeServiceDrainState)(request, homeDirectory);
    const cancellation = installServiceDrainCancellation(homeDirectory, request.requestId);
    if (!deps.quiet) {
        console.log((0, catalog_1.translate)(locale, "service.command.serviceDrain.waiting", {
            action,
        }));
    }
    try {
        while (true) {
            if (cancellation.interrupted) {
                throw new Error((0, catalog_1.translate)(locale, "service.command.serviceDrain.interrupted"));
            }
            const liveService = await (0, service_state_1.getLiveServiceState)(homeDirectory);
            if (liveService === null ||
                liveService.pid !== current.pid ||
                liveService.startedAt !== current.startedAt) {
                throw new Error((0, catalog_1.translate)(locale, "service.command.serviceDrain.serviceChanged"));
            }
            const updated = await (0, service_drain_state_1.updateServiceDrainState)(homeDirectory, request.requestId, (state) => ({
                ...state,
                leaseExpiresAt: new Date(Date.now() + SERVICE_DRAIN_LEASE_MS).toISOString(),
            }));
            if (updated === null) {
                throw new Error((0, catalog_1.translate)(locale, "service.command.serviceDrain.requestLost"));
            }
            if (updated.current.status === service_drain_state_1.SERVICE_DRAIN_STATUSES.drained) {
                return { shouldAct: true, requestId: request.requestId };
            }
            await sleep(SERVICE_DRAIN_POLL_MS);
        }
    }
    catch (error) {
        await (0, service_drain_state_1.removeServiceDrainStateIfCurrent)(homeDirectory, request.requestId);
        throw error;
    }
    finally {
        cancellation.dispose();
    }
}
function installServiceDrainCancellation(homeDirectory, requestId) {
    let interrupted = false;
    const cleanup = async () => {
        await (0, service_drain_state_1.removeServiceDrainStateIfCurrent)(homeDirectory, requestId);
    };
    const signalHandler = () => {
        interrupted = true;
        void cleanup();
    };
    process.once(SERVICE_DRAIN_CANCEL_SIGNAL, signalHandler);
    return {
        get interrupted() {
            return interrupted;
        },
        dispose() {
            process.off(SERVICE_DRAIN_CANCEL_SIGNAL, signalHandler);
        },
    };
}
function buildDetectServiceManagerOptions(homeDirectory, deps, locale) {
    return {
        ...(homeDirectory ? { homeDirectory } : {}),
        ...(deps.platform ? { platform: deps.platform } : {}),
        ...(deps.release ? { release: deps.release } : {}),
        ...(deps.env ? { env: deps.env } : {}),
        ...(deps.uid !== undefined ? { uid: deps.uid } : {}),
        ...(locale ? { locale } : {}),
        ...(deps.commandExists ? { commandExists: deps.commandExists } : {}),
        runCommand: deps.runCommand ?? command_1.runCommand,
    };
}
async function requireSupportedServiceManager(homeDirectory, deps) {
    const locale = await (0, locale_1.loadCommandLocale)(resolveServiceCommandCwd(deps));
    const manager = await (0, service_manager_1.detectServiceManager)(buildDetectServiceManagerOptions(homeDirectory, deps, locale));
    if (!manager.supported) {
        throw new Error((0, catalog_1.translate)(locale, "service.command.unsupportedEnvironment", {
            reason: manager.reason,
        }));
    }
    return manager;
}
async function readInstalledServiceDefinition(homeDirectory, deps) {
    const manager = await requireSupportedServiceManager(homeDirectory, deps);
    return (0, file_readers_1.readTextFileIfExists)(manager.definitionPath);
}
function resolveServiceCommandCwd(deps) {
    return deps.cwd ?? process.cwd();
}
async function ensureServiceLogFile(logPath) {
    await (0, promises_1.mkdir)(node_path_1.default.dirname(logPath), { recursive: true });
    const logFile = await (0, promises_1.open)(logPath, "a");
    await logFile.close();
}
async function buildServiceInstallPlan(homeDirectory, deps, input) {
    const nodePath = node_path_1.default.resolve(deps.nodePath ?? process.execPath);
    const previousEnvironment = input.refreshEnv === false
        ? (0, definition_1.readServiceDefinitionEnvironment)(input.definitionForEnvPreservation, input.manager)
        : null;
    const servicePath = previousEnvironment?.servicePath ?? resolveServicePath(deps.env);
    const environment = previousEnvironment?.environment ??
        (await (0, environment_1.collectServiceEnvironment)(homeDirectory, deps.env));
    const logPath = (0, paths_1.getServiceLogPath)(homeDirectory);
    const definition = (0, definition_1.renderServiceDefinition)({
        manager: input.manager,
        nodePath,
        cliEntrypoint: input.cliEntrypoint,
        logPath,
        ...(servicePath ? { servicePath } : {}),
        environment,
    });
    return {
        manager: input.manager,
        definitionPath: input.manager.definitionPath,
        definition,
        identity: {
            nodePath,
            cliEntrypoint: input.cliEntrypoint,
            logPath,
        },
    };
}
function serviceDefinitionMatchesInstallPlan(definition, installPlan) {
    return (0, definition_1.serviceDefinitionMatchesIdentity)(definition, installPlan.manager, installPlan.identity);
}
async function hasPendingServicePackageUpdate(current) {
    if (current.packageName === undefined ||
        current.packageVersion === undefined ||
        current.entrypointPath === undefined) {
        return false;
    }
    const currentPackageMetadata = await (0, package_metadata_1.readPackageMetadataForEntrypoint)(current.entrypointPath);
    return (currentPackageMetadata !== null &&
        currentPackageMetadata.packageName === current.packageName &&
        currentPackageMetadata.packageVersion !== current.packageVersion);
}
async function activateInstalledService(homeDirectory, installPlan, restartChangedSystemdService, run, sleep) {
    if (installPlan.manager.kind === service_manager_1.SERVICE_MANAGER_KINDS.launchd) {
        await run({
            command: service_manager_1.SERVICE_MANAGER_COMMANDS.launchctl,
            args: [
                "bootout",
                installPlan.manager.launchctlDomain,
                installPlan.definitionPath,
            ],
            allowNonZeroExit: true,
            timeoutMs: 30_000,
        });
        await run({
            command: service_manager_1.SERVICE_MANAGER_COMMANDS.launchctl,
            args: [
                "bootstrap",
                installPlan.manager.launchctlDomain,
                installPlan.definitionPath,
            ],
            timeoutMs: 30_000,
        });
        const bootstrappedService = await waitForServiceState(homeDirectory, LAUNCHD_BOOTSTRAP_GRACE_MS, sleep);
        if (!bootstrappedService) {
            await run({
                command: service_manager_1.SERVICE_MANAGER_COMMANDS.launchctl,
                args: ["kickstart", installPlan.manager.launchctlTarget],
                timeoutMs: 30_000,
            });
        }
        return;
    }
    await run({
        command: service_manager_1.SERVICE_MANAGER_COMMANDS.systemctl,
        args: ["--user", "daemon-reload"],
        timeoutMs: 30_000,
    });
    await run({
        command: service_manager_1.SERVICE_MANAGER_COMMANDS.systemctl,
        args: ["--user", "enable", "--now", branding_1.SYSTEMD_UNIT_NAME],
        timeoutMs: 30_000,
    });
    if (restartChangedSystemdService) {
        await run({
            command: service_manager_1.SERVICE_MANAGER_COMMANDS.systemctl,
            args: ["--user", "restart", branding_1.SYSTEMD_UNIT_NAME],
            timeoutMs: 30_000,
        });
    }
}
async function waitForServiceState(homeDirectory, timeoutMs, sleep) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        const liveService = await (0, service_state_1.getLiveServiceState)(homeDirectory);
        if (liveService) {
            return liveService;
        }
        await sleep(100);
    }
    return (0, service_state_1.getLiveServiceState)(homeDirectory);
}
async function waitForServiceStop(homeDirectory, timeoutMs, sleep) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        if ((await (0, service_state_1.getLiveServiceState)(homeDirectory)) === null) {
            return true;
        }
        await sleep(100);
    }
    return (await (0, service_state_1.getLiveServiceState)(homeDirectory)) === null;
}
async function defaultSleep(milliseconds) {
    await new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function resolveCliEntrypoint(deps) {
    if (deps.cliEntrypointPath) {
        return node_path_1.default.resolve(deps.cliEntrypointPath);
    }
    if (require.main?.filename?.endsWith(`${node_path_1.default.sep}main.js`)) {
        return node_path_1.default.resolve(process.argv[1] ?? require.main.filename);
    }
    return node_path_1.default.join(node_path_1.default.resolve(deps.packageRootPath ?? (0, paths_1.getPackageRoot)()), "dist/cli/main.js");
}
function resolveServicePackageRoot(deps, cliEntrypoint) {
    if (deps.packageRootPath) {
        return node_path_1.default.resolve(deps.packageRootPath);
    }
    return node_path_1.default.resolve(cliEntrypoint, "..", "..", "..");
}
function resolveServicePath(env) {
    const candidate = env?.[PATH_ENV_VAR] ?? process.env[PATH_ENV_VAR];
    if (!candidate || candidate.trim().length === 0) {
        return undefined;
    }
    return candidate;
}
async function removeDefinitionFiles(definitionPaths) {
    for (const definitionPath of definitionPaths) {
        await (0, promises_1.rm)(definitionPath, { force: true });
    }
}
