import * as color from '@heroku/heroku-cli-util/color';
import socks from '@heroku/socksv5';
import { ux } from '@oclif/core/ux';
import child from 'child_process';
import cliProgress from 'cli-progress';
import crypto from 'crypto';
import debug from 'debug';
import * as fsp from 'fs/promises';
import os from 'os';
import path from 'path';
import { Client } from 'ssh2';
import stream from 'stream';
import tty from 'tty';
const sshDebug = debug('cli:ps-exec:ssh');
export class HerokuSsh {
    connect(context, addonHost, dynoUser, privateKey, proxyKey, callback) {
        return new Promise((resolve, reject) => {
            const conn = new Client();
            sshDebug('[cli-ssh] created');
            conn.on('ready', () => {
                sshDebug('[cli-ssh] ready');
                ux.action.stop('up');
                if (context.args.length > 0 && !context.args.includes('bash')) {
                    const cmd = this._buildCommand(context.args);
                    sshDebug(`[cli-ssh] command: ${cmd}`);
                    conn.exec(cmd, { pty: true }, (err, stream) => {
                        sshDebug('[cli-ssh] exec');
                        if (err) {
                            sshDebug(`[cli-ssh] err: ${err}`);
                            throw err;
                        }
                        stream.on('close', () => {
                            sshDebug('[cli-ssh] close');
                            conn.end();
                            resolve();
                            if (callback)
                                callback();
                        })
                            .on('data', this._readData(stream))
                            .stderr.on('data', (data) => {
                            process.stderr.write(data);
                        });
                        stream.on('error', reject);
                        process.once('SIGINT', () => conn.end());
                    });
                }
                else {
                    sshDebug('[cli-ssh] bash');
                    conn.shell((err, stream) => {
                        sshDebug('[cli-ssh] shell');
                        if (err) {
                            sshDebug(`[cli-ssh] err: ${err}`);
                            return this._logConnectionError();
                        }
                        stream.on('close', () => {
                            sshDebug('[cli-ssh] close');
                            conn.end();
                            resolve();
                        })
                            .on('data', this._readData(stream))
                            .on('error', (error) => {
                            sshDebug(error);
                            reject(error);
                            ux.error('There was a networking error! Please try connecting again.');
                        });
                        process.once('SIGINT', () => conn.end());
                    });
                }
            }).on('error', err => {
                sshDebug(err);
                reject(err);
                if (err.message === 'Keepalive timeout') {
                    ux.error('Connection to the dyno timed out!');
                }
                else {
                    ux.error('There was an error connecting to the dyno!');
                }
            }).connect({
                ...this._connectionDefaults(proxyKey),
                debug: sshDebug,
                host: addonHost,
                keepaliveCountMax: 3,
                keepaliveInterval: 10000,
                privateKey,
                username: dynoUser,
            });
        });
    }
    scp(addonHost, dynoUser, privateKey, proxyKey, src, dest) {
        return new Promise((resolve, reject) => {
            const conn = new Client();
            conn.on('ready', () => {
                sshDebug('[scp] ready');
                ux.action.stop('up');
                conn.sftp((error, sftp) => {
                    if (error) {
                        return this._logConnectionError();
                    }
                    let bar = null;
                    const progressCallback = function (totalTransferred, _chunk, totalFile) {
                        if (!bar) {
                            bar = new cliProgress.SingleBar({
                                barsize: 25,
                                format: 'Downloading... [{bar}] {percentage}% ETA: {eta}s',
                            });
                            bar.start(totalFile, 0);
                        }
                        bar.update(totalTransferred);
                    };
                    sftp.fastGet(src, dest, {
                        step(totalTransferred, chunk, totalFile) {
                            progressCallback(totalTransferred, chunk, totalFile);
                        },
                    }, error => {
                        if (bar)
                            bar.stop();
                        if (error) {
                            sshDebug(error);
                            ux.error('Could not transfer the file. Make sure the filename is correct.');
                        }
                        conn.end();
                        resolve();
                    });
                });
            }).on('error', err => {
                sshDebug('[scp] error:', err);
                reject(err);
            }).connect({
                ...this._connectionDefaults(proxyKey),
                debug: sshDebug,
                host: addonHost,
                privateKey,
                username: dynoUser,
            });
        });
    }
    socksv5(addonHost, dynoUser, privateKey, proxyKey, callback) {
        const socksPort = 1080;
        socks.createServer((info, accept, deny) => {
            const conn = new Client();
            conn.on('ready', () => {
                conn.forwardOut(info.srcAddr, info.srcPort, info.dstAddr, info.dstPort, (err, stream) => {
                    if (err) {
                        conn.end();
                        return deny();
                    }
                    const clientSocket = accept(true);
                    if (clientSocket) {
                        stream.pipe(clientSocket).pipe(stream).on('close', () => {
                            conn.end();
                        });
                    }
                    else
                        conn.end();
                });
            }).on('error', () => {
                deny();
            }).connect({
                ...this._connectionDefaults(proxyKey),
                host: addonHost,
                privateKey,
                username: dynoUser,
            });
        }).listen(socksPort, '127.0.0.1', () => {
            console.log(`SOCKSv5 proxy server started on port ${color.info(socksPort.toString())}`);
            if (callback)
                callback(socksPort);
        }).useAuth(socks.auth.None()); // eslint-disable-line new-cap
    }
    async ssh(context, addonHost, dynoUser, privateKey, proxyKey) {
        sshDebug('[cli-ssh] native');
        const tmpDir = os.tmpdir();
        const keyPath = path.join(tmpDir, `heroku-exec-key-${Date.now()}-${Math.random().toString(36).slice(2)}`);
        const proxyKeyPath = path.join(tmpDir, `heroku-exec-proxy-key-${Date.now()}-${Math.random().toString(36).slice(2)}`);
        try {
            await fsp.writeFile(keyPath, Buffer.isBuffer(privateKey) ? privateKey : Buffer.from(privateKey), { mode: 0o600 });
            await fsp.writeFile(proxyKeyPath, `[${addonHost}]:80 ${proxyKey}`, { mode: 0o600 });
            let sshCommand = 'ssh '
                + `-o UserKnownHostsFile=${proxyKeyPath} `
                + '-o ServerAliveInterval=10 '
                + '-o ServerAliveCountMax=3 '
                + '-p 80 '
                + `-i ${keyPath} `
                + `${dynoUser}@${addonHost} `;
            if (context.args.length > 0 && !context.args.includes('bash')) {
                sshCommand = `${sshCommand} ${this._buildCommand(context.args)}`;
            }
            try {
                child.execSync(sshCommand, { stdio: ['inherit', 'inherit', 'ignore'] });
            }
            catch (error) {
                if (error.stderr)
                    sshDebug(error.stderr);
                sshDebug(`[cli-ssh] exit: ${error.status}, ${error.message}`);
            }
        }
        finally {
            await fsp.unlink(keyPath).catch(() => { });
            await fsp.unlink(proxyKeyPath).catch(() => { });
        }
    }
    _buildCommand(args) {
        if (args.length === 1) {
            // do not add quotes around arguments if there is only one argument
            // `heroku run "rake test"` should work like `heroku run rake test`
            return args[0];
        }
        let cmd = '';
        for (let arg of args) {
            if (arg.includes(' ') || arg.includes('"')) {
                arg = '"' + arg.replaceAll('"', '\\"') + '"';
            }
            cmd = cmd + ' ' + arg;
        }
        return cmd.trim();
    }
    _connectionDefaults(proxyKey) {
        return {
            hostHash: 'sha256',
            hostVerifier(hashedKey) {
                const hasher = crypto.createHash('sha256');
                hasher.update(Buffer.from(proxyKey.split(' ')[1], 'base64'));
                return hasher.digest('hex') === hashedKey;
            },
            port: 80,
        };
    }
    _logConnectionError() {
        ux.error(`Could not connect to the dyno. Check that the dyno is active by running ${color.command('heroku ps')}`);
    }
    _readData(c) {
        let firstLine = true;
        return (data) => {
            if (firstLine) {
                firstLine = false;
                this._readStdin(c);
            }
            if (data) {
                data = data.toString().replace(' \r', '\n');
                process.stdout.write(data);
            }
        };
    }
    _readStdin(c) {
        const { stdin } = process;
        stdin.setEncoding('utf8');
        if (stdin.unref)
            stdin.unref();
        if (tty.isatty(0)) {
            stdin.setRawMode(true);
            stdin.pipe(c);
            let sigints = [];
            stdin.on('data', (c) => {
                if (c === '\u0003')
                    sigints.push(new Date());
                sigints = sigints.filter(d => d.getTime() > Date.now() - 1000);
                if (sigints.length >= 4) {
                    ux.error('forcing dyno disconnect');
                }
            });
        }
        else {
            stdin.pipe(new stream.Transform({
                flush: done => c.write('\u0004', done),
                objectMode: true,
                transform: (chunk, _, next) => c.write(chunk, next),
            }));
        }
    }
}
