"use strict";
/* TODO auto-generated eslint ignore, please fix! */
/* eslint @typescript-eslint/no-var-requires: "off", @typescript-eslint/no-inferrable-types: "off", no-var: "off", @typescript-eslint/no-this-alias: "off", prefer-arrow-callback: "off", eqeqeq: "off" */
/**
 * Mongo DB DAO for program server
 *
 *  Copyright (C) 2010-2019 R-T Specialty, LLC.
 *
 *  This file is part of the Liza Data Collection Framework.
 *
 *  liza is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongoServerDao = void 0;
var NoPendingError_1 = require("../../error/NoPendingError");
var EventEmitter = require('events').EventEmitter;
/**
 * Uses MongoDB as a data store
 */
var MongoServerDao = /** @class */ (function (_super) {
    __extends(MongoServerDao, _super);
    /**
     * Initializes DAO
     *
     * @param _db      - mongo database connection
     * @param _env     - the name of the current environment
     * @param _ts_ctor - a timestamp constructor
     */
    function MongoServerDao(_db, _env, _ts_ctor) {
        var _this = _super.call(this) || this;
        _this._db = _db;
        _this._env = _env;
        _this._ts_ctor = _ts_ctor;
        /** Collection used to store quotes */
        _this.COLLECTION = 'quotes';
        /** Sequence (auto-increment) collection */
        _this.COLLECTION_SEQ = 'seq';
        /** Sequence key for quote ids */
        _this.SEQ_QUOTE_ID = 'quoteId';
        /** Sequence quoteId default */
        _this.SEQ_QUOTE_ID_DEFAULT = 200000;
        /** Whether the DAO is initialized and ready to be used */
        _this._ready = false;
        return _this;
    }
    /**
     * Initializes error events and attempts to connect to the database
     *
     * connectError event will be emitted on failure.
     *
     * @param success - function to call when connection is complete
     *                          (will not be called if connection fails)
     */
    MongoServerDao.prototype.init = function (success) {
        var dao = this;
        // map db error event (on connection error) to our connectError event
        this._db.on('error', function (err) {
            dao._ready = false;
            dao._collection = null;
            dao.emit('connectError', err);
        });
        this.connect(success);
        return this;
    };
    /**
     * Attempts to connect to the database
     *
     * connectError event will be emitted on failure.
     *
     * @param success - function to call when connection is complete
     *                          (will not be called if connection fails)
     */
    MongoServerDao.prototype.connect = function (success) {
        var dao = this;
        // attempt to connect to the database
        this._db.open(function (err, db) {
            // if there was an error, don't bother with anything else
            if (err) {
                // in some circumstances, it may just be telling us that we're
                // already connected (even though the connection may have been
                // broken)
                if (err.errno !== undefined) {
                    dao.emit('connectError', err);
                    return;
                }
            }
            var ready_count = 0;
            var check_ready = function () {
                if (++ready_count < 2) {
                    return;
                }
                // we're ready to roll!
                dao._ready = true;
                dao.emit('ready');
                // connection was successful; call the callback
                if (success instanceof Function) {
                    success.call(dao);
                }
            };
            // quotes collection
            db.collection(dao.COLLECTION, function (_err, collection) {
                // for some reason this gets called more than once
                if (collection == null) {
                    return;
                }
                // initialize indexes
                collection.createIndex([['id', 1]], true, function (_err, _index) {
                    // mark the DAO as ready to be used
                    dao._collection = collection;
                    check_ready();
                });
            });
            // seq collection
            db.collection(dao.COLLECTION_SEQ, function (err, collection) {
                if (err) {
                    dao.emit('seqError', err);
                    return;
                }
                if (collection == null) {
                    return;
                }
                dao._seqCollection = collection;
                // has the sequence we'll be referencing been initialized?
                collection.find({ _id: dao.SEQ_QUOTE_ID }, { limit: 1 }, function (err, cursor) {
                    if (err) {
                        dao._initQuoteIdSeq(check_ready);
                        return;
                    }
                    cursor.toArray(function (_err, data) {
                        if (data.length == 0) {
                            dao._initQuoteIdSeq(check_ready);
                            return;
                        }
                        check_ready();
                    });
                });
            });
        });
        return this;
    };
    /**
     * Initialize the quote id sequence collection
     *
     * @param success - a function to call once it has been initialized
     */
    MongoServerDao.prototype._initQuoteIdSeq = function (success) {
        var dao = this;
        this._seqCollection.insert({
            _id: this.SEQ_QUOTE_ID,
            val: this.SEQ_QUOTE_ID_DEFAULT,
        }, function (err, _docs) {
            if (err) {
                dao.emit('seqError', err);
                return;
            }
            dao.emit('seqInit', dao.SEQ_QUOTE_ID);
            success.call(dao);
        });
    };
    /**
     * Saves a quote to the database
     *
     * A full save will include all metadata.  This should not cause any
     * problems with race conditions for pending Data API calls on meta
     * fields because those results write to individual indexes and do not
     * rely on existing data.
     *
     * @param quote         - the quote to save
     * @param success       - function to call on success
     * @param failure       - function to call if save fails
     * @param save_data     - quote data to save (optional)
     * @param push_data     - quote data to push (optional)
     * @param force_publish - reset published indicator (optional)
     */
    MongoServerDao.prototype.saveQuote = function (quote, success, failure, save_data, push_data, force_publish) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        if (force_publish === void 0) { force_publish = true; }
        var dao = this;
        // if we're not ready, then we can't save the quote!
        if (this._ready === false) {
            this.emit('saveQuoteError', { message: 'Database server not ready' }, Error('Database not ready'), quote);
            failure.call(this, quote);
            return dao;
        }
        if (save_data === undefined) {
            save_data = {};
            var quote_data_1 = quote.getBucket().getData();
            Object.keys(quote_data_1).forEach(function (key) { return (save_data['data.' + key] = quote_data_1[key]); });
        }
        var id = quote.getId();
        var save_ts = this._ts_ctor();
        // some data should always be saved because the quote will be created if
        // it does not yet exist
        save_data.id = id;
        save_data.env = this._env;
        save_data.pver = quote.getProgramVersion();
        save_data.importDirty = 1;
        save_data.lastPremDate = quote.getLastPremiumDate();
        save_data.retryAttempts = quote.getRetryAttempts();
        save_data.initialRatedDate = quote.getRatedDate();
        save_data.explicitLock = quote.getExplicitLockReason();
        save_data.explicitLockStepId = quote.getExplicitLockStep();
        save_data.importedInd = +quote.isImported();
        save_data.boundInd = +quote.isBound();
        save_data.lastUpdate = save_ts;
        save_data.quoteSetId = id;
        if (force_publish) {
            save_data.publishResetTs = save_ts;
            save_data.published = false;
        }
        var exp_date = quote.getExpirationDate();
        save_data.quoteExpDate = exp_date === Infinity ? 0 : exp_date;
        // meta will eventually take over for much of the above data
        save_data['meta.liza_timestamp_initial_rated'] = [quote.getRatedDate()];
        var last_updated_by = quote.getUserName();
        if (last_updated_by !== '') {
            save_data['meta.last_updated_by_username'] = [last_updated_by];
        }
        // save the stack so we can track this call via the oplog
        save_data._stack = new Error().stack;
        // only set created by on initial document insert
        var insert_only = { 'meta.created_by_username': [quote.getUserName()] };
        // only set bind username on concluding save
        if (quote.isImported() && last_updated_by !== '') {
            save_data['meta.bound_by_username'] = [quote.getUserName()];
        }
        // do not push empty objects
        var document = !push_data || !Object.keys(push_data).length
            ? { $set: save_data, $setOnInsert: insert_only }
            : { $set: save_data, $setOnInsert: insert_only, $push: push_data };
        // update the quote data if it already exists (same id), otherwise
        // insert it
        this._collection.update({ id: id }, document, 
        // create record if it does not yet exist
        { upsert: true }, 
        // on complete
        function (err, _docs) {
            // if an error occurred, then we cannot continue
            if (err) {
                dao.emit('saveQuoteError', err, quote);
                // let the caller handle the error
                if (failure instanceof Function) {
                    failure.call(dao, quote);
                }
                return;
            }
            // successful
            if (success instanceof Function) {
                success.call(dao, quote);
            }
        });
        return this;
    };
    /**
     * Merges quote data with the existing (rather than overwriting)
     *
     * @param quote   - quote to save
     * @param data    - quote data
     * @param success - successful callback
     * @param failure - failure callback
     * @param options - mongo options to specify
     */
    MongoServerDao.prototype.mergeData = function (quote, data, success, failure, options) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        if (options === void 0) { options = {}; }
        if (!data || !Object.keys(data).length) {
            success(quote);
            return this;
        }
        // we do not want to alter the original data; use it as a prototype
        var update = data;
        // save the stack so we can track this call via the oplog
        var _self = this;
        this._collection.update({ id: quote.getId() }, { $set: update }, options, function (err, _docs) {
            if (err) {
                _self.emit('saveQuoteError', err, quote);
                if (typeof failure === 'function') {
                    failure(quote);
                }
                return;
            }
            if (typeof success === 'function') {
                success(quote);
            }
        });
        return this;
    };
    /**
     * Merges bucket data with the existing bucket (rather than overwriting the
     * entire bucket)
     *
     * @param quote   - quote to save
     * @param data    - bucket data
     * @param success - successful callback
     * @param failure - failure callback
     */
    MongoServerDao.prototype.mergeBucket = function (quote, data, success, failure) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        var update = {};
        for (var field in data) {
            if (!field) {
                continue;
            }
            update['data.' + field] = data[field];
        }
        return this.mergeData(quote, update, success, failure);
    };
    /**
     * Saves the quote state to the database
     *
     * The quote state includes the current step, the top visited step and the
     * explicit lock message.
     *
     * @param quote   - the quote to save
     * @param success - function to call on success
     * @param failure - function to call if save fails
     */
    MongoServerDao.prototype.saveQuoteState = function (quote, success, failure) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        var update = {
            currentStepId: quote.getCurrentStepId(),
            topVisitedStepId: quote.getTopVisitedStepId(),
            topSavedStepId: quote.getTopSavedStepId(),
        };
        var field_state = quote.getFieldState();
        // Do not wipe out field state if it's not provided (having
        // getFieldState run the classifier automatically would be dangerous
        // since it is so expensive)
        if (field_state && Object.keys(field_state).length > 0) {
            update.fieldState = field_state;
        }
        return this.mergeData(quote, update, success, failure);
    };
    /**
     * Ensure the quote has pending rates
     *
     * @param quote - the quote to validate
     *
     * @returns a promise with the quote
     */
    MongoServerDao.prototype.ensurePendingRate = function (quote) {
        var _this = this;
        return new Promise(function (resolve, reject) {
            _this._collection.find({ id: quote.getId() }, {
                limit: 1,
                fields: { 'ratedata.__rate_pending': 1 },
            }, function (_err, cursor) {
                cursor.toArray(function (_err, data) {
                    // was the quote found?
                    if (data.length == 0) {
                        // Return the quote unchanged
                        reject(new Error('Could not find quote ' + quote.getId()));
                        return;
                    }
                    var ratedata = data[0].ratedata;
                    if (!ratedata) {
                        reject(new NoPendingError_1.NoPendingError('No prior rate for quote ' + quote.getId()));
                        return;
                    }
                    if (!ratedata.__rate_pending || ratedata.__rate_pending <= 0) {
                        reject(new NoPendingError_1.NoPendingError('No pending rates for quote ' + quote.getId()));
                        return;
                    }
                    resolve(quote);
                });
            });
        });
    };
    /**
     * Save document metadata (meta field on document)
     *
     * Only the provided indexes will be modified (that is---data will be
     * merged with what is already in the database).
     *
     * @param quote    - destination quote
     * @param new_meta - bucket-formatted data to write
     * @param success  - callback on success
     * @param failure  - callback on error
     * @param options  - mongo options to specify
     */
    MongoServerDao.prototype.saveQuoteMeta = function (quote, new_meta, success, failure, options) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        if (options === void 0) { options = {}; }
        var update = {};
        new_meta = new_meta || quote.getMetabucket().getData();
        for (var key in new_meta) {
            var meta = new_meta[key];
            for (var i in meta) {
                update['meta.' + key + '.' + i] = new_meta[key][i];
            }
        }
        this.mergeData(quote, update, success, failure, options);
    };
    /**
     * Saves the quote lock state to the database
     *
     * @param quote   - the quote to save
     * @param success - function to call on success
     * @param failure - function to call if save fails
     */
    MongoServerDao.prototype.saveQuoteLockState = function (quote, success, failure) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        // lock state is saved by default
        return this.saveQuote(quote, success, failure, {});
    };
    /**
     * Pulls quote data from the database
     *
     * @param quote_id - id of quote
     */
    MongoServerDao.prototype.pullQuote = function (quote_id) {
        var _this = this;
        return new Promise(function (resolve, reject) {
            // XXX: TODO: Do not read whole of record into memory; filter out
            // revisions!
            _this._collection.find({ id: quote_id }, { limit: 1 }, function (_err, cursor) {
                cursor.toArray(function (err, data) {
                    if (err) {
                        return reject(err);
                    }
                    // was the quote found?
                    if (data.length == 0) {
                        return resolve(null);
                    }
                    // return the quote data
                    resolve(data[0]);
                });
            });
        });
    };
    /**
     * Returns the minimum allowable quote id
     *
     * @param callback - function to call with minimum quote id
     */
    MongoServerDao.prototype.getMinQuoteId = function (callback) {
        // just in case it's asynchronous later on
        callback.call(this, this.SEQ_QUOTE_ID_DEFAULT);
        return this;
    };
    /**
     * Returns the current highest quote id
     *
     * @param callback - function to call with current highest quote id
     */
    MongoServerDao.prototype.getMaxQuoteId = function (callback) {
        var dao = this;
        this._seqCollection.find({ _id: this.SEQ_QUOTE_ID }, { limit: 1 }, function (_err, cursor) {
            cursor.toArray(function (_err, data) {
                if (data.length == 0) {
                    callback.call(dao, 0);
                    return;
                }
                // return the max quote id
                callback.call(dao, data[0].val);
            });
        });
    };
    /**
     * Returns the next available quote id
     *
     * @param callback - function to call with next available quote id
     */
    MongoServerDao.prototype.getNextQuoteId = function () {
        var _this = this;
        var dao = this;
        return new Promise(function (accept, reject) {
            _this._seqCollection.findAndModify({ _id: _this.SEQ_QUOTE_ID }, [['val', 'descending']], { $inc: { val: 1 } }, { new: true }, function (err, doc) {
                if (err) {
                    dao.emit('seqError', err);
                    reject(err);
                    return;
                }
                // return the new id
                accept(doc.val);
            });
        });
    };
    /**
     * Create a new revision with the provided quote data
     *
     * The revision will contain the whole the quote. If space is a concern, we
     * can (in the future) calculate a delta instead (Mike recommends the Git
     * model of storing the deltas in previous revisions and the whole of the
     * bucket in the most recently created revision).
     *
     * @param quote    - the quote to create a revision with
     * @param callback - function to call on error
     */
    MongoServerDao.prototype.createRevision = function (quote, callback) {
        var _self = this, qid = quote.getId(), data = quote.getBucket().getData();
        this._collection.update({ id: qid }, { $push: { revisions: { data: data } } }, 
        // create record if it does not yet exist
        { upsert: true }, 
        // on complete
        function (err) {
            if (err) {
                _self.emit('mkrevError', err);
            }
            callback(err);
            return;
        });
    };
    /**
     * Get a quote revision by its revision id
     *
     * @param quote    - the quote
     * @param revid    - the revision id
     * @param callback - a function to call with results
     */
    MongoServerDao.prototype.getRevision = function (quote, revid, callback) {
        revid = +revid;
        // XXX: TODO: Filter out all but the revision we want
        this._collection.find({ id: quote.getId() }, { limit: 1 }, function (_err, cursor) {
            cursor.toArray(function (_err, data) {
                // was the quote found?
                if (data.length === 0 || data[0].revisions.length < revid + 1) {
                    callback(null);
                    return;
                }
                // return the quote data
                callback(data[0].revisions[revid]);
            });
        });
    };
    /**
     * Set worksheet data
     *
     * @param qid      - The quote id
     * @param data     - worksheet data
     * @param failure  - a function to call on error
     */
    MongoServerDao.prototype.setWorksheets = function (qid, data, failure) {
        this._collection.update({ id: qid }, { $set: { worksheets: { data: data } } }, 
        // create record if it does not yet exist
        { upsert: true }, 
        // on complete
        function (err) {
            failure(err);
            return;
        });
    };
    /**
     * Retrieve worksheet data for a given quote id and supplier
     *
     * @param qid      - the quote id
     * @param supplier - the supplier to retrieve the worksheet for
     * @param index    - the worksheet index
     *
     * @return Promise with worksheet data
     */
    MongoServerDao.prototype.getWorksheet = function (qid, supplier, index) {
        var _this = this;
        return new Promise(function (resolve, reject) {
            _this._collection.find({ id: qid }, { limit: 1 }, function (err, cursor) {
                if (err) {
                    reject(err);
                    return;
                }
                cursor.toArray(function (_err, data) {
                    // was the quote found?
                    if (data.length === 0 ||
                        !data[0].worksheets ||
                        !data[0].worksheets.data ||
                        !data[0].worksheets.data[supplier]) {
                        reject('Worksheet data not found');
                        return;
                    }
                    // return the quote data
                    var worksheet_data = {
                        data: data[0].worksheets.data[supplier][index],
                    };
                    resolve(worksheet_data);
                });
            });
        });
    };
    return MongoServerDao;
}(EventEmitter));
exports.MongoServerDao = MongoServerDao;
//# sourceMappingURL=data:application/json;base64, 