'use strict'

var shimmer = require('../shimmer.js')
var urltils = require('../util/urltils.js')
var logger = require('../logger.js').child({component: 'express'})
var record = require('../metrics/recorders/generic.js')
var middlewareRecorder = require('../metrics/recorders/express.js')
var NAMES = require('../metrics/names.js')

var VIEW = NAMES.VIEW
var ORIGINAL = '__NR_original'
var RESERVED = [ // http://es5.github.io/#x7.6.1.2
  // always (how would these even get here?)
  'class', 'enum', 'extends', 'super', 'const', 'export', 'import',
  // strict
  'implements', 'let', 'private', 'public', 'yield', 'interface',
  'package', 'protected', 'static'
]

/**
 * ES5 strict mode disallows some identifiers that are allowed in non-strict
 * code. Mangle function names that are on that list of keywords so they're
 * non-objectionable in strict mode (which is currently enabled everywhere
 * inside the agent, as well as at many customer sites).
 *
 * If you really need to crawl your Express apps middleware stack, change
 * your test to use name.indexOf('whatever') === 0 as the predicate instead
 * of name === 'whatever'. It's a little slower, but you shouldn't be doing
 * that anyway.
 *
 * @param {string} name The candidate function name
 *
 * @returns {string} A safe (potentially mangled) function name.
 */
function mangle(name) {
  var parts = name.split(' ')
  name = parts[parts.length - 1]

  if (RESERVED.indexOf(name) !== -1) return name + '_'

  return name
}

// Ensures that partialName begins with the express prefix
// http instrumentation will set partialName before passing the request off to express
function ensurePartialName(trans) {
  if (!trans.partialName ||
      trans.partialName.lastIndexOf(NAMES.EXPRESS.PREFIX, 0) !== 0) {
    trans.partialName = NAMES.EXPRESS.PREFIX + trans.verb + NAMES.ACTION_DELIMITER + '/'
  }
}

function generateMiddlewarePath(routerStack) {
  var res = ''
  for (var i = 0; i < routerStack.length; ++i) {
    var resLastChar = res[res.length - 1]
    var path = routerStack[i].path
    if (resLastChar !== '/' && path[0] !== '/') {
      res += '/'
    }
    if (resLastChar === '/' && path[0] === '/') {
      res += path.substr(1)
    } else {
      res += path
    }
  }
  return res || '/'
}

function nameFromRoute(segment, route, params, append) {
  if (!segment) return logger.error("No New Relic context to set Express route name on.")
  if (!route) return logger.debug("No Express route to use for naming.")

  params = route.params

  var trans = segment.transaction
  var path = route.path || route.regexp

  if (!path) return logger.debug({route: route}, "No path found on Express route.")

  // when route is a regexp, route.path will be a regexp
  if (path instanceof RegExp) path = path.source

  urltils.copyParameters(trans.agent.config, params, segment.parameters)

  if (append) {
    ensurePartialName(trans)
    trans.partialName += path[0] === '/' ? path.slice(1) : path
  } else {
    trans.partialName = NAMES.EXPRESS.PREFIX + trans.verb + NAMES.ACTION_DELIMITER
    trans.partialName += path[0] === '/' ? path : '/' + path
  }
}

var transactionInfoById = {}
function onTransactionFinished(transaction) {
  delete transactionInfoById[transaction.id]
}

module.exports = function initialize(agent, express) {
  var tracer = agent.tracer

  var registered =
    agent.listeners('transactionFinished')
    .indexOf(onTransactionFinished) !== -1

  if (!registered) {
    agent.on('transactionFinished', onTransactionFinished)
  }

  function setDispatcher(app) {
    return function wrappedCreateServer() {
      agent.environment.setDispatcher('express')
      agent.environment.setFramework('express')

      return app.apply(this, arguments)
    }
  }

  /**
   * This needs to be kept up to date with Express to ensure that it's using
   * the same logic to decide where the callback is hiding.
   */
  function wrapRender(version, render) {
    /* jshint maxparams:5 */ // follow Express as closely as possible
    return function cls_wrapRender(view, options, cb, parent, sub) {
      logger.trace("Rendering Express %d view %s.", version, view)
      if (!tracer.getTransaction()) {
        logger.trace(
          'Express %d view %s rendered outside transaction, not measuring.',
          version,
          view
        )
        return render.apply(this, arguments)
      }

      var name = VIEW.PREFIX + view + VIEW.RENDER
      var segment = tracer.createSegment(name, record)
      var wrapped


      if (typeof options === 'function') {
        cb = options
        options = null
      }

      if (cb === null || cb === undefined) {
        /* CAUTION: Need this to generate a metric, but adding a callback
         * changes Express's control flow.
         */
        wrapped = tracer.bindFunction(function syntheticCallback(err, rendered) {
          if (err) {
            segment.end()
            logger.trace(err,
                         "Express %d rendering for metric %s failed for transaction %s:",
                         version,
                         name,
                         segment.transaction.id)

            return this.req.next(err)
          }

          segment.end()
          var returned = this.send(rendered)

          logger.trace("Rendered Express %d view with metric %s for transaction %s.",
                       version,
                       name,
                       segment.transaction.id)

          return returned
        }.bind(this))
      } else {
        wrapped = tracer.bindFunction(function renderWrapper() {
          segment.end()
          var returned = cb.apply(this, arguments)

          return returned
        }, segment)
      }

      return render.call(this, view, options, wrapped, parent, sub)
    }
  }

  function wrapMatchRequest(version, matchRequest) {
    return function cls_wrapMatchRequest() {
      if (!tracer.getTransaction()) {
        logger.trace(
          'Express %d router called outside transaction (wrapMatchRequest).',
          version
        )
        return matchRequest.apply(this, arguments)
      }
      var route = matchRequest.apply(this, arguments)

      nameFromRoute(tracer.getSegment(), route)
      return route
    }
  }

  function wrapProcessParams(version, process_params) {
    return function cls_wrapProcessParams(layer) {
      var transaction = tracer.getTransaction()
      if (!transaction) {
        logger.trace(
          'Express %d router called outside transaction (wrapProcessParams).',
          version
        )
        return process_params.apply(this, arguments)
      }

      // process_params is the first place (I think) where we have access to a Layer
      // instance.  Layer is basically the interface for (req, res, next) - could be
      // a container for actual route handler, or an instance of Router, or an instance
      // of an Express sub-app.
      // Layers are called in the order they are defined.  The method
      // Layer.handle_request(req, res, next) is the method called in the chain.
      // If we wrap it, we know exactly when each handler in the chain is called. Based on
      // that we can build the path from nested route handlers.
      if (layer) {
          if (layer.constructor) {
            var layerProto = layer.constructor.prototype
            if (
              layerProto.handle_request &&
              !shimmer.isWrapped(layerProto.handle_request)
            ) {
              shimmer.wrapMethod(layerProto,
                                'express.Layer',
                                'handle_request',
                                wrapLayerHandleRequest)
            }
            if (
              layerProto.handle_error &&
              !shimmer.isWrapped(layerProto.handle_error)
            ) {
              shimmer.wrapMethod(layerProto,
                                'express.Layer',
                                'handle_error',
                                wrapLayerHandleError)
            }
          }

          if (layer.handle && !shimmer.isWrapped(layer.handle)) {
            layer.handle = wrapHandle(layer.handle, layer.path)
          }
      }

      function wrapLayerHandleError(handleError) {
        return function wrappedLayerHandleError(error, req, res, next) {
          var parent = tracer.segment
          var transaction = parent.transaction

          if (!transaction.isActive()) {
            return handleError.apply(this, arguments)
          }

          var transactionInfo = getTransactionInfo(transaction)
          transactionInfo.errorHandled = true

          if (agent.config.feature_flag.express_segments) {
            var routerStack = transactionInfo.routerStack

            var parentSegment
            if (routerStack.length > 0) {
              var parentRouter = routerStack[routerStack.length - 1]
              while (--parentRouter.length === 0) {
                parentRouter = routerStack.pop()
              }
              parentSegment = parentRouter.segment
            }

            var handlerName = (this.handle.name || 'anonymous')
            if (this.handle.length === 4) {
              var prefix = NAMES.EXPRESS.ERROR_HANDLER
              var segment = tracer.createSegment(prefix + handlerName, record,
                parentSegment)

              if (segment) {
                segment.start()
              }
            }
          }

          if (next) {
            arguments[3] = endErrorHandlerRecorder
          }

          handleError.apply(this, arguments)

          function endErrorHandlerRecorder(err) {
            if (err) {
              transactionInfo.errorHandled = true
              transactionInfo.error = err
            }

            if (parent.transaction.isActive()) {
              tracer.segment = parent
            }

            if (agent.config.feature_flag.express_segments) {
              if (segment && segment.transaction.isActive()) {
                segment.end()
              }
              if (parentSegment) {
                parentSegment.touch()
              }
            }
            next.apply(this, arguments)
          }
        }
      }

      function wrapLayerHandleRequest(handleRequest) {
        return function wrappedLayerHandleRequest(req, res, next) {
          var segment
          var parent = tracer.getSegment()
          if (!parent) {
            return handleRequest.apply(this, arguments)
          }

          var transaction = parent.transaction
          var transactionInfo = getTransactionInfo(transaction)

          if (!transaction.isActive()) {
            return handleRequest.apply(this, arguments)
          }

          // wrap res.end()
          if (!res.end[ORIGINAL]) {
            var oldEnd = res.end

            res.end = function wrappedEnd() {
              // end the current middleware segment
              if (transactionInfo.lastMiddlewareSegment) {
                transactionInfo.lastMiddlewareSegment.end()
              }

              // end all router segments
              var routerStack = transactionInfo.routerStack
              if (routerStack.length > 0) {
                for (var i = (routerStack.length - 1); i >= 0; i--) {
                  if (routerStack[i].segment) {
                    routerStack[i].segment.end()
                  }
                }
              }

              transactionInfo.responded = true

              var err = transactionInfo.error
              var errHandled = transactionInfo.errorHandled
              var isHttpError = urltils.isError(agent.config, this.statusCode)
              // report error if it was not handled by an error handler, or when
              // the status code is an HTTP error (more useful to report the actual error
              // than a generic HTTP status error)
              if (err && (!errHandled || isHttpError)) {
                agent.errors.add(transaction, err)
              }

              var name = transaction.partialName
              if (name && name[name.length - 1] === '/') {
                transaction.partialName = name.substring(0, name.length - 1)
              }
              return oldEnd.apply(res, arguments)
            }

            res.end[ORIGINAL] = oldEnd
          }

          var isErrorHandler = (this.handle.length === 4)
          var isMountedApp = (this.name === 'mounted_app' ||
                              this.handle.lazyrouter)
          var isRouter = (this.handle[ORIGINAL] && this.handle[ORIGINAL].stack)
          var isRoute = this.route
          var stack = (this.handle[ORIGINAL] && this.handle[ORIGINAL].stack) ||
                      (this.route && this.route.stack)
          var inRoute = stack

          if (agent.config.feature_flag.express_segments) {
            var routerStack = transactionInfo.routerStack

            var parentSegment
            if (routerStack.length > 0) {
              parentSegment = routerStack[routerStack.length - 1].segment
            }

            if (!isErrorHandler) {
              var segmentName
              var recorder

              if (isMountedApp) {
                segmentName = NAMES.EXPRESS.PREFIX + 'Mounted App: ' +
                  this.handle.__NR_path
              } else if (isRouter) {
                segmentName = NAMES.EXPRESS.PREFIX + 'Router: ' + this.handle.__NR_path
              } else if (isRoute) {
                segmentName = NAMES.EXPRESS.PREFIX + 'Route Path: ' +
                  this.handle.__NR_path
              } else {
                var middlewareName = (this.handle.name || 'anonymous')
                segmentName = NAMES.EXPRESS.MIDDLEWARE + middlewareName
                var middlewarePath = generateMiddlewarePath(routerStack)
                recorder = middlewareRecorder.bind(null, middlewarePath)
              }

              segment = tracer.createSegment(segmentName, recorder, parentSegment)
              tracer.segment = segment
              segment.start()
            }

            if (inRoute) {
              routerStack.push({
                length: stack.length,
                path: this.handle.__NR_path,
                segment: segment
              })
            } else if (isMountedApp) {
              routerStack.push({
                length: req.app._router.length,
                path: this.handle.__NR_path,
                segment: segment
              })
            } else {
              transactionInfo.lastMiddlewareSegment = segment
            }
          }
          // END FEATURE FLAG

          if (isRoute || isRouter) {
            if (req.params) {
              // Express 4.3.0 changed where params live. On newer versions of Express
              // params should be populated, on older it shouldn't be.
              urltils.copyParameters(
                transaction.agent.config,
                req.params,
                parent.parameters
              )
            }
          }

          // call cleanup before next middleware function in order to restore
          // transaction.partialName
          if (next) {
            arguments[2] = cleanup
          }

          var original
          if (this.handle.__NR_path || this.route) {
            var path = this.handle.__NR_path

            original = transaction.partialName

            // If route is defined on the Layer, then it is the middleware
            // responsible for routing, and we call nameFromRoute as it worked before.
            // Otherwise if path is defined, then it is a Router or subApp middleware,
            // which originally went through wrappedHandle().
            if (this.route) {
              nameFromRoute(tracer.getSegment(), this.route, this.params, true)
            } else {
              ensurePartialName(transaction)

              if (path && path !== '/') {
                transaction.partialName += path[0] === '/' ? path.slice(1) : path
              }

              if (transaction.partialName[transaction.partialName.length - 1] !== '/') {
                transaction.partialName += '/'
              }
            }
          }

          return handleRequest.apply(this, arguments)

          function cleanup(err) {
            if (agent.config.feature_flag.express_segments) {
              if (!inRoute && !isMountedApp && !transactionInfo.responded &&
                  routerStack.length > 0) {
                var parentRouter = routerStack[routerStack.length - 1]
                while (--parentRouter.length === 0) {
                  parentRouter = routerStack.pop()
                }
                parentSegment = parentRouter.segment
              }
              if (segment) {
                segment.end()
              }
              if (parentSegment) {
                parentSegment.touch()
              }
            }

            if (parent.transaction.isActive()) {
              tracer.segment = parent
            }

            if (err && err !== 'route') {
              transactionInfo.error = err
              return next.apply(this, arguments)
            }

            if (!transactionInfo.responded && original) {
              transaction.partialName = original
            }

            next.apply(this, arguments)
          }
        }
      }

      return process_params.apply(this, arguments)
    }
  }

  /**
   * Problem:
   *
   * 1. Express determines whether middleware functions are error handlers by
   *    testing their arity. Not cool.
   * 2. Downstream Express users rely upon being able to iterate over their
   *    middleware stack to find specific middleware functions. Sorta less
   *    uncool, but still a pain.
   *
   * Solution:
   *
   * Use eval. This once. For this one specific purpose. Not anywhere else for
   * any reason.
   */
  function wrapHandle(__NR_handle, path) {
    var name = ''
    var arglist


    // reiterated: testing function arity is stupid
    switch (__NR_handle.length) {
      case 2:
        arglist = '(req, res)'
        break

      case 3:
        arglist = '(req, res, next)'
        break

      // don't break other error handlers
      case 4:
        arglist = '(err, req, res, next)'
        break

      default:
        arglist = '()'
    }

    if (__NR_handle.name) name = mangle(__NR_handle.name)

    // All closure scope variables used in templates must be passed
    // in as formal arguments to the wrapped function constructor
    /* eslint-disable func-names */
    var template = function() {
      var args = tracer.slice(arguments)
      var last = args.length - 1


      if (typeof args[last] === 'function') {
        args[last] = tracer.bindFunction(args[last])
      }

      __NR_handle.apply(this, args)
    }

    var routerTemplate = function() {
      return wrappedHandle.call(this, path, template, [].slice.call(arguments))
    }
    /* eslint-enable func-names */

    var handlerTemplate
    if (Object.getPrototypeOf(__NR_handle) === express.Router
                    || __NR_handle instanceof express.Router) {
      handlerTemplate = routerTemplate
    } else {
      handlerTemplate = template
    }

    // I am a bad person and this makes me feel bad.
    // We use eval because we need to insert the function with a specific
    // name to allow for lookup.
    var wrapped = new Function(
      'tracer', '__NR_handle', 'wrappedHandle', 'path', 'template',
      'return function ' + name + arglist + handlerTemplate.toString().substring(11)
    )(tracer, __NR_handle, wrappedHandle, path, template)

    wrapped[ORIGINAL] = __NR_handle

    // pull the attributes from the original handle up to the wrapped one
    var handleKeys = Object.keys(__NR_handle)
    for (var i = 0; i < handleKeys.length; i++) {
      var key = handleKeys[i]
      wrapped[key] = __NR_handle[key]
    }

    wrapped.__NR_path = path
    return wrapped
  }

  function wrapMiddlewareStack(route, use) {
    return function cls_wrapMiddlewareStack() {
      /* We allow `use` to go through the arguments so it can reject bad things
       * for us so we don't have to also do argument type checking.
       */
      var app = use.apply(this, arguments)
      var path = typeof arguments[0] === 'string' ? arguments[0] : '/'

      /* Express adds routes to the same stack as middleware. We need to wrap
       * that adder too but we only want to wrap the middleware that are
       * added, not the Router.
       */
      // wrap most recently added unwrapped handler
      var i = this.stack.length
      var top
      /* eslint-disable no-cond-assign */
      while (top = this.stack[--i]) {
        if (!top.handle || typeof top.handle !== 'function' || top.handle[ORIGINAL]) {
          break
        }

        top.handle = wrapHandle(top.handle, path)
      }
      /* eslint-enable no-cond-assign */

      return app
    }
  }

  function wrappedHandle(path, handle, args) {
    var transaction = agent.tracer.getTransaction()
    if (!transaction) {
      logger.trace(
        'Express %d handle for path %s called outside transaction (wrappedHandle).',
        version,
        path
      )
    }

    return handle.apply(this, args)
  }

  function getTransactionInfo(transaction) {
    if (!transactionInfoById[transaction.id]) {
      transactionInfoById[transaction.id] = {
        // since Layer.handleRequest isn't scoped to a transaction we need to
        // track the transaction state outside in these maps.

        // routerStacks is transaction.id -> a stack of the last seen routers
        // and their segments. this is for reconstructing the call tree while
        // we traverse using next
        routerStack: [],

        // if a handler responds asynchronously and calls next synchronously,
        // which is the usual case, we will incorrectly name the segment as a
        // middleware rather than a responder.  this tracks whether a response
        // has gone out for the transaction yet.
        responded: false,

        // we don't want to send errors that a user is handling themselves, so
        // we stash the errors we see for a transaction till we know they
        // aren't handling it themselves
        errors: null,

        // if we see an error handler in the middleware tree we consider the
        // error handled
        errorHandled: false,

        // when res.end() is called from a middleware handler, we need to end
        // the middleware segment that contains it
        lastMiddlewareSegment: null
      }
    }
    return transactionInfoById[transaction.id]
  }

  /**
   * Major versions of express have very different factoring,
   * even though the core instrumentation is the same.
   */
  var version = express && express.version && express.version[0]

  /* TJ decided he didn't want to deal with the hassle of updating a
   * version field. Thanks, TJ!
   */
  if (!version && express && express.application &&
      express.application.init && express.response &&
      express.response.render && express.Router &&
      express.Router.prototype.matchRequest) {
    version = '3'
  } else if (!version && express && express.application &&
           express.application.init && express.response &&
           express.response.render && express.Router &&
           express.Router.process_params && express.application.del) {
    version = '4'
  } else if (!version && express && express.application &&
             !express.application.del) {
    version = '5'
  }

  switch (version) {
    case '2':
      /* Express 2 doesn't directly expose its Router constructor, so create an
       * app and grab the constructor off it. Do it before instrumenting
       * createServer so the agent doesn't automatically set the dispatcher
       * to Express.
       */
      var oneoff = express.createServer()
      var Router = oneoff.routes.constructor


      shimmer.wrapMethod(express,
                         'express',
                         'createServer',
                         setDispatcher)

      /* Express 2 squirts its functionality directly onto http.ServerResponse,
       * leaving no clean way to wrap its functionality without pulling in the
       * http module ourselves.
       */
      var http = require('http')
      shimmer.wrapMethod(http.ServerResponse.prototype,
                         'http.ServerResponse.prototype',
                         'render',
                         wrapRender.bind(null, 2))

      shimmer.wrapMethod(Router.prototype,
                         'Router.prototype',
                         '_match',
                         wrapMatchRequest.bind(null, 2))
      break

    case '3':
      shimmer.wrapMethod(express.application,
                         'express.application',
                         'init',
                         setDispatcher)

      shimmer.wrapMethod(express.response,
                         'express.response',
                         'render',
                         wrapRender.bind(null, 3))

      shimmer.wrapMethod(express.Router.prototype,
                         'express.Router.prototype',
                         'matchRequest',
                         wrapMatchRequest.bind(null, 3))
      break

    case '4':
      shimmer.wrapMethod(express.application,
                         'express.application',
                         'init',
                         setDispatcher)

      shimmer.wrapMethod(express.response,
                         'express.response',
                         'render',
                         wrapRender.bind(null, 4))

      shimmer.wrapMethod(express.Router,
                         'express.Router',
                         'process_params',
                         wrapProcessParams.bind(null, 4))

      shimmer.wrapMethod(express.Router,
                         'express.Router',
                         'use',
                         wrapMiddlewareStack.bind(null, false))

      shimmer.wrapMethod(express.Router,
                         'express.Router',
                         'route',
                         wrapMiddlewareStack.bind(null, true))

      break
    case '5':
      // FLAG: express5 instrumentation
      if (agent.config.feature_flag.express5) {
        shimmer.wrapMethod(express.application,
                          'express.application',
                          'init',
                          setDispatcher)

        shimmer.wrapMethod(express.response,
                          'express.response',
                          'render',
                          wrapRender.bind(null, 5))

        shimmer.wrapMethod(express.Router.prototype,
                          'express.Router.prototype',
                          'process_params',
                          wrapProcessParams.bind(null, 5))

        shimmer.wrapMethod(express.Router.prototype,
                          'express.Router.prototype',
                          'use',
                          wrapMiddlewareStack.bind(null, false))

        shimmer.wrapMethod(express.Router.prototype,
                          'express.Router.prototype',
                          'route',
                          wrapMiddlewareStack.bind(null, true))
      }
      break
    default:
      logger.warn("Unrecognized version %s of Express detected; not instrumenting",
                  version)
  }
}
