import {
    Deferred
} from '#core/data-structures/promise';
import {
    parseQueryString
} from '#core/types/string/url';
import {
    WindowInterface
} from '#core/window/interface';

import {
    isExperimentOn
} from '#experiments';

import {
    Services
} from '#service';

import {
    dev,
    user,
    userAssert
} from '#utils/log';

import {
    getMode
} from './mode';
import {
    addParamsToUrl,
    isProxyOrigin,
    parseUrlDeprecated
} from './url';

const TIMEOUT_VALUE = 8000;

let trackImpressionPromise = null;

const DEFAULT_APPEND_URL_PARAM = ['gclid', 'gclsrc'];

/**
 * These domains are trusted with more sensitive viewer operations such as
 * sending impression requests. If you believe your domain should be here,
 * file the issue on GitHub to discuss. The process will be similar
 * (but somewhat more stringent) to the one described in the [3p/README.md](
 * https://github.com/ampproject/amphtml/blob/main/3p/README.md)
 *
 * @type {!Array<!RegExp>}
 */
const TRUSTED_REFERRER_HOSTS = [
    /**
     * Twitter's link wrapper domains:
     * - t.co
     */
    /^t.co$/,
];

/**
 * A function to get the trackImpressionPromise;
 * @return {!Promise}
 */
export function getTrackImpressionPromise() {
    return userAssert(trackImpressionPromise, 'E#19457 trackImpressionPromise');
}

/**
 * Function that reset the trackImpressionPromise only for testing
 * @visibleForTesting
 */
export function resetTrackImpressionPromiseForTesting() {
    trackImpressionPromise = null;
}

/**
 * Emit a HTTP request to a destination defined on the incoming URL.
 * Launched for trusted viewer. Otherwise guarded by experiment.
 * @param {!Window} win
 */
export function maybeTrackImpression(win) {
    const deferred = new Deferred();
    const {
        promise,
        resolve: resolveImpression
    } = deferred;

    trackImpressionPromise = Services.timerFor(win)
        .timeoutPromise(TIMEOUT_VALUE, promise, 'TrackImpressionPromise timeout')
        .catch((error) => {
            dev().warn('IMPRESSION', error);
        });

    const viewer = Services.viewerForDoc(win.document.documentElement);
    const isTrustedViewerPromise = viewer.isTrustedViewer();
    const isTrustedReferrerPromise = viewer
        .getReferrerUrl()
        .then((referrer) => isTrustedReferrer(referrer));
    Promise.all([isTrustedViewerPromise, isTrustedReferrerPromise]).then(
        (results) => {
            const isTrustedViewer = results[0];
            const isTrustedReferrer = results[1];
            // Enable the feature in the case of trusted viewer,
            // or trusted referrer
            // or with experiment turned on
            if (!isTrustedViewer &&
                !isTrustedReferrer &&
                !isExperimentOn(win, 'alp')
            ) {
                resolveImpression();
                return;
            }

            const replaceUrlPromise = handleReplaceUrl(win);
            const clickUrlPromise = handleClickUrl(win);

            Promise.all([replaceUrlPromise, clickUrlPromise]).then(
                () => {
                    resolveImpression();
                },
                () => {}
            );
        }
    );
}

/**
 * Signal that impression tracking is not relevant in this environment.
 */
export function doNotTrackImpression() {
    trackImpressionPromise = Promise.resolve();
}

/**
 * Handle the getReplaceUrl and return a promise when url is replaced Only
 * handles replaceUrl when viewer indicates AMP to do so. Viewer should indicate
 * by setting the legacy replaceUrl init param and add `replaceUrl` to its
 * capability param. Future plan is to change the type of legacy init replaceUrl
 * param from url string to boolean value. Please NOTE replaceUrl and adLocation
 * will never arrive at same time, so there is no race condition on the order of
 * handling url replacement.
 * @param {!Window} win
 * @return {!Promise}
 */
function handleReplaceUrl(win) {
    const viewer = Services.viewerForDoc(win.document.documentElement);

    // ReplaceUrl substitution doesn't have to wait until the document is visible
    if (!viewer.getParam('replaceUrl')) {
        // The init replaceUrl param serve as a signal on whether replaceUrl is
        // required for this doc.
        return Promise.resolve();
    }

    if (!viewer.hasCapability('replaceUrl')) {
        // If Viewer is not capability of providing async replaceUrl, use the legacy
        // init replaceUrl param.
        viewer.replaceUrl(viewer.getParam('replaceUrl') || null);
        return Promise.resolve();
    }

    // request async replaceUrl is viewer support getReplaceUrl.
    return viewer
        .sendMessageAwaitResponse('getReplaceUrl', /* data */ undefined)
        .then(
            (response) => {
                if (!response || typeof response != 'object') {
                    dev().warn('IMPRESSION', 'get invalid replaceUrl response');
                    return;
                }
                viewer.replaceUrl(response['replaceUrl'] || null);
            },
            (err) => {
                dev().warn('IMPRESSION', 'Error request replaceUrl from viewer', err);
            }
        );
}

/**
 * @param {string} referrer
 * @return {boolean}
 * @visibleForTesting
 */
export function isTrustedReferrer(referrer) {
    const url = parseUrlDeprecated(referrer);
    if (url.protocol != 'https:') {
        return false;
    }
    return TRUSTED_REFERRER_HOSTS.some((th) => th.test(url.hostname));
}

/**
 * Perform the impression request if it has been provided via
 * the click param in the viewer arguments. Returns a promise.
 * @param {!Window} win
 * @return {!Promise}
 */
function handleClickUrl(win) {
    const ampdoc = Services.ampdoc(win.document.documentElement);
    const viewer = Services.viewerForDoc(ampdoc);

    /** @const {?string} */
    const clickUrl = viewer.getParam('click');
    if (!clickUrl) {
        return Promise.resolve();
    }

    if (clickUrl.indexOf('https://') != 0) {
        user().warn(
            'IMPRESSION',
            'click fragment param should start with https://. Found ',
            clickUrl
        );
        return Promise.resolve();
    }

    if (WindowInterface.getLocation(win).hash) {
        // This is typically done using replaceState inside the viewer.
        // If for some reason it failed, get rid of the fragment here to
        // avoid duplicate tracking.
        WindowInterface.getLocation(win).hash = '';
    }

    // TODO(@zhouyx) need test with a real response.
    return ampdoc
        .whenFirstVisible()
        .then(() => {
            return invoke(win, dev().assertString(clickUrl));
        })
        .then((response) => {
            applyResponse(win, response);
        })
        .catch((err) => {
            user().warn('IMPRESSION', 'Error on request clickUrl: ', err);
        });
}

/**
 * Send the url to ad server and wait for its response
 * @param {!Window} win
 * @param {string} clickUrl
 * @return {!Promise<?JsonObject>}
 */
function invoke(win, clickUrl) {
    if (getMode().localDev && !getMode().test) {
        clickUrl = 'http://localhost:8000/impression-proxy?url=' + clickUrl;
    }
    return Services.xhrFor(win)
        .fetchJson(clickUrl, {
            credentials: 'include',
        })
        .then((res) => {
            // Treat 204 no content response specially
            if (res.status == 204) {
                return null;
            }
            return res.json();
        });
}

/**
 * parse the response back from ad server
 * Set for analytics purposes
 * @param {!Window} win
 * @param {?JsonObject} response
 */
function applyResponse(win, response) {
    if (!response) {
        return;
    }

    const adLocation = response['location'];
    const adTracking = response['tracking_url'];

    // If there is a tracking_url, need to track it
    // Otherwise track the location
    const trackUrl = adTracking || adLocation;

    if (trackUrl && !isProxyOrigin(trackUrl)) {
        // To request the provided trackUrl for tracking purposes.
        new Image().src = trackUrl;
    }

    // Replace the location href params with new location params we get (if any).
    if (adLocation) {
        if (!win.history.replaceState) {
            return;
        }

        const viewer = Services.viewerForDoc(win.document.documentElement);
        const currentHref = WindowInterface.getLocation(win).href;
        const url = parseUrlDeprecated(adLocation);
        const params = parseQueryString(url.search);
        const newHref = addParamsToUrl(currentHref, params);
        // TODO: Avoid overwriting the fragment parameter.
        win.history.replaceState(null, '', newHref);
        viewer.maybeUpdateFragmentForCct();
    }
}

/**
 * Return a promise that whether appending extra url params to outgoing link is
 * required.
 * @param {!./service/ampdoc-impl.AmpDoc} ampdoc
 * @return {!Promise<boolean>}
 */
export function shouldAppendExtraParams(ampdoc) {
    return ampdoc.whenReady().then(() => {
        return !!ampdoc
            .getBody()
            .querySelector('amp-analytics[type=googleanalytics]');
    });
}

/**
 * Return the extra url params string that should be appended to outgoing link
 * @param {!Window} win
 * @param {!Element} target
 * @return {string}
 */
export function getExtraParamsUrl(win, target) {
    // Get an array with extra params that needs to append.
    const url = parseUrlDeprecated(WindowInterface.getLocation(win).href);
    const params = parseQueryString(url.search);
    const appendParams = [];
    for (let i = 0; i < DEFAULT_APPEND_URL_PARAM.length; i++) {
        const param = DEFAULT_APPEND_URL_PARAM[i];
        if (typeof params[param] !== 'undefined') {
            appendParams.push(param);
        }
    }

    // Check if the param already exists
    const additionalUrlParams = target.getAttribute('data-amp-addparams');
    let {
        href
    } = target;
    if (additionalUrlParams) {
        href = addParamsToUrl(href, parseQueryString(additionalUrlParams));
    }
    const loc = parseUrlDeprecated(href);
    const existParams = parseQueryString(loc.search);
    for (let i = appendParams.length - 1; i >= 0; i--) {
        const param = appendParams[i];
        if (typeof existParams[param] !== 'undefined') {
            appendParams.splice(i, 1);
        }
    }
    return getQueryParamUrl(appendParams);
}

/**
 * Helper method to convert an query param array to string
 * @param {!Array<string>} params
 * @return {string}
 */
function getQueryParamUrl(params) {
    let url = '';
    for (let i = 0; i < params.length; i++) {
        const param = params[i];
        url +=
            i == 0 ?
            `${param}=QUERY_PARAM(${param})` :
            `&${param}=QUERY_PARAM(${param})`;
    }
    return url;
}