import {
  NativeModules,
  NativeEventEmitter,
  Platform,
  EmitterSubscription,
} from 'react-native';

const LINKING_ERROR =
  `The package '@microsoft/react-native-clarity' doesn't seem to be linked. Make sure: \n\n` +
  Platform.select({
    ios: "- You have run 'pod install --repo-update'\n",
    default: '',
  }) +
  '- You rebuilt the app after installing the package\n' +
  '- You are not using Expo Go\n';

const ReferToDocs =
  'Please check the docs at https://www.npmjs.com/package/@microsoft/react-native-clarity for assistance.';

const Clarity = NativeModules.Clarity;
const ClarityEmitter = new NativeEventEmitter(NativeModules.ClarityEmitter);

const SupportedPlatforms = ['android', 'ios'];

let SupportWarningWasShown = false;

/**
 * A class that allows you to configure the Clarity SDK behavior.
 */
export interface ClarityConfig {
  /**
   * @deprecated This property is deprecated and would be removed in a future major version. Use {@linkcode setCustomSessionId} instead.
   *
   * The unique identifier associated with the application user. This ID persists across multiple sessions on the same device.
   *
   * **Notes:**
   * - If {@linkcode userId} isn't provided, a random one is generated automatically.
   * - Must be a base-36 string smaller than `1Z141Z4`.
   * - If an invalid {@linkcode userId} is supplied:
   *     - If `customUserId` isn't specified, {@linkcode userId} acts as the `customUserId`, and a new random {@linkcode userId} is assigned.
   *     - If `customUserId` is specified, the invalid {@linkcode userID} is ignored.
   * - For more flexibility in user identification, consider using the {@linkcode setCustomUserId} API. However, keep in mind that `customUserId` length must be between 1 and 255 characters.
   */
  userId?: string;

  /**
   * The level of logging to show in the device’s logging stream while debugging. By default, the SDK logs nothing.
   */
  logLevel?: LogLevel;
}

const ValidConfigKeys = new Set(
  Object.keys({
    userId: undefined, // Deprecated.
    logLevel: undefined,
  } as ClarityConfig)
);

const RemovedDynamicConfigKeys = new Set([
  'allowMeteredNetworkUsage',
  'enableWebViewCapture',
  'allowedDomains',
  'disableOnLowEndDevices',
  'maximumDailyNetworkUsageInMB',
]);

/**
 * The level of logging to show in the device logging stream.
 */
export enum LogLevel {
  Verbose = 'Verbose',
  Debug = 'Debug',
  Info = 'Info',
  Warning = 'Warning',
  Error = 'Error',
  None = 'None',
}

class ClarityError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ClarityError';
  }
}

function isClarityUnavailable(): boolean {
  if (!SupportedPlatforms.includes(Platform.OS)) {
    if (!SupportWarningWasShown) {
      console.warn(
        'Clarity supports ' + SupportedPlatforms.join(', ') + ' only for now.'
      );
      SupportWarningWasShown = true;
    }

    return true;
  }

  if (Clarity === null) {
    console.error('Clarity did not initialize properly.', LINKING_ERROR);
    return true;
  }

  return false;
}

function validateClarityConfig(config?: ClarityConfig) {
  if (typeof config === 'undefined') {
    // Allow using defaults when config is `null` or `undefined`.
    return;
  }

  if (typeof config !== 'object') {
    throw new ClarityError(
      `Invalid Clarity initialization \`config\` argument. Expected an object, but received ${typeof config}. ${ReferToDocs}`
    );
  }

  if (config?.logLevel && !Object.values(LogLevel).includes(config.logLevel)) {
    console.error(
      `Invalid ClarityConfig \`logLevel\` property value: ${
        config.logLevel
      }. Valid values are: ${Object.values(LogLevel).join(
        ', '
      )}. ${ReferToDocs}`
    );
  }

  for (const key in config) {
    if (RemovedDynamicConfigKeys.has(key)) {
      throw new ClarityError(
        `The ClarityConfig \`${key}\` property is removed and it's configurable from Clarity dashboard.`
      );
    } else if (key === 'enableIOS_experimental') {
      const errorMessage =
        'The ClarityConfig `enableIOS_experimental` property is removed and iOS is supported by default.';
      if ((config as any).enableIOS_experimental) {
        // Proceed with initialization if the user intended to enable iOS support.
        console.error(errorMessage);
      } else {
        throw new ClarityError(errorMessage);
      }
    } else if (key === 'userId') {
      console.warn(
        'The ClarityConfig `userId` property is deprecated and would be removed in a future major version. Use `setCustomUserId()` instead.'
      );
    } else if (!ValidConfigKeys.has(key)) {
      console.warn(`Invalid ClarityConfig property: \`${key}\`.`);
    }
  }
}

/**
 * @param projectId - The unique identifier assigned to your Clarity project. You can find it on the **Settings** page of Clarity dashboard. This ID is essential for routing data to the correct Clarity project (required).
 * @param config - Configuration of Clarity that tunes the SDK behavior (for example, which log level to use, and so on).
 *
 * **Notes:**
 * - The initialization function is asynchronous, meaning it returns before Clarity is fully initialized.
 * - For actions that require Clarity to be fully initialized, it's recommended to use the {@linkcode setOnSessionStartedCallback} function.
 */
export function initialize(projectId: string, config?: ClarityConfig) {
  if (typeof projectId !== 'string') {
    throw new ClarityError(
      `Invalid Clarity initialization \`projectId\` argument. Expected a string, but received ${typeof projectId}. ${ReferToDocs}`
    );
  }

  validateClarityConfig(config);

  // applying default values
  let { userId = null, logLevel = LogLevel.None } = config ?? {};

  if (isClarityUnavailable()) {
    return;
  }

  Clarity.initialize(projectId, userId, logLevel);
}

/**
 * Pauses the Clarity session capturing until a call to the {@linkcode resume} function is made.
 *
 * @returns {Promise<boolean>} A Promise that resolves with `true` if Clarity session capturing was paused successfully; otherwise `false`.
 */
export function pause(): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.pause();
}

/**
 * Resumes the Clarity session capturing only if it was previously paused by a call to the {@linkcode pause} function.
 *
 * @returns {Promise<boolean>} A Promise that resolves with `true` if Clarity session capturing was resumed successfully; otherwise `false`.
 */
export function resume(): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.resume();
}

/**
 * Checks if Clarity session capturing is currently paused based on an earlier call to the {@linkcode pause} function.
 *
 * @returns {Promise<boolean>} A Promise that resolves with `true` if Clarity session capturing is currently in the paused state based on an earlier call to the {@linkcode pause} function; otherwise `false`.
 */
export function isPaused(): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.isPaused();
}

/**
 * Forces Clarity to start a new session asynchronously.
 *
 * @param callback - A callback that is invoked when the new session starts. The callback receives the new session ID as a string parameter.
 *
 * **Notes:**
 * - This function is asynchronous, meaning it returns before the new session is started.
 * - Use the {@linkcode callback} parameter to execute logic that needs to run after the new session begins.
 * - Events that occur before invoking the callback are associated with the previous session.
 * - To ensure proper association of custom tags, user ID, or session ID with the new session, set them within the callback.
 */
export function startNewSession(callback: (sessionId: String) => void): void {
  if (isClarityUnavailable()) {
    return;
  }

  Clarity.startNewSession(callback);
}

/**
 * Sets a custom user ID for the current session. This ID can be used to filter sessions on the Clarity dashboard.
 *
 * @param customUserId - The custom user ID to associate with the current session. The value must be a nonempty string, with a maximum length of 255 characters, and can't consist only of whitespace.
 * @returns {Promise<boolean>} A Promise that resolves with `true` if the custom user ID was set successfully; otherwise `false`.
 *
 *  **Notes:**
 *  - To ensure that the custom user ID is associated with the correct session, it's recommended to call this function within the callbacks of {@linkcode setOnSessionStartedCallback} or {@linkcode startNewSession}.
 *  - Unlike the `userID`, the {@linkcode customUserId} value has fewer restrictions.
 *  - We recommend **not** to set any Personally Identifiable Information (PII) values inside this field.
 */
export function setCustomUserId(customUserId: string): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.setCustomUserId(customUserId);
}

/**
 * Sets a custom session ID for the current session. This ID can be used to filter sessions on the Clarity dashboard.
 *
 * @param customSessionId - The custom session ID to associate with the current session. The value must be a nonempty string, with a maximum length of 255 characters, and can't consist only of whitespace.
 * @returns {Promise<boolean>} A Promise that resolves with `true` if the custom session ID was set successfully; otherwise `false`.
 *
 *  **Notes:**
 *  - To ensure that the custom session ID is associated with the correct session, it's recommended to call this function within the callbacks of {@linkcode setOnSessionStartedCallback} or {@linkcode startNewSession}.
 */
export function setCustomSessionId(customSessionId: string): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.setCustomSessionId(customSessionId);
}

/**
 * Sets a custom tag for the current session. This tag can be used to filter sessions on the Clarity dashboard.
 *
 * @param key - The key for the custom tag. The value must be a nonempty string, with a maximum length of 255 characters, and can't consist only of whitespace.
 * @param value - The value for the custom tag. The value must be a nonempty string, with a maximum length of 255 characters, and can't consist only of whitespace.
 * @returns {Promise<boolean>} A Promise that resolves with `true` if the custom tag was set successfully; otherwise `false`.
 *
 *  **Notes:**
 *  - To ensure that the custom tag is associated with the correct session, it's recommended to call this function within the callbacks of {@linkcode setOnSessionStartedCallback} or {@linkcode startNewSession}.
 */
export function setCustomTag(key: string, value: string): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.setCustomTag(key, value);
}

/**
 * Sets custom tags for the current session. These tags can be used to filter sessions on the Clarity dashboard.
 *
 * @param key - The key for the custom tag. The value must be a nonempty string, with a maximum length of 255 characters, and can't consist only of whitespace.
 * @param values - The values for the custom tag. Each value must be a nonempty string, with a maximum length of 255 characters, and can't consist only of whitespace.
 * @returns {Promise<boolean>} A Promise that resolves with `true` if the custom tag was set successfully; otherwise `false`.
 *
 *  **Notes:**
 *  - To ensure that the custom tag is associated with the correct session, it's recommended to call this function within the callbacks of {@linkcode setOnSessionStartedCallback} or {@linkcode startNewSession}.
 */
export function setCustomTags(key: string, values: string[]): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.setCustomTags(key, values);
}

/**
 * Sends a custom event to the current Clarity session. These custom events can be used to track specific user interactions or actions that Clarity's built-in event tracking doesn't capture.
 *
 * @param value - The name of the custom event. The value must be a nonempty string, with a maximum length of 254 characters, and can't consist only of whitespace.
 * @returns {Promise<boolean>} A Promise that resolves with `true` if the custom event was sent successfully; otherwise `false`.
 *
 * **Notes:**
 * - This API can be called multiple times per page to track various user actions.
 * - Each custom event is logged individually and can be filtered, viewed, and analyzed on the Clarity dashboard.
 */
export function sendCustomEvent(value: string): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.sendCustomEvent(value);
}

let onSessionStartedSubscription: EmitterSubscription | null = null;
var onSessionStartedCallback: ((sessionId: string) => void) | null = null;

/**
 * Sets a callback function that's invoked whenever a new Clarity session starts or an existing session is resumed at app startup.
 *
 * @param callback - The callback to be invoked whenever a Clarity session starts. The callback receives the new or resumed session ID as a string parameter.
 * @returns {boolean} `true` if the callback was set successfully; otherwise `false`.
 *
 * **Notes:**
 * - If the callback is set after a session has already started, the callback is invoked right away with the current session ID.
 * - The specified callback is guaranteed to run on the main thread.
 */
export function setOnSessionStartedCallback(
  callback: (sessionId: String) => void
): boolean {
  if (isClarityUnavailable()) {
    return false;
  }

  onSessionStartedCallback = callback;

  if (onSessionStartedSubscription === null) {
    onSessionStartedSubscription = ClarityEmitter.addListener(
      'sessionStarted',
      ({ sessionId }) => {
        if (onSessionStartedCallback !== null) {
          onSessionStartedCallback(sessionId);
        }
      }
    );
  }

  return true;
}

/**
 * This function allows you to provide a custom screen name that replaces the default screen name. The default name is automatically generated based on the current activity in Android, or the currently presented view controller's title or type in iOS.
 *
 * @param screenName - The desired screen name. The value must be a nonempty string, with a maximum length of 255 characters, and can't consist only of whitespace. Set to `null` to reset.
 *
 * @returns {Promise<boolean>} A Promise that resolves with `true` if the specified screen name was set successfully; otherwise `false`.
 *
 * **Example:**
 * - If the presented iOS view controller is a `UIViewController`, and `setCurrentScreenName("Settings")` is called, the screen name is tracked as "Settings".
 * - If `setCurrentScreenName(null)` is called on the same view controller, the screen name is tracked as "UIViewController".
 *
 * **Notes:**
 * - Clarity starts a new page whenever the screen name changes.
 * - To mask or disallow a screen, specify the view controller's type that is displayed as "ViewController" custom tag of the page visit on the Clarity dashboard's recordings. For example, to mask the iOS view controller in the previous example, mask the "&TabBarController" screen instead of "&Settings".
 * - The custom screen name is set globally and persists across all subsequent view controllers until explicitly reset.
 */
export function setCurrentScreenName(
  screenName: string | null
): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.setCurrentScreenName(screenName);
}

/**
 * @deprecated This function is deprecated and would be removed in a future major version. Use {@linkcode getCurrentSessionUrl} instead.
 *
 * Returns the ID of the currently active Clarity session if a session has already started; otherwise `undefined`.
 *
 * @returns {Promise<string | undefined>} A Promise that resolves with a string representing the ID of the currently active Clarity session if a session has already started; otherwise `undefined`.
 *
 * **Note:**
 * - The session ID can be used to correlate Clarity sessions with other telemetry services.
 * - Initially, this function might return `undefined` until a Clarity session begins.
 * - To ensure a valid session ID, use this method within the callbacks of {@linkcode setOnSessionStartedCallback} or {@linkcode startNewSession}.
 */
export function getCurrentSessionId(): Promise<string | undefined> {
  if (isClarityUnavailable()) {
    return Promise.resolve(undefined);
  }

  return Clarity.getCurrentSessionId();
}

/**
 * Returns the URL of the current Clarity session recording on the Clarity dashboard if a session has already started; otherwise `undefined`.
 *
 * @returns {Promise<string | undefined>} A Promise that resolves with a string representing the URL of the current Clarity session recording on the Clarity dashboard if a session has already started; otherwise `undefined`.
 *
 * **Notes:**
 * - Initially, this function might return `undefined` until a Clarity session begins.
 * - To ensure a valid session URL, use this method within the callbacks of {@linkcode setOnSessionStartedCallback} or {@linkcode startNewSession}.
 */
export function getCurrentSessionUrl(): Promise<string | undefined> {
  if (isClarityUnavailable()) {
    return Promise.resolve(undefined);
  }

  return Clarity.getCurrentSessionUrl();
}

/**
 * Sets the user's consent status for data collection, which adjusts the SDK's behavior based on the user's choices.
 *
 * @param adsStorage - The user's consent status for ads data collection.
 * @param analyticsStorage - The user's consent status for analytics data collection.
 * @returns {Promise<boolean>} A Promise that resolves with `true` if the consent status was set successfully; otherwise `false`.
 *
 * **Notes:**
 * - You are responsible for obtaining the user's consent (for example, via a consent banner or another custom solution).
 */
export function consent(
  adsStorage: boolean,
  analyticsStorage: boolean
): Promise<boolean> {
  if (isClarityUnavailable()) {
    return Promise.resolve(false);
  }

  return Clarity.consent(adsStorage, analyticsStorage);
}
