#!/usr/bin/env node

import assert from 'node:assert';
import async from 'async';
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { program } from 'commander';
import safe from 'safetydance';
import { Client as SshClient } from 'ssh2';
import util from 'node:util';

function exit(error) {
    if (error instanceof Error) console.log(error.message);
    else if (error) console.error(util.format.apply(null, Array.prototype.slice.call(arguments)));

    process.exit(error ? 1 : 0);
}

function findSshKey(key) {
    assert.strictEqual(typeof key, 'string');

    let test = key;

    // remove .pub in case the user passed in the public key
    if (path.extname(test) === '.pub') test = test.slice(0, -4);

    if (fs.existsSync(test)) return test;

    test = path.join(os.homedir(), '.ssh', key);
    if (fs.existsSync(test)) return test;

    test = path.join(os.homedir(), '.ssh', 'id_ed25519_' + key);
    if (fs.existsSync(test)) return test;

    test = path.join(os.homedir(), '.ssh', 'id_rsa_' + key);
    if (fs.existsSync(test)) return test;

    test = path.join(os.homedir(), '.ssh', 'id_rsa_caas_' + key);
    if (fs.existsSync(test)) return test;

    return null;
}

function sshExec(host, port, sshKey, username, cmds, callback) {
    const passphrase = process.env.SSH_PASSPHRASE || undefined;
    console.log(`connecting ${username}@${host}:${port} with ${sshKey} (passphrase: ${passphrase || ''})`);

    const sshClient = new SshClient();
    sshClient.connect({
        host,
        port,
        username,
        privateKey: fs.readFileSync(sshKey),
        passphrase,
        // debug: (s) => { console.log(s); } // https://github.com/mscdex/ssh2/issues/989
    });

    sshClient.on('ready', function () {
        console.log('connected');

        async.eachSeries(cmds, function (cmd, iteratorDone) {
            const command = cmd.cmd;

            console.log(`Executing: ${command}`);

            sshClient.exec(command, function(err, stream) {
                if (err) return callback(err);

                if (cmd.stdin) cmd.stdin.pipe(stream);
                stream.pipe(process.stdout);
                stream.on('close', function () {
                    iteratorDone();
                }).stderr.pipe(process.stderr);
            });
        }, function seriesDone(error) {
            if (error) return callback(error);

            sshClient.end();
        });
    });
    sshClient.on('end', function () {
        console.log('\ndisconnected');
        callback();
    });
    sshClient.on('error', function (error) {
        console.log(`SSH client error: ${error.message}`);
        callback(error);
    });
    sshClient.on('exit', function (exitCode) {
        callback(exitCode === 0 ? null : new Error('ssh exec returned + ' + exitCode));
    });
}

function hotfix(options) {
    assert.strictEqual(typeof options, 'object');

    if (!options.cloudron) exit('--cloudron is required');
    if (!options.release) exit('--release <version> is required');
    if (!options.sshKey) exit('--ssh-key is required');

    let ip;
    if (net.isIP(options.cloudron)) {
        ip = options.cloudron;
    } else {
        const out = safe.child_process.execSync(`host -t A ${options.cloudron}`, { encoding: 'utf8' });
        if (!out) exit(`Could not resolve ${options.cloudron}`);
        ip = out.trim().split(' ')[3];
    }

    const sshKey = findSshKey(options.sshKey);
    if (!sshKey) exit('Unable to find SSH key');

    const version = options.release;
    const sshPort = options.sshPort || 22;
    const sshUser = options.sshUser || 'root';

    const tarballScript = path.join(import.meta.dirname, 'create-release-tarball');
    if (!fs.existsSync(tarballScript)) exit('Could not find create-release-taball script. Run this command from the box repo checkout directory.');

    const tarball = `${os.tmpdir()}/boxtarball-${version}.tar.gz`;
    try {
        execSync(`${tarballScript} --output ${tarball} --version ${version}`, { stdio: [ null, process.stdout, process.stderr ] });
    } catch (e) {
        console.log('Unable to create version tarball.');
        process.exit(1);
    }

    const cmds = [
        { cmd: 'sudo rm -rf /tmp/box-src-hotfix' },
        { cmd: 'sudo mkdir -p /tmp/box-src-hotfix' },
        { cmd: 'sudo tar zxf - -C /tmp/box-src-hotfix', stdin: fs.createReadStream(tarball) },
        { cmd: 'sudo dd of=/tmp/remote_hotfix.js', stdin: fs.createReadStream(path.resolve(import.meta.dirname, './remote_hotfix.js')) },
        { cmd: 'sudo HOME=/home/yellowtent BOX_ENV=cloudron node /tmp/remote_hotfix.js' },
        { cmd: 'sudo rm -rf /tmp/box-src-hotfix' }
    ];

    sshExec(ip, sshPort, sshKey, sshUser, cmds, function (error) {
        if (error) exit(error);

        console.log('Done patching');
    });
}

// main commander setup
program.description('Hotfix Cloudron with latest code')
    .version('1.0.0')
    .option('--cloudron <domain/ip>', 'Cloudron domain or IP')
    .option('--release <version>', 'Cloudron release version')
    .option('--ssh-port <ssh port>', 'SSH port')
    .option('--ssh-key <ssh key>', 'SSH Key file path')
    .option('--ssh-user <ssh user>', 'SSH username');

program.parse();

hotfix(program.opts());
