'use strict';

const arrayStrictEqual = require('../utils').arrayStrictEqual;
const tagsStrictEqual = require('../utils').tagsStrictEqual;
const errorStrictEqual = require('../utils').errorStrictEqual;
const ServerType = require('./common').ServerType;
const now = require('../../utils').now;

const WRITABLE_SERVER_TYPES = new Set([
  ServerType.RSPrimary,
  ServerType.Standalone,
  ServerType.Mongos
]);

const DATA_BEARING_SERVER_TYPES = new Set([
  ServerType.RSPrimary,
  ServerType.RSSecondary,
  ServerType.Mongos,
  ServerType.Standalone
]);

const ISMASTER_FIELDS = [
  'minWireVersion',
  'maxWireVersion',
  'maxBsonObjectSize',
  'maxMessageSizeBytes',
  'maxWriteBatchSize',
  'compression',
  'me',
  'hosts',
  'passives',
  'arbiters',
  'tags',
  'setName',
  'setVersion',
  'electionId',
  'primary',
  'logicalSessionTimeoutMinutes',
  'saslSupportedMechs',
  '__nodejs_mock_server__',
  '$clusterTime'
];

/**
 * The client's view of a single server, based on the most recent ismaster outcome.
 *
 * Internal type, not meant to be directly instantiated
 */
class ServerDescription {
  /**
   * Create a ServerDescription
   * @param {String} address The address of the server
   * @param {Object} [ismaster] An optional ismaster response for this server
   * @param {Object} [options] Optional settings
   * @param {Number} [options.roundTripTime] The round trip time to ping this server (in ms)
   * @param {Error} [options.error] An Error used for better reporting debugging
   * @param {any} [options.topologyVersion] The topologyVersion
   */
  constructor(address, ismaster, options) {
    options = options || {};
    ismaster = Object.assign(
      {
        minWireVersion: 0,
        maxWireVersion: 0,
        hosts: [],
        passives: [],
        arbiters: [],
        tags: []
      },
      ismaster
    );

    if (ismaster.isWritablePrimary != null) {
      ismaster.ismaster = ismaster.isWritablePrimary;
    }

    this.address = address;
    this.error = options.error;
    this.roundTripTime = options.roundTripTime || -1;
    this.lastUpdateTime = now();
    this.lastWriteDate = ismaster.lastWrite ? ismaster.lastWrite.lastWriteDate : null;
    this.opTime = ismaster.lastWrite ? ismaster.lastWrite.opTime : null;
    this.type = parseServerType(ismaster);
    this.topologyVersion = options.topologyVersion || ismaster.topologyVersion;

    // direct mappings
    ISMASTER_FIELDS.forEach(field => {
      if (typeof ismaster[field] !== 'undefined') this[field] = ismaster[field];
    });

    // normalize case for hosts
    if (this.me) this.me = this.me.toLowerCase();
    this.hosts = this.hosts.map(host => host.toLowerCase());
    this.passives = this.passives.map(host => host.toLowerCase());
    this.arbiters = this.arbiters.map(host => host.toLowerCase());
  }

  get allHosts() {
    return this.hosts.concat(this.arbiters).concat(this.passives);
  }

  /**
   * @return {Boolean} Is this server available for reads
   */
  get isReadable() {
    return this.type === ServerType.RSSecondary || this.isWritable;
  }

  /**
   * @return {Boolean} Is this server data bearing
   */
  get isDataBearing() {
    return DATA_BEARING_SERVER_TYPES.has(this.type);
  }

  /**
   * @return {Boolean} Is this server available for writes
   */
  get isWritable() {
    return WRITABLE_SERVER_TYPES.has(this.type);
  }

  get host() {
    const chopLength = `:${this.port}`.length;
    return this.address.slice(0, -chopLength);
  }

  get port() {
    const port = this.address.split(':').pop();
    return port ? Number.parseInt(port, 10) : port;
  }

  /**
   * Determines if another `ServerDescription` is equal to this one per the rules defined
   * in the {@link https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#serverdescription|SDAM spec}
   *
   * @param {ServerDescription} other
   * @return {Boolean}
   */
  equals(other) {
    const topologyVersionsEqual =
      this.topologyVersion === other.topologyVersion ||
      compareTopologyVersion(this.topologyVersion, other.topologyVersion) === 0;

    return (
      other != null &&
      errorStrictEqual(this.error, other.error) &&
      this.type === other.type &&
      this.minWireVersion === other.minWireVersion &&
      this.me === other.me &&
      arrayStrictEqual(this.hosts, other.hosts) &&
      tagsStrictEqual(this.tags, other.tags) &&
      this.setName === other.setName &&
      this.setVersion === other.setVersion &&
      (this.electionId
        ? other.electionId && this.electionId.equals(other.electionId)
        : this.electionId === other.electionId) &&
      this.primary === other.primary &&
      this.logicalSessionTimeoutMinutes === other.logicalSessionTimeoutMinutes &&
      topologyVersionsEqual
    );
  }
}

/**
 * Parses an `ismaster` message and determines the server type
 *
 * @param {Object} ismaster The `ismaster` message to parse
 * @return {ServerType}
 */
function parseServerType(ismaster) {
  if (!ismaster || !ismaster.ok) {
    return ServerType.Unknown;
  }

  if (ismaster.isreplicaset) {
    return ServerType.RSGhost;
  }

  if (ismaster.msg && ismaster.msg === 'isdbgrid') {
    return ServerType.Mongos;
  }

  if (ismaster.setName) {
    if (ismaster.hidden) {
      return ServerType.RSOther;
    } else if (ismaster.ismaster) {
      return ServerType.RSPrimary;
    } else if (ismaster.secondary) {
      return ServerType.RSSecondary;
    } else if (ismaster.arbiterOnly) {
      return ServerType.RSArbiter;
    } else {
      return ServerType.RSOther;
    }
  }

  return ServerType.Standalone;
}

/**
 * Compares two topology versions.
 *
 * @param {object} lhs
 * @param {object} rhs
 * @returns A negative number if `lhs` is older than `rhs`; positive if `lhs` is newer than `rhs`; 0 if they are equivalent.
 */
function compareTopologyVersion(lhs, rhs) {
  if (lhs == null || rhs == null) {
    return -1;
  }

  if (lhs.processId.equals(rhs.processId)) {
    // TODO: handle counters as Longs
    if (lhs.counter === rhs.counter) {
      return 0;
    } else if (lhs.counter < rhs.counter) {
      return -1;
    }

    return 1;
  }

  return -1;
}

module.exports = {
  ServerDescription,
  parseServerType,
  compareTopologyVersion
};
