/* 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";

/**
 * Firefox Accounts Profile helper.
 *
 * This class abstracts interaction with the profile server for an account.
 * It will handle things like fetching profile data, listening for updates to
 * the user's profile in open browser tabs, and cacheing/invalidating profile data.
 */

this.EXPORTED_SYMBOLS = ["FxAccountsProfile"];

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

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://gre/modules/FxAccounts.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfileClient",
  "resource://gre/modules/FxAccountsProfileClient.jsm");

this.FxAccountsProfile = function(options = {}) {
  this._currentFetchPromise = null;
  this._cachedAt = 0; // when we saved the cached version.
  this._isNotifying = false; // are we sending a notification?
  this.fxa = options.fxa || fxAccounts;
  this.client = options.profileClient || new FxAccountsProfileClient({
    fxa: this.fxa,
    serverURL: options.profileServerUrl,
  });

  // An observer to invalidate our _cachedAt optimization. We use a weak-ref
  // just incase this.tearDown isn't called in some cases.
  Services.obs.addObserver(this, ON_PROFILE_CHANGE_NOTIFICATION, true);
  // for testing
  if (options.channel) {
    this.channel = options.channel;
  }
}

this.FxAccountsProfile.prototype = {
  // If we get subsequent requests for a profile within this period, don't bother
  // making another request to determine if it is fresh or not.
  PROFILE_FRESHNESS_THRESHOLD: 120000, // 2 minutes

  observe(subject, topic, data) {
    // If we get a profile change notification from our webchannel it means
    // the user has just changed their profile via the web, so we want to
    // ignore our "freshness threshold"
    if (topic == ON_PROFILE_CHANGE_NOTIFICATION && !this._isNotifying) {
      log.debug("FxAccountsProfile observed profile change");
      this._cachedAt = 0;
    }
  },

  tearDown() {
    this.fxa = null;
    this.client = null;
    Services.obs.removeObserver(this, ON_PROFILE_CHANGE_NOTIFICATION);
  },

  _notifyProfileChange(uid) {
    this._isNotifying = true;
    Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, uid);
    this._isNotifying = false;
  },

  // Cache fetched data and send out a notification so that UI can update.
  _cacheProfile(response) {
    let profileCache = {
      profile: response.body,
      etag: response.etag
    };

    return this.fxa.setProfileCache(profileCache)
      .then(() => {
        return this.fxa.getSignedInUser();
      })
      .then(userData => {
        log.debug("notifying profile changed for user ${uid}", userData);
        this._notifyProfileChange(userData.uid);
        return response.body;
      });
  },

  _fetchAndCacheProfileInternal() {
    let onFinally = () => {
      this._cachedAt = Date.now();
      this._currentFetchPromise = null;
    }
    return this.fxa.getProfileCache()
      .then(profileCache => {
        const etag = profileCache ? profileCache.etag : null;
        return this.client.fetchProfile(etag);
      })
      .then(response => {
        // response may be null if the profile was not modified (same ETag).
        return response ? this._cacheProfile(response) : null;
      })
      .then(body => { // finally block
        onFinally();
        // body may be null if the profile was not modified
        return body;
      }, err => {
        onFinally();
        throw err;
      });
  },

  _fetchAndCacheProfile() {
    if (!this._currentFetchPromise) {
      this._currentFetchPromise = this._fetchAndCacheProfileInternal();
    }
    return this._currentFetchPromise;
  },

  // Returns cached data right away if available, then fetches the latest profile
  // data in the background. After data is fetched a notification will be sent
  // out if the profile has changed.
  getProfile() {
    return this.fxa.getProfileCache()
      .then(profileCache => {
        if (profileCache) {
          if (Date.now() > this._cachedAt + this.PROFILE_FRESHNESS_THRESHOLD) {
            // Note that _fetchAndCacheProfile isn't returned, so continues
            // in the background.
            this._fetchAndCacheProfile().catch(err => {
              log.error("Background refresh of profile failed", err);
            });
          } else {
            log.trace("not checking freshness of profile as it remains recent");
          }
          return profileCache.profile;
        }
        return this._fetchAndCacheProfile();
      });
  },

  QueryInterface: XPCOMUtils.generateQI([
      Ci.nsIObserver,
      Ci.nsISupportsWeakReference,
  ]),
};
