const UB = require('@unitybase/ub')
const App = UB.App
/* global ubs_numcounter ubs_numcounterreserv ubs_settings */
ubs_numcounter.entity.addMethod('getRegnumCounter')
let AUTO_REG_WITH_DELETED_NUMBER_SETTING

/**
 * @returns {boolean}
 */
function getAutoRegWithDeletedNumberSetting () {
  if (AUTO_REG_WITH_DELETED_NUMBER_SETTING === undefined) {
    AUTO_REG_WITH_DELETED_NUMBER_SETTING = ubs_settings.loadKey('ubs.numcounter.autoRegWithDeletedNumber', true)
  }
  return AUTO_REG_WITH_DELETED_NUMBER_SETTING
}

const REGNUM_CS = App.registerCriticalSection('getRegnumLoosely')
const REGNUM_CACHE_SIZE = 100

/**
 * Returns the counter number by mask. Locks the row in the `ubs_numcouner` table until the transaction is committed,
 * so other transactions will wait, but guarantees that the returned number is incremented continuously, without gaps.
 *
 * In case gaps are allowed by business logic, better to use `ubs_numcounter.getRegnumLoosely` - it's lock
 * the `ubs_numcouner` very rarely and much faster because of in-memory cache
 *
 * @function getRegnum
 * @memberOf ubs_numcounter_ns.prototype
 * @memberOfModule @unitybase/ubs
 * @param {string} regKeyValue Registration key mask
 * @param {number} [startNum] The starting counter value in case mask not exists
 * @param {boolean} [skipReservedNumber=false] When "true" skip loading number from reserve and calculate new number by mask
 * @returns {number} Next number for this mask
 */
ubs_numcounter.getRegnum = function (regKeyValue, startNum, skipReservedNumber) {
  let counterInData = -1

  if (startNum !== 0) startNum = startNum || 1
  // Get autoRegWithDeletedNumber from settings if skipReservedNumber is not true
  const autoRegWithDeletedNumber = !skipReservedNumber ? getAutoRegWithDeletedNumberSetting() : false
  // Get counter from reserved if autoRegWithDeletedNumber set to true in settings
  const reservedCounter = (autoRegWithDeletedNumber === true) ? ubs_numcounterreserv.getReservedRegnum(regKeyValue) : -1

  if (reservedCounter !== -1) {
    counterInData = reservedCounter
  } else {
    counterInData = getCurrentCounterAndIncrBy(regKeyValue, startNum, 1) // increment in DB
    counterInData += 1 // increment result
  }
  return counterInData
}

/**
 * Returns the counter number by mask with possible gaps in the sequence byt faster when `getRegnum`.
 * In `-dev` mode fallback to `getRegnum`
 *
 * @function getRegnumLoosely
 * @memberOf ubs_numcounter_ns.prototype
 * @memberOfModule @unitybase/ubs
 * @param {string} regKeyValue Registration key mask
 * @param {number} [startNum=0] The starting counter value in case mask not exists
 * @returns {number} Next number for this mask
 */
ubs_numcounter.getRegnumLoosely = function (regKeyValue, startNum = 0) {
  let res
  App.enterCriticalSection(REGNUM_CS)
  try {
    let cachedVal = App.memCacheGet('ubs_numcounter.getRegnumLoosely:' + regKeyValue)
    if (!cachedVal || (+cachedVal % REGNUM_CACHE_SIZE === 0)) {
      // incr DB counter value in separate transaction
      App.runInAutonomousTransaction(function getNumCounterAutonomous () {
        cachedVal = getCurrentCounterAndIncrBy(regKeyValue, startNum, REGNUM_CACHE_SIZE)
      })
    }
    res = parseInt(cachedVal, 10) + 1
    App.memCachePut('ubs_numcounter.getRegnumLoosely:' + regKeyValue, '' + res)
  } finally {
    App.leaveCriticalSection(REGNUM_CS)
  }
  return res
}

/**
 * Increment counter in DB (or create new if not exists) and return an INITIAL value
 *
 * @private
 * @param {string} regKeyValue
 * @param {number} startNum
 * @param {number} incr
 * @returns {number}
 */
function getCurrentCounterAndIncrBy (regKeyValue, startNum, incr) {
  let res
  let counterInData = -1
  // check number mask exist in ubs_numcounter
  let store = UB.Repository('ubs_numcounter')
    .attrs(['ID'])
    .where('[regKey]', '=', regKeyValue)
    .select()

  // if mask not exists - add it
  if (store.eof) {
    counterInData = startNum > 0 ? startNum - 1 : startNum
    res = store.run('insert', {
      execParams: {
        regKey: regKeyValue,
        counter: counterInData + incr // insert incremented counter value
      }
    })
    if (!res) throw store.lastError
  } else {
    // in case mask exist
    const IDInData = store.get('ID')
    // lock it for update
    store.run('update', {
      __skipSelectBeforeUpdate: true,
      execParams: {
        ID: IDInData,
        fakeLock: 1
      }
    })
    // retrieve current number
    store = UB.Repository('ubs_numcounter')
      .attrs(['ID', 'counter'])
      .where('ID', '=', IDInData)
      .select()
    // increment counter
    // If counter is not aligned to incr, for example: counter = 107  incr = 100, next returned number is 207
    // client will stop on 300, and we do not get an intersection, but loose numbers 109-207 - this is ok, since method is loosely
    counterInData = store.get('counter')
    if ((incr !== 1) && (counterInData % incr !== 0)) {
      // counter not aligned (maybe used in lossless mode before)
      // align to incr, for example: counter = 107, incr = 100 -> new value is 200
      incr = incr - (counterInData % incr)
    }
    // and update an incremented counter value
    // We can safely use `__skipSelectBeforeUpdate: true` because entity is not audited
    res = store.run('update', {
      __skipSelectBeforeUpdate: true,
      execParams: {
        ID: IDInData,
        counter: counterInData + incr,
        fakeLock: 0
      }
    })
    if (!res) throw store.lastError
  }
  return counterInData
}

/**
 * Generate auto incremental code for specified entity attribute in case
 * attribute value in execParams is empty or equal to attribute default value,
 * specified in meta file.
 *
 * Will create a numcounter with code === entity.name and 1 as initial value.
 *
 * Result value will be left padded by '0' to the length specified in ubs_settings
 * To be used in `insert:before` handler as
 *
 * @example
cdn_profession.on('insert:before', generateAutoIncrementalCode)

function generateAutoIncrementalCode (ctx) {
  ubs_numcounter.generateAutoIncrementalCode(ctx, 'code')
}
//or even simple if attribute name is `code`
cdn_profession.on('insert:before', ubs_numcounter.generateAutoIncrementalCode)
 * @function generateAutoIncrementalCode
 * @memberOf ubs_numcounter_ns.prototype
 * @memberOfModule @unitybase/ubs
 * @param {ubMethodParams} ctx
 * @param {string} ctx.mParams.entity
 * @param {TubList|object} ctx.mParams.execParams
 * @param {string} [forAttribute='code'] Code of attribute for number generation
 */
ubs_numcounter.generateAutoIncrementalCode = function (ctx, forAttribute = 'code') {
  const mParams = ctx.mParams
  const execParams = mParams.execParams
  if (!execParams) return
  const entityCode = mParams.entity
  const ubEntity = App.domainInfo.get(entityCode)
  const attr = ubEntity.attributes[forAttribute]
  const newAttrValue = execParams[forAttribute]
  if (execParams && (!newAttrValue || (attr.defaultValue && attr.defaultValue === execParams[forAttribute]))) {
    const padTo = ubs_settings.loadKey('ubs.numcounter.autoIncrementalCodeLen', 6)
    const newNum = '' + ubs_numcounter.getRegnumLoosely(entityCode, 1)
    execParams[forAttribute] = newNum.padStart(padTo, '0')
  }
}
/**
 * Get counter value by registration key. If both `skipReservedNumber` and `loosely` is true - use loosely non-blocking algo
 *
 * @function getRegnumCounter
 * @memberOf ubs_numcounter_ns.prototype
 * @memberOfModule @unitybase/ubs
 * @published
 * @param {ubMethodParams} ctx
 * @param {string} ctx.mParams.execParams.regkey
 * @param {boolean} ctx.mParams.execParams.skipReservedNumber Skip loading number from reserve and calculate new number by mask
 * @param {boolean} [ctx.mParams.execParams.loosely=false] If true and skipReservedNumber is true - use loosely non-blocking algo
 * @returns {boolean}
 */
ubs_numcounter.getRegnumCounter = function (ctx) {
  // RegKey caller pass to method
  const execParams = ctx.mParams.execParams
  const upregKey = execParams.regkey
  const skipReservedNumber = execParams.skipReservedNumber || false
  if (skipReservedNumber && execParams.loosely) {
    ctx.mParams.getRegnumCounter = ubs_numcounter.getRegnumLoosely(upregKey, 1)
  } else {
    ctx.mParams.getRegnumCounter = ubs_numcounter.getRegnum(upregKey, 1, skipReservedNumber)
  }
  return true
}