var _              = require('lodash');
var httpProxy      = require('http-proxy');
var configFactory  = require('./config-factory');
var handlers       = require('./handlers');
var contextMatcher = require('./context-matcher');
var PathRewriter   = require('./path-rewriter');
var Router         = require('./router');
var logger         = require('./logger').getInstance();
var getArrow       = require('./logger').getArrow;

module.exports = HttpProxyMiddleware;

function HttpProxyMiddleware(context, opts) {
    // https://github.com/chimurai/http-proxy-middleware/issues/57
    var wsUpgradeDebounced  = _.debounce(handleUpgrade);
    var wsInitialized       = false;
    var config              = configFactory.createConfig(context, opts);
    var proxyOptions        = config.options;

    // create proxy
    var proxy = httpProxy.createProxyServer({});
    logger.info('[HPM] Proxy created:', config.context, ' -> ', proxyOptions.target);

    var pathRewriter = PathRewriter.create(proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided

    // attach handler to http-proxy events
    handlers.init(proxy, proxyOptions);

    // log errors for debug purpose
    proxy.on('error', logError);

    // https://github.com/chimurai/http-proxy-middleware/issues/19
    // expose function to upgrade externally
    middleware.upgrade = wsUpgradeDebounced;

    return middleware;

    function middleware(req, res, next) {
        if (shouldProxy(config.context, req)) {
            var activeProxyOptions = prepareProxyRequest(req);
            proxy.web(req, res, activeProxyOptions);
        } else {
            next();
        }

        if (proxyOptions.ws === true) {
            // use initial request to access the server object to subscribe to http upgrade event
            catchUpgradeRequest(req.connection.server);
        }
    }

    function catchUpgradeRequest(server) {
        // subscribe once; don't subscribe on every request...
        // https://github.com/chimurai/http-proxy-middleware/issues/113
        if (!wsInitialized) {
            server.on('upgrade', wsUpgradeDebounced);
            wsInitialized = true;
        }
    }

    function handleUpgrade(req, socket, head) {
        // set to initialized when used externally
        wsInitialized = true;

        if (shouldProxy(config.context, req)) {
            var activeProxyOptions = prepareProxyRequest(req);
            proxy.ws(req, socket, head, activeProxyOptions);
            logger.info('[HPM] Upgrading to WebSocket');
        }
    }

    /**
     * Determine whether request should be proxied.
     *
     * @private
     * @return {Boolean}
     */
    function shouldProxy(context, req) {
        var path = (req.originalUrl || req.url);
        return contextMatcher.match(context, path, req);
    }

    /**
     * Apply option.router and option.pathRewrite
     * Order matters:
          Router uses original path for routing;
          NOT the modified path, after it has been rewritten by pathRewrite
     */
    function prepareProxyRequest(req) {
        // https://github.com/chimurai/http-proxy-middleware/issues/17
        // https://github.com/chimurai/http-proxy-middleware/issues/94
        req.url = (req.originalUrl || req.url);

        // store uri before it gets rewritten for logging
        var originalPath = req.url;
        var newProxyOptions = _.assign({}, proxyOptions);

        // Apply in order:
        // 1. option.router
        // 2. option.pathRewrite
        __applyRouter(req, newProxyOptions);
        __applyPathRewrite(req, pathRewriter);

        // debug logging for both http(s) and websockets
        if (proxyOptions.logLevel === 'debug') {
            var arrow = getArrow(originalPath, req.url, proxyOptions.target, newProxyOptions.target);
            logger.debug('[HPM] %s %s %s %s', req.method, originalPath, arrow, newProxyOptions.target);
        }

        return newProxyOptions;
    }

    // Modify option.target when router present.
    function __applyRouter(req, options) {
        var newTarget;

        if (options.router) {
            newTarget = Router.getTarget(req, options);

            if (newTarget) {
                logger.debug('[HPM] Router new target: %s -> "%s"', options.target, newTarget);
                options.target = newTarget;
            }
        }
    }

    // rewrite path
    function __applyPathRewrite(req, pathRewriter) {
        if (pathRewriter) {
            var path = pathRewriter(req.url, req);

            if (typeof path === 'string') {
                req.url =  path;
            } else {
                logger.info('[HPM] pathRewrite: No rewritten path found. (%s)', req.url);
            }
        }
    }

    function logError(err, req, res) {
        var hostname = (req.headers && req.headers.host) || (req.hostname || req.host);     // (websocket) || (node0.10 || node 4/5)
        var target = proxyOptions.target.host || proxyOptions.target;
        var errReference = 'https://nodejs.org/api/errors.html#errors_common_system_errors'; // link to Node Common Systems Errors page

        logger.error('[HPM] Error occurred while trying to proxy request %s from %s to %s (%s) (%s)', req.url, hostname, target, err.code, errReference);
    }
};
