// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;

CU.import("resource:///modules/FlockSvcUtils.jsm");

const MODULE_NAME = "AuthManager";
const CLASS_NAME = "Flock Authentication Manager";
const CLASS_SHORT_NAME = "authmgr";
const CLASS_TITLE = "Auth Manager";
const CLASS_ID = Components.ID("{96b62c22-4420-4b64-b919-3bf62af335b1}");
const CONTRACT_ID = "@flock.com/authmgr;1";

// Associative array of "network" => array("service contract IDs")
var gNetworks = [];


function AuthManager() {
  FlockSvcUtils.getLogger(this);
  this._logger.init(CLASS_SHORT_NAME);
  FlockSvcUtils.getCoop(this);
  FlockSvcUtils.getACUtils(this);
  var obs = CC["@mozilla.org/observer-service;1"]
            .getService(CI.nsIObserverService);
  obs.addObserver(this, "xpcom-shutdown", false);
}

AuthManager.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  AuthManager,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.nsIObserver,
    CI.flockIAuthenticationManager
  ]
);


// BEGIN nsIObserver
AuthManager.prototype.observe =
function AuthManager_observe(aSubject, aTopic, aState)
{
  switch (aTopic) {
    case "xpcom-shutdown":
      var obs = CC["@mozilla.org/observer-service;1"]
                .getService(CI.nsIObserverService);
      obs.removeObserver(this, "xpcom-shutdown");
      break;
  }
}
// END nsIObserver


// BEGIN flockIAuthenticationManager
AuthManager.prototype.joinNetwork =
function AuthManager_joinNetwork(aServiceID, aNetworkName)
{
  this._logger.info(".joinNetwork('" + aServiceID + "', '"
                                     + aNetworkName + "')");
  if (!gNetworks[aNetworkName]) {
    gNetworks[aNetworkName] = [];
  }
  gNetworks[aNetworkName].push(aServiceID);
}

AuthManager.prototype.findPasswordFromRelatedAccount =
function AuthManager_findPasswordFromRelatedAccount(aServiceID, aAccountID)
{
  this._logger.info(".findPasswordFromRelatedAccount('" + aServiceID + "', '"
                                                        + aAccountID + "')");
  function flockILoginInfo(aUser, aPassword) {
    return {
      QueryInterface: function authmgr_flockILoginInfo_QI(aIID) {
        if (!aIID.equals(CI.nsISupports) &&
            !aIID.equals(CI.nsILoginInfo) &&
            !aIID.equals(CI.flockILoginInfo))
        {
          throw CR.NS_ERROR_NO_INTERFACE;
        }
        return this;
      },
      formSubmitURL: "",
      httpRealm: "",
      hostname: "",
      username: aUser,
      usernameField: "",
      password: aPassword,
      passwordField: "",
      formType: "authMgr"
    };
  }
  // Loop through all services in the network
  var network = this._getNetworkForService(aServiceID);
  for each (var contractID in gNetworks[network]) {
    if (contractID && CC[contractID]) {
      var svc = CC[contractID].getService(CI.flockIWebService);
      var accounts = svc.getAccounts();
      // Loop through all the accounts configured for this service
      while (accounts && accounts.hasMoreElements()) {
        var account = accounts.getNext()
                              .QueryInterface(CI.flockIWebServiceAccount);
        var c_acct = this._coop.get(account.urn);
        if (c_acct) {
          var pwKey = svc.urn + ":" + c_acct.accountId;
          var pw = this._acUtils.getPassword(pwKey);
          if (pw) {
            // We have a password for this account, let's see if it matches
            // aAccountID
            if (c_acct.accountId.toLowerCase() == aAccountID.toLowerCase() ||
                pw.username.toLowerCase() == aAccountID.toLowerCase())
            {
              return flockILoginInfo(pw.username, pw.password);
            }
          }
        }
      }
    } else {
      this._logger.warn("no service for contractID: " + contractID);
    }
  }
  return null;
}

AuthManager.prototype.reportAccountAuthState =
function AuthManager_reportAccountAuthState(aAccountURN, aIsLoggedIn)
{
  this._logger.info(".reportAccountAuthState('" + aAccountURN + "', "
                                                + aIsLoggedIn + ")");
  if (aIsLoggedIn) {
    // The initial account whose auth state is changing is considered a
    // "catalyst" account.  The "catalyst" account has just been logged in to.
    // This implies that:
    //   (1) Accounts using *different* account IDs are now logged OUT; and
    //   (2) Accounts using the same ID on other services are now also
    //       logged IN.
    var c_catalystAcct = this._coop.get(aAccountURN);
    var network = this._getNetworkForService(c_catalystAcct.serviceId);
    if (!network) {
      this._logger.error("ERROR: network is null!!");
      return;
    }
    for each (var contractID in gNetworks[network]) {
      if (contractID == c_catalystAcct.serviceId) {
        // We don't need to update any accounts from the catalyst service; it's
        // assumed that they are already accounted for.
        continue;
      }
      if (contractID && CC[contractID]) {
        var svc = CC[contractID].getService(CI.flockIWebService);
        var accounts = svc.getAccounts();
        while (accounts && accounts.hasMoreElements()) {
          var account = accounts.getNext()
                                .QueryInterface(CI.flockIWebServiceAccount);
          var c_acct = this._coop.get(account.urn);
          if (!this._areAccountsRelated(c_acct, c_catalystAcct) &&
              c_acct.isAuthenticated)
          {
            if (c_acct.isAuthenticated) {
              this._logoutAccount(account);
            }
          }
        }
      } else {
        this._logger.warn("no service for contractID: " + contractID);
      }
    }
  } else {
    // aAccountURN has just been logged out of.  This does not necessarily imply
    // that any other accounts are automatically logged in or out.
  }
}

AuthManager.prototype.reportCompleteLoginToNetwork =
function AuthManager_reportCompleteLoginToNetwork(aAccountURN)
{
  this._logger.info(".reportCompleteLoginToNetwork('" + aAccountURN + "')");
  var c_catalystAcct = this._coop.get(aAccountURN);
  var network = this._getNetworkForService(c_catalystAcct.serviceId);
  if (!network) {
    this._logger.error("ERROR: network is null!!");
    return;
  }
  for each (var contractID in gNetworks[network]) {
    if (contractID && CC[contractID]) {
      var svc = CC[contractID].getService(CI.flockIWebService);
      var accounts = svc.getAccounts();
      while (accounts && accounts.hasMoreElements()) {
        var account = accounts.getNext()
                              .QueryInterface(CI.flockIWebServiceAccount);
        var c_acct = this._coop.get(account.urn);
        if (this._areAccountsRelated(c_acct, c_catalystAcct)) {
          if (contractID != c_catalystAcct.serviceId &&
              !c_acct.isAuthenticated)
          {
            // This is an account with the same ID, in the same network, but for
            // a different service.  We need to make sure it's logged in.
            this._loginAccount(account);
          }
        } else {
          // This is an account in the same network, but with a different ID.
          // We need to ensure that it's logged out.
          if (c_acct.isAuthenticated) {
            this._logoutAccount(account);
          }
        }
      }
    } else {
      this._logger.warn("no service for contractID: " + contractID);
    }
  }
}

AuthManager.prototype.reportCompleteLogoutFromNetwork =
function AuthManager_reportCompleteLogoutFromNetwork(aServiceID)
{
  this._logger.info(".reportCompleteLogoutFromNetwork('" + aServiceID + "')");
  var network = this._getNetworkForService(aServiceID);
  if (!network) {
    this._logger.error("ERROR: network is null!!");
    return;
  }
  for each (var contractID in gNetworks[network]) {
    if (contractID && CC[contractID]) {
      var svc = CC[contractID].getService(CI.flockIWebService);
      var accounts = svc.getAccounts();
      while (accounts && accounts.hasMoreElements()) {
        var account = accounts.getNext()
                              .QueryInterface(CI.flockIWebServiceAccount);
        if (account.isAuthenticated()) {
          this._logoutAccount(account);
        }
      }
    } else {
      this._logger.warn("no service for contractID: " + contractID);
    }
  }
}
// END flockIAuthenticationManager


// Returns TRUE iff the two accounts are related in their network.
// Assumes that they belong to the same network.
AuthManager.prototype._areAccountsRelated =
function AuthManager__areAccountsRelated(aCoopAcct1, aCoopAcct2)
{
  if (aCoopAcct1.accountId == aCoopAcct2.accountId) {
    return true;
  }
  var pwKey1 = aCoopAcct1.service.id() + ":" + aCoopAcct1.accountId;
  var pw1 = this._acUtils.getPassword(pwKey1);
  if (pw1 && pw1.username && pw1.username == aCoopAcct2.accountId) {
    return true;
  }
  var pwKey2 = aCoopAcct2.service.id() + ":" + aCoopAcct2.accountId;
  var pw2 = this._acUtils.getPassword(pwKey2);
  if (pw2 && pw2.username && pw2.username == aCoopAcct1.accountId) {
    return true;
  }
  if (pw1 && pw1.username && pw1.username.length &&
      pw2 && pw2.username && pw2.username.length &&
      pw1.username == pw2.username)
  {
    return true;
  }
  return false;
}

AuthManager.prototype._getNetworkForService =
function AuthManager__getNetworkForService(aContractID)
{
  for (var network in gNetworks) {
    var services = gNetworks[network];
    for each (var cid in services) {
      if (cid == aContractID) {
        this._logger.debug("network is: " + network);
        return network;
      }
    }
  }
  return null;
}

AuthManager.prototype._loginAccount =
function AuthManager__loginAccount(aAccount)
{
  this._logger.debug("._loginAccount('" + aAccount.urn + "')");
  var inst = this;
  var loginListener = {
    onSuccess: function loginListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("loginListener.onSuccess() " + aAccount.urn);
    },
    onError: function loginListener_onError(aFlockError, aTopic) {
      inst._logger.error("loginListener.onError() " + aAccount.urn);
    }
  };
  aAccount.login(loginListener);
}

AuthManager.prototype._logoutAccount =
function AuthManager__logoutAccount(aAccount)
{
  this._logger.debug("._logoutAccount('" + aAccount.urn + "')");
  var inst = this;
  var logoutListener = {
    onSuccess: function logoutListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("logoutListener.onSuccess() " + aAccount.urn);
    },
    onError: function logoutListener_onError(aFlockError, aTopic) {
      inst._logger.error("logoutListener.onError() " + aAccount.urn);
    }
  };
  aAccount.logout(logoutListener);
}


// Create array of components.
var componentsArray = [AuthManager];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      componentsArray);
