/* jshint moz: true, esnext: true */
/* 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/. */

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

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Messaging.jsm");
const {
  PushCrypto,
  getCryptoParams,
} = Cu.import("resource://gre/modules/PushCrypto.jsm", {});

XPCOMUtils.defineLazyServiceGetter(this, "PushService",
  "@mozilla.org/push/Service;1", "nsIPushService");
XPCOMUtils.defineLazyGetter(this, "_decoder", () => new TextDecoder());

const FXA_PUSH_SCOPE = "chrome://fxa-push";
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccountsPush");

function FxAccountsPush() {
  Services.obs.addObserver(this, "FxAccountsPush:ReceivedPushMessageToDecode");

  EventDispatcher.instance.sendRequestForResult({
    type: "FxAccountsPush:Initialized"
  });
}

FxAccountsPush.prototype = {
  observe: function(subject, topic, data) {
    switch (topic) {
      case "android-push-service":
        if (data === "android-fxa-subscribe") {
          this._subscribe();
        } else if (data === "android-fxa-unsubscribe") {
          this._unsubscribe();
        } else if (data === "android-fxa-resubscribe") {
          // If unsubscription fails, we still want to try to subscribe.
          this._unsubscribe().then(this._subscribe, this._subscribe);
        }
        break;
      case "FxAccountsPush:ReceivedPushMessageToDecode":
        this._decodePushMessage(data);
        break;
    }
  },

  _subscribe() {
    Log.i("FxAccountsPush _subscribe");
    return new Promise((resolve, reject) => {
      PushService.subscribe(FXA_PUSH_SCOPE,
        Services.scriptSecurityManager.getSystemPrincipal(),
        (result, subscription) => {
          if (Components.isSuccessCode(result)) {
            Log.d("FxAccountsPush got subscription");
            resolve(subscription);
          } else {
            Log.w("FxAccountsPush failed to subscribe", result);
            const err = new Error("FxAccountsPush failed to subscribe");
            err.result = result;
            reject(err);
          }
        });
    })
    .then(subscription => {
      EventDispatcher.instance.sendRequest({
        type: "FxAccountsPush:Subscribe:Response",
        subscription: {
          pushCallback: subscription.endpoint,
          pushPublicKey: urlsafeBase64Encode(subscription.getKey("p256dh")),
          pushAuthKey: urlsafeBase64Encode(subscription.getKey("auth"))
        }
      });
    })
    .catch(err => {
      Log.i("Error when registering FxA push endpoint " + err);
      EventDispatcher.instance.sendRequest({
        type: "FxAccountsPush:Subscribe:Response",
        error: err.result.toString() // Convert to string because the GeckoBundle can't getLong();
      });
    });
  },

  _unsubscribe() {
    Log.i("FxAccountsPush _unsubscribe");
    return new Promise((resolve) => {
      PushService.unsubscribe(FXA_PUSH_SCOPE,
        Services.scriptSecurityManager.getSystemPrincipal(),
        (result, ok) => {
          if (Components.isSuccessCode(result)) {
            if (ok === true) {
              Log.d("FxAccountsPush unsubscribed");
            } else {
              Log.d("FxAccountsPush had no subscription to unsubscribe");
            }
          } else {
            Log.w("FxAccountsPush failed to unsubscribe", result);
          }
          return resolve(ok);
        });
    }).catch(err => {
      Log.e("Error during unsubscribe", err);
    });
  },

  _decodePushMessage(data) {
    Log.i("FxAccountsPush _decodePushMessage");
    data = JSON.parse(data);
    let { headers, message } = this._messageAndHeaders(data);
    return new Promise((resolve, reject) => {
      PushService.getSubscription(FXA_PUSH_SCOPE,
        Services.scriptSecurityManager.getSystemPrincipal(),
        (result, subscription) => {
          if (!subscription) {
            return reject(new Error("No subscription found"));
          }
          return resolve(subscription);
        });
    }).then(subscription => {
      return PushCrypto.decrypt(subscription.p256dhPrivateKey,
                                new Uint8Array(subscription.getKey("p256dh")),
                                new Uint8Array(subscription.getKey("auth")),
                                headers, message);
    })
    .then(plaintext => {
      let decryptedMessage = plaintext ? _decoder.decode(plaintext) : "";
      EventDispatcher.instance.sendRequestForResult({
        type: "FxAccountsPush:ReceivedPushMessageToDecode:Response",
        message: decryptedMessage
      });
    })
    .catch(err => {
      Log.d("Error while decoding incoming message : " + err);
    });
  },

  // Copied from PushServiceAndroidGCM
  _messageAndHeaders(data) {
    // Default is no data (and no encryption).
    let message = null;
    let headers = null;

    if (data.message && data.enc && (data.enckey || data.cryptokey)) {
      headers = {
        encryption_key: data.enckey,
        crypto_key: data.cryptokey,
        encryption: data.enc,
        encoding: data.con,
      };
      // Ciphertext is (urlsafe) Base 64 encoded.
      message = ChromeUtils.base64URLDecode(data.message, {
        // The Push server may append padding.
        padding: "ignore",
      });
    }
    return { headers, message };
  },

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

  classID: Components.ID("{d1bbb0fd-1d47-4134-9c12-d7b1be20b721}")
};

function urlsafeBase64Encode(key) {
  return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
}

var components = [ FxAccountsPush ];
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
