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

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

// @ts-expect-error
const isTurboModuleEnabled = global.__turboModuleProxy != null;

const RNQualtricsDigitalModule = isTurboModuleEnabled
  ? require('./NativeRNQualtricsDigital').default
  : NativeModules.RNQualtricsDigital;

export const RNQualtricsDigital = RNQualtricsDigitalModule
  ? RNQualtricsDigitalModule
  : new Proxy(
      {},
      {
        get() {
          throw new Error(LINKING_ERROR);
        },
      }
    );

const Q_LOGTAG = 'Qualtrics: ';
const REACT_NATIVE_PROPERTY = 'Qualtrics_IS_REACT_NATIVE';

/* Set up validating function parameter types*/
// Parameter IDs
// Add constant for new function parameters
const BRAND_ID = 'brandid';
const PROJECT_ID = 'projectid';
const INTERCEPT_ID = 'interceptid';
const EXT_REF_ID = 'extrefid';
const CALLBACK = 'callback';
const VIEW_NAME = 'viewName';
const PROPERTY_KEY = 'propertyKey';
const PROPERTY_STRING = 'propertyString';
const PROPERTY_NUMBER = 'propertyNumber';
const PROPERTY_ASSET = 'propertyAsset';

const INITIALIZE_PROJECT_EVENT = 'initializeProjectEvent';
const INITIALIZATION_EVENT = 'initializeEvent';
const EVALUATE_EVENT = 'evaluateEvent';
const EVALUATE_PROJECT_EVENT = 'evaluateProjectEvent';
const EVALUATE_INTERCEPT_EVENT = 'evaluateInterceptEvent';

// Parameter Types
// add constant for new function parameter types
const STRING_TYPE = 'string';
const FUNCTION_TYPE = 'function';
const NUMBER_TYPE = 'number';

// Parameter IDs to Parameter Types Dictionary
// add parameter ID as key and parameter type as value
const EXPECTED_PARAMETER_TYPES = {
  [BRAND_ID]: STRING_TYPE,
  [PROJECT_ID]: STRING_TYPE,
  [INTERCEPT_ID]: STRING_TYPE,
  [EXT_REF_ID]: STRING_TYPE,
  [CALLBACK]: FUNCTION_TYPE,
  [VIEW_NAME]: STRING_TYPE,
  [PROPERTY_KEY]: STRING_TYPE,
  [PROPERTY_STRING]: STRING_TYPE,
  [PROPERTY_NUMBER]: NUMBER_TYPE,
  [PROPERTY_ASSET]: STRING_TYPE,
};

const eventEmitter = Platform.OS === 'android' ? new NativeEventEmitter()
  : new NativeEventEmitter(RNQualtricsDigital);

/* End validate parameter types setup */

/**
 * @class
 * @classdesc a class used to hold the results of the initialization methods
 * @property {boolean} passed - whether or not initialization succeeded
 * @property {string} surveyUrl  - initialization message used to determine initialization status
 */
export class InitializationResult {
  passed: boolean;
  message: string;
  constructor(passed: boolean, message: string) {
    this.passed = passed;
    this.message = message;
  }
}

type TargetingResultStatus =
  | 'passed'
  | 'failedLogic'
  | 'sampledOut'
  | 'multipleDisplayPrevented'
  | 'contactFrequencyFailed'
  | 'error'
  | 'inactive'
  | undefined;

type TargetingResultType = {
  passed?: boolean;
  targetingResultStatus?: TargetingResultStatus;
  error?: string;
  creativeType?: string;
  surveyUrl?: string;
};

/**
 * @class
 * @classdesc a class used to hold the results of the evaluation methods
 * @property {boolean} passed - whether or not target evaluation succeeded
 * @property {string} targetingResultStatus - one of:
 *           "passed" | "failedLogic" | "sampledOut" | "multipleDisplayPrevented" |
 *           "contactFrequencyFailed" | "error" | "inactive"
 *
 *           Android-only prop
 *
 * @property {string} error - error from the last evaluation of the intercept if
 *           applicable;
 *
 *           Android-only prop
 *
 * @property {string} surveyUrl - link to the survey in Creative where logic evaluation passed. url used to display survey in display()
 * @property {string} creativeType - the type of the creative associated with the passing action set.
 */
export class TargetingResult {
  passed?: boolean;
  targetingResultStatus: TargetingResultStatus;
  error?: string;
  creativeType?: string;
  surveyUrl?: string;

  constructor({
    passed,
    targetingResultStatus,
    error,
    surveyUrl,
    creativeType,
  }: TargetingResultType = {}) {
    this.passed = passed;
    this.targetingResultStatus = targetingResultStatus;
    this.error = error;
    this.surveyUrl = surveyUrl;
    this.creativeType = creativeType;
  }

  /**
   * @method recordClick() - void method
   * @description records a click to the server associated with intercept
   */
  recordClick(): void {
    try {
      RNQualtricsDigital.recordClick();
    } catch (error) {
      console.log(Q_LOGTAG + 'recordClick() failed -- ' + error);
    }
  }

  /**
   * @method recordImpression() - void method
   * @description records an impression to the server associated with intercept
   */
  recordImpression(): void {
    try {
      RNQualtricsDigital.recordImpression();
    } catch (error) {
      console.log(Q_LOGTAG + 'recordImpression() failed -- ' + error);
    }
  }
}

/**
 * validateParameterTypes() - void utility method that checks types in runtime.
 * Suppressed with ts-ignore by design.
 * throws error if function parameters do not match expected parameter types
 * @param parameters – any type of parameter
 */
function validateParameterTypes(parameters: any): void {
  for (let p_key in parameters) {
    if (
      parameters.hasOwnProperty(p_key) &&
      EXPECTED_PARAMETER_TYPES.hasOwnProperty(p_key)
    ) {
      let paramType = typeof parameters[p_key];
      // @ts-ignore
      if (paramType !== EXPECTED_PARAMETER_TYPES[p_key]) {
        throw new TypeError(
          // @ts-ignore
          `${p_key} parameter did not match expected type of ${EXPECTED_PARAMETER_TYPES[p_key]}`
        );
      }
    }
  }
}

function isWaitingForEventToFinish(eventType: string) {
  return (
    eventEmitter &&
    eventEmitter.listenerCount(eventType) &&
    eventEmitter.listenerCount(eventType) > 0
  );
}

/** initialize()
 * @description initializes Qualtrics singleton with specified params
 * @deprecate marked for future deprecation with version 2.0.0
 * @param brandid - intercept brand id
 * @param projectid - intercept project/zone id
 * @param interceptid - intercept id
 * @param {callback} [callback] - optional method called, if defined, to handle the InitializationResult
 * @returns {InitializationResult} in callback
 */
function initialize(
  brandid: string,
  projectid: string,
  interceptid: string,
  callback?: (result: InitializationResult) => void
) {
  // Build dictionary of required parameters for type validation
  let functionParameters = {
    [BRAND_ID]: brandid,
    [PROJECT_ID]: projectid,
    [INTERCEPT_ID]: interceptid,
  };

  try {
    RNQualtricsDigital.setString(REACT_NATIVE_PROPERTY, 'true');
    if (typeof callback === 'undefined') {
      validateParameterTypes(functionParameters);
      RNQualtricsDigital.initialize(brandid, projectid, interceptid);
    } else {
      // Add callback to required parameters
      // @ts-ignore
      functionParameters[CALLBACK] = callback;
      validateParameterTypes(functionParameters);

      if (isWaitingForEventToFinish(INITIALIZATION_EVENT)) {
        callback(
          new InitializationResult(false, 'Initialization already in progress')
        );
        return;
      }
      eventEmitter.addListener(INITIALIZATION_EVENT, (result) => {
        if (result !== null) {
          callback(new InitializationResult(result.passed, result.message));
        } else {
          callback(
            new InitializationResult(
              false,
              'Invalid InitializationResult Received'
            )
          );
        }
        eventEmitter.removeAllListeners(INITIALIZATION_EVENT);
      });
      RNQualtricsDigital.initializeWithCompletion(
        brandid,
        projectid,
        interceptid
      );
    }
  } catch (error) {
    console.log(Q_LOGTAG + 'initialize() failed -- ' + error);
  }
}

/** initializeProject() - void method
 * @description initializes Qualtrics singleton with specified params
 * @param brandId - intercept brand id
 * @param projectId - intercept project/zone id
 * @param {callback} [callback] - optional method called, if defined, to handle initializationResults
 * @return {InitializationResult}s to the callback
 */
function initializeProject(
  brandId: string,
  projectId: string,
  callback: (initializationResults: {
    [interceptId: string]: InitializationResult;
  }) => void
): void {
  initializeProjectWithExtRefId(brandId, projectId, '', callback);
}

/** initializeProjectWithExtRefId() - void method
 * @description initializes Qualtrics singleton with specified params
 * @param brandid - intercept brand id
 * @param projectid - intercept project/zone id
 * @param extrefid - external reference id
 * @param {callback} [callback] - optional method called, if defined, to handle initializationResults
 * @return {InitializationResult}s to the callback
 */
function initializeProjectWithExtRefId(
  brandid: string,
  projectid: string,
  extrefid: string,
  callback: (initializationResults: {
    [interceptId: string]: InitializationResult;
  }) => void
): void {
  let functionParameters = {
    [BRAND_ID]: brandid,
    [PROJECT_ID]: projectid,
    [EXT_REF_ID]: extrefid,
    [CALLBACK]: callback,
  };

  try {
    RNQualtricsDigital.setString(REACT_NATIVE_PROPERTY, 'true');
    validateParameterTypes(functionParameters);

    if (isWaitingForEventToFinish(INITIALIZE_PROJECT_EVENT)) {
      let errorResult: { [interceptId: string]: InitializationResult } = {};
      errorResult.ERROR = new InitializationResult(
        false,
        'Invalid InitializationResult Received'
      );
      callback(errorResult);
      return;
    }
    eventEmitter.addListener(INITIALIZE_PROJECT_EVENT, (result) => {
      if (result !== null) {
        var initializationResults = {};
        for (var interceptId in result) {
          if (Object.prototype.hasOwnProperty.call(result, interceptId)) {
            if (result[interceptId].message && result[interceptId].passed) {
              // @ts-ignore
              initializationResults[interceptId] = new InitializationResult(
                result[interceptId].passed,
                result[interceptId].message
              );
            }
          }
        }
        callback(initializationResults);
      } else {
        let errorResult: { [interceptId: string]: InitializationResult } = {};
        errorResult.ERROR = new InitializationResult(
          false,
          'Invalid InitializationResult Received'
        );
        callback(errorResult);
      }
      eventEmitter.removeAllListeners(INITIALIZE_PROJECT_EVENT);
    });
    RNQualtricsDigital.initializeProject(brandid, projectid, extrefid);
  } catch (error) {
    console.log(Q_LOGTAG + 'initializeProject() failed -- ' + error);
  }
}

/** evaluateTargetingLogic() - void method
 * @deprecate marked for future deprecation with version 2.0.0
 * @description evaluates current intercept and calls callback
 * @return {TargetingResult} in callback
 * @param callback - method called, to handle TargetingResult, must be defined
 */
function evaluateTargetingLogic(
  callback: (result: TargetingResult) => void
): void {
  // Build dictionary of required parameters for type validation
  const functionParameters = {
    [CALLBACK]: callback,
  };

  try {
    validateParameterTypes(functionParameters);

    if (isWaitingForEventToFinish(EVALUATE_EVENT)) {
      callback(
        new TargetingResult({
          passed: false,
          targetingResultStatus: 'error',
          error: 'Evaluation already in progress',
          surveyUrl: 'Evaluation already in progress',
        })
      );
      return;
    }
    eventEmitter.addListener(EVALUATE_EVENT, (result) => {
      if (result !== null) {
        callback(
          new TargetingResult({
            passed: result.passed,
            targetingResultStatus: result.targetingResultStatus,
            error: result.error,
            surveyUrl: result.surveyUrl,
          })
        );
      } else {
        callback(
          new TargetingResult({
            passed: false,
            targetingResultStatus: 'error',
            error: 'Invalid TargetingResult Received',
            surveyUrl: 'Invalid TargetingResult Received',
          })
        );
      }
      eventEmitter.removeAllListeners(EVALUATE_EVENT);
    });
    RNQualtricsDigital.evaluateTargetingLogic();
  } catch (error) {
    console.log(Q_LOGTAG + 'evaluateTargetingLogic() failed -- ' + error);
  }
}

/** evaluateProject() - void method
 * @description evaluates all the initialized intercepts and calls callback
 * @returns {TargetingResult}s in callback
 * @param callback - method called to handle TargetingResults, must be defined
 */
function evaluateProject(
  callback: (targetingResults: { [key: string]: TargetingResult }) => void
): void {
  // Build dictionary of required parameters for type validation
  const functionParameters = {
    [CALLBACK]: callback,
  };

  try {
    validateParameterTypes(functionParameters);

    if (isWaitingForEventToFinish(EVALUATE_PROJECT_EVENT)) {
      let errorResult: { [key: string]: TargetingResult } = {};
      errorResult.ERROR = new TargetingResult({
        passed: false,
        targetingResultStatus: 'error',
        error: 'Evaluation already in progress',
        surveyUrl: 'Evaluation already in progress',
        creativeType: '',
      });
      callback(errorResult);
      return;
    }
    eventEmitter.addListener(EVALUATE_PROJECT_EVENT, (result) => {
      if (result !== null) {
        let targetingResults: { [key: string]: TargetingResult } = {};

        for (let interceptId in result) {
          if (Object.prototype.hasOwnProperty.call(result, interceptId)) {
            const singleResult = result[interceptId];
            targetingResults[interceptId] = new TargetingResult({
              passed: singleResult.passed,
              targetingResultStatus: singleResult.targetingResultStatus,
              error: singleResult.error,
              surveyUrl: singleResult.surveyUrl,
              creativeType: singleResult.creativeType,
            });
          }
        }

        callback(targetingResults);
      } else {
        let errorResult: { [key: string]: TargetingResult } = {};
        errorResult.ERROR = new TargetingResult({
          passed: false,
          targetingResultStatus: 'error',
          error: 'Invalid TargetingResult Received',
          surveyUrl: 'Invalid TargetingResult Received',
          creativeType: '',
        });
        callback(errorResult);
      }
      eventEmitter.removeAllListeners(EVALUATE_PROJECT_EVENT);
    });
    RNQualtricsDigital.evaluateProject();
  } catch (error) {
    console.log(Q_LOGTAG + 'evaluateProject() failed -- ' + error);
  }
}

/** evaluateIntercept() - void method
 * @description evaluates the passed intercept and calls callback
 * @param interceptId
 * @param callback - method called to handle TargetingResult, must be defined
 * @return {TargetingResult}s in callback
 */
function evaluateIntercept(
  interceptId: string,
  callback: (result: TargetingResult) => void
): void {
  // Build dictionary of required parameters for type validation
  const functionParameters = {
    [CALLBACK]: callback,
  };

  try {
    validateParameterTypes(functionParameters);

    if (isWaitingForEventToFinish(EVALUATE_INTERCEPT_EVENT)) {
      callback(
        new TargetingResult({
          passed: false,
          targetingResultStatus: 'error',
          error: 'Evaluation already in progress',
          surveyUrl: 'Evaluation already in progress',
          creativeType: '',
        })
      );
      return;
    }

    if (!interceptId) {
      callback(
        new TargetingResult({
          passed: false,
          targetingResultStatus: 'error',
          error: 'Intercept ID cannot be empty',
          surveyUrl: 'Intercept ID cannot be empty',
          creativeType: '',
        })
      );
      return;
    }

    eventEmitter.addListener(EVALUATE_INTERCEPT_EVENT, (result) => {
      if (result !== null) {
        callback(
          new TargetingResult({
            passed: result.passed,
            targetingResultStatus: result.targetingResultStatus,
            error: result.error,
            surveyUrl: result.surveyUrl,
            creativeType: result.creativeType,
          })
        );
      } else {
        callback(
          new TargetingResult({
            passed: false,
            targetingResultStatus: 'error',
            error: 'Invalid TargetingResult Received',
            surveyUrl: 'Invalid TargetingResult Received',
            creativeType: '',
          })
        );
      }
      eventEmitter.removeAllListeners(EVALUATE_INTERCEPT_EVENT);
    });

    RNQualtricsDigital.evaluateIntercept(interceptId);
  } catch (error) {
    console.log(Q_LOGTAG + 'evaluateIntercept() failed -- ' + error);
  }
}

/** display() - bool return method
 * @description displays creative in the view controller
 * @param {boolean} autoCloseSurvey Parameter that enables auto close for displayed survey
 * @returns {Promise} Promise object representing a bool
 */
function display(autoCloseSurvey: boolean = false): Promise<boolean> {
  const closeSurvey = autoCloseSurvey ?? false;
  return new Promise<boolean>((resolve, reject) => {
    try {
      RNQualtricsDigital.display(closeSurvey, (result: boolean) => {
        if (result) {
          resolve(true);
        }
        reject(false);
      });
    } catch (error) {
      console.log(Q_LOGTAG + 'display() failed -- ' + error);
      reject(false);
    }
  });
}

/** displayTarget() - void method
 * @description displays evaluated TargetingResult in webview
 * @param {string} surveyUrl - Optional link to the surveyUrl
 * @param {string} autoCloseSurvey - Optional boolean for auto closing survey at the end
 */
function displayTarget(surveyUrl: string | null = null, autoCloseSurvey: boolean = false): void {
  const closeSurvey = autoCloseSurvey ?? false;
  if (surveyUrl === '') {
    surveyUrl = null;
  }

  try {
    RNQualtricsDigital.displayTarget(surveyUrl, closeSurvey, (result: boolean) => {
      // Display target failed; log error
      if (!result) {
        console.log(
          Q_LOGTAG +
            'displayTarget() failed -- no TargetingResult with valid target URL'
        );
      }
    });
  } catch (error) {
    console.log(Q_LOGTAG + 'displayTarget() failed -- ' + error);
  }
}

/** displayIntercept() - bool return method
 * @description displays creative in the view controller
 * @param {string} interceptId - Id of evaluated intercept that should be displayed
 * @param {string} autoCloseSurvey - Optional boolean for auto closing survey at the end
 * @returns {Promise} Promise object representing a bool
 */
function displayIntercept(interceptId: string, autoCloseSurvey: boolean = false): Promise<boolean> {
  const closeSurvey = autoCloseSurvey ?? false;
  return new Promise<boolean>((resolve, reject) => {
    try {
      RNQualtricsDigital.displayIntercept(interceptId, closeSurvey, (result: boolean) => {
        if (result) {
          resolve(true);
        }
        reject(false);
      });
    } catch (error) {
      console.log(Q_LOGTAG + 'displayIntercept() failed -- ' + error);
      reject(false);
    }
  });
}

/** setString() - void method
 * @description sets string value for specified key in properties
 * @param propertyKey - Key for value
 * @param propertyString - String value to assign with key
 */
function setString(propertyKey: string, propertyString: string): void {
  // Build dictionary of required parameters for type validation
  const functionParameters = {
    [PROPERTY_KEY]: propertyKey,
    [PROPERTY_STRING]: propertyString,
  };

  try {
    validateParameterTypes(functionParameters);
    RNQualtricsDigital.setString(propertyKey, propertyString);
  } catch (error) {
    console.log(Q_LOGTAG + 'setString() failed -- ' + error);
  }
}

/** setNumber() - void method
 * @description sets double value for specified key in properties
 * @param propertyKey - Key for value
 * @param propertyNumber - Numeric value to assign with key
 */
function setNumber(propertyKey: string, propertyNumber: number): void {
  // Build dictionary of required parameters for type validation
  const functionParameters = {
    [PROPERTY_KEY]: propertyKey,
    [PROPERTY_NUMBER]: propertyNumber,
  };

  try {
    validateParameterTypes(functionParameters);
    RNQualtricsDigital.setNumber(propertyKey, propertyNumber);
  } catch (error) {
    console.log(Q_LOGTAG + 'setNumber() failed -- ' + error);
  }
}

/** setDateTime() - void method
 * @description sets current datetime value for specified key in properties
 * @param propertyKey - Key for value
 */
function setDateTime(propertyKey: string): void {
  // Build dictionary of required parameters for type validation
  const functionParameters = {
    [PROPERTY_KEY]: propertyKey,
  };

  try {
    validateParameterTypes(functionParameters);
    RNQualtricsDigital.setDateTime(propertyKey);
  } catch (error) {
    console.log(Q_LOGTAG + 'setDateTime() failed -- ' + error);
  }
}

/** setLastDisplayTimeForIntercept() - void method
 * @description sets the lastDisplayedTime for specified interceptId to now
 * @param {string} interceptId - The ID of the intercept for which the last display time is being set
 */
function setLastDisplayTimeForIntercept(interceptId: string): void {
  try {
    RNQualtricsDigital.setLastDisplayTimeForIntercept(interceptId);
  } catch (error) {
    console.log(
      Q_LOGTAG + 'setLastDisplayTimeForIntercept() failed --' + error
    );
  }
}

/** setNotificationIconAsset() - void method
 * @description Sets string file name for notificationIconAsset in properties
 * @param {string} propertyAsset - File name of Image stored in Drawable folder of Android Project
 */
function setNotificationIconAsset(propertyAsset: string): void {
  // Build dictionary of required parameters for type validation
  const functionParameters = {
    [PROPERTY_ASSET]: propertyAsset,
  };

  try {
    validateParameterTypes(functionParameters);
    RNQualtricsDigital.setNotificationIconAsset(propertyAsset);
  } catch (error) {
    console.log(Q_LOGTAG + 'setNotificationIconAsset() failed -- ' + error);
  }
}

/** registerViewVisit() - void method
 * @description record visits to a particular view
 * @param viewName - name of view to record
 */
function registerViewVisit(viewName: string): void {
  // Build dictionary of required parameters for type validation
  const functionParameters = {
    [VIEW_NAME]: viewName,
  };

  try {
    validateParameterTypes(functionParameters);
    RNQualtricsDigital.registerViewVisit(viewName);
  } catch (error) {
    console.log(Q_LOGTAG + 'registerViewVisit() failed -- ' + error);
  }
}

/** resetTimer() - void method
 * @description resets timer used for Time Spent In App
 */
function resetTimer(): void {
  try {
    RNQualtricsDigital.resetTimer();
  } catch (error) {
    console.log(Q_LOGTAG + 'resetTimer() failed -- ' + error);
  }
}

/** resetViewCounter() - void method
 * @description resets view count used for View Count
 */
function resetViewCounter(): void {
  try {
    RNQualtricsDigital.resetViewCounter();
  } catch (error) {
    console.log(Q_LOGTAG + 'resetViewCounter() failed -- ' + error);
  }
}

/**
 *
 * @returns Returns an array of all the initialized intercepts
 */
function getInitializedIntercepts(): Promise<Array<string>> {
  return new Promise<Array<string>>((resolve) => {
    try {
      RNQualtricsDigital.getInitializedIntercepts(
        (intercepts: Array<string>) => {
          resolve(intercepts);
        }
      );
    } catch (error) {
      resolve(['']);
    }
  });
}

/**
 *
 * @returns Returns an array of all the passing intercepts
 */
function getPassingIntercepts(): Promise<Array<string>> {
  return new Promise<Array<string>>((resolve) => {
    try {
      RNQualtricsDigital.getPassingIntercepts((intercepts: Array<string>) => {
        resolve(intercepts);
      });
    } catch (error) {
      resolve(['']);
    }
  });
}

export default {
  initialize: initialize,
  initializeProject: initializeProject,
  initializeProjectWithExtRefId: initializeProjectWithExtRefId,
  evaluateTargetingLogic: evaluateTargetingLogic,
  evaluateIntercept: evaluateIntercept,
  evaluateProject: evaluateProject,
  display: display,
  displayTarget: displayTarget,
  displayIntercept: displayIntercept,
  setString: setString,
  setNumber: setNumber,
  setDateTime: setDateTime,
  setLastDisplayTimeForIntercept: setLastDisplayTimeForIntercept,
  setNotificationIconAsset: setNotificationIconAsset,
  registerViewVisit: registerViewVisit,
  resetTimer: resetTimer,
  resetViewCounter: resetViewCounter,
  getInitializedIntercepts: getInitializedIntercepts,
  getPassingIntercepts: getPassingIntercepts,
};
