const Model = require('objection').Model
const path = require('path')
const fs = require('fs-extra')
const _ = require('lodash')
const yaml = require('js-yaml')
const commonHelper = require('../helpers/common')

/* global WIKI */

/**
 * Storage model
 */
module.exports = class Storage extends Model {
  static get tableName() { return 'storage' }
  static get idColumn() { return 'key' }

  static get jsonSchema () {
    return {
      type: 'object',
      required: ['key', 'isEnabled'],

      properties: {
        key: {type: 'string'},
        isEnabled: {type: 'boolean'},
        mode: {type: 'string'}
      }
    }
  }

  static get jsonAttributes() {
    return ['config', 'state']
  }

  static async getTargets() {
    return WIKI.models.storage.query()
  }

  static async refreshTargetsFromDisk() {
    let trx
    try {
      const dbTargets = await WIKI.models.storage.query()

      // -> Fetch definitions from disk
      const storageDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/storage'))
      let diskTargets = []
      for (let dir of storageDirs) {
        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/storage', dir, 'definition.yml'), 'utf8')
        diskTargets.push(yaml.safeLoad(def))
      }
      WIKI.data.storage = diskTargets.map(target => ({
        ...target,
        isAvailable: _.get(target, 'isAvailable', false),
        props: commonHelper.parseModuleProps(target.props)
      }))

      // -> Insert new targets
      let newTargets = []
      for (let target of WIKI.data.storage) {
        if (!_.some(dbTargets, ['key', target.key])) {
          newTargets.push({
            key: target.key,
            isEnabled: false,
            mode: target.defaultMode || 'push',
            syncInterval: target.schedule || 'P0D',
            config: _.transform(target.props, (result, value, key) => {
              _.set(result, key, value.default)
              return result
            }, {}),
            state: {
              status: 'pending',
              message: '',
              lastAttempt: null
            }
          })
        } else {
          const targetConfig = _.get(_.find(dbTargets, ['key', target.key]), 'config', {})
          await WIKI.models.storage.query().patch({
            config: _.transform(target.props, (result, value, key) => {
              if (!_.has(result, key)) {
                _.set(result, key, value.default)
              }
              return result
            }, targetConfig)
          }).where('key', target.key)
        }
      }
      if (newTargets.length > 0) {
        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)
        for (let target of newTargets) {
          await WIKI.models.storage.query(trx).insert(target)
        }
        await trx.commit()
        WIKI.logger.info(`Loaded ${newTargets.length} new storage targets: [ OK ]`)
      } else {
        WIKI.logger.info(`No new storage targets found: [ SKIPPED ]`)
      }

      // -> Delete removed targets
      for (const target of dbTargets) {
        if (!_.some(WIKI.data.storage, ['key', target.key])) {
          await WIKI.models.storage.query().where('key', target.key).del()
          WIKI.logger.info(`Removed target ${target.key} because it is no longer present in the modules folder: [ OK ]`)
        }
      }
    } catch (err) {
      WIKI.logger.error(`Failed to scan or load new storage providers: [ FAILED ]`)
      WIKI.logger.error(err)
      if (trx) {
        trx.rollback()
      }
    }
  }

  /**
   * Initialize active storage targets
   */
  static async initTargets() {
    this.targets = await WIKI.models.storage.query().where('isEnabled', true).orderBy('key')
    try {
      // -> Stop and delete existing jobs
      const prevjobs = _.remove(WIKI.scheduler.jobs, job => job.name === `sync-storage`)
      if (prevjobs.length > 0) {
        prevjobs.forEach(job => job.stop())
      }

      // -> Initialize targets
      for (let target of this.targets) {
        const targetDef = _.find(WIKI.data.storage, ['key', target.key])
        target.fn = require(`../modules/storage/${target.key}/storage`)
        target.fn.config = target.config
        target.fn.mode = target.mode
        try {
          await target.fn.init()

          // -> Save succeeded init state
          await WIKI.models.storage.query().patch({
            state: {
              status: 'operational',
              message: '',
              lastAttempt: new Date().toISOString()
            }
          }).where('key', target.key)

          // -> Set recurring sync job
          if (targetDef.schedule && target.syncInterval !== `P0D`) {
            WIKI.scheduler.registerJob({
              name: `sync-storage`,
              immediate: false,
              schedule: target.syncInterval,
              repeat: true
            }, target.key)
          }

          // -> Set internal recurring sync job
          if (targetDef.internalSchedule && targetDef.internalSchedule !== `P0D`) {
            WIKI.scheduler.registerJob({
              name: `sync-storage`,
              immediate: false,
              schedule: target.internalSchedule,
              repeat: true
            }, target.key)
          }
        } catch (err) {
          // -> Save initialization error
          await WIKI.models.storage.query().patch({
            state: {
              status: 'error',
              message: err.message,
              lastAttempt: new Date().toISOString()
            }
          }).where('key', target.key)
        }
      }
    } catch (err) {
      WIKI.logger.warn(err)
      throw err
    }
  }

  static async pageEvent({ event, page }) {
    try {
      for (let target of this.targets) {
        await target.fn[event](page)
      }
    } catch (err) {
      WIKI.logger.warn(err)
      throw err
    }
  }

  static async assetEvent({ event, asset }) {
    try {
      for (let target of this.targets) {
        await target.fn[`asset${_.capitalize(event)}`](asset)
      }
    } catch (err) {
      WIKI.logger.warn(err)
      throw err
    }
  }

  static async getLocalLocations({ asset }) {
    const locations = []
    const promises = this.targets.map(async (target) => {
      try {
        const path = await target.fn.getLocalLocation(asset)
        locations.push({
          path,
          key: target.key
        })
      } catch (err) {
        WIKI.logger.warn(err)
      }
    })
    await Promise.all(promises)
    return locations
  }

  static async executeAction(targetKey, handler) {
    try {
      const target = _.find(this.targets, ['key', targetKey])
      if (target) {
        if (_.hasIn(target.fn, handler)) {
          await target.fn[handler]()
        } else {
          throw new Error('Invalid Handler for Storage Target')
        }
      } else {
        throw new Error('Invalid or Inactive Storage Target')
      }
    } catch (err) {
      WIKI.logger.warn(err)
      throw err
    }
  }
}
