/**
* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0
**/

var _ = require('lodash')
var Promise = require('bluebird')

var dbapi = require('../../../db/api')
var logger = require('../../../util/logger')
var log = logger.createLogger('api:controllers:devices')

const apiutil = require('../../../util/apiutil')
const lockutil = require('../../../util/lockutil')
const util = require('util')
const uuid = require('uuid')
const wire = require('../../../wire')
const wireutil = require('../../../wire/util')
const wirerouter = require('../../../wire/router')

/* ------------------------------------ PRIVATE FUNCTIONS ------------------------------- */

function filterGenericDevices(req, res, devices) {
  apiutil.respond(res, 200, 'Devices Information', {
    devices: devices.map(function(device) {
      return apiutil.filterDevice(req, device)
    })
  })
}

function getGenericDevices(req, res, loadDevices) {
  loadDevices(req.user.groups.subscribed).then(function(devices) {
    filterGenericDevices(req, res, devices)
  })
  .catch(function(err) {
    apiutil.internalError(res, 'Failed to load device list: ', err.stack)
  })
}

function getDeviceFilteredGroups(serial, fields, bookingOnly) {
  return dbapi.getDeviceGroups(serial).then(function(groups) {
    return Promise.map(groups, function(group) {
      return !bookingOnly || !apiutil.isOriginGroup(group.class) ?
        group :
        'filtered'
    })
    .then(function(groups) {
      return _.without(groups, 'filtered').map(function(group) {
        if (fields) {
          return _.pick(apiutil.publishGroup(group), fields.split(','))
        }
        return apiutil.publishGroup(group)
      })
    })
  })
}

function extractStandardizableDevices(devices) {
  return dbapi.getTransientGroups().then(function(groups) {
    return Promise.map(devices, function(device) {
      return Promise.map(groups, function(group) {
        if (group.devices.indexOf(device.serial) > -1) {
          return Promise.reject('booked')
        }
        return true
      })
      .then(function() {
        return device
      })
      .catch(function(err) {
         if (err !== 'booked') {
          throw err
        }
        return err
      })
    })
    .then(function(devices) {
      return _.without(devices, 'booked')
    })
  })
}

function getStandardizableDevices(req, res) {
  dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
    extractStandardizableDevices(devices).then(function(devices) {
      filterGenericDevices(req, res, devices)
    })
  })
  .catch(function(err) {
    apiutil.internalError(res, 'Failed to load device list: ', err.stack)
  })
}

function removeDevice(serial, req, res) {
  const presentState = req.swagger.params.present.value
  const bookingState = req.swagger.params.booked.value
  const notesState = req.swagger.params.annotated.value
  const controllingState = req.swagger.params.controlled.value
  const anyPresentState = typeof presentState === 'undefined'
  const anyBookingState = typeof bookingState === 'undefined'
  const anyNotesState = typeof notesState === 'undefined'
  const anyControllingState = typeof controllingState === 'undefined'
  const lock = {}

  function deleteGroupDevice(email, id) {
    const lock = {}

    return dbapi.lockGroupByOwner(email, id).then(function(stats) {
      if (!stats.replaced) {
        return apiutil.lightComputeStats(res, stats)
      }
      const group = lock.group = stats.changes[0].new_val

      if (group.devices.indexOf(serial) > -1) {
        return apiutil.isOriginGroup(group.class) ?
          dbapi.removeOriginGroupDevice(group, serial) :
          dbapi.removeGroupDevices(group, [serial])
      }
      return group
    })
    .finally(function() {
      lockutil.unlockGroup(lock)
    })
  }

  function deleteDeviceInDatabase() {
    function wrappedDeleteDeviceInDatabase() {
      const result = {
        status: false
      , data: 'not deleted'
      }

      return dbapi.loadDeviceBySerial(serial).then(function(device) {
        if (device && device.group.id === device.group.origin) {
          return deleteGroupDevice(device.group.owner.email, device.group.id)
            .then(function(group) {
              if (group !== 'not found') {
                return dbapi.deleteDevice(serial).then(function() {
                  result.status = true
                  result.data = 'deleted'
                })
              }
              return false
            })
        }
        return false
      })
      .then(function() {
        return result
      })
    }
    return apiutil.setIntervalWrapper(
      wrappedDeleteDeviceInDatabase
    , 10
    , Math.random() * 500 + 50)
  }

  return dbapi.lockDeviceByOrigin(req.user.groups.subscribed, serial).then(function(stats) {
    if (!stats.replaced) {
      return apiutil.lightComputeStats(res, stats)
    }
    const device = lock.device = stats.changes[0].new_val

    if (!anyPresentState && device.present !== presentState ||
      !anyControllingState && (device.owner === null) === controllingState ||
      !anyNotesState &&
        (typeof device.notes !== 'undefined' && device.notes !== '') !== notesState ||
      !anyBookingState && (device.group.id !== device.group.origin && !bookingState ||
                           device.group.class === apiutil.STANDARD && bookingState)) {
      return 'unchanged'
    }
    if (device.group.class === apiutil.STANDARD) {
      return deleteDeviceInDatabase()
    }
    return dbapi.getDeviceTransientGroups(serial).then(function(groups) {
      if (groups.length && !anyBookingState && !bookingState) {
        return 'unchanged'
      }
      return Promise.each(groups, function(group) {
        return deleteGroupDevice(group.owner.email, group.id)
      })
      .then(function() {
        if (!groups.length && !anyBookingState && bookingState) {
          return 'unchanged'
        }
        return deleteDeviceInDatabase()
      })
    })
  })
  .finally(function() {
    lockutil.unlockDevice(lock)
  })
}

/* ------------------------------------ PUBLIC FUNCTIONS ------------------------------- */

function getDevices(req, res) {
  const target = req.swagger.params.target.value

  switch(target) {
    case apiutil.BOOKABLE:
      getGenericDevices(req, res, dbapi.loadBookableDevices)
      break
    case apiutil.ORIGIN:
      getGenericDevices(req, res, dbapi.loadDevicesByOrigin)
      break
    case apiutil.STANDARD:
      getGenericDevices(req, res, dbapi.loadStandardDevices)
      break
    case apiutil.STANDARDIZABLE:
      getStandardizableDevices(req, res)
      break
    default:
      getGenericDevices(req, res, dbapi.loadDevices)
  }
}

function getDeviceBySerial(req, res) {
  var serial = req.swagger.params.serial.value
  var fields = req.swagger.params.fields.value

  dbapi.loadDevice(req.user.groups.subscribed, serial)
    .then(function(cursor) {
      cursor.next(function(err, device) {
        if (err) {
          return res.status(404).json({
            success: false
          , description: 'Device not found'
          })
        }
        let responseDevice = apiutil.publishDevice(device, req.user)

        if (fields) {
          responseDevice = _.pick(device, fields.split(','))
        }
        res.json({
          success: true
        , device: responseDevice
        })
      })
    })
    .catch(function(err) {
      log.error('Failed to load device "%s": ', serial, err.stack)
      res.status(500).json({
        success: false
      })
    })
}

function getDeviceGroups(req, res) {
  const serial = req.swagger.params.serial.value
  const fields = req.swagger.params.fields.value

  dbapi.loadDevice(req.user.groups.subscribed, serial).then(function(cursor) {
    return cursor.toArray()
  })
  .then(function(devices) {
    if (!devices.length) {
      apiutil.respond(res, 404, 'Not Found (device)')
    }
    else {
      getDeviceFilteredGroups(serial, fields, false)
        .then(function(groups) {
          return apiutil.respond(res, 200, 'Groups Information', {groups: groups})
        })
    }
  })
  .catch(function(err) {
    apiutil.internalError(res, 'Failed to get device groups: ', err.stack)
  })
}

function getDeviceBookings(req, res) {
  const serial = req.swagger.params.serial.value
  const fields = req.swagger.params.fields.value

  dbapi.loadDevice(req.user.groups.subscribed, serial).then(function(cursor) {
    return cursor.toArray()
  })
  .then(function(devices) {
    if (!devices.length) {
      apiutil.respond(res, 404, 'Not Found (device)')
    }
    else {
      getDeviceFilteredGroups(serial, fields, true)
        .then(function(bookings) {
          apiutil.respond(res, 200, 'Bookings Information', {bookings: bookings})
        })
    }
  })
  .catch(function(err) {
    apiutil.internalError(res, 'Failed to get device bookings: ', err.stack)
  })
}

function addOriginGroupDevices(req, res) {
  const serials = apiutil.getBodyParameter(req.body, 'serials')
  const fields = apiutil.getQueryParameter(req.swagger.params.fields)
  const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'
  const lock = {}

  function askUpdateDeviceOriginGroup(group, serial) {
    return new Promise(function(resolve, reject) {
      const signature = util.format('%s', uuid.v4()).replace(/-/g, '')
      let messageListener
      const responseTimer = setTimeout(function() {
        req.options.channelRouter.removeListener(wireutil.global, messageListener)
        apiutil.respond(res, 504, 'Gateway Time-out')
        reject('timeout')
      }, 5000)

      messageListener = wirerouter()
        .on(wire.DeviceOriginGroupMessage, function(channel, message) {
          if (message.signature === signature) {
            clearTimeout(responseTimer)
            req.options.channelRouter.removeListener(wireutil.global, messageListener)
            dbapi.loadDeviceBySerial(serial).then(function(device) {
              if (fields) {
                resolve(_.pick(apiutil.publishDevice(device, req.user), fields.split(',')))
              }
              else {
                resolve(apiutil.publishDevice(device, req.user))
              }
            })
          }
        })
        .handler()

      req.options.channelRouter.on(wireutil.global, messageListener)
      return dbapi.askUpdateDeviceOriginGroup(serial, group, signature)
    })
  }

  function updateDeviceOriginGroup(group, serial) {
    const lock = {}

    return dbapi.lockDeviceByOrigin(req.user.groups.subscribed, serial).then(function(stats) {
      if (!stats.replaced) {
        return apiutil.lightComputeStats(res, stats)
      }
      lock.device = stats.changes[0].new_val

      return dbapi.isUpdateDeviceOriginGroupAllowed(serial, group)
        .then(function(updatingAllowed) {
          if (!updatingAllowed) {
            apiutil.respond(res, 403, 'Forbidden (device is currently booked)')
            return Promise.reject('booked')
          }
          return askUpdateDeviceOriginGroup(group, serial)
        })
    })
    .finally(function() {
      lockutil.unlockDevice(lock)
    })
  }

  function updateDevicesOriginGroup(group, serials) {
    let results = []

    return Promise.each(serials, function(serial) {
      return updateDeviceOriginGroup(group, serial).then(function(result) {
        results.push(result)
      })
    })
    .then(function() {
      const result = target === 'device' ? {device: {}} : {devices: []}

      results = _.without(results, 'unchanged')
      if (!results.length) {
        return apiutil.respond(res, 200, `Unchanged (${target})`, result)
      }
      results = _.without(results, 'not found')
      if (!results.length) {
        return apiutil.respond(res, 404, `Not Found (${target})`)
      }
      if (target === 'device') {
        result.device = results[0]
      }
      else {
        result.devices = results
      }
      return apiutil.respond(res, 200, `Updated (${target})`, result)
    })
    .catch(function(err) {
      if (err !== 'booked' && err !== 'timeout' && err !== 'busy') {
        throw err
      }
    })
  }

  return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
    if (lockingSuccessed) {
      const group = lock.group

      if (!apiutil.isOriginGroup(group.class)) {
        return apiutil.respond(res, 400, 'Bad Request (this group cannot act as an origin one)')
      }
      if (typeof serials !== 'undefined') {
        return updateDevicesOriginGroup(
          group
        , _.without(serials.split(','), '').filter(function(serial) {
            return group.devices.indexOf(serial) < 0
          })
        )
      }
      return dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
        if (group.class === apiutil.BOOKABLE) {
          return devices
        }
        return extractStandardizableDevices(devices)
      })
      .then(function(devices) {
        const serials = []

        devices.forEach(function(device) {
          if (group.devices.indexOf(device.serial) < 0) {
            serials.push(device.serial)
          }
        })
        return updateDevicesOriginGroup(group, serials)
      })
    }
    return false
  })
  .catch(function(err) {
    apiutil.internalError(res, `Failed to update ${target} origin group: `, err.stack)
  })
  .finally(function() {
    lockutil.unlockGroup(lock)
  })
}

function addOriginGroupDevice(req, res) {
  apiutil.redirectApiWrapper('serial', addOriginGroupDevices, req, res)
}

function removeOriginGroupDevices(req, res) {
  const lock = {}

  return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) {
    if (lockingSuccessed) {
      const group = lock.group

      if (!apiutil.checkBodyParameter(req.body, 'serials')) {
        req.body = {serials: group.devices.join()}
      }
      return dbapi.getRootGroup().then(function(group) {
        req.swagger.params.id = {value: group.id}
        return addOriginGroupDevices(req, res)
      })
    }
    return false
  })
  .finally(function() {
    lockutil.unlockGroup(lock)
  })
}

function removeOriginGroupDevice(req, res) {
  apiutil.redirectApiWrapper('serial', removeOriginGroupDevices, req, res)
}

function putDeviceInfoBySerial(req, res) {
  const serial = req.swagger.params.serial.value
  const body = req.swagger.params.device.value.device
  dbapi.loadDeviceBySerial(serial)
    .then((data) => {
      if (!data) {
        return apiutil.respond(res, 404, `Not Found (${serial})`)
      }
      var updates = []
      // Update fields based on given body
      if (_.has(body, 'note')) {
        updates.push(dbapi.setDeviceNote(serial, body.note))
      }
      if (updates.length === 0) {
        return apiutil.respond(res, 400, 'No content to update')
      }
      return Promise.all(updates)
        .then(function() {
            apiutil.respond(res, 200)
        })
    })
    .catch(function(err) {
      apiutil.internalError(res, 'Failed to update device: ', err.stack)
    })
}

function deleteDevices(req, res) {
  const serials = apiutil.getBodyParameter(req.body, 'serials')
  const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices'

  function removeDevices(serials) {
    let results = []

    return Promise.each(serials, function(serial) {
      return removeDevice(serial, req, res).then(function(result) {
        if (result === 'not deleted') {
          apiutil.respond(res, 503, 'Server too busy [code: 2], please try again later')
          return Promise.reject('busy')
        }
        return results.push(result)
      })
    })
    .then(function() {
      results = _.without(results, 'unchanged')
      if (!results.length) {
        return apiutil.respond(res, 200, `Unchanged (${target})`)
      }
      if (!_.without(results, 'not found').length) {
        return apiutil.respond(res, 404, `Not Found (${target})`)
      }
      return apiutil.respond(res, 200, `Deleted (${target})`)
    })
    .catch(function(err) {
      if (err !== 'busy') {
        throw err
      }
    })
  }

  (function() {
    if (typeof serials === 'undefined') {
      return dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) {
        return removeDevices(devices.map(function(device) {
          return device.serial
        }))
      })
    }
    else {
      return removeDevices(_.without(serials.split(','), ''))
    }
  })()
  .catch(function(err) {
    apiutil.internalError(res, `Failed to delete ${target}: `, err.stack)
  })
}

function putDeviceBySerial(req, res) {
  apiutil.redirectApiWrapper('serial', putDeviceInfoBySerial, req, res)
}

function deleteDevice(req, res) {
  apiutil.redirectApiWrapper('serial', deleteDevices, req, res)
}

module.exports = {
  getDevices: getDevices
, putDeviceBySerial: putDeviceBySerial
, getDeviceBySerial: getDeviceBySerial
, getDeviceGroups: getDeviceGroups
, getDeviceBookings: getDeviceBookings
, addOriginGroupDevice: addOriginGroupDevice
, addOriginGroupDevices: addOriginGroupDevices
, removeOriginGroupDevice: removeOriginGroupDevice
, removeOriginGroupDevices: removeOriginGroupDevices
, deleteDevice: deleteDevice
, deleteDevices: deleteDevices
}
