import gtag from "./gtag";
import format from "./format";

/*
Links
https://developers.google.com/gtagjs/reference/api
https://developers.google.com/tag-platform/gtagjs/reference
*/

export interface GaOptions {
  cookieUpdate?: boolean; // true
  cookieExpires?: number; // Default two years 63072000
  cookieDomain?: string; // auto
  cookieFlags?: string;
  userId?: string;
  clientId?: string;
  anonymizeIp?: boolean;
  contentGroup1?: string;
  contentGroup2?: string;
  contentGroup3?: string;
  contentGroup4?: string;
  contentGroup5?: string;
  allowAdFeatures?: boolean;
  allowAdPersonalizationSignals?: boolean;
  nonInteraction?: boolean;
  page?: string;
}

export interface UaEventOptions {
  action: string;
  category: string;
  label?: string;
  value?: number;
  nonInteraction?: boolean;
  transport?: 'beacon' | 'xhr' | 'image';
}

export interface InitOptions {
  trackingId: string;
  gaOptions?: GaOptions | any;
  gtagOptions?: any; // New parameter
}

export class GA4 {
  isInitialized!: boolean;
  _testMode!: boolean;
  _currentMeasurementId!: string;
  _hasLoadedGA!: boolean;
  _isQueuing!: boolean;
  _queueGtag!: any[];

  constructor() {
    this.reset();
  }

  reset = (): void => {
    this.isInitialized = false;

    this._testMode = false;
    this._currentMeasurementId = "";
    this._hasLoadedGA = false;
    this._isQueuing = false;
    this._queueGtag = [];
  };

  _gtag = (...args: any[]): void => {
    if (!this._testMode) {
      if (this._isQueuing) {
        this._queueGtag.push(args);
      } else {
        gtag(...args);
      }
    } else {
      this._queueGtag.push(args);
    }
  };

  gtag(...args: any[]): void {
    this._gtag(...args);
  }

  _loadGA = (
    GA_MEASUREMENT_ID: string,
    nonce?: string,
    gtagUrl: string = "https://www.googletagmanager.com/gtag/js"
  ): void => {
    if (typeof window === "undefined" || typeof document === "undefined") {
      return;
    }

    if (!this._hasLoadedGA) {
      // Global Site Tag (gtag.js) - Google Analytics
      const script = document.createElement("script");
      script.async = true;
      script.src = `${gtagUrl}?id=${GA_MEASUREMENT_ID}`;
      if (nonce) {
        script.setAttribute("nonce", nonce);
      }
      document.body.appendChild(script);

      window.dataLayer = window.dataLayer || [];
      window.gtag = function gtag() {
        window.dataLayer.push(arguments);
      };

      this._hasLoadedGA = true;
    }
  };

  _toGtagOptions = (gaOptions?: GaOptions): Record<string, any> | undefined => {
    if (!gaOptions) {
      return;
    }

    const mapFields: Record<string, string> = {
      // Old https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#cookieUpdate
      // New https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id#cookie_update
      cookieUpdate: "cookie_update",
      cookieExpires: "cookie_expires",
      cookieDomain: "cookie_domain",
      cookieFlags: "cookie_flags", // must be in set method?
      userId: "user_id",
      clientId: "client_id",
      anonymizeIp: "anonymize_ip",
      // https://support.google.com/analytics/answer/2853546?hl=en#zippy=%2Cin-this-article
      contentGroup1: "content_group1",
      contentGroup2: "content_group2",
      contentGroup3: "content_group3",
      contentGroup4: "content_group4",
      contentGroup5: "content_group5",
      // https://support.google.com/analytics/answer/9050852?hl=en
      allowAdFeatures: "allow_google_signals",
      allowAdPersonalizationSignals: "allow_ad_personalization_signals",
      nonInteraction: "non_interaction",
      page: "page_path",
      hitCallback: "event_callback",
    };

    const gtagOptions = Object.entries(gaOptions).reduce(
      (prev, [key, value]) => {
        if (mapFields[key]) {
          prev[mapFields[key]] = value;
        } else {
          prev[key] = value;
        }

        return prev;
      },
      {} as Record<string, any>
    );

    return gtagOptions;
  };

  /**
   *
   * @param {InitOptions[]|string} GA_MEASUREMENT_ID
   * @param {Object} [options]
   * @param {string} [options.nonce]
   * @param {boolean} [options.testMode=false]
   * @param {string} [options.gtagUrl=https://www.googletagmanager.com/gtag/js]
   * @param {GaOptions|any} [options.gaOptions]
   * @param {Object} [options.gtagOptions] New parameter
   */
  initialize = (GA_MEASUREMENT_ID: InitOptions[] | string, options: {
    nonce?: string;
    testMode?: boolean;
    gtagUrl?: string;
    gaOptions?: GaOptions | any;
    gtagOptions?: any;
  } = {}): void => {
    if (!GA_MEASUREMENT_ID) {
      throw new Error("Require GA_MEASUREMENT_ID");
    }

    const initConfigs: InitOptions[] =
      typeof GA_MEASUREMENT_ID === "string"
        ? [{ trackingId: GA_MEASUREMENT_ID }]
        : GA_MEASUREMENT_ID;

    this._currentMeasurementId = initConfigs[0].trackingId;
    const {
      gaOptions,
      gtagOptions,
      nonce,
      testMode = false,
      gtagUrl,
    } = options;
    this._testMode = testMode;

    if (!testMode) {
      this._loadGA(this._currentMeasurementId, nonce, gtagUrl);
    }
    if (!this.isInitialized) {
      this._gtag("js", new Date());

      initConfigs.forEach((config) => {
        const mergedGtagOptions = {
          ...this._toGtagOptions({ ...gaOptions, ...config.gaOptions }),
          ...gtagOptions,
          ...config.gtagOptions,
        };
        if (Object.keys(mergedGtagOptions).length) {
          this._gtag("config", config.trackingId, mergedGtagOptions);
        } else {
          this._gtag("config", config.trackingId);
        }
      });
    }
    this.isInitialized = true;

    if (!testMode) {
      const queues = [...this._queueGtag];
      this._queueGtag = [];
      this._isQueuing = false;
      while (queues.length) {
        const queue = queues.shift();
        this._gtag(...queue);
        if (queue[0] === "get") {
          this._isQueuing = true;
        }
      }
    }
  };

  set = (fieldsObject: any): void => {
    if (!fieldsObject) {
      console.warn("`fieldsObject` is required in .set()");

      return;
    }

    if (typeof fieldsObject !== "object") {
      console.warn("Expected `fieldsObject` arg to be an Object");

      return;
    }

    if (Object.keys(fieldsObject).length === 0) {
      console.warn("empty `fieldsObject` given to .set()");
    }

    this._gaCommand("set", fieldsObject);
  };

  _gaCommandSendEvent = (
    eventCategory: string,
    eventAction: string,
    eventLabel?: string,
    eventValue?: number,
    fieldsObject?: any
  ): void => {
    this._gtag("event", eventAction, {
      event_category: eventCategory,
      event_label: eventLabel,
      value: eventValue,
      ...(fieldsObject && { non_interaction: fieldsObject.nonInteraction }),
      ...this._toGtagOptions(fieldsObject),
    });
  };

  _gaCommandSendEventParameters = (...args: any[]): void => {
    if (typeof args[0] === "string") {
      this._gaCommandSendEvent(...(args.slice(1) as [string, string, string?, number?, any?]));
    } else {
      const {
        eventCategory,
        eventAction,
        eventLabel,
        eventValue,
        // eslint-disable-next-line no-unused-vars
        hitType,
        ...rest
      } = args[0];
      this._gaCommandSendEvent(
        eventCategory,
        eventAction,
        eventLabel,
        eventValue,
        rest
      );
    }
  };

  _gaCommandSendTiming = (
    timingCategory: string,
    timingVar: string,
    timingValue: number,
    timingLabel?: string
  ): void => {
    this._gtag("event", "timing_complete", {
      name: timingVar,
      value: timingValue,
      event_category: timingCategory,
      event_label: timingLabel,
    });
  };

  _gaCommandSendPageview = (page?: string, fieldsObject?: any): void => {
    if (fieldsObject && Object.keys(fieldsObject).length) {
      const { title, location, ...rest } = this._toGtagOptions(fieldsObject) || {};

      this._gtag("event", "page_view", {
        ...(page && { page_path: page }),
        ...(title && { page_title: title }),
        ...(location && { page_location: location }),
        ...rest,
      });
    } else if (page) {
      this._gtag("event", "page_view", { page_path: page });
    } else {
      this._gtag("event", "page_view");
    }
  };

  _gaCommandSendPageviewParameters = (...args: any[]): void => {
    if (typeof args[0] === "string") {
      this._gaCommandSendPageview(...args.slice(1));
    } else {
      const {
        page,
        // eslint-disable-next-line no-unused-vars
        hitType,
        ...rest
      } = args[0];
      this._gaCommandSendPageview(page, rest);
    }
  };

  // https://developers.google.com/analytics/devguides/collection/analyticsjs/command-queue-reference#send
  _gaCommandSend = (...args: any[]): void => {
    const hitType = typeof args[0] === "string" ? args[0] : args[0].hitType;

    switch (hitType) {
      case "event":
        this._gaCommandSendEventParameters(...args);
        break;
      case "pageview":
        this._gaCommandSendPageviewParameters(...args);
        break;
      case "timing":
        this._gaCommandSendTiming(...(args.slice(1) as [string, string, number, string?]));
        break;
      case "screenview":
      case "transaction":
      case "item":
      case "social":
      case "exception":
        console.warn(`Unsupported send command: ${hitType}`);
        break;
      default:
        console.warn(`Send command doesn't exist: ${hitType}`);
    }
  };

  _gaCommandSet = (...args: any[]): void => {
    if (typeof args[0] === "string") {
      args[0] = { [args[0]]: args[1] };
    }
    this._gtag("set", this._toGtagOptions(args[0]));
  };

  _gaCommand = (command: string, ...args: any[]): void => {
    switch (command) {
      case "send":
        this._gaCommandSend(...args);
        break;
      case "set":
        this._gaCommandSet(...args);
        break;
      default:
        console.warn(`Command doesn't exist: ${command}`);
    }
  };

  ga = (...args: any[]): any => {
    if (typeof args[0] === "string") {
      this._gaCommand(...(args as [string, ...any[]]));
    } else {
      const [readyCallback] = args;
      this._gtag("get", this._currentMeasurementId, "client_id", (clientId: string) => {
        this._isQueuing = false;
        const queues = this._queueGtag;

        readyCallback({
          get: (property: string) =>
            property === "clientId"
              ? clientId
              : property === "trackingId"
              ? this._currentMeasurementId
              : property === "apiVersion"
              ? "1"
              : undefined,
        });

        while (queues.length) {
          const queue = queues.shift();
          this._gtag(...queue);
        }
      });

      this._isQueuing = true;
    }

    return this.ga;
  };

  /**
   * @param {UaEventOptions|string} optionsOrName
   * @param {Object} [params]
   */
  event = (optionsOrName: UaEventOptions | string, params?: any): void => {
    if (typeof optionsOrName === "string") {
      this._gtag("event", optionsOrName, this._toGtagOptions(params));
    } else {
      const { action, category, label, value, nonInteraction, transport } =
        optionsOrName;
      if (!category || !action) {
        console.warn("args.category AND args.action are required in event()");

        return;
      }

      // Required Fields
      const fieldObject = {
        hitType: "event",
        eventCategory: format(category),
        eventAction: format(action),
      };

      // Optional Fields
      if (label) {
        (fieldObject as any).eventLabel = format(label);
      }

      if (typeof value !== "undefined") {
        if (typeof value !== "number") {
          console.warn("Expected `args.value` arg to be a Number.");
        } else {
          (fieldObject as any).eventValue = value;
        }
      }

      if (typeof nonInteraction !== "undefined") {
        if (typeof nonInteraction !== "boolean") {
          console.warn("`args.nonInteraction` must be a boolean.");
        } else {
          (fieldObject as any).nonInteraction = nonInteraction;
        }
      }

      if (typeof transport !== "undefined") {
        if (typeof transport !== "string") {
          console.warn("`args.transport` must be a string.");
        } else {
          if (["beacon", "xhr", "image"].indexOf(transport) === -1) {
            console.warn(
              "`args.transport` must be either one of these values: `beacon`, `xhr` or `image`"
            );
          }

          (fieldObject as any).transport = transport;
        }
      }

      this._gaCommand("send", fieldObject);
    }
  };

  send = (fieldObject: any): void => {
    this._gaCommand("send", fieldObject);
  };
}

export default new GA4();
