'use strict';

module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi, errorIfAssets) {
  var PromiseA = require('bluebird');
  var path = require('path');
  var fs = PromiseA.promisifyAll(require('fs'));
  // NOTE: each process has its own cache
  var localCache = { le: {}, statics: {} };
  var express = require('express');
  var setupDomain = xconfx.setupDomain = ('cloud.' + xconfx.primaryDomain);
  var apiApp;
  var setupApp;
  var CORS;
  var cors;

  function redirectSetup(reason, req, res) {
    console.log('xconfx', xconfx);
    var url = 'https://cloud.' + xconfx.primaryDomain;

    if (443 !== xconfx.externalPort) {
      url += ':' + xconfx.externalPort;
    }

    url += '#referrer=' + reason;

    res.statusCode = 302;
    res.setHeader('Location', url);
    res.end("The static pages for '" + reason + "' are not listed in '" + path.join(xconfx.sitespath, reason) + "'");
  }

  function disallowSymLinks(req, res) {
    res.end(
      "Symbolic Links are not supported on all platforms and are therefore disallowed."
    + " Instead, simply create a file of the same name as the link with a single line of text"
    + " which should be the relative or absolute path to the target directory."
    );
  }

  function disallowNonFiles(req, res) {
    res.end(
      "Pipes, Blocks, Sockets, FIFOs, and other such nonsense are not permitted."
    + " Instead please create a directory from which to read or create a file "
    + " with a single line of text which should be the target directory to read from."
    );
  }

  function securityError(req, res) {
    res.end("Security Error: Link points outside of packages/pages");
  }

  function notConfigured(req, res, next) {
    if (setupDomain !== req.hostname) {
      console.log('[notConfigured] req.hostname', req.hostname);
      if ('/' === req.url[req.url.length - 1] || /\.html\b/.test(req.url)) {
        redirectSetup(req.hostname, req, res);
        return;
      }
    }

    if (!setupApp) {
      //setupApp = express.static(path.join(xconfx.staticpath, 'walnut@daplie.com'));
      setupApp = express.static(path.join(__dirname, 'walnut@daplie.com', 'setup'));
    }
    setupApp(req, res, function () {
      if ('/' === req.url) {
        res.end('Sanity Fail: Configurator not found');
        return;
      }
      next();
    });
  }

  function loadSiteHandler(name) {
    return function handler(req, res, next) {
      // path.join('packages/pages', 'com.daplie.hello') // package name (used as file-link)
      // path.join('packages/pages', 'domain.tld#hello') // dynamic exact url match
      var sitepath = path.join(xconfx.sitespath, name);

      console.log('sitepath', sitepath);
      return fs.lstatAsync(sitepath).then(function (stat) {
        if (stat.isSymbolicLink()) {
          return disallowSymLinks;
        }

        if (stat.isDirectory()) {
          return express.static(sitepath);
        }

        if (!stat.isFile()) {
          return disallowNonFiles;
        }

        // path.join('packages/pages', 'domain.tld#hello') // a file (not folder) which contains a list of roots
        // may look like this:
        //
        //   com.daplie.hello
        //   tld.domain.app
        //
        // this is basically a 'recursive mount' to signify that 'com.daplie.hello' should be tried first
        // and if no file matches that 'tld.domain.app' may be tried next, and so on
        //
        // this may well become a .htaccess type of situation allowing for redirects and such
        return fs.readFileAsync(sitepath, 'utf8').then(function (text) {
          // TODO allow cascading multiple lines
          text = text.trim().split(/\n/)[0];

          // TODO rerun the above, disallowing link-style (or count or memoize to prevent infinite loop)
          // TODO make safe
          var packagepath = path.resolve(xconfx.staticpath, text);
          if (0 !== packagepath.indexOf(xconfx.staticpath)) {
            return securityError;
          }

          // instead of actually creating new instances of express.static
          // this same effect could be managed by internally re-writing the url (and restoring it)
          return express.static(packagepath);
        });
      }, function (/*err*/) {
        return notConfigured;
      }).then(function (handler) {

        // keep object reference intact
        localCache.statics[name].handler = handler;
        handler(req, res, next);
      });
    };
  }

  function staticHelper(appId, opts) {
    console.log('[staticHelper]', appId);
    // TODO inter-process cache expirey
    // TODO add to xconfx.staticpath
    xconfx.staticpath = path.join(__dirname, '..', '..', 'packages', 'pages');
    xconfx.sitespath = path.join(__dirname, '..', '..', 'packages', 'sites');

    // Reads in each of the sites directives as 'nodes'
    return fs.readdirAsync(xconfx.sitespath).then(function (nodes) {
      if (opts && opts.clear) {
        localCache.statics = {};
      }

      // Order from longest (index length - 1) to shortest (index 0)
      function shortToLong(a, b) {
        return b.length - a.length;
      }
      nodes.sort(shortToLong);

      nodes = nodes.filter(function (pkgName) {
        console.log('[all apps]', pkgName);
        // load the apps that match this id's domain and could match the path
        // domain.daplie.me matches domain.daplie.me
        // daplie.me#path#to#thing matches daplie.me
        // daplie.me does NOT match daplie.me#path#to#thing
        var reqParts = appId.split('#');
        var pkgParts = pkgName.split('#');
        var reqDomain = reqParts.shift();
        var pkgDomain = pkgParts.shift();
        var reqPath = reqParts.join('#');
        var pkgPath = pkgParts.join('#');
        if (reqPath.length) {
          reqPath += '#';
        }
        if (pkgPath.length) {
          pkgPath += '#';
        }
        if (!(reqDomain === pkgDomain && 0 === reqPath.indexOf(pkgPath))) {
          return false;
        }
        if (!localCache.statics[pkgName]) {
          console.log('[load this app]', pkgName);
          localCache.statics[pkgName] = { handler: loadSiteHandler(pkgName), createdAt: Date.now() };
        }
        return true;
      });

      // Secure Matching
      // apple.com#blah#  apple.com#blah#
      // apple.com.us#    apple.com#foo#
      // apple.com#       apple.com#foo#
      console.log('[lib/main.js] nodes', nodes);
      nodes.some(function (pkgName) {
        console.log('pkgName, appId', pkgName, appId);
        if (0 === (appId + '#').indexOf(pkgName + '#')) {
          if (appId !== pkgName) {
            localCache.statics[appId] = localCache.statics[pkgName];
          }
          return true;
        }
      });

      if (!localCache.statics[appId]) {
        localCache.statics[appId] = { handler: notConfigured, createdAt: Date.now() };
      }

      localCache.staticsKeys = Object.keys(localCache.statics).sort(shortToLong);
      return localCache.statics[appId];
    });
  }

  function serveStaticHelper(appId, opts, req, res, next) {
    var appIdParts = appId.split('#');
    var appIdPart;

    // TODO for <domain.tld>/<path>/apps/<package> the Uri should be <domain.tld>/<path>
    res.setHeader('X-Walnut-Uri', appId.replace(/#/g, '/'));

    // TODO configuration for allowing www
    if (/^www\./.test(req.hostname)) {
      // NOTE: acme responder and appcache unbricker must come before scrubTheDub
      if (/\.(appcache|manifest)\b/.test(req.url)) {
        require('./unbrick-appcache').unbrick(req, res);
        return;
      }
      require('./no-www').scrubTheDub(req, res);
      return;
    }

    /*
    if (!redirectives && config.redirects) {
      redirectives = require('./hostname-redirects').compile(config.redirects);
    }
    */

    /*
    // TODO assets.example.com/sub/assets/com.example.xyz/
    if (/^assets\./.test(req.hostname) && /\/assets(\/|$)/.test(req.url)) {
      ...
    }
    */

    // There may be some app folders named 'apple.com', 'apple.com#foo', and 'apple.com#foo#bar'
    // Here we're sorting an appId broken into parts like [ 'apple.com', 'foo', 'bar' ]
    // and wer're checking to see if this is perhaps '/' of 'apple.com/foo/bar' or '/foo/bar' of 'apple.com', etc
    while (appIdParts.length) {
      // TODO needs IPC to expire cache when an API says the app mounts have been updated
      appIdPart = appIdParts.join('#');
      if (localCache.statics[appIdPart]) {
        break;
      }
      // TODO test via staticsKeys

      appIdParts.pop();
    }

    if (!appIdPart || !localCache.statics[appIdPart]) {
      console.log('[serveStaticHelper] appId', appId);
      return staticHelper(appId).then(function (webapp) {
        //localCache.statics[appId].handler(req, res, next);
        webapp.handler(req, res, next);
      });
    }

    console.log('[serveStaticHelper] appIdPart', appIdPart);
    if (opts && opts.rewrite && -1 !== req.url.indexOf(appIdPart.replace(/#/g, '/').replace(/\/$/, ''))) {
      req.url = req.url.slice(req.url.indexOf(appIdPart.replace(/#/g, '/').replace(/\/$/, '')) + appIdPart.replace(/(\/|#)$/, '').length);
      if (0 !== req.url.indexOf('/')) {
        req.url = '/' + req.url;
      }
    }
    localCache.statics[appIdPart].handler(req, res, next);
    if (Date.now() - localCache.statics[appIdPart].createdAt > (5 * 60 * 1000)) {
      staticHelper(appId, { clear: true });
    }
  }

  function serveStatic(req, res, next) {
    // We convert the URL that was sent in the browser bar from
    // 'https://domain.tld/foo/bar' to 'domain.tld#foo#bar'
    var appId = req.hostname + req.url.replace(/\/+/g, '#').replace(/#$/, '');
    serveStaticHelper(appId, null, req, res, next);
  }

  function serveApps(req, res, next) {
    var appId = req.url.slice(1).replace(/\/+/g, '#').replace(/#$/, '');

    if (/^apps\./.test(req.hostname)) {
      appId = appId.replace(/^apps#/, '');
    } else if (/\bapps#/.test(appId)) {
      appId = appId.replace(/.*\bapps#/, '');
    } else {
      next();
      return;
    }

    console.log('[serveApps] appId', appId);
    serveStaticHelper(appId, { rewrite: true }, req, res, next);
  }

  // TODO set header 'X-ExperienceId: domain.tld/sub/path'
  // This would let an app know whether its app is 'domain.tld' with a path of '/sub/path'
  // or if its app is 'domain.tld/sub' with a path of '/path'

  // TODO handle assets.example.com/sub/assets/com.example.xyz/

  app.use('/api', require('connect-send-error').error());
  app.use('/assets', require('connect-send-error').error());
  app.use('/', function (req, res, next) {
    // If this doesn't look like an API or assets we can move along

    /*
    console.log('.');
    console.log('[main.js] req.url, req.hostname');
    console.log(req.url);
    console.log(req.hostname);
    console.log('.');
    */

    if (!/\/(api|assets)(\/|$)/.test(req.url)) {
      //console.log('[main.js] api|assets');
      next();
      return;
    }

    // keep https://assets.example.com/assets but skip https://example.com/assets
    if (/\/assets(\/|$)/.test(req.url) && !/(^|\.)(api|assets)(\.)/.test(req.hostname) && !/^[0-9\.]+$/.test(req.hostname)) {
      //console.log('[main.js] skip');
      next();
      return;
    }

    // supports api.example.com/sub/app/api/com.example.xyz/
    if (!apiApp) {
      apiApp = require('./apis').create(xconfx, apiFactories, apiDeps);
    }

    if (/^OPTIONS$/i.test(req.method)) {
      if (!cors) {
        CORS = require('connect-cors');
        cors = CORS({ credentials: true, headers: [
          'X-Requested-With'
        , 'X-HTTP-Method-Override'
        , 'Content-Type'
        , 'Accept'
        , 'Authorization'
        ], methods: [ "GET", "POST", "PATCH", "PUT", "DELETE" ] });
      }
      cors(req, res, apiApp);
      return;
    }

    apiApp(req, res, next);
    return;
  });
  app.use('/', errorIfApi);
  app.use('/', errorIfAssets);
  app.use('/', serveStatic);
  app.use('/', serveApps);

  return PromiseA.resolve();
};
