/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';

const util = require('util');
const policies = require('./policies');
const types = require('./types');
const utils = require('./utils');
const tracker = require('./tracker');
const metrics = require('./metrics');
const auth = require('./auth');

/** Core connections per host for protocol versions 1 and 2 */
const coreConnectionsPerHostV2 = {
  [types.distance.local]: 2,
  [types.distance.remote]: 1,
  [types.distance.ignored]: 0
};

/** Core connections per host for protocol version 3 and above */
const coreConnectionsPerHostV3 = {
  [types.distance.local]: 1,
  [types.distance.remote]: 1,
  [types.distance.ignored]: 0
};

/** Default maxRequestsPerConnection value for protocol v1 and v2 */
const maxRequestsPerConnectionV2 = 128;

/** Default maxRequestsPerConnection value for protocol v3+ */
const maxRequestsPerConnectionV3 = 2048;

const continuousPageUnitBytes = 'bytes';
const continuousPageDefaultSize = 5000;
const continuousPageDefaultHighWaterMark = 10000;

/**
 * @returns {ClientOptions}
 */
function defaultOptions () {
  return ({
    policies: {
      addressResolution: policies.defaultAddressTranslator(),
      loadBalancing: policies.defaultLoadBalancingPolicy(),
      reconnection: policies.defaultReconnectionPolicy(),
      retry: policies.defaultRetryPolicy(),
      speculativeExecution: policies.defaultSpeculativeExecutionPolicy(),
      timestampGeneration: policies.defaultTimestampGenerator()
    },
    queryOptions: {
      fetchSize: 5000,
      prepare: false,
      captureStackTrace: false
    },
    protocolOptions: {
      port: 9042,
      maxSchemaAgreementWaitSeconds: 10,
      maxVersion: 0,
      noCompact: false
    },
    pooling: {
      heartBeatInterval: 30000,
      warmup: true
    },
    socketOptions: {
      connectTimeout: 5000,
      defunctReadTimeoutThreshold: 64,
      keepAlive: true,
      keepAliveDelay: 0,
      readTimeout: 12000,
      tcpNoDelay: true,
      coalescingThreshold: 65536
    },
    authProvider: null,
    requestTracker: null,
    metrics: new metrics.DefaultMetrics(),
    maxPrepared: 500,
    refreshSchemaDelay: 1000,
    isMetadataSyncEnabled: true,
    prepareOnAllHosts: true,
    rePrepareOnUp: true,
    encoding: {
      copyBuffer: true,
      useUndefinedAsUnset: true
    },
    monitorReporting: {
      enabled: true
    }
  });
}

/**
 * Extends and validates the user options
 * @param {Object} [baseOptions] The source object instance that will be overridden
 * @param {Object} userOptions
 * @returns {Object}
 */
function extend(baseOptions, userOptions) {
  if (arguments.length === 1) {
    userOptions = arguments[0];
    baseOptions = {};
  }
  const options = utils.deepExtend(baseOptions, defaultOptions(), userOptions);

  if (!options.cloud) {
    if (!Array.isArray(options.contactPoints) || options.contactPoints.length === 0) {
      throw new TypeError('Contacts points are not defined.');
    }

    for (let i = 0; i < options.contactPoints.length; i++) {
      const hostName = options.contactPoints[i];
      if (!hostName) {
        throw new TypeError(util.format('Contact point %s (%s) is not a valid host name, ' +
          'the following values are valid contact points: ipAddress, hostName or ipAddress:port', i, hostName));
      }
    }

    options.sni = undefined;
  } else {
    validateCloudOptions(options);
  }

  if (!options.logEmitter) {
    options.logEmitter = function () {};
  }
  if (!options.queryOptions) {
    throw new TypeError('queryOptions not defined in options');
  }

  if (options.requestTracker !== null && !(options.requestTracker instanceof tracker.RequestTracker)) {
    throw new TypeError('requestTracker must be an instance of RequestTracker');
  }

  if (!(options.metrics instanceof metrics.ClientMetrics)) {
    throw new TypeError('metrics must be an instance of ClientMetrics');
  }

  validatePoliciesOptions(options.policies);

  validateProtocolOptions(options.protocolOptions);

  validateSocketOptions(options.socketOptions);

  validateAuthenticationOptions(options);

  options.encoding = options.encoding || {};

  validateEncodingOptions(options.encoding);

  if (options.profiles && !Array.isArray(options.profiles)) {
    throw new TypeError('profiles must be an Array of ExecutionProfile instances');
  }

  validateApplicationInfo(options);

  validateMonitorReporting(options);

  return options;
}

/**
 * Validates the options to connect to a cloud instance.
 * @private
 */
function validateCloudOptions(options) {
  const bundle = options.cloud.secureConnectBundle;

  // eslint-disable-next-line no-undef
  if (!(typeof bundle === 'string' || (typeof URL !== 'undefined' && bundle instanceof URL))) {
    throw new TypeError('secureConnectBundle in cloud options must be of type string');
  }

  if (options.contactPoints) {
    throw new TypeError('Contact points can not be defined when cloud settings are provided');
  }

  if (options.sslOptions) {
    throw new TypeError('SSL options can not be defined when cloud settings are provided');
  }
}

/**
 * Validates the policies from the client options.
 * @param {ClientOptions.policies} policiesOptions
 * @private
 */
function validatePoliciesOptions(policiesOptions) {
  if (!policiesOptions) {
    throw new TypeError('policies not defined in options');
  }
  if (!(policiesOptions.loadBalancing instanceof policies.loadBalancing.LoadBalancingPolicy)) {
    throw new TypeError('Load balancing policy must be an instance of LoadBalancingPolicy');
  }
  if (!(policiesOptions.reconnection instanceof policies.reconnection.ReconnectionPolicy)) {
    throw new TypeError('Reconnection policy must be an instance of ReconnectionPolicy');
  }
  if (!(policiesOptions.retry instanceof policies.retry.RetryPolicy)) {
    throw new TypeError('Retry policy must be an instance of RetryPolicy');
  }
  if (!(policiesOptions.addressResolution instanceof policies.addressResolution.AddressTranslator)) {
    throw new TypeError('Address resolution policy must be an instance of AddressTranslator');
  }
  if (policiesOptions.timestampGeneration !== null &&
    !(policiesOptions.timestampGeneration instanceof policies.timestampGeneration.TimestampGenerator)) {
    throw new TypeError('Timestamp generation policy must be an instance of TimestampGenerator');
  }
}

/**
 * Validates the protocol options.
 * @param {ClientOptions.protocolOptions} protocolOptions
 * @private
 */
function validateProtocolOptions(protocolOptions) {
  if (!protocolOptions) {
    throw new TypeError('protocolOptions not defined in options');
  }
  const version = protocolOptions.maxVersion;
  if (version && (typeof version !== 'number' || !types.protocolVersion.isSupported(version))) {
    throw new TypeError(util.format('protocolOptions.maxVersion provided (%s) is invalid', version));
  }
}

/**
 * Validates the socket options.
 * @param {ClientOptions.socketOptions} socketOptions
 * @private
 */
function validateSocketOptions(socketOptions) {
  if (!socketOptions) {
    throw new TypeError('socketOptions not defined in options');
  }
  if (typeof socketOptions.readTimeout !== 'number') {
    throw new TypeError('socketOptions.readTimeout must be a Number');
  }
  if (typeof socketOptions.coalescingThreshold !== 'number' || socketOptions.coalescingThreshold <= 0) {
    throw new TypeError('socketOptions.coalescingThreshold must be a positive Number');
  }
}

/**
 * Validates authentication provider and credentials.
 * @param {ClientOptions} options
 * @private
 */
function validateAuthenticationOptions(options) {
  if (!options.authProvider) {
    const credentials = options.credentials;
    if (credentials) {
      if (typeof credentials.username !== 'string' || typeof credentials.password !== 'string') {
        throw new TypeError('credentials username and password must be a string');
      }

      options.authProvider = new auth.PlainTextAuthProvider(credentials.username, credentials.password);
    } else {
      options.authProvider = new auth.NoAuthProvider();
    }
  } else if (!(options.authProvider instanceof auth.AuthProvider)) {
    throw new TypeError('options.authProvider must be an instance of AuthProvider');
  }
}

/**
 * Validates the encoding options.
 * @param {ClientOptions.encoding} encodingOptions
 * @private
 */
function validateEncodingOptions(encodingOptions) {
  if (encodingOptions.map) {
    const mapConstructor = encodingOptions.map;
    if (typeof mapConstructor !== 'function' ||
      typeof mapConstructor.prototype.forEach !== 'function' ||
      typeof mapConstructor.prototype.set !== 'function') {
      throw new TypeError('Map constructor not valid');
    }
  }

  if (encodingOptions.set) {
    const setConstructor = encodingOptions.set;
    if (typeof setConstructor !== 'function' ||
      typeof setConstructor.prototype.forEach !== 'function' ||
      typeof setConstructor.prototype.add !== 'function') {
      throw new TypeError('Set constructor not valid');
    }
  }

  if ((encodingOptions.useBigIntAsLong || encodingOptions.useBigIntAsVarint) && typeof BigInt === 'undefined') {
    throw new TypeError('BigInt is not supported by the JavaScript engine');
  }
}

function validateApplicationInfo(options) {
  function validateString(key) {
    const str = options[key];

    if (str !== null && str !== undefined && typeof str !== 'string') {
      throw new TypeError(`${key} should be a String`);
    }
  }

  validateString('applicationName');
  validateString('applicationVersion');

  if (options.id !== null && options.id !== undefined && !(options.id instanceof types.Uuid)) {
    throw new TypeError('Client id must be a Uuid');
  }
}

function validateMonitorReporting(options) {
  const o = options.monitorReporting;
  if (o === null || typeof o !== 'object') {
    throw new TypeError(`Monitor reporting must be an object, obtained: ${o}`);
  }
}

/**
 * Sets the default options that depend on the protocol version and other metadata.
 * @param {Client} client
 */
function setMetadataDependent(client) {
  const version = client.controlConnection.protocolVersion;
  let coreConnectionsPerHost = coreConnectionsPerHostV3;
  let maxRequestsPerConnection = maxRequestsPerConnectionV3;

  if (!types.protocolVersion.uses2BytesStreamIds(version)) {
    coreConnectionsPerHost = coreConnectionsPerHostV2;
    maxRequestsPerConnection = maxRequestsPerConnectionV2;
  }

  if (client.options.queryOptions.consistency === undefined) {
    client.options.queryOptions.consistency =
      client.metadata.isDbaas() ? types.consistencies.localQuorum : types.consistencies.localOne;
  }

  client.options.pooling = utils.deepExtend(
    {}, { coreConnectionsPerHost, maxRequestsPerConnection }, client.options.pooling);
}

exports.extend = extend;
exports.defaultOptions = defaultOptions;
exports.coreConnectionsPerHostV2 = coreConnectionsPerHostV2;
exports.coreConnectionsPerHostV3 = coreConnectionsPerHostV3;
exports.maxRequestsPerConnectionV2 = maxRequestsPerConnectionV2;
exports.maxRequestsPerConnectionV3 = maxRequestsPerConnectionV3;
exports.setMetadataDependent = setMetadataDependent;
exports.continuousPageUnitBytes = continuousPageUnitBytes;
exports.continuousPageDefaultSize = continuousPageDefaultSize;
exports.continuousPageDefaultHighWaterMark = continuousPageDefaultHighWaterMark;
