'use strict';

const ClientConstants = require('../constants/client.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const Packet = require('../packets/packet.js');

const auth41 = require('../auth_41.js');

class HandshakeResponse {
  constructor(handshake) {
    this.user = handshake.user || '';
    this.database = handshake.database || '';
    this.password = handshake.password || '';
    this.passwordSha1 = handshake.passwordSha1;
    this.authPluginData1 = handshake.authPluginData1;
    this.authPluginData2 = handshake.authPluginData2;
    this.compress = handshake.compress;
    this.clientFlags = handshake.flags;
    this.mariadbExtendedClientFlags = handshake.mariadbExtendedClientFlags || 0;

    // Accept pre-calculated authToken and authPluginName from caller
    // This allows the caller to optimize by using the server's preferred auth method
    if (
      handshake.authToken !== undefined &&
      handshake.authPluginName !== undefined
    ) {
      // Validate types to fail fast with clear errors
      if (!Buffer.isBuffer(handshake.authToken)) {
        throw new TypeError(
          'HandshakeResponse authToken must be a Buffer when provided'
        );
      }
      if (typeof handshake.authPluginName !== 'string') {
        throw new TypeError(
          'HandshakeResponse authPluginName must be a string when provided'
        );
      }
      this.authToken = handshake.authToken;
      this.authPluginName = handshake.authPluginName;
    } else {
      // Fallback to legacy behavior: calculate mysql_native_password token
      // TODO: pre-4.1 auth support
      let authToken;
      if (this.passwordSha1) {
        authToken = auth41.calculateTokenFromPasswordSha(
          this.passwordSha1,
          this.authPluginData1,
          this.authPluginData2
        );
      } else {
        authToken = auth41.calculateToken(
          this.password,
          this.authPluginData1,
          this.authPluginData2
        );
      }
      this.authToken = authToken;
      this.authPluginName = 'mysql_native_password';
    }

    this.charsetNumber = handshake.charsetNumber;
    this.encoding = CharsetToEncoding[handshake.charsetNumber];
    this.connectAttributes = handshake.connectAttributes;
  }

  serializeResponse(buffer) {
    const isSet = (flag) => this.clientFlags & ClientConstants[flag];
    const packet = new Packet(0, buffer, 0, buffer.length);
    packet.offset = 4;
    packet.writeInt32(this.clientFlags);
    packet.writeInt32(0); // max packet size. todo: move to config
    packet.writeInt8(this.charsetNumber);
    // the last 4 of the 23 reserved bytes carry the MariaDB extended client
    // capabilities (zero when not negotiated, i.e. plain filler)
    packet.skip(19);
    packet.writeInt32(this.mariadbExtendedClientFlags);
    const encoding = this.encoding;
    packet.writeNullTerminatedString(this.user, encoding);
    let k;
    if (isSet('PLUGIN_AUTH_LENENC_CLIENT_DATA')) {
      packet.writeLengthCodedNumber(this.authToken.length);
      packet.writeBuffer(this.authToken);
    } else if (isSet('SECURE_CONNECTION')) {
      packet.writeInt8(this.authToken.length);
      packet.writeBuffer(this.authToken);
    } else {
      packet.writeBuffer(this.authToken);
      packet.writeInt8(0);
    }
    if (isSet('CONNECT_WITH_DB')) {
      packet.writeNullTerminatedString(this.database, encoding);
    }
    if (isSet('PLUGIN_AUTH')) {
      // Use the auth plugin name specified by the caller (optimized for server's preference)
      // or fall back to mysql_native_password for backward compatibility
      packet.writeNullTerminatedString(
        this.authPluginName || 'mysql_native_password',
        'latin1'
      );
    }
    if (isSet('CONNECT_ATTRS')) {
      const connectAttributes = this.connectAttributes || {};
      const attrNames = Object.keys(connectAttributes);
      let keysLength = 0;
      for (k = 0; k < attrNames.length; ++k) {
        keysLength += Packet.lengthCodedStringLength(attrNames[k], encoding);
        keysLength += Packet.lengthCodedStringLength(
          connectAttributes[attrNames[k]],
          encoding
        );
      }
      packet.writeLengthCodedNumber(keysLength);
      for (k = 0; k < attrNames.length; ++k) {
        packet.writeLengthCodedString(attrNames[k], encoding);
        packet.writeLengthCodedString(
          connectAttributes[attrNames[k]],
          encoding
        );
      }
    }
    return packet;
  }

  toPacket() {
    if (typeof this.user !== 'string') {
      throw new Error('"user" connection config property must be a string');
    }
    if (typeof this.database !== 'string') {
      throw new Error('"database" connection config property must be a string');
    }
    // dry run: calculate resulting packet length
    const p = this.serializeResponse(Packet.MockBuffer());
    return this.serializeResponse(Buffer.alloc(p.offset));
  }
  static fromPacket(packet, serverFlags = 0xffffffff) {
    const args = {};
    args.clientFlags = packet.readInt32();
    function isSet(flag) {
      return args.clientFlags & serverFlags & ClientConstants[flag];
    }
    args.maxPacketSize = packet.readInt32();
    args.charsetNumber = packet.readInt8();
    const encoding = CharsetToEncoding[args.charsetNumber];
    args.encoding = encoding;
    packet.skip(23);
    args.user = packet.readNullTerminatedString(encoding);
    let authTokenLength;
    if (isSet('PLUGIN_AUTH_LENENC_CLIENT_DATA')) {
      authTokenLength = packet.readLengthCodedNumber(encoding);
      args.authToken = packet.readBuffer(authTokenLength);
    } else if (isSet('SECURE_CONNECTION')) {
      authTokenLength = packet.readInt8();
      args.authToken = packet.readBuffer(authTokenLength);
    } else {
      args.authToken = packet.readNullTerminatedString(encoding);
    }
    if (isSet('CONNECT_WITH_DB')) {
      args.database = packet.readNullTerminatedString(encoding);
    }
    if (isSet('PLUGIN_AUTH')) {
      args.authPluginName = packet.readNullTerminatedString(encoding);
    }
    if (isSet('CONNECT_ATTRS')) {
      const keysLength = packet.readLengthCodedNumber(encoding);
      const keysEnd = packet.offset + keysLength;
      const attrs = {};
      while (packet.offset < keysEnd) {
        attrs[packet.readLengthCodedString(encoding)] =
          packet.readLengthCodedString(encoding);
      }
      args.connectAttributes = attrs;
    }
    return args;
  }
}

module.exports = HandshakeResponse;
