/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

this.EXPORTED_SYMBOLS = ["FirefoxAccounts"];

const {classes: Cc, interfaces: Ci, utils: Cu} = Components;

Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/identity/LogUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
                                  "resource://gre/modules/identity/IdentityUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
                                  "resource://gre/modules/identity/IdentityUtils.jsm");

// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
// default.
const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
try {
  this.LOG_LEVEL =
    Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
    && Services.prefs.getCharPref(PREF_LOG_LEVEL);
} catch (e) {
  this.LOG_LEVEL = Log.Level.Error;
}

var log = Log.repository.getLogger("Identity.FxAccounts");
log.level = LOG_LEVEL;
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));

//@line 44 "/pbulk/work/www/firefox52/work/firefox-52.9.0esr/toolkit/identity/FirefoxAccounts.jsm"
log.warn("The FxAccountsManager is only functional in B2G at this time.");
var FxAccountsManager = null;
var ONVERIFIED_NOTIFICATION = null;
var ONLOGIN_NOTIFICATION = null;
var ONLOGOUT_NOTIFICATION = null;
//@line 50 "/pbulk/work/www/firefox52/work/firefox-52.9.0esr/toolkit/identity/FirefoxAccounts.jsm"

function FxAccountsService() {
  Services.obs.addObserver(this, "quit-application-granted", false);
  if (ONVERIFIED_NOTIFICATION) {
    Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, false);
    Services.obs.addObserver(this, ONLOGIN_NOTIFICATION, false);
    Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
  }

  // Maintain interface parity with Identity.jsm and MinimalIdentity.jsm
  this.RP = this;

  this._rpFlows = new Map();

  // Enable us to mock FxAccountsManager service in testing
  this.fxAccountsManager = FxAccountsManager;
}

FxAccountsService.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),

  observe: function observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case null:
        // Guard against matching null ON*_NOTIFICATION
        break;
      case ONVERIFIED_NOTIFICATION:
        log.debug("Received " + ONVERIFIED_NOTIFICATION + "; firing request()s");
        for (let [rpId,] of this._rpFlows) {
          this.request(rpId);
        }
        break;
      case ONLOGIN_NOTIFICATION:
        log.debug("Received " + ONLOGIN_NOTIFICATION + "; doLogin()s fired");
        for (let [rpId,] of this._rpFlows) {
          this.request(rpId);
        }
        break;
      case ONLOGOUT_NOTIFICATION:
        log.debug("Received " + ONLOGOUT_NOTIFICATION + "; doLogout()s fired");
        for (let [rpId,] of this._rpFlows) {
          this.doLogout(rpId);
        }
        break;
      case "quit-application-granted":
        Services.obs.removeObserver(this, "quit-application-granted");
        if (ONVERIFIED_NOTIFICATION) {
          Services.obs.removeObserver(this, ONVERIFIED_NOTIFICATION);
          Services.obs.removeObserver(this, ONLOGIN_NOTIFICATION);
          Services.obs.removeObserver(this, ONLOGOUT_NOTIFICATION);
        }
        break;
    }
  },

  cleanupRPRequest: function(aRp) {
    aRp.pendingRequest = false;
    this._rpFlows.set(aRp.id, aRp);
  },

  /**
   * Register a listener for a given windowID as a result of a call to
   * navigator.id.watch().
   *
   * @param aRPCaller
   *        (Object)  an object that represents the caller document, and
   *                  is expected to have properties:
   *                  - id (unique, e.g. uuid)
   *                  - origin (string)
   *
   *                  and a bunch of callbacks
   *                  - doReady()
   *                  - doLogin()
   *                  - doLogout()
   *                  - doError()
   *                  - doCancel()
   *
   */
  watch: function watch(aRpCaller) {
    this._rpFlows.set(aRpCaller.id, aRpCaller);
    log.debug("watch: " + aRpCaller.id);
    log.debug("Current rp flows: " + this._rpFlows.size);

    // Log the user in, if possible, and then call ready().
    let runnable = {
      run: () => {
        this.fxAccountsManager.getAssertion(aRpCaller.audience,
                                            aRpCaller.principal,
                                            { silent:true }).then(
          data => {
            if (data) {
              this.doLogin(aRpCaller.id, data);
            } else {
              this.doLogout(aRpCaller.id);
            }
            this.doReady(aRpCaller.id);
          },
          error => {
            log.error("get silent assertion failed: " + JSON.stringify(error));
            this.doError(aRpCaller.id, error);
          }
        );
      }
    };
    Services.tm.currentThread.dispatch(runnable,
                                       Ci.nsIThread.DISPATCH_NORMAL);
  },

  /**
   * Delete the flow when the screen is unloaded
   */
  unwatch: function(aRpCallerId, aTargetMM) {
    log.debug("unwatching: " + aRpCallerId);
    this._rpFlows.delete(aRpCallerId);
  },

  /**
   * Initiate a login with user interaction as a result of a call to
   * navigator.id.request().
   *
   * @param aRPId
   *        (integer) the id of the doc object obtained in .watch()
   *
   * @param aOptions
   *        (Object) options including privacyPolicy, termsOfService
   */
  request: function request(aRPId, aOptions) {
    aOptions = aOptions || {};
    let rp = this._rpFlows.get(aRPId);
    if (!rp) {
      log.error("request() called before watch()");
      return;
    }

    // We check if we already have a pending request for this RP and in that
    // case we just bail out. We don't want duplicated onlogin or oncancel
    // events.
    if (rp.pendingRequest) {
      log.debug("request() already called");
      return;
    }

    // Otherwise, we set the RP flow with the pending request flag.
    rp.pendingRequest = true;
    this._rpFlows.set(rp.id, rp);

    let options = makeMessageObject(rp);
    objectCopy(aOptions, options);

    log.debug("get assertion for " + rp.audience);

    this.fxAccountsManager.getAssertion(rp.audience, rp.principal, options)
    .then(
      data => {
        log.debug("got assertion for " + rp.audience + ": " + data);
        this.doLogin(aRPId, data);
      },
      error => {
        log.debug("get assertion failed: " + JSON.stringify(error));
        // Cancellation is passed through an error channel; here we reroute.
        if ((error.error && (error.error.details == "DIALOG_CLOSED_BY_USER")) ||
            (error.details == "DIALOG_CLOSED_BY_USER")) {
          return this.doCancel(aRPId);
        }
        this.doError(aRPId, error);
      }
    )
    .then(
      () => {
        this.cleanupRPRequest(rp);
      }
    )
    .catch(
      () => {
        this.cleanupRPRequest(rp);
      }
    );
  },

  /**
   * Invoked when a user wishes to logout of a site (for instance, when clicking
   * on an in-content logout button).
   *
   * @param aRpCallerId
   *        (integer)  the id of the doc object obtained in .watch()
   *
   */
  logout: function logout(aRpCallerId) {
    // XXX Bug 945363 - Resolve the SSO story for FXA and implement
    // logout accordingly.
    //
    // For now, it makes no sense to logout from a specific RP in
    // Firefox Accounts, so just directly call the logout callback.
    if (!this._rpFlows.has(aRpCallerId)) {
      log.error("logout() called before watch()");
      return;
    }

    // Call logout() on the next tick
    let runnable = {
      run: () => {
        this.fxAccountsManager.signOut().then(() => {
          this.doLogout(aRpCallerId);
        });
      }
    };
    Services.tm.currentThread.dispatch(runnable,
                                       Ci.nsIThread.DISPATCH_NORMAL);
  },

  childProcessShutdown: function childProcessShutdown(messageManager) {
    for (let [key,] of this._rpFlows) {
      if (this._rpFlows.get(key)._mm === messageManager) {
        this._rpFlows.delete(key);
      }
    }
  },

  doLogin: function doLogin(aRpCallerId, aAssertion) {
    let rp = this._rpFlows.get(aRpCallerId);
    if (!rp) {
      log.warn("doLogin found no rp to go with callerId " + aRpCallerId);
      return;
    }

    rp.doLogin(aAssertion);
  },

  doLogout: function doLogout(aRpCallerId) {
    let rp = this._rpFlows.get(aRpCallerId);
    if (!rp) {
      log.warn("doLogout found no rp to go with callerId " + aRpCallerId);
      return;
    }

    rp.doLogout();
  },

  doReady: function doReady(aRpCallerId) {
    let rp = this._rpFlows.get(aRpCallerId);
    if (!rp) {
      log.warn("doReady found no rp to go with callerId " + aRpCallerId);
      return;
    }

    rp.doReady();
  },

  doCancel: function doCancel(aRpCallerId) {
    let rp = this._rpFlows.get(aRpCallerId);
    if (!rp) {
      log.warn("doCancel found no rp to go with callerId " + aRpCallerId);
      return;
    }

    rp.doCancel();
  },

  doError: function doError(aRpCallerId, aError) {
    let rp = this._rpFlows.get(aRpCallerId);
    if (!rp) {
      log.warn("doError found no rp to go with callerId " + aRpCallerId);
      return;
    }

    rp.doError(aError);
  }
};

this.FirefoxAccounts = new FxAccountsService();

