/* global ubq_scheduler ncrc32 */
// eslint-disable-next-line camelcase
const me = ubq_scheduler

const fs = require('fs')
const path = require('path')
const LocalDataStore = require('@unitybase/cs-shared').LocalDataStore
const argv = require('@unitybase/base').argv
const UBDomain = require('@unitybase/cs-shared').UBDomain
const UB = require('@unitybase/ub')
const App = UB.App
const _ = require('lodash')

me.entity.addMethod('select')
me.entity.addMethod('estimateCronSchedule')

// here we store loaded schedulers
let resultDataCache = null
const FILE_NAME_TEMPLATE = '_schedulers.json'
const ATTRIBUTES_NAMES = me.entity.getAttributeNames()

const defaultValues = {}
ATTRIBUTES_NAMES.forEach(attrName => {
  const attr = me.entity.attributes[attrName]
  if (attr.defaultValue) {
    defaultValues[attrName] = attr.defaultValue
  }
})

/**
 * Load a schedulers from a file. Override a already loaded schedulers if need
 * @private
 * @param {UBModel} model
 * @param {Array<Object>} loadedData Data already loaded
 */
function loadOneFile (model, loadedData) {
  if (!model.realPath) return // model with public path only
  const fn = path.join(model.realPath, FILE_NAME_TEMPLATE)
  const modelName = model.name

  if (!fs.existsSync(fn)) { return }
  const schedulersEnabled = (!(App.serverConfig.application.schedulers && (App.serverConfig.application.schedulers.enabled === false)))
  const FALSE_CONDITION = 'false //disabled in app config'
  try {
    const content = argv.safeParseJSONfile(fn)
    if (!Array.isArray(content)) {
      console.error('SCHEDULER: invalid config in %. Must be a array ob objects', fn)
      return
    }
    for (let i = 0, L = content.length; i < L; i++) {
      const item = content[i]
      const existedItem = _.find(loadedData, { name: item.name })
      if (!existedItem) { // assign defaults for new items only
        _.defaults(item, defaultValues)
      } else {
        existedItem.originalModel = existedItem.actualModel
      }
      item.actualModel = modelName
      if (!schedulersEnabled) {
        item.schedulingCondition = FALSE_CONDITION
      }
      if (existedItem) { // override
        Object.assign(existedItem, item)
        existedItem.overridden = '1'
      } else {
        item.ID = ncrc32(0, item.name)
        loadedData.push(item)
      }
    }
  } catch (e) {
    console.error('SCHEDULER: Invalid config in %. Error: %. File is ignored', fn, e.toString())
  }
}

function loadAll () {
  const models = App.domainInfo.models
  const loadedData = []

  if (!resultDataCache) {
    console.debug('load schedulers from models directory structure')
    for (const modelName in models) {
      const model = models[modelName]
      loadOneFile(model, loadedData)
    }

    resultDataCache = {
      version: 0,
      fields: ATTRIBUTES_NAMES,
      data: LocalDataStore.arrayOfObjectsToSelectResult(loadedData, ATTRIBUTES_NAMES)
    }
  } else {
    console.debug('ubq_scheduler: already loaded')
  }
  return resultDataCache
}

/**
 * Retrieve data from resultDataCache and init ctx.dataStore
 * caller MUST set dataStore.currentDataName before call doSelect function
 * @private
 * @param {ubMethodParams} ctx
 * @param {UBQL} ctx.mParams ORM query in UBQL format
 */
function doSelect (ctx) {
  const mP = ctx.mParams
  const aID = mP.ID
  const cType = ctx.dataStore.entity.cacheType
  const cachedData = loadAll()

  if (!(aID && (aID > -1)) && (cType === UBDomain.EntityCacheTypes.Entity || cType === UBDomain.EntityCacheTypes.SessionEntity) && (!mP.skipCache)) {
    const reqVersion = mP.version
    mP.version = resultDataCache.version
    if (reqVersion === resultDataCache.version) {
      mP.resultData = {}
      mP.resultData.notModified = true
      return
    }
  }
  const filteredData = LocalDataStore.doFilterAndSort(cachedData, mP)
  // return as asked in fieldList using compact format  {fieldCount: 2, rowCount: 2, values: ["ID", "name", 1, "ss", 2, "dfd"]}
  const resp = LocalDataStore.flatten(mP.fieldList, filteredData.resultData)
  ctx.dataStore.initFromJSON(resp)
}

/**
 * Virtual `select` implementation. Actual data are stored in `_schedulers.json` files from models folders
 * @method select
 * @param {ubMethodParams} ctx
 * @param {UBQL} ctx.mParams ORM query in UBQL format
 * @return {Boolean}
 * @memberOf ubq_scheduler_ns.prototype
 * @memberOfModule @unitybase/ubq
 * @published
 */
me.select = function (ctx) {
  ctx.dataStore.currentDataName = 'select' // do we need it????
  doSelect(ctx)
  return true // everything is OK
}

/**
 *  Return next resultCount execution dates for specified cronExpression;
 *  Supports non-standard 7-part cron expression syntax, where last field `@occurrence` mean - fires on every x occurrence
 *  Example - `0 0 1 * * 1 @2` = At 01:00 AM, only on Monday, once per 2 occurrence (every second Monday)
 *
 * @param {string} cronExpression Cron expression; support for non-standard `@occurrence` expression syntax
 * @param {Date} [initialDate] initial date. Default is now
 * @param {number} [resultCount=1]
 * @param {boolean} [isFirstExecution=true] skip oncePer for first execution.
 *   In case today is monday, and we need each second (@2) monday - first occurrence should be today (initialDate should be 00:00:01)
 * @returns {Date[]|Date} if resultCount=1 - return Date, else - array of dates
 */
me.calculateNextCronTerm = function (cronExpression, initialDate, resultCount = 1, isFirstExecution = true) {
  const parser = require('cron-parser') // lazy load inside method because it needed rarely
  let cronDate = initialDate || new Date()
  let cnt = resultCount || 1
  const res = []

  const oncePerIdx = cronExpression.indexOf('@')
  let oncePer = oncePerIdx !== -1 ? parseInt(cronExpression.substring(oncePerIdx + 1), 10) : 1
  if (oncePer < 1) {
    oncePer = 1
  } else if (oncePer > 10) {
    oncePer = 10
  }
  if (oncePerIdx !== -1) {
    cronExpression = cronExpression.substring(0, oncePerIdx)
  }
  const crontab = parser.parseExpression(cronExpression, { currentDate: cronDate })
  while (cnt--) {
    cronDate = crontab.next()
    if (isFirstExecution) { // skip oncePer for first execution. In case today is monday, and we need each second (@2) monday - first occurrence should be today
      isFirstExecution = false
    } else {
      for (let i = 0; i < oncePer - 1; i++) {
        cronDate = crontab.next()
      }
    }
    if (!resultCount || resultCount === 1) {
      return cronDate
    } else {
      res.push(cronDate)
    }
  }
  return res
}

/**
 * Estimate matched dates for cron expression
 * @method select
 * @param {ubMethodParams} ctx
 * @param {object} ctx.mParams
 * @param {string} ctx.mParams.cronExpression
 * @param {string} [ctx.mParams.currentDate] initial date in ISO8601 format
 * @param {number} [ctx.mParams.cnt=1] matched dates count, max = 20
 * @return {Boolean}
 * @memberOf ubq_scheduler_ns.prototype
 * @memberOfModule @unitybase/ubq
 * @published
 */
me.estimateCronSchedule = function (ctx) {
  const expression = ctx.mParams.cronExpression
  const currentDate = ctx.mParams.currentDate
  const cronDate = currentDate ? new Date(currentDate) : new Date()
  let cnt = ctx.mParams.cnt || 1
  if (cnt > 20) cnt = 20
  let res = me.calculateNextCronTerm(expression, cronDate, cnt)
  if (Array.isArray(res)) {
    res = res.map(d => d.toISOString())
  } else {
    res = res.toISOString()
  }
  ctx.mParams.dates = res
  return true // everything is OK
}