// Copyright (c) 2015, 2022, Oracle and/or its affiliates.

//-----------------------------------------------------------------------------
//
// You may not use the identified files except in compliance with the Apache
// License, Version 2.0 (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 nodbUtil = require('./util.js');
const util = require('util');

// This version of node-oracledb works with Node.js 8.16, 10.16 or
// later.  The test stops hard-to-interpret runtime errors and crashes
// with older Node.js versions.  Also Node.js 8.16 and 10.16 (and
// 12.0) contain an important Node-API performance regression fix.  If
// you're using the obsolete Node.js 9 or 11 versions, you're on your
// own regarding performance and functionality
let vs = process.version.substring(1).split(".").map(Number);
if (vs[0] < 8 || (vs[0] === 8 && vs[1] < 16)) {
  throw new Error(nodbUtil.getErrorMessage('NJS-069', nodbUtil.PACKAGE_JSON_VERSION, "8.16"));
} else if ((vs[0] === 10 && vs[1] < 16)) {
  throw new Error(nodbUtil.getErrorMessage('NJS-069', nodbUtil.PACKAGE_JSON_VERSION, "10.16"));
}

const AqDeqOptions = require('./aqDeqOptions.js');
const AqEnqOptions = require('./aqEnqOptions.js');
const AqMessage = require('./aqMessage.js');
const AqQueue = require('./aqQueue.js');
const BaseDbObject = require('./dbObject.js');
const Connection = require('./connection.js');
const Lob = require('./lob.js');
const Pool = require('./pool.js');
const PoolStatistics = require('./poolStatistics.js');
const ResultSet = require('./resultset.js');
const SodaDatabase = require('./sodaDatabase.js');
const SodaCollection = require('./sodaCollection.js');
const SodaDocCursor = require('./sodaDocCursor.js');
const SodaDocument = require('./sodaDocument.js');
const SodaOperation = require('./sodaOperation.js');

let poolCache = {};
let tempUsedPoolAliases = {};
const defaultPoolAlias = 'default';

// Load the Oracledb binary

/*global __non_webpack_require__*/  // quieten eslint
const requireBinary = (typeof __non_webpack_require__ === 'function') ? __non_webpack_require__ : require; // See Issue 1156

const binaryLocations = [
  '../' + nodbUtil.RELEASE_DIR + '/' + nodbUtil.BINARY_FILE,  // pre-built binary
  '../' + nodbUtil.RELEASE_DIR + '/' + 'oracledb.node',       // binary built from source
  '../build/Debug/oracledb.node',                             // debug binary
  // For Webpack.  A Webpack copy plugin is still needed to copy 'node_modules/oracledb/build/' to the output directory
  // See https://github.com/oracle/node-oracledb/issues/1156
  './node_modules/oracledb/' + nodbUtil.RELEASE_DIR + '/' + nodbUtil.BINARY_FILE,
  './node_modules/oracledb/' + nodbUtil.RELEASE_DIR + '/' + 'oracledb.node'
];

let oracledbCLib;
for (let i = 0; i < binaryLocations.length; i++) {
  try {
    oracledbCLib = requireBinary(binaryLocations[i]);
    break;
  } catch (err) {
    if (err.code !== 'MODULE_NOT_FOUND' || i == binaryLocations.length - 1) {
      let nodeInfo;
      if (err.code === 'MODULE_NOT_FOUND') {
        // A binary was not found in any of the search directories.
        // Note this message may not be accurate for Webpack users since Webpack changes __dirname
        nodeInfo = `\n  Looked for ${binaryLocations.map(x => require('path').resolve(__dirname, x)).join(', ')}\n  ${nodbUtil.getInstallURL()}\n`;
      } else {
        nodeInfo = `\n  Node.js require('oracledb') error was:\n  ${err.message}\n  ${nodbUtil.getInstallHelp()}\n`;
      }
      throw new Error(nodbUtil.getErrorMessage('NJS-045', nodeInfo));
    }
  }
}


class OracleDb {

  constructor() {
    this.queueTimeout = 60000;
    this.queueMax     = 500;
    this.errorOnConcurrentExecute = false;
  }

  // extend class with promisified functions
  _extend(_oracledb) {
    this.getConnection = nodbUtil.callbackify(getConnection).bind(_oracledb);
    this.createPool = nodbUtil.callbackify(createPool).bind(_oracledb);
    this.shutdown = nodbUtil.callbackify(shutdown).bind(_oracledb);
    this.startup = nodbUtil.callbackify(startup).bind(_oracledb);
    this.initOracleClient = this.initOracleClient.bind(_oracledb);
    this._accessTokenHandler = this._accessTokenHandler.bind(_oracledb);
  }

  // temporary method for determining if an object is a date until
  // napi_is_date() can be used (when Node-API v5 can be used)
  _isDate(val) {
    return util.isDate(val);
  }

  // handler for access token callbacks
  async _accessTokenHandler(userFn, externalObj, refresh) {
    let accessToken;
    try {
      let result = userFn(refresh);
      if (result instanceof Promise) {
        result = await result;
      }

      _checkToken(result);
      accessToken = result;
    } finally {
      this._returnAccessToken(externalObj, accessToken);
    }
  }

  // retrieves a pool from the pool cache (synchronous method)
  getPool(poolAlias) {
    let pool;

    nodbUtil.checkArgCount(arguments, 0, 1);

    if (poolAlias) {
      nodbUtil.assert(typeof poolAlias === 'string' || typeof poolAlias === 'number', 'NJS-005', 1);
    }

    poolAlias = poolAlias || defaultPoolAlias;

    pool = poolCache[poolAlias];

    if (!pool) {
      throw new Error(nodbUtil.getErrorMessage('NJS-047', poolAlias));
    }

    return pool;
  }

  initOracleClient(arg1) {
    let options = {};
    nodbUtil.checkArgCount(arguments, 0, 1);
    if (arg1 !== undefined) {
      nodbUtil.assert(nodbUtil.isObject(arg1), 'NJS-005', 1);
      options = arg1;
    }
    this._initOracleClient(options);
  }

}

// Oracledb functions and classes

//-----------------------------------------------------------------------------
// createPool()
//   Create a pool with the specified options and return it to the caller.
//-----------------------------------------------------------------------------
async function createPool(poolAttrs) {
  let poolAlias;

  // check arguments
  nodbUtil.checkArgCount(arguments, 1, 1);
  nodbUtil.assert(nodbUtil.isObject(poolAttrs), 'NJS-005', 1);
  if (poolAttrs.poolAlias !== undefined) {
    if (typeof poolAttrs.poolAlias !== 'string' ||
        poolAttrs.poolAlias.length === 0) {
      throw new Error(nodbUtil.getErrorMessage('NJS-004',
        'poolAttrs.poolAlias'));
    }
    poolAlias = poolAttrs.poolAlias;
  } else if (poolAttrs.poolAlias === undefined
      && !poolCache[defaultPoolAlias]
      && !tempUsedPoolAliases[defaultPoolAlias]) {
    poolAlias = defaultPoolAlias;
  }
  if (poolCache[poolAlias] || tempUsedPoolAliases[poolAlias]) {
    throw new Error(nodbUtil.getErrorMessage('NJS-046', poolAlias));
  }

  if (poolAttrs.accessToken !== undefined) {
    // cannot set username or password for token based authentication
    if (poolAttrs.user !== undefined ||
        poolAttrs.password !== undefined) {
      throw new Error(nodbUtil.getErrorMessage('NJS-084'));
    }

    // homogeneous and externalAuth must be set to true for token based
    // authentication
    if (poolAttrs.homogeneous === false ||
        poolAttrs.externalAuth === false) {
      throw new Error(nodbUtil.getErrorMessage('NJS-085'));
    }
  }

  // create an adjusted set of pool attributes to pass to the C layer; the
  // session callback must be removed if it is a JavaScript function and the
  // queue timeout is used to specify the maximum amount of time that the C
  // layer will wait for a connection to be returned; ordinarily since the
  // JavaScript layer never calls the C layer to get a connection unless one is
  // known to be available, this should not be needed, but in some cases (such
  // as when the maximum for a particular shard is specified) this may not be
  // known, so this prevents an unnecessarily long wait from taking place
  const adjustedPoolAttrs = Object.defineProperties({},
    Object.getOwnPropertyDescriptors(poolAttrs));
  if (typeof poolAttrs.sessionCallback === 'function') {
    delete adjustedPoolAttrs.sessionCallback;
  }
  if (adjustedPoolAttrs.queueTimeout === undefined) {
    adjustedPoolAttrs.queueTimeout = this.queueTimeout;
  }

  // token based authentication
  if (poolAttrs.accessToken !== undefined) {
    // accessTokenCallback is depricated from node-oracledb 5.5
    if (poolAttrs.accessTokenCallback !== undefined &&
        typeof poolAttrs.accessToken === 'function') {
      throw new Error(nodbUtil.getErrorMessage('NJS-088'));
    }

    let accessToken;
    if (typeof poolAttrs.accessToken === 'function') {
      adjustedPoolAttrs.accessTokenCallback = poolAttrs.accessToken;
      accessToken = await poolAttrs.accessToken(false);

      if (_isTokenExpired(accessToken)) {
        accessToken = await poolAttrs.accessToken(true);
      }
    } else {
      accessToken = poolAttrs.accessToken;
    }
    _checkToken(accessToken, adjustedPoolAttrs);
  }

  // Need to prevent another call in the same stack from succeeding, otherwise
  // two pools could be created with the same poolAlias and the second one that
  // comes back would overwrite the first in the cache.
  if (poolAlias) {
    tempUsedPoolAliases[poolAlias] = true;
  }

  try {
    const pool = await this._createPool(adjustedPoolAttrs);

    if (poolAlias) {
      poolCache[poolAlias] = pool;

      // It's now safe to remove this alias from the tempUsedPoolAliases.
      delete tempUsedPoolAliases[poolAlias];
    }

    pool._setup(poolAttrs, poolAlias, this);
    pool.on('_afterPoolClose', () => {
      if (pool.poolAlias) {
        delete poolCache[pool.poolAlias];
      }
    });

    return pool;

  } catch (err) {

    // We need to free this up since the creation of the pool failed.
    if (poolAlias) {
      delete tempUsedPoolAliases[poolAlias];
    }

    // add installation help instructions to error message, if applicable
    if (err.message.match(/DPI-1047/)) {
      err.message += "\n" + nodbUtil.getInstallHelp();
    }

    throw err;
  }
}


//-----------------------------------------------------------------------------
// getConnection()
//   Gets either a standalone connection, or a connection from a pool cache
//-----------------------------------------------------------------------------
async function getConnection(a1) {
  let pool;
  let poolAlias;
  let connAttrs = {};

  // verify the number and types of arguments
  nodbUtil.checkArgCount(arguments, 0, 1);
  if (arguments.length == 0) {
    poolAlias = defaultPoolAlias;
  } else {
    nodbUtil.assert(typeof a1 === 'string' || nodbUtil.isObject(a1),
      'NJS-005', 1);
    if (typeof a1 === 'string') {
      poolAlias = a1;
    } else {
      connAttrs = a1;
      if (connAttrs.poolAlias) {
        poolAlias = connAttrs.poolAlias;
      }
    }
  }

  // if a pool alias is available, acquire a connection from the specified pool
  if (poolAlias) {
    pool = poolCache[poolAlias];
    if (!pool) {
      throw new Error(nodbUtil.getErrorMessage('NJS-047', poolAlias));
    }
    return await pool.getConnection(connAttrs);

  // otherwise, create a new standalone connection
  } else {
    if (connAttrs.accessToken !== undefined) {
      // cannot set username or password for token based authentication
      if (connAttrs.user !== undefined ||
          connAttrs.password !== undefined) {
        throw new Error(nodbUtil.getErrorMessage('NJS-084'));
      }

      // externalAuth must be set to true for token based authentication
      if (connAttrs.externalAuth === false) {
        throw new Error(nodbUtil.getErrorMessage('NJS-086'));
      }

      let accessToken;
      if (typeof connAttrs.accessToken === 'function') {
        accessToken = await connAttrs.accessToken(false);

        if (_isTokenExpired(accessToken)) {
          accessToken = await connAttrs.accessToken(true);
        }
      } else {
        accessToken = connAttrs.accessToken;
      }
      _checkToken(accessToken, connAttrs);
    }

    try {
      return await this._getConnection(connAttrs);
    } catch (err) {
      if (err.message.match(/DPI-1047/)) {
        err.message += "\n" + nodbUtil.getInstallHelp();
      }
      throw err;
    }
  }
}


//-----------------------------------------------------------------------------
// shutdown()
//   Shuts down the database.
//-----------------------------------------------------------------------------
async function shutdown(a1, a2) {
  let connAttr = {};
  let shutdownMode = this.SHUTDOWN_MODE_DEFAULT;

  // verify the number and types of arguments
  nodbUtil.checkArgCount(arguments, 0, 2);
  if (arguments.length == 2) {
    nodbUtil.assert(typeof a1 === 'object', 'NJS-005', 1);
    nodbUtil.assert(typeof a2 === 'number', 'NJS-005', 2);
    connAttr = a1;
    shutdownMode = a2;
  } else if (arguments.length == 1) {
    nodbUtil.assert(typeof a1 === 'object', 'NJS-005', 1);
    connAttr = a1;
  }

  // only look for the keys that are used for shutting down the database
  // use SYSOPER privilege
  const dbConfig = {
    user: connAttr.user,
    password: connAttr.password,
    connectString: connAttr.connectString,
    connectionString: connAttr.connectionString,
    externalAuth: connAttr.externalAuth,
    privilege: this.SYSOPER
  };

  const conn = await this.getConnection(dbConfig);
  await conn.shutdown(shutdownMode);
  if (shutdownMode != this.SHUTDOWN_MODE_ABORT) {
    await conn.execute("ALTER DATABASE CLOSE");
    await conn.execute("ALTER DATABASE DISMOUNT");
    await conn.shutdown(this.SHUTDOWN_MODE_FINAL);
  }
  await conn.close();
}


//-----------------------------------------------------------------------------
// startup()
//   Starts up the database.
//-----------------------------------------------------------------------------
async function startup(a1, a2) {
  let connAttr = {};
  let startupAttr = {};

  // verify the number and types of arguments
  nodbUtil.checkArgCount(arguments, 0, 2);
  if (arguments.length == 2) {
    nodbUtil.assert (typeof a1 === 'object', 'NJS-005', 1);
    nodbUtil.assert (typeof a2 === 'object', 'NJS-005', 2);
    connAttr = a1;
    startupAttr = a2;
  } else if (arguments.length == 1) {
    nodbUtil.assert(typeof a1 === 'object', 'NJS-005', 1);
    connAttr = a1;
  }

  // only look for the keys that are used for starting up the database
  // use SYSOPER and SYSPRELIM privileges
  const dbConfig = {
    user: connAttr.user,
    password: connAttr.password,
    connectString: connAttr.connectString,
    connectionString: connAttr.connectionString,
    externalAuth: connAttr.externalAuth,
    privilege: this.SYSOPER | this.SYSPRELIM
  };

  let conn = await this.getConnection(dbConfig);
  await conn.startup(startupAttr);
  await conn.close();

  dbConfig.privilege = this.SYSOPER;
  conn = await this.getConnection(dbConfig);
  await conn.execute("ALTER DATABASE MOUNT");
  await conn.execute("ALTER DATABASE OPEN");
  await conn.close();
}


//-----------------------------------------------------------------------------
// _isTokenExpiredUtil()
//   Function to check validity and expiry of token
//-----------------------------------------------------------------------------
function _isTokenExpiredUtil(accessToken) {
  nodbUtil.assert(typeof accessToken === 'string', 'NJS-084', 1);

  if (accessToken.split('.')[1] === undefined) {
    throw new Error(nodbUtil.getErrorMessage('NJS-084'));
  }

  const base64Url = accessToken.split('.')[1];
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
  const buff = Buffer.from(base64, 'base64');
  const payloadInit = buff.toString('ascii');

  let expiry = JSON.parse(payloadInit).exp;
  nodbUtil.assert(expiry != undefined, 'NJS-084');
  expiry = expiry * 1000;

  return (new Date().getTime() > expiry);
}


//-----------------------------------------------------------------------------
// _isTokenExpired()
//   Function to check validity of token parameter
//-----------------------------------------------------------------------------
function _isTokenExpired(accessToken) {
  switch (typeof accessToken) {
    case 'string':
      if (accessToken === '') {
        throw new Error(nodbUtil.getErrorMessage('NJS-084'));
      }

      return _isTokenExpiredUtil(accessToken);
    case 'object':
      if (accessToken.token === undefined ||
          accessToken.token === '' ||
          accessToken.privateKey === undefined ||
          accessToken.privateKey === '') {
        throw new Error(nodbUtil.getErrorMessage('NJS-084'));
      }

      return _isTokenExpiredUtil(accessToken.token);
    default:
      throw new Error(nodbUtil.getErrorMessage('NJS-084'));
  }
}


//-----------------------------------------------------------------------------
// _checkToken()
//   Function to check validity of token parameter
//-----------------------------------------------------------------------------
function _checkToken(accessToken, attrs) {
  if (_isTokenExpired(accessToken)) {
    throw new Error(nodbUtil.getErrorMessage('NJS-087'));
  }

  if (attrs === undefined)
    return;

  if (typeof accessToken === 'string') {
    attrs.token = accessToken;
  } else {
    attrs.token = accessToken.token;
    attrs.privateKey = accessToken.privateKey;
  }
}


// create instance which will be exported
let oracleDbInst = new OracleDb();

// add classes to prototype
let proto = Object.getPrototypeOf(oracleDbInst);
proto.OracleDb = OracleDb;
proto.AqDeqOptions = AqDeqOptions;
proto.AqEnqOptions = AqEnqOptions;
proto.AqMessage = AqMessage;
proto.AqQueue = AqQueue;
proto.BaseDbObject = BaseDbObject;
proto.Connection = Connection;
proto.Lob = Lob;
proto.Pool = Pool;
proto.PoolStatistics = PoolStatistics;
proto.ResultSet = ResultSet;
proto.SodaDatabase = SodaDatabase;
proto.SodaCollection = SodaCollection;
proto.SodaDocCursor = SodaDocCursor;
proto.SodaDocument = SodaDocument;
proto.SodaOperation = SodaOperation;

// call C to extend classes
oracledbCLib.init(oracleDbInst);

module.exports = oracleDbInst;
