"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyExperiments = void 0;
const data_1 = require("./data");
const relay_1 = require("./relay");
const models_1 = require("./models");
const enums_1 = require("./enums");
const logger_1 = require("./edge-decider/logger");
const cookie_1 = __importDefault(require("cookie"));
const constants_1 = require("./edge-decider/constants");
const env_1 = require("./edge-decider/env");
const visitor_id_1 = require("./visitor_id");
const isProdEnv = (env) => env === enums_1.EnvironmentsEnum.prod;
const isDevEnv = (env) => env === enums_1.EnvironmentsEnum.dev;
function printMode(mode) {
    const envMode = mode === enums_1.EnvironmentsEnum.dev
        ? `${enums_1.EnvironmentsEnum.dev.toUpperCase()} `
        : enums_1.EnvironmentsEnum.prod.toUpperCase();
    console.log(`=================================================================`);
    console.log(`||                           ${envMode} MODE                         ||`);
    console.log(`=================================================================`);
}
function validateOptions(options) {
    const parsedOptions = new models_1.Options(options);
    let errors = [];
    !options.environment?.trim() && errors.push('environment is required');
    options.environment?.trim() !== enums_1.EnvironmentsEnum.dev &&
        options.environment?.trim() !== enums_1.EnvironmentsEnum.prod &&
        errors.push('environment should be dev or prod');
    options.accountId && isNaN(options.accountId) && errors.push('accountId'); //only integers
    if (options.accountId) {
        parsedOptions.accountId = !isNaN(options.accountId)
            ? Number(options.accountId)
            : undefined;
    }
    options.snippetId &&
        !validSnippetId(options.snippetId) &&
        errors.push('snippetId'); //string or integer
    parsedOptions.snippetId = options.snippetId;
    options.kvNamespace &&
        typeof options.kvNamespace.get !== 'function' &&
        errors.push('kvNamespace');
    parsedOptions.kvNamespace = options.kvNamespace;
    options.isProd &&
        typeof options.isProd !== 'boolean' &&
        errors.push('isProd'); //only boolean
    parsedOptions.isProd =
        typeof options.isProd === 'boolean' ? options.isProd : true;
    parsedOptions.isSnippetless =
        typeof options.isSnippetless === 'boolean'
            ? options.isSnippetless
            : false;
    printMode(options.environment);
    options.nonce &&
        (typeof options.nonce !== 'string' ||
            !stringIsAlphaNumeric(options.nonce)) &&
        errors.push('nonce'); //only string
    parsedOptions.nonce = options.nonce;
    parsedOptions.position = options.position || 'top';
    parsedOptions.cacheTTLs = options.cacheTTLs;
    parsedOptions.deployId = options.deployId || '';
    parsedOptions.fallback = options.fallback || '';
    parsedOptions.existingSnippet = options.existingSnippet || '';
    parsedOptions.control = options.control;
    parsedOptions.rewriter = options.rewriter;
    parsedOptions.environment = options.environment;
    isProdEnv(options.environment) &&
        (options.dev_protocol?.trim() ||
            options.dev_port?.trim() ||
            options.dev_pathname?.trim()) &&
        errors.push('unused dev variables');
    if (isDevEnv(options.environment)) {
        parsedOptions.dev_protocol = options.dev_protocol?.trim() ?? 'https';
        parsedOptions.dev_host = options.dev_host?.trim() ?? 'example.com';
        parsedOptions.dev_port = options.dev_port?.trim();
        parsedOptions.dev_pathname = options.dev_pathname?.trim();
    }
    isValidJSON(options.DATA) && errors.push('DATA'); //only valid JSON
    parsedOptions.DATA = options.DATA;
    return [parsedOptions, errors];
}
function validSnippetId(value) {
    return (typeof value === 'string' ||
        (typeof value === 'number' && !isNaN(value)));
}
function stringIsAlphaNumeric(value) {
    return /^[A-Za-z0-9]+$/.test(value);
}
function isValidJSON(data) {
    try {
        JSON.parse(data);
        return true;
    }
    catch (e) {
        return false;
    }
}
function getDevUrl(localUrl, pathname) {
    let url = '';
    if (!localUrl?.dev_host?.trim()) {
        return url;
    }
    if (localUrl.dev_protocol?.trim()) {
        url = `${localUrl.dev_protocol?.trim()}://`;
    }
    if (localUrl.dev_host?.trim()) {
        url += localUrl.dev_host?.trim();
    }
    if (!localUrl.dev_port?.trim()) {
        url += '/';
    }
    if (localUrl.dev_port?.trim()) {
        url += `:${localUrl.dev_port?.trim()}/`;
    }
    if (pathname?.trim()) {
        url += `${pathname?.trim().slice(1)}/`;
    }
    if (!pathname?.trim() && localUrl.dev_pathname) {
        url += `${localUrl.dev_pathname}/`;
    }
    return url;
}
function getTargetRequest(request, isDev, devUrl) {
    let requestUrl = new URL(request.url);
    if (isDev) {
        const url = getDevUrl(devUrl, requestUrl.pathname);
        requestUrl = new URL(url);
    }
    return new Request(requestUrl, request);
}
async function getControl(targetRequest, control, cacheTTL, context) {
    const logger = (0, logger_1.getLogger)(context);
    logger.debug('Control - Starting...');
    if (control) {
        logger.info('Control - Returning from option parameter');
        return control;
    }
    logger.debug('Control - Fetching from targetRequest...');
    const response = await fetch(targetRequest, {
        cf: {
            cacheTtl: cacheTTL,
        },
    });
    logger.debug('Control - ... completed. ');
    logger.info('Control - Cache Status', response.headers.get('cf-cache-status'));
    return response;
}
async function getEdgeRewriter(targetRequest, context, options) {
    const logger = (0, logger_1.getLogger)(context);
    logger.debug('Edge Rewriter - Starting...');
    logger.debug('Edge Rewriter - Getting input data ... ');
    const inputs = (0, env_1.getInputData)(targetRequest, logger);
    logger.debug('Edge Rewriter - ... Got input data.');
    logger.setLogLevel(inputs.logLevel);
    let DATA = options.DATA;
    if (!DATA && options.kvNamespace && options.snippetId) {
        try {
            logger.info('Getting data from KV namespace');
            const snippetName = options.accountId
                ? `${options.accountId}/s/${options.snippetId.toString()}`
                : options.snippetId.toString();
            const data = await options.kvNamespace.get(snippetName);
            DATA = data ? JSON.parse(data) : null;
            logger.debug('Got data from KV:', DATA);
        }
        catch (e) {
            console.log('Error fetching or parsing JSON DATA from KV config', e);
        }
    }
    if (!DATA) {
        DATA = await (0, data_1.getData)(options.accountId, options.snippetId, options.isProd, options.cacheTTLs?.dataTTL, logger);
    }
    else {
        logger.info('Edge Rewriter - Getting DATA from option parameter.');
    }
    const relay = new relay_1.CfEdgeRelay(DATA, options, inputs, logger);
    if ((0, visitor_id_1.isValidEdgeLocator)(relay.config.visitorIdLocator)) {
        logger.debug('Edge Rewriter - Calculating visitor id...');
        const visitorId = (0, visitor_id_1.getVisitorId)(relay.config.visitorIdLocator, inputs, logger);
        if (!relay.config.visitorIdLocator &&
            visitorId &&
            (0, visitor_id_1.canSetOEUCookie)(inputs)) {
            // Only try to set the cookie for OEUId visitor type
            const visitorIdCookie = (0, visitor_id_1.getVisitorIdCookieString)(visitorId, inputs.cookieDomain, inputs.cookieExpirationDays, (0, env_1.shouldUseSecureCrossSiteCookies)(relay.generatedDefines), logger);
            relay.additionalCookies.push(visitorIdCookie);
        }
        relay.inputs.visitorId = visitorId;
        logger.debug('Edge Rewriter - .... Visitor id calculated.', visitorId);
        // Evaluate all relevant Views and Audiences
        // And determine the current unappliedChangesets (transforms)
        logger.debug('Edge Rewriter - Deciding ... ');
        await relay.decide(context);
        logger.debug('Edge Rewriter - ... Decided. ');
    }
    else {
        logger.warning('The visitorIdLocator type is not supported in the Edge Relay.  Not deciding on the edge.');
    }
    const rewriter = options.rewriter || new HTMLRewriter();
    logger.debug('Edge Rewriter - Creating transforms ... ');
    // Apply all decided transforms that can be applied presently
    relay.applyTransforms(rewriter, options.existingSnippet);
    logger.debug('Edge Rewriter - Created.');
    let newHeaders = relay.getAdditionalHeaders();
    // Compile the Javascript necessary for executing any remaining functionality on the browser
    if (!options.isSnippetless) {
        logger.debug('Edge Rewriter - Applying browser transforms ... ');
        await relay.applyBrowserJs(rewriter, options.cacheTTLs?.browserTTL, options.position, options.nonce);
        logger.info('Edge Rewriter - Browser JS added.');
    }
    return { rewriter, newHeaders };
}
async function applyExperiments(request, context, options) {
    const logger = (0, logger_1.getLogger)(context);
    logger.debug('Validating options');
    const [edgeOptions, errors] = validateOptions(options);
    if (errors.length > 0) {
        throw new Error(`Invalid options provided, the following fields have errors: ${errors.join(', ')}`);
    }
    const devUrl = isDevEnv(edgeOptions.environment)
        ? {
            dev_host: edgeOptions.dev_host,
            dev_protocol: edgeOptions.dev_protocol,
            dev_pathname: edgeOptions.dev_pathname,
            dev_port: edgeOptions.dev_port,
        }
        : {};
    const targetRequest = getTargetRequest(request, isDevEnv(edgeOptions.environment), devUrl);
    let control = edgeOptions.control;
    let edgeRewriterResult;
    let editMode = checkIfEditMode(targetRequest);
    logger.debug(`Checking if were in edit mode: ${editMode}`);
    if (!editMode) {
        try {
            [control, edgeRewriterResult] = await Promise.all([
                getControl(targetRequest, control, edgeOptions.cacheTTLs?.targetTTL, logger),
                getEdgeRewriter(targetRequest, context, edgeOptions),
            ]);
            const { rewriter, newHeaders } = edgeRewriterResult;
            // Add additional headers to the control response
            const headers = new Headers(control.headers);
            newHeaders.forEach((value, key) => {
                if (key.toLowerCase() === 'set-cookie') {
                    headers.append(key, value);
                }
                else {
                    headers.set(key, value);
                }
            });
            return rewriter.transform(new Response(control?.body, {
                ...control,
                headers: headers,
            }));
        }
        catch (e) {
            const error = e;
            // If an error occurs, or if a timeout occurs, etc., bail and don't apply experiments
            logger.error(`An error occurred, using fallback strategy: ${edgeOptions.fallback}.`, error.message);
            error.stack && logger.error(error.stack);
            // 1. fallback error, throw the exception
            if (edgeOptions.fallback && edgeOptions.fallback == 'error') {
                throw error;
            }
        }
    }
    //2. fallback snippet, return injected snippet version
    if (edgeOptions.fallback || editMode) {
        logger.debug('Attempting to return snippet injected page');
        let newControl = await getControl(targetRequest, control, edgeOptions.cacheTTLs?.targetTTL, logger);
        let transformed = injectSnippet(edgeOptions, newControl, logger);
        if (transformed) {
            return transformed;
        }
    }
    //3. fallback null, return control
    if (control) {
        logger.info('Returning Control');
        return control;
    }
    logger.info('Returning fetch request');
    return fetch(request);
}
exports.applyExperiments = applyExperiments;
function injectSnippet(edgeOptions, control, logger) {
    if (edgeOptions.snippetId !== undefined) {
        let configUrl = (0, data_1.getSnippetUrl)(edgeOptions.accountId, edgeOptions.snippetId, edgeOptions.isProd);
        logger.debug(`Generated url: ${configUrl}`);
        let snippetRewriter = edgeOptions.rewriter
            ? edgeOptions.rewriter
            : new HTMLRewriter();
        snippetRewriter.on('head', new SnippetInjectApplicator(configUrl, edgeOptions.nonce));
        logger.info('Successfully injected snippet on page');
        if (control) {
            logger.info('Returning transformed page');
            return snippetRewriter.transform(control);
        }
        return null;
    }
}
function checkIfEditMode(targetRequest) {
    let cookies = cookie_1.default.parse(targetRequest.headers.get('Cookie') || '');
    if (cookies[constants_1.OptimizelyCookies.OPTIMIZELY_TOKEN]) {
        //preview mode
        return true;
    }
    let targetPageURLString = targetRequest.url;
    //logger.updateMetadata('targetPageURLString', targetPageURLString);
    let targetPageURL;
    try {
        targetPageURL = targetPageURLString
            ? new URL(targetPageURLString)
            : null;
    }
    catch (e) {
        // This is the only type of error URL constructor should throw: https://developer.mozilla.org/en-US/docs/Web/API/URL/URL#Syntax
        if (e instanceof TypeError) {
            throw new Error(`Target page URL is not a valid URL string: ${targetPageURLString}`);
        }
        throw e;
    }
    //logger.updateMetadata('targetPageURL', targetPageURL);
    if (!targetPageURL) {
        throw new Error('Target page URL is null.');
    }
    let requestUrl = new URL(targetPageURLString);
    let requestParams = requestUrl.searchParams;
    let p13n_param = requestParams.get('optimizely_p13n'); //edit mode
    let token_param = requestParams.get('optimizely_token'); //preview mode
    if (p13n_param || token_param) {
        return true;
    }
    return false;
}
class SnippetInjectApplicator {
    constructor(snippet, nonce) {
        this.snippetUrl = snippet;
        this.scriptNonce = nonce;
    }
    element(element) {
        var nonceString = this.scriptNonce
            ? ` nonce="${this.scriptNonce}"`
            : '';
        var injectFn = element.append;
        injectFn.call(element, `<script type="text/javascript"${nonceString} src="${this.snippetUrl}"></script>`, { html: true });
    }
}
