import { Client, ClientChannel } from 'ssh2';

import { Deferred, getDeferred } from '@f2/defer';
import { beginEventEmitter } from '@f2/event';
import { Task, TaskObserver } from '@f2/task';


export interface SshShell {

    on(event: 'error', cb: (err: Error) => void): this;

    once(event: 'error', cb: (err: Error) => void): this;

    emit(event: 'error', err: Error): boolean;
}

const EndRegex = /[a-z0-9_-]+@[\.a-z0-9_-]+\:.*[\$\#] $/gi;

interface ShellTaskStep {
    'data': Buffer;
}

type SshShellCheckEndCmdFn = (buffer: Buffer) => boolean;

interface SshShellCommand {
    cmd: string;
    observer?: TaskObserver<ShellTaskStep>;
    data?: Buffer[];
    deferred: Deferred<any>;
    checkEndFn?: SshShellCheckEndCmdFn;
}

export interface SshShellCmdOptions {
    checkEndFn?: SshShellCheckEndCmdFn;
}

export interface SshShellOptions {
    firstCmd?: string;
    checkEndCmdFn?: SshShellCheckEndCmdFn;
}

export class SshShell {

    private _channel?: ClientChannel;
    private _deferredReady? = getDeferred<void>();
    private _readyToRunCmd = false;
    private _inProgressCmd?: SshShellCommand;
    private _cmdQueue: SshShellCommand[] = [];
    private _checkEndCmdFn?: SshShellCheckEndCmdFn;

    constructor(client: Client, options?: SshShellOptions) {

        beginEventEmitter(this);

        client.shell((err, channel) => {

            if(err) {
                return this.emit('error', err);
            }

            this._channel = channel;

            const onData = (chunk: Buffer): void => {

                //console.log('recv - ' + chunk.toString());
                if(this._inProgressCmd) {

                    this._onCmdData(this._inProgressCmd, chunk);

                    return;
                }

                if(this._checkDefaultTermReturn(chunk)) {

                    this._next();
                }
            }

            channel.stdout.on('data', (chunk: Buffer) => {

                //console.log('recv start - ' + chunk.toString());
                if(!this._checkDefaultTermReturn(chunk)) {

                    return;
                }

                const deferred = this._deferredReady!;

                if(deferred) {

                    this._deferredReady = undefined;
                    deferred.resolve();
                }

                channel.stdout.removeAllListeners('data');
                channel.stdout.on('data', onData);

                this._next();
            });
        });

        if(options) {

            this._checkEndCmdFn = options.checkEndCmdFn;

            if(options.firstCmd) {
                this.runTaskCmd(options.firstCmd);
            }
        }
    }

    private _checkDefaultTermReturn(data: Buffer): boolean {

        const dataStr = data.toString();

        const lines = dataStr.split('\r');
        
        const lastLine = lines[lines.length - 1];

        return lastLine.match(EndRegex) !== null;
    }

    private _next(): void {

        const cmd = this._inProgressCmd;

        if(cmd){

            if(cmd.data) {
                cmd.deferred.resolve(Buffer.concat(cmd.data).toString());
            } else {
                cmd.deferred.resolve(undefined);
            }

            this._inProgressCmd = undefined;
        }

        this._readyToRunCmd = true;
        this._runNext();
    }

    private _onCmdData(cmd: SshShellCommand, data: Buffer): void {

        if(cmd.observer) {
            cmd.observer.progress('data', data);
        } else {
            cmd.data!.push(data);
        }

        if(cmd.checkEndFn) {

            if(cmd.checkEndFn(data)) {
                this._next();
            }

        } else if(this._checkDefaultTermReturn(data)) {

            this._next();
            
        } else if(this._checkEndCmdFn) {

            if(this._checkEndCmdFn(data)) {
                this._next();
            }
        }
    }

    private _runNext(): void {

        if(!this._readyToRunCmd || this._inProgressCmd) {
            return;
        }

        if(this._cmdQueue.length === 0) {
            return;
        }

        const cmd = this._cmdQueue.shift()!;

        this._inProgressCmd = cmd;

        this.write(cmd.cmd);
    }

    public runCmd(cmd: string): Promise<string> {

        const deferred = getDeferred<string>();

        this._cmdQueue.push({
            cmd,
            deferred,
            data: []
        });

        this._runNext();

        return deferred.promise;
    }

    public runTaskCmd(cmd: string): Task<void, ShellTaskStep> {

        const observer = new TaskObserver<ShellTaskStep>();
        const deferred = getDeferred<void>();
        const task = Task.from<void, ShellTaskStep>(deferred.promise, observer);

        this._cmdQueue.push({
            cmd,
            observer,
            deferred
        });

        this._runNext();

        return task;
    }

    public async waitReady(): Promise<void> {

        if(this._deferredReady) {
            return this._deferredReady.promise;
        }
    }

    public write(cmd: string): void {

        //console.log('send - ' + cmd);
        this._channel?.write(cmd + '\n');
    }
}