import ExceptionsManager from './ExceptionsManager';

type GlobalThis = {
  HermesInternal:
    | {
        enablePromiseRejectionTracker?: (options: {
          allRejections?: boolean;
          onUnhandled?: (id: number, rejection?: any) => void;
          onHandled?: (id: number) => void;
        }) => void;
        hasPromise?: () => boolean;
      }
    | undefined;
};

// https://github.com/facebook/react-native/commit/c4082c9ce208a324c2d011823ca2ba432411aafc
export function enablePromiseRejectionTracking() {
  const global = globalThis as unknown as GlobalThis;
  if (
    // Early return if Hermes Promise is not available or tracker is not available
    // https://github.com/facebook/react-native/blob/256565cb1198c02cda218e06de5700c85d8ad589/packages/react-native/Libraries/Core/polyfillPromise.js#L25
    !global?.HermesInternal?.hasPromise?.() ||
    !global?.HermesInternal?.enablePromiseRejectionTracker
  ) {
    return;
  }

  global.HermesInternal.enablePromiseRejectionTracker({
    allRejections: true,
    onUnhandled: (id, rejection) => {
      let message: string;

      if (rejection === undefined) {
        message = '';
      } else if (
        // $FlowFixMe[method-unbinding] added when improving typing for this parameters
        Object.prototype.toString.call(rejection) === '[object Error]'
      ) {
        // $FlowFixMe[method-unbinding] added when improving typing for this parameters
        message = Error.prototype.toString.call(rejection);
      } else {
        try {
          message = require('pretty-format').format(rejection);
        } catch {
          message = typeof rejection === 'string' ? rejection : JSON.stringify(rejection);
        }
      }

      const rejectionPrefix = `Uncaught (in promise, id: ${id})`;
      // This is not an Expo error, but
      // an uncaught promise rejection in the app.
      const rejectionError = new Error(`${rejectionPrefix} ${message ?? ''}`, {
        cause: rejection,
      });
      if (typeof rejection === 'object' && 'stack' in rejection) {
        // If original rejection stack exists, use it
        rejectionError.stack = `${rejectionPrefix} ${rejection.stack ?? ''}`;
      } else {
        // Deleting the stack property would trigger exception handler to collect the current stack
        // Stack of the error created here won't be any useful to the user
        rejectionError.stack = `${rejectionPrefix} ${message ?? ''}`;
      }
      ExceptionsManager.handleException(rejectionError);
    },
    onHandled: (id) => {
      const warning =
        `Promise rejection handled (id: ${id})\n` +
        'This means you can ignore any previous messages of the form ' +
        `"Uncaught (in promise, id: ${id})"`;
      console.warn(warning);
    },
  });
}
