import urljoin from 'url-join';
import WinChan from 'winchan';

import urlHelper from '../helper/url';
import assert from '../helper/assert';
import responseHandler from '../helper/response-handler';
import PopupHandler from '../helper/popup-handler';
import objectHelper from '../helper/object';
import windowHelper from '../helper/window';
import Warn from '../helper/warn';
import TransactionManager from './transaction-manager';
import CrossOriginAuthentication from './cross-origin-authentication';

/**
 * @class
 * @classdesc This class cannot be instantiated directly. Instead, use WebAuth.popup
 * @hideconstructor
 */
function Popup(webAuth, options) {
  this.baseOptions = options;
  this.baseOptions.popupOrigin = options.popupOrigin;
  this.client = webAuth.client;
  this.webAuth = webAuth;

  this.transactionManager = new TransactionManager(this.baseOptions);
  this.crossOriginAuthentication = new CrossOriginAuthentication(
    webAuth,
    this.baseOptions
  );
  this.warn = new Warn({
    disableWarnings: !!options._disableDeprecationWarnings
  });
}

/**
 * Returns a new instance of the popup handler
 *
 * @method buildPopupHandler
 * @private
 */
Popup.prototype.buildPopupHandler = function() {
  var pluginHandler = this.baseOptions.plugins.get('popup.getPopupHandler');

  if (pluginHandler) {
    return pluginHandler.getPopupHandler();
  }

  return new PopupHandler();
};

/**
 * Initializes the popup window and returns the instance to be used later in order to avoid being blocked by the browser.
 *
 * @method preload
 * @param {Object} options receives the window height and width and any other window feature to be sent to window.open
 * @memberof Popup.prototype
 */
Popup.prototype.preload = function(options) {
  options = options || {};

  var popup = this.buildPopupHandler();

  popup.preload(options);
  return popup;
};

/**
 * Internal use.
 *
 * @method getPopupHandler
 * @private
 */
Popup.prototype.getPopupHandler = function(options, preload) {
  if (options.popupHandler) {
    return options.popupHandler;
  }

  if (preload) {
    return this.preload(options);
  }

  return this.buildPopupHandler();
};

/**
 * Handles the popup logic for the callback page.
 *
 * @method callback
 * @param {Object} options
 * @param {String} options.hash the url hash. If not provided it will extract from window.location.hash
 * @param {String} [options.state] value originally sent in `state` parameter to {@link authorize} to mitigate XSRF
 * @param {String} [options.nonce] value originally sent in `nonce` parameter to {@link authorize} to prevent replay attacks
 * @see   {@link parseHash}
 * @memberof Popup.prototype
 */
Popup.prototype.callback = function(options) {
  var _this = this;
  var theWindow = windowHelper.getWindow();
  options = options || {};
  var originUrl =
    options.popupOrigin ||
    this.baseOptions.popupOrigin ||
    windowHelper.getOrigin();

  /*
    in IE 11, there's a bug that makes window.opener return undefined.
    The callback page will still call `popup.callback()` which will run this method
    in the relay page. WinChan expects the relay page to have a global `doPost` function,
    which will be called with the response.

    IE11 Bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/110920/
   */
  if (!theWindow.opener) {
    theWindow.doPost = function(msg) {
      if (theWindow.parent) {
        theWindow.parent.postMessage(msg, originUrl);
      }
    };
    return;
  }

  WinChan.onOpen(function(popupOrigin, r, cb) {
    if (popupOrigin !== originUrl) {
      return cb({
        error: 'origin_mismatch',
        error_description:
          "The popup's origin (" +
          popupOrigin +
          ') should match the `popupOrigin` parameter (' +
          originUrl +
          ').'
      });
    }
    _this.webAuth.parseHash(options || {}, function(err, data) {
      return cb(err || data);
    });
  });
};

/**
 * Shows inside a new window the hosted login page (`/authorize`) in order to start a new authN/authZ transaction and post its result using `postMessage`.
 *
 * @method authorize
 * @param {Object} options
 * @param {String} [options.clientID] the Client ID found on your Application settings page
 * @param {String} options.redirectUri url that the Auth0 will redirect after Auth with the Authorization Response
 * @param {String} options.responseType type of the response used by OAuth 2.0 flow. It can be any space separated list of the values `code`, `token`, `id_token`. {@link https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html}
 * @param {String} [options.responseMode] how the Auth response is encoded and redirected back to the client. Supported values are `query`, `fragment` and `form_post`. The `query` value is only supported when `responseType` is `code`. {@link https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes}
 * @param {String} [options.state] value used to mitigate XSRF attacks. {@link https://auth0.com/docs/protocols/oauth2/oauth-state}
 * @param {String} [options.nonce] value used to mitigate replay attacks when using Implicit Grant. {@link https://auth0.com/docs/api-auth/tutorials/nonce}
 * @param {String} [options.scope] scopes to be requested during Auth. e.g. `openid email`
 * @param {String} [options.audience] identifier of the resource server who will consume the access token issued after Auth
 * @param {String} [options.organization] the Id of an organization to log in to
 * @param {String} [options.invitation] the ID of an invitation to accept. This is available from the user invitation URL that is given when participating in a user invitation flow
 * @param {Boolean} [options.owp] determines if Auth0 should render the relay page or not and the caller is responsible of handling the response.
 * @param {authorizeCallback} cb
 * @see {@link https://auth0.com/docs/api/authentication#authorize-client}
 * @memberof Popup.prototype
 */
Popup.prototype.authorize = function(options, cb) {
  var popup;
  var url;
  var relayUrl;
  var popOpts = {};

  var pluginHandler = this.baseOptions.plugins.get('popup.authorize');

  var params = objectHelper
    .merge(this.baseOptions, [
      'clientID',
      'scope',
      'domain',
      'audience',
      'tenant',
      'responseType',
      'redirectUri',
      '_csrf',
      'state',
      '_intstate',
      'nonce',
      'organization',
      'invitation'
    ])
    .with(objectHelper.blacklist(options, ['popupHandler']));

  assert.check(
    params,
    { type: 'object', message: 'options parameter is not valid' },
    {
      responseType: {
        type: 'string',
        message: 'responseType option is required'
      }
    }
  );

  // the relay page should not be necessary as long it happens in the same domain
  // (a redirectUri shoul be provided). It is necessary when using OWP
  relayUrl = urljoin(this.baseOptions.rootUrl, 'relay.html');

  // if a owp is enabled, it should use the owp flag
  if (options.owp) {
    // used by server to render the relay page instead of sending the chunk in the
    // url to the callback
    params.owp = true;
  } else {
    popOpts.origin = urlHelper.extractOrigin(params.redirectUri);
    relayUrl = params.redirectUri;
  }

  if (options.popupOptions) {
    popOpts.popupOptions = objectHelper.pick(options.popupOptions, [
      'width',
      'height',
      'top',
      'left'
    ]);
  }

  if (pluginHandler) {
    params = pluginHandler.processParams(params);
  }

  params = this.transactionManager.process(params);
  params.scope = params.scope || 'openid profile email';
  delete params.domain;

  url = this.client.buildAuthorizeUrl(params);

  popup = this.getPopupHandler(options);

  return popup.load(
    url,
    relayUrl,
    popOpts,
    responseHandler(cb, { keepOriginalCasing: true })
  );
};

/**
 * Performs authentication with username/email and password with a database connection inside a new window
 *
 * This method is not compatible with API Auth so if you need to fetch API tokens with audience
 * you should use {@link authorize} or {@link login}.
 *
 * @method loginWithCredentials
 * @param {Object} options
 * @param {String} [options.redirectUri] url that the Auth0 will redirect after Auth with the Authorization Response
 * @param {String} [options.responseType] type of the response used. It can be any of the values `code` and `token`
 * @param {String} [options.responseMode] how the AuthN response is encoded and redirected back to the client. Supported values are `query` and `fragment`. The `query` value is only supported when `responseType` is `code`.
 * @param {String} [options.scope] scopes to be requested during AuthN. e.g. `openid email`
 * @param {credentialsCallback} cb
 * @memberof Popup.prototype
 */
Popup.prototype.loginWithCredentials = function (options, cb) {
  options.realm = options.realm || options.connection;
  options.popup = true;
  options = objectHelper
    .merge(this.baseOptions, [
      'redirectUri',
      'responseType',
      'state',
      'nonce',
      'timeout'
    ])
    .with(objectHelper.blacklist(options, ['popupHandler', 'connection']));

  options = this.transactionManager.process(options);
  this.crossOriginAuthentication.login(options, cb);
};

/**
 * Verifies the passwordless TOTP and redirects to finish the passwordless transaction
 *
 * @method passwordlessVerify
 * @param {Object} options
 * @param {String} options.type `sms` or `email`
 * @param {String} options.phoneNumber only if type = sms
 * @param {String} options.email only if type = email
 * @param {String} options.connection the connection name
 * @param {String} options.verificationCode the TOTP code
 * @param {Function} cb
 * @memberof Popup.prototype
 */
Popup.prototype.passwordlessVerify = function(options, cb) {
  var _this = this;
  return this.client.passwordless.verify(
    objectHelper.blacklist(options, ['popupHandler']),
    function(err) {
      if (err) {
        return cb(err);
      }

      options.username = options.phoneNumber || options.email;
      options.password = options.verificationCode;

      delete options.email;
      delete options.phoneNumber;
      delete options.verificationCode;
      delete options.type;

      _this.client.loginWithResourceOwner(options, cb);
    }
  );
};

/**
 * Signs up a new user and automatically logs the user in after the signup.
 *
 * This method is not compatible with API Auth so if you need to fetch API tokens with audience
 * you should use {@link authorize} or {@link signupAndAuthorize}.
 *
 * @method signupAndLogin
 * @param {Object} options
 * @param {String} options.email user email address
 * @param {String} options.password user password
 * @param {String} options.connection name of the connection where the user will be created
 * @param {credentialsCallback} cb
 * @memberof Popup.prototype
 */
Popup.prototype.signupAndLogin = function(options, cb) {
  var _this = this;

  return this.client.dbConnection.signup(options, function(err) {
    if (err) {
      return cb(err);
    }
    _this.loginWithCredentials(options, cb);
  });
};

export default Popup;
