"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CfEdgeRelay = void 0;
const client_experimentation_1 = require("@optimizely/client-experimentation");
const transformer_1 = require("@optimizely/client-experimentation/lib/cf/transformer");
const browserDefines_1 = require("./browserDefines");
const utils_1 = require("./utils");
const edge_delivery_browser_modules_1 = __importDefault(require("@optimizely/edge-delivery-browser-modules"));
const index_1 = require("./edge-decider/decider/index");
const track_1 = require("./track");
const index_2 = require("./edge-decider/views/index");
const models_1 = require("./models");
const cookie_1 = require("./edge-decider/utils/cookie");
const visitor_id_1 = require("./visitor_id");
const sticky_1 = require("./sticky");
const attributes_1 = require("./edge-decider/attributes");
const Package = require('../package.json');
class Applicator {
    constructor(browserJS, nonce, position) {
        this.browserJS = browserJS;
        this.scriptNonce = nonce;
        this.position = position;
    }
    element(element) {
        var nonceString = this.scriptNonce
            ? ` nonce="${this.scriptNonce}"`
            : '';
        var injectFn = this.position === 'bottom' ? element.append : element.prepend;
        injectFn.call(element, `<script type="text/javascript"${nonceString}>${this.browserJS}</script>`, { html: true });
    }
}
class CfEdgeRelay {
    constructor(DATA, options, inputs, logger) {
        this.packageVersion = Package.version;
        this.additionalCookies = [];
        this.additionalHeaders = {};
        this.inputs = {};
        this.variationMap = {};
        this.config = new models_1.ExperienceConfig(DATA['config']);
        this.generatedDefines = DATA['defines'] || {};
        this.queue = new models_1.ExperienceQueue(DATA['queue']);
        this.results = new models_1.ExperienceResults(DATA['results']);
        this.deployId = options.deployId;
        this.isSnippetless = options.isSnippetless || false;
        this.logger = logger;
        this.inputs = inputs;
        this.visitorProfile = {}; // This is mutable and will be updated by the decider
    }
    getAdditionalHeaders() {
        const newHeaders = new Headers();
        for (let [key, value] of Object.entries(this.additionalHeaders)) {
            newHeaders.set(key, value);
        }
        for (let cookie of this.additionalCookies) {
            newHeaders.append('Set-Cookie', cookie);
        }
        return newHeaders;
    }
    getStickyProcessor() {
        if (!this.stickyProcessor) {
            this.stickyProcessor = new sticky_1.StickyProcesor(this.inputs.visitorId, this.config.namespace, this.inputs.cookies);
        }
        return this.stickyProcessor;
    }
    loadStickyData() {
        const stickyProcesor = this.getStickyProcessor();
        this.variationMap = stickyProcesor.getValue(sticky_1.STICKY_STORAGE_KEY.VARIATION_MAP);
        this.visitorProfile = stickyProcesor.getValue(sticky_1.STICKY_STORAGE_KEY.VISITOR_PROFILE);
    }
    saveStickyData() {
        const stickyProcesor = this.getStickyProcessor();
        if (this.variationMap) {
            const cookieString = stickyProcesor.createCookieString(sticky_1.STICKY_STORAGE_KEY.VARIATION_MAP, this.variationMap);
            this.additionalCookies.push(cookieString);
        }
        if (this.visitorProfile) {
            const toSave = (0, attributes_1.getStickyAttributes)().reduce((acc, attribute) => {
                if (this.visitorProfile['profile'][attribute] !== undefined) {
                    acc['profile'][attribute] =
                        this.visitorProfile['profile'][attribute];
                    acc['metadata'][attribute] =
                        this.visitorProfile['metadata'][attribute] || {};
                }
                return acc;
            }, {
                profile: {},
                metadata: {},
            });
            const cookieString = stickyProcesor.createCookieString(sticky_1.STICKY_STORAGE_KEY.VISITOR_PROFILE, toSave);
            this.additionalCookies.push(cookieString);
        }
    }
    async decide(context) {
        this.loadStickyData();
        // Add segmentation features to track events
        await (0, attributes_1.enrichProfile)((0, attributes_1.getShouldTrackAttributes)(), this.visitorProfile, this.inputs);
        //init tracker
        let tracker = new track_1.Track(context, this.inputs, this.config, this.packageVersion, this.inputs.visitorId);
        //generate and send track client activation event
        if (this.isSnippetless) {
            tracker.trackClientActivation(this.logger);
        }
        else {
            this.logger.debug('Not sending client activation event');
        }
        // TODO: filter out undefined views, and log if we do that
        let viewsToEvaluate = this.config.views.filter(view => this.queue.unevaluatedViewsIds.includes(view.id));
        // const viewStates: Record<string, boolean> = getViewStates(
        const viewStates = (0, index_2.getViewStates)({ views: viewsToEvaluate, dynamicWebsiteSupport: false }, this.inputs, this.logger);
        this.logger.debug('ViewStates', viewStates);
        this.queue.unevaluatedViewsIds = [];
        this.results.activeViewsIds = [];
        this.results.inactiveViewsIds = [];
        viewsToEvaluate.forEach(({ id }) => {
            switch (viewStates[id]) {
                case true:
                    this.results.activeViewsIds.push(id);
                    break;
                case false:
                    this.results.inactiveViewsIds.push(id);
                    break;
                default:
                    this.queue.unevaluatedViewsIds.push(id);
                    break;
            }
        });
        // let audiencesToEvaluate = this.queue.unevaluatedAudiences;
        // TODO: Audiences are not evaluated as part of getViewStates, they are evaluated as part of decideExperiments below
        // Modify it to determine which audiences have been evaluated
        // https://github.com/optimizely/edge-services/blob/master/packages/edge-decider/src/decider/index.js#L83
        // let newlyActiveAudiences: Audience[] = [];
        // let newlyInactiveAudiences: Audience[] = [];
        // let stillUnevaluatedAudiences: Audience[] = [];
        // this.queue.unevaluatedAudiences = stillUnevaluatedAudiences;
        // this.results.activeAudiences.push(...newlyActiveAudiences);
        // this.results.inactiveAudiences.push(...newlyInactiveAudiences);
        this.logger.debug('======= Active Views ========', this.results.activeViewsIds);
        const activeViewIds = this.results.activeViewsIds;
        this.results.decisions = await (0, index_1.decideExperiments)(activeViewIds, this.inputs.visitorId || '', this.config, this.visitorProfile, this.inputs, this.variationMap, this.logger);
        this.logger.debug('======== Decisions ==========', this.results.decisions);
        //generate and send campaign activated and pageview events
        let latestEvent = Date.now();
        if (this.isSnippetless) {
            tracker.trackCampaignPageActivated(
            // filter out sticky decisions
            this.results.decisions.filter(decision => !decision.stickyDecision), this.logger, latestEvent);
        }
        else {
            this.logger.debug('Not sending track campaign event because snippetless is false');
        }
        const decisionAnalysis = {
            activeVariationIds: new Set(),
            activeLayerIds: new Set(),
            inactiveLayerIds: new Set(),
        };
        this.results.decisions.reduce((acc, d) => {
            if (d.experimentId !== null) {
                acc.activeLayerIds.add(d.layerId);
                if (d.variationId) {
                    acc.activeVariationIds.add(d.variationId);
                    this.variationMap[d.layerId] = Object.fromEntries([
                        [d.experimentId, d.variationId],
                    ]);
                }
            }
            else {
                acc.inactiveLayerIds.add(d.layerId);
            }
            return acc;
        }, decisionAnalysis);
        this.logger.debug('========= activeVariationIds ============', decisionAnalysis.activeVariationIds);
        this.logger.debug('========= activeLayerIds ================', decisionAnalysis.activeLayerIds);
        this.logger.debug('========= inactiveLayerIds ==============', decisionAnalysis.inactiveLayerIds);
        this.queue.undecidedLayerIds = this.queue.undecidedLayerIds.filter(exp => !decisionAnalysis.activeLayerIds.has(exp) &&
            !decisionAnalysis.inactiveLayerIds.has(exp));
        this.results.inactiveLayerIds = Array.from(decisionAnalysis.inactiveLayerIds);
        this.results.activeLayerIds = Array.from(decisionAnalysis.activeLayerIds);
        for (let expId of decisionAnalysis.activeLayerIds) {
            const exp = this.config.layers.find(layer => layer.id === expId);
            if (exp === undefined) {
                // TODO: log an undefined experience ID
                continue;
            }
            this.queue.unappliedChangesets.push(...this.getChangesFromVariation(exp, decisionAnalysis.activeVariationIds));
        }
        this.saveStickyData();
        let sessionCookie = (0, cookie_1.getSessionCookieString)(this.inputs.first_session ? latestEvent : '0');
        this.logger.debug('Setting cookie on header:', sessionCookie);
        this.additionalCookies.push(sessionCookie);
    }
    getChangesFromVariation(exp, variationIds) {
        for (let e of exp.experiments) {
            for (let v of e.variations) {
                if (variationIds.has(v.id)) {
                    // TODO:  filter by v.actions[].viewId
                    if (v.actions.length > 0 &&
                        v.actions[0].changes.length > 0) {
                        return v.actions[0].changes;
                    }
                }
            }
        }
        return [];
    }
    applyTransforms(rewriter, existingSnippet) {
        const transformAnalysis = {
            changesApplied: new Set(),
            toApply: new Set(),
            unapplied: new Set(),
        };
        // TODO: Filter only include those that can be applied on the edge (all except custom css, JS?)
        this.queue.unappliedChangesets.forEach(change => {
            if (change.transform.length == 0 && change.src) {
                // Support for async changes
                transformAnalysis.unapplied.add(change);
            }
            change.transform.forEach(t => {
                const [_, __, op] = t;
                // TODO:  Update client-experimetnation library with new OP_CUSTOM value
                if ([client_experimentation_1.Experimentation.Operations.CustomJs, 99].includes(op)) {
                    transformAnalysis.unapplied.add(change);
                }
                else {
                    transformAnalysis.changesApplied.add(change);
                    transformAnalysis.toApply.add(t);
                }
            });
        });
        this.results.appliedChangesets = Array.from(transformAnalysis.changesApplied);
        this.queue.unappliedChangesets = Array.from(transformAnalysis.unapplied);
        if (existingSnippet && existingSnippet !== 'keep') {
            this.logger.debug('existingSnippet parameter set to:', existingSnippet);
            let snippetChange = this.removeSnippetChange(existingSnippet);
            transformAnalysis.changesApplied.add(snippetChange);
            snippetChange.transform.forEach(t => {
                transformAnalysis.toApply.add(t);
            });
        }
        return (0, transformer_1.applyTransformations)(rewriter, Array.from(transformAnalysis.toApply));
    }
    shouldDeferBrowserJs() {
        // If the visitorIdLocator is not valid, we should defer the browserJS
        if (!(0, visitor_id_1.isValidEdgeLocator)(this.config.visitorIdLocator)) {
            return true;
        }
        // This is just checking if there is not unapplied sync changes and any unevaluated views
        // It may get more complex in the future
        const possiblePendingChanges = this.queue.unappliedChangesets.filter(change => !change.src);
        return (possiblePendingChanges.length == 0 &&
            this.queue.unevaluatedViewsIds.length == 0);
    }
    async applyBrowserJs(rewriter, cacheTTL, position, nonce) {
        let browserJS = await this.computeBrowserJS(cacheTTL);
        const shouldDefer = this.shouldDeferBrowserJs();
        browserJS = (0, browserDefines_1.setSnippetMetadata)(browserJS, {
            logs: this.logger.getLogs(),
            visitorData: {
                id: this.inputs.visitorId,
                // TODO: Add more visitor attributes data for sticky audiences
            },
        });
        rewriter.on(shouldDefer || position == 'bottom' ? 'body' : 'head', new Applicator(browserJS, nonce, shouldDefer ? 'bottom' : position));
        return rewriter;
    }
    async computeBrowserJS(cacheTTL = 0) {
        let computeHash = '';
        if (cacheTTL > 0) {
            computeHash = await (0, utils_1.digestTextSha256)(this.deployId +
                this.packageVersion +
                this.config.revision +
                JSON.stringify(this.queue) +
                JSON.stringify(this.results));
            const cachedBrowserJs = await (0, utils_1.getCachedString)(computeHash);
            if (cachedBrowserJs) {
                this.logger.info('Edge Rewriter - BrowserJS with cache status:  HIT');
                return cachedBrowserJs;
            }
        }
        // Calculate over all views to add all the triggers and view match functions
        const newDefines = (0, browserDefines_1.recalculateDefines)(this.generatedDefines, this.config, this.queue, this.results);
        const snippet = (0, edge_delivery_browser_modules_1.default)(newDefines);
        if (cacheTTL > 0) {
            this.logger.info('Edge Rewriter - BrowserJS with cache status:  REVALIDATED');
            await (0, utils_1.saveCachedString)(computeHash, snippet, cacheTTL);
        }
        else {
            this.logger.info('Edge Rewriter - BrowserJS with cache status:  DYNAMIC');
        }
        return snippet;
    }
    removeSnippetChange(existingSnippetParam) {
        const SNIPPET_REGEX = /https:\/\/(?:cdn\.optimizely\.com|optimizely-staging\.s3\.amazonaws\.com)\/(?:js|public\/[\d]*\/s)\/[\w]*\.js$/;
        let DATA = {};
        DATA['id'] = 'remove existing snippet';
        DATA['selector'] = 'script[src]';
        DATA['attributes'] = { src: SNIPPET_REGEX };
        if (existingSnippetParam === 'comment') {
            DATA['type'] = 'append';
            DATA['transform'] = [
                [1, 'script[src]', 1, '<!--'],
                [1, 'script[src]', 2, '-->'],
            ];
        }
        else if (existingSnippetParam === 'remove') {
            DATA['type'] = 'remove';
            DATA['transform'] = [[1, 'script[src]', 7]];
        }
        return new models_1.Change(DATA);
    }
}
exports.CfEdgeRelay = CfEdgeRelay;
