// TODO:
//    * handle multi-line header values (OpenSSH)?
//    * more thorough validation?

var utils;
var Ber = require('asn1').Ber;
var semver = require('semver');

var readUInt32BE = require('./buffer-helpers').readUInt32BE;

var RE_PPK = /^PuTTY-User-Key-File-2: ssh-(rsa|dss)\r?\nEncryption: (aes256-cbc|none)\r?\nComment: ([^\r\n]*)\r?\nPublic-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-MAC: ([^\r\n]+)/;
var RE_HEADER_OPENSSH_PRIV = /^-----BEGIN (RSA|DSA|EC) PRIVATE KEY-----$/i;
var RE_FOOTER_OPENSSH_PRIV = /^-----END (?:RSA|DSA|EC) PRIVATE KEY-----$/i;
var RE_HEADER_OPENSSH_PUB = /^((?:(?:ssh-(rsa|dss))|ecdsa-sha2-nistp(256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z\/+=]+)(?:$|\s+([\S].*)?)$/i;
var RE_HEADER_RFC4716_PUB = /^---- BEGIN SSH2 PUBLIC KEY ----$/i;
var RE_FOOTER_RFC4716_PUB = /^---- END SSH2 PUBLIC KEY ----$/i;
var RE_HEADER_OPENSSH = /^([^:]+):\s*([\S].*)?$/i;
var RE_HEADER_RFC4716 = /^([^:]+): (.*)?$/i;

module.exports = function(data) {
  if (Buffer.isBuffer(data))
    data = data.toString('utf8');
  else if (typeof data !== 'string')
    return new Error('Key data must be a Buffer or string');

  var ret = {
    fulltype: undefined,
    type: undefined,
    curve: undefined,
    extra: undefined,
    comment: undefined,
    encryption: undefined,
    private: undefined,
    privateOrig: undefined,
    public: undefined,
    publicOrig: undefined
  };
  var m;
  var i;
  var len;

  data = data.trim().split(/\r\n|\n/);

  while (!data[0].length)
    data.shift();
  while (!data.slice(-1)[0].length)
    data.pop();

  var orig = data.join('\n');

  if ((m = RE_HEADER_OPENSSH_PRIV.exec(data[0]))
      && RE_FOOTER_OPENSSH_PRIV.test(data.slice(-1))) {
    // OpenSSH private key
    var keyType = m[1].toLowerCase();
    if (keyType === 'dsa')
      keyType = 'dss';

    if (keyType === 'ec' && semver.lt(process.version, '5.2.0')) {
      return new Error(
        'EC private keys are not supported in this version of node'
      );
    }

    if (!RE_HEADER_OPENSSH.test(data[1])) {
      // unencrypted, no headers
      var privData = Buffer.from(data.slice(1, -1).join(''), 'base64');
      if (keyType !== 'ec') {
        ret.fulltype = 'ssh-' + keyType;
      } else {
        // ECDSA
        var asnReader = new Ber.Reader(privData);
        asnReader.readSequence();
        asnReader.readInt();
        asnReader.readString(Ber.OctetString, true);
        asnReader.readByte(); // Skip "complex" context type byte
        var offset = asnReader.readLength(); // Skip context length
        if (offset !== null) {
          asnReader._offset = offset;
          switch (asnReader.readOID()) {
            case '1.2.840.10045.3.1.7':
              // prime256v1/secp256r1
              ret.fulltype = 'ecdsa-sha2-nistp256';
              break;
            case '1.3.132.0.34':
              // secp384r1
              ret.fulltype = 'ecdsa-sha2-nistp384';
              break;
            case '1.3.132.0.35':
              // secp521r1
              ret.fulltype = 'ecdsa-sha2-nistp521';
              break;
          }
        }
        if (ret.fulltype === undefined)
          return new Error('Unsupported EC private key type');
      }
      ret.private = privData;
    } else {
      // possibly encrypted, headers
      for (i = 1, len = data.length; i < len; ++i) {
        m = RE_HEADER_OPENSSH.exec(data[i]);
        if (m) {
          m[1] = m[1].toLowerCase();
          if (m[1] === 'dek-info') {
            m[2] = m[2].split(',');
            ret.encryption = m[2][0].toLowerCase();
            if (m[2].length > 1)
              ret.extra = m[2].slice(1);
          }
        } else if (data[i].length)
          break;
      }
      ret.private = Buffer.from(data.slice(i, -1).join(''), 'base64');
    }
    ret.type = keyType;
    ret.privateOrig = Buffer.from(orig);
  } else if (m = RE_HEADER_OPENSSH_PUB.exec(data[0])) {
    // OpenSSH public key
    ret.fulltype = m[1];
    ret.type = (m[2] || 'ec').toLowerCase();
    ret.public = Buffer.from(m[4], 'base64');
    ret.publicOrig = Buffer.from(orig);
    ret.comment = m[5];
    if (m[3]) // ECDSA only
      ret.curve = 'nistp' + m[3];
  } else if (RE_HEADER_RFC4716_PUB.test(data[0])
             && RE_FOOTER_RFC4716_PUB.test(data.slice(-1))) {
    if (data[1].indexOf(': ') === -1) {
      // no headers
      ret.public = Buffer.from(data.slice(1, -1).join(''), 'base64');
    } else {
      // headers
      for (i = 1, len = data.length; i < len; ++i) {
        if (data[i].indexOf(': ') === -1) {
          if (data[i].length)
            break; // start of key data
          else
            continue; // empty line
        }
        while (data[i].substr(-1) === '\\') {
          if (i + 1 < len) {
            data[i] = data[i].slice(0, -1) + data[i + 1];
            data.splice(i + 1, 1);
            --len;
          } else
            return new Error('RFC4716 public key missing header continuation line');
        }
        m = RE_HEADER_RFC4716.exec(data[i]);
        if (m) {
          m[1] = m[1].toLowerCase();
          if (m[1] === 'comment') {
            ret.comment = m[2] || '';
            if (ret.comment[0] === '"' && ret.comment.substr(-1) === '"')
              ret.comment = ret.comment.slice(1, -1);
          }
        } else
          return new Error('RFC4716 public key invalid header line');
      }
      ret.public = Buffer.from(data.slice(i, -1).join(''), 'base64');
    }
    len = readUInt32BE(ret.public, 0);
    var fulltype = ret.public.toString('ascii', 4, 4 + len);
    ret.fulltype = fulltype;
    if (fulltype === 'ssh-dss')
      ret.type = 'dss';
    else if (fulltype === 'ssh-rsa')
      ret.type = 'rsa';
    else
      return new Error('Unsupported RFC4716 public key type: ' + fulltype);
    ret.public = ret.public.slice(11);
    ret.publicOrig = Buffer.from(orig);
  } else if (m = RE_PPK.exec(orig)) {
    // m[1] = short type
    // m[2] = encryption type
    // m[3] = comment
    // m[4] = base64-encoded public key data:
    //         for "ssh-rsa":
    //          string "ssh-rsa"
    //          mpint  e    (public exponent)
    //          mpint  n    (modulus)
    //         for "ssh-dss":
    //          string "ssh-dss"
    //          mpint p     (modulus)
    //          mpint q     (prime)
    //          mpint g     (base number)
    //          mpint y     (public key parameter: g^x mod p)
    // m[5] = base64-encoded private key data:
    //         for "ssh-rsa":
    //          mpint  d    (private exponent)
    //          mpint  p    (prime 1)
    //          mpint  q    (prime 2)
    //          mpint  iqmp ([inverse of q] mod p)
    //         for "ssh-dss":
    //          mpint x     (private key parameter)
    // m[6] = SHA1 HMAC over:
    //          string  name of algorithm ("ssh-dss", "ssh-rsa")
    //          string  encryption type
    //          string  comment
    //          string  public key data
    //          string  private-plaintext (including the final padding)

    // avoid cyclic require by requiring on first use
    if (!utils)
      utils = require('./utils');

    ret.ppk = true;
    ret.type = m[1];
    ret.fulltype = 'ssh-' + m[1];
    if (m[2] !== 'none')
      ret.encryption = m[2];
    ret.comment = m[3];

    ret.public = Buffer.from(m[4].replace(/\r?\n/g, ''), 'base64');
    var privateKey = Buffer.from(m[5].replace(/\r?\n/g, ''), 'base64');

    ret.privateMAC = m[6].replace(/\r?\n/g, '');

    // automatically verify private key MAC if we don't need to wait for
    // decryption
    if (!ret.encryption) {
      var valid = utils.verifyPPKMAC(ret, undefined, privateKey);
      if (!valid)
        throw new Error('PPK MAC mismatch');
    }

    // generate a PEM encoded version of the public key
    var pubkey = utils.genPublicKey(ret);
    ret.public = pubkey.public;
    ret.publicOrig = pubkey.publicOrig;

    ret.private = privateKey;

    // automatically convert private key data to OpenSSL format (including PEM)
    // if we don't need to wait for decryption
    if (!ret.encryption)
      utils.convertPPKPrivate(ret);
  } else
    return new Error('Unsupported key format');

  return ret;
};
