text: 'use strict';
  
  // required first because it loads env files.
  const loadConfiguration = require('./core/app-configuration');
  
  const http = require('http');
  const path = require('path');
  const fse = require('fs-extra');
  const Koa = require('koa');
  const Router = require('koa-router');
  const _ = require('lodash');
  const chalk = require('chalk');
  const CLITable = require('cli-table3');
  const { logger, models, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('strapi-utils');
  const { createDatabaseManager } = require('strapi-database');
  
  const utils = require('./utils');
  const loadModules = require('./core/load-modules');
  const bootstrap = require('./core/bootstrap');
  const initializeMiddlewares = require('./middlewares');
  const initializeHooks = require('./hooks');
  const createStrapiFs = require('./core/fs');
  const createEventHub = require('./services/event-hub');
  const createWebhookRunner = require('./services/webhook-runner');
  const { webhookModel, createWebhookStore } = require('./services/webhook-store');
  const { createCoreStore, coreStoreModel } = require('./services/core-store');
  const createEntityService = require('./services/entity-service');
  const createEntityValidator = require('./services/entity-validator');
  const createTelemetry = require('./services/metrics');
  const ee = require('./utils/ee');
  
  /**
   * Construct an Strapi instance.
   *
   * @constructor
   */
  class Strapi {
    constructor(opts = {}) {
      this.reload = this.reload();
  
      // Expose `koa`.
      this.app = new Koa();
      this.router = new Router();
  
      this.server = http.createServer(this.handleRequest.bind(this));
  
      // Logger.
      this.log = logger;
  
      // Utils.
      this.utils = {
        models,
      };
  
      this.dir = opts.dir || process.cwd();
  
      this.admin = {};
      this.plugins = {};
      this.config = loadConfiguration(this.dir, opts);
      this.app.proxy = this.config.get('server.proxy');
  
      this.isLoaded = false;
  
      // internal services.
      this.fs = createStrapiFs(this);
      this.eventHub = createEventHub();
  
      this.requireProjectBootstrap();
    }
  
    get EE() {
      return ee({ dir: this.dir, logger });
    }
  
    handleRequest(req, res) {
      if (!this.requestHandler) {
        this.requestHandler = this.app.callback();
      }
  
      return this.requestHandler(req, res);
    }
  
    requireProjectBootstrap() {
      const bootstrapPath = path.resolve(this.dir, 'config/functions/bootstrap.js');
  
      if (fse.existsSync(bootstrapPath)) {
        require(bootstrapPath);
      }
    }
  
    logStats() {
      const columns = Math.min(process.stderr.columns, 80) - 2;
      console.log();
      console.log(chalk.black.bgWhite(_.padEnd(' Project information', columns)));
      console.log();
  
      const infoTable = new CLITable({
        colWidths: [20, 50],
        chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
      });
  
      const isEE = strapi.EE === true && ee.isEE === true;
  
      infoTable.push(
        [chalk.blue('Time'), `${new Date()}`],
        [chalk.blue('Launched in'), Date.now() - this.config.launchedAt + ' ms'],
        [chalk.blue('Environment'), this.config.environment],
        [chalk.blue('Process PID'), process.pid],
        [chalk.blue('Version'), `${this.config.info.strapi} (node ${process.version})`],
        [chalk.blue('Edition'), isEE ? 'Enterprise' : 'Community']
      );
  
      console.log(infoTable.toString());
      console.log();
      console.log(chalk.black.bgWhite(_.padEnd(' Actions available', columns)));
      console.log();
    }
  
    logFirstStartupMessage() {
      this.logStats();
  
      console.log(chalk.bold('One more thing...'));
      console.log(
        chalk.grey('Create your first administrator 💻 by going to the administration panel at:')
      );
      console.log();
  
      const addressTable = new CLITable();
  
      const adminUrl = getAbsoluteAdminUrl(strapi.config);
      addressTable.push([chalk.bold(adminUrl)]);
  
      console.log(`${addressTable.toString()}`);
      console.log();
    }
  
    logStartupMessage() {
      this.logStats();
  
      console.log(chalk.bold('Welcome back!'));
  
      if (this.config.serveAdminPanel === true) {
        console.log(chalk.grey('To manage your project 🚀, go to the administration panel at:'));
        const adminUrl = getAbsoluteAdminUrl(strapi.config);
        console.log(chalk.bold(adminUrl));
        console.log();
      }
  
      console.log(chalk.grey('To access the server ⚡️, go to:'));
      const serverUrl = getAbsoluteServerUrl(strapi.config);
      console.log(chalk.bold(serverUrl));
      console.log();
    }
  
    async start(cb) {
      try {
        if (!this.isLoaded) {
          await this.load();
        }
  
        this.app.use(this.router.routes()).use(this.router.allowedMethods());
  
        // Launch server.
        this.listen(cb);
      } catch (err) {
        this.stopWithError(err);
      }
    }
  
    /**
     * Add behaviors to the server
     */
    async listen(cb) {
      // handle port in use cleanly
      this.server.on('error', err => {
        if (err.code === 'EADDRINUSE') {
          return this.stopWithError(`The port ${err.port} is already used by another application.`);
        }
  
        this.log.error(err);
      });
  
      // Close current connections to fully destroy the server
      const connections = {};
  
      this.server.on('connection', conn => {
        const key = conn.remoteAddress + ':' + conn.remotePort;
        connections[key] = conn;
  
        conn.on('close', function() {
          delete connections[key];
        });
      });
  
      this.server.destroy = cb => {
        this.server.close(cb);
  
        for (let key in connections) {
          connections[key].destroy();
        }
      };
  
      const onListen = async err => {
        if (err) return this.stopWithError(err);
  
        // Is the project initialised?
        const isInitialised = await utils.isInitialised(this);
  
        // Should the startup message be displayed?
        const hideStartupMessage = process.env.STRAPI_HIDE_STARTUP_MESSAGE
          ? process.env.STRAPI_HIDE_STARTUP_MESSAGE === 'true'
          : false;
  
        if (hideStartupMessage === false) {
          if (!isInitialised) {
            this.logFirstStartupMessage();
          } else {
            this.logStartupMessage();
          }
        }
  
        // Emit started event.
        await this.telemetry.send('didStartServer');
  
        if (cb && typeof cb === 'function') {
          cb();
        }
  
        if (
          (this.config.environment === 'development' &&
            this.config.get('server.admin.autoOpen', true) !== false) ||
          !isInitialised
        ) {
          await utils.openBrowser.call(this);
        }
      };
  
      this.server.listen(this.config.get('server.port'), this.config.get('server.host'), err =>
        onListen(err).catch(err => this.stopWithError(err))
      );
    }
  
    stopWithError(err, customMessage) {
      this.log.debug(`⛔️ Server wasn't able to start properly.`);
      if (customMessage) {
        this.log.error(customMessage);
      }
      this.log.error(err);
      return this.stop();
    }
  
    stop(exitCode = 1) {
      // Destroy server and available connections.
      if (_.has(this, 'server.destroy')) {
        this.server.destroy();
      }
  
      if (this.config.autoReload) {
        process.send('stop');
      }
  
      // Kill process.
      process.exit(exitCode);
    }
  
    async load() {
      this.app.use(async (ctx, next) => {
        if (ctx.request.url === '/_health' && ctx.request.method === 'HEAD') {
          ctx.set('strapi', 'You are so French!');
          ctx.status = 204;
        } else {
          await next();
        }
      });
  
      const modules = await loadModules(this);
  
      this.api = modules.api;
      this.admin = modules.admin;
      this.components = modules.components;
      this.plugins = modules.plugins;
      this.middleware = modules.middlewares;
      this.hook = modules.hook;
  
      await bootstrap(this);
  
      // init webhook runner
      this.webhookRunner = createWebhookRunner({
        eventHub: this.eventHub,
        logger: this.log,
        configuration: this.config.get('server.webhooks', {}),
      });
  
      // Init core store
      this.models['core_store'] = coreStoreModel(this.config);
      this.models['strapi_webhooks'] = webhookModel(this.config);
  
      this.db = createDatabaseManager(this);
      await this.db.initialize();
  
      this.store = createCoreStore({
        environment: this.config.environment,
        db: this.db,
      });
  
      this.webhookStore = createWebhookStore({ db: this.db });
  
      await this.startWebhooks();
  
      this.entityValidator = createEntityValidator({
        strapi: this,
      });
  
      this.entityService = createEntityService({
        db: this.db,
        eventHub: this.eventHub,
        entityValidator: this.entityValidator,
      });
  
      this.telemetry = createTelemetry(this);
  
      // Initialize hooks and middlewares.
      await initializeMiddlewares.call(this);
      await initializeHooks.call(this);
  
      await this.runBootstrapFunctions();
      await this.freeze();
  
      this.isLoaded = true;
  
      return this;
    }
  
    async startWebhooks() {
      const webhooks = await this.webhookStore.findWebhooks();
      webhooks.forEach(webhook => this.webhookRunner.add(webhook));
    }
  
    reload() {
      const state = {
        shouldReload: 0,
      };
  
      const reload = function() {
        if (state.shouldReload > 0) {
          // Reset the reloading state
          state.shouldReload -= 1;
          reload.isReloading = false;
          return;
        }
  
        if (this.config.autoReload) {
          this.server.close();
          process.send('reload');
        }
      };
  
      Object.defineProperty(reload, 'isWatching', {
        configurable: true,
        enumerable: true,
        set: value => {
          // Special state when the reloader is disabled temporarly (see GraphQL plugin example).
          if (state.isWatching === false && value === true) {
            state.shouldReload += 1;
          }
          state.isWatching = value;
        },
        get: () => {
          return state.isWatching;
        },
      });
  
      reload.isReloading = false;
      reload.isWatching = true;
  
      return reload;
    }
  
    async runBootstrapFunctions() {
      const execBootstrap = async fn => {
        if (!fn) return;
  
        return fn();
      };
  
      // plugins bootstrap
      const pluginBoostraps = Object.keys(this.plugins).map(plugin => {
        return execBootstrap(_.get(this.plugins[plugin], 'config.functions.bootstrap')).catch(err => {
          strapi.log.error(`Bootstrap function in plugin "${plugin}" failed`);
          strapi.log.error(err);
          strapi.stop();
        });
      });
      await Promise.all(pluginBoostraps);
  
      // user bootstrap
      await execBootstrap(_.get(this.config, ['functions', 'bootstrap']));
  
      // admin bootstrap : should always run after the others
      const adminBootstrap = _.get(this.admin.config, 'functions.bootstrap');
      return execBootstrap(adminBootstrap).catch(err => {
        strapi.log.error(`Bootstrap function in admin failed`);
        strapi.log.error(err);
        strapi.stop();
      });
    }
  
    async freeze() {
      Object.freeze(this.config);
      Object.freeze(this.dir);
      Object.freeze(this.admin);
      Object.freeze(this.plugins);
      Object.freeze(this.api);
    }
  
    getModel(modelKey, plugin) {
      return this.db.getModel(modelKey, plugin);
    }
  
    /**
     * Binds queries with a specific model
     * @param {string} entity - entity name
     * @param {string} plugin - plugin name or null
     */
    query(entity, plugin) {
      return this.db.query(entity, plugin);
    }
  }
  
  module.exports = options => {
    const strapi = new Strapi(options);
    global.strapi = strapi;
    return strapi;
  };
  