const PluginAuth = require('./plugin-auth');
const fs = require('fs');
const crypto = require('crypto');
const Errors = require('../../../misc/errors');

/**
 * Use Sha256 authentication
 */
class Sha256PasswordAuth extends PluginAuth {
  constructor(packSeq, compressPackSeq, pluginData, resolve, reject, multiAuthResolver) {
    super(resolve, reject, multiAuthResolver);
    this.pluginData = pluginData;
    this.sequenceNo = packSeq;
    this.counter = 0;
    this.initialState = true;
  }

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

  exchange(buffer, out, opts, info) {
    if (this.initialState) {
      if (!opts.password) {
        out.startPacket(this);
        out.writeEmptyPacket(true);
        return;
      } else if (opts.ssl) {
        // using SSL, so sending password in clear
        out.startPacket(this);
        if (opts.password) {
          out.writeString(opts.password);
        }
        out.writeInt8(0);
        out.flushBuffer(true);
        return;
      } else {
        // retrieve public key from configuration or from server
        if (opts.rsaPublicKey) {
          try {
            let key = opts.rsaPublicKey;
            if (!key.includes('-----BEGIN')) {
              // rsaPublicKey contain path
              key = fs.readFileSync(key, 'utf8');
            }
            this.publicKey = Sha256PasswordAuth.retreivePublicKey(key);
          } catch (err) {
            return this.throwError(err, info);
          }
        } else {
          if (!opts.allowPublicKeyRetrieval) {
            return this.throwError(
              Errors.createError(
                'RSA public key is not available client side. Either set option `rsaPublicKey` 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.initialState = false;

          // ask public Key Retrieval
          out.startPacket(this);
          out.writeInt8(0x01);
          out.flushBuffer(true);
          return;
        }
      }

      // send Sha256Password Packet
      Sha256PasswordAuth.sendSha256PwdPacket(
        this,
        this.pluginData,
        this.publicKey,
        opts.password,
        out
      );
    } else {
      // has request public key
      this.publicKey = Sha256PasswordAuth.retreivePublicKey(buffer.toString('utf8', 1));
      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 = Sha256PasswordAuth;
