import { accessSync, constants as fsConstants } from 'node:fs';
import { chmod, mkdir, open, rename, rm, stat, writeFile } from 'node:fs/promises';
import { basename, dirname, join } from 'node:path';
import { createManagedToolScratchDir } from './createManagedToolScratchDir.js';
import { downloadGitHubReleaseAsset } from './downloadGitHubReleaseAsset.js';
import { extractGitHubReleaseAsset } from './extractGitHubReleaseAsset.js';
import { fetchNodeRuntimeReleaseAsset } from './nodeRelease.js';
import { resolveHappyHomeDirFromEnvironment } from './resolveHappyHomeDir.js';
function resolveManagedJavaScriptRuntimeBinaryName() {
    return process.platform === 'win32' ? 'happier-js-runtime.cmd' : 'happier-js-runtime';
}
export function managedJavaScriptRuntimeInstallDir(processEnv = process.env) {
    return join(resolveHappyHomeDirFromEnvironment(processEnv), 'tools', 'js-runtime');
}
export function managedJavaScriptRuntimeBinPath(processEnv = process.env) {
    return join(managedJavaScriptRuntimeInstallDir(processEnv), 'current', 'bin', resolveManagedJavaScriptRuntimeBinaryName());
}
function resolveNextManagedJavaScriptRuntimeBinPath(processEnv) {
    return join(managedJavaScriptRuntimeInstallDir(processEnv), 'next', 'bin', resolveManagedJavaScriptRuntimeBinaryName());
}
function resolveManagedNodeBinaryPath(processEnv) {
    return process.platform === 'win32'
        ? join(managedJavaScriptRuntimeInstallDir(processEnv), 'current', 'runtime', 'node.exe')
        : join(managedJavaScriptRuntimeInstallDir(processEnv), 'current', 'runtime', 'bin', 'node');
}
export function resolveJavaScriptRuntimePathEntries(params = {}) {
    const processEnv = params.processEnv ?? process.env;
    const runtimeCommand = typeof params.runtimeCommand === 'string' && params.runtimeCommand.trim().length > 0
        ? params.runtimeCommand.trim()
        : resolveJavaScriptRuntimeCommand({ isBunRuntime: isDirectBunExecutablePath(process.execPath), processEnv });
    if (!runtimeCommand) {
        return [];
    }
    const normalizedBase = basename(runtimeCommand).trim().toLowerCase();
    if (normalizedBase === 'node' || normalizedBase === 'node.exe') {
        return [dirname(runtimeCommand)];
    }
    const managedWrapperPath = managedJavaScriptRuntimeBinPath(processEnv);
    if (runtimeCommand === managedWrapperPath) {
        return [dirname(resolveManagedNodeBinaryPath(processEnv))];
    }
    if (normalizedBase === 'happier-js-runtime' || normalizedBase === 'happier-js-runtime.cmd') {
        const siblingRuntimeDir = process.platform === 'win32'
            ? join(dirname(runtimeCommand), '..', 'runtime')
            : join(dirname(runtimeCommand), '..', 'runtime', 'bin');
        try {
            accessSync(process.platform === 'win32' ? join(siblingRuntimeDir, 'node.exe') : join(siblingRuntimeDir, 'node'), process.platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
            return [siblingRuntimeDir];
        }
        catch {
            return [dirname(runtimeCommand)];
        }
    }
    return [dirname(runtimeCommand)];
}
function resolveNextManagedNodeBinaryPath(processEnv, binaryRelativePath) {
    return join(managedJavaScriptRuntimeInstallDir(processEnv), 'next', 'runtime', binaryRelativePath);
}
export function resolveExplicitJavaScriptRuntimeCommand(processEnv = process.env) {
    const override = readExplicitJavaScriptRuntimeCommand(processEnv);
    if (override) {
        try {
            accessSync(override, process.platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
            return override;
        }
        catch {
            return null;
        }
    }
    return null;
}
export function readExplicitJavaScriptRuntimeCommand(processEnv = process.env) {
    const override = [
        processEnv.HAPPIER_JS_RUNTIME_PATH,
        processEnv.HAPPIER_MANAGED_NODE_BIN,
        processEnv.HAPPIER_NODE_PATH,
    ]
        .map((value) => String(value ?? '').trim())
        .find((value) => value.length > 0);
    return override ?? null;
}
export function resolveExistingManagedJavaScriptRuntimeCommand(processEnv = process.env) {
    const rawOverride = readExplicitJavaScriptRuntimeCommand(processEnv);
    const override = resolveExplicitJavaScriptRuntimeCommand(processEnv);
    if (rawOverride && !override)
        return null;
    if (override)
        return override;
    const managedPath = managedJavaScriptRuntimeBinPath(processEnv);
    try {
        accessSync(managedPath, process.platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
        accessSync(resolveManagedNodeBinaryPath(processEnv), process.platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
        return managedPath;
    }
    catch {
        return null;
    }
}
function isDirectBunExecutablePath(execPath) {
    const normalized = basename(execPath).trim().toLowerCase();
    return normalized === 'bun' || normalized === 'bun.exe';
}
function isDirectNodeExecutablePath(execPath) {
    const normalized = basename(execPath).trim().toLowerCase();
    return normalized === 'node' || normalized === 'node.exe';
}
function isManagedJavaScriptRuntimeWrapperPath(execPath) {
    const normalized = basename(execPath).trim().toLowerCase();
    return normalized === 'happier-js-runtime' || normalized === 'happier-js-runtime.cmd';
}
function isRunnableExecutablePath(pathLike) {
    const candidate = String(pathLike ?? '').trim();
    if (!candidate)
        return false;
    try {
        accessSync(candidate, process.platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
        return true;
    }
    catch {
        return false;
    }
}
export function resolveJavaScriptRuntimeCommand(params) {
    const processEnv = params.processEnv ?? process.env;
    const currentExecPath = typeof params.currentExecPath === 'string' && params.currentExecPath.trim().length > 0
        ? params.currentExecPath.trim()
        : process.execPath;
    const rawOverride = readExplicitJavaScriptRuntimeCommand(processEnv);
    const override = resolveExplicitJavaScriptRuntimeCommand(processEnv);
    if (rawOverride && !override)
        return null;
    if (override)
        return override;
    if (!params.isBunRuntime) {
        if (isRunnableExecutablePath(currentExecPath)
            && (isDirectNodeExecutablePath(currentExecPath) || isManagedJavaScriptRuntimeWrapperPath(currentExecPath))) {
            return currentExecPath;
        }
        return resolveExistingManagedJavaScriptRuntimeCommand(processEnv);
    }
    if (isRunnableExecutablePath(currentExecPath)
        && (isDirectNodeExecutablePath(currentExecPath) || isManagedJavaScriptRuntimeWrapperPath(currentExecPath))) {
        return currentExecPath;
    }
    if (isDirectBunExecutablePath(currentExecPath))
        return currentExecPath;
    return resolveExistingManagedJavaScriptRuntimeCommand(processEnv);
}
async function writeManagedJavaScriptRuntimeWrapper(params) {
    await mkdir(dirname(params.outputPath), { recursive: true });
    if (process.platform === 'win32') {
        await writeFile(params.outputPath, `@echo off\r\n"${params.runtimeBinaryPath}" %*\r\n`, 'utf8');
        return;
    }
    await writeFile(params.outputPath, `#!/bin/sh\nexec "${params.runtimeBinaryPath}" "$@"\n`, 'utf8');
    await chmod(params.outputPath, 0o755);
}
const JS_RUNTIME_BOOTSTRAP_LOCK_MAX_RETRIES = 40;
const JS_RUNTIME_BOOTSTRAP_LOCK_BASE_DELAY_MS = 25;
const JS_RUNTIME_BOOTSTRAP_LOCK_STALE_MS = 5 * 60 * 1000;
function resolveManagedJavaScriptRuntimeBootstrapLockPath(processEnv) {
    return join(managedJavaScriptRuntimeInstallDir(processEnv), '.lock', 'bootstrap.lock');
}
function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
async function acquireManagedJavaScriptRuntimeBootstrapLock(processEnv) {
    const lockPath = resolveManagedJavaScriptRuntimeBootstrapLockPath(processEnv);
    await mkdir(dirname(lockPath), { recursive: true });
    for (let attempt = 0; attempt < JS_RUNTIME_BOOTSTRAP_LOCK_MAX_RETRIES; attempt += 1) {
        try {
            return await open(lockPath, 'wx');
        }
        catch (error) {
            const err = error;
            if (err.code !== 'EEXIST') {
                throw error;
            }
            try {
                const lockStats = await stat(lockPath);
                if (Date.now() - lockStats.mtimeMs > JS_RUNTIME_BOOTSTRAP_LOCK_STALE_MS) {
                    await rm(lockPath, { force: true });
                    continue;
                }
            }
            catch (statError) {
                const statErr = statError;
                if (statErr.code === 'ENOENT') {
                    continue;
                }
                throw statError;
            }
            if (attempt === JS_RUNTIME_BOOTSTRAP_LOCK_MAX_RETRIES - 1) {
                throw new Error('Failed to acquire managed JavaScript runtime bootstrap lock');
            }
            await delay(Math.round(JS_RUNTIME_BOOTSTRAP_LOCK_BASE_DELAY_MS * Math.pow(1.5, attempt)));
        }
    }
    throw new Error('Failed to acquire managed JavaScript runtime bootstrap lock');
}
export async function ensureManagedJavaScriptRuntimeCommand(processEnv = process.env, deps = {}) {
    const rawOverride = readExplicitJavaScriptRuntimeCommand(processEnv);
    if (rawOverride) {
        return resolveExplicitJavaScriptRuntimeCommand(processEnv);
    }
    const existing = resolveExistingManagedJavaScriptRuntimeCommand(processEnv);
    if (existing)
        return existing;
    const installDir = managedJavaScriptRuntimeInstallDir(processEnv);
    const nextDir = join(installDir, 'next');
    const nextBinPath = resolveNextManagedJavaScriptRuntimeBinPath(processEnv);
    const fetchNodeRelease = deps.fetchNodeRuntimeReleaseAsset ?? fetchNodeRuntimeReleaseAsset;
    const downloadAsset = deps.downloadGitHubReleaseAsset ?? downloadGitHubReleaseAsset;
    const extractAsset = deps.extractGitHubReleaseAsset ?? extractGitHubReleaseAsset;
    let lockHandle = null;
    try {
        lockHandle = await acquireManagedJavaScriptRuntimeBootstrapLock(processEnv);
        const managedAfterLock = resolveExistingManagedJavaScriptRuntimeCommand(processEnv);
        if (managedAfterLock) {
            return managedAfterLock;
        }
        const release = await fetchNodeRelease({ processEnv });
        const scratchDir = await createManagedToolScratchDir({
            installDir,
            prefix: 'bootstrap',
        });
        try {
            const archivePath = join(scratchDir, release.name);
            const extractDir = join(scratchDir, 'extract');
            const nextRuntimeDir = join(nextDir, 'runtime');
            const nextNodeBinaryPath = resolveNextManagedNodeBinaryPath(processEnv, release.binaryRelativePath);
            await downloadAsset({
                url: release.url,
                destinationPath: archivePath,
                digest: release.digest,
                userAgent: 'happier-cli',
            });
            await rm(nextDir, { recursive: true, force: true });
            await extractAsset({
                archivePath,
                archiveName: release.name,
                extractDir,
                outputPath: nextRuntimeDir,
            });
            await mkdir(dirname(nextNodeBinaryPath), { recursive: true });
            accessSync(nextNodeBinaryPath, process.platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
            await writeManagedJavaScriptRuntimeWrapper({
                outputPath: nextBinPath,
                runtimeBinaryPath: process.platform === 'win32' ? '%~dp0..\\runtime\\node.exe' : '$(dirname "$0")/../runtime/bin/node',
            });
            await rm(join(installDir, 'current'), { recursive: true, force: true });
            await mkdir(installDir, { recursive: true });
            await rename(nextDir, join(installDir, 'current'));
            return managedJavaScriptRuntimeBinPath(processEnv);
        }
        finally {
            await rm(scratchDir, { recursive: true, force: true });
        }
    }
    catch {
        return null;
    }
    finally {
        if (lockHandle) {
            try {
                await lockHandle.close();
            }
            finally {
                try {
                    await rm(resolveManagedJavaScriptRuntimeBootstrapLockPath(processEnv), { force: true });
                }
                catch {
                    // Ignore best-effort lock cleanup failures after bootstrap completes.
                }
            }
        }
    }
}
//# sourceMappingURL=managedJavaScriptRuntime.js.map