/* Required libraries.  */
/* global g_redis log */
/* eslint class-methods-use-this:warn */
/* eslint consistent-return:warn */
/* eslint import/no-dynamic-require:warn */
/* eslint no-underscore-dangle: [2, { "allow": ["_id"] }] */
/* eslint no-unused-vars:warn */
/* eslint no-use-before-define:warn */
/* eslint prefer-destructuring:warn */

const fs = require('fs-extra');
const uuid = require('uuid');
const path = require('path');

/* Fetch in the other needed components for the this Adaptor */
const { MongoClient } = require('mongodb');

// Other global variables
let adapterDir = '';
let saveMetric = false;
let id = null;

/* DB UTILS INTERNAL FUNCTIONS         */
/** getFromJson
 * @summary returns information from a json storage file
 */
function getFromJson(fileName, ent, act, filter) {
  const origin = `${id}-dbUtil-getFromJson`;
  log.trace(origin);
  let storDir = `${adapterDir}/storage`;

  // if we were provided with a path to save metrics - instead of just a boolean
  if (saveMetric && typeof saveMetric === 'string' && saveMetric !== '') {
    storDir = saveMetric;
  }

  // make sure what we need exists and has been provided
  if (!fs.existsSync(storDir)) {
    log.warn(`${origin}: Could not find adapter storage directory - nothing to retrieve`);
    return null;
  }
  if (!fileName) {
    log.warn(`${origin}: Must provide a file name`);
    return null;
  }
  if ((ent && !act) || (!ent && act)) {
    log.warn(`${origin}: provided with one of entity/action, require both or neither`);
    return null;
  }

  // determine if the file exists so we can retrieve the data
  if (fs.existsSync(`${storDir}/${fileName}.json`)) {
    // have to read, append & save
    const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
    const toReturn = [];
    if (ent && act) {
      content.table.forEach((item) => {
        if (ent === item.entity && act === item.action) {
          toReturn.push(item);
        }
      });
    } else {
      const key = Object.keys(filter);
      content.table.forEach((item) => {
        let push = true;
        key.forEach((fil) => {
          if (filter[fil] !== item[fil]) push = false;
        });
        if (push) toReturn.push(item);
      });
    }
    const filtered = content.table.filter((el) => {
      Object.keys(filter).forEach((obj) => {
        if (el[obj] !== filter[obj]) {
          return false;
        }
      });
      return true;
    });
    return toReturn;
  }
  // if no file, nothing to return
  return null;
}

/** countJSON
 * @summary returns a count from a json storage file
 */
function countJSON(fileName, ent, act, filter) {
  const origin = `${id}-dbUtil-countJSON`;
  log.trace(origin);
  let storDir = `${adapterDir}/storage`;

  // if we were provided with a path to save metrics - instead of just a boolean
  if (saveMetric && typeof saveMetric === 'string' && saveMetric !== '') {
    storDir = saveMetric;
  }

  // make sure what we need exists and has been provided
  if (!fs.existsSync(`${storDir}`)) {
    log.warn(`${origin}: Could not find adapter storage directory - nothing to retrieve`);
    return null;
  }
  if (!fileName) {
    log.warn(`${origin}: Must provide a file name`);
    return null;
  }
  if ((ent && !act) || (!ent && act)) {
    log.warn(`${origin}: provided with one of entity/action, require both or neither`);
    return null;
  }

  // determine if the file exists so we can retrieve the data
  if (fs.existsSync(`${storDir}/${fileName}.json`)) {
    const data = getFromJson(fileName, ent, act, filter);
    if (data) {
      return data.length;
    }
    return -1;
  }
}

/** deleteJSON
 * @summary deletes a json storage file
 */
function deleteJSON(fileName) {
  const origin = `${id}-dbUtil-deleteJSON`;
  log.trace(origin);
  let storDir = `${adapterDir}/storage`;

  // if we were provided with a path to save metrics - instead of just a boolean
  if (saveMetric && typeof saveMetric === 'string' && saveMetric !== '') {
    storDir = saveMetric;
  }

  // make sure what we need exists and has been provided
  if (!fs.existsSync(`${storDir}`)) {
    log.warn(`${origin}: Could not find adapter storage directory - nothing to retrieve`);
    return null;
  }
  if (!fileName) {
    log.warn(`${origin}: Must provide a file name`);
    return null;
  }

  // determine if the file exists so we remove
  if (fs.existsSync(`${storDir}/${fileName}.json`)) {
    fs.remove(`${storDir}/${fileName}.json`).catch((some) => {
      log.info(`${origin}: ${some}`);
      fs.rmdirSync(`${storDir}/${fileName}.json`);
    });
  }
  return fileName;
}

/** removeFromJSON
 * @summary removes information from a json storage file
 */
function removeFromJSON(fileName, ent, act, filter, multiple) {
  const origin = `${id}-dbUtil-removeFromJSON`;
  log.trace(origin);
  let storDir = `${adapterDir}/storage`;

  // if we were provided with a path to save metrics - instead of just a boolean
  if (saveMetric && typeof saveMetric === 'string' && saveMetric !== '') {
    storDir = saveMetric;
  }

  // make sure what we need exists and has been provided
  if (!fs.existsSync(`${storDir}`)) {
    log.warn(`${origin}: Could not find adapter storage directory - nothing to retrieve`);
    return null;
  }
  if (!fileName) {
    log.warn(`${origin}: Must provide a file name`);
    return null;
  }
  if ((ent && !act) || (!ent && act)) {
    log.warn(`${origin}: provided with one of entity/action, require both or neither`);
    return null;
  }

  // determine if the file exists so we remove data from it
  if (fs.existsSync(`${storDir}/${fileName}.json`)) {
    const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
    const toReturn = [];
    if (ent && act) {
      content.table = content.table.filter((item) => {
        if (ent === item.entity && act === item.action) {
          toReturn.push(item);
          return false;
        }
        return true;
      });
    } else {
      let ctr = 0;
      const removed = content.table.filter((el) => {
        Object.keys(filter).forEach((obj) => {
          if (el[obj] !== filter[obj]) {
            return false;
          }
        });
        ctr += 1;
        if (!multiple && ctr > 1) {
          return false;
        }
        return true;
      });
      let ctr1 = 0;
      content.table = content.table.filter((el, i) => {
        Object.keys(filter).forEach((obj) => {
          if (el[obj] !== filter[obj]) {
            return true;
          }
        });
        ctr1 += 1;
        if (!multiple && ctr1 > 1) {
          return true;
        }
        return false;
      });
      fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
      return removed;
    }

    fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
    return toReturn;
  }
  log.error(`${origin}: Collection ${fileName} does not exist`);
  return null;
}

/** saveAsJson
 * @summary saves information into a json storage file
 */
function saveAsJson(fileName, data, ent, act) {
  const origin = `${id}-dbUtil-saveAsJson`;
  log.trace(origin);
  let storDir = `${adapterDir}/storage`;

  // if we were provided with a path to save metrics - instead of just a boolean
  if (saveMetric && typeof saveMetric === 'string' && saveMetric !== '') {
    storDir = saveMetric;
  }

  // make sure what we need exists and has been provided
  if (!fs.existsSync(`${storDir}`)) {
    try {
      // need to make the storage directory if it does not exist
      fs.mkdirSync(`${storDir}`);
    } catch (storEx) {
      log.warn(`${origin}: Could not create storage directory - can not store metrics`);
      return null;
    }
  }
  if (!fileName) {
    log.warn(`${origin}: Must provide a file name`);
    return null;
  }
  if ((ent && !act) || (!ent && act)) {
    log.warn(`${origin}: provided with one of entity/action, require both or neither`);
    return null;
  }

  if (fs.existsSync(`${storDir}/${fileName}.json`)) {
    // have to read, append & save
    const content = JSON.parse(fs.readFileSync(`${storDir}/${fileName}.json`, 'utf-8'));
    let exists = false;
    content.table.forEach((item) => {
      const toPush = item;
      if (ent === item.entity && act === item.action) {
        exists = true;
        Object.keys(data).forEach((key) => {
          if (key === '$inc') {
            Object.keys(data[key]).forEach((inc) => {
              if (!toPush[inc]) {
                toPush[inc] = data[key][inc];
              }
              toPush[inc] += data[key][inc];
            });
          } else if (key === '$set') {
            Object.keys(data[key]).forEach((set) => {
              toPush[set] = data[key][set];
            });
          }
        });
      }
    });
    if (!exists) {
      // push new thing to table.
      const toPush = {};
      const keysArray = Object.keys(data);
      keysArray.forEach((i) => {
        const newKeys = Object.keys(data[i]);
        newKeys.forEach((j) => {
          toPush[j] = data[i][j];
        });
      });
      content.table.push(toPush);
    }
    fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(content, null, 2));
    return data;
  }
  const obj = { table: [] };
  const toPush = {};
  const keysArray = Object.keys(data);
  keysArray.forEach((outer) => {
    const newKeys = Object.keys(data[outer]);
    newKeys.forEach((inner) => {
      toPush[inner] = data[outer][inner];
    });
  });
  obj.table.push(toPush);
  fs.writeFileSync(`${storDir}/${fileName}.json`, JSON.stringify(obj, null, 2));
  return data;
}

class DBUtil {
  /**
   * Used to add & modify metrics in the database from connectorRest.js
   * @constructor
   */
  constructor(prongId, properties, directory) {
    this.myid = prongId;
    id = prongId;
    this.baseDir = directory;
    adapterDir = this.baseDir;
    this.props = properties;
    saveMetric = this.props.save_metric || false;

    // set up the properties I care about
    this.refreshProperties(properties);
  }

  /**
   * refreshProperties is used to set up all of the properties for the db utils.
   * It allows properties to be changed later by simply calling refreshProperties rather
   * than having to restart the db utils.
   *
   * @function refreshProperties
   * @param {Object} properties - an object containing all of the properties
   */
  refreshProperties(properties) {
    const origin = `${this.myid}-dbUtil-refreshProperties`;
    log.trace(origin);

    if (!properties) {
      log.error(`${origin}: DB Utils received no properties!`);
      return;
    }

    this.dburl = null;
    this.dboptions = {};
    this.database = this.myid;

    // are we using a database - must have mongo properties and a host
    if (properties.mongo && properties.mongo.host) {
      // set the database port
      let port = 27017;
      if (properties.mongo.port) {
        port = properties.mongo.port;
      }
      // set the database
      if (properties.mongo.database) {
        this.database = properties.mongo.database;
      }
      // set the user
      let username = null;
      if (properties.mongo.username) {
        username = properties.mongo.username;
      }
      // set the password
      let password = null;
      if (properties.mongo.password) {
        password = properties.mongo.password;
      }

      // format the database url
      this.dburl = 'mongodb://';

      if (username) {
        this.dburl += `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`;
      }
      this.dburl += `${encodeURIComponent(properties.mongo.host)}:${encodeURIComponent(port)}/${encodeURIComponent(this.database)}`;

      // are we using a replication set need to add it to the url
      if (properties.mongo.replSet) {
        this.dburl += `?${properties.mongo.replSet}`;
      }

      // Do we need SSL to connect to the database
      if (properties.mongo.db_ssl && properties.mongo.db_ssl.enabled === true) {
        log.info(`${origin}: Connecting to MongoDB with SSL.`);
        this.dboptions.ssl = true;

        // validate the server's certificate against a known certificate authority?
        if (properties.mongo.db_ssl.accept_invalid_cert === false) {
          this.dboptions.sslValidate = true;
          log.info(`${origin}: Certificate based SSL MongoDB connections will be used.`);
          // if validation is enabled, we need to read the CA file
          if (properties.mongo.db_ssl.ca_file) {
            try {
              this.dboptions.sslCA = [fs.readFileSync(properties.mongo.db_ssl.ca_file)];
            } catch (err) {
              log.error(`${origin}: Error: Unable to load Mongo CA file: ${err}`);
            }
          } else {
            log.error(`${origin}: Error: Certificate validation is enabled but a CA is not specified.`);
          }
          if (properties.mongo.db_ssl.key_file) {
            try {
              this.dboptions.sslKey = [fs.readFileSync(properties.mongo.db_ssl.key_file)];
            } catch (err) {
              log.error(`${origin}: Error: Unable to load Mongo Key file: ${err}`);
            }
          }
          if (properties.mongo.db_ssl.cert_file) {
            try {
              this.dboptions.sslCert = [fs.readFileSync(properties.mongo.db_ssl.cert_file)];
            } catch (err) {
              log.error(`${origin}: Error: Unable to load Mongo Certificate file: ${err}`);
            }
          }
        } else {
          this.dboptions.sslValidate = false;
          log.info(`${origin}: SSL MongoDB connection without CA certificate validation.`);
        }
      } else {
        log.warn(`${origin}: WARNING: Connecting to MongoDB without SSL.`);
      }
    }
  }

  /**
   * createCollection creates the provided collection in the file system or database.
   *
   * @function createCollection
   * @param {String} collectionName - the name of the collection to create
   */
  createCollection(collectionName, callback) {
    const origin = `${this.myid}-dbUtil-createCollection`;
    log.trace(origin);

    try {
      // verify the required data has been provided
      if (!collectionName || (typeof collectionName !== 'string')) {
        log.warn(`${origin}: Missing Collection Name or not string`);
        return callback(`${origin}: Missing Collection Name or not string`, null);
      }

      // if using file storage
      if (this.dburl === null) {
        // if the collection already exists
        if (fs.existsSync(`${adapterDir}/storage/${collectionName}`)) {
          log.trace(`${origin}: storage file collection already exists`);
          return callback(null, collectionName);
        }
        // build the pieces that are needed for collection storage
        if (!fs.existsSync(`${adapterDir}/storage`)) {
          if (!fs.existsSync(`${adapterDir}`)) {
            fs.mkdirSync(`${adapterDir}`);
          }
          fs.mkdirSync(`${adapterDir}/storage`);
        }
        fs.mkdirSync(`${adapterDir}/storage/${collectionName}`);
        log.trace(`${origin}: storage file collection ${collectionName} created`);
        return callback(null, collectionName);
      }

      // work with the provided database
      return MongoClient.connect(this.dburl, this.dboptions, (error, db) => {
        if (error) {
          // No Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        // Add the collection to the database
        return db.db(this.database).createCollection(collectionName, (err, res) => {
          db.close();
          if (err) {
            // error we get back if the collection already existed - not a true error
            if (err.codeName === 'NamespaceExists') {
              log.trace(`${origin}: database collection already exists`);
              return callback(null, collectionName);
            }
            log.warn(`${origin}: Error creating collection ${error}`);
            return callback(`${origin}: Error creating collection ${error}`, null);
          }
          log.trace(`${origin}: database collection ${collectionName} created`);
          return callback(null, collectionName);
        });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * removeCollection removes the provided collection from the file system or database.
   *
   * @function removeCollection
   * @param {String} collectionName - the name of the collection to remove
   */
  removeCollection(collectionName, callback) {
    const origin = `${this.myid}-dbUtil-removeCollection`;
    log.trace(origin);

    try {
      // verify the required data has been provided
      if (!collectionName || (typeof collectionName !== 'string')) {
        log.warn(`${origin}: Missing Collection Name or not string`);
        return callback(`${origin}: Missing Collection Name or not string`, null);
      }

      // if using file storage
      if (this.dburl === null) {
        const deld = deleteJSON(collectionName);
        if (deld) {
          log.trace(`${origin}: storage file collection ${collectionName} removed`);
          return callback(null, deld);
        }
        log.trace(`${origin}: could not remove storage file collection`);
        return callback(`${origin}: could not remove storage file collection`, null);
      }

      // work with the provided database
      return MongoClient.connect(this.dburl, this.dboptions, (error, db) => {
        if (error) {
          // no Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        // get the list of collections from the database
        db.db(this.database).listCollections().toArray((err, result) => {
          if (err) {
            log.warn(`${origin}: Failed to get collections ${err}`);
            return callback(`${origin}: Failed to get collections ${err}`, null);
          }
          // go through the collections to get the correct one for removal
          result.forEach((elem) => {
            if (elem.name === collectionName) {
              // now that we found it, remove it
              return db.db(this.database).collection(collectionName).drop({}, (err1, res) => {
                db.close();
                if (err1) {
                  log.warn(`${origin}: Failed to remove collection ${err1}`);
                  return callback(`${origin}: Failed to remove collection ${err1}`, null);
                }
                log.trace(`${origin}: database collection ${collectionName} removed`);
                return callback(null, collectionName);
              });
            }
          });
          db.close();
          return callback(null, collectionName);
        });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * Call to create an item in the database
   *
   * @function create
   * @param {String} collectionName - the collection to save the item in. (required)
   * @param {String} data - the modification to make. (required)
   */
  create(collectionName, data, callback) {
    const origin = `${this.myid}-dbUtil-create`;
    log.trace(origin);

    try {
      // verify the required data has been provided
      if (!collectionName) {
        log.warn(`${origin}: Missing Collection Name`);
        return callback(`${origin}: Missing Collection Name`, null);
      }
      if (!data) {
        log.warn(`${origin}: Missing data to add`);
        return callback(`${origin}: Missing data to add`, null);
      }
      if ((data.entity && !data.action) || (!data.entity && data.action)) {
        log.warn(`${origin}: Inconsistent entity/action set`);
        return callback(`${origin}: Inconsistent entity/action set`, null);
      }

      const dataInfo = data;
      if (!{}.hasOwnProperty.call(dataInfo, '_id')) {
        dataInfo._id = uuid.v4();
      }

      // if using file storage
      if (this.dburl === null) {
        // save it to file in the adapter storage directory
        const saved = saveAsJson(collectionName, data, data.entity, data.action);
        if (!saved) {
          log.warn(`${origin}: Data has not been saved to file storage`);
          return callback(`${origin}: Data has not been saved to file storage`, null);
        }
        log.trace(`${origin}: Data saved in file storage`);
        return callback(null, saved);
      }

      // work with the provided database
      return MongoClient.connect(this.dburl, this.dboptions, (error, db) => {
        if (error) {
          // no Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        // Add the data to the database
        // insertOne has only 2 parameters: the data to be added & callback. Not an identifier.
        return db.db(this.database).collection(collectionName).insertOne(dataInfo, (err, result) => {
          db.close();
          if (err) {
            log.warn(`${origin}: Failed to insert data in collection ${err}`);
            return callback(`${origin}: Failed to insert data in collection ${err}`, null);
          }
          log.trace(`${origin}: Data saved in database`);
          return callback(null, data);
        });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * Call to create an index in the database
   *
   * @function createIndex
   * @param {String} collectionName - the collection to index. (required)
   * @param {String} fieldOrSpec - what to index. (required)
   */
  createIndex(collectionName, fieldOrSpec, options, callback) {
    const origin = `${this.myid}-dbUtil-createIndex`;
    log.trace(origin);

    try {
      // verify the required data has been provided
      if (!collectionName) {
        log.warn(`${origin}: Missing Collection Name`);
        return callback(`${origin}: Missing Collection Name`, null);
      }
      if (!fieldOrSpec) {
        log.warn(`${origin}: Missing Specs`);
        return callback(`${origin}: Missing Specs`, null);
      }

      // if using file storage
      if (this.dburl === null) {
        // no database - no index
        log.warn(`${origin}: No database - no index`);
        return callback(`${origin}: No database - no index`, null);
      }

      // work with the provided database
      return MongoClient.connect(this.dburl, this.dboptions, (error, db) => {
        if (error) {
          // no Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        // create the index on the collection
        return db.db(this.database).collection(collectionName).createIndex(fieldOrSpec, options || {}, (err, res) => {
          db.close();
          if (err) {
            log.warn(`${origin}: Failed to index data in collection ${err}`);
            return callback(`${origin}: Failed to index data in collection ${err}`, null);
          }
          log.trace(`${origin}: Data in collection indexed`);
          return callback(null, res);
        });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * Call to count the documents in a collection
   *
   * @function countDocuments
   * @param {String} collectionName - the collection to count documents in. (required)
   * @param {Object} query - the query to minimize documents you count. (required)
   */
  countDocuments(collectionName, query, options, callback) {
    const origin = `${this.myid}-dbUtil-countDocuments`;
    log.trace(origin);

    try {
      if (!collectionName) {
        log.warn(`${origin}: Missing Collection Name`);
        return callback(`${origin}: Missing Collection Name`, null);
      }

      // if using file storage
      if (this.dburl === null) {
        const data = countJSON(collectionName, null, null, query);
        if (!data || data === -1) {
          log.warn(`${origin}: Could not count data from file storage`);
          return callback(`${origin}: Could not count data from file storage`, null);
        }
        log.trace(`${origin}: Count from file storage ${data}`);
        return callback(null, data);
      }

      // work with the provided database
      return MongoClient.connect(this.dburl, this.dboptions, (error, db) => {
        if (error) {
          // no Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        // get the count from mongo
        return db.db(this.database).collection(collectionName).count(query, options, (err, res) => {
          db.close();
          if (err) {
            log.warn(`${origin}: Failed to count collection ${err}`);
            return callback(`${origin}: Failed to count collection ${err}`, null);
          }
          log.trace(`${origin}: Count from database ${res}`);
          return callback(null, res);
        });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * Delete items from a collection
   *
   * @function delete
   * @param {String} collectionName - the collection to remove document from. (required)
   * @param {Object} filter - the filter for the document(s) to remove. (required)
   */
  delete(collectionName, filter, options, multiple, callback) {
    const origin = `${this.myid}-dbUtil-delete`;
    log.trace(origin);

    try {
      if (!collectionName) {
        log.warn(`${origin}: Missing Collection Name`);
        return callback(`${origin}: Missing Collection Name`, null);
      }
      if (!filter) {
        log.warn(`${origin}: Missing Filter`);
        return callback(`${origin}: Missing Filter`, null);
      }
      if (!multiple && multiple !== false) {
        log.warn(`${origin}: Missing Multiple flag`);
        return callback(`${origin}: Missing Multiple flag`, null);
      }

      // if using file storage
      if (this.dburl === null) {
        if (!fs.existsSync(`${adapterDir}/storage/${collectionName}.json`)) {
          log.warn(`${origin}: Collection ${collectionName} does not exist`);
          return callback(null, `${origin}: Collection ${collectionName} does not exist`);
        }
        const deld = removeFromJSON(collectionName, null, null, filter, multiple);
        if (!deld) {
          log.warn(`${origin}: Data has not been deleted from file storage`);
          return callback(`${origin}: Data has not been deleted from file storage`, null);
        }
        log.trace(`${origin}: Data has been deleted from file storage`);
        return callback(null, deld);
      }

      // work with the provided database
      return MongoClient.connect(this.dburl, this.dboptions, (error, db) => {
        if (error) {
          // no Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        if (!multiple) {
          // delete the single item from mongo
          return db.db(this.database).collection(collectionName).deleteOne(filter, options, (err, res) => {
            db.close();
            if (err) {
              log.warn(`${origin}: Failed to delete data from database ${err}`);
              return callback(`${origin}: Failed delete data from database ${err}`, null);
            }
            log.trace(`${origin}: Data has been deleted from database`);
            return callback(null, res);
          });
        }

        // delete the multiple items from mongo
        return db.db(this.database).collection(collectionName).deleteMany(filter, options, (err, res) => {
          db.close();
          if (err) {
            log.warn(`${origin}: Failed to delete data from database ${err}`);
            return callback(`${origin}: Failed delete data from database ${err}`, null);
          }
          log.trace(`${origin}: Data has been deleted from database`);
          return callback(null, res);
        });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * Replace an item in a collection
   *
   * @function replaceOne
   * @param {String} collectionName - the collection to replace document in. (required)
   * @param {Object} filter - the filter for the document(s) to replace. (required)
   * @param {Object} doc - the filter for the document(s) to replace. (required)
   */
  replaceOne(collectionName, filter, doc, options, callback) {
    const origin = `${this.myid}-dbUtil-replaceOne`;
    log.trace(origin);

    try {
      if (!collectionName) {
        log.warn(`${origin}: Missing Collection Name`);
        return callback(`${origin}: Missing Collection Name`, null);
      }
      if (!filter) {
        log.warn(`${origin}: Missing Filter`);
        return callback(`${origin}: Missing Filter`, null);
      }
      if (!doc) {
        log.warn(`${origin}: Missing Document`);
        return callback(`${origin}: Missing Document`, null);
      }

      // if using file storage
      if (this.dburl === null) {
        const rem = removeFromJSON(collectionName, null, null, filter, false);
        if (rem) {
          const sav = saveAsJson(collectionName, doc, null, null);
          if (sav) {
            log.trace(`${origin}: Data replaced in file storage`);
            return callback(null, sav);
          }
          log.warn(`${origin}: Could not save doc into file storage`);
          return callback(`${origin}: Could not save doc into file storage`, null);
        }
        log.warn(`${origin}: Could not delete from file storage`);
        return callback(`${origin}: Could not delete from file storage`, null);
      }

      // work with the provided database
      return MongoClient.connect(this.dburl, this.dboptions, (error, db) => {
        if (error) {
          // no Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        // replace an items in mongo
        return db.db(this.database).collection(collectionName).replaceOne(filter, doc, options, (err, res) => {
          db.close();
          if (err) {
            log.warn(`${origin}: Failed to replace data in database ${err}`);
            return callback(`${origin}: Failed replace data in database ${err}`, null);
          }
          log.trace(`${origin}: Data replaced in file storage`);
          return callback(null, res);
        });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * Call to find items in the database
   *
   * @function find
   * @param {String} collectionName - the collection name to search in. (required)
   * @param {String} entity - the entity to use to find data. (required)
   * @param {String} action - the action to use to find data. (required)
   * @param {Object} options - the options to use to find data.
   * options = {
   *  filter : <filter Obj>,
   *  sort : <sort Obj>,
   *  start : <start position>,
   *  limit : <limit of results>,
   * }
   */
  find(collectionName, entity, action, options, callback) {
    const origin = `${this.myid}-dbUtil-find`;
    log.trace(origin);

    try {
      // verify the required data has been provided
      if (!collectionName) {
        log.warn(`${origin}: Missing Collection Name`);
        return callback(`${origin}: Missing Collection Name`, null);
      }
      if ((entity && !action) || (!entity && action)) {
        log.warn(`${origin}: Inconsistent entity/action set`);
        return callback(`${origin}: Inconsistent entity/action set`, null);
      }

      // get the collection so we can run the remove on the collection
      let filter = {};
      let start = 0;
      let sort = {};
      let limit = 10;
      if (options) {
        filter = options.filter || {};
        start = options.start || 0;
        sort = options.sort || {};
        if (Object.hasOwnProperty.call(options, 'limit')) {
          ({ limit } = options);
        }
      }

      // If limit is not specified, default to 10.
      // Note: limit may be 0, which is equivalent to setting no limit.

      // Replace filter with regex to allow for substring lookup
      // TODO: Need to create a new filter object instead of mutating the exsisting one
      const filterKeys = Object.keys(filter).filter((key) => (key[0] !== '$' && typeof filter[key] === 'string'));
      filterKeys.map((key) => {
        try {
          const escapedFilter = filter[key].replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
          const regexedFilter = new RegExp(`.*${escapedFilter}.*`, 'i');
          filter[key] = {
            $regex: regexedFilter
          };
        } catch (e) {
          delete filter[key];
        }
        return key;
      });

      // if using file storage
      if (this.dburl === null) {
        // Find it from file in the adapter
        let toReturn = getFromJson(collectionName, entity, action, filter);
        if (toReturn && toReturn.length > limit) {
          let curEnd = start + limit;
          if (curEnd < toReturn.length) {
            curEnd = toReturn.length;
          }
          toReturn = toReturn.slice(start, curEnd);
        }
        log.trace(`${origin}: Data retrieved from file storage`);
        return callback(null, toReturn);
      }

      // use the database
      return MongoClient.connect(this.dburl, this.dboptions, async (error, db) => {
        if (error) {
          // no Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        // Find the data in the database
        const col = entity || 'metrics';
        const collection = db.db(this.database).collection(col);
        return collection.find(filter).sort(sort).skip(start).limit(limit)
          .toArray()
          .then((value) => {
            db.close();
            log.trace(`${origin}: Data retrieved from database`);
            return callback(null, value);
          });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * Call to find an item in a collection and modify it.
   *
   * @function findAndModify
   * @param {String} collectionName - the collection to find things from. (required)
   * @param {Object} filter - the filter used to find objects. (optional)
   * @param {Array} sort - how to sort the items (first one in order will be modified). (optional)
   * @param {Object} data - the modification to make. (required)
   * @param {Boolean} upsert - option for the whether to insert new objects. (optional)
   * @param {updateCallback} callback - a callback function to return a result
   *                                    (the new object) or the error
   */
  findAndModify(collectionName, filter, sort, data, upsert, callback) {
    const origin = `${this.myid}-dbUtil-findAndModify`;
    log.trace(origin);

    try {
      // verify the required data has been provided
      if (!collectionName) {
        log.warn(`${origin}: Missing Collection Name`);
        return callback(`${origin}: Missing Collection Name`, null);
      }
      if (!data) {
        log.warn(`${origin}: Missing data for modification`);
        return callback(`${origin}: Missing data for modification`, null);
      }

      const options = {
        sort,
        upsert,
        returnOriginal: false
      };
      const keysArray = Object.keys(data);
      let ent = null;
      let act = null;
      if (keysArray.length >= 2) {
        ent = data[keysArray[1]].entity;
        act = data[keysArray[1]].action || undefined;
      }
      if (!ent) {
        log.warn(`${origin}: Missing Entity`);
        return callback('Missing Entity', null);
      }
      if (!act) {
        log.warn(`${origin}: Missing Action`);
        return callback('Missing Action', null);
      }
      // if ((ent && !act) || (!ent && act)) {
      //   log.warn(`${origin}: provided with one of entity/action, require both or neither`);
      //   return callback('Inconsistent entity/action set', null);
      // } findAndModify is not used in throttle.js so action & entity still apply.
      // get the collection so we can run the remove on the collection

      // if using file storage
      if (this.dburl === null) {
        // save it to file in the adapter storage directory
        const saved = saveAsJson(collectionName, data, ent, act);
        if (!saved) {
          log.error(`${origin}: Data has not been saved`);
          return callback(`${origin}: Data has not been saved`, null);
        }
        log.trace(`${origin}: Data modified in file storage`);
        return callback(null, saved);
      }

      // use the database
      return MongoClient.connect(this.dburl, this.dboptions, (error, db) => {
        if (error) {
          // no Database connection available
          log.warn(`${origin}: Error connecting to Mongo ${error}`);
          return callback(`${origin}: Error connecting to Mongo ${error}`, null);
        }

        // Modify the data in the database
        const coln = db.db(this.database).collection(collectionName);
        return coln.findOneAndUpdate((filter || {}), data, options, (err, result) => {
          db.close();
          if (err) {
            log.warn(`${origin}: Failed to modified data in database ${err}`);
            return callback(`${origin}: Failed modified data in database ${err}`, null);
          }
          log.trace(`${origin}: Data modified in database`);
          return callback(null, result);
        });
      });
    } catch (ex) {
      log.warn(`${origin}: Caught Exception - ${ex}`);
      return callback(`${origin}: Caught Exception - ${ex}`, null);
    }
  }

  /**
   * Call to connect and authenticate to the database
   *
   * @function connect
   */
  connect(callback) {
    // callback format: (this.alive, mongoClient); generally mongoClient should only be used if this.alive is true.
    const origin = `${this.myid}-dbUtil-connect`;
    log.trace(origin);

    // const options = (replSetEnabled === true) ? { replSet: opts } : { server: opts };
    log.debug(`${origin}: Connecting to MongoDB with options ${JSON.stringify(this.dboptions)}`);

    // Now we will start the process of connecting to mongo db
    return MongoClient.connect(this.dburl, this.dboptions, (err, mongoClient) => {
      if (!mongoClient) {
        log.error(`${origin}: Error! Exiting... Must start MongoDB first ${err}`);
        this.alive = false;
        return callback(this.alive);
      }
      mongoClient.on('close', () => {
        this.alive = false;
        log.error(`${origin}: MONGO CONNECTION LOST...`);
      });

      mongoClient.on('reconnect', () => {
        // we still need to check if we are properly authenticated
        // so we just list collections to test it.
        this.clientDB.listCollections().toArray((error) => {
          if (error) {
            log.error(`${origin}: ${error}`);
            this.alive = false;
          } else {
            log.info(`${origin}: MONGO CONNECTION BACK...`);
            this.alive = true;
          }
        });
      });

      mongoClient.db(this.database).serverConfig.on('left', (type) => {
        if (type === 'primary') {
          this.alive = false;
          log.info(`${origin}: MONGO PRIMARY CONNECTION LOST...`);
        } else if (type === 'secondary') {
          log.info(`${origin}: MONGO SECONDARY CONNECTION LOST...`);
        }
      });

      mongoClient.db(this.database).serverConfig.on('joined', (type) => {
        if (type === 'primary') {
          log.info(`${origin}: MONGO PRIMARY CONNECTION BACK...`);
        } else if (type === 'secondary') {
          log.info(`${origin}: MONGO SECONDARY CONNECTION BACK...`);
        }
      });

      log.info(`${origin}: mongo running @${this.dburl}/${this.database}`);
      this.clientDB = mongoClient.db(this.database);

      // we don't have authentication defined but we still need to check if Mongo does not
      // require one, so we just list collections to test if it's doable.
      return this.clientDB.listCollections().toArray((error) => {
        if (error) {
          log.error(`${origin}: ${error}`);
          this.alive = false;
        } else {
          log.info(`${origin}: MongoDB connection has been established`);
          this.alive = true;
        }
        return callback(this.alive, mongoClient);
      });
    });
  }
}

module.exports = DBUtil;
