import mongoose from 'mongoose';
import cfenv from 'cfenv';
import _ from 'lodash';
import ChangelogModel from './models/changelog';


// database urls, prod db url is retrieved automatically
const TESTING_URL = 'mongodb://localhost/dfopdb_test';
const DEV_URL = 'mongodb://localhost/dfopdb';
const debugging = true;

if (process.env.NODE_ENV !== 'production' && debugging) {
  mongoose.set('debug', true);  // enable mongoose debug
}

export default class db {
  private static state = {  // db object and current mode (test, dev, prod)
    db: null,
    mode: null,
  };

  // set mode to test for unit/integration tests, otherwise skip parameters. done is also only needed for testing
  static connect (mode = '', done: Function = () => {}) {
    if (this.state.db) return done();  // db is already connected

    // find right connection url
    let connectionString: string = "";
    if (mode === 'test') {  // testing
      connectionString = TESTING_URL;
      this.state.mode = 'test';
    }
    else if(process.env.NODE_ENV === 'production') {
      let services = cfenv.getAppEnv().getServices();
      for (let service in services) {
        if(services[service].tags.indexOf("mongodb") >= 0) {
          connectionString = services[service]["credentials"].uri;
        }
      }
      this.state.mode = 'prod';
    }
    else {
      connectionString = DEV_URL;
      this.state.mode = 'dev';
    }

    // connect to db
    mongoose.connect(connectionString, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true,
      connectTimeoutMS: 10000
    }, err => {
      if (err) done(err);
    });
    mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
    mongoose.connection.on('connected', () => {  // evaluation connection behaviour on prod
      if (process.env.NODE_ENV !== 'test') {  // Do not interfere with testing
        console.info('Database connected');
      }
    });
    mongoose.connection.on('disconnected', () => {  // reset state on disconnect
      if (process.env.NODE_ENV !== 'test') {  // Do not interfere with testing
        console.info('Database disconnected');
        // this.state.db = 0;   // prod database connects and disconnects automatically
      }
    });
    process.on('SIGINT', () => {  // close connection when app is terminated
      if (!this.state.db) {  // database still connected
        mongoose.connection.close(() => {
          console.info('Mongoose default connection disconnected through app termination');
          process.exit(0);
        });
      }
    });
    mongoose.connection.once('open', () => {
      mongoose.set('useFindAndModify', false);
      console.info(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`);
      this.state.db = mongoose.connection;
      done();
    });
  }

  static disconnect (done) {
    mongoose.connection.close(() => {
      console.info(process.env.NODE_ENV === 'test' ? '' : `Disconnected from database`);
      this.state.db = 0;
      done();
    });
  }

  static getState () {
    return this.state;
  }

  // drop all collections of connected db (only dev and test for safety reasons)
  static drop (done: Function = () => {}) {
    if (!this.state.db || this.state.mode === 'prod') return done();  // no db connection or prod db
    this.state.db.db.listCollections().toArray((err, collections) => {  // get list of all collections
      if (collections.length === 0) {  // there are no collections to drop
        return done();
      }
      else {
        let dropCounter = 0;  // count number of dropped collections to know when to return done()
        collections.forEach(collection => {  // drop each collection
          this.state.db.dropCollection(collection.name, () => {
            if (++ dropCounter >= collections.length) {  // all collections dropped
              done();
            }
          });
        });
      }
    });
  }

  static loadJson (json, done: Function = () => {}) {  // insert given JSON data into db, uses core mongodb methods
    // no db connection or nothing to load
    if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) {
      return done();
    }

    let loadCounter = 0;  // count number of loaded collections to know when to return done()
    Object.keys(json.collections).forEach(collectionName => {  // create each collection
      json.collections[collectionName] = this.oidResolve(json.collections[collectionName]);
      this.state.db.createCollection(collectionName, (err, collection) => {
        if (err) {
          console.error(err);
        }
        collection.insertMany(json.collections[collectionName], () => {  // insert JSON data
          if (++ loadCounter >= Object.keys(json.collections).length) {  // all collections loaded
            done();
          }
        });
      });
    });
  }

  // changelog entry, expects (req, this (from query helper)) or (req, collection, conditions, data)
  static log(req, thisOrCollection, conditions = null, data = null) {
    if (! (conditions || data)) {  // (req, this)
      data = thisOrCollection._update ? _.cloneDeep(thisOrCollection._update) : {};  // replace undefined with {}
      Object.keys(data).forEach(key => {
        if (key[0] === '$') {
          data[key.substr(1)] = data[key];
          delete data[key];
        }
      });
      new ChangelogModel({
        action: req.method + ' ' + req.url,
        collection_name: thisOrCollection._collection.collectionName,
        conditions: thisOrCollection._conditions,
        data: data,
        user_id: req.authDetails.id ? req.authDetails.id : null
      }).save(err => {
        if (err) console.error(err);
      });
    }
    else {  // (req, collection, conditions, data)
      new ChangelogModel({
        action: req.method + ' ' + req.url,
        collection_name: thisOrCollection,
        conditions: conditions,
        data: data,
        user_id: req.authDetails.id ? req.authDetails.id : null
      }).save(err => {
        if (err) console.error(err);
      });
    }
  }

  private static oidResolve (object: any) {  // resolve $oid fields to actual ObjectIds recursively
    Object.keys(object).forEach(key => {
      if (object[key] !== null && object[key].hasOwnProperty('$oid')) {  // found oid, replace
        object[key] = mongoose.Types.ObjectId(object[key].$oid);
      }
      else if (typeof object[key] === 'object' && object[key] !== null) {  // deeper into recursion
        object[key] = this.oidResolve(object[key]);
      }
    });
    return object;
  }
};
