text: 'use strict'
  
  const FindMyWay = require('find-my-way')
  const proxyAddr = require('proxy-addr')
  const Context = require('./context')
  const { buildMiddie, onRunMiddlewares } = require('./middleware')
  const { hookRunner, hookIterator } = require('./hooks')
  const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
  const supportedHooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization', 'onResponse', 'onSend']
  const validation = require('./validation')
  const buildSchema = validation.build
  const { buildSchemaCompiler } = validation
  const { beforeHandlerWarning } = require('./warnings')
  
  const {
    codes: {
      FST_ERR_SCH_BUILD,
      FST_ERR_SCH_MISSING_COMPILER
    }
  } = require('./errors')
  
  const {
    kRoutePrefix,
    kLogLevel,
    kLogSerializers,
    kHooks,
    kSchemas,
    kOptions,
    kSchemaCompiler,
    kSchemaResolver,
    kContentTypeParser,
    kReply,
    kReplySerializerDefault,
    kRequest,
    kMiddlewares,
    kGlobalHooks,
    kDisableRequestLogging
  } = require('./symbols.js')
  
  function buildRouting (options) {
    const router = FindMyWay(options.config)
  
    const schemaCache = new Map()
    schemaCache.put = schemaCache.set
  
    let avvio
    let fourOhFour
    let trustProxy
    let requestIdHeader
    let querystringParser
    let requestIdLogLabel
    let logger
    let hasLogger
    let setupResponseListeners
    let throwIfAlreadyStarted
    let proxyFn
    let modifyCoreObjects
    let genReqId
    let disableRequestLogging
    let ignoreTrailingSlash
    let return503OnClosing
  
    let closing = false
  
    return {
      setup (options, fastifyArgs) {
        avvio = fastifyArgs.avvio
        fourOhFour = fastifyArgs.fourOhFour
        logger = fastifyArgs.logger
        hasLogger = fastifyArgs.hasLogger
        setupResponseListeners = fastifyArgs.setupResponseListeners
        throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
  
        proxyFn = getTrustProxyFn(options)
        trustProxy = options.trustProxy
        requestIdHeader = options.requestIdHeader
        querystringParser = options.querystringParser
        requestIdLogLabel = options.requestIdLogLabel
        modifyCoreObjects = options.modifyCoreObjects
        genReqId = options.genReqId
        disableRequestLogging = options.disableRequestLogging
        ignoreTrailingSlash = options.ignoreTrailingSlash
        return503OnClosing = Object.prototype.hasOwnProperty.call(options, 'return503OnClosing') ? options.return503OnClosing : true
      },
      routing: router.lookup.bind(router), // router func to find the right handler to call
      route, // configure a route in the fastify instance
      prepareRoute,
      routeHandler,
      closeRoutes: () => { closing = true },
      printRoutes: router.prettyPrint.bind(router)
    }
  
    // Convert shorthand to extended route declaration
    function prepareRoute (method, url, options, handler) {
      if (!handler && typeof options === 'function') {
        handler = options
        options = {}
      } else if (handler && typeof handler === 'function') {
        if (Object.prototype.toString.call(options) !== '[object Object]') {
          throw new Error(`Options for ${method}:${url} route must be an object`)
        } else if (options.handler) {
          if (typeof options.handler === 'function') {
            throw new Error(`Duplicate handler for ${method}:${url} route is not allowed!`)
          } else {
            throw new Error(`Handler for ${method}:${url} route must be a function`)
          }
        }
      }
  
      options = Object.assign({}, options, {
        method,
        url,
        handler: handler || (options && options.handler)
      })
  
      return route.call(this, options)
    }
  
    // Route management
    function route (opts) {
      throwIfAlreadyStarted('Cannot add route when fastify instance is already started!')
  
      if (Array.isArray(opts.method)) {
        for (var i = 0; i < opts.method.length; i++) {
          if (supportedMethods.indexOf(opts.method[i]) === -1) {
            throw new Error(`${opts.method[i]} method is not supported!`)
          }
        }
      } else {
        if (supportedMethods.indexOf(opts.method) === -1) {
          throw new Error(`${opts.method} method is not supported!`)
        }
      }
  
      if (!opts.handler) {
        throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`)
      }
  
      validateBodyLimitOption(opts.bodyLimit)
  
      if (opts.preHandler == null && opts.beforeHandler != null) {
        beforeHandlerWarning()
        opts.preHandler = opts.beforeHandler
      }
  
      const prefix = this[kRoutePrefix]
  
      this.after((notHandledErr, done) => {
        var path = opts.url || opts.path
        if (path === '/' && prefix.length > 0) {
          switch (opts.prefixTrailingSlash) {
            case 'slash':
              afterRouteAdded.call(this, path, notHandledErr, done)
              break
            case 'no-slash':
              afterRouteAdded.call(this, '', notHandledErr, done)
              break
            case 'both':
            default:
              afterRouteAdded.call(this, '', notHandledErr, done)
              // If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one.
              if (ignoreTrailingSlash !== true) {
                afterRouteAdded.call(this, path, notHandledErr, done)
              }
          }
        } else if (path[0] === '/' && prefix.endsWith('/')) {
          // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'
          afterRouteAdded.call(this, path.slice(1), notHandledErr, done)
        } else {
          afterRouteAdded.call(this, path, notHandledErr, done)
        }
      })
  
      // chainable api
      return this
  
      function afterRouteAdded (path, notHandledErr, done) {
        const url = prefix + path
  
        opts.url = url
        opts.path = url
        opts.prefix = prefix
        opts.logLevel = opts.logLevel || this[kLogLevel]
  
        if (this[kLogSerializers] || opts.logSerializers) {
          opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)
        }
  
        if (opts.attachValidation == null) {
          opts.attachValidation = false
        }
  
        // run 'onRoute' hooks
        for (const hook of this[kGlobalHooks].onRoute) {
          try {
            hook.call(this, opts)
          } catch (error) {
            done(error)
            return
          }
        }
  
        const config = opts.config || {}
        config.url = url
  
        const context = new Context(
          opts.schema,
          opts.handler.bind(this),
          this[kReply],
          this[kRequest],
          this[kContentTypeParser],
          config,
          this._errorHandler,
          opts.bodyLimit,
          opts.logLevel,
          opts.logSerializers,
          opts.attachValidation,
          this[kReplySerializerDefault]
        )
  
        for (const hook of supportedHooks) {
          if (opts[hook]) {
            if (Array.isArray(opts[hook])) {
              opts[hook] = opts[hook].map(fn => fn.bind(this))
            } else {
              opts[hook] = opts[hook].bind(this)
            }
          }
        }
  
        try {
          router.on(opts.method, opts.url, { version: opts.version }, routeHandler, context)
        } catch (err) {
          done(err)
          return
        }
  
        // It can happen that a user register a plugin with some hooks/middlewares *after*
        // the route registration. To be sure to load also that hooks/middlewares,
        // we must listen for the avvio's preReady event, and update the context object accordingly.
        avvio.once('preReady', () => {
          const onResponse = this[kHooks].onResponse
          const onSend = this[kHooks].onSend
          const onError = this[kHooks].onError
  
          context.onSend = onSend.length ? onSend : null
          context.onError = onError.length ? onError : null
          context.onResponse = onResponse.length ? onResponse : null
  
          for (const hook of supportedHooks) {
            const toSet = this[kHooks][hook].concat(opts[hook] || [])
            context[hook] = toSet.length ? toSet : null
          }
  
          context._middie = buildMiddie(this[kMiddlewares])
  
          // Must store the 404 Context in 'preReady' because it is only guaranteed to
          // be available after all of the plugins and routes have been loaded.
          fourOhFour.setContext(this, context)
  
          if (opts.schema) {
            if (this[kSchemaCompiler] == null && this[kSchemaResolver]) {
              throw new FST_ERR_SCH_MISSING_COMPILER(opts.method, url)
            }
  
            try {
              if (opts.schemaCompiler == null && this[kSchemaCompiler] == null) {
                const externalSchemas = this[kSchemas].getJsonSchemas({ onlyAbsoluteUri: true })
                this.setSchemaCompiler(buildSchemaCompiler(externalSchemas, this[kOptions].ajv, schemaCache))
              }
  
              buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas], this[kSchemaResolver])
            } catch (error) {
              // bubble up the FastifyError instance
              throw (error.code ? error : new FST_ERR_SCH_BUILD(opts.method, url, error.message))
            }
          }
        })
  
        done(notHandledErr)
      }
    }
  
    // HTTP request entry point, the routing has already been executed
    function routeHandler (req, res, params, context) {
      if (closing === true) {
        if (req.httpVersionMajor !== 2) {
          res.once('finish', () => req.destroy())
          res.setHeader('Connection', 'close')
        }
  
        if (return503OnClosing) {
          const headers = {
            'Content-Type': 'application/json',
            'Content-Length': '80'
          }
          res.writeHead(503, headers)
          res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
          return
        }
      }
  
      req.id = req.headers[requestIdHeader] || genReqId(req)
      req.originalUrl = req.url
      var hostname = req.headers.host || req.headers[':authority']
      var ip = req.connection.remoteAddress
      var ips
  
      if (trustProxy) {
        ip = proxyAddr(req, proxyFn)
        ips = proxyAddr.all(req, proxyFn)
        if (ip !== undefined && req.headers['x-forwarded-host']) {
          hostname = req.headers['x-forwarded-host']
        }
      }
  
      var loggerOpts = {
        [requestIdLogLabel]: req.id,
        level: context.logLevel
      }
  
      if (context.logSerializers) {
        loggerOpts.serializers = context.logSerializers
      }
      var childLogger = logger.child(loggerOpts)
      childLogger[kDisableRequestLogging] = disableRequestLogging
  
      // added hostname, ip, and ips back to the Node req object to maintain backward compatibility
      if (modifyCoreObjects) {
        req.hostname = hostname
        req.ip = ip
        req.ips = ips
  
        req.log = res.log = childLogger
      }
  
      if (disableRequestLogging === false) {
        childLogger.info({ req }, 'incoming request')
      }
  
      var queryPrefix = req.url.indexOf('?')
      var query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '')
      var request = new context.Request(params, req, query, req.headers, childLogger, ip, ips, hostname)
      var reply = new context.Reply(res, context, request, childLogger)
  
      if (hasLogger === true || context.onResponse !== null) {
        setupResponseListeners(reply)
      }
  
      if (context.onRequest !== null) {
        hookRunner(
          context.onRequest,
          hookIterator,
          request,
          reply,
          middlewareCallback
        )
      } else {
        middlewareCallback(null, request, reply)
      }
    }
  }
  
  function validateBodyLimitOption (bodyLimit) {
    if (bodyLimit === undefined) return
    if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) {
      throw new TypeError(`'bodyLimit' option must be an integer > 0. Got '${bodyLimit}'`)
    }
  }
  
  function middlewareCallback (err, request, reply) {
    if (reply.sent === true) return
    if (err != null) {
      reply.send(err)
      return
    }
  
    if (reply.context._middie !== null) {
      reply.context._middie.run(request.raw, reply.res, reply)
    } else {
      onRunMiddlewares(null, null, null, reply)
    }
  }
  
  function getTrustProxyFn (options) {
    const tp = options.trustProxy
    if (typeof tp === 'function') {
      return tp
    }
    if (tp === true) {
      // Support plain true/false
      return function () { return true }
    }
    if (typeof tp === 'number') {
      // Support trusting hop count
      return function (a, i) { return i < tp }
    }
    if (typeof tp === 'string') {
      // Support comma-separated tps
      const vals = tp.split(',').map(it => it.trim())
      return proxyAddr.compile(vals)
    }
    return proxyAddr.compile(tp || [])
  }
  
  module.exports = { buildRouting, validateBodyLimitOption }
  