import cluster from 'cluster'

import { Low } from 'lowdb'
import Application from './application.mjs'
import Util from './util.mjs'
import getLog from './log.mjs'
import StaticProvider from './providers/static.mjs'
import GitProvider from './providers/git.mjs'

export default class Core {
  static providers = new Map()
  static addProvider(name, provider) {
    if (!name || typeof(name) !== 'string')
      throw new Error('addProvider name must be a string')
    if (typeof(provider) !== 'function')
      throw new Error(`addProvider ${name} provider must be a class`)

    let test = new provider({})
    if (typeof(test.checkConfig) !== 'function')
      throw new Error(`addProvider ${name} provider class missing checkConfig`)
    if (typeof(test.getLatestVersion) !== 'function')
      throw new Error(`addProvider ${name} provider class missing getLatestVersion`)

    Core.providers.set(name, provider)
  }

  constructor(db, util, log, restart = function() {}) {
    // some sanity checks
    if (!log || typeof(log) !== 'object') throw new Error('log parameter was invalid')
    if (typeof(log.event) !== 'object') throw new Error('log parameter was invalid')
    if (typeof(log.info) !== 'function'
      || typeof(log.warn) !== 'function'
      || typeof(log.error) !== 'function'
      || typeof(log.event.info) !== 'function'
      || typeof(log.event.warn) !== 'function'
      || typeof(log.event.error) !== 'function') throw new Error('log parameter was invalid')
    if (!util || !(util instanceof Util)) throw new Error('util not instance of Util')
    if (!db || !(db instanceof Low)) throw new Error('db not instance of Low')
    if (typeof(restart) !== 'function') throw new Error('restart was not a function')

    this.running = false
    this.db = db
    this.util = util
    this.log = log
    this.restart = restart
    this.applications = []
    this.applicationMap = new Map()
    this._applicationFatalCrash = null
    this.isSlave = cluster.isWorker
  }

  getApplication(name) {
    return this.applicationMap.get(name)
  }

  async init() {
    this.log.info(`Verifying config`)

    this.util.verifyConfig(this.db.config)
    let names = this.util.getAppNames(this.db.config)

    this.log.info(`Found applications: ${names.join(', ')}.`)

    let hasCluster = false

    for (let name of names) {
      if (this.isSlave && process.env.CLUSTER_APP_NAME !== name) {
        continue
      }

      try {
        let provConstructor = Core.providers.get(this.db.config[name].provider)
        let provider = new provConstructor(this.db.config[name])

        if (!this.isSlave) {
          await provider.checkConfig(this.db.config[name])
        }

        let logName = name
        if (this.isSlave && process.env.CLUSTER_APP_INDEX) {
          logName += '-' + process.env.CLUSTER_APP_INDEX
        }

        let application = new Application({
          db: this.db,
          util: this.util,
          log: getLog(logName, this.db.config[name].log || null, { name: name }),
          core: this,
        }, provider, name)
        this.applications.push(application)
        this.applicationMap.set(name, application)

        if (this.db.config[name].cluster) {
          hasCluster = true
        }
      } catch (err) {
        this.log.error(err, `Error creating application ${name} with provider ${this.db.config[name].provider} with config ${JSON.stringify(this.db.config[name])}: ${err.message}`)
      }
    }

    if (hasCluster && !this.isSlave) {
      cluster.on('message', (worker, message) => {
        // Some sanity checking
        if (!message
            || typeof(message) !== 'object'
            || typeof(message.apptarget) !== 'string'
            || typeof(message.type) !== 'string'
            || typeof(message.payload) !== 'object'
            || !message.payload
            ) {
          return
        }

        let app = this.getApplication(message.apptarget)
        let targetLog = null
        if (app) {
          targetLog = app.ctx.log
        } else if (message.apptarget === this.db.config.name) {
          targetLog = this.log
        }

        if (!targetLog) return

        if (message.type === 'newlog') {
          targetLog.emit('newlog', message.payload)
        }
      })
    }

    if (names.length && !this.applications.length) {
      this.log.error('None of the application were successful in running')
      return Promise.reject(new Error('None of the application were successful in running'))
    }
  }

  async run() {
    if (this.running) return
    this.running = true

    if (!this.isSlave) {
      this.log.info(`Running updater on ${this.applications.length} apps`)
      await Promise.all(this.applications.map((app) => {
        return app.update().catch(err => {
          app.ctx.log.error(err, `Error updating: ${err.message}`)
        })
      }))
    }

    let found = false
    
    if (this.isSlave) {
      let app = this.getApplication(process.env.CLUSTER_APP_NAME)
      try {
        await app.runVersion(process.env.CLUSTER_APP_VERSION)
      } catch (err) {
        app.ctx.log.fatal(err)
        return Promise.reject(err)
      }
      return
    }

    for (let app of this.applications) {
      app.startAutoupdater()

      await this.runApplication(app).then(
        () => {
          found = true
        },
        err => {
          app.ctx.log.error(err, `Error running: ${err.message}`)
        }
      )

      app.on('updated', this.runApplication.bind(this, app))
    }

    if (!found) {
      throw new Error('No stable application was found')
    }
  }

  criticalError(application, version, errorCode) {
    application.ctx.log.fatal(`Critical error ${errorCode} running ${version.version}`)
    version.stable = -2
    this.db.writeSync()
  }

  async runApplication(application) {
    let name = application.name
    let found = false

    if (!this.db.data.core[name].versions.length) {
      return Promise.reject(new Error(`No versions were found`))
    }

    for (let i = 0; i < this.db.data.core[name].versions.length; i++) {
      let version = this.db.data.core[name].versions[i]
      if (!version.installed || version.stable < -1) continue
      if (version.stable < 0 && !application.fresh) {
        application.ctx.log.warn(`Restarting for ${version.version} due to last run failing while not being in fresh state`)
        return this.restart(`Application ${name} has fresh false while attempting to run ${version.version} with stable -1`)
      }
      
      await application.closeServer()

      this._applicationFatalCrash = this.criticalError.bind(this, application, version)
      process.once('exit', this._applicationFatalCrash)

      let wasFresh = application.fresh

      try {
        application.ctx.log.info(`Attempting to run version ${version.version}`)
        await application.runVersion(version.version)
        found = true
        version.stable = 1
        await this.db.write()
        application.ctx.log.info(`${version.version} is up and running`)
        break
      } catch(err) {
        application.ctx.log.error(err, `Error starting ${version.version}: ${err.message}`)
        process.off('exit', this._applicationFatalCrash)

        if (version.stable < 1) {
          if (wasFresh) {
            version.stable = -2
          } else {
            version.stable = -1
          }
          await this.db.write()
          if (version.stable === -1) {
            return this.restart(`Application ${name} version ${version.version} failed to start but application was dirty, check if restarting fixes it`)
          }
        } else {
          await this.db.write()
          return this.restart(`Application ${name} version ${version.version} previously stable but now failing`)
        }
      } finally {
        process.off('exit', this._applicationFatalCrash)
      }
    }

    if (!found) {
      return Promise.reject(Error(`No stable versions were found`))
    }
  }
}

Core.addProvider('static', StaticProvider)
Core.addProvider('git', GitProvider)
