"use strict";
/**
 * Loads documents into memory
 *
 *  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 __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cleanDocument = exports.fieldsNowApplicable = exports.kickBackToNewlyApplicable = exports.documentUsesProgram = exports.initNewDocument = exports.prepareNewDocument = exports.initExistingDocument = exports.loadDocument = exports.loadDocumentIntoQuote = exports.loadSessionIntoQuote = exports.applyBucketDefaults = exports.incrementAndPersistNextQuoteId = exports.applyDocumentDefaults = exports.pullDocumentFromDao = void 0;
var A = require("fp-ts/Array");
var B = require("fp-ts/Bounded");
var E = require("fp-ts/Either");
var IO = require("fp-ts/IO");
var M = require("fp-ts/Monoid");
var O = require("fp-ts/Option");
var R = require("fp-ts/Record");
var TE = require("fp-ts/TaskEither");
var function_1 = require("fp-ts/function");
var Ord_1 = require("fp-ts/Ord");
var isArray = Array.isArray;
var invalid_1 = require("../../validate/invalid");
/** Pull a document from a datasource using a DAO */
exports.pullDocumentFromDao = function (dao) {
    return function_1.flow(TE.tryCatchK(dao.pullQuote.bind(dao), String), TE.chainEitherKW(E.fromNullable('Quote not found')));
};
exports.applyDocumentDefaults = function (program) { return function (doc_data) { return function () {
    var defaulted = exports.applyBucketDefaults(program)(doc_data.data || {})();
    doc_data.data = defaulted;
    return doc_data;
}; }; };
/**
 * Retrieve next available quote id from DB, with side-effects
 *
 * This will increment the counter in the database atomically.
 *
 * Other languages have conventions for side-effect-laden functions (e.g. a
 * '!' suffix in Scheme), which we don't have here, so this is instead
 * verbose to try to indicate what it's doing.
 */
exports.incrementAndPersistNextQuoteId = function (dao) {
    return TE.tryCatch(dao.getNextQuoteId.bind(dao), E.toError);
};
/**
 * Apply default bucket data for the given program
 *
 * This is a near-verbatim migration of what was `ProgramInit`; it still
 * needs to be converted into a more functional style.
 */
exports.applyBucketDefaults = function (program) { return function (data) { return function () {
    var _a = program.defaults, defaults = _a === void 0 ? {} : _a, _b = program.meta.groups, groups = _b === void 0 ? {} : _b;
    Object.keys(program.groupExclusiveFields).forEach(function (group) {
        var i = program.groupExclusiveFields[group].length;
        while (i--) {
            var field = program.groupExclusiveFields[group][i];
            var init_value = defaults[field];
            // generated questions with no types should never be part of
            // the bucket
            if (!program.hasKnownType(field)) {
                continue;
            }
            if (defaults[field] === undefined) {
                continue;
            }
            // If no document data, initialize with default value
            if (data[field] === undefined) {
                data[field] = [init_value];
            }
            // If min rows on the group is greater than the data
            // currently in the bucket, then populate the rest
            // of the data with the default data until the
            // arrays are the same length
            if (groups[group] !== undefined &&
                data[field].length < groups[group].min) {
                var index = data[field].length;
                while (index < groups[group].min) {
                    data[field][index] = init_value;
                    index++;
                }
            }
        }
    });
    return data;
}; }; };
/**
 * Populate a document (quote) with session data, giving precedence to data
 * already present on the document
 */
exports.loadSessionIntoQuote = function (session) { return function (quote) { return function (data) { return function () {
    return quote
        .setAgentId(data.agentId || session.agentId() || 0)
        .setUserName(session.userName() || '')
        .setAgentName(data.agentName || session.agentName() || '');
}; }; }; };
/**
 * Populate a document (quote) with data returned from the database
 */
exports.loadDocumentIntoQuote = function (quote) { return function (quote_data) { return function () {
    return quote
        .setData(quote_data.data || {})
        .setLastPersistedFieldState(quote_data.fieldState || undefined)
        .setMetadata(quote_data.meta || {})
        .setAgentEntityId(quote_data.agentEntityId || 0)
        .setInitialRatedDate(quote_data.initialRatedDate || 0)
        .setStartDate(quote_data.startDate || 0)
        .setImported(!!quote_data.importedInd)
        .setBound(!!quote_data.boundInd)
        .needsImport(!!quote_data.importDirty)
        .setCurrentStepId(quote_data.currentStepId || 0)
        .setTopVisitedStepId(quote_data.topVisitedStepId || 0)
        .setTopSavedStepId(quote_data.topSavedStepId || 0)
        .setProgramVersion(quote_data.pver || '')
        .setExplicitLock(quote_data.explicitLock || '', quote_data.explicitLockStepId || 0)
        .setError(quote_data.error || '')
        .setCreditScoreRef(quote_data.creditScoreRef || '')
        .setLastPremiumDate(quote_data.lastPremDate || 0)
        .setRatedDate(quote_data.initialRatedDate || 0)
        .setRatingData(quote_data.ratedata || {})
        .setRetryAttempts(quote_data.retryAttempts || 0);
}; }; };
/** Load data into a document (quote) */
exports.loadDocument = function (pullDocument) { return function (program) { return function (session) { return function (quote) {
    return function_1.pipe(quote.getId(), pullDocument, TE.chain(function_1.flow(exports.applyDocumentDefaults(program), TE.fromIO)), TE.chainFirst(function_1.flow(exports.loadSessionIntoQuote(session)(quote), TE.fromIO)), TE.chain(function_1.flow(exports.loadDocumentIntoQuote(quote), TE.fromIO)));
}; }; }; };
/** Load and initialize an existing document */
exports.initExistingDocument = function_1.flow(exports.pullDocumentFromDao, exports.loadDocument);
var processNaFields = function (program) { return function (data) { return function () {
    return program.processNaFields(data.data || {});
}; }; };
/** Prepare initial data for a new document (quote) */
exports.prepareNewDocument = function (program) {
    return function_1.pipe(exports.applyDocumentDefaults(program)({}), TE.fromIO, TE.chainFirst(function_1.flow(processNaFields(program), TE.fromIO)));
};
/** Initialize a new document that has not yet been persisted */
exports.initNewDocument = function (dao) { return function (quote_new) { return function (program) { return function (session) {
    return function_1.pipe(exports.incrementAndPersistNextQuoteId(dao), TE.map(quote_new), TE.chain(exports.loadDocument(function () { return exports.prepareNewDocument(program); })(program)(session)));
}; }; }; };
exports.documentUsesProgram = function (program) { return function (quote) { return quote.getProgramId() === program.getId(); }; };
/** Whether the automatic step kickback system is enabled */
var featureKickbackEnabled = function (quote) { var _a; return ((_a = quote.getDataByName('__feature_pver_kickback')) === null || _a === void 0 ? void 0 : _a[0]) === '1'; };
/**
 * Field state as of when the user last interacted with the document
 *
 * If the feature is not enabled, this will return the _current_ field
 * state, which will effectively result in a noop if kickback logic is
 * run.  Kickback logic should _not_ be run, since the flag applies there as
 * well, but this value is safe in that, if there happens to be a bug where
 * it is run, we won't wipe out a ton of user data.
 *
 * Note that an empty object means that the user sees no fields.  This could
 * be a legitimate scenario (e.g. maybe a static message is shown), but it's
 * important that we do not act on it if it's not legitimate, since that
 * would cause all applicable fields to be set to an invalid state.
 *
 * _If this fails and the feature flag has been removed, migration will not
 * be possible._  This represents a corrupt document.  To recover, update
 * the `pver` on the document to skip migration, and manually inspect, then
 * figure out how that could have possibly happened.
 */
var lastPersistedFieldState = function (quote) {
    return function_1.pipe(featureKickbackEnabled(quote)
        ? quote.getLastPersistedFieldState()
        : quote.getFieldState(), E.fromNullable(Error('Last field state is not available')));
};
/** Current field state, as of this moment, with the current Program */
var currentFieldState = function (quote) {
    return function_1.pipe(quote.getFieldState(), E.fromNullable(Error('Current field state is not available')));
};
/**
 * Kick document back to the earliest field that is now applicable that was
 * not previously
 */
exports.kickBackToNewlyApplicable = function (program) { return function (quote) { return function () {
    return function_1.pipe(
    // Determine which fields are now applicable that were not previously,
    // failing if one of the arguments is not available
    function_1.pipe(E.of(exports.fieldsNowApplicable), E.ap(lastPersistedFieldState(quote)), E.ap(currentFieldState(quote))), 
    // Determine kickback step, if any, not to exceed the current step
    E.map(function_1.flow(R.mapWithIndex(fieldStep(program)), R.filter(betweenPermittedKickbackRange(quote)), invalidateFields(quote), R.foldMap(meetBeforeCurrentStep(quote))(stepIdFromPair))), 
    // Impose step as upper bound on quote
    E.map(function (kickback_step_id) {
        return quote
            .setCurrentStepId(kickback_step_id)
            .setTopVisitedStepId(kickback_step_id);
    }));
}; }; };
/**
 * Whether the given step id is within the range permissable for kickback
 *
 * A kickback will not occur if the feature flag is not set on the
 * document.  The will be removed for release.
 *
 * If a document it locked, it'll be kicked back no lower than the lock step
 * if one is set; if there is no lock step but the document is locked, then
 * it will not be kicked back at all.
 *
 * The document will never be kicked back prior to the current step.
 */
var betweenPermittedKickbackRange = function (quote) {
    return Ord_1.between(ordPairStepId)(pairFromStepId(quote.isLocked() || !featureKickbackEnabled(quote)
        ? quote.getExplicitLockStep() || quote.getCurrentStepId()
        : -Infinity), pairFromStepId(featureKickbackEnabled(quote) ? quote.getCurrentStepId() : -Infinity));
};
/**
 * If only one of the provided values is an array, convert the other into an
 * array by broadcasting to an array of the same length as the other
 */
var normalizeIndexes = function (a, b) {
    return isArray(a) && !isArray(b)
        ? [a, a.map(function (_) { return b; })]
        : !isArray(a) && isArray(b)
            ? [b.map(function (_) { return a; }), b]
            : [a, b];
};
/**
 * Whether a given field has indexes that are applicable when they were not
 * previously
 *
 * The returned value, if `some`, has all indexes cleared except for those
 * indexes that have become applicable.
 */
var fieldNewlyApplicable = function (_a) {
    var x = _a[0], y = _a[1];
    return function_1.pipe(isArray(x) ? x.map(function (v, i) { return +(v && v !== y[i]); }) : +(x && x !== y), O.fromPredicate(function (x) { return (isArray(x) ? x.some(function (v) { return !!v; }) : !!x); }));
};
/** List of fields that are now applicable but were not previously */
exports.fieldsNowApplicable = function (last) { return function (cur) {
    return function_1.pipe(cur, R.filterMapWithIndex(function (key, state) {
        return fieldNewlyApplicable(normalizeIndexes(state, last[key] || 0));
    }));
}; };
/** Step field is assigned to within the given program, otherwise Infinity **/
var fieldStep = function (program) { return function (field, state) { var _a; return [state, (_a = program.qstep[field]) !== null && _a !== void 0 ? _a : Infinity]; }; };
/** Meet (minimum) of the given array of values, defaulting to current step */
var meetBeforeCurrentStep = function (quote) {
    return M.getMeetMonoid(__assign(__assign({}, B.boundedNumber), { top: quote.getCurrentStepId() }));
};
var pairFromStepId = function (id) { return [[], id]; };
var stateFromPair = function (pair) { return pair[0]; };
var stepIdFromPair = function (pair) { return pair[1]; };
var ordPairStepId = Ord_1.contramap(stepIdFromPair)(Ord_1.ordNumber);
/**
 * Invalidate all newly applicable fields, by index
 *
 * All indexes that are set for a given field will be set to the invalid
 * value `invalidValue`, which will cause a validation failure and force the
 * user to change its value.
 */
var invalidateFields = function (quote) { return function (fields) {
    function_1.pipe(fields, R.map(stateFromPair), R.mapWithIndex(broadcastScalar(quote)), R.map(invalidDelta), quote.setData.bind(quote));
    return fields;
}; };
/** Generate a value for an invalid delta from the given `State` */
var invalidDelta = function (x) {
    return isArray(x) ? x.map(invalidDelta) : [undefined, invalid_1.invalidate('')][x];
};
/** Broadcast a scalar value onto a vector */
var broadcastScalar = function (quote) { return function (field, x) {
    return isArray(x) ? x : quote.getDataByName(field).map(function (_) { return x; });
}; };
/**
 * "Clean" quote, getting it into a stable state
 *
 * Quote cleaning will ensure that all group fields share at least the
 * same number of indexes as its leader, and that meta fields are
 * initialized.  This is useful when questions or meta fields are added.
 */
exports.cleanDocument = function (program) { return function (quote) {
    return function_1.pipe(quote, 
    // Consider it an error to attempt cleaning a quote with the incorrect
    // program, which would surely corrupt it [even further]
    TE.fromPredicate(exports.documentUsesProgram(program), function_1.constant(Error('Quote/program mismatch'))), 
    // Fix groups
    TE.chainFirst(function_1.flow(function_1.flow(fixGroup(program), O.of), 
    // TODO: get rid of this extra expensive classify, considering that
    // a previous part of the system probably already did it!
    O.ap(O.of(quote.classify())), O.map(function (f) {
        return A.array.traverse(IO.io)(Object.keys(program.groupIndexField || {}), f)();
    }), TE.fromOption(function_1.constant(Error('fixGroup failure'))))), 
    // Fix metadata
    TE.chainFirst(function_1.flow(fixMeta(program), TE.fromIO)), 
    // Kick user back to newly applicable fields
    TE.chainIOEitherKW(exports.kickBackToNewlyApplicable(program)));
}; };
/**
 * Correct group fields to be at least the length of the leader
 *
 * If a group is part of a link, then its leader may be part of another
 * group, and the length of the fields of all linked groups will match
 * be at least the length of the leader.
 *
 * Unlike previous implementations, this _does not_ truncate fields,
 * since that risks data loss.  Instead, field length should be
 * validated on save.
 */
var fixGroup = function (program) { return function (quote) { return function (class_data) { return function (group_id) { return function () {
    var length = groupLength(program)(quote)(group_id);
    // if we cannot accurately determine the length then it's too
    // dangerous to proceed and risk screwing up the data; abort
    // processing this group (this should never happen unless a program
    // is either not properly compiled or is out of date)
    if (isNaN(length)) {
        return;
    }
    var update = {};
    var group_fields = program.groupExclusiveFields[group_id];
    group_fields.forEach(function (field) {
        var flen = (quote.getDataByName(field) || []).length;
        // generated questions with no types should never be part of
        // the bucket
        if (!program.hasKnownType(field)) {
            return;
        }
        if (flen >= length) {
            return;
        }
        var data = [];
        var field_default = program.defaults[field] || '';
        for (var i = flen; i < length; i++) {
            data[i] =
                program.clearNaFields && program.hasNaField(field, class_data, i)
                    ? program.naFieldValue
                    : field_default;
        }
        update[field] = data;
    });
    quote.setData(update);
}; }; }; }; };
/**
 * Determine length of group GROUP_ID
 *
 * The length of a group is the length of its leader, which may be part
 * of another group (if the group is linked).
 */
var groupLength = function (program) { return function (quote) { return function (group_id) {
    var index_field = program.groupIndexField[group_id];
    // we don't want to give the wrong answer, so just abort
    if (!index_field) {
        return NaN;
    }
    var data = quote.getDataByName(index_field);
    return Array.isArray(data) ? data.length : NaN;
}; }; };
/**
 * Initialize missing metadata
 *
 * This is similar to bucket initialization, except there are no leaders
 * or default values---just empty arrays.  That may change in the future.
 */
var fixMeta = function (program) { return function (quote) { return function () {
    var _a = program.meta.fields, fields = _a === void 0 ? {} : _a;
    var metabucket = quote.getMetabucket();
    var metadata = metabucket.getData();
    Object.keys(fields).forEach(function (field_name) {
        var _a;
        if (Array.isArray(metadata[field_name])) {
            return;
        }
        metabucket.setValues((_a = {}, _a[field_name] = [], _a));
    });
}; }; };
//# sourceMappingURL=data:application/json;base64, 