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

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

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

this.EXPORTED_SYMBOLS = [];

const PAYMENT_IPC_MSG_NAMES = ["Payment:Pay",
                               "Payment:Success",
                               "Payment:Failed"];

const PREF_PAYMENTPROVIDERS_BRANCH = "dom.payment.provider.";
const PREF_PAYMENT_BRANCH = "dom.payment.";
const PREF_DEBUG = "dom.payment.debug";

XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                   "@mozilla.org/parentprocessmessagemanager;1",
                                   "nsIMessageListenerManager");

XPCOMUtils.defineLazyServiceGetter(this, "prefService",
                                   "@mozilla.org/preferences-service;1",
                                   "nsIPrefService");

let PaymentManager =  {
  init: function init() {
    // Payment providers data are stored as a preference.
    this.registeredProviders = null;

    this.messageManagers = {};

    // The dom.payment.skipHTTPSCheck pref is supposed to be used only during
    // development process. This preference should not be active for a
    // production build.
    let paymentPrefs = prefService.getBranch(PREF_PAYMENT_BRANCH);
    this.checkHttps = true;
    try {
      if (paymentPrefs.getPrefType("skipHTTPSCheck")) {
        this.checkHttps = !paymentPrefs.getBoolPref("skipHTTPSCheck");
      }
    } catch(e) {}

    for each (let msgname in PAYMENT_IPC_MSG_NAMES) {
      ppmm.addMessageListener(msgname, this);
    }

    Services.obs.addObserver(this, "xpcom-shutdown", false);

    try {
      this._debug =
        Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL
        && Services.prefs.getBoolPref(PREF_DEBUG);
    } catch(e) {
      this._debug = false;
    }
  },

  /**
   * Process a message from the content process.
   */
  receiveMessage: function receiveMessage(aMessage) {
    let name = aMessage.name;
    let msg = aMessage.json;
    if (this._debug) {
      this.LOG("Received '" + name + "' message from content process");
    }

    switch (name) {
      case "Payment:Pay": {
        // First of all, we register the payment providers.
        if (!this.registeredProviders) {
          this.registeredProviders = {};
          this.registerPaymentProviders();
        }

        // We save the message target message manager so we can later dispatch
        // back messages without broadcasting to all child processes.
        let requestId = msg.requestId;
        this.messageManagers[requestId] = aMessage.target;

        // We check the jwt type and look for a match within the
        // registered payment providers to get the correct payment request
        // information.
        let paymentRequests = [];
        let jwtTypes = [];
        for (let i in msg.jwts) {
          let pr = this.getPaymentRequestInfo(requestId, msg.jwts[i]);
          if (!pr) {
            continue;
          }
          // We consider jwt type repetition an error.
          if (jwtTypes[pr.type]) {
            this.paymentFailed(requestId,
                               "PAY_REQUEST_ERROR_DUPLICATED_JWT_TYPE");
            return;
          }
          jwtTypes[pr.type] = true;
          paymentRequests.push(pr);
        }

        if (!paymentRequests.length) {
          this.paymentFailed(requestId,
                             "PAY_REQUEST_ERROR_NO_VALID_REQUEST_FOUND");
          return;
        }

        // After getting the list of valid payment requests, we ask the user
        // for confirmation before sending any request to any payment provider.
        // If there is more than one choice, we also let the user select the one
        // that he prefers.
        let glue = Cc["@mozilla.org/payment/ui-glue;1"]
                   .createInstance(Ci.nsIPaymentUIGlue);
        if (!glue) {
          if (this._debug) {
            this.LOG("Could not create nsIPaymentUIGlue instance");
          }
          this.paymentFailed(requestId,
                             "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED");
          return;
        }

        let confirmPaymentSuccessCb = function successCb(aRequestId,
                                                         aResult) {
          // Get the appropriate payment provider data based on user's choice.
          let selectedProvider = this.registeredProviders[aResult];
          if (!selectedProvider || !selectedProvider.uri) {
            if (this._debug) {
              this.LOG("Could not retrieve a valid provider based on user's " +
                        "selection");
            }
            this.paymentFailed(aRequestId,
                               "INTERNAL_ERROR_NO_VALID_SELECTED_PROVIDER");
            return;
          }

          let jwt;
          for (let i in paymentRequests) {
            if (paymentRequests[i].type == aResult) {
              jwt = paymentRequests[i].jwt;
              break;
            }
          }
          if (!jwt) {
            if (this._debug) {
              this.LOG("The selected request has no JWT information " +
                        "associated");
            }
            this.paymentFailed(aRequestId,
                               "INTERNAL_ERROR_NO_JWT_ASSOCIATED_TO_REQUEST");
            return;
          }

          this.showPaymentFlow(aRequestId, selectedProvider, jwt);
        };

        let confirmPaymentErrorCb = this.paymentFailed;

        glue.confirmPaymentRequest(requestId,
                                   paymentRequests,
                                   confirmPaymentSuccessCb.bind(this),
                                   confirmPaymentErrorCb.bind(this));
        break;
      }
      case "Payment:Success":
      case "Payment:Failed": {
        let mm = this.messageManagers[msg.requestId];
        mm.sendAsyncMessage(name, {
          requestId: msg.requestId,
          result: msg.result,
          errorMsg: msg.errorMsg
        });
        break;
      }
    }
  },

  /**
   * Helper function to register payment providers stored as preferences.
   */
  registerPaymentProviders: function registerPaymentProviders() {
    let paymentProviders = prefService
                           .getBranch(PREF_PAYMENTPROVIDERS_BRANCH)
                           .getChildList("");

    // First get the numbers of the providers by getting all ###.uri prefs.
    let nums = [];
    for (let i in paymentProviders) {
      let match = /^(\d+)\.uri$/.exec(paymentProviders[i]);
      if (!match) {
        continue;
      } else {
        nums.push(match[1]);
      }
    }

    // Now register the payment providers.
    for (let i in nums) {
      let branch = prefService
                   .getBranch(PREF_PAYMENTPROVIDERS_BRANCH + nums[i] + ".");
      let vals = branch.getChildList("");
      if (vals.length == 0) {
        return;
      }
      try {
        let type = branch.getCharPref("type");
        if (type in this.registeredProviders) {
          continue;
        }
        this.registeredProviders[type] = {
          name: branch.getCharPref("name"),
          uri: branch.getCharPref("uri"),
          description: branch.getCharPref("description"),
          requestMethod: branch.getCharPref("requestMethod")
        };
        if (this._debug) {
          this.LOG("Registered Payment Providers: " +
                    JSON.stringify(this.registeredProviders[type]));
        }
      } catch (ex) {
        if (this._debug) {
          this.LOG("An error ocurred registering a payment provider. " + ex);
        }
      }
    }
  },

  /**
   * Helper for sending a Payment:Failed message to the parent process.
   */
  paymentFailed: function paymentFailed(aRequestId, aErrorMsg) {
    let mm = this.messageManagers[aRequestId];
    mm.sendAsyncMessage("Payment:Failed", {
      requestId: aRequestId,
      errorMsg: aErrorMsg
    });
  },

  /**
   * Helper function to get the payment request info according to the jwt
   * type. Payment provider's data is stored as a preference.
   */
  getPaymentRequestInfo: function getPaymentRequestInfo(aRequestId, aJwt) {
    if (!aJwt) {
      this.paymentFailed(aRequestId, "INTERNAL_ERROR_CALL_WITH_MISSING_JWT");
      return true;
    }

    // First thing, we check that the jwt type is an allowed type and has a
    // payment provider flow information associated.

    // A jwt string consists in three parts separated by period ('.'): header,
    // payload and signature.
    let segments = aJwt.split('.');
    if (segments.length !== 3) {
      if (this._debug) {
        this.LOG("Error getting payment provider's uri. " +
                  "Not enough or too many segments");
      }
      this.paymentFailed(aRequestId,
                         "PAY_REQUEST_ERROR_WRONG_SEGMENTS_COUNT");
      return true;
    }

    let payloadObject;
    try {
      // We only care about the payload segment, which contains the jwt type
      // that should match with any of the stored payment provider's data and
      // the payment request information to be shown to the user.
      // Before decoding the JWT string we need to normalize it to be compliant
      // with RFC 4648.
      segments[1] = segments[1].replace("-", "+", "g").replace("_", "/", "g");
      let payload = atob(segments[1]);
      if (this._debug) {
        this.LOG("Payload " + payload);
      }
      if (!payload.length) {
        this.paymentFailed(aRequestId, "PAY_REQUEST_ERROR_EMPTY_PAYLOAD");
        return true;
      }
      payloadObject = JSON.parse(payload);
      if (!payloadObject) {
        this.paymentFailed(aRequestId,
                           "PAY_REQUEST_ERROR_ERROR_PARSING_JWT_PAYLOAD");
        return true;
      }
    } catch (e) {
      this.paymentFailed(aRequestId,
                         "PAY_REQUEST_ERROR_ERROR_DECODING_JWT");
      return true;
    }

    if (!payloadObject.typ) {
      this.paymentFailed(aRequestId,
                         "PAY_REQUEST_ERROR_NO_TYP_PARAMETER");
      return true;
    }

    if (!payloadObject.request) {
      this.paymentFailed(aRequestId,
                         "PAY_REQUEST_ERROR_NO_REQUEST_PARAMETER");
      return true;
    }

    // Once we got the jwt 'typ' value we look for a match within the payment
    // providers stored preferences. If the jwt 'typ' is not recognized as one
    // of the allowed values for registered payment providers, we skip the jwt
    // validation but we don't fire any error. This way developers might have
    // a default set of well formed JWTs that might be used in different B2G
    // devices with a different set of allowed payment providers.
    let provider = this.registeredProviders[payloadObject.typ];
    if (!provider) {
      if (this._debug) {
        this.LOG("Not registered payment provider for jwt type: " +
                  payloadObject.typ);
      }
      return false;
    }

    if (!provider.uri || !provider.name) {
      this.paymentFailed(aRequestId,
                         "INTERNAL_ERROR_WRONG_REGISTERED_PAY_PROVIDER");
      return true;
    }

    // We only allow https for payment providers uris.
    if (this.checkHttps && !/^https/.exec(provider.uri.toLowerCase())) {
      // We should never get this far.
      if (this._debug) {
        this.LOG("Payment provider uris must be https: " + provider.uri);
      }
      this.paymentFailed(aRequestId,
                         "INTERNAL_ERROR_NON_HTTPS_PROVIDER_URI");
      return true;
    }

    let pldRequest = payloadObject.request;
    return { jwt: aJwt, type: payloadObject.typ, providerName: provider.name };
  },

  showPaymentFlow: function showPaymentFlow(aRequestId,
                                            aPaymentProvider,
                                            aJwt) {
    let paymentFlowInfo = Cc["@mozilla.org/payment/flow-info;1"]
                          .createInstance(Ci.nsIPaymentFlowInfo);
    paymentFlowInfo.uri = aPaymentProvider.uri;
    paymentFlowInfo.requestMethod = aPaymentProvider.requestMethod;
    paymentFlowInfo.jwt = aJwt;

    let glue = Cc["@mozilla.org/payment/ui-glue;1"]
               .createInstance(Ci.nsIPaymentUIGlue);
    if (!glue) {
      if (this._debug) {
        this.LOG("Could not create nsIPaymentUIGlue instance");
      }
      this.paymentFailed(aRequestId,
                         "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED");
      return false;
    }
    glue.showPaymentFlow(aRequestId,
                         paymentFlowInfo,
                         this.paymentFailed.bind(this));
  },

  // nsIObserver

  observe: function observe(subject, topic, data) {
    if (topic == "xpcom-shutdown") {
      for each (let msgname in PAYMENT_IPC_MSG_NAMES) {
        ppmm.removeMessageListener(msgname, this);
      }
      this.registeredProviders = null;
      this.messageManagers = null;

      Services.obs.removeObserver(this, "xpcom-shutdown");
    }
  },

  LOG: function LOG(s) {
    if (!this._debug) {
      return;
    }
    dump("-*- PaymentManager: " + s + "\n");
  }
};

PaymentManager.init();
