// Copyright (c) 2016, 2024, Oracle and/or its affiliates.

//-----------------------------------------------------------------------------
//
// This software is dual-licensed to you under the Universal Permissive License
// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
// either license.
//
// If you elect to accept the software under the Apache License, Version 2.0,
// the following applies:
//
// Licensed 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
//
//    https://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 Connection = require('./connection.js');
const EventEmitter = require('events');
const constants = require('./constants.js');
const errors = require('./errors.js');
const settings = require('./settings.js');
const nodbUtil = require('./util.js');
const impl = require('./impl');
const PoolStatistics = require('./poolStatistics.js');


class Pool extends EventEmitter {

  constructor() {
    super();
    this._impl = new impl.PoolImpl();
    this._queueMax = 0;
    this._queueTimeout = 0;
    this._enableStatistics = false;
    this._timeOfReset = this._createdDate = Date.now();
    this._sessionCallback = undefined;
    this._connRequestQueue = [];
    this._connectionClass = settings.connectionClass;
  }

  //---------------------------------------------------------------------------
  // _checkPoolOpen()
  //
  // Check if the pool is open (not draining/reconfiguring/closed) and throw an
  // appropriate exception if not.
  //---------------------------------------------------------------------------
  _checkPoolOpen(ignoreReconfiguring) {
    if (this.status === constants.POOL_STATUS_DRAINING) {
      errors.throwErr(errors.ERR_POOL_CLOSING);
    } else if (this.status === constants.POOL_STATUS_CLOSED) {
      errors.throwErr(errors.ERR_POOL_CLOSED);
    } else if (!ignoreReconfiguring) {
      if (this.status === constants.POOL_STATUS_RECONFIGURING) {
        errors.throwErr(errors.ERR_POOL_RECONFIGURING);
      }
    }
  }

  //---------------------------------------------------------------------------
  // _checkRequestQueue()
  //
  // When a connection is returned to the pool, this method is called (via an
  // event handler) to determine when requests for connections should be
  // resumed and cancels any timeout that may have been associated with the
  // request. This method is also called from reconfigure() so that waiting
  // connection requests can be processed. Note the use of a local variable for
  // the number of connections out. This is because the connection requests will
  // not resume until after the loop is finished, and therefore the number of
  // connections the pool thinks is out will not be incremented.
  //---------------------------------------------------------------------------
  _checkRequestQueue() {
    let connectionsOut = this._connectionsOut;
    while (this._connRequestQueue.length > 0 && connectionsOut < this.poolMax) {
      connectionsOut += 1;
      const payload = this._connRequestQueue.shift();
      if (this._enableStatistics) {
        this._totalRequestsDequeued += 1;
        this._updateWaitStatistics(payload);
      }
      if (payload.timeoutHandle) {
        clearTimeout(payload.timeoutHandle);
      }
      // inform the waiter that processing can continue
      payload.resolve();
    }
  }

  //---------------------------------------------------------------------------
  // _enableStats (DEPRECATED)
  //
  // Property for whether statistics are enabled on the pool.
  //---------------------------------------------------------------------------
  get _enableStats() {
    return this._enableStatistics;
  }

  //---------------------------------------------------------------------------
  // _resetStatistics()
  //  To initialize the counters/timers
  //---------------------------------------------------------------------------
  _resetStatistics() {
    this._timeOfReset = Date.now();
    this._totalConnectionRequests = 0;
    this._totalRequestsEnqueued = 0;
    this._totalRequestsDequeued = 0;
    this._totalFailedRequests = 0;
    this._totalRequestsRejected = 0;
    this._totalRequestTimeouts = 0;
    this._maximumQueueLength = this._connRequestQueue.length;
    this._totalTimeInQueue = 0;
    this._minTimeInQueue = 0;
    this._maxTimeInQueue = 0;
  }

  //---------------------------------------------------------------------------
  // _setup()
  //
  // Sets up the pool instance with additional attributes used for logging
  // statistics and managing the connection queue.
  //---------------------------------------------------------------------------
  _setup(options, poolAlias) {
    this._queueTimeout = options.queueTimeout;
    this._queueMax = options.queueMax;
    this._enableStatistics = options.enableStatistics;
    this._edition = options.edition;
    this._eventsFlag = options.events;
    this._externalAuth = options.externalAuth;
    this._homogeneous = options.homogeneous;
    this._user = options.user;
    this._connectString = options.connectString;
    this._status = constants.POOL_STATUS_OPEN;
    this._connectionsOut = 0;
    this._poolAlias = poolAlias;

    // register event handler for when request queue should be checked
    this.on('_checkRequestQueue', this._checkRequestQueue);

    this._resetStatistics();

  }

  //---------------------------------------------------------------------------
  // _updateWaitStatistics()
  //
  // Update pool wait statistics after a connect request has spent some time in
  // the queue.
  //---------------------------------------------------------------------------
  _updateWaitStatistics(payload) {
    const waitTime = Date.now() - payload.enqueuedTime;
    this._totalTimeInQueue += waitTime;
    if (this._minTimeInQueue === 0) {
      this._minTimeInQueue = waitTime;
    } else {
      this._minTimeInQueue = Math.min(this._minTimeInQueue, waitTime);
    }
    this._maxTimeInQueue = Math.max(this._maxTimeInQueue, waitTime);
  }

  //---------------------------------------------------------------------------
  // _verifyGetConnectionOptions()
  //
  // Verify the getConnection() options are acceptable. Performs any
  // transformations that are needed before returning the options to the
  // caller.
  //---------------------------------------------------------------------------
  _verifyGetConnectionOptions(options) {

    // define normalized options (value returned to caller)
    const outOptions = {};

    // only one of "user" and "username" may be specified (and must be strings)
    if (options.user !== undefined) {
      errors.assertParamPropValue(typeof options.user === 'string', 1, "user");
      outOptions.user = options.user;
    }
    if (options.username !== undefined) {
      errors.assert(outOptions.user === undefined, errors.ERR_DBL_USER);
      errors.assertParamPropValue(typeof options.username === 'string', 1,
        "username");
      outOptions.user = options.username;
    }

    if (this.externalAuth &&
      outOptions.user && (outOptions.user[0] !== '['
        || outOptions.user.slice(-1) !== ']')) {
      // username is not enclosed in [].
      errors.throwErr(errors.ERR_WRONG_USER_FORMAT_EXTAUTH_PROXY);
    }

    // password must be a string
    if (options.password !== undefined) {
      errors.assertParamPropValue(typeof options.password === 'string', 1,
        "password");
      if (this.externalAuth) {
        errors.throwErr(errors.ERR_WRONG_CRED_FOR_EXTAUTH);
      }
      outOptions.password = options.password;
    }

    // tag must be a string
    if (options.tag !== undefined) {
      errors.assertParamPropValue(typeof options.tag === 'string', 1, "tag");
      outOptions.tag = options.tag;
    }

    // matchAnyTag must be a boolean
    if (options.matchAnyTag !== undefined) {
      errors.assertParamPropValue(typeof options.matchAnyTag === 'boolean', 1,
        "matchAnyTag");
      outOptions.matchAnyTag = options.matchAnyTag;
    }

    // shardingKey must be an array of values
    if (options.shardingKey !== undefined) {
      const value = options.shardingKey;
      errors.assertParamPropValue(nodbUtil.isShardingKey(value), 1,
        "shardingKey");
      outOptions.shardingKey = options.shardingKey;
    }

    // superShardingKey must be an array of values
    if (options.superShardingKey !== undefined) {
      const value = options.superShardingKey;
      errors.assertParamPropValue(nodbUtil.isShardingKey(value), 1,
        "superShardingKey");
      outOptions.superShardingKey = options.superShardingKey;
    }

    // privilege must be one of a set of named constants
    if (options.privilege !== undefined) {
      errors.assertParamPropValue(nodbUtil.isPrivilege(options.privilege), 1,
        "privilege");
      outOptions.privilege = options.privilege;
    }

    return outOptions;
  }

  //---------------------------------------------------------------------------
  // close()
  //
  // Close the pool, optionally allowing for a period of time to pass for
  // connections to "drain" from the pool.
  //---------------------------------------------------------------------------
  async close(a1) {
    let drainTime = 0;
    let forceClose = false;

    // check arguments
    errors.assertArgCount(arguments, 0, 1);
    if (arguments.length == 1) {

      // drain time must be a valid number; timeouts larger than a 32-bit signed
      // integer are not supported
      errors.assertParamValue(typeof a1 === 'number', 1);
      if (a1 < 0 || isNaN(a1) || a1 > 2 ** 31) {
        errors.throwErr(errors.ERR_INVALID_PARAMETER_VALUE, 1);
      }

      // no need to worry about drain time if no connections are out!
      forceClose = true;
      if (this._connectionsOut > 0) {
        drainTime = a1 * 1000;
      }

    }

    // if the pool is draining/reconfiguring/closed, throw an appropriate error
    this._checkPoolOpen(false);

    // wait for the pool to become empty or for the drain timeout to expire
    // (whichever comes first)
    if (drainTime > 0) {
      this._status = constants.POOL_STATUS_DRAINING;
      await new Promise(resolve => {
        const timeout = setTimeout(() => {
          this.removeAllListeners('_allCheckedIn');
          resolve();
        }, drainTime);
        this.once('_allCheckedIn', () => {
          clearTimeout(timeout);
          resolve();
        });
      });
    }

    // if any connections are still out and the pool is not being force closed,
    // throw an exception
    if (!forceClose && this._connectionsOut > 0) {
      errors.throwErr(errors.ERR_POOL_HAS_BUSY_CONNECTIONS);
    }

    // close the pool
    await this._impl.close();
    this._status = constants.POOL_STATUS_CLOSED;
    this.emit('_afterPoolClose');

  }

  //---------------------------------------------------------------------------
  // connectionsInUse
  //
  // Property for the number of connections in use by the pool.
  //---------------------------------------------------------------------------
  get connectionsInUse() {
    return this._impl.getConnectionsInUse();
  }

  //---------------------------------------------------------------------------
  // connectionsOpen
  //
  // Property for the number of connections opened by the pool.
  //---------------------------------------------------------------------------
  get connectionsOpen() {
    return this._impl.getConnectionsOpen();
  }

  //---------------------------------------------------------------------------
  // connectString
  //
  // Property for the connect string used to create the pool.
  //---------------------------------------------------------------------------
  get connectString() {
    return this._connectString;
  }

  //---------------------------------------------------------------------------
  // thin()
  //
  // return true, if driver mode is thin while creating pool
  // return false, if driver mode is thick while creating pool
  //---------------------------------------------------------------------------
  get thin() {
    return settings.thin;
  }

  //---------------------------------------------------------------------------
  // edition
  //
  // Property for the edition used to create the pool.
  //---------------------------------------------------------------------------
  get edition() {
    return this._edition;
  }

  //---------------------------------------------------------------------------
  // enableStatistics
  //
  // Property for whether statistics are enabled on the pool.
  //---------------------------------------------------------------------------
  get enableStatistics() {
    return this._enableStatistics;
  }

  //---------------------------------------------------------------------------
  // events
  //
  // Property for the events flag value used to create the pool.
  //---------------------------------------------------------------------------
  get events() {
    return this._eventsFlag;
  }

  //---------------------------------------------------------------------------
  // externalAuth
  //
  // Property for the externalAuth flag value used to create the pool.
  //---------------------------------------------------------------------------
  get externalAuth() {
    return this._externalAuth;
  }

  //---------------------------------------------------------------------------
  // getConnection()
  //
  // Gets a connection from the pool and returns it to the caller. If there are
  // fewer connections out than the poolMax setting, then the request will
  // return immediately; otherwise, the request will be queued for up to
  // queueTimeout milliseconds.
  //---------------------------------------------------------------------------
  async getConnection(a1) {
    let poolMax;
    let options = {};

    // check arguments
    errors.assertArgCount(arguments, 0, 1);
    if (arguments.length == 1) {
      errors.assertParamValue(nodbUtil.isObject(a1), 1);
      options = this._verifyGetConnectionOptions(a1);
    }

    // get connection class value from pool
    options.connectionClass = this._connectionClass;

    // if pool is draining/closed, throw an appropriate error
    this._checkPoolOpen(true);

    // manage stats, if applicable
    if (this._enableStatistics) {
      this._totalConnectionRequests += 1;
    }

    // getting the poolMax setting on the pool may fail if the pool is no
    // longer valid
    try {
      poolMax = this.poolMax;
    } catch (err) {
      if (this._enableStatistics) {
        this._totalFailedRequests += 1;
      }
      throw err;
    }

    while (this._connectionsOut >= poolMax ||
        this.status === constants.POOL_STATUS_RECONFIGURING) {

      // when the queue is huge, throw error early without waiting for queue
      // timeout
      if (this._connRequestQueue.length >= this._queueMax &&
          this._queueMax >= 0) {
        if (this._enableStatistics) {
          this._totalRequestsRejected += 1;
        }
        errors.throwErr(errors.ERR_QUEUE_MAX_EXCEEDED, this._queueMax);
      }

      // if too many connections are out, wait until room is made available or
      // the queue timeout expires
      await new Promise((resolve, reject) => {

        // set up a payload which will be added to the queue for processing
        const payload = { resolve: resolve, reject: reject };

        // if using a queue timeout, establish the timeout so that when it
        // expires the payload will be removed from the queue and an exception
        // thrown
        if (this._queueTimeout !== 0) {
          payload.timeoutHandle = setTimeout(() => {
            const ix = this._connRequestQueue.indexOf(payload);
            if (ix >= 0) {
              this._connRequestQueue.splice(ix, 1);
            }
            if (this._enableStatistics) {
              this._totalRequestTimeouts += 1;
              this._updateWaitStatistics(payload);
            }
            try {
              errors.throwErr(errors.ERR_CONN_REQUEST_TIMEOUT,
                this._queueTimeout);
            } catch (err) {
              reject(err);
            }
          }, this._queueTimeout);
        }

        // add payload to the queue
        this._connRequestQueue.push(payload);
        if (this._enableStatistics) {
          payload.enqueuedTime = Date.now();
          this._totalRequestsEnqueued += 1;
          this._maximumQueueLength = Math.max(this._maximumQueueLength,
            this._connRequestQueue.length);
        }

      });

      // check if pool is draining/closed after delay has
      // completed and throw an appropriate error
      this._checkPoolOpen(true);

    }

    // room is available in the queue, so proceed to acquire a connection from
    // the pool; adjust the connections out immediately in order to ensure that
    // another attempt doesn't proceed while this one is underway
    this._connectionsOut += 1;
    try {

      // acquire connection from the pool
      const conn = new Connection();
      conn._impl = await this._impl.getConnection(options);
      conn._pool = this;

      // invoke tag fixup callback method if one has been specified and the
      // actual tag on the connection doesn't match the one requested, or the
      // connection is freshly created; if the callback fails, close the
      // connection and remove it from the pool
      const requestedTag = options.tag || "";
      if (typeof this.sessionCallback === 'function' &&
          (conn._impl._newSession || conn.tag != requestedTag)) {
        try {
          await new Promise((resolve, reject) => {
            this.sessionCallback(conn, requestedTag, function(err) {
              if (err) {
                reject(err);
              } else {
                resolve();
              }
            });
          });
        } catch (err) {
          await conn.close({ drop: true });
          throw err;
        }
      }

      // when connection is closed, check to see if another request should be
      // processed and update any stats, as needed
      conn.on('_afterConnClose', () => {
        this._connectionsOut -= 1;
        this.emit('_checkRequestQueue');
        if (this._connectionsOut == 0) {
          this.emit('_allCheckedIn');
        }
      });

      return (conn);

    } catch (err) {
      this._connectionsOut -= 1;
      if (this._enableStatistics) {
        this._totalFailedRequests += 1;
      }
      this.emit('_checkRequestQueue');
      throw err;
    }

  }

  //---------------------------------------------------------------------------
  // getStatistics()
  //
  // Method to obtain a JSON object with all statistical metrics and pool
  // properties
  //---------------------------------------------------------------------------
  getStatistics() {
    this._checkPoolOpen(false);

    if (this._enableStatistics !== true) {
      return null;
    }
    return new PoolStatistics(this);
  }

  //---------------------------------------------------------------------------
  // homogeneous
  //
  // Property for the homogeneous flag value used to create the pool.
  //---------------------------------------------------------------------------
  get homogeneous() {
    return this._homogeneous;
  }

  //---------------------------------------------------------------------------
  // logStatistics()
  //
  // Method to print statistical related information and pool related
  // information when enableStatistics is set to true.
  //
  // NOTE: This function replaces the DEPRECATED _logStats() function.
  //---------------------------------------------------------------------------
  logStatistics() {
    const stats = this.getStatistics();
    if (stats === null) {
      errors.throwErr(errors.ERR_POOL_STATISTICS_DISABLED);
    }
    stats.logStatistics();
  }

  //---------------------------------------------------------------------------
  // poolAlias
  //
  // Property for the alias assigned to the pool.
  // ---------------------------------------------------------------------------
  get poolAlias() {
    return this._poolAlias;
  }

  //---------------------------------------------------------------------------
  // poolIncrement
  //
  // Property for the number of connections to create each time the pool needs
  // to grow.
  // ---------------------------------------------------------------------------
  get poolIncrement() {
    return this._impl.getPoolIncrement();
  }

  //---------------------------------------------------------------------------
  // poolMax
  //
  // Property for the maximum number of connections allowed in the pool.
  //---------------------------------------------------------------------------
  get poolMax() {
    return this._impl.getPoolMax();
  }

  //---------------------------------------------------------------------------
  // poolMaxPerShard
  //
  // Property for the maximum number of connections allowed in the pool for
  // each shard.
  //---------------------------------------------------------------------------
  get poolMaxPerShard() {
    return this._impl.getPoolMaxPerShard();
  }

  //---------------------------------------------------------------------------
  // poolMin
  //
  // Property for the minimum number of connections allowed in the pool.
  //---------------------------------------------------------------------------
  get poolMin() {
    return this._impl.getPoolMin();
  }

  //---------------------------------------------------------------------------
  // poolPingInterval
  //
  // Property for the ping interval to use for the pool.
  //---------------------------------------------------------------------------
  get poolPingInterval() {
    return this._impl.getPoolPingInterval();
  }

  //---------------------------------------------------------------------------
  // poolPingTimeout
  //
  // Property for the ping timeout associated with the pool.
  //---------------------------------------------------------------------------
  get poolPingTimeout() {
    return this._impl.getPoolPingTimeout();
  }

  //---------------------------------------------------------------------------
  // poolTimeout
  //
  // Property for the timeout associated with the pool.
  //---------------------------------------------------------------------------
  get poolTimeout() {
    return this._impl.getPoolTimeout();
  }

  //---------------------------------------------------------------------------
  // queueMax
  //
  // Property for the maximum number of pending pool connections that can be
  // queued.
  //---------------------------------------------------------------------------
  get queueMax() {
    return this._queueMax;
  }

  //---------------------------------------------------------------------------
  // queueTimeout
  //
  // Property for the milliseconds a connection request can spend in the queue
  // before an exception is thrown.
  //---------------------------------------------------------------------------
  get queueTimeout() {
    return this._queueTimeout;
  }

  //---------------------------------------------------------------------------
  // reconfigure()
  //
  // Reconfigure the pool, change the value for given pool-properties.
  //---------------------------------------------------------------------------
  async reconfigure(options) {

    // check arguments
    errors.assertArgCount(arguments, 1, 1);
    errors.assertParamValue(nodbUtil.isObject(options));
    errors.assertParamPropUnsignedInt(options, 1, "queueMax");
    errors.assertParamPropUnsignedInt(options, 1, "queueTimeout");
    errors.assertParamPropBool(options, 1, "enableStatistics");
    errors.assertParamPropBool(options, 1, "resetStatistics");
    errors.assertParamPropUnsignedInt(options, 1, "poolMin");
    errors.assertParamPropUnsignedIntNonZero(options, 1, "poolMax");
    errors.assertParamPropUnsignedInt(options, 1, "poolMaxPerShard");
    errors.assertParamPropUnsignedInt(options, 1, "poolIncrement");
    errors.assertParamPropInt(options, 1, "poolPingInterval");
    errors.assertParamPropUnsignedInt(options, 1, "poolTimeout");
    errors.assertParamPropUnsignedInt(options, 1, "stmtCacheSize");
    errors.assertParamPropBool(options, 1, "sodaMetaDataCache");

    // poolMax must be greater than or equal to poolMin
    if (options.poolMin > options.poolMax) {
      errors.throwErr(errors.ERR_INVALID_NUMBER_OF_CONNECTIONS, options.poolMax,
        options.poolMin);
    }

    // reconfiguration can happen only when status is OPEN
    this._checkPoolOpen(false);

    this._status = constants.POOL_STATUS_RECONFIGURING;
    try {
      // poolMin/poolMax/poolIncrement/poolPingInterval/poolTimeout/
      // poolMaxPerShard/stmtCacheSize/sodaMetaDataCache parameters
      await this._impl.reconfigure(options);

      // pool JS parameters: queueMax, queueTimeout, enableStatistics,
      // resetStatistics

      // reset the statistics-metrics only if 'resetStatistics' is true or
      // 'enableStatistics' is being set to true
      if (options.resetStatistics == true ||
          (options.enableStatistics == true &&
          this._enableStatistics == false)) {
        this._resetStatistics();
      }

      if (options.queueMax !== undefined) {
        this._queueMax = options.queueMax;
      }

      if (options.queueTimeout !== undefined) {
        this._queueTimeout = options.queueTimeout;
      }

      if (options.enableStatistics !== undefined) {
        this._enableStatistics = options.enableStatistics;
      }
    } finally {
      this._status = constants.POOL_STATUS_OPEN;
    }
    this.emit('_checkRequestQueue');
  }

  //---------------------------------------------------------------------------
  // sessionCallback
  //
  // Property for the session callback associated with the pool.
  //---------------------------------------------------------------------------
  get sessionCallback() {
    return this._sessionCallback;
  }

  //---------------------------------------------------------------------------
  // setAccessToken()
  //
  // Set parameters for token based authentication.
  //---------------------------------------------------------------------------
  async setAccessToken(options) {
    errors.assertArgCount(arguments, 1, 1);
    errors.assertParamValue(nodbUtil.isObject(options), 1);
    errors.assertParamPropString(options, 1, "token");
    errors.assertParamPropString(options, 1, "privateKey");
    await this._impl.setAccessToken(options);
  }

  //---------------------------------------------------------------------------
  // sodaMetaDataCache
  //
  // Property for whether the SODA metadata cache is enabled or not.
  //---------------------------------------------------------------------------
  get sodaMetaDataCache() {
    return this._impl.getSodaMetaDataCache();
  }

  //---------------------------------------------------------------------------
  // status
  //
  // Property for the pool's status.
  //---------------------------------------------------------------------------
  get status() {
    return this._status;
  }

  //---------------------------------------------------------------------------
  // stmtCacheSize
  //
  // Property for the size of the statement cache to use when creating
  // connections in the pool.
  //---------------------------------------------------------------------------
  get stmtCacheSize() {
    return this._impl.getStmtCacheSize();
  }

  //---------------------------------------------------------------------------
  // user
  //
  // Property for the user used to create the pool.
  //---------------------------------------------------------------------------
  get user() {
    return this._user;
  }

  //---------------------------------------------------------------------------
  // connectTraceConfig
  //
  // Property for getting the connection related config.
  //---------------------------------------------------------------------------
  get connectTraceConfig() {
    return this._impl && this._impl._getConnectTraceConfig();
  }
}

nodbUtil.wrapFns(Pool.prototype, false,
  "close",
  "getConnection",
  "reconfigure",
  "setAccessToken");

// DEPRECATED aliases
Pool.prototype.terminate = Pool.prototype.close;
Pool.prototype._logStats = Pool.prototype.logStatistics;

module.exports = Pool;
