const envPathKey = require('env-path-key');
const fs = require('fs');
const { safeRmSync } = require('fs-remove-compat');
const getFile = require('get-file-compat');
const mkdirp = require('mkdirp-classic');
const os = require('os');
const path = require('path');
const Queue = require('queue-cb');
const moduleRoot = require('module-root-sync');
const root = moduleRoot(__dirname);
// Configuration
const GITHUB_REPO = 'kmalakoff/node-version-use';
const BINARY_VERSION = require(path.join(root, 'package.json')).binaryVersion;
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE);
const hasHomedir = typeof os.homedir === 'function';
function homedir() {
    if (hasHomedir) return os.homedir();
    const home = require('homedir-polyfill');
    return home();
}
// Allow NVU_HOME override for testing
const storagePath = process.env.NVU_HOME || path.join(homedir(), '.nvu');
const hasTmpdir = typeof os.tmpdir === 'function';
function tmpdir() {
    if (hasTmpdir) return os.tmpdir();
    const osShim = require('os-shim');
    return osShim.tmpdir();
}
function removeIfExistsSync(filePath) {
    if (fs.existsSync(filePath)) {
        try {
            fs.unlinkSync(filePath);
        } catch (_e) {
        // ignore cleanup errors
        }
    }
}
/**
 * Move a file out of the way (works even if running on Windows)
 * First tries to unlink; if that fails (Windows locked), rename to .old-timestamp
 */ function moveOutOfWay(filePath) {
    if (!fs.existsSync(filePath)) return;
    // First try to unlink (works on Unix, fails on Windows if running)
    try {
        fs.unlinkSync(filePath);
        return;
    } catch (_e) {
    // Unlink failed (likely Windows locked file), try rename
    }
    // Rename to .old-timestamp as fallback
    const timestamp = Date.now();
    const oldPath = `${filePath}.old-${timestamp}`;
    try {
        fs.renameSync(filePath, oldPath);
    } catch (_e2) {
    // Both unlink and rename failed - will fail on atomic rename instead
    }
}
/**
 * Clean up old .old-* files from previous installs
 */ function cleanupOldFiles(dir) {
    try {
        const entries = fs.readdirSync(dir);
        for (const entry of entries){
            if (entry.includes('.old-')) {
                try {
                    fs.unlinkSync(path.join(dir, entry));
                } catch (_e) {
                // ignore - file may still be in use
                }
            }
        }
    } catch (_e) {
    // ignore if dir doesn't exist
    }
}
/**
 * Get the platform-specific archive base name (without extension)
 */ function getArchiveBaseName() {
    const { platform, arch } = process;
    const platformMap = {
        darwin: 'darwin',
        linux: 'linux',
        win32: 'win32'
    };
    const archMap = {
        x64: 'x64',
        arm64: 'arm64',
        amd64: 'x64'
    };
    const platformName = platformMap[platform];
    const archName = archMap[arch];
    if (!platformName || !archName) return null;
    return `nvu-binary-${platformName}-${archName}`;
}
/**
 * Copy file
 */ function copyFileSync(src, dest) {
    const content = fs.readFileSync(src);
    fs.writeFileSync(dest, content);
}
/**
 * Sync all shims by copying the nvu binary to all other files in the bin directory
 * All shims (node, npm, npx, corepack, eslint, etc.) are copies of the same binary
 */ module.exports.syncAllShims = function syncAllShims(binDir) {
    const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE);
    const ext = isWindows ? '.exe' : '';
    // Source: nvu binary
    const nvuSource = path.join(binDir, `nvu${ext}`);
    if (!fs.existsSync(nvuSource)) return;
    try {
        const entries = fs.readdirSync(binDir);
        for (const name of entries){
            // Skip nvu itself and nvu.json
            if (name === `nvu${ext}` || name === 'nvu.json') continue;
            // On Windows, only process .exe files
            if (isWindows && !name.endsWith('.exe')) continue;
            const shimPath = path.join(binDir, name);
            const stat = fs.statSync(shimPath);
            if (!stat.isFile()) continue;
            // Move existing file out of the way (Windows compatibility)
            moveOutOfWay(shimPath);
            // Copy nvu binary to shim
            copyFileSync(nvuSource, shimPath);
            // Make executable on Unix
            if (!isWindows) {
                fs.chmodSync(shimPath, 0o755);
            }
        }
    } catch (_e) {
    // Ignore errors - shim sync is best effort
    }
};
/**
 * Atomic rename with fallback to copy+delete for cross-device moves
 */ function atomicRename(src, dest, callback) {
    fs.rename(src, dest, (err)=>{
        if (!err) return callback(null);
        // Cross-device link error - fall back to copy + delete
        if (err.code === 'EXDEV') {
            try {
                copyFileSync(src, dest);
                fs.unlinkSync(src);
                callback(null);
            } catch (copyErr) {
                callback(copyErr);
            }
            return;
        }
        callback(err);
    });
}
/**
 * Extract archive to a directory (callback-based)
 */ function extractArchive(archivePath, dest, callback) {
    const Iterator = isWindows ? require('zip-iterator') : require('tar-iterator');
    const stream = isWindows ? fs.createReadStream(archivePath) : fs.createReadStream(archivePath).pipe(require('zlib').createGunzip());
    let iterator = new Iterator(stream);
    // one by one
    const links = [];
    iterator.forEach((entry, callback)=>{
        if (entry.type === 'link') {
            links.unshift(entry);
            callback();
        } else if (entry.type === 'symlink') {
            links.push(entry);
            callback();
        } else entry.create(dest, callback);
    }, {
        callbacks: true,
        concurrency: 1
    }, (_err)=>{
        // create links after directories and files
        const queue = new Queue();
        for(let index = 0; index < links.length; index++){
            const entry = links[index];
            queue.defer(entry.create.bind(entry, dest));
        }
        queue.await((err)=>{
            iterator.destroy();
            iterator = null;
            callback(err);
        });
    });
}
/**
 * Install binaries using atomic rename pattern
 * 1. Extract to temp directory
 * 2. Copy binary to temp files in destination directory
 * 3. Atomic rename temp files to final names
 */ function extractAndInstall(archivePath, destDir, binaryName, callback) {
    const ext = isWindows ? '.exe' : '';
    // Create temp extraction directory
    const tempExtractDir = path.join(tmpdir(), `nvu-extract-${Date.now()}`);
    mkdirp.sync(tempExtractDir);
    extractArchive(archivePath, tempExtractDir, (err)=>{
        if (err) {
            safeRmSync(tempExtractDir);
            callback(err);
            return;
        }
        const extractedPath = path.join(tempExtractDir, binaryName);
        if (!fs.existsSync(extractedPath)) {
            safeRmSync(tempExtractDir);
            callback(new Error(`Extracted binary not found: ${binaryName}. ${archivePath} ${tempExtractDir}`));
            return;
        }
        // Binary names to install
        const binaries = [
            'node',
            'npm',
            'npx',
            'corepack'
        ];
        const timestamp = Date.now();
        let installError = null;
        // Step 1: Copy extracted binary to temp files in destination directory
        // This ensures the temp files are on the same filesystem for atomic rename
        for(let i = 0; i < binaries.length; i++){
            const name = binaries[i];
            const tempDest = path.join(destDir, `${name}.tmp-${timestamp}${ext}`);
            try {
                // Copy to temp file in destination directory
                copyFileSync(extractedPath, tempDest);
                // Set permissions on Unix
                if (!isWindows) fs.chmodSync(tempDest, 0o755);
            } catch (err) {
                installError = err;
                break;
            }
        }
        if (installError) {
            // Clean up any temp files we created
            for(let j = 0; j < binaries.length; j++){
                const tempPath = path.join(destDir, `${binaries[j]}.tmp-${timestamp}${ext}`);
                removeIfExistsSync(tempPath);
            }
            safeRmSync(tempExtractDir);
            callback(installError);
            return;
        }
        // Step 2: Atomic rename temp files to final names
        let renameError = null;
        function doRename(index) {
            if (index >= binaries.length) {
                // All renames complete
                safeRmSync(tempExtractDir);
                callback(renameError);
                return;
            }
            const name = binaries[index];
            const tempDest = path.join(destDir, `${name}.tmp-${timestamp}${ext}`);
            const finalDest = path.join(destDir, `${name}${ext}`);
            // Move existing file out of the way (works even if running on Windows)
            moveOutOfWay(finalDest);
            atomicRename(tempDest, finalDest, (err)=>{
                if (err && !renameError) {
                    renameError = err;
                }
                doRename(index + 1);
            });
        }
        doRename(0);
    });
}
/**
 * Print setup instructions
 */ module.exports.printInstructions = function printInstructions() {
    const _nvuBinPath = path.join(storagePath, 'bin');
    console.log('nvu binaries installed in ~/.nvu/bin/');
    const pathKey = envPathKey();
    const envPath = process.env[pathKey] || '';
    if (envPath.indexOf('.nvu/bin') >= 0) return; // path exists
    // provide instructions for path setup
    console.log('');
    console.log('============================================================');
    console.log('  Global node setup');
    console.log('============================================================');
    console.log('');
    if (isWindows) {
        console.log('  # Edit your PowerShell profile');
        console.log('  # Open with: notepad $PROFILE');
        console.log('  # Add this line:');
        console.log('    $env:PATH = "$HOME\\.nvu\\bin;$env:APPDATA\\npm;$env:PATH"');
        console.log('');
        console.log('  # This adds:');
        console.log('  #   ~/.nvu/bin     - node/npm version switching shims');
        console.log('  #   %APPDATA%/npm  - globally installed npm packages (like nvu)');
    } else {
        console.log('  # For bash (~/.bashrc):');
        console.log('   echo \'export PATH="$HOME/.nvu/bin:$PATH"\' >> ~/.bashrc');
        console.log('');
        console.log('  # For zsh (~/.zshrc):');
        console.log('   echo \'export PATH="$HOME/.nvu/bin:$PATH"\' >> ~/.zshrc');
        console.log('');
        console.log('  # For fish (~/.config/fish/config.fish):');
        console.log("   echo 'set -gx PATH $HOME/.nvu/bin $PATH' >> ~/.config/fish/config.fish");
    }
    console.log('');
    console.log('Then restart your terminal or source your shell profile.');
    console.log('');
    console.log("Without this, 'nvu 18 npm test' still works - you just won't have");
    console.log("transparent 'node' command override.");
    console.log('============================================================');
};
/**
 * Main installation function
 */ module.exports.installBinaries = function installBinaries(options, callback) {
    const archiveBaseName = getArchiveBaseName();
    if (!archiveBaseName) {
        callback(new Error('Unsupported platform/architecture for binary.'));
        return;
    }
    const extractedBinaryName = `${archiveBaseName}${isWindows ? '.exe' : ''}`;
    const binDir = path.join(storagePath, 'bin');
    const nvuJsonPath = path.join(binDir, 'nvu.json');
    // check if we need to upgrade
    if (!options.force) {
        try {
            // already installed - read nvu.json
            const nvuJson = JSON.parse(fs.readFileSync(nvuJsonPath, 'utf8'));
            if (nvuJson.binaryVersion === BINARY_VERSION) {
                callback(null, false);
                return;
            }
        } catch (_err) {}
    }
    // Create directories
    mkdirp.sync(storagePath);
    mkdirp.sync(binDir);
    mkdirp.sync(path.join(storagePath, 'cache'));
    // Clean up old .old-* files from previous installs
    cleanupOldFiles(binDir);
    const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/binary-v${BINARY_VERSION}/${archiveBaseName}${isWindows ? '.zip' : '.tar.gz'}`;
    const cachePath = path.join(storagePath, 'cache', `${archiveBaseName}${isWindows ? '.zip' : '.tar.gz'}`);
    // Check cache first
    if (fs.existsSync(cachePath)) {
        console.log('Using cached binary...');
        // Use cached file
        extractAndInstall(cachePath, binDir, extractedBinaryName, (err)=>{
            if (err) return callback(err);
            // save binary version for upgrade checks
            fs.writeFileSync(nvuJsonPath, JSON.stringify({
                binaryVersion: BINARY_VERSION
            }, null, 2), 'utf8');
            console.log('Binary installed successfully!');
            callback(null, true);
        });
        return;
    }
    // Download to temp file
    console.log(`Downloading binary for ${process.platform}-${process.arch}...`);
    const tempPath = path.join(tmpdir(), `nvu-binary-${Date.now()}${isWindows ? '.zip' : '.tar.gz'}`);
    getFile(downloadUrl, tempPath, (err)=>{
        if (err) {
            removeIfExistsSync(tempPath);
            callback(new Error(`No prebuilt binary available for ${process.platform}-${process.arch}. Download: ${downloadUrl}. Error: ${err.message}`));
            return;
        }
        // Copy to cache for future use
        try {
            copyFileSync(tempPath, cachePath);
        } catch (_e) {
        // Cache write failed, continue anyway
        }
        extractAndInstall(tempPath, binDir, extractedBinaryName, (err)=>{
            removeIfExistsSync(tempPath);
            if (err) return callback(err);
            // save binary version for upgrade checks
            fs.writeFileSync(nvuJsonPath, JSON.stringify({
                binaryVersion: BINARY_VERSION
            }, null, 2), 'utf8');
            console.log('Binary installed successfully!');
            callback(null, true);
        });
    });
};
