"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 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 environment_1 = require("./environment");
const shared_1 = require("./shared");
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) {
    return runServiceInstall(homeDirectory, deps, {
        registerCurrentRepo: true,
    });
}
async function runServiceInstallCommandWithoutCurrentRepoRegistration(homeDirectory, deps) {
    return runServiceInstall(homeDirectory, deps, {
        registerCurrentRepo: false,
    });
}
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, locale, cwd);
    }
    if (current &&
        (await hasKnownServiceDefinitionForLogPath(homeDirectory, logPath))) {
        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;
    }
    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 installPlan = await buildServiceInstallPlan(homeDirectory, deps, {
        manager: resolvedManager,
        cliEntrypoint,
    });
    const previousDefinition = await (0, file_readers_1.readTextFileIfExists)(installPlan.definitionPath);
    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, previousDefinition, 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) {
    return runServiceInstallCommand(homeDirectory, deps);
}
async function registerCurrentRepoIfConfigured(homeDirectory, locale, cwd) {
    let config;
    try {
        config = await (0, control_plane_config_1.loadConfig)(cwd);
    }
    catch (error) {
        if (isMissingConfigError(error)) {
            return;
        }
        throw error;
    }
    const result = await (0, repo_registry_1.registerRepo)({
        repositoryHost: config.repositoryHost,
        ticketTracker: config.ticketTracker,
        cwd,
    }, homeDirectory);
    if (result.kind === "conflict") {
        throw new Error((0, catalog_1.translate)(locale, "init.command.alreadyRegistered", {
            repo: `${config.repositoryHost.kind} ${config.repositoryHost.projectRef}`,
            cwd: result.existing.cwd,
        }));
    }
}
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);
    }
    const drainWait = await waitForServiceDrain(homeDirectory, deps, service_drain_state_1.SERVICE_DRAIN_ACTIONS.restart);
    if (!drainWait.shouldAct) {
        return runServiceInstallCommand(homeDirectory, deps);
    }
    await runWithServiceDrainCleanup(homeDirectory, drainWait.requestId, () => runImmediateServiceDisableCommand(homeDirectory, deps));
    return runServiceInstallCommand(homeDirectory, deps);
}
async function runImmediateServiceRestartCommand(homeDirectory, deps) {
    if ((await (0, service_state_1.getLiveServiceState)(homeDirectory)) === null) {
        return runServiceInstallCommand(homeDirectory, deps);
    }
    await runImmediateServiceDisableCommand(homeDirectory, deps);
    return runServiceInstallCommand(homeDirectory, deps);
}
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;
}
function resolveServiceCommandCwd(deps) {
    return deps.cwd ?? process.cwd();
}
async function hasKnownServiceDefinitionForLogPath(homeDirectory, logPath) {
    const definitions = await Promise.all((0, service_manager_1.getKnownServiceDefinitionPaths)(homeDirectory).map((definitionPath) => (0, file_readers_1.readTextFileIfExists)(definitionPath)));
    const existingDefinitions = definitions.filter((definition) => definition !== null);
    return (existingDefinitions.length > 0 &&
        existingDefinitions.every((definition) => definition.includes(logPath)));
}
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 servicePath = resolveServicePath(deps.env);
    const environment = await (0, environment_1.collectServiceEnvironment)(homeDirectory, deps.env);
    const definition = renderServiceDefinition({
        manager: input.manager,
        nodePath,
        cliEntrypoint: input.cliEntrypoint,
        logPath: (0, paths_1.getServiceLogPath)(homeDirectory),
        ...(servicePath ? { servicePath } : {}),
        environment,
    });
    return {
        manager: input.manager,
        definitionPath: input.manager.definitionPath,
        definition,
    };
}
async function activateInstalledService(homeDirectory, installPlan, previousDefinition, 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 (previousDefinition !== null &&
        previousDefinition !== installPlan.definition) {
        await run({
            command: service_manager_1.SERVICE_MANAGER_COMMANDS.systemctl,
            args: ["--user", "restart", branding_1.SYSTEMD_UNIT_NAME],
            timeoutMs: 30_000,
        });
    }
}
function renderServiceDefinition(options) {
    if (options.manager.kind === service_manager_1.SERVICE_MANAGER_KINDS.launchd) {
        return renderLaunchdPlist(options);
    }
    return renderSystemdUnit(options);
}
function renderLaunchdPlist(options) {
    const args = [
        options.nodePath,
        options.cliEntrypoint,
        "service",
        shared_1.SERVICE_COMMAND_ACTIONS.run,
    ]
        .map((value) => `    <string>${escapeXml(value)}</string>`)
        .join("\n");
    const pathEntry = options.servicePath
        ? `    <key>PATH</key>\n    <string>${escapeXml(options.servicePath)}</string>\n`
        : "";
    const environmentEntries = Object.entries(options.environment)
        .map(([key, value]) => `    <key>${escapeXml(key)}</key>\n    <string>${escapeXml(value)}</string>\n`)
        .join("");
    return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>${branding_1.SERVICE_LABEL}</string>
  <key>ProgramArguments</key>
  <array>
${args}
  </array>
  <key>EnvironmentVariables</key>
  <dict>
    <key>${branding_1.INTERNAL_SERVICE_ENV_NAME}</key>
    <string>1</string>
${pathEntry}${environmentEntries}  </dict>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>StandardOutPath</key>
  <string>${escapeXml(options.logPath)}</string>
  <key>StandardErrorPath</key>
  <string>${escapeXml(options.logPath)}</string>
</dict>
</plist>
`;
}
function renderSystemdUnit(options) {
    const execStart = [
        options.nodePath,
        options.cliEntrypoint,
        "service",
        shared_1.SERVICE_COMMAND_ACTIONS.run,
    ]
        .map(quoteSystemdArgument)
        .join(" ");
    const pathEnvironment = options.servicePath
        ? `Environment=${quoteSystemdEnvironmentAssignment("PATH", options.servicePath)}\n`
        : "";
    const environment = Object.entries(options.environment)
        .map(([key, value]) => `Environment=${quoteSystemdEnvironmentAssignment(key, value)}\n`)
        .join("");
    return `[Unit]
Description=${branding_1.SERVICE_DESCRIPTION}

[Service]
Environment=${branding_1.INTERNAL_SERVICE_ENV_NAME}=1
${pathEnvironment}${environment}ExecStart=${execStart}
Restart=always
RestartSec=5
StandardOutput=append:${options.logPath}
StandardError=append:${options.logPath}

[Install]
WantedBy=default.target
`;
}
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 quoteSystemdArgument(value) {
    return `"${value
        .replaceAll("\\", "\\\\")
        .replaceAll('"', '\\"')
        .replaceAll("%", "%%")}"`;
}
function quoteSystemdEnvironmentAssignment(key, value) {
    return `"${key}=${value
        .replaceAll("\\", "\\\\")
        .replaceAll('"', '\\"')
        .replaceAll("%", "%%")}"`;
}
function resolveServicePath(env) {
    const candidate = env?.[PATH_ENV_VAR] ?? process.env[PATH_ENV_VAR];
    if (!candidate || candidate.trim().length === 0) {
        return undefined;
    }
    return candidate;
}
function escapeXml(value) {
    return value
        .replaceAll("&", "&amp;")
        .replaceAll("<", "&lt;")
        .replaceAll(">", "&gt;")
        .replaceAll('"', "&quot;")
        .replaceAll("'", "&apos;");
}
async function removeDefinitionFiles(definitionPaths) {
    for (const definitionPath of definitionPaths) {
        await (0, promises_1.rm)(definitionPath, { force: true });
    }
}
