(function (exports) {
  'use strict';

  var TherapySession;
  var Oauth3 = (exports.OAUTH3 || require('./oauth3'));

  //
  // Pure convenience / utility funcs
  //
  function createSession() {
    return { logins: [], accounts: [] };
  }
  function removeItem(array, item) {
    var i = array.indexOf(item);

    if (-1 !== i) {
      array.splice(i, 1);
    }
  }

  var TLogins = {};
  var TAccounts = {};
  var InternalApi;
  var api;

  function create(opts) {
    var myInstance = {};
    var conf = {
      session: createSession()
    , sessionKey: opts.namespace + '.' + opts.sessionKey // 'session'
    , cache: opts.cache
    , config: opts.config
    , usernameMinLength: opts.usernameMinLength
    , secretMinLength: opts.secretMinLength
    };

    Object.keys(TherapySession.api).forEach(function (key) {
      myInstance[key] = function () {
        var args = Array.prototype.slice.call(arguments);
        args.unshift(conf);
        return TherapySession.api[key].apply(null, args);
      };
    });

    myInstance.getId = TherapySession.getId;
    myInstance.openAuthorizationDialog = function () {
      // TODO guarantee that this happens assignment happens before initialization?
      return (opts.invokeLogin || opts.config.invokeLogin).apply(null, arguments);
    };
    myInstance.usernameMinLength = opts.usernameMinLength;
    myInstance.secretMinLength = opts.secretMinLength;
    myInstance.api = api;

    myInstance._conf = conf;

    return myInstance;
  }

  // TODO track and compare granted scopes locally
  function save(conf, updates) {
    // TODO make sure session.logins[0] is most recent
    api.updateSession(conf, updates.login, updates.accounts);

    // TODO should this be done by the LocalApiStorage?
    // TODO how to have different accounts selected in different tabs?
    localStorage.setItem(conf.sessionKey, JSON.stringify(conf.session));
    return Oauth3.PromiseA.resolve(conf.session);
  }

  function restore(conf) {
    // Being very careful not to trigger a false onLogin or onLogout via $watch
    var storedSession;

    if (conf.session.token) {
      return api.sanityCheckAccounts(conf);
      // return Oauth3.PromiseA.resolve(conf.session);
    }

    storedSession = JSON.parse(localStorage.getItem(conf.sessionKey) || null) || createSession();

    if (storedSession.token) {
      conf.session = storedSession;
      return api.sanityCheckAccounts(conf);
      //return Oauth3.PromiseA.resolve(conf.session);
    } else {
      return Oauth3.PromiseA.reject(new Error("No Session"));
    }
  }

  function destroy(conf) {
    conf.session = createSession();
    localStorage.removeItem(conf.sessionKey);
    return conf.cache.destroy(conf).then(function (session) {
      return session;
    });
  }

  function accounts(conf, login) {
    return Oauth3.request({
      url: conf.config.apiBaseUri + conf.config.apiPrefix + '/accounts'
    , method: 'GET'
    , headers: { 'Authorization': 'Bearer ' + login.token }
    }).then(function (resp) {
      var accounts = resp.data && (resp.data.accounts || resp.data.result || resp.data.results)
        || resp.data || { error: { message: "Unknown Error when retrieving accounts" } }
        ;

      if (accounts.error) {
        console.error("[ERROR] couldn't fetch accounts", accounts);
        return Oauth3.PromiseA.reject(new Error("Could not verify login:" + accounts.error.message));
      }

      if (!Array.isArray(accounts)) {
        console.error("[Uknown ERROR] couldn't fetch accounts, no proper error", accounts);
        // TODO destroy(conf);
        return Oauth3.PromiseA.reject(new Error("could not verify login")); // destroy(conf);
      }

      return accounts;
    });
  }

  // TODO move to LocalApiLogin?
  function testLoginAccounts(conf, login) {
    // TODO cache this also, but with a shorter shelf life?
    return TherapySession.api.accounts(conf, login).then(function (accounts) {
      return { login: login, accounts: accounts };
    }, function (err) {
      console.error("[Error] couldn't get accounts (might not be linked)");
      console.warn(err);
      return { login: login, accounts: [] };
    });
  }

  function logout(conf) {
    console.log('DEBUG logout', conf);
    return Oauth3.logout(conf.config.providerUri, {}).then(function () {
      console.log('DEBUG Oauth3.logout');
      return destroy(conf);
    }, function () {
      return destroy(conf);
    });
  }

  function backgroundLogin(conf, opts) {
    opts = opts || {};

    opts.background = true;
    return TherapySession.api.login(conf, opts);
  }

  function login(conf, opts) {
    console.log('##### DEBUG TherapySession');
    console.log(conf);
    console.log(opts);
    // this should work first party and third party
    var promise;
    var providerUri = (opts && opts.providerUri) || conf.config.providerUri;

    opts = opts || {};
    //opts.redirectUri = conf.config.appUri + '/oauth3.html';

    // TODO note that this must be called on a click event
    // otherwise the browser will block the popup
    function forceLogin() {
      opts.appId = opts.appId || conf.config.appId;
      opts.clientUri = opts.clientUri || conf.config.clientUri;
      opts.clientAgreeTos = opts.clientAgreeTos || conf.config.clientAgreeTos;
      var username = opts.username;
      // TODO why is login modifying the opts?
      return Oauth3.login(providerUri, opts).then(function (params) {
        return TLogins.getLoginFromTokenParams(conf, providerUri, username, params).then(function (login) {
          return testLoginAccounts(conf, login).then(function (updates) {
            return save(conf, updates);
          });
        });
      });
    }

    if (!opts.force) {
      promise = restore(conf, opts.scope);
    } else {
      promise = Oauth3.PromiseA.reject();
    }

    // TODO check for scope in session
    return promise.then(function (session) {
      if (!session.appScopedId || opts && opts.force) {
        return forceLogin();
      }

      var promise = Oauth3.PromiseA.resolve();

      // TODO check expirey
      session.logins.forEach(function (login) {
        promise = promise.then(function () {
          return testLoginAccounts(conf, login).then(function (updates) {
            return save(conf, updates);
          });
        });
      });

      return promise;
    }, forceLogin).then(function (session) {
      // testLoginAccounts().then(save);
      return session;
    });
  }

  function requireSession(conf, opts) {
    var promise = Oauth3.PromiseA.resolve(opts);

    // TODO create middleware stack
    return promise.then(function () {
      return TLogins.requireLogin(conf, opts);
    }).then(function () {
      return TAccounts.requireAccount(conf, opts);
    });
      // .then(selectAccount).then(verifyAccount)
  }

  function onLogin(conf, _scope, fn) {
    // This is better than using a promise.notify
    // because the watches will unwatch when the controller is destroyed
    _scope.__stsessionshared__ = conf;
    _scope.$watch('__stsessionshared__.session', function (newValue, oldValue) {
      if (newValue.accountId && oldValue.accountId !== newValue.accountId) {
        fn(conf.session);
      }
    }, true);
  }

  function onLogout(conf, _scope, fn) {
    _scope.__stsessionshared__ = conf;
    _scope.$watch('__stsessionshared__.session', function (newValue, oldValue) {
      if (!newValue.accountId && oldValue.accountId) {
        fn(null);
      }
    }, true);
  }


  function getToken(conf, accountId) {
    var session = conf.session;
    var logins = [];
    var login;
    accountId = TAccounts.getId(accountId) || accountId;

    // search logins first because we know we're actually
    // logged in with said login, y'know?
    session.logins.forEach(function (login) {
      login.accounts.forEach(function (account) {
        if (TAccounts.getId(account) === accountId) {
          logins.push(login);
        }
      });
    });

    login = logins.sort(function (a, b) {
      // b - a // most recent first
      return (new Date(b.expiresAt).value || 0) - (new Date(a.expiresAt).value || 0);
    })[0];

    return login && login.token;
  }

  // this should be done at every login
  // even an existing login may gain new accounts
  function addAccountsToSession(conf, login, accounts) {
    var now = Date.now();

    login.accounts = accounts.map(function (account) {
      account.addedAt = account.addedAt || now;
      return {
        id: TAccounts.getId(account)
      , addedAt: now
      };
    });

    accounts.forEach(function (newAccount) {
      if (!conf.session.accounts.some(function (other, i) {
        if (TAccounts.getId(other) === TAccounts.getId(newAccount)) {
          conf.session.accounts[i] = newAccount;
          return true;
        }
      })) {
        conf.session.accounts.push(newAccount);
      }
    });

    conf.session.accounts.sort(function (a, b) {
      return b.addedAt - a.addedAt;
    });
  }

  // this should be done on login and logout
  // an old login may have lost or gained accounts
  function pruneAccountsFromSession(conf) {
    var session = conf.session;
    var accounts = session.accounts.slice(0);

    // remember, you can't modify an array while it's in-loop
    // well, you can... but it would be bad!
    accounts.forEach(function (account) {
      if (!session.logins.some(function (login) {
        return login.accounts.some(function (a) {
          return TAccounts.getId(a) === TAccounts.getId(account);
        });
      })) {
        removeItem(session.accounts, account);
      }
    });
  }

  function refreshCurrentAccount(conf) {
    var session = conf.session;

    // select a default session
    if (1 === session.accounts.length) {
      session.accountId = TAccounts.getId(session.accounts[0]);
      session.id = session.accountId;
      session.appScopedId = session.accountId;
      session.token = session.accountId && api.getToken(conf, session.accountId) || null;
      session.userVerifiedAt = session.accounts[0].userVerifiedAt;
      return;
    }

    if (!session.logins.some(function (account) {
      if (session.accountId === TAccounts.getId(account)) {
        session.accountId = TAccounts.getId(account);
        session.id = session.accountId;
        session.appScopedId = session.accountId;
        session.token = session.accountId && api.getToken(conf, session.accountId) || null;
        session.userVerifiedAt = account.userVerifiedAt;
      }
    })) {
      session.accountId = null;
      session.id = null;
      session.appScopedId = null;
      session.token = null;
      session.userVerifiedAt = null;
    }
  }

  function updateSession(conf, login, accounts) {
    var session = conf.session;

    login.addedAt = login.addedAt || Date.now();

    // sanity check login
    if (0 === accounts.length) {
      login.selectedAccountId = null;
    }
    else if (1 === accounts.length) {
      login.selectedAccountId = TAccounts.getId(accounts[0]);
    }
    else if (accounts.length >= 1) {
      login.selectedAccountId = null;
    }
    else {
      throw new Error("[SANITY CHECK FAILED] bad account length'");
    }

    api.addAccountsToSession(conf, login, accounts);

    // update login if it exists
    // (or add it if it doesn't)
    if (!session.logins.some(function (other, i) {
      if ((login.loginId && other.loginId === login.loginId) || (other.token === login.token)) {
        session.logins[i] = login;
        return true;
      }
    })) {
      session.logins.push(login);
    }

    api.pruneAccountsFromSession(conf);

    api.refreshCurrentAccount(conf);

    session.logins.sort(function (a, b) {
      return b.addedAt - a.addedAt;
    });
  }

  function sanityCheckAccounts(conf) {
    var promise;
    var session = conf.session;

    // XXX this is just a bugfix for previously deployed code
    // it probably only affects about 10 users and can be deleted
    // at some point in the future (or left as a sanity check)

    if (session.accounts.every(function (account) {
      if (account.appScopedId) {
        return true;
      }
    })) {
      return Oauth3.PromiseA.resolve(session);
    }

    promise = Oauth3.PromiseA.resolve();
    session.logins.forEach(function (login) {
      promise = promise.then(function () {
        return testLoginAccounts(conf, login).then(function (updates) {
          return save(conf, updates);
        });
      });
    });

    return promise.then(function (session) {
      return session;
    }, function () {
      // this is just bad news...
      return conf.cache.destroy(conf).then(function () {
        window.alert("Sorry, but an error occurred which can only be fixed by logging you out"
          + " and refreshing the page.\n\nThis will happen automatically.\n\nIf you get this"
          + " message even after the page refreshes, please contact support@betopool.com."
        );
        window.location.reload();
        return Oauth3.PromiseA.reject(new Error("A session error occured. You must log out and log back in."));
      });
    });
  }

  // TODO is this more logins or accounts or session? session?
  function handleOrphanLogins(conf) {
    var promise;
    var session = conf.session;

    promise = Oauth3.PromiseA.resolve();

    if (session.logins.some(function (login) {
      return !login.accounts.length;
    })) {
      if (session.accounts.length > 1) {
        throw new Error("[Not Implemented] can't yet attach new social logins when more than one local account is in the session."
          + " Please logout and sign back in with your Local Account only. Then attach the other login.");
      }
      session.logins.forEach(function (login) {
        if (!login.accounts.length) {
          promise = promise.then(function () {
            return TAccounts.attachLoginToAccount(conf, session.accounts[0], login);
          });
        }
      });
    }

    return promise.then(function () {
      return session;
    });
  }

  TLogins.getLoginFromTokenParams = function (conf, providerUri, username, params) {
    var err;
    var accessToken;
    var refreshToken;
    var expiresAt;
    var match;
    var data;
    var login;
    var now = Date.now();

    if (!params) {
      err = new Error("[Developer Error] No params were passed to the token parser");
      err.code = 'E_DEV_ERROR';
      err.uri = 'https://oauth3.org/docs/errors/#E_DEV_ERROR';
      return Oauth3.PromiseA.reject(err);
    }

    console.log('[DEBUG] params', params);

    accessToken = (params.oauth3_token || params.oauth3Token || params.jwt || params.access_token || params.accessToken || params.token);
    refreshToken = (params.oauth3Refresh || params.oauth3_refresh || params.jwt_refresh || params.refresh_token || params.refreshToken);

    if (!accessToken) {
      if (!(params.error || params.error_description)) {
        err = new Error("[Server Error] The server did not grant access nor give an error message");
        err.code = "E_SERVER_ERROR";
        err.uri = params.error_uri || '';
        return Oauth3.PromiseA.reject(err);
      }

      err = new Error(params.error_description || ": invalid username or secret");
      err.code = params.error || "_access_denied";
      err.uri = params.error_uri || '';
      return Oauth3.PromiseA.reject(err);
    }

    // JWT <<base64>>.<<base64>>.<<base64>>
    // pass yada.yada.yada
    // fail yada yada.yada
    // fail y?da.yada.yada
    match = accessToken.match(/^[A-Za-z0-9+=_\/\-]+\.([A-Za-z0-9+=_\/\-]+)\.[A-Za-z0-9+=_\/\-]+$/);
    if (match) {
      try {
        data = JSON.parse(atob(match[1]));
      } catch(e) {
        data = {};
      }
    } else {
      data = {};
    }

    // TODO support fewer expiry methods
    expiresAt = [
      params.expires_at, params.expiresAt, params.expires_in, params.expiresIn, params.expires, data.exp
    ].map(function (exp) {
      exp = parseInt(exp, 10) || 0;
      var year = 365 * 24 * 60 * 60 * 1000;
      var min = now - (1 * year);
      var max = now + (2 * year);

      // date of expiration, already in ms
      if (exp > min && exp < max) {
        return exp;
      }
      // date of expiration in seconds
      if (exp > (min / 1000) && exp < (max / 1000)) {
        return exp * 1000;
      }
      // time remaining in seconds
      if (exp > 1 && exp < (2 * year)) {
        return now + (exp * 1000);
      }
    }).filter(function (exp) {
      return exp;
    })[0] || (Date.now() + 1 * 60 * 60 * 1000);

    // TODO drop prefixes everywhere
    providerUri = providerUri.replace(/^(https?:\/\/)?(www\.)?/, '');
    login = {
      token: accessToken
    , refreshToken: refreshToken
    , expiresAt: expiresAt
    , appScopedId: params.app_scoped_id || params.appScopedId
        || data.idx || data.usr || username
        || null
    , loginId: params.loginId || params.login_id
        || data.id || data.usr
    , accountId: params.accountId || params.account_id
        || data.acx || data.acc
      // TODO app_name in oauth3.json "AJ on Facebook"
    , comment: data.sub || data.com ||
        (
          (username && (username + ' via ') || '')
        + (providerUri)
        )
    , loginType: ('password' === data.grt || username) ? 'localaccount' : null
    , providerUri: providerUri
    };

    return Oauth3.PromiseA.resolve(login);
  };

  TLogins.requireLogin = function (conf, opts) {
    return restore(conf).then(function (session) {
      return session;
    }, function (/*err*/) {

      return conf.config.invokeLogin(opts);
    });
  };

  TLogins.create = function (conf, username, type, secret, kdf, mfa) {
    // secret is optional (for server-side requirement checking)
    // kdf is mandatory (
    return Oauth3.request({
      url: conf.config.apiBaseUri + '/api'
        + '/org.oauth3.provider'
        + '/logins/'
    , method: 'POST'
    , data: {
        id: username
      , type: type
      , secret: secret
      , kdf: kdf
      , mfa: mfa
      }
    });
  };

  TLogins.softTestUsername = function (conf, username) {
    if ('string' !== typeof username) {
      throw new Error("[Developer Error] username should be a string");
    }

    /*
    if (!/^[0-9a-z\.\-_]+$/i.test(username)) {
      // TODO validate this is true on the server
      return new Error("Only alphanumeric characters, '-', '_', and '.' are allowed in usernames.");
    }
    */

    if (!/^[^@]+@[^\.]+\.[^\.]+$/i.test(username)) {
      // TODO validate this is true on the server
      return new Error("You must use an email address.");
    }

    if (username.length < conf.usernameMinLength) {
      // TODO validate this is true on the server
      return new Error('Username too short. Use at least '
        + conf.usernameMinLength + ' characters.');
    }

    return true;
  };

  TLogins.getMeta = function (conf, username) {
    // TODO support username as type
    var type = null;

    // TODO update backend to /api/promoonlyonline/username/:username?
    return Oauth3.request({
      url: conf.config.apiBaseUri + '/api'
        + '/org.oauth3.provider'
        + '/logins/meta/' + type + '/' + username
    , method: 'GET'
    }).then(function (resp) {
      if (!resp.data.kdf) {
        return Oauth3.PromiseA.reject(new Error("metadata for username does not exist"));
      }

      return resp.data;
    }, function (err) {
      if (/does not exist/.test(err.message)) {
        return Oauth3.PromiseA.reject(err);
      }

      throw err;
    });
  };

  TLogins.meta = function (conf, username, type) {
    // TODO support username as type

    // TODO update backend to /api/promoonlyonline/username/:username?
    return Oauth3.request({
      url: conf.config.apiBaseUri + '/api'
        + '/org.oauth3.provider'
        + '/logins/meta/' + type + '/' + username
    , method: 'GET'
    }).then(function (resp) {
      // TODO better check
      if (!resp.data.salt) {
        return Oauth3.PromiseA.reject(new Error("data for username does not exist"));
      }

      return resp.data;
    }, function (err) {
      if (/does not exist/.test(err.message)) {
        return Oauth3.PromiseA.reject(err);
      }

      throw err;
    });
  };

  TLogins.hardTestUsername = function (conf, username) {
    // TODO support username as type
    var type = null;

    // TODO update backend to /api/promoonlyonline/username/:username?
    return Oauth3.request({
      url: conf.config.apiBaseUri + '/api'
        + '/org.oauth3.provider'
        + '/logins/check/' + type + '/' + username
    , method: 'GET'
    }).then(function (result) {
      if (!result.data.exists) {
        return Oauth3.PromiseA.reject(new Error("username does not exist"));
      }
    }, function (err) {
      if (/does not exist/.test(err.message)) {
        return Oauth3.PromiseA.reject(err);
      }

      throw err;
    });
  };

  TAccounts.getId = function (o, p) {
    // object
    if (!o) {
      return null;
    }
    // prefix
    if (!p) {
      return o.appScopedId || o.app_scoped_id || o.id || null;
    } else {
      return o[p + 'AppScopedId'] || o[p + '_app_scoped_id'] || o[p + 'Id'] || o[p + '_id'] || null;
    }
  };

  TAccounts.realCreateAccount = function (conf, login) {
    return Oauth3.request({
      url: conf.config.apiBaseUri + '/api'
        + '/org.oauth3.provider'
        + '/accounts'
    , method: 'POST'
    , data: { account: {}
      , logins: [{
          // TODO make appScopedIds even for root app
          id: login.appScopedId || login.app_scoped_id || login.loginId || login.login_id || login.id
        , token: login.token || login.accessToken || login.accessToken
        }]
      }
    , headers: {
        Authorization: 'Bearer ' + login.token
      }
    }).then(function (resp) {
      return resp.data;
    }, function (err) {
      return Oauth3.PromiseA.reject(err);
    });
  };

  // TODO move to LocalApiLogin ?
  TAccounts.attachLoginToAccount = function (conf, account, newLogin) {
    var url = conf.config.apiBaseUri + '/api'
      + '/org.oauth3.provider'
      + '/accounts/' + account.appScopedId + '/logins';
    var token = TherapySession.api.getToken(conf, account);

    return Oauth3.request({
      url: url
    , method: 'POST'
    , data: { logins: [{
        id: newLogin.appScopedId || newLogin.app_scoped_id || newLogin.loginId || newLogin.login_id || newLogin.id
      , token: newLogin.token || newLogin.accessToken || newLogin.access_token
      }] }
    , headers: { 'Authorization': 'Bearer ' + token }
    }).then(function (resp) {
      if (!resp.data) {
        return Oauth3.PromiseA.reject(new Error("no response when linking login to account"));
      }
      if (resp.data.error) {
        return Oauth3.PromiseA.reject(resp.data.error);
      }

      // return nothing
    }, function (err) {
      console.error('[Error] failed to attach login to account');
      console.warn(err.message);
      console.warn(err.stack);
      return Oauth3.PromiseA.reject(err);
    });
  };

  TAccounts.requireAccountHelper = function (conf) {
    var session = conf.session;
    var promise;
    var locallogins;
    var err;

    if (session.accounts.length) {
      return Oauth3.PromiseA.resolve(session);
    }

    if (!session.logins.length) {
      console.error("doesn't have any logins");
      return Oauth3.PromiseA.reject(new Error("[Developer Error] do not call requireAccount when you have not called requireLogin."));
    }

    locallogins = session.logins.filter(function (login) {
      return 'localaccount' === login.loginType;
    });

    if (!locallogins.length) {
      console.error("no local accounts");
      err = new Error("Login with your Local Account at least once before linking other accounts.");
      err.code = "E_NO_LOCAL_ACCOUNT";
      return Oauth3.PromiseA.reject(err);
    }

    // at this point we have a valid locallogin, but still no localaccount
    promise = Oauth3.PromiseA.resolve();

    locallogins.forEach(function (login) {
      promise = promise.then(function () {
        return TAccounts.realCreateAccount(conf, login).then(function (account) {
          login.accounts.push(account);
          return save(conf, { login: login, accounts: login.accounts });
        });
      });
    });

    return promise.then(function (session) {
      return session;
    });
  };

  TAccounts.requireAccount = function (conf) {
    return TAccounts.requireAccountHelper(conf).then(function () {
      return api.handleOrphanLogins(conf);
    });
  };

  // TODO move to LocalApiAccount ?
  TAccounts.cloneAccount = function (conf, account) {
    // retrieve the most fresh token of all associated logins
    var token = TherapySession.api.getToken(conf, account);
    var id = TAccounts.getId(account);
    // We don't want to modify the original object and end up
    // with potentially whole stakes in the local storage session key
    account = JSON.parse(JSON.stringify(account));

    account.token = token;
    account.accountId = account.accountId || account.appScopedId || id;
    account.appScopedId = account.appScopedId || id;

    return account;
  };

  // TODO check for account and account create if not exists in requireSession
  // TODO move to LocalApiAccount ?
  TAccounts.selectAccount = function (conf, accountId) {
    var session = conf.session;
    // needs to return the account with a valid login
    var account;
    if (!accountId) {
      accountId = session.accountId;
    }

    if (!session.accounts.some(function (a) {
      if (!accountId || accountId === TAccounts.getId(a)) {
        account = a;
        return true;
      }
    })) {
      account = session.accounts[0];
    }

    if (!account) {
      console.error("Developer Error: require session before selecting an account");
      console.error(session);
      throw new Error("Developer Error: require session before selecting an account");
    }

    account = TAccounts.cloneAccount(conf, account);
    session.accountId = account.accountId;
    session.id = account.accountId;
    session.appScopedId = account.accountId;
    session.token = account.token;

    // XXX really?
    conf.account = account;
    return account;
  };

  InternalApi = {
    accounts: accounts
  , login: login
  , getToken: getToken
  };

  api = {
    save: save
  , restore: restore
  , checkSession: restore
  , destroy: destroy
  , require: requireSession
  , accounts: accounts
  , requireSession: requireSession
  , getToken: getToken
  , addAccountsToSession: addAccountsToSession
  , pruneAccountsFromSession: pruneAccountsFromSession
  , refreshCurrentAccount: refreshCurrentAccount
  , updateSession: updateSession
  , sanityCheckAccounts: sanityCheckAccounts
  , handleOrphanLogins: handleOrphanLogins
  , validateUsername: TLogins.softTestUsername
  , checkUsername: TLogins.hardTestUsername
  , getMeta: TLogins.meta
  , createLogin: TLogins.create
  , login: login
      // this is intended for the resourceOwnerPassword strategy
  , backgroundLogin: backgroundLogin
  , logout: logout
  , onLogin: onLogin
  , onLogout: onLogout
  , requireAccount: TAccounts.requireAccount
  , selectAccount: TAccounts.selectAccount // TODO nix this 'un
  , account: TAccounts.selectAccount
  , testLoginAccounts: testLoginAccounts
  , cloneAccount: TAccounts.cloneAccount
  //, getId: TAccounts.getId
  };

  TherapySession = {
    create: create
  , api: api
  , getId: TAccounts.getId
  };

  // XXX
  // These are underscore prefixed because they aren't official API yet
  // I need more time to figure out the proper separation
  TherapySession._logins = TLogins;
  TherapySession._accounts = TAccounts;

  exports.TherapySession = TherapySession.TherapySession = TherapySession;

  if ('undefined' !== typeof module) {
    module.exports = TherapySession;
  }
}('undefined' !== typeof exports ? exports : window));