var net = require('net');
var EventEmitter = require('events').EventEmitter;
var listenerCount = EventEmitter.listenerCount;
var inherits = require('util').inherits;

var ssh2_streams = require('ssh2-streams');
var parseKey = ssh2_streams.utils.parseKey;
var SSH2Stream = ssh2_streams.SSH2Stream;
var SFTPStream = ssh2_streams.SFTPStream;
var consts = ssh2_streams.constants;
var DISCONNECT_REASON = consts.DISCONNECT_REASON;
var CHANNEL_OPEN_FAILURE = consts.CHANNEL_OPEN_FAILURE;
var ALGORITHMS = consts.ALGORITHMS;

var Channel = require('./Channel');
var KeepaliveManager = require('./keepalivemgr');
var writeUInt32BE = require('./buffer-helpers').writeUInt32BE;

var MAX_CHANNEL = Math.pow(2, 32) - 1;
var MAX_PENDING_AUTHS = 10;

var kaMgr;

function Server(cfg, listener) {
  if (!(this instanceof Server))
    return new Server(cfg, listener);

  var hostKeys = {
    'ssh-rsa': null,
    'ssh-dss': null,
    'ssh-ed25519': null,
    'ecdsa-sha2-nistp256': null,
    'ecdsa-sha2-nistp384': null,
    'ecdsa-sha2-nistp521': null
  };

  var hostKeys_ = cfg.hostKeys;
  if (!Array.isArray(hostKeys_))
    throw new Error('hostKeys must be an array');

  var i;
  for (i = 0; i < hostKeys_.length; ++i) {
    var privateKey;
    if (Buffer.isBuffer(hostKeys_[i]) || typeof hostKeys_[i] === 'string')
      privateKey = parseKey(hostKeys_[i]);
    else
      privateKey = parseKey(hostKeys_[i].key, hostKeys_[i].passphrase);
    if (privateKey instanceof Error)
      throw new Error('Cannot parse privateKey: ' + privateKey.message);
    if (Array.isArray(privateKey))
      privateKey = privateKey[0]; // OpenSSH's newer format only stores 1 key for now
    if (privateKey.getPrivatePEM() === null)
      throw new Error('privateKey value contains an invalid private key');
    if (hostKeys[privateKey.type])
      continue;
    hostKeys[privateKey.type] = privateKey;
  }

  var algorithms = {
    kex: undefined,
    kexBuf: undefined,
    cipher: undefined,
    cipherBuf: undefined,
    serverHostKey: undefined,
    serverHostKeyBuf: undefined,
    hmac: undefined,
    hmacBuf: undefined,
    compress: undefined,
    compressBuf: undefined
  };
  if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) {
    var algosSupported;
    var algoList;

    algoList = cfg.algorithms.kex;
    if (Array.isArray(algoList) && algoList.length > 0) {
      algosSupported = ALGORITHMS.SUPPORTED_KEX;
      for (i = 0; i < algoList.length; ++i) {
        if (algosSupported.indexOf(algoList[i]) === -1)
          throw new Error('Unsupported key exchange algorithm: ' + algoList[i]);
      }
      algorithms.kex = algoList;
    }

    algoList = cfg.algorithms.cipher;
    if (Array.isArray(algoList) && algoList.length > 0) {
      algosSupported = ALGORITHMS.SUPPORTED_CIPHER;
      for (i = 0; i < algoList.length; ++i) {
        if (algosSupported.indexOf(algoList[i]) === -1)
          throw new Error('Unsupported cipher algorithm: ' + algoList[i]);
      }
      algorithms.cipher = algoList;
    }

    algoList = cfg.algorithms.serverHostKey;
    var copied = false;
    if (Array.isArray(algoList) && algoList.length > 0) {
      algosSupported = ALGORITHMS.SUPPORTED_SERVER_HOST_KEY;
      for (i = algoList.length - 1; i >= 0; --i) {
        if (algosSupported.indexOf(algoList[i]) === -1) {
          throw new Error('Unsupported server host key algorithm: '
                           + algoList[i]);
        }
        if (!hostKeys[algoList[i]]) {
          // Silently discard for now
          if (!copied) {
            algoList = algoList.slice();
            copied = true;
          }
          algoList.splice(i, 1);
        }
      }
      if (algoList.length > 0)
        algorithms.serverHostKey = algoList;
    }

    algoList = cfg.algorithms.hmac;
    if (Array.isArray(algoList) && algoList.length > 0) {
      algosSupported = ALGORITHMS.SUPPORTED_HMAC;
      for (i = 0; i < algoList.length; ++i) {
        if (algosSupported.indexOf(algoList[i]) === -1)
          throw new Error('Unsupported HMAC algorithm: ' + algoList[i]);
      }
      algorithms.hmac = algoList;
    }

    algoList = cfg.algorithms.compress;
    if (Array.isArray(algoList) && algoList.length > 0) {
      algosSupported = ALGORITHMS.SUPPORTED_COMPRESS;
      for (i = 0; i < algoList.length; ++i) {
        if (algosSupported.indexOf(algoList[i]) === -1)
          throw new Error('Unsupported compression algorithm: ' + algoList[i]);
      }
      algorithms.compress = algoList;
    }
  }

  // Make sure we at least have some kind of valid list of support key
  // formats
  if (algorithms.serverHostKey === undefined) {
    var hostKeyAlgos = Object.keys(hostKeys);
    for (i = hostKeyAlgos.length - 1; i >= 0; --i) {
      if (!hostKeys[hostKeyAlgos[i]])
        hostKeyAlgos.splice(i, 1);
    }
    algorithms.serverHostKey = hostKeyAlgos;
  }

  if (!kaMgr
      && Server.KEEPALIVE_INTERVAL > 0
      && Server.KEEPALIVE_CLIENT_INTERVAL > 0
      && Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0) {
    kaMgr = new KeepaliveManager(Server.KEEPALIVE_INTERVAL,
                                 Server.KEEPALIVE_CLIENT_INTERVAL,
                                 Server.KEEPALIVE_CLIENT_COUNT_MAX);
  }

  var self = this;

  EventEmitter.call(this);

  if (typeof listener === 'function')
    self.on('connection', listener);

  var streamcfg = {
    algorithms: algorithms,
    hostKeys: hostKeys,
    server: true
  };
  var keys;
  var len;
  for (i = 0, keys = Object.keys(cfg), len = keys.length; i < len; ++i) {
    var key = keys[i];
    if (key === 'privateKey'
        || key === 'publicKey'
        || key === 'passphrase'
        || key === 'algorithms'
        || key === 'hostKeys'
        || key === 'server') {
      continue;
    }
    streamcfg[key] = cfg[key];
  }

  if (typeof streamcfg.debug === 'function') {
    var oldDebug = streamcfg.debug;
    var cfgKeys = Object.keys(streamcfg);
  }

  this._srv = new net.Server(function(socket) {
    if (self._connections >= self.maxConnections) {
      socket.destroy();
      return;
    }
    ++self._connections;
    socket.once('close', function(had_err) {
      --self._connections;

      // since joyent/node#993bb93e0a, we have to "read past EOF" in order to
      // get an `end` event on streams. thankfully adding this does not
      // negatively affect node versions pre-joyent/node#993bb93e0a.
      sshstream.read();
    }).on('error', function(err) {
      sshstream.reset();
      sshstream.emit('error', err);
    });

    var conncfg = streamcfg;

    // prepend debug output with a unique identifier in case there are multiple
    // clients connected at the same time
    if (oldDebug) {
      conncfg = {};
      for (var i = 0, key; i < cfgKeys.length; ++i) {
        key = cfgKeys[i];
        conncfg[key] = streamcfg[key];
      }
      var debugPrefix = '[' + process.hrtime().join('.') + '] ';
      conncfg.debug = function(msg) {
        oldDebug(debugPrefix + msg);
      };
    }

    var sshstream = new SSH2Stream(conncfg);
    var client = new Client(sshstream, socket);

    socket.pipe(sshstream).pipe(socket);

    // silence pre-header errors
    function onClientPreHeaderError(err) {}
    client.on('error', onClientPreHeaderError);

    sshstream.once('header', function(header) {
      if (sshstream._readableState.ended) {
        // already disconnected internally in SSH2Stream due to incompatible
        // protocol version
        return;
      } else if (!listenerCount(self, 'connection')) {
        // auto reject
        return sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION);
      }

      client.removeListener('error', onClientPreHeaderError);

      self.emit('connection',
                client,
                { ip: socket.remoteAddress,
                  family: socket.remoteFamily,
                  port: socket.remotePort,
                  header: header });
    });
  }).on('error', function(err) {
    self.emit('error', err);
  }).on('listening', function() {
    self.emit('listening');
  }).on('close', function() {
    self.emit('close');
  });
  this._connections = 0;
  this.maxConnections = Infinity;
}
inherits(Server, EventEmitter);

Server.prototype.listen = function() {
  this._srv.listen.apply(this._srv, arguments);
  return this;
};

Server.prototype.address = function() {
  return this._srv.address();
};

Server.prototype.getConnections = function(cb) {
  this._srv.getConnections(cb);
};

Server.prototype.close = function(cb) {
  this._srv.close(cb);
  return this;
};

Server.prototype.ref = function() {
  this._srv.ref();
};

Server.prototype.unref = function() {
  this._srv.unref();
};


function Client(stream, socket) {
  EventEmitter.call(this);

  var self = this;

  this._sshstream = stream;
  var channels = this._channels = {};
  this._curChan = -1;
  this._sock = socket;
  this.noMoreSessions = false;
  this.authenticated = false;

  stream.on('end', function() {
    socket.resume();
    self.emit('end');
  }).on('close', function(hasErr) {
    self.emit('close', hasErr);
  }).on('error', function(err) {
    self.emit('error', err);
  }).on('drain', function() {
    self.emit('drain');
  }).on('continue', function() {
    self.emit('continue');
  });

  var exchanges = 0;
  var acceptedAuthSvc = false;
  var pendingAuths = [];
  var authCtx;

  // begin service/auth-related ================================================
  stream.on('SERVICE_REQUEST', function(service) {
    if (exchanges === 0
        || acceptedAuthSvc
        || self.authenticated
        || service !== 'ssh-userauth')
      return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);

    acceptedAuthSvc = true;
    stream.serviceAccept(service);
  }).on('USERAUTH_REQUEST', onUSERAUTH_REQUEST);
  function onUSERAUTH_REQUEST(username, service, method, methodData) {
    if (exchanges === 0
        || (authCtx
            && (authCtx.username !== username || authCtx.service !== service))
          // TODO: support hostbased auth
        || (method !== 'password'
            && method !== 'publickey'
            && method !== 'hostbased'
            && method !== 'keyboard-interactive'
            && method !== 'none')
        || pendingAuths.length === MAX_PENDING_AUTHS)
      return stream.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR);
    else if (service !== 'ssh-connection')
      return stream.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);

    // XXX: this really shouldn't be reaching into private state ...
    stream._state.authMethod = method;

    var ctx;
    if (method === 'keyboard-interactive') {
      ctx = new KeyboardAuthContext(stream, username, service, method,
                                    methodData, onAuthDecide);
    } else if (method === 'publickey') {
      ctx = new PKAuthContext(stream, username, service, method, methodData,
                              onAuthDecide);
    } else if (method === 'hostbased') {
      ctx = new HostbasedAuthContext(stream, username, service, method,
                                     methodData, onAuthDecide);
    } else if (method === 'password') {
      ctx = new PwdAuthContext(stream, username, service, method, methodData,
                               onAuthDecide);
    } else if (method === 'none')
      ctx = new AuthContext(stream, username, service, method, onAuthDecide);

    if (authCtx) {
      if (!authCtx._initialResponse)
        return pendingAuths.push(ctx);
      else if (authCtx._multistep && !this._finalResponse) {
        // RFC 4252 says to silently abort the current auth request if a new
        // auth request comes in before the final response from an auth method
        // that requires additional request/response exchanges -- this means
        // keyboard-interactive for now ...
        authCtx._cleanup && authCtx._cleanup();
        authCtx.emit('abort');
      }
    }

    authCtx = ctx;

    if (listenerCount(self, 'authentication'))
      self.emit('authentication', authCtx);
    else
      authCtx.reject();
  }
  function onAuthDecide(ctx, allowed, methodsLeft, isPartial) {
    if (authCtx === ctx && !self.authenticated) {
      if (allowed) {
        stream.removeListener('USERAUTH_REQUEST', onUSERAUTH_REQUEST);
        authCtx = undefined;
        self.authenticated = true;
        stream.authSuccess();
        pendingAuths = [];
        self.emit('ready');
      } else {
        stream.authFailure(methodsLeft, isPartial);
        if (pendingAuths.length) {
          authCtx = pendingAuths.pop();
          if (listenerCount(self, 'authentication'))
            self.emit('authentication', authCtx);
          else
            authCtx.reject();
        }
      }
    }
  }
  // end service/auth-related ==================================================

  var unsentGlobalRequestsReplies = [];

  function sendReplies() {
    var reply;
    while (unsentGlobalRequestsReplies.length > 0
           && unsentGlobalRequestsReplies[0].type) {
      reply = unsentGlobalRequestsReplies.shift();
      if (reply.type === 'SUCCESS')
        stream.requestSuccess(reply.buf);
      if (reply.type === 'FAILURE')
        stream.requestFailure();
    }
  }

  stream.on('GLOBAL_REQUEST', function(name, wantReply, data) {
    var reply = {
      type: null,
      buf: null
    };

    function setReply(type, buf) {
      reply.type = type;
      reply.buf = buf;
      sendReplies();
    }

    if (wantReply)
      unsentGlobalRequestsReplies.push(reply);

    if ((name === 'tcpip-forward'
         || name === 'cancel-tcpip-forward'
         || name === 'no-more-sessions@openssh.com'
         || name === 'streamlocal-forward@openssh.com'
         || name === 'cancel-streamlocal-forward@openssh.com')
        && listenerCount(self, 'request')
        && self.authenticated) {
      var accept;
      var reject;

      if (wantReply) {
        var replied = false;
        accept = function(chosenPort) {
          if (replied)
            return;
          replied = true;
          var bufPort;
          if (name === 'tcpip-forward'
              && data.bindPort === 0
              && typeof chosenPort === 'number') {
            bufPort = Buffer.allocUnsafe(4);
            writeUInt32BE(bufPort, chosenPort, 0);
          }
          setReply('SUCCESS', bufPort);
        };
        reject = function() {
          if (replied)
            return;
          replied = true;
          setReply('FAILURE');
        };
      }

      if (name === 'no-more-sessions@openssh.com') {
        self.noMoreSessions = true;
        accept && accept();
        return;
      }

      self.emit('request', accept, reject, name, data);
    } else if (wantReply)
      setReply('FAILURE');
  });

  stream.on('CHANNEL_OPEN', function(info) {
    // do early reject in some cases to prevent wasteful channel allocation
    if ((info.type === 'session' && self.noMoreSessions)
        || !self.authenticated) {
      var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
      return stream.channelOpenFail(info.sender, reasonCode);
    }

    var localChan = nextChannel(self);
    var accept;
    var reject;
    var replied = false;
    if (localChan === false) {
      // auto-reject due to no channels available
      return stream.channelOpenFail(info.sender,
                                    CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE);
    }

    // be optimistic, reserve channel to prevent another request from trying to
    // take the same channel
    channels[localChan] = true;

    reject = function() {
      if (replied)
        return;

      replied = true;

      delete channels[localChan];

      var reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
      return stream.channelOpenFail(info.sender, reasonCode);
    };

    switch (info.type) {
      case 'session':
        if (listenerCount(self, 'session')) {
          accept = function() {
            if (replied)
              return;

            replied = true;

            stream.channelOpenConfirm(info.sender,
                                      localChan,
                                      Channel.MAX_WINDOW,
                                      Channel.PACKET_SIZE);

            return new Session(self, info, localChan);
          };

          self.emit('session', accept, reject);
        } else
          reject();
      break;
      case 'direct-tcpip':
        if (listenerCount(self, 'tcpip')) {
          accept = function() {
            if (replied)
              return;

            replied = true;

            stream.channelOpenConfirm(info.sender,
                                      localChan,
                                      Channel.MAX_WINDOW,
                                      Channel.PACKET_SIZE);

            var chaninfo = {
              type: undefined,
              incoming: {
                id: localChan,
                window: Channel.MAX_WINDOW,
                packetSize: Channel.PACKET_SIZE,
                state: 'open'
              },
              outgoing: {
                id: info.sender,
                window: info.window,
                packetSize: info.packetSize,
                state: 'open'
              }
            };

            return new Channel(chaninfo, self);
          };

          self.emit('tcpip', accept, reject, info.data);
        } else
          reject();
      break;
      case 'direct-streamlocal@openssh.com':
        if (listenerCount(self, 'openssh.streamlocal')) {
          accept = function() {
            if (replied)
              return;

            replied = true;

            stream.channelOpenConfirm(info.sender,
                                      localChan,
                                      Channel.MAX_WINDOW,
                                      Channel.PACKET_SIZE);

            var chaninfo = {
              type: undefined,
              incoming: {
                id: localChan,
                window: Channel.MAX_WINDOW,
                packetSize: Channel.PACKET_SIZE,
                state: 'open'
              },
              outgoing: {
                id: info.sender,
                window: info.window,
                packetSize: info.packetSize,
                state: 'open'
              }
            };

            return new Channel(chaninfo, self);
          };

          self.emit('openssh.streamlocal', accept, reject, info.data);
        } else
          reject();
      break;
      default:
        // auto-reject unsupported channel types
        reject();
    }
  });

  stream.on('NEWKEYS', function() {
    if (++exchanges > 1)
      self.emit('rekey');
  });

  if (kaMgr) {
    this.once('ready', function() {
      kaMgr.add(stream);
    });
  }
}
inherits(Client, EventEmitter);

Client.prototype.end = function() {
  return this._sshstream.disconnect(DISCONNECT_REASON.BY_APPLICATION);
};

Client.prototype.x11 = function(originAddr, originPort, cb) {
  var opts = {
    originAddr: originAddr,
    originPort: originPort
  };
  return openChannel(this, 'x11', opts, cb);
};

Client.prototype.forwardOut = function(boundAddr, boundPort, remoteAddr,
                                       remotePort, cb) {
  var opts = {
    boundAddr: boundAddr,
    boundPort: boundPort,
    remoteAddr: remoteAddr,
    remotePort: remotePort
  };
  return openChannel(this, 'forwarded-tcpip', opts, cb);
};

Client.prototype.openssh_forwardOutStreamLocal = function(socketPath, cb) {
  var opts = {
    socketPath: socketPath
  };
  return openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb);
};

Client.prototype.rekey = function(cb) {
  var stream = this._sshstream;
  var ret = true;
  var error;

  try {
    ret = stream.rekey();
  } catch (ex) {
    error = ex;
  }

  // TODO: re-throw error if no callback?

  if (typeof cb === 'function') {
    if (error) {
      process.nextTick(function() {
        cb(error);
      });
    } else
      this.once('rekey', cb);
  }

  return ret;
};

function Session(client, info, localChan) {
  this.subtype = undefined;

  var ending = false;
  var self = this;
  var outgoingId = info.sender;
  var channel;

  var chaninfo = {
    type: 'session',
    incoming: {
      id: localChan,
      window: Channel.MAX_WINDOW,
      packetSize: Channel.PACKET_SIZE,
      state: 'open'
    },
    outgoing: {
      id: info.sender,
      window: info.window,
      packetSize: info.packetSize,
      state: 'open'
    }
  };

  function onREQUEST(info) {
    var replied = false;
    var accept;
    var reject;

    if (info.wantReply) {
      // "real session" requests will have custom accept behaviors
      if (info.request !== 'shell'
          && info.request !== 'exec'
          && info.request !== 'subsystem') {
        accept = function() {
          if (replied || ending || channel)
            return;

          replied = true;

          return client._sshstream.channelSuccess(outgoingId);
        };
      }

      reject = function() {
        if (replied || ending || channel)
          return;

        replied = true;

        return client._sshstream.channelFailure(outgoingId);
      };
    }

    if (ending) {
      reject && reject();
      return;
    }

    switch (info.request) {
      // "pre-real session start" requests
      case 'env':
        if (listenerCount(self, 'env')) {
          self.emit('env', accept, reject, {
            key: info.key,
            val: info.val
          });
        } else
          reject && reject();
      break;
      case 'pty-req':
        if (listenerCount(self, 'pty')) {
          self.emit('pty', accept, reject, {
            cols: info.cols,
            rows: info.rows,
            width: info.width,
            height: info.height,
            term: info.term,
            modes: info.modes,
          });
        } else
          reject && reject();
      break;
      case 'window-change':
        if (listenerCount(self, 'window-change')) {
          self.emit('window-change', accept, reject, {
            cols: info.cols,
            rows: info.rows,
            width: info.width,
            height: info.height
          });
        } else
          reject && reject();
      break;
      case 'x11-req':
        if (listenerCount(self, 'x11')) {
          self.emit('x11', accept, reject, {
            single: info.single,
            protocol: info.protocol,
            cookie: info.cookie,
            screen: info.screen
          });
        } else
          reject && reject();
      break;
      // "post-real session start" requests
      case 'signal':
        if (listenerCount(self, 'signal')) {
          self.emit('signal', accept, reject, {
            name: info.signal
          });
        } else
          reject && reject();
      break;
      // XXX: is `auth-agent-req@openssh.com` really "post-real session start"?
      case 'auth-agent-req@openssh.com':
        if (listenerCount(self, 'auth-agent'))
          self.emit('auth-agent', accept, reject);
        else
          reject && reject();
      break;
      // "real session start" requests
      case 'shell':
        if (listenerCount(self, 'shell')) {
          accept = function() {
            if (replied || ending || channel)
              return;

            replied = true;

            if (info.wantReply)
              client._sshstream.channelSuccess(outgoingId);

            channel = new Channel(chaninfo, client, { server: true });

            channel.subtype = self.subtype = info.request;

            return channel;
          };

          self.emit('shell', accept, reject);
        } else
          reject && reject();
      break;
      case 'exec':
        if (listenerCount(self, 'exec')) {
          accept = function() {
            if (replied || ending || channel)
              return;

            replied = true;

            if (info.wantReply)
              client._sshstream.channelSuccess(outgoingId);

            channel = new Channel(chaninfo, client, { server: true });

            channel.subtype = self.subtype = info.request;

            return channel;
          };

          self.emit('exec', accept, reject, {
            command: info.command
          });
        } else
          reject && reject();
      break;
      case 'subsystem':
        accept = function() {
          if (replied || ending || channel)
            return;

          replied = true;

          if (info.wantReply)
            client._sshstream.channelSuccess(outgoingId);

          channel = new Channel(chaninfo, client, { server: true });

          channel.subtype = self.subtype = (info.request + ':' + info.subsystem);

          if (info.subsystem === 'sftp') {
            var sftp = new SFTPStream({
              server: true,
              debug: client._sshstream.debug
            });
            channel.pipe(sftp).pipe(channel);

            return sftp;
          } else
            return channel;
        };

        if (info.subsystem === 'sftp' && listenerCount(self, 'sftp'))
          self.emit('sftp', accept, reject);
        else if (info.subsystem !== 'sftp' && listenerCount(self, 'subsystem')) {
          self.emit('subsystem', accept, reject, {
            name: info.subsystem
          });
        } else
          reject && reject();
      break;
      default:
        reject && reject();
    }
  }
  function onEOF() {
    ending = true;
    self.emit('eof');
    self.emit('end');
  }
  function onCLOSE() {
    ending = true;
    self.emit('close');
  }
  client._sshstream
        .on('CHANNEL_REQUEST:' + localChan, onREQUEST)
        .once('CHANNEL_EOF:' + localChan, onEOF)
        .once('CHANNEL_CLOSE:' + localChan, onCLOSE);
}
inherits(Session, EventEmitter);


function AuthContext(stream, username, service, method, cb) {
  EventEmitter.call(this);

  var self = this;

  this.username = this.user = username;
  this.service = service;
  this.method = method;
  this._initialResponse = false;
  this._finalResponse = false;
  this._multistep = false;
  this._cbfinal = function(allowed, methodsLeft, isPartial) {
    if (!self._finalResponse) {
      self._finalResponse = true;
      cb(self, allowed, methodsLeft, isPartial);
    }
  };
  this._stream = stream;
}
inherits(AuthContext, EventEmitter);
AuthContext.prototype.accept = function() {
  this._cleanup && this._cleanup();
  this._initialResponse = true;
  this._cbfinal(true);
};
AuthContext.prototype.reject = function(methodsLeft, isPartial) {
  this._cleanup && this._cleanup();
  this._initialResponse = true;
  this._cbfinal(false, methodsLeft, isPartial);
};

var RE_KBINT_SUBMETHODS = /[ \t\r\n]*,[ \t\r\n]*/g;
function KeyboardAuthContext(stream, username, service, method, submethods, cb) {
  AuthContext.call(this, stream, username, service, method, cb);
  this._multistep = true;

  var self = this;

  this._cb = undefined;
  this._onInfoResponse = function(responses) {
    if (self._cb) {
      var callback = self._cb;
      self._cb = undefined;
      callback(responses);
    }
  };
  this.submethods = submethods.split(RE_KBINT_SUBMETHODS);
  this.on('abort', function() {
    self._cb && self._cb(new Error('Authentication request aborted'));
  });
}
inherits(KeyboardAuthContext, AuthContext);
KeyboardAuthContext.prototype._cleanup = function() {
  this._stream.removeListener('USERAUTH_INFO_RESPONSE', this._onInfoResponse);
};
KeyboardAuthContext.prototype.prompt = function(prompts, title, instructions,
                                                cb) {
  if (!Array.isArray(prompts))
    prompts = [ prompts ];

  if (typeof title === 'function') {
    cb = title;
    title = instructions = undefined;
  } else if (typeof instructions === 'function') {
    cb = instructions;
    instructions = undefined;
  }

  for (var i = 0; i < prompts.length; ++i) {
    if (typeof prompts[i] === 'string') {
      prompts[i] = {
        prompt: prompts[i],
        echo: true
      };
    }
  }

  this._cb = cb;
  this._initialResponse = true;
  this._stream.once('USERAUTH_INFO_RESPONSE', this._onInfoResponse);

  return this._stream.authInfoReq(title, instructions, prompts);
};

function PKAuthContext(stream, username, service, method, pkInfo, cb) {
  AuthContext.call(this, stream, username, service, method, cb);

  this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
  this.signature = pkInfo.signature;
  var sigAlgo;
  if (this.signature) {
    // TODO: move key type checking logic to ssh2-streams
    switch (pkInfo.keyAlgo) {
      case 'ssh-rsa':
      case 'ssh-dss':
        sigAlgo = 'sha1';
        break;
      case 'ssh-ed25519':
        sigAlgo = null;
        break;
      case 'ecdsa-sha2-nistp256':
        sigAlgo = 'sha256';
        break;
      case 'ecdsa-sha2-nistp384':
        sigAlgo = 'sha384';
        break;
      case 'ecdsa-sha2-nistp521':
        sigAlgo = 'sha512';
        break;
    }
  }
  this.sigAlgo = sigAlgo;
  this.blob = pkInfo.blob;
}
inherits(PKAuthContext, AuthContext);
PKAuthContext.prototype.accept = function() {
  if (!this.signature) {
    this._initialResponse = true;
    this._stream.authPKOK(this.key.algo, this.key.data);
  } else {
    AuthContext.prototype.accept.call(this);
  }
};

function HostbasedAuthContext(stream, username, service, method, pkInfo, cb) {
  AuthContext.call(this, stream, username, service, method, cb);

  this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
  this.signature = pkInfo.signature;
  var sigAlgo;
  if (this.signature) {
    // TODO: move key type checking logic to ssh2-streams
    switch (pkInfo.keyAlgo) {
      case 'ssh-rsa':
      case 'ssh-dss':
        sigAlgo = 'sha1';
        break;
      case 'ssh-ed25519':
        sigAlgo = null;
        break;
      case 'ecdsa-sha2-nistp256':
        sigAlgo = 'sha256';
        break;
      case 'ecdsa-sha2-nistp384':
        sigAlgo = 'sha384';
        break;
      case 'ecdsa-sha2-nistp521':
        sigAlgo = 'sha512';
        break;
    }
  }
  this.sigAlgo = sigAlgo;
  this.blob = pkInfo.blob;
  this.localHostname = pkInfo.localHostname;
  this.localUsername = pkInfo.localUsername;
}
inherits(HostbasedAuthContext, AuthContext);

function PwdAuthContext(stream, username, service, method, password, cb) {
  AuthContext.call(this, stream, username, service, method, cb);

  this.password = password;
}
inherits(PwdAuthContext, AuthContext);


function openChannel(self, type, opts, cb) {
  // ask the client to open a channel for some purpose
  // (e.g. a forwarded TCP connection)
  var localChan = nextChannel(self);
  var initWindow = Channel.MAX_WINDOW;
  var maxPacket = Channel.PACKET_SIZE;
  var ret = true;

  if (localChan === false)
    return cb(new Error('No free channels available'));

  if (typeof opts === 'function') {
    cb = opts;
    opts = {};
  }

  self._channels[localChan] = true;

  var sshstream = self._sshstream;
  sshstream.once('CHANNEL_OPEN_CONFIRMATION:' + localChan, function(info) {
    sshstream.removeAllListeners('CHANNEL_OPEN_FAILURE:' + localChan);

    var chaninfo = {
      type: type,
      incoming: {
        id: localChan,
        window: initWindow,
        packetSize: maxPacket,
        state: 'open'
      },
      outgoing: {
        id: info.sender,
        window: info.window,
        packetSize: info.packetSize,
        state: 'open'
      }
    };
    cb(undefined, new Channel(chaninfo, self, { server: true }));
  }).once('CHANNEL_OPEN_FAILURE:' + localChan, function(info) {
    sshstream.removeAllListeners('CHANNEL_OPEN_CONFIRMATION:' + localChan);

    delete self._channels[localChan];

    var err = new Error('(SSH) Channel open failure: ' + info.description);
    err.reason = info.reason;
    err.lang = info.lang;
    cb(err);
  });

  if (type === 'forwarded-tcpip')
    ret = sshstream.forwardedTcpip(localChan, initWindow, maxPacket, opts);
  else if (type === 'x11')
    ret = sshstream.x11(localChan, initWindow, maxPacket, opts);
  else if (type === 'forwarded-streamlocal@openssh.com') {
    ret = sshstream.openssh_forwardedStreamLocal(localChan,
                                                 initWindow,
                                                 maxPacket,
                                                 opts);
  }

  return ret;
}

function nextChannel(self) {
  // get the next available channel number

  // fast path
  if (self._curChan < MAX_CHANNEL)
    return ++self._curChan;

  // slower lookup path
  for (var i = 0, channels = self._channels; i < MAX_CHANNEL; ++i)
    if (!channels[i])
      return i;

  return false;
}


Server.createServer = function(cfg, listener) {
  return new Server(cfg, listener);
};
Server.KEEPALIVE_INTERVAL = 1000;
Server.KEEPALIVE_CLIENT_INTERVAL = 15000;
Server.KEEPALIVE_CLIENT_COUNT_MAX = 3;

module.exports = Server;
module.exports.IncomingClient = Client;
