text: 'use strict';
  
  const path = require('path');
  const fs = require('fs');
  const graceful = require('graceful');
  const http = require('http');
  const cluster = require('cluster-client');
  const { assign } = require('utility');
  const eggUtils = require('egg-core').utils;
  const EggApplication = require('./egg');
  const AppWorkerLoader = require('./loader').AppWorkerLoader;
  
  const KEYS = Symbol('Application#keys');
  const HELPER = Symbol('Application#Helper');
  const LOCALS = Symbol('Application#locals');
  const BIND_EVENTS = Symbol('Application#bindEvents');
  const WARN_CONFUSED_CONFIG = Symbol('Application#warnConfusedConfig');
  const EGG_LOADER = Symbol.for('egg#loader');
  const EGG_PATH = Symbol.for('egg#eggPath');
  const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients');
  
  // client error => 400 Bad Request
  // Refs: https://nodejs.org/dist/latest-v8.x/docs/api/http.html#http_event_clienterror
  const DEFAULT_BAD_REQUEST_HTML = `<html>
    <head><title>400 Bad Request</title></head>
    <body bgcolor="white">
    <center><h1>400 Bad Request</h1></center>
    <hr><center>❤</center>
    </body>
    </html>`;
  const DEFAULT_BAD_REQUEST_HTML_LENGTH = Buffer.byteLength(DEFAULT_BAD_REQUEST_HTML);
  const DEFAULT_BAD_REQUEST_RESPONSE =
    `HTTP/1.1 400 Bad Request\r\nContent-Length: ${DEFAULT_BAD_REQUEST_HTML_LENGTH}` +
    `\r\n\r\n${DEFAULT_BAD_REQUEST_HTML}`;
  
  /**
   * Singleton instance in App Worker, extend {@link EggApplication}
   * @extends EggApplication
   */
  class Application extends EggApplication {
  
    /**
     * @constructor
     * @param {Object} options - see {@link EggApplication}
     */
    constructor(options = {}) {
      options.type = 'application';
      super(options);
  
      try {
        this.loader.load();
      } catch (e) {
        // close gracefully
        this[CLUSTER_CLIENTS].forEach(cluster.close);
        throw e;
      }
  
      // dump config after loaded, ensure all the dynamic modifications will be recorded
      this.dumpConfig();
      this[WARN_CONFUSED_CONFIG]();
      this[BIND_EVENTS]();
    }
  
    get [EGG_LOADER]() {
      return AppWorkerLoader;
    }
  
    get [EGG_PATH]() {
      return path.join(__dirname, '..');
    }
  
    onClientError(err, socket) {
      this.logger.error('A client (%s:%d) error [%s] occurred: %s',
        socket.remoteAddress,
        socket.remotePort,
        err.code,
        err.message);
  
      // because it's a raw socket object, we should return the raw HTTP response
      // packet.
      socket.end(DEFAULT_BAD_REQUEST_RESPONSE);
    }
  
    onServer(server) {
      /* istanbul ignore next */
      graceful({
        server: [ server ],
        error: (err, throwErrorCount) => {
          if (err.message) {
            err.message += ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')';
          }
          this.coreLogger.error(err);
        },
      });
  
      server.on('clientError', (err, socket) => this.onClientError(err, socket));
    }
  
    /**
     * global locals for view
     * @member {Object} Application#locals
     * @see Context#locals
     */
    get locals() {
      if (!this[LOCALS]) {
        this[LOCALS] = {};
      }
      return this[LOCALS];
    }
  
    set locals(val) {
      if (!this[LOCALS]) {
        this[LOCALS] = {};
      }
  
      assign(this[LOCALS], val);
    }
  
    /**
     * Create egg context
     * @method Application#createContext
     * @param  {Req} req - node native Request object
     * @param  {Res} res - node native Response object
     * @return {Context} context object
     */
    createContext(req, res) {
      const app = this;
      const context = Object.create(app.context);
      const request = context.request = Object.create(app.request);
      const response = context.response = Object.create(app.response);
      context.app = request.app = response.app = app;
      context.req = request.req = response.req = req;
      context.res = request.res = response.res = res;
      request.ctx = response.ctx = context;
      request.response = response;
      response.request = request;
      context.onerror = context.onerror.bind(context);
      context.originalUrl = request.originalUrl = req.url;
  
      /**
       * Request start time
       * @member {Number} Context#starttime
       */
      context.starttime = Date.now();
      return context;
    }
  
    /**
     * save routers to `run/router.json`
     * @private
     */
    dumpConfig() {
      super.dumpConfig();
  
      // dump routers to router.json
      const rundir = this.config.rundir;
      const FULLPATH = this.loader.FileLoader.FULLPATH;
      try {
        const dumpRouterFile = path.join(rundir, 'router.json');
        const routers = [];
        for (const layer of this.router.stack) {
          routers.push({
            name: layer.name,
            methods: layer.methods,
            paramNames: layer.paramNames,
            path: layer.path,
            regexp: layer.regexp.toString(),
            stack: layer.stack.map(stack => stack[FULLPATH] || stack._name || stack.name || 'anonymous'),
          });
        }
        fs.writeFileSync(dumpRouterFile, JSON.stringify(routers, null, 2));
      } catch (err) {
        this.coreLogger.warn(`dumpConfig router.json error: ${err.message}`);
      }
    }
  
    /**
     * Create an anonymous context, the context isn't request level, so the request is mocked.
     * then you can use context level API like `ctx.service`
     * @member {String} Application#createAnonymousContext
     * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function.
     * @return {Context} context
     */
    createAnonymousContext(req) {
      const request = {
        headers: {
          'x-forwarded-for': '127.0.0.1',
        },
        query: {},
        querystring: '',
        host: '127.0.0.1',
        hostname: '127.0.0.1',
        protocol: 'http',
        secure: 'false',
        method: 'GET',
        url: '/',
        path: '/',
        socket: {
          remoteAddress: '127.0.0.1',
          remotePort: 7001,
        },
      };
      if (req) {
        for (const key in req) {
          if (key === 'headers' || key === 'query' || key === 'socket') {
            Object.assign(request[key], req[key]);
          } else {
            request[key] = req[key];
          }
        }
      }
      const response = new http.ServerResponse(request);
      return this.createContext(request, response);
    }
  
    /**
     * Run async function in the background
     * @see Context#runInBackground
     * @param {Function} scope - the first args is an anonymous ctx
     */
    runInBackground(scope) {
      const ctx = this.createAnonymousContext();
      if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true);
      ctx.runInBackground(scope);
    }
  
    /**
     * secret key for Application
     * @member {String} Application#keys
     */
    get keys() {
      if (!this[KEYS]) {
        if (!this.config.keys) {
          if (this.config.env === 'local' || this.config.env === 'unittest') {
            const configPath = path.join(this.config.baseDir, 'config/config.default.js');
            console.error('Cookie need secret key to sign and encrypt.');
            console.error('Please add `config.keys` in %s', configPath);
          }
          throw new Error('Please set config.keys first');
        }
  
        this[KEYS] = this.config.keys.split(',').map(s => s.trim());
      }
      return this[KEYS];
    }
  
    /**
     * reference to {@link Helper}
     * @member {Helper} Application#Helper
     */
    get Helper() {
      if (!this[HELPER]) {
        /**
         * The Helper class which can be used as utility function.
         * Files from `${baseDir}/app/helper` will be loaded to the prototype of Helper,
         * then you can use all method on `ctx.helper` that is a instance of Helper.
         */
        class Helper extends this.BaseContextClass {}
        this[HELPER] = Helper;
      }
      return this[HELPER];
    }
  
    /**
     * bind app's events
     *
     * @private
     */
    [BIND_EVENTS]() {
      // Browser Cookie Limits: http://browsercookielimits.squawky.net/
      this.on('cookieLimitExceed', ({ name, value, ctx }) => {
        const err = new Error(`cookie ${name}'s length(${value.length}) exceed the limit(4093)`);
        err.name = 'CookieLimitExceedError';
        err.key = name;
        err.cookie = value;
        ctx.coreLogger.error(err);
      });
      // expose server to support websocket
      this.once('server', server => this.onServer(server));
    }
  
    /**
     * warn when confused configurations are present
     *
     * @private
     */
    [WARN_CONFUSED_CONFIG]() {
      const confusedConfigurations = this.config.confusedConfigurations;
      Object.keys(confusedConfigurations).forEach(key => {
        if (this.config[key] !== undefined) {
          this.logger.warn('Unexpected config key `%s` exists, Please use `%s` instead.',
            key, confusedConfigurations[key]);
        }
      });
    }
  }
  
  module.exports = Application;
  