const _ = require('lodash/fp')

const db = require('../db')
const configManager = require('./config-manager')
const logger = require('../logger')
const schema = require('./lamassu-schema.json')

const REMOVED_FIELDS = ['crossRefVerificationActive', 'crossRefVerificationThreshold']

const SETTINGS_LOADER_SCHEMA_VERSION = 1

function allScopes (cryptoScopes, machineScopes) {
  const scopes = []
  cryptoScopes.forEach(c => {
    machineScopes.forEach(m => scopes.push([c, m]))
  })

  return scopes
}

function allCryptoScopes (cryptos, cryptoScope) {
  const cryptoScopes = []

  if (cryptoScope === 'global' || cryptoScope === 'both') cryptoScopes.push('global')
  if (cryptoScope === 'specific' || cryptoScope === 'both') cryptos.forEach(r => cryptoScopes.push(r))

  return cryptoScopes
}

function allMachineScopes (machineList, machineScope) {
  const machineScopes = []

  if (machineScope === 'global' || machineScope === 'both') machineScopes.push('global')
  if (machineScope === 'specific' || machineScope === 'both') machineList.forEach(r => machineScopes.push(r))

  return machineScopes
}

function satisfiesRequire (config, cryptos, machineList, field, anyFields, allFields) {
  const fieldCode = field.code

  const scopes = allScopes(
    allCryptoScopes(cryptos, field.cryptoScope),
    allMachineScopes(machineList, field.machineScope)
  )

  return scopes.every(scope => {
    const isAnyEnabled = () => _.some(refField => {
      return isScopeEnabled(config, cryptos, machineList, refField, scope)
    }, anyFields)

    const areAllEnabled = () => _.every(refField => {
      return isScopeEnabled(config, cryptos, machineList, refField, scope)
    }, allFields)

    const isBlank = _.isNil(configManager.scopedValue(scope[0], scope[1], fieldCode, config))
    const isRequired = (_.isEmpty(anyFields) || isAnyEnabled()) &&
      (_.isEmpty(allFields) || areAllEnabled())
    const hasDefault = !_.isNil(_.get('default', field))

    const isValid = !isRequired || !isBlank || hasDefault

    return isValid
  })
}

function isScopeEnabled (config, cryptos, machineList, refField, scope) {
  const [cryptoScope, machineScope] = scope
  const candidateCryptoScopes = cryptoScope === 'global'
    ? allCryptoScopes(cryptos, refField.cryptoScope)
    : [cryptoScope]

  const candidateMachineScopes = machineScope === 'global'
    ? allMachineScopes(machineList, refField.machineScope)
    : [ machineScope ]

  const allRefCandidateScopes = allScopes(candidateCryptoScopes, candidateMachineScopes)
  const getFallbackValue = scope => configManager.scopedValue(scope[0], scope[1], refField.code, config)
  const values = allRefCandidateScopes.map(getFallbackValue)

  return values.some(r => r)
}

function getCryptos (config, machineList) {
  const scopes = allScopes(['global'], allMachineScopes(machineList, 'both'))
  const scoped = scope => configManager.scopedValue(scope[0], scope[1], 'cryptoCurrencies', config)
  return scopes.reduce((acc, scope) => _.union(acc, scoped(scope)), [])
}

function getGroup (fieldCode) {
  return _.find(group => _.includes(fieldCode, group.fields), schema.groups)
}

function getField (fieldCode) {
  const group = getGroup(fieldCode)
  return getGroupField(group, fieldCode)
}

function getGroupField (group, fieldCode) {
  const field = _.find(_.matchesProperty('code', fieldCode), schema.fields)
  return _.merge(_.pick(['cryptoScope', 'machineScope'], group), field)
}

// Note: We can't use machine-loader because it relies on settings-loader,
// which relies on this
function getMachines () {
  return db.any('select device_id from devices')
}

function fetchMachines () {
  return getMachines()
    .then(machineList => machineList.map(r => r.device_id))
}

function validateFieldParameter (value, validator) {
  switch (validator.code) {
    case 'required':
      return true // We don't validate this here
    case 'min':
      return value >= validator.min
    case 'max':
      return value <= validator.max
    default:
      throw new Error('Unknown validation type: ' + validator.code)
  }
}

function ensureConstraints (config) {
  const pickField = fieldCode => schema.fields.find(r => r.code === fieldCode)

  return Promise.resolve()
    .then(() => {
      config.every(fieldInstance => {
        const fieldCode = fieldInstance.fieldLocator.code
        if (_.includes(fieldCode, REMOVED_FIELDS)) return

        const field = pickField(fieldCode)
        if (!field) {
          logger.warn('No such field: %s, %j', fieldCode, fieldInstance.fieldLocator.fieldScope)
          return
        }

        const fieldValue = fieldInstance.fieldValue

        const isValid = field.fieldValidation
          .every(validator => validateFieldParameter(fieldValue.value, validator))

        if (isValid) return true
        throw new Error('Invalid config value')
      })
    })
}

function validateRequires (config) {
  return fetchMachines()
    .then(machineList => {
      const cryptos = getCryptos(config, machineList)

      return schema.groups.filter(group => {
        return group.fields.some(fieldCode => {
          const field = getGroupField(group, fieldCode)

          if (!field.fieldValidation.find(r => r.code === 'required')) return false

          const refFieldsAny = _.map(_.partial(getField, group), field.enabledIfAny)
          const refFieldsAll = _.map(_.partial(getField, group), field.enabledIfAll)
          const isInvalid = !satisfiesRequire(config, cryptos, machineList, field, refFieldsAny, refFieldsAll)

          return isInvalid
        })
      })
    })
    .then(arr => arr.map(r => r.code))
}

function validate (config) {
  return Promise.resolve()
    .then(() => ensureConstraints(config))
    .then(() => validateRequires(config))
    .then(arr => {
      if (arr.length === 0) return config
      throw new Error('Invalid configuration:' + arr)
    })
}

module.exports = {
  SETTINGS_LOADER_SCHEMA_VERSION,
  validate,
  ensureConstraints,
  validateRequires
}
