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;
}
