'use strict';

module.exports.create = function (xconfx, apiFactories, apiDeps) {
  var PromiseA = apiDeps.Promise;
  var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
  //var express = require('express');
  var express = require('express-lazy');
  var fs = PromiseA.promisifyAll(require('fs'));
  var path = require('path');
  var localCache = { rests: {}, pkgs: {} };
  var promisableRequest = require('./common').promisableRequest;
  var rejectableRequest = require('./common').rejectableRequest;
  var crypto = require('crypto');

  // TODO xconfx.apispath
  xconfx.restPath = path.join(__dirname, '..', '..', 'packages', 'rest');
  xconfx.apiPath = path.join(__dirname, '..', '..', 'packages', 'api');
  xconfx.appApiGrantsPath = path.join(__dirname, '..', '..', 'packages', 'client-api-grants');
  xconfx.appConfigPath = path.join(__dirname, '..', '..', 'var');

  function notConfigured(req, res) {
    var msg = "api package '" + req.pkgId + "' not configured for client uri '" + req.experienceId + "'"
      + ". To configure it place a new line '" + req.pkgId + "' in the file '/srv/walnut/packages/client-api-grants/" + req.experienceId + "'"
      ;

    res.send({ error: { message: msg } });
  }

  /*
  function isThisPkgInstalled(myConf, pkgId) {
  }
  */

  function isThisClientAllowedToUseThisPkg(myConf, clientUrih, pkgId) {
    var appApiGrantsPath = path.join(myConf.appApiGrantsPath, clientUrih);

    return fs.readFileAsync(appApiGrantsPath, 'utf8').then(function (text) {
      return text.trim().split(/\n/);
    }, function (err) {
      if ('ENOENT' !== err.code) {
        console.error(err);
      }
      return [];
    }).then(function (apis) {
      if (apis.some(function (api) {
        if (api === pkgId) {
          return true;
        }
      })) {
        return true;
      }
        if (clientUrih === ('api.' + xconfx.setupDomain) && 'org.oauth3.consumer' === pkgId) {
          // fallthrough
          return true;
        } else {
          return null;
        }
    });
  }

  function getSitePackageConfig(clientUrih, pkgId) {
    var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
    return mkdirpAsync(siteConfigPath).then(function () {
      return fs.readFileAsync(path.join(siteConfigPath, pkgId + '.json'), 'utf8').then(function (text) {
        return JSON.parse(text);
      }).then(function (data) { return data; }, function (/*err*/) { return {}; });
    });
  }

  function getSiteConfig(clientUrih) {
    // TODO test if the requesting package has permission to the root-level site config
    var siteConfigPath = path.join(xconfx.appConfigPath, clientUrih);
    return mkdirpAsync(siteConfigPath).then(function () {
      return fs.readFileAsync(path.join(siteConfigPath, 'config.json'), 'utf8').then(function (text) {
        return JSON.parse(text);
      }).then(function (data) { return data; }, function (/*err*/) { return {}; });
    });
  }

  var modelsCache = {};
  function getSiteStore(clientUrih, pkgId, dir) {
    if (!modelsCache[clientUrih]) {
      modelsCache[clientUrih] = apiFactories.systemSqlFactory.create({
        init: true
      , dbname: clientUrih // '#' is a valid file name character
      });
    }

    // DB scopes:
    // system (global)
    // experience (per domain)
    // api (per api)
    // account (per user account)
    // client (per 3rd party client)

    // scope Experience to db
    // scope Api by table
    // scope Account and Client by column
    return modelsCache[clientUrih].then(function (db) {
      var wrap = require('masterquest-sqlite3');

      return wrap.wrap(db, dir).then(function (models) {
        //modelsCache[clientUrih] = PromiseA.resolve(models);
        return models;
      });
    });
  }

  function accountRequiredById(req, res, next) {
    var promise = req.oauth3.verifyAsync().then(function (/*result*/) {
      var tok = req.oauth3.token;
      var accountId = req.params.accountId || '__NO_ID_GIVEN__';
      var ppid;

      if (tok.sub && tok.sub.split(/,/g).filter(function (ppid) {
        return ppid === accountId;
      }).length) {
        ppid = accountId;
      }

      if (tok.axs && tok.axs.filter(function (acc) {
        return acc.id === accountId || acc.appScopedId === accountId;
      }).length) {
        ppid = accountId;
      }

      if (tok.acx && accountId === (tok.acx.appScopedId || tok.acx.id || tok.acx)) {
        ppid = accountId;
      }

      if (!ppid) {
        return PromiseA.reject(new Error("missing accountId '" + accountId + "' in access token"));
      }

      return req.oauth3.rescope(ppid).then(function (accountIdx) {
        req.oauth3.accountIdx = accountIdx;
        req.oauth3.ppid = ppid;
        req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');
        //console.log('[com.daplie.walnut] accountIdx:', accountIdx);
        //console.log('[com.daplie.walnut] ppid:', ppid);

        next();
      });
    });

    rejectableRequest(req, res, promise, "[com.daplie.walnut] attach account by id");
  }

  function accountRequired(req, res, next) {
    // if this already has auth, great
    if (req.oauth3.ppid) {
      next();
      return;
    }

    // being public does not disallow authentication
    if (req.isPublic && !req.oauth3.encodedToken) {
      next();
      return;
    }

    if (!req.oauth3.encodedToken) {
      rejectableRequest(
        req
      , res
      , PromiseA.reject(new Error("this secure resource requires an access token"))
      , "[com.daplie.walnut] required account (not /public)"
      );
      return;
    }

    // verify the auth if it's here
    var promise = req.oauth3.verifyAsync().then(function (/*result*/) {
      var tok = req.oauth3.token;
      var ppid;
      var err;

      if (tok.sub) {
        if (tok.sub.split(/,/g).length > 1) {
          err = new Error("more than one 'sub' specified in token");
          return PromiseA.reject(err);
        }
        ppid = tok.sub;
      }
      else if (tok.axs && tok.axs.length) {
        if (tok.axs.length > 1) {
          err = new Error("more than one 'axs' specified in token (also, update to using 'sub' instead)");
          return PromiseA.reject(err);
        }
        ppid = tok.axs[0].appScopedId || tok.axs[0].id;
      }
      else if (tok.acx) {
        ppid = tok.acx.appScopedId || tok.acx.id || tok.acx;
      }

      if (!ppid) {
        return PromiseA.reject(new Error("could not determine accountId from access token"));
      }

      return req.oauth3.rescope(ppid).then(function (accountIdx) {
        req.oauth3.accountIdx = accountIdx;
        req.oauth3.ppid = ppid;
        req.oauth3.accountHash = crypto.createHash('sha1').update(accountIdx).digest('hex');

        next();
      });
    });

    rejectableRequest(req, res, promise, "[com.daplie.walnut] required account (not /public)");
  }

  function loadRestHelper(myConf, clientUrih, pkgId) {
    var pkgPath = path.join(myConf.restPath, pkgId);
    var pkgLinks = [];
    pkgLinks.push(pkgId);

    // TODO allow recursion, but catch cycles
    return fs.lstatAsync(pkgPath).then(function (stat) {
      if (!stat.isFile()) {
        return;
      }

      return fs.readFileAsync(pkgPath, 'utf8').then(function (text) {
        pkgId = text.trim();
        pkgPath = path.join(myConf.restPath, pkgId);
      });
    }, function () {
      // ignore error
      return;
    }).then(function () {
      // TODO should not require package.json. Should work with files alone.
      return fs.readFileAsync(path.join(pkgPath, 'package.json'), 'utf8').then(function (text) {
        var pkg = JSON.parse(text);
        var pkgDeps = {};
        var myApp;

        if (pkg.walnut) {
          pkgPath = path.join(pkgPath, pkg.walnut);
        }

        Object.keys(apiDeps).forEach(function (key) {
          pkgDeps[key] = apiDeps[key];
        });
        Object.keys(apiFactories).forEach(function (key) {
          pkgDeps[key] = apiFactories[key];
        });

        // TODO pull db stuff from package.json somehow and pass allowed data models as deps
        //
        // how can we tell which of these would be correct?
        // deps.memstore = apiFactories.memstoreFactory.create(pkgId);
        // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
        // deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + pkgId);

        // let's go with this one for now and the api can choose to scope or not to scope
        pkgDeps.memstore = apiFactories.memstoreFactory.create(pkgId);

        console.log('DEBUG pkgPath', pkgPath);
        myApp = express();
        myApp.handlePromise = promisableRequest;
        myApp.handleRejection = rejectableRequest;
        myApp.grantsRequired = function (grants) {
          if (!Array.isArray(grants)) {
            throw new Error("Usage: app.grantsRequired([ 'name|altname|altname2', 'othergrant' ])");
          }

          if (!grants.length) {
            return function (req, res, next) {
              next();
            };
          }

          return function (req, res, next) {
            var tokenScopes;

            if (!(req.oauth3 || req.oauth3.token)) {
              // TODO some error generator for standard messages
              res.send({ error: { message: "You must be logged in", code: "E_NO_AUTHN" } });
              return;
            }
            if ('string' !== typeof req.oauth3.token.scp) {
              res.send({ error: { message: "Token must contain a grants string in 'scp'", code: "E_NO_GRANTS" } });
              return;
            }

            tokenScopes = req.oauth3.token.scp.split(/[,\s]+/mg);
            if (-1 !== tokenScopes.indexOf('*')) {
              // has full account access
              next();
              return;
            }

            // every grant in the array must be present
            if (!grants.every(function (grant) {
              var scopes = grant.split(/\|/g);
              return scopes.some(function (scp) {
                return tokenScopes.some(function (s) {
                  return scp === s;
                });
              });
            })) {
              res.send({ error: { message: "Token does not contain valid grants: '" + grants + "'", code: "E_NO_GRANTS" } });
              return;
            }

            next();
          };
        };

        var _getOauth3Controllers = pkgDeps.getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(
          { sqlite3Sock: xconfx.sqlite3Sock, ipcKey: xconfx.ipcKey }
        ).getControllers;
        //require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
        require('oauthcommon').inject(_getOauth3Controllers, myApp/*, pkgConf, pkgDeps*/);

        // TODO delete these caches when config changes
        var _stripe;
        var _stripe_test;
        var _mandrill;
        var _mailchimp;
        var _twilio;
        myApp.use('/', function preHandler(req, res, next) {
          return getSiteConfig(clientUrih).then(function (siteConfig) {
            Object.defineProperty(req, 'getSiteMailer', {
              enumerable: true
            , configurable: false
            , writable: false
            , value: function getSiteMailerProp() {
                var nodemailer = require('nodemailer');
                var transport = require('nodemailer-mailgun-transport');
                //var mailconf = require('../../../com.daplie.mailer/config.mailgun');
                var mailconf = siteConfig['mailgun.org'];
                var mailer = PromiseA.promisifyAll(nodemailer.createTransport(transport(mailconf)));

                return mailer;
              }
            });

            Object.defineProperty(req, 'getSiteConfig', {
              enumerable: true
            , configurable: false
            , writable: false
            , value: function getSiteConfigProp(section) {
                return PromiseA.resolve((siteConfig || {})[section]);
              }
            });

            Object.defineProperty(req, 'getSitePackageConfig', {
              enumerable: true
            , configurable: false
            , writable: false
            , value: function getSitePackageConfigProp() {
                return getSitePackageConfig(clientUrih, pkgId);
              }
            });

            Object.defineProperty(req, 'getSiteStore', {
              enumerable: true
            , configurable: false
            , writable: false
            , value: function getSiteStoreProp() {
                var restPath = path.join(myConf.restPath, pkgId);
                var apiPath = path.join(myConf.apiPath, pkgId);
                var dir;

                // TODO usage package.json as a falback if the standard location is not used
                try {
                  dir = require(path.join(apiPath, 'models.js'));
                } catch(e) {
                  dir = require(path.join(restPath, 'models.js'));
                }

                return getSiteStore(clientUrih, pkgId, dir);
              }
            });

            /*
            Object.defineProperty(req, 'getSitePayments', {
              enumerable: true
            , configurable: false
            , writable: false
            , value: function getSitePaymentsProp() {
              }
            });
            */
            // TODO allow third-party clients stripe ids destination
            // https://stripe.com/docs/connect/payments-fees
            Object.defineProperty(req, 'Stripe', {
              enumerable: true
            , configurable: false
            , get: function () {
                _stripe = _stripe || require('stripe')(siteConfig['stripe.com'].live.secret);
                return _stripe;
              }
            });

            Object.defineProperty(req, 'StripeTest', {
              enumerable: true
            , configurable: false
            , get: function () {
                _stripe_test = _stripe_test || require('stripe')(siteConfig['stripe.com'].test.secret);
                return _stripe_test;
              }
            });

            Object.defineProperty(req, 'Mandrill', {
              enumerable: true
            , configurable: false
            , get: function () {
                if (!_mandrill) {
                  var Mandrill = require('mandrill-api/mandrill');
                  _mandrill = new Mandrill.Mandrill(siteConfig['mandrill.com'].apiKey);
                  _mandrill.messages.sendTemplateAsync = function (opts) {
                    return new PromiseA(function (resolve, reject) {
                      _mandrill.messages.sendTemplate(opts, resolve, reject);
                    });
                  };
                }
                return _mandrill;
              }
            });

            Object.defineProperty(req, 'Mailchimp', {
              enumerable: true
            , configurable: false
            , get: function () {
                var Mailchimp = require('mailchimp-api-v3');
                _mailchimp = _mailchimp || new Mailchimp(siteConfig['mailchimp.com'].apiKey);
                return _mailchimp;
              }
            });

            var Twilio = require('twilio');
            function twilioTel(/*opts*/) {
              if (_twilio) {
                return _twilio;
              }

              _twilio = new Twilio.RestClient(
                siteConfig['twilio.com'].live.id
              , siteConfig['twilio.com'].live.auth
              );
              return apiDeps.Promise.resolve(_twilio);
            }

            // TODO shared memory db
            var mailgunTokens = {};
            function validateMailgun(apiKey, timestamp, token, signature) {
              // https://gist.github.com/coolaj86/81a3b61353d2f0a2552c
              // (realized later)
              // HAHA HAHA HAHAHAHAHA this is my own gist... so much more polite attribution
              var scmp = require('scmp')
                , crypto = require('crypto')
                , mailgunExpirey = 15 * 60 * 1000
                , mailgunHashType = 'sha256'
                , mailgunSignatureEncoding = 'hex'
                ;
              var actual
                , adjustedTimestamp = parseInt(timestamp, 10) * 1000
                , fresh = (Math.abs(Date.now() - adjustedTimestamp) < mailgunExpirey)
                ;

              if (!fresh) {
                console.error('[mailgun] Stale Timestamp: this may be an attack');
                console.error('[mailgun] However, this is most likely your fault\n');
                console.error('[mailgun] run `ntpdate ntp.ubuntu.com` and check your system clock\n');
                console.error('[mailgun] System Time: ' + new Date().toString());
                console.error('[mailgun] Mailgun Time: ' + new Date(adjustedTimestamp).toString(), timestamp);
                console.error('[mailgun] Delta: ' + (Date.now() - adjustedTimestamp));
                return false;
              }

              if (mailgunTokens[token]) {
                console.error('[mailgun] Replay Attack');
                return false;
              }

              mailgunTokens[token] = true;

              setTimeout(function () {
                delete mailgunTokens[token];
              }, mailgunExpirey + (5 * 1000));

              actual = crypto.createHmac(mailgunHashType, apiKey)
                .update(new Buffer(timestamp + token, 'utf8'))
                .digest(mailgunSignatureEncoding)
                ;
              return scmp(signature, actual);
            }

            function mailgunMail(/*opts*/) {
              return apiDeps.Promise.resolve(req.getSiteMailer());
            }

            // Twilio Parameters are often 26 long
            var bodyParserTwilio = require('body-parser').urlencoded({ limit: '4kb', parameterLimit: 100, extended: false });
            var bodyParserMailgun = require('body-parser').urlencoded({ limit: '100kb', parameterLimit: 500, extended: false });
            function bodyMultiParserMailgun (req, res, next) {
              var multiparty = require('multiparty');
              var form = new multiparty.Form();

              form.parse(req, function (err, fields/*, files*/) {
                if (err) {
                  console.error('Error');
                  console.error(err);
                  res.end("Couldn't parse form");
                  return;
                }

                var body;
                req.body = req.body || {};
                Object.keys(fields).forEach(function (key) {
                  // TODO what if there were two of something?
                  // (even though there won't be)
                  req.body[key] = fields[key][0];
                });
                body = req.body;

                next();
              });
            }

            var caps = {
              //
              // Capabilities for APIs
              //
              'mailer@daplie.com': mailgunMail    // whichever mailer
            , 'mailgun@daplie.com': mailgunMail   // specifically mailgun
            , 'tel@daplie.com': twilioTel         // whichever telephony service
            , 'twilio@daplie.com': twilioTel      // specifically twilio
            , 'com.daplie.tel.twilio': twilioTel  // deprecated alias

              //
              // Webhook Parsers
              //
            //, 'mailgun.urlencoded@daplie.com': function (req, res, next) { ... }
            , 'mailgun.parsers@daplie.com': function (req, res, next) {
                console.log('mailgun parser req.headers');
                console.log(req.headers);

                function verify() {
                  var body = req.body;
                  var mailconf = siteConfig['mailgun.org'];

                  if (!validateMailgun(mailconf.apiKey, body.timestamp, body.token, body.signature)) {
                    console.error('Request came, but not from Mailgun');
                    res.send({ error: { message: 'Invalid signature. Are you even Mailgun?' } });
                    return;
                  }

                  next();
                }

                if (/urlencoded/.test(req.headers['content-type'])) {
                  bodyParserMailgun(req, res, verify);
                }
                else if (/multipart/.test(req.headers['content-type'])) {
                  bodyMultiParserMailgun(req, res, verify);
                }
                else {
                  next();
                }
              }
            , 'twilio.urlencoded@daplie.com': function (req, res, next) {
                // TODO null for res and Promise instead of next?
                return bodyParserTwilio(req, res, function () {
                  var signature = req.headers['x-twilio-signature'];
                  var auth = siteConfig['twilio.com'].live.auth;
                  var fullUrl = 'https://' + req.headers.host + req._walnutOriginalUrl;
                  var validSig = Twilio.validateRequest(auth, signature, fullUrl, req.body);
                  /*
                  console.log('Twilio Signature Check');
                  console.log('auth', auth);
                  console.log('sig', signature);
                  console.log('fullUrl', fullUrl);
                  console.log(req.body);
                  console.log('valid', validSig);
                  */
                  if (!validSig) {
                    res.statusCode = 401;
                    res.setHeader('Content-Type', 'text/xml');
                    res.end('<Error>Invalid signature. Are you even Twilio?</Error>');
                    return;
                  }
                  // TODO session via db req.body.CallId req.body.smsId
                  next();
                });
              }
            };
            req.getSiteCapability = function (capname, opts, b, c) {
              if (caps[capname]) {
                return caps[capname](opts, b, c);
              }
              return apiDeps.Promise.reject(
                new Error("['" + req.clientApiUri + '/' + pkgId + "'] "
                  + "capability '" + capname + "' not implemented")
              );
            };

            req._walnutOriginalUrl = req.url;
            // "/path/api/com.example/hello".replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/') => '/hello'
            req.url = req.url.replace(/\/api\//, '').replace(/.*\/api\//, '').replace(/([^\/]*\/+)/, '/');
            next();
          });
        });
        myApp.use('/public', function preHandler(req, res, next) {
          // TODO authenticate or use guest user
          req.isPublic = true;
          next();
        });
        myApp.use('/accounts/:accountId', accountRequiredById);
        myApp.use('/acl', accountRequired);

        //
        // TODO handle /accounts/:accountId
        //
        return PromiseA.resolve(require(pkgPath).create({
          etcpath: xconfx.etcpath
        }/*pkgConf*/, pkgDeps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {

          myApp.use('/', function postHandler(req, res, next) {
            req.url = req._walnutOriginalUrl;
            next();
          });

          localCache.pkgs[pkgId] = { pkgId: pkgId, pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
          pkgLinks.forEach(function (pkgLink) {
            localCache.pkgs[pkgLink] = localCache.pkgs[pkgId];
          });
          return localCache.pkgs[pkgId];
        });
      });
    });
  }

  // Read packages/apis/sub.sld.tld (forward dns) to find list of apis as tld.sld.sub (reverse dns)
  // TODO packages/allowed_apis/sub.sld.tld (?)
  // TODO auto-register org.oauth3.consumer for primaryDomain (and all sites?)
  function loadRestHandler(myConf, clientUrih, pkgId) {
    return PromiseA.resolve().then(function () {
      if (!localCache.pkgs[pkgId]) {
        return loadRestHelper(myConf, clientUrih, pkgId);
      }

      return localCache.pkgs[pkgId];
      // TODO expire require cache
      /*
      if (Date.now() - localCache.pkgs[pkgId].createdAt < (5 * 60 * 1000)) {
        return;
      }
      */
    }, function (/*err*/) {
      // TODO what kind of errors might we want to handle?
      return null;
    }).then(function (restPkg) {
      return restPkg;
    });
  }

  var CORS = require('connect-cors');
  var cors = CORS({ credentials: true, headers: [
    'X-Requested-With'
  , 'X-HTTP-Method-Override'
  , 'Content-Type'
  , 'Accept'
  , 'Authorization'
  ], methods: [ "GET", "POST", "PATCH", "PUT", "DELETE" ] });
  var staleAfter = (5 * 60 * 1000);

  return function (req, res, next) {
    cors(req, res, function () {
      if (xconfx.debug) { console.log('[api.js] post cors'); }

      // Canonical client names
      // example.com should use api.example.com/api for all requests
      // sub.example.com/api should resolve to sub.example.com
      // example.com/subpath/api should resolve to example.com#subapp
      // sub.example.com/subpath/api should resolve to sub.example.com#subapp
      var clientUrih = req.hostname.replace(/^api\./, '') + req.url.replace(/\/api\/.*/, '/').replace(/\/+/g, '#').replace(/#$/, '');
      var clientApiUri = req.hostname + req.url.replace(/\/api\/.*/, '/').replace(/\/$/, '');
      // Canonical package names
      // '/api/com.daplie.hello/hello' should resolve to 'com.daplie.hello'
      // '/subapp/api/com.daplie.hello/hello' should also 'com.daplie.hello'
      // '/subapp/api/com.daplie.hello/' may exist... must be a small api
      var pkgId = req.url.replace(/.*\/api\//, '').replace(/^\//, '').replace(/\/.*/, '');
      var now = Date.now();
      var hasBeenHandled = false;

      // Existing (Deprecated)
      Object.defineProperty(req, 'apiUrlPrefix', {
        enumerable: true
      , configurable: false
      , writable: false
      , value: 'https://' + clientApiUri + '/' + pkgId
      });
      Object.defineProperty(req, 'experienceId', {
        enumerable: true
      , configurable: false
      , writable: false
      , value: clientUrih
      });
      Object.defineProperty(req, 'clientApiUri', {
        enumerable: true
      , configurable: false
      , writable: false
      , value: clientApiUri
      });
      Object.defineProperty(req, 'apiId', {
        enumerable: true
      , configurable: false
      , writable: false
      , value: pkgId
      });

      // New
      Object.defineProperty(req, 'clientUrih', {
        enumerable: true
      , configurable: false
      , writable: false
        // TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app
        // (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
        // NOTE: probably best to alias the name logically
      , value: clientUrih
      });
      Object.defineProperty(req, 'pkgId', {
        enumerable: true
      , configurable: false
      , writable: false
      , value: pkgId
      });

      // TODO cache permission (although the FS is already cached, NBD)
      var promise = isThisClientAllowedToUseThisPkg(xconfx, clientUrih, pkgId).then(function (yes) {
        if (!yes) {
          notConfigured(req, res);
          return null;
        }

        if (localCache.rests[pkgId]) {
          localCache.rests[pkgId].handler(req, res, next);
          hasBeenHandled = true;

          if (now - localCache.rests[pkgId].createdAt > staleAfter) {
            localCache.rests[pkgId] = null;
          }
        }

        if (!localCache.rests[pkgId]) {
          //return doesThisPkgExist

          return loadRestHandler(xconfx, clientUrih, pkgId).then(function (myHandler) {
            if (!myHandler) {
              notConfigured(req, res);
              return;
            }

            localCache.rests[pkgId] = { handler: myHandler.handler, createdAt: now };
            if (!hasBeenHandled) {
              myHandler.handler(req, res, next);
            }
          });
        }
      });
      rejectableRequest(req, res, promise, "[com.daplie.walnut] load api package");
    });
  };
};
