/*global ErrorUtils:false*/

/**
 * react-native plugin for Raven
 *
 * Usage:
 *   var Raven = require('raven-js');
 *   Raven.addPlugin(require('raven-js/plugins/react-native'));
 *
 * Options:
 *
 *   pathStrip: A RegExp that matches the portions of a file URI that should be
 *     removed from stacks prior to submission.
 *
 *   onInitialize: A callback that fires once the plugin has fully initialized
 *     and checked for any previously thrown fatals.  If there was a fatal, its
 *     data payload will be passed as the first argument of the callback.
 *
 */
var wrappedCallback = require('../src/utils').wrappedCallback;

// Example React Native path format (iOS):
// /var/containers/Bundle/Application/{DEVICE_ID}/HelloWorld.app/main.jsbundle

var PATH_STRIP_RE = /^.*\/[^\.]+(\.app|CodePush)/;
var stringify = require('../vendor/json-stringify-safe/stringify');
var FATAL_ERROR_KEY = '--rn-fatal--';
var ASYNC_STORAGE_KEY = '--raven-js-global-error-payload--';

/**
 * Strip device-specific IDs from React Native file:// paths
 */
function normalizeUrl(url, pathStripRe) {
  if (url.indexOf('/') !== -1) {
    return url.replace(/^file\:\/\//, '').replace(pathStripRe, '');
  } else {
    return '/' + url;
  }
}

/**
 * Extract key/value pairs from an object and encode them for
 * use in a query string
 */
function urlencode(obj) {
  var pairs = [];
  for (var key in obj) {
    if ({}.hasOwnProperty.call(obj, key))
      pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
  }
  return pairs.join('&');
}

/**
 * Initializes React Native plugin
 */
function reactNativePlugin(Raven, options) {
  options = options || {};

  // react-native doesn't have a document, so can't use default Image
  // transport - use XMLHttpRequest instead
  Raven.setTransport(reactNativePlugin._transport);

  // Use data callback to strip device-specific paths from stack traces
  Raven.setDataCallback(
    wrappedCallback(function(data) {
      return reactNativePlugin._normalizeData(data, options.pathStrip);
    })
  );

  // Check for a previously persisted payload, and report it.
  reactNativePlugin
    ._restorePayload()
    .then(function(payload) {
      options.onInitialize && options.onInitialize(payload);
      if (!payload) return;
      Raven._sendProcessedPayload(payload, function(error) {
        if (error) return; // Try again next launch.
        reactNativePlugin._clearPayload();
      });
    })
    ['catch'](function() {});

  // Make sure that if multiple fatals occur, we only persist the first one.
  //
  // The first error is probably the most important/interesting error, and we
  // want to crash ASAP, rather than potentially queueing up multiple errors.
  var handlingFatal = false;

  var defaultHandler =
    (ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler()) ||
    ErrorUtils._globalHandler;

  Raven.setShouldSendCallback(function(data, originalCallback) {
    if (!(FATAL_ERROR_KEY in data)) {
      // not a fatal (will not crash runtime), continue as planned
      return originalCallback ? originalCallback.call(this, data) : true;
    }

    var origError = data[FATAL_ERROR_KEY];
    delete data[FATAL_ERROR_KEY];

    reactNativePlugin
      ._persistPayload(data)
      .then(function() {
        defaultHandler(origError, true);
        handlingFatal = false; // In case it isn't configured to crash.
        return null;
      })
      ['catch'](function() {});

    return false; // Do not continue.
  });

  ErrorUtils.setGlobalHandler(function(error, isFatal) {
    var captureOptions = {
      timestamp: new Date() / 1000
    };
    var error = arguments[0];
    // We want to handle fatals, but only in production mode.
    var shouldHandleFatal = isFatal && !global.__DEV__;
    if (shouldHandleFatal) {
      if (handlingFatal) {
        console.log('Encountered multiple fatals in a row. The latest:', error);
        return;
      }
      handlingFatal = true;
      // We need to preserve the original error so that it can be rethrown
      // after it is persisted (see our shouldSendCallback above).
      captureOptions[FATAL_ERROR_KEY] = error;
    }
    Raven.captureException(error, captureOptions);
    // Handle non-fatals regularly.
    if (!shouldHandleFatal) {
      defaultHandler(error);
    }
  });
}

/**
 * Saves the payload for a globally-thrown error, so that we can report it on
 * next launch.
 *
 * Returns a promise that guarantees never to reject.
 */
reactNativePlugin._persistPayload = function(payload) {
  var AsyncStorage = require('react-native').AsyncStorage;
  return AsyncStorage.setItem(ASYNC_STORAGE_KEY, stringify(payload))['catch'](function() {
    return null;
  });
};

/**
 * Checks for any previously persisted errors (e.g. from last crash)
 *
 * Returns a promise that guarantees never to reject.
 */
reactNativePlugin._restorePayload = function() {
  var AsyncStorage = require('react-native').AsyncStorage;
  var promise = AsyncStorage.getItem(ASYNC_STORAGE_KEY)
    .then(function(payload) {
      return JSON.parse(payload);
    })
    ['catch'](function() {
      return null;
    });
  // Make sure that we fetch ASAP.
  AsyncStorage.flushGetRequests();

  return promise;
};

/**
 * Clears any persisted payloads.
 */
reactNativePlugin._clearPayload = function() {
  var AsyncStorage = require('react-native').AsyncStorage;
  return AsyncStorage.removeItem(ASYNC_STORAGE_KEY)['catch'](function() {
    return null;
  });
};

/**
 * Custom HTTP transport for use with React Native applications.
 */
reactNativePlugin._transport = function(options) {
  var request = new XMLHttpRequest();
  request.onreadystatechange = function(e) {
    if (request.readyState !== 4) {
      return;
    }

    if (request.status === 200) {
      if (options.onSuccess) {
        options.onSuccess();
      }
    } else {
      if (options.onError) {
        var err = new Error('Sentry error code: ' + request.status);
        err.request = request;
        options.onError(err);
      }
    }
  };

  request.open('POST', options.url + '?' + urlencode(options.auth));

  // NOTE: React Native ignores CORS and will NOT send a preflight
  //       request for application/json.
  // See: https://facebook.github.io/react-native/docs/network.html#xmlhttprequest
  request.setRequestHeader('Content-type', 'application/json');

  // Sentry expects an Origin header when using HTTP POST w/ public DSN.
  // Just set a phony Origin value; only matters if Sentry Project is configured
  // to whitelist specific origins.
  request.setRequestHeader('Origin', 'react-native://');
  request.send(stringify(options.data));
};

/**
 * Strip device-specific IDs found in transaction and frame filenames
 * when running React Native applications on a physical device.
 */
reactNativePlugin._normalizeData = function(data, pathStripRe) {
  if (!pathStripRe) {
    pathStripRe = PATH_STRIP_RE;
  }

  if (data.culprit) {
    data.culprit = normalizeUrl(data.culprit, pathStripRe);
  }

  if (data.transaction) {
    data.transaction = normalizeUrl(data.transaction, pathStripRe);
  }

  // NOTE: if data.exception exists, exception.values and exception.values[0] are
  // guaranteed to exist
  var stacktrace =
    data.stacktrace || (data.exception && data.exception.values[0].stacktrace);
  if (stacktrace) {
    stacktrace.frames.forEach(function(frame) {
      frame.filename = normalizeUrl(frame.filename, pathStripRe);
    });
  }
  return data;
};

module.exports = reactNativePlugin;
