/* global _App */
if (typeof _App === 'undefined') {
  throw new Error('(@unitybase/ub).App accessible only inside server thread')
}
const argv = require('@unitybase/base').argv
const path = require('path')
const UBDomain = require('@unitybase/cs-shared').UBDomain
const EventEmitter = require('events').EventEmitter
const THTTPResponse = require('./HTTPResponse')
const THTTPRequest = require('./HTTPRequest')
const createDBConnectionPool = require('@unitybase/base').createDBConnectionPool
const blobStores = require('@unitybase/blob-stores')
const base = require('@unitybase/base')

/**
 * @classdesc
 * Singleton instance of UnityBase application. Allow direct access to the database connections, blob stores,
 * HTTP endpoints (full control on HTTP request & response) registration, read domain and server config.
 *
 * Mixes EventEmitter, and emit:
 *
 *  - `launchEndpoint:before` with parameters: (req, resp, endpointName)
 *  - `endpointName + ':before'` event before endpoint handler  execution
 *  - `endpointName + ':after'` event in case neither exception is raised nor App.preventDefault() is called
 *  - `launchEndpoint:after` with parameters: (req, resp, endpointName, defaultPrevented)
 *
 * To prevent endpoint handler execution `App.preventDefault()` can be used inside `:before` handler.
 * @example
const App = require('@unitybase/ub').App
// Register public (accessible without authentication) endpoint
App.registerEndpoint('echoToFile', echoToFile, false)
// write custom request body to file FIXTURES/req and echo file back to client
// @ param {THTTPRequest} req
// @ param {THTTPResponse} resp
function echoToFile (req, resp) {
  var fs = require('fs')
  fs.writeFileSync(path.join(FIXTURES, 'req'), req.read('bin'))
  resp.statusCode = 200
  resp.writeEnd(fs.readFileSync(path.join(FIXTURES, 'req'), {encoding: 'bin'}))
}
//Before getDocument requests
//@ param {THTTPRequest} req
//@ param {THTTPResponse} resp
function doSomethingBeforeGetDocumentCall(req, resp){
  console.log('User with ID', Session.userID, 'try to get document')
}
// Adds hook called before each call to getDocument endpoint
App.on('getDocument:before', doSomethingBeforeGetDocumentCall)
//
//After getDocument requests
//@ param {THTTPRequest} req
//@ param {THTTPResponse} resp
function doSomethingAfterGetDocumentCall(req, resp){
  params = req.parsedParameters
  console.log('User with ID', Session.userID, 'obtain document using params',  params)
}
App.on('getDocument:after', doSomethingAfterGetDocumentCall)
 * @class ServerApp
 * @mixes EventEmitter
 */
const ServerApp = {}

/**
 * Fires inside each working thread for an {@link module:@unitybase/ub#App UB.App} just after all domain entities (all *.meta) are loaded into server memory
 * and all server-side js are evaluated (for each working thread) but before BLOB stores initialization and endpoints registration.
 *
 * On this stage you can subscribe on a cross-model handles.
 *
 * @example
const UB = require('@unitybase/ub')
const App = UB.App
App.once('domainIsLoaded', function () {
   const entities = App.domainInfo.entities
   for (const eName in entities) {
      // if entity have attribute mi_fedUnit
      if (entities[eName].attributes.mi_fedUnit) {
        let entityObj = global[eName]
        entityObj.on('insert:before', fedBeforeInsert) // add before insert handler
      }
   }
 })
 * @event domainIsLoaded
 * @memberOf ServerApp
 */

/**
 * Fires inside each working thread for an {@link module:@unitybase/ub#App UB.App} after all initialization step is done (domain, endpoints and BLOB stores are initialized).
 *
 * On this stage application can use both domain and blobStores.
 *
 * @event applicationReady
 * @memberOf ServerApp
 */

/**
 * Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just after HTTP request context tries to get
 * a DB connection for the first time
 *
 * On this stage DB session properties, specific for a current Session can be sets.
 * For example multi-tenancy mixin subscribes for this event and sets a `ub_tenantID` DB session variable value to `Session.tenantID`
 *
 * @event enterConnectionContext
 * @memberOf ServerApp
 */

/**
 * Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just before RDBMS "commit" is called
 *
 * @example
App.on('commit:before', (connectionName) => {
  console.debug('Event commit:before fired for connection', connectionName)
})
 * @event 'commit:before'
 * @param {string} connectionName Name of the connection being committed
 * @memberOf ServerApp
 */

/**
 * Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just after RDBMS "commit" is successfully called
 *
 * @event 'commit:after'
 * @param {string} connectionName Name of the committed connection
 * @memberOf ServerApp
 */

/**
 * Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just before RDBMS "rollback" is called
 *
 * @example
 App.on('rollback:before', (connectionName) => {
  console.debug('Event rollback:before fired for connection', connectionName)
})
 * @event 'rollback:before'
 * @param {string} connectionName Name of the connection being rollback
 * @memberOf ServerApp
 */

/**
 * Fires (by native code) for an {@link module:@unitybase/ub#App UB.App} just after RDBMS "rollback" is called
 *
 * @event 'rollback:after'
 * @param {string} connectionName Name of the rollback connection
 * @memberOf ServerApp
 */

// add eventEmitter to application object
EventEmitter.call(ServerApp, 'App')
Object.assign(ServerApp, EventEmitter.prototype)

let __preventDefault = false
// TODO - remove `ServerApp.emitWithPrevent` when all App level method will be implemented in JS
/**
 * Called by native
 *
 * @param {string} eventName
 * @param {THTTPRequest} req
 * @param {THTTPResponse} resp
 * @private
 * @returns {boolean}
 */
ServerApp.emitWithPrevent = function (eventName, req, resp) {
  __preventDefault = false
  this.emit(eventName, req, resp)
  return __preventDefault
}
/**
 * Accessible inside app-level `:before` event handler. Call to prevent default method handler.
 * In this case developer are responsible to fill response object, otherwise HTTP 400 is returned
 *
 * @memberOf ServerApp
 */
ServerApp.preventDefault = function () {
  __preventDefault = true
}

/**
 * Called by native HTTP server worker.
 * Before UB@5.22.23 `App.endpointContext` variable is cleared inside JS launchEndpoint and therefore
 * context is not available in 'commit|rollback:before|after' events
 *
 * In UB@5.22.23 context is cleared by native code after connections committed/rollback
 *
 * @param {string} endpointName
 * @fires 'launchEndpoint:before'
 * @fires 'launchEndpoint:after'
 * @private
 */
ServerApp.launchEndpoint = launchEndpointWOClearContext

/**
 * @param {string} endpointName
 */
function launchEndpointWOClearContext (endpointName) {
  __preventDefault = false
  const req = new THTTPRequest()
  const resp = new THTTPResponse()
  /**
   * Fires before any endpoint execution
   *
   * @event 'launchEndpoint:before'
   * @memberOf ServerApp
   * @param {THTTPRequest} req
   * @param {THTTPResponse} resp
   * @param {string} endpointName
   */
  this.emit('launchEndpoint:before', req, resp, endpointName)
  /**
   * Fires before endpoint execution. In example below handler is called before each `getDocument` execution
   *
   * @example
   const UB = require('@unitybase/ub')
   const App = UB.App
   function doSomethingBeforeGetDocumentCall(req, resp){
console.log('User with ID', Session.userID, 'try to get document')
}
   // Adds hook called before each call to getDocument endpoint
   App.on('getDocument:before', doSomethingBeforeGetDocumentCall)
   * @event 'endpointName:before'
   * @memberOf ServerApp
   * @param {THTTPRequest} req
   * @param {THTTPResponse} resp
   * @param {string} endpointName
   */
  this.emit(endpointName + ':before', req, resp)
  if (!__preventDefault) {
    const handler = appBinding.endpoints[endpointName]
    if (handler) { // JS endpoint
      handler(req, resp)
    } else { // native endpoint
      appBinding.launchNativeEndpoint()
    }
    /**
     * Fires after endpoint execution. In example below handler is called before each `getDocument` execution
     *
     * @event 'endpointName:after'
     * @memberOf ServerApp
     * @param {THTTPRequest} req
     * @param {THTTPResponse} resp
     * @param {string} endpointName
     */
    this.emit(endpointName + ':after', req, resp)
  }
  /**
   * Fires after any endpoint execution
   *
   * @event 'launchEndpoint:after'
   * @memberOf ServerApp
   * @param {THTTPRequest} req
   * @param {THTTPResponse} resp
   * @param {string} endpointName
   */
  this.emit('launchEndpoint:after', req, resp, endpointName, __preventDefault)
  // ServerApp.endpointContext = {} is called by native in UB@5.22.23
}

const ERR_VALUE_UNIQ = '<<<VALUE_MUST_BE_UNIQUE>>>'
const dbErrorHandlers = [
  // Reference Errors ==================================================================================================

  /**
   * Handle Postgres DB reference error.
   *
   * Examples of "errMsg" to handle (added line breaks - the strings are long):
   *
   * 1) when trying to delete a record being referenced by a foreign key constraint.
   *    Rarely, this is an update of the PK value referenced by another table:
   *
   *   TSQLDBPostgresLib PGERRCODE: 23503, ERROR:
   *   update or delete on table "uba_role" violates foreign key constraint "fk_els_rulerole_ref_role" on table "uba_els"
   *   DETAIL:  Key (id)=(1000000006207) is still referenced from table "uba_els".
   *
   * 2) when trying to insert or update a records using values that points to non-existing records in other tables,
   *    rarely, it is an update of PK value referenced by other table (almost never happens in UB):
   *
   *   TSQLDBPostgresLib PGERRCODE: 23503, ERROR:
   *   insert or update on table "uba_userrole" violates foreign key constraint "fk_usrole_roleid_ref_role"
   *   DETAIL:  Key (roleid)=(23434) is not present in table "uba_role".
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function postgresReferenceError (errMsg) {
    if (!errMsg.includes('PGERRCODE: 23503')) {
      // In either case, the error code is 23503
      return
    }

    if (errMsg.includes('update or delete')) {
      return '<<<ERR_REFERENCE_ERROR>>>'
    }

    if (errMsg.includes('insert or update')) {
      return '<<<ERR_SET_REFERENCE_ERROR>>>'
    }

    console.warn('Unhandled Postgres reference error:', errMsg)
  },

  /**
   * Handle SQL Server DB reference error.
   *
   * Examples of "errMsg" to handle (added line breaks - the strings are long):
   *
   * 1) when trying to delete a record being referenced by a foreign key constraint.
   *
   *   TODBCStatement - TODBCLib error: [23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
   *   The DELETE statement conflicted with the REFERENCE constraint "FK_ELS_RULEROLE_REF_ROLE".
   *   The conflict occurred in database "scriptum", table "dbo.uba_els", column 'ruleRole'. (547)
   *   [01000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
   *   The statement has been terminated. (3621)
   *
   * 2) when trying to insert a records using values that points to non-existing records in other tables,
   *   TODBCStatement - TODBCLib error: [23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
   *   The INSERT statement conflicted with the FOREIGN KEY constraint "FK_USROLE_ROLEID_REF_ROLE".
   *   The conflict occurred in database "scriptum", table "dbo.uba_role", column 'ID'. (547)
   *   [01000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
   *   The statement has been terminated. (3621)
   *
   * 3) when trying to update a records using values that points to non-existing records in other tables,
   *
   *   TODBCStatement - TODBCLib error: [23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
   *   The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_USROLE_ROLEID_REF_ROLE".
   *   The conflict occurred in database "scriptum", table "dbo.uba_role", column 'ID'. (547)
   *   [01000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
   *   The statement has been terminated. (3621)
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function sqlServerReferenceError (errMsg) {
    if (errMsg.includes('The DELETE statement conflicted with the REFERENCE constraint')) {
      return '<<<ERR_REFERENCE_ERROR>>>'
    }

    if (errMsg.includes('The INSERT statement conflicted with the FOREIGN KEY constraint') ||
      errMsg.includes('The UPDATE statement conflicted with the FOREIGN KEY constraint')) {
      return '<<<ERR_SET_REFERENCE_ERROR>>>'
    }
  },

  /**
   * Handle Oracle DB reference error.
   *
   * Examples of "errMsg" to handle (added line breaks - the strings are long):
   *
   * 1) when trying to delete a record being referenced by a foreign key constraint.
   *
   *   TSQLDBOracleStatement error: ORA-02292: integrity constraint (UB.FK_ELS_RULEROLE_REF_ROLE) violated - child record found
   *
   * 2) when trying to insert or update a records using values that points to non-existing records in other tables:
   *
   *   ORA-02291: integrity constraint (UB.FK_USROLE_ROLEID_REF_ROLE) violated - parent key not found
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function oracleReferenceError (errMsg) {
    if (errMsg.includes(' ORA-02292')) {
      return '<<<ERR_REFERENCE_ERROR>>>'
    }

    if (errMsg.includes(' ORA-02291')) {
      return '<<<ERR_SET_REFERENCE_ERROR>>>'
    }
  },

  function sqliteReferenceError (errMsg) {
    if (errMsg.includes('FOREIGN KEY')) {
      return '<<<ERR_REFERENCE_ERROR>>>'
    }
  },

  // Unique Errors =====================================================================================================

  /**
   * Handle Postgres unique constraint error.
   *
   * Example of "errMsg" to handle:
   *   TSQLDBPostgresLib PGERRCODE: 23505, ERROR:  duplicate key value violates unique constraint "uidx_role_name"
   *   DETAIL:  Key (name)=(SysOps) already exists.
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function postgresUniqueError (errMsg) {
    if (errMsg.includes('unique constraint')) {
      const r = /\)=(\(.*\))/.exec(errMsg)
      if (!r || !r[1]) {
        return JSON.stringify(`${ERR_VALUE_UNIQ}|[""]`)
      }

      // Remove `, 9999-12-31 00:00:00` - the mi_deleteDate value
      const tuple = r[1].replace(/, 9999-12-31 00:00:00/g, '')
      return JSON.stringify(`${ERR_VALUE_UNIQ}|["${tuple}"]`)
    }
  },

  /**
   * Handle SQL Server unique constraint error.
   *
   * Example of "errMsg" to handle:
   *   TODBCStatement - TODBCLib error: [23000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
   *   Cannot insert duplicate key row in object 'dbo.uba_role' with unique index 'UIDX_ROLE_NAME'.
   *   The duplicate key value is (SysOps). (2601)
   *   [01000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]
   *   The statement has been terminated. (3621)
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function sqlServerUniqueError (errMsg) {
    if (errMsg.includes(' duplicate key row')) {
      const r = /duplicate key value is (\(.*\))\./.exec(errMsg)
      if (!r || !r[1]) {
        return JSON.stringify(`${ERR_VALUE_UNIQ}|[""]`)
      }

      // Remove `, Dec 31 9999 12:00AM` - the mi_deleteDate value
      const tuple = r[1].replace(/, Dec 31 9999 12:00AM/g, '')
      return JSON.stringify(`${ERR_VALUE_UNIQ}|["${tuple}"]`)
    }
  },

  /**
   * Handle Oracle unique constraint error.
   *
   * Example of "errMsg" to handle:
   *   ORA-00001: unique constraint (UB.UIDX_ROLE_NAME) violated
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function oracleUniqueError (errMsg) {
    if (errMsg.includes(' ORA-00001')) {
      return ERR_VALUE_UNIQ
    }
  },

  function sqliteUniqueError (errMsg) {
    // UNIQUE constraint failed: org_employee.code, org_employee.mi_dateTo, org_employee.mi_deleteDate,
    // extended_errcode=2067
    if (errMsg.includes('UNIQUE constraint')) {
      return ERR_VALUE_UNIQ
    }
  },

  // Resource Limit Errors =============================================================================================

  /**
   * Handle Postgres resource limit error.
   *
   * Example of "errMsg" to handle:
   *   TSQLDBPostgresLib PGERRCODE: 57014, ERROR:  canceling statement due to statement timeout
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function postgresResourceLimitError (errMsg) {
    if (errMsg.includes('statement timeout')) {
      return '<<<ERR_RESOURCE_LIMITS_EXCEED>>>'
    }
  },

  /**
   * Handle SQL Server resource limit error.
   *
   * Example of "errMsg" to handle:
   *   Query timeout expired
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function sqlServerResourceLimitError (errMsg) {
    if (errMsg.includes('Query timeout expired')) {
      return '<<<ERR_RESOURCE_LIMITS_EXCEED>>>'
    }
  },

  /**
   * Handle Oracle resource limit error.
   *
   * Example of "errMsg" to handle:
   *   ORA-00040: active time limit exceeded - call aborted
   *   ORA-56735: elapsed time limit exceeded - call aborted
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function oracleResourceLimitError (errMsg) {
    if (
      errMsg.includes('ORA-00040') ||
      errMsg.includes('ORA-56735')
    ) {
      return '<<<ERR_RESOURCE_LIMITS_EXCEED>>>'
    }
  },

  /**
   * Handle UB error, when statement memory limit exceeded
   *
   * Example of "errMsg" to handle:
   *   `FetchAllTo[Binary|JSON|CSVValues]: overflow` max statement memory
   *
   * @param {string} errMsg
   * @returns {string|undefined}
   */
  function maxStatementMemoryError (errMsg) {
    if (errMsg.includes('FetchAllTo') && errMsg.includes(': overflow')) {
      return '<<<ERR_RESOURCE_LIMITS_EXCEED>>>'
    }
  }
]

/**
 * Called by native HTTP server worker in case of unhandled error in `launchEndpoint` (after transaction is rolled back).
 * Should not use a DB, because on this stage DB connection may be in inconsistent stage.
 *
 * @param {string} errMsg
 * @returns {string|null} Either null for default behavior, or <<< >>> wrapped error text what returned to caller (client)
 * @private
 */
ServerApp.transformUnhandledError = function (errMsg) {
  for (const handler of dbErrorHandlers) {
    const result = handler(errMsg)
    if (result) {
      return result
    }
  }

  return null
}

/**
 * Called by native RLS mixin. Task of method is to either run a `rls.func` or eval a `rls.expression` for ctx.dataStore.Entity
 *
 * @param {ubMethodParams} ctx
 * @private
 */
ServerApp.launchRLS = function (ctx) {
  const rlsMixin = ctx.dataStore.entity.mixins.rls
  if (rlsMixin.func) { // functional RLS
    if (!rlsMixin.__funcVar) { // parse func namespace 'uba_user.somefunc' (string) -> uba_user.somefunc (function)
      const fPath = rlsMixin.func.split('.')
      let f = global[fPath[0]]
      for (let i = 1, L = fPath.length; i < L; i++) {
        f = f[fPath[i]]
      }
      if (typeof f !== 'function') throw new Error(`${ctx.dataStore.entity.name} rls func "${rlsMixin.func}" is not a function`)
      rlsMixin.__funcVar = f
    }
    console.debug('Call func', rlsMixin.func)
    rlsMixin.__funcVar.call(global[ctx.dataStore.entity.name], ctx) // call RLS function using entity namespace as this
  } else { // expression
    const mParams = ctx.mParams
    // eslint-disable-next-line no-eval
    const expr = eval(rlsMixin.expression)
    console.debug('Eval rls expression to', expr)
    if (!mParams.whereList) {
      mParams.whereList = {}
    }
    mParams.whereList[`rls${Date.now()}`] = {
      expression: expr,
      condition: 'custom'
    }
  }
}

// eslint-disable-next-line n/no-deprecated-api
const appBinding = process.binding('ub_app')
/**
 * Register a server endpoint.
 * One of the endpoints can be default endpoint - it will be used as a fallback
 * in case URL do not start with any of known endpoints name.
 *
 * Exceptions inside endpoint handler are intercepted by UB server. In case exception is occurred
 * server will roll back any active DB transactions and serialize an exception message
 * to response depending on server execution mode:
 *  - for `dev` mode - original exception text will be serialized (for debugging purpose)
 *  - for production mode - in case exception message is wrapped into `<<<..>>>` then this message will be serialized,
 *  if not - text will be always `Internal server error` (for security reason)
 *
 *  Recommended way to throw a handled error inside endpoint handler is `throw new UB.UBAbort('.....')`
 *
 * @example
// Write a custom request body to file FIXTURES/req and echo file back to client
// @param {THTTPRequest} req
// @param {THTTPResponse} resp
//
function echoToFile(req, resp) {
  var fs = require('fs');
  fs.writeFileSync(FIXTURES + 'req', req.read('bin'));
  resp.statusCode = 200;
  resp.writeEnd(fs.readFileSync(FIXTURES + 'req', {encoding: 'bin'}));
}
App.registerEndpoint('echoToFile', echoToFile);
 * @param {string} endpointName
 * @param {Function} handler
 * @param {boolean} [authorizationRequired=true] If `true` UB will check for valid Authorization header before
 *  execute endpoint handler
 * @param {boolean} [isDefault=false]
 * @param {boolean} [bypassHTTPLogging=false] Do not put HTTP body into log (for example if body contains sensitive information, like password)
 * @memberOf ServerApp
 */
ServerApp.registerEndpoint = function (endpointName, handler, authorizationRequired, isDefault, bypassHTTPLogging) {
  if (!appBinding.endpoints[endpointName]) {
    appBinding.endpoints[endpointName] = handler
    return appBinding.registerEndpoint(
      endpointName,
      authorizationRequired === undefined ? true : authorizationRequired,
      isDefault === true,
      bypassHTTPLogging === true
    )
  }
}

/**
 * Grant endpoint to role
 *
 * @param {string} endpointName
 * @param {string} roleCode
 * @returns {boolean} true if endpoint exists and role not already granted, false otherwise
 * @memberOf ServerApp
 */
ServerApp.grantEndpointToRole = function (endpointName, roleCode) {
  return appBinding.grantEndpointToRole(endpointName, roleCode)
}

/**
 * Server configuration - result of {@link module:argv~getServerConfiguration argv.getServerConfiguration}
 *
 * @readonly
 * @type {object}
 * @property {object} httpServer HTTP server config
 * @property {object} application
 * @property {string} application.name
 * @property {string} application.defaultLang
 * @property {object} application.domain
 * @property {Array} application.domain.models
 * @property {Array<string>} application.domain.supportedLanguages
 * @property {object} application.customSettings
 * @property {object} uiSettings Section `uiSettings` of ubConfig
 * @property {object} security
 */
ServerApp.serverConfig = undefined
const SERVER_CONFIG_CS = appBinding.registerCriticalSection('SERVER_CONFIG_CS')
appBinding.enterCriticalSection(SERVER_CONFIG_CS)
try {
  try {
    ServerApp.serverConfig = argv.getServerConfiguration()
  } catch (e) {
    console.error(e.message, e)
  }
} finally {
  appBinding.leaveCriticalSection(SERVER_CONFIG_CS)
}

/**
 * Application `package.json` content (parsed)
 *
 * @type {object}
 */
ServerApp.package = require(path.join(process.configPath, 'package.json'))

/**
 * Full path to application static folder if any, '' if static folder not set
 *
 * @type {string}
 * @readonly
 */
ServerApp.staticPath = ''
if (ServerApp.serverConfig.httpServer && ServerApp.serverConfig.httpServer.inetPub &&
  ServerApp.serverConfig.httpServer.inetPub.trim()) {
  const sp = ServerApp.serverConfig.httpServer.inetPub
  ServerApp.staticPath = path.isAbsolute(sp)
    ? sp
    : path.join(process.configPath, sp)
}

/**
 * Application default language
 *
 * @type {string}
 * @readonly
 */
ServerApp.defaultLang = ServerApp.serverConfig.application.defaultLang

/**
 * Custom settings for application from ubConfig.app.customSettings
 *
 * @deprecated Use App.serverConfig.application.customSettings: Object instead
 * @type {string}
 */
Object.defineProperty(ServerApp, 'customSettings', {
  enumerable: true,
  get: function () {
    console.warn('App.customSettings is deprecated. Use App.serverConfig.application.customSettings instead')
    return JSON.stringify(ServerApp.serverConfig.application.customSettings)
  }
})

/**
 * Return stringify JSON specified in serverConfig.uiSettings
 *
 * @deprecated Use App.serverConfig.uiSettings: Object instead
 * @returns {string}
 */
ServerApp.getUISettings = function () {
  console.warn('App.getUISettings is deprecated. Use App.serverConfig.uiSettings: Object instead')
  return JSON.stringify(ServerApp.serverConfig.uiSettings)
}

/**
 * Full URl HTTP server is listen on (if HTTP server enabled, else - empty string)
 *
 * @type {string}
 * @readonly
 */
ServerApp.serverURL = argv.serverURLFromConfig(ServerApp.serverConfig)

/**
 * URL that the User from the internet will use to access your server. To be used in case server is behind a reverse proxy
 *
 * @type {string}
 * @readonly
 */
ServerApp.externalURL = ServerApp.serverConfig.httpServer.externalURL || ServerApp.serverURL

/**
 * List of a local server IP addresses CRLF (or CR for non-windows) separated
 */
ServerApp.localIPs = _App.localIPs

/**
 * Current application Domain
 *
 * @deprecated UB >=4 use App.domainInfo - a pure JS domain representation
 * @readonly
 */
Object.defineProperty(ServerApp, 'domain', {
  enumerable: true,
  get: function () {
    throw new Error('App.domain is obsolete. Use App.domainInfo')
  }
})

/**
 * For UB EE return true in case product license is exceed. For UB Se always `false`
 *
 * @type {string}
 */
Object.defineProperty(ServerApp, 'isLicenseExceed', {
  enumerable: true,
  get: function () {
    return typeof appBinding.isLicenseExceed === 'function'
      ? appBinding.isLicenseExceed()
      : false
  }
})

const getDomainInfo = appBinding.getDomainInfo
let _domainCache
/**
 * Extended information about application domain (metadata)
 *
 * @memberOf ServerApp
 * @member {UBDomain} domainInfo
 */
Object.defineProperty(ServerApp, 'domainInfo', {
  enumerable: true,
  get: function () {
    if (!_domainCache) {
      _domainCache = (new UBDomain(getDomainInfo(true))) // get extended domain information
    }
    return _domainCache
  }
})

/**
 * Called by @unitybase/ub initialization process to reset domain after all models evaluated, because entity methods
 * are added by models using entity.addMethod
 *
 * @private
 */
ServerApp._resetDomainCache = function () {
  _domainCache = undefined
}

/**
 * Get value from global cache. Global cache shared between all threads and,
 * if redis.useForGlobalCache=true in config - between all servers in server group
 *
 * Return '' (empty string) in case key not present in cache.
 *
 * For multi-tenancy environments key is automatically appended by tenantID (own cache for each tenant)
 *
 * @param {string} key Key to retrieve
 * @param {object} [options]
 * @param {boolean} [options.ignoreTenants=false] for multi-tenancy environment. If `true` - do not append key by tenantID
 * @returns {string}
 */
ServerApp.globalCacheGet = function (key, options) {
  const opt = Object.assign({ ignoreTenants: false }, options)
  return _App.globalCacheGet(key, opt.ignoreTenants)
}
/**
 * Put value to global cache. Global cache shared between all threads and,
 * if redis.useForGlobalCache=true in config - between all servers in server group
 *
 * For multi-tenancy environments key is automatically appended by tenantID (own cache for each tenant)
 *
 * @param {string} key  key to hold the string value
 * @param {string|null} value Value to put into this key. If === null then key will be remover from cache
 * @param {object} [options]
 * @param {boolean} [options.nx=false] only set the key if it does not already exist
 * @param {number} [options.px=0] *REDIS* backend only. Set the specified expire time, in milliseconds (a positive integer); 0 - not expired
 * @param {boolean} [options.ignoreTenants=false] for multi-tenancy environment. If `true` - do not append key by tenantID
 * @returns {boolean} return true if key is set, otherwise - false (for example when nx=true and key exists)
 */
ServerApp.globalCachePut = function (key, value, options) {
  const opt = Object.assign({ ignoreTenants: false, nx: false, px: 0 }, options)
  return _App.globalCachePut(key, value, opt.ignoreTenants, opt.nx, opt.px)
}

/**
 * Get value from memory cache. Memory cache shared between all threads of current instance.
 * If you are *completely sure* that the value should be used by _only_ one instance, use this function, otherwise use `globalCacheGet`.
 *
 * Return '' (empty string) in case key not present in cache.
 *
 * For multi-tenancy environments key is automatically appended by tenantID (own cache for each tenant)
 *
 * @param {string} key Key to retrieve
 * @param {object} [options]
 * @param {boolean} [options.ignoreTenants=false] for multi-tenancy environment. If `true` - do not append key by tenantID
 * @returns {string}
 */
ServerApp.memCacheGet = function (key, options) {
  const opt = Object.assign({ ignoreTenants: false }, options)
  return _App.memCacheGet(key, opt.ignoreTenants)
}
/**
 * Put value to memory cache. Memory cache shared between all threads of current instance.
 * If you are *completely sure* that the value should be used by _only_ one instance, use this function, otherwise use `globalCachePut`.
 *
 * For multi-tenancy environments key is automatically appended by tenantID (own cache for each tenant)
 *
 * @param {string} key  Key to put into
 * @param {string|null} value Value to put into this key. If === null then key will be remover from cache
 * @param {object} [options]
 * @param {boolean} [options.nx=false] only set the key if it does not already exist
 * @param {boolean} [options.ignoreTenants=false] for multi-tenancy environment. If `true` - do not append key by tenantID
 * @returns {boolean} return true if key is set, otherwise - false (for example when nx=true and key exists)
 */
ServerApp.memCachePut = function (key, value, options) {
  const opt = Object.assign({ ignoreTenants: false, nx: false, px: 0 }, options)
  return _App.memCachePut(key, value, opt.ignoreTenants, opt.nx, opt.px)
}

/**
 * Delete row from FTS index for exemplar with `instanceID` of entity `entityName` (mixin `fts` must be enabled for entity)
 *
 * @param {string} entityName
 * @param {number} instanceID
 */
ServerApp.deleteFromFTSIndex = function (entityName, instanceID) {
  _App.deleteFromFTSIndex(entityName, instanceID)
}
/**
 * Update FTS index for exemplar with `instanceID` of entity `entityName` (mixin `fts` must be enabled for entity).
 * In case row does not exist in FTS perform insert action automatically.
 *
 * @param {string} entityName
 * @param {number} instanceID
 */
ServerApp.updateFTSIndex = function (entityName, instanceID) {
  _App.updateFTSIndex(entityName, instanceID)
}

/**
 * Databases connections pool
 *
 * @type {Object<string, DBConnection>}
 */
ServerApp.dbConnections = createDBConnectionPool(ServerApp.domainInfo.connections)

/**
 * Check database are used in current endpoint context and DB transaction is already active
 *
 * @param {string} connectionName
 * @returns {boolean}
 */
ServerApp.dbInTransaction = function (connectionName) {
  return _App.dbInTransaction(connectionName)
}
/**
 * Commit active database transaction if any.
 * In case `connectionName` is not passed will commit all active transactions for all connections.
 * Return `true` if transaction is committed, or `false` if database not in use or no active transaction
 *
 * @param {string} [connectionName]
 * @returns {boolean}
 */
ServerApp.dbCommit = function (connectionName) {
  return connectionName ? _App.dbCommit(connectionName) : _App.dbCommit()
}
/**
 * Rollback active database transaction if any.
 * In case `connectionName` is not passed will rollback all active transactions for all connections.
 * Return `true` if transaction is rollback'ed, or `false` if database not in use or no active transaction
 *
 * @param {string} [connectionName]
 * @returns {boolean}
 */
ServerApp.dbRollback = function (connectionName) {
  return connectionName ? _App.dbRollback(connectionName) : _App.dbRollback()
}
/**
 * Start a transaction for a specified database. If database is not used in this context will
 * create a connection to the database and start transaction.
 *
 * For Oracle with DBLink first statement to DBLink'ed table must be
 * either update/insert/delete or you MUST manually start transaction
 * to prevent "ORA-01453: SET TRANSACTION be first statement"
 *
 * @param {string} connectionName
 * @returns {boolean}
 */
ServerApp.dbStartTransaction = function (connectionName) {
  return _App.dbStartTransaction(connectionName)
}

/**
 * Run `func` in autonomous transaction
 *
 * All database statements, or direct SQL execution from inside `func` will be executed in a separate connection
 * intended for executing autonomous transactions and automatically committed (or rolled back in case of an exception).
 *
 * Can't be nested.
 *
 * @param {function} func
 * @returns {*} a `func` result
 */
ServerApp.runInAutonomousTransaction = function (func) {
  let res
  appBinding.logEnter('Autonomous transaction')
  try {
    _App.enterAutonomousTransaction()
    try {
      res = func()
      _App.commitAutonomousTransaction()
    } catch (e) {
      _App.rollbackAutonomousTransaction()
      throw e
    }
  } finally {
    appBinding.logLeave()
  }
  return res
}

/**
 * Try retrieve  or create new session from request headers.
 * Return `true` if success, `false` if more auth handshakes is required.
 * In case of invalid credential throw security exception
 *
 * @param {boolean} noHTTPBodyInResp If true do not write a uData to the HTTP response
 * @param {boolean} doSetOutCookie If true set an out authorization cookie on success response (Negotiate only)
 * @param {string} [fallbackAuthToken] Optional fallback auth token. If passed - will be used by UB schema authorization
 *   if no other token (in headers\cookies\sessin_signature URI parameter) found
 * @returns {boolean}
 */
ServerApp.authFromRequest = function (noHTTPBodyInResp = false, doSetOutCookie = false, fallbackAuthToken) {
  return _App.authFromRequest(noHTTPBodyInResp, doSetOutCookie, fallbackAuthToken)
}

/**
 * Logout a current user (kill current session)
 * @returns {boolean}
 */
ServerApp.logout = function () {
  return _App.logout()
}
/**
 * Check Entity-Level-Security for specified entity/method
 *
 * @example
if App.els('uba_user', 'insert'){
       // do something
}
 * @param {string} entityCode
 * @param {string} methodCode
 * @param {Array<number>} [rolesIDs] If not passed - current user session is used for roles (faster)
 * @returns {boolean}
 */
ServerApp.els = function (entityCode, methodCode, rolesIDs) {
  return rolesIDs ? _App.els(entityCode, methodCode, rolesIDs) : _App.els(entityCode, methodCode)
}

/**
 * Check IP match any of line from file specified in `ubConfig.security.blackListFileName`.
 * Returns false if not match or string with reason (comment from file line) if so
 *
 * @param {string} IP
 * @returns {boolean|string}
 */
ServerApp.isIPInBlackList = function (IP) {
  return _App.isIPInBlackList(IP)
}

/**
 * Register a named critical section. Can be done only in initialization mode.
 * In case section with the same name already registered in another thread - returns existed CS index
 *
 * All threads MUST register section in the same way, do not put call into condition what may evaluate
 * to the different values in the different threads.
 *
 * @example
const App = require('@unitybase/ub').App
// critical section must be registered once, at the moment modules are evaluated without any conditions
const MY_CS = App.registerCriticalSection('SHARED_FILE_ACCESS')

function concurrentFileAccess() {
  // prevents mutual access to the same file from the different threads
  App.enterCriticalSection(FSSTORAGE_CS)
  try {
    const data = fs.readfileSync('/tmp/concurrent.txt', 'utf8')
    // do some operation what modify data
    fs.writefileSync('/tmp/concurrent.txt', data)
  } finally {
    // important to leave critical section in finally block to prevent forever lock
    App.leaveCriticalSection(FSSTORAGE_CS)
  }
}
 * @function
 * @param {string} csName Critical section name
 * @returns {number}
 */
ServerApp.registerCriticalSection = appBinding.registerCriticalSection
/**
 * Waits for ownership of the specified critical section object. The function returns when the calling thread is granted ownership.
 *
 * ** IMPORTANT** A thread must call `App.leaveCriticalSection` once for each time that it entered the critical section.
 *
 * @function
 * @param {number} csIndex A critical section index returned by `App.registerCriticalSection`
 */
ServerApp.enterCriticalSection = appBinding.enterCriticalSection
/**
 * Releases ownership of the specified critical section
 *
 * @function
 * @param {number} csIndex
 */
ServerApp.leaveCriticalSection = appBinding.leaveCriticalSection

/**
 * Enter a log recursion call.
 * ** IMPORTANT** A thread must call `App.logLeave` once for each time that it entered the log recursion.
 * For method with `ctx: ubMethodParam` parameter `App.wrapEnterLeaveForUbMethod` can be used
 * to create an enter/leave log wrapper
 *
 * @example
 function wrapEnterLeave(enterText, originalMethod) {
    return function(ctx) {
      App.logEnter(enterText)
      try {
        originalMethod(ctx)
      } finally {
        App.logLeave()
      }
    }
  }
 * @function
 * @param {string} methodName
 */
ServerApp.logEnter = appBinding.logEnter
/**
 * Exit a log recursion call
 *
 * @function
 */
ServerApp.logLeave = appBinding.logLeave

/**
 * Enter a log recursion call with `enterText`, call `methodImpl` and exit from log recursion call.
 *
 * In case `enterText` is `method(myMixin) my_entity.select`, logging will be:
 *
 *    20210314 09224807  "  +      method(myMixin) my_entity.select
 *    20210314 09224807  " debug    some debug (shifted by recursion level automatically)
 *    20210314 09224807  "  -      00.005.124
 *
 * @function
 * @param {string} enterText Text what will be logged in the beginning of function call
 * @param {Function} originalMethod function what accept one parameter - ctx: ubMethodParam
 * @returns {Function}
 */
ServerApp.wrapEnterLeaveForUbMethod = function wrapEnterLeaveForUbMethod (enterText, originalMethod) {
  const f = function (ctx) {
    appBinding.logEnter(enterText)
    try {
      originalMethod(ctx)
    } finally {
      appBinding.logLeave()
    }
  }
  f._skipEmitterTrace = true
  return f
}

/**
 * Partially reload server config - the same as -HUP signal for process
 *   + set JS variable `App.serverConfig` to new config
 *   + reset generated index page (uiSettings block of config)
 *
 * @function
 * @returns {boolean} true is config s valid and successfully reloaded
 */
ServerApp.reloadConfig = function () {
  appBinding.reloadConfig()
  const arrData = JSON.parse(_App.globalCacheList(base.GC_KEYS.COMPILED_INDEX_)) // [['ID', 'key', 'keyValue'],...]
  // for UB < 5.23.7 _App.globalCacheList returns all keys
  const compiledIndexes = arrData.filter(r => r[1].startsWith(base.GC_KEYS.COMPILED_INDEX_)).map(r => r[1])
  compiledIndexes.forEach(cIdxKey => {
    this.globalCachePut(cIdxKey, null) // reset compiled index page
  })
  console.debug('Compiled index page resets for', compiledIndexes)
  let result = true
  try {
    const newServerCfg = argv.getServerConfiguration()
    ServerApp.serverConfig = newServerCfg
    console.debug('App.serverConfig reloaded')
  } catch (e) {
    result = false
    console.error(e.message, e)
  }
  return result
}

/**
 * Observe a file system operation time (exposed as prometheus `unitybase_fs_operation_duration_seconds` histogram).
 *
 * **WARNING** do not use a full file path - use a folder name or better a mnemonic name (BLOB store name for example).
 * Amount of metric labels SHOULD be as small as possible. The same is true for operation`s names.
 *
 * See fileSystemBlobStore for real life usage example.
 *
 * @param {number} durationSec fs operation duration in **seconds**
 * @param {string} path
 * @param {string} operation
 */
ServerApp.fsObserve = appBinding.fsObserve

/**
 * Observe an HTTP client operation time (exposed as prometheus `unitybase_httpext_duration_seconds` histogram).
 *
 * `http` module automatically observe each request, passing `host` as `uri` parameter.
 * Method can be called manually in case some part of path should be observed also.
 *
 * **WARNING** do not use a full path - use a part what identify an endpoint without parameters.
 * Amount of metric labels SHOULD be as small as possible. The same is true for operation`s names.
 *
 * @param {number} durationSec request duration in **seconds**
 * @param {string} uri request URI
 * @param {number} respStatus HTTP response status code
 */
ServerApp.httpCallObserve = appBinding.httpCallObserve

/**
 * Remove all user sessions (logout user).
 *
 * If `exceptCurrent` is `true` - do not remove current session (logout all other sessions except my).
 *
 * @example
const UB = require('@unitybase/ub')
const Session = UB.Session
const App = UB.App
Session.on('login', logoutAllMyOldSessions)
// One user - one session mode
function logoutAllMyOldSessions (req) {
  if (App.removeUserSessions(Session.userID, true)) {
    console.log(`All other sessions for user ${Session.userID} are removed`)
  }
}
 * @function
 * @param {number} userID
 * @param {boolean} [exceptCurrent=false] If `true` - do not remove current session
 * @returns {boolean} true if user had had any session
 */
ServerApp.removeUserSessions = function (userID, exceptCurrent = false) {
  return appBinding.removeUserSessions(userID, exceptCurrent)
}

/**
 * Return session count for specified user, including current session
 *
 * @function
 * @param {number} userID
 * @returns {number} session count for specified user, including current session
 * @since UB@5.22
 */
ServerApp.getUserSessionsCount = function (userID) {
  return appBinding.getUserSessionsCount(userID)
}

/**
 * Confirm 2FA secret previously sets by `Session.setExpected2faSecret`.
 * If secrets match - reset session expected secret and returns `true`. After this endpoints execution became allowed;
 * If secret does not match - return string with one of the reason (do not expose this reason to caller!):
 *   - 'sessionNotFound': provided sessionID not exists
 *   - 'sessionIsSystem': provided sessionID is for system session, such sessions can't be a subject of 2FA
 *   - 'wrongSecret': provided secret do not match session 2FA secret
 *   - 'wrongSecretUserLocked': provided secret do not match session 2FA secret many times, so session is closed and user is locked
 * If secret is empty - return confirmation status `true` if 2fa is confirmed, `false` if not, string with reason in case of error
 *
 * @function
 * @param {string} sessionID
 * @param {string} [secret=''] if secret is empty - status request
 * @returns {boolean|string}
 * @since UB@5.22.18
 */
ServerApp.confirmSession2faSecret = function (sessionID, secret = '') {
  return appBinding.confirmSession2faSecret(sessionID, secret)
}

/**
 * Is event emitter enabled for App singleton. Default is `false`
 *
 * @deprecated Starting from 1.11 this property ignored (always TRUE)
 * @type {boolean}
 */
ServerApp.emitterEnabled = true

/**
 * Defense edition only,
 * Base64 encoded public server certificate
 *
 * Contains non-empty value in case `security.dstu.trafficEncryption` === `true` and
 * key name defined in `security.dstu.novaLib.keyName`
 *
 * @type {string}
 */
ServerApp.serverPublicCert = _App.serverPublicCert

/**
 * BLOB stores methods. For usage examples see:
 *  - {@link module:@unitybase/blob-stores~getContent App.blobStores.getContent} - load content of BLOB into memory
 *  - {@link module:@unitybase/blob-stores~getContentPath App.blobStores.getContentPath} - get a path to the file based store BLOB content
 *  - {@link module:@unitybase/blob-stores~putContent App.blobStores.putContent} - put a BLOB content to the temporary storage
 *  - {@link module:@unitybase/blob-stores~markRevisionAsPermanent App.blobStores.markRevisionAsPermanent} - mark specified revision of a historical store as permanent
 *  - {@link module:@unitybase/blob-stores~internalWriteDocumentToResp App.blobStores.internalWriteDocumentToResp} - mark specified revision of a historical store as permanent
 *  - {@link module:@unitybase/blob-stores~shred App.blobStores.shred} - *completely remove*, including all historical revisions all BLOBs content for
 * specified entity, attribute and ID(s)
 * - {@link module:@unitybase/blob-stores~shred App.blobStores.shredAll} - like shred, but for all entity BLOB attributes
 */
ServerApp.blobStores = {
  getContent: blobStores.getContent,
  getContentPath: blobStores.getContentPath,
  getContentPathAndBlobInfo: blobStores.getContentPathAndBlobInfo,
  putContent: blobStores.putContent,
  markRevisionAsPermanent: blobStores.markRevisionAsPermanent,
  internalWriteDocumentToResp: blobStores.internalWriteDocumentToResp,
  shred: blobStores.shred,
  shredAll: blobStores.shredAll
}

/**
 * Endpoint context. Application logic can store here some data what required during single HTTP method call;
 * Starting from UB@5.17.9 server reset `App.endpointContext` to {} after endpoint implementation execution,
 * so in the beginning of execution it's always empty
 *
 *    App.endpointContext.MYMODEL_mykey = 'some value we need to share between different methods during a single user request handling'
 *
 * @type {object}
 */
ServerApp.endpointContext = {}

module.exports = ServerApp