const PluginAuth = require('./plugin-auth');
const fs = require('fs');
const crypto = require('crypto');
const Errors = require('../../../misc/errors');
const NativePasswordAuth = require('./native-password-auth');
const Sha256PasswordAuth = require('./sha256-password-auth');

const State = {
  INIT: 'INIT',
  FAST_AUTH_RESULT: 'FAST_AUTH_RESULT',
  REQUEST_SERVER_KEY: 'REQUEST_SERVER_KEY',
  SEND_AUTH: 'SEND_AUTH'
};

/**
 * Use caching Sha2 password authentication
 */
class CachingSha2PasswordAuth extends PluginAuth {
  constructor(packSeq, compressPackSeq, pluginData, resolve, reject, multiAuthResolver) {
    super(resolve, reject, multiAuthResolver);
    this.pluginData = pluginData;
    this.sequenceNo = packSeq;
    this.counter = 0;
    this.state = State.INIT;
  }

  start(out, opts, info) {
    this.exchange(this.pluginData, out, opts, info);
    this.onPacketReceive = this.response;
  }

  exchange(buffer, out, opts, info) {
    switch (this.state) {
      case State.INIT:
        const truncatedSeed = this.pluginData.slice(0, this.pluginData.length - 1);
        const encPwd = NativePasswordAuth.encryptPassword(opts.password, truncatedSeed, 'sha256');
        out.startPacket(this);
        if (encPwd.length > 0) {
          out.writeBuffer(encPwd, 0, encPwd.length);
          out.flushBuffer(true);
        } else {
          out.writeEmptyPacket(true);
        }
        this.state = State.FAST_AUTH_RESULT;
        return;

      case State.FAST_AUTH_RESULT:
        // length encoded numeric : 0x01 0x03/0x04
        const fastAuthResult = buffer[1];
        switch (fastAuthResult) {
          case 0x03:
            // success authentication
            this.emit('send_end');
            return this.successSend(packet, out, opts, info);

          case 0x04:
            if (opts.ssl) {
              // using SSL, so sending password in clear
              out.startPacket(this);
              out.writeString(opts.password);
              out.writeInt8(0);
              out.flushBuffer(true);
              return;
            }

            // retrieve public key from configuration or from server
            if (opts.cachingRsaPublicKey) {
              try {
                let key = opts.cachingRsaPublicKey;
                if (!key.includes('-----BEGIN')) {
                  // rsaPublicKey contain path
                  key = fs.readFileSync(key, 'utf8');
                }
                this.publicKey = Sha256PasswordAuth.retreivePublicKey(key);
              } catch (err) {
                return this.throwError(err, info);
              }
              // send Sha256Password Packet
              Sha256PasswordAuth.sendSha256PwdPacket(
                this,
                this.pluginData,
                this.publicKey,
                opts.password,
                out
              );
            } else {
              if (!opts.allowPublicKeyRetrieval) {
                return this.throwError(
                  Errors.createError(
                    'RSA public key is not available client side. Either set option `cachingRsaPublicKey` to indicate' +
                      ' public key path, or allow public key retrieval with option `allowPublicKeyRetrieval`',
                    null,
                    true,
                    info,
                    '08S01',
                    Errors.ER_CANNOT_RETRIEVE_RSA_KEY
                  ),
                  info
                );
              }
              this.state = State.REQUEST_SERVER_KEY;
              // ask caching public Key Retrieval
              out.startPacket(this);
              out.writeInt8(0x02);
              out.flushBuffer(true);
            }
            return;
        }

      case State.REQUEST_SERVER_KEY:
        this.publicKey = Sha256PasswordAuth.retreivePublicKey(buffer.toString('utf8', 1));
        this.state = State.SEND_AUTH;
        Sha256PasswordAuth.sendSha256PwdPacket(
          this,
          this.pluginData,
          this.publicKey,
          opts.password,
          out
        );
    }
  }

  static retreivePublicKey(key) {
    return key.replace('(-+BEGIN PUBLIC KEY-+\\r?\\n|\\n?-+END PUBLIC KEY-+\\r?\\n?)', '');
  }

  static sendSha256PwdPacket(cmd, pluginData, publicKey, password, out) {
    const truncatedSeed = pluginData.slice(0, pluginData.length - 1);
    out.startPacket(cmd);
    const enc = Sha256PasswordAuth.encrypt(truncatedSeed, password, publicKey);
    out.writeBuffer(enc, 0, enc.length);
    out.flushBuffer(cmd);
  }

  // encrypt password with public key
  static encrypt(seed, password, publicKey) {
    const nullFinishedPwd = Buffer.from(password + '\0');
    const xorBytes = Buffer.allocUnsafe(nullFinishedPwd.length);
    const seedLength = seed.length;
    for (let i = 0; i < xorBytes.length; i++) {
      xorBytes[i] = nullFinishedPwd[i] ^ seed[i % seedLength];
    }
    return crypto.publicEncrypt(
      { key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
      xorBytes
    );
  }

  response(packet, out, opts, info) {
    const marker = packet.peek();
    switch (marker) {
      //*********************************************************************************************************
      //* OK_Packet and Err_Packet ending packet
      //*********************************************************************************************************
      case 0x00:
      case 0xff:
        this.emit('send_end');
        return this.successSend(packet, out, opts, info);

      default:
        let promptData = packet.readBufferRemaining();
        this.exchange(promptData, out, opts, info);
        this.onPacketReceive = this.response;
    }
  }
}

module.exports = CachingSha2PasswordAuth;
