import { spawn, spawnSync } from 'node:child_process';
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createManualUpdateInfo, isPermissionUpdateFailure } from '../../shared/manualUpdate.js';
const updateLockStaleMs = 10 * 60 * 1000;
const packageUpdateTimeoutMs = 5 * 60 * 1000;
const defaultUpdateHelperPath = '/usr/local/sbin/nterminal-update';
function appDir() {
    return process.env.NTERMINAL_APP_DIR ? path.resolve(process.env.NTERMINAL_APP_DIR) : process.cwd();
}
function npm(args, cwd, timeout = 60_000) {
    const result = spawnSync(process.env.NTERMINAL_NPM_BIN || 'npm', args, { cwd, encoding: 'utf8', timeout });
    return {
        ok: result.status === 0,
        stdout: (result.stdout ?? '').trim(),
        stderr: (result.stderr ?? '').trim()
    };
}
function updateHelperPath() {
    return process.env.NTERMINAL_UPDATE_HELPER_PATH?.trim() || defaultUpdateHelperPath;
}
function sudoBin() {
    return process.env.NTERMINAL_SUDO_BIN?.trim() || 'sudo';
}
function runUpdateHelper(action, cwd, timeout = 60_000) {
    const result = spawnSync(sudoBin(), ['-n', updateHelperPath(), action], { cwd, encoding: 'utf8', timeout });
    return {
        ok: result.status === 0,
        stdout: (result.stdout ?? '').trim(),
        stderr: (result.stderr ?? '').trim()
    };
}
function helperOutput(result) {
    return result.stderr || result.stdout || 'privileged update helper failed';
}
function isHelperPermissionFailure(output) {
    return (isPermissionUpdateFailure(output) ||
        /sudo|password is required|a password is required|not in the sudoers file|not allowed|authentication|privileged update helper/i.test(output));
}
function readPackageName(cwd) {
    try {
        const pkg = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
        return typeof pkg.name === 'string' && pkg.name.trim() ? pkg.name.trim() : 'nterminal';
    }
    catch {
        return 'nterminal';
    }
}
function readPackageVersion(cwd) {
    try {
        const pkg = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
        return typeof pkg.version === 'string' ? pkg.version : 'unknown';
    }
    catch {
        return 'unknown';
    }
}
// Frozen at process start. It only changes once the server actually restarts
// into a newly installed package; package.json on disk may update earlier.
const startupAppDir = appDir();
const startupVersion = readPackageVersion(startupAppDir);
// Cross-process guard (lockfile) so repeated clicks, multiple tabs, or a still-running update
// don't spawn overlapping installs. Auto-expires so a crashed/hung update doesn't wedge updates.
function resolveRuntimePath(cwd, value) {
    return path.isAbsolute(value) ? value : path.resolve(cwd, value);
}
function runtimeStateDir(cwd) {
    const configured = process.env.NTERMINAL_STATE_PATH?.trim();
    return configured ? path.dirname(resolveRuntimePath(cwd, configured)) : path.join(os.homedir(), '.nterminal');
}
function lockPath(cwd) {
    return path.join(runtimeStateDir(cwd), 'update.lock');
}
function updateLocked(cwd) {
    const file = lockPath(cwd);
    if (!existsSync(file)) {
        return false;
    }
    try {
        const stamped = Number(readFileSync(file, 'utf8').trim());
        return Number.isFinite(stamped) && Date.now() - stamped < updateLockStaleMs;
    }
    catch {
        return false;
    }
}
function acquireUpdateLock(cwd) {
    mkdirSync(runtimeStateDir(cwd), { recursive: true });
    const file = lockPath(cwd);
    for (;;) {
        try {
            const fd = openSync(file, 'wx');
            try {
                writeFileSync(fd, String(Date.now()));
            }
            finally {
                closeSync(fd);
            }
            return true;
        }
        catch (error) {
            if (nodeErrorCode(error) !== 'EEXIST') {
                throw error;
            }
            if (updateLocked(cwd)) {
                return false;
            }
            rmSync(file, { force: true });
        }
    }
}
function releaseUpdateLock(cwd) {
    rmSync(lockPath(cwd), { force: true });
}
export function getVersionInfo(cwd = appDir(), manualUpdate = null) {
    return {
        version: readPackageVersion(cwd),
        runningVersion: runningPackageVersion(cwd),
        manualUpdate
    };
}
export function checkPackageUpdate(cwd = appDir()) {
    const currentVersion = runningPackageVersion(cwd);
    const packageName = readPackageName(cwd);
    const latest = npm(['view', `${packageName}@latest`, 'version', '--prefer-online'], cwd);
    if (!latest.ok) {
        return {
            currentVersion,
            latestVersion: null,
            updateAvailable: null,
            error: latest.stderr || latest.stdout || 'npm_view_failed'
        };
    }
    const latestVersion = latest.stdout.split(/\r?\n/).at(-1)?.trim() || null;
    return {
        currentVersion,
        latestVersion,
        updateAvailable: latestVersion ? latestVersion !== currentVersion : null
    };
}
export function runUpdate(cwd = appDir()) {
    if (updateLocked(cwd)) {
        return { ok: false, changed: false, message: 'An update is already in progress on this server.', restarting: false };
    }
    const check = checkPackageUpdate(cwd);
    if (check.error) {
        return { ok: false, changed: false, message: check.error, restarting: false };
    }
    if (!check.updateAvailable) {
        return { ok: true, changed: false, message: 'Already up to date.', restarting: false };
    }
    if (!acquireUpdateLock(cwd)) {
        return { ok: false, changed: false, message: 'An update is already in progress on this server.', restarting: false };
    }
    const packageName = readPackageName(cwd);
    const installedVersion = readPackageVersion(cwd);
    if (!check.latestVersion || installedVersion !== check.latestVersion) {
        const install = npm(['install', '-g', `${packageName}@latest`, '--prefer-online'], cwd, packageUpdateTimeoutMs);
        if (!install.ok) {
            const output = install.stderr || install.stdout || 'npm install -g failed';
            const helperCheck = isPermissionUpdateFailure(output) ? runUpdateHelper('check', cwd) : null;
            if (helperCheck?.ok) {
                const helperInstall = runUpdateHelper('install', cwd, packageUpdateTimeoutMs);
                if (!helperInstall.ok) {
                    releaseUpdateLock(cwd);
                    const elevatedOutput = helperOutput(helperInstall);
                    const manualUpdate = isHelperPermissionFailure(elevatedOutput) ? createManualUpdateInfo(packageName) : null;
                    return {
                        ok: false,
                        changed: false,
                        message: manualUpdate?.message ?? elevatedOutput,
                        restarting: false,
                        ...(manualUpdate ? { manualUpdate } : {})
                    };
                }
            }
            else {
                releaseUpdateLock(cwd);
                const manualUpdate = isPermissionUpdateFailure(output) || (helperCheck && isHelperPermissionFailure(helperOutput(helperCheck))) ? createManualUpdateInfo(packageName) : null;
                return {
                    ok: false,
                    changed: false,
                    message: manualUpdate?.message ?? output,
                    restarting: false,
                    ...(manualUpdate ? { manualUpdate } : {})
                };
            }
        }
    }
    const restart = spawn('sh', ['-c', 'sleep 0.2; exec bash "$1" restart-after-update', 'nterminal-restart', path.join(cwd, 'scripts/nterminalctl')], {
        cwd,
        detached: true,
        env: { ...process.env, NTERMINAL_UPDATE_LOCK_PATH: lockPath(cwd) },
        stdio: 'ignore'
    });
    restart.unref();
    return {
        ok: true,
        changed: true,
        message: `Updated ${packageName} ${check.currentVersion ?? ''} -> ${check.latestVersion ?? 'latest'}; restarting.`,
        restarting: true
    };
}
function runningPackageVersion(cwd) {
    return path.resolve(cwd) === startupAppDir ? startupVersion : readPackageVersion(cwd);
}
function nodeErrorCode(error) {
    return typeof error === 'object' && error !== null && 'code' in error ? String(error.code) : undefined;
}
//# sourceMappingURL=packageUpdate.js.map