import { LOAD_ACTION_ATTR_SELECTOR } from './constants';
import { dispatchSnowplowEvent } from './dispatch_snowplow_event';
import getStandardContext from './get_standard_context';
import {
  getEventHandlers,
  createEventPayload,
  renameKey,
  addExperimentContext,
  getReferrersCache,
  addReferrersCacheEntry,
} from './utils';

export default class Tracking {
  static queuedEvents = [];
  static initialized = false;

  /**
   * (Legacy) Determines if tracking is enabled at the user level.
   * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT.
   *
   * @returns {Boolean}
   */
  static trackable() {
    return !['1', 'yes'].includes(
      window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
    );
  }

  /**
   * Determines if Snowplow is available/enabled.
   *
   * @returns {Boolean}
   */
  static enabled() {
    return typeof window.snowplow === 'function' && this.trackable();
  }

  /**
   * Dispatches a structured event per our taxonomy:
   * https://docs.gitlab.com/ee/development/snowplow/index.html#structured-event-taxonomy.
   *
   * If the library is not initialized and events are trying to be
   * dispatched (data-attributes, load-events), they will be added
   * to a queue to be flushed afterwards.
   *
   * @param  {...any} eventData defined event taxonomy
   * @returns {undefined|Boolean}
   */
  static event(...eventData) {
    if (!this.enabled()) {
      return false;
    }

    if (!this.initialized) {
      this.queuedEvents.push(eventData);
      return false;
    }

    return dispatchSnowplowEvent(...eventData);
  }

  /**
   * Dispatches any event emitted before initialization.
   *
   * @returns {undefined}
   */
  static flushPendingEvents() {
    this.initialized = true;

    while (this.queuedEvents.length) {
      dispatchSnowplowEvent(...this.queuedEvents.shift());
    }
  }

  /**
   * Attaches event handlers for data-attributes powered events.
   *
   * @param {String} category - the default category for all events
   * @param {HTMLElement} parent - element containing data-attributes
   * @returns {Array}
   */
  static bindDocument(category = document.body.dataset.page, parent = document) {
    if (!this.enabled() || parent.trackingBound) {
      return [];
    }

    // eslint-disable-next-line no-param-reassign
    parent.trackingBound = true;

    const handlers = getEventHandlers(category, (...args) => this.event(...args));
    handlers.forEach((event) => parent.addEventListener(event.name, event.func));

    return handlers;
  }

  /**
   * Attaches event handlers for load-events (on render).
   *
   * @param {String} category - the default category for all events
   * @param {HTMLElement} parent - element containing event targets
   * @returns {Array}
   */
  static trackLoadEvents(category = document.body.dataset.page, parent = document) {
    if (!this.enabled()) {
      return [];
    }

    const loadEvents = parent.querySelectorAll(LOAD_ACTION_ATTR_SELECTOR);

    loadEvents.forEach((element) => {
      const { action, data } = createEventPayload(element);
      this.event(category, action, data);
    });

    return loadEvents;
  }

  /**
   * Enable Snowplow automatic form tracking.
   * The config param requires at least one array of either forms
   * class names, or field name attributes.
   * https://docs.gitlab.com/ee/development/snowplow/index.html#form-tracking.
   *
   * @param {Object} config
   * @param {Array} contexts
   * @returns {undefined}
   */
  static enableFormTracking(config, contexts = []) {
    if (!this.enabled()) {
      return;
    }

    if (!Array.isArray(config?.forms?.allow) && !Array.isArray(config?.fields?.allow)) {
      // eslint-disable-next-line @gitlab/require-i18n-strings
      throw new Error('Unable to enable form event tracking without allow rules.');
    }

    // Ignore default/standard schema
    const standardContext = getStandardContext();
    const userProvidedContexts = contexts.filter(
      (context) => context.schema !== standardContext.schema,
    );

    const mappedConfig = {};
    if (config.forms) {
      mappedConfig.forms = renameKey(config.forms, 'allow', 'whitelist');
    }

    if (config.fields) {
      mappedConfig.fields = renameKey(config.fields, 'allow', 'whitelist');
    }

    const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);

    if (document.readyState === 'complete') {
      enabler();
    } else {
      document.addEventListener('readystatechange', () => {
        if (document.readyState === 'complete') {
          enabler();
        }
      });
    }
  }

  /**
   * Replaces the URL and referrer for the default web context
   * if the replacements are available.
   *
   * @returns {undefined}
   */
  static setAnonymousUrls() {
    const { snowplowPseudonymizedPageUrl: pageUrl } = window.gl;

    if (!pageUrl) {
      return;
    }

    const referrers = getReferrersCache();
    const pageLinks = Object.seal({ url: '', referrer: '', originalUrl: window.location.href });

    pageLinks.url = `${pageUrl}${window.location.hash}`;
    window.snowplow('setCustomUrl', pageLinks.url);

    if (document.referrer) {
      const node = referrers.find((links) => links.originalUrl === document.referrer);

      if (node) {
        pageLinks.referrer = node.url;
        window.snowplow('setReferrerUrl', pageLinks.referrer);
      }
    }

    addReferrersCacheEntry(referrers, pageLinks);
  }

  /**
   * Returns an implementation of this class in the form of
   * a Vue mixin.
   *
   * @param {Object} opts - default options for all events
   * @returns {Object}
   */
  static mixin(opts = {}) {
    return {
      computed: {
        trackingCategory() {
          const localCategory = this.tracking ? this.tracking.category : null;
          return localCategory || opts.category;
        },
        trackingOptions() {
          const options = addExperimentContext(opts);
          return { ...options, ...this.tracking };
        },
      },
      methods: {
        track(action, data = {}) {
          const category = data.category || this.trackingCategory;
          const options = {
            ...this.trackingOptions,
            ...data,
          };

          Tracking.event(category, action, options);
        },
      },
    };
  }
}
