require('dotenv').config()
const CveId = require('../../model/cve-id')
const logger = require('../../middleware/logger')
const getConstants = require('../../constants').getConstants
const errors = require('./error')
const error = new errors.CveIdControllerError()

// Called by GET /api/cve-id
async function getFilteredCveId (req, res, next) {
  const CONSTANTS = getConstants()

  // temporary measure to allow tests to work after fixing #920
  // tests required changing the global limit to force pagination
  if (req.TEST_PAGINATOR_LIMIT) {
    CONSTANTS.PAGINATOR_OPTIONS.limit = req.TEST_PAGINATOR_LIMIT
  }

  const options = CONSTANTS.PAGINATOR_OPTIONS
  options.sort = { owning_cna: 'asc', cve_id: 'asc' }

  try {
    const orgShortName = req.ctx.org
    let state
    let year
    const timeReserved = {
      timeStamp: [],
      dateOperator: []
    }
    const timeModified = {
      timeStamp: [],
      dateOperator: []
    }
    options.page = req.ctx.query.page ? parseInt(req.ctx.query.page) : CONSTANTS.PAGINATOR_PAGE // if 'page' query parameter is not defined, set 'page' to the default page value
    const cveIdRepo = req.ctx.repositories.getCveIdRepository()
    const orgRepo = req.ctx.repositories.getOrgRepository()
    const userRepo = req.ctx.repositories.getUserRepository()
    const isSecretariat = await orgRepo.isSecretariat(orgShortName)
    const isBulkDownload = await orgRepo.isBulkDownload(orgShortName)

    // Create map of orgUUID to shortnames and users to simplify aggregation later
    const orgs = await orgRepo.getAllOrgs()
    const users = await userRepo.getAllUsers()

    const orgMap = {}
    orgs.forEach(org => {
      orgMap[org.UUID] = { shortname: org.short_name, users: {} }
    })

    users.forEach(user => {
      if (!orgMap[user.org_UUID]) {
        orgMap[user.org_UUID] = { shortname: `MISSING ORG ${user.org_UUID}`, users: {} }
      }
      orgMap[user.org_UUID].users[user.UUID] = user.username
    })

    Object.keys(req.ctx.query).forEach(k => {
      const key = k.toLowerCase()

      if (key === 'state') {
        state = req.ctx.query.state
      } else if (key === 'cve_id_year') {
        year = req.ctx.query.cve_id_year
      } else if (key === 'time_reserved.lt') {
        timeReserved.dateOperator.push('lt')
        timeReserved.timeStamp.push(req.ctx.query['time_reserved.lt'])
      } else if (key === 'time_reserved.gt') {
        timeReserved.dateOperator.push('gt')
        timeReserved.timeStamp.push(req.ctx.query['time_reserved.gt'])
      } else if (key === 'time_modified.lt') {
        timeModified.dateOperator.push('lt')
        timeModified.timeStamp.push(req.ctx.query['time_modified.lt'])
      } else if (key === 'time_modified.gt') {
        timeModified.dateOperator.push('gt')
        timeModified.timeStamp.push(req.ctx.query['time_modified.gt'])
      }
    })

    const query = {
      state: { $ne: CONSTANTS.CVE_STATES.AVAILABLE }
    }

    // Secretariat and BulkDownload get results for all CNAs
    if (!(isSecretariat || isBulkDownload)) {
      query.owning_cna = await orgRepo.getOrgUUID(orgShortName)
    }

    if (year) {
      query.cve_year = year
    }

    if (state) {
      query.state = state
    }

    if (timeReserved.timeStamp.length > 0) {
      query.reserved = {}

      for (let i = 0; i < timeReserved.timeStamp.length; i++) {
        if (timeReserved.dateOperator[i] === 'lt') {
          query.reserved.$lt = timeReserved.timeStamp[i]
        } else {
          query.reserved.$gt = timeReserved.timeStamp[i]
        }
      }
    }

    if (timeModified.timeStamp.length > 0) {
      query['time.modified'] = {}

      for (let i = 0; i < timeModified.timeStamp.length; i++) {
        if (timeModified.dateOperator[i] === 'lt') {
          query['time.modified'].$lt = timeModified.timeStamp[i]
        } else {
          query['time.modified'].$gt = timeModified.timeStamp[i]
        }
      }
    }

    const agt = setMinAggregateObj(query)
    const pg = await cveIdRepo.aggregatePaginate(agt, options)
    const payload = {
      cve_ids: pg.itemsList.map((i) => {
        const cnaid = i.requested_by.cna
        i.requested_by.cna = orgMap[cnaid].shortname
        i.requested_by.user = orgMap[cnaid].users[i.requested_by.user]
        i.owning_cna = orgMap[i.owning_cna].shortname
        return i
      })
    }

    if (pg.itemCount >= CONSTANTS.PAGINATOR_OPTIONS.limit) {
      payload.totalCount = pg.itemCount
      payload.itemsPerPage = pg.itemsPerPage
      payload.pageCount = pg.pageCount
      payload.currentPage = pg.currentPage
      payload.prevPage = pg.prevPage
      payload.nextPage = pg.nextPage
    }

    logger.info({ uuid: req.ctx.uuid, message: 'The CVEs were sent to the user.' })
    return res.status(200).json(payload)
  } catch (err) {
    next(err)
  }
}

// Called by POST /api/cve-id
async function reserveCveId (req, res, next) {
  const requester = req.ctx.user
  const orgShortName = req.ctx.org
  let hasLock = false
  let batchType
  let amount
  let shortName
  let year
  const orgRepo = req.ctx.repositories.getOrgRepository()

  try {
    Object.keys(req.ctx.query).forEach(k => {
      const key = k.toLowerCase()

      if (key === 'amount') {
        amount = req.ctx.query.amount
      } else if (key === 'batch_type') {
        batchType = req.ctx.query.batch_type
      } else if (key === 'short_name') {
        shortName = req.ctx.query.short_name
      } else if (key === 'cve_year') {
        year = req.ctx.query.cve_year
      }
    })

    const isSecretariat = await orgRepo.isSecretariat(orgShortName)
    if (orgShortName !== shortName && !isSecretariat) {
      return res.status(403).json(error.orgCannotReserveForOther())
    }

    if (!shortName) {
      return res.status(400).json(error.noOrgShortName())
    }

    if (!year) {
      return res.status(400).json(error.noYear())
    }

    if (!amount) {
      return res.status(400).json(error.noAmount())
    }

    if (amount <= 0) {
      return res.status(400).json(error.invalidAmount())
    }

    if (amount > 1 && !batchType) {
      return res.status(400).json(error.noBatchType())
    }

    const result = await orgRepo.findOneByShortName(shortName)
    if (!result) {
      logger.info({ uuid: req.ctx.uuid, message: shortName + ' organization does not exist.' })
      return res.status(403).json(error.orgDne(shortName, 'short_name', 'query'))
    }

    const payload = await getPayload(req, result)
    if (amount > payload.available) {
      return res.status(403).json(error.overIdQuota(payload))
    }

    hasLock = await orgRepo.findOneAndUpdate({ short_name: shortName, inUse: false }, { $set: { inUse: true } }, { new: true }) // set lock for org
    if (!hasLock) {
      return res.status(403).json(error.reservationInProgress())
    }

    if (batchType === undefined) { // priority
      await priorityReservation(year, amount, shortName, orgShortName, requester, payload.available, res, req)
    } else if (batchType === 'sequential') {
      await sequentialReservation(year, amount, shortName, orgShortName, requester, payload.available, false, res, req)
    } else if (batchType === 'non-sequential' || batchType === 'nonsequential') {
      if (amount > getConstants().NONSEQUENTIAL_MAX_AMOUNT) {
        return res.status(403).json(error.overNonSequentialMaxAmount())
      }
      await nonSequentialReservation(year, amount, shortName, orgShortName, requester, payload.available, res, req)
    } else {
      return res.status(400).json(error.invalidBatchType())
    }
  } catch (err) {
    next(err)
  } finally {
    if (shortName && hasLock) {
      await orgRepo.findOneAndUpdate({ short_name: shortName, inUse: true }, { $set: { inUse: false } }, { new: true }) // release lock for org
    }
  }
}

/* Expected behavior by role:
Secretariat: can retrieve full information for all CVE IDs
Regular, CNA & Admin Users: Retrieve full information for a CVE ID owned by their organization
Unauthenticated users along with Regular, CNA & Admin users requesting ids not owned by their organization retrieve
  partial information as follows:
    requested_by not included for a CVE ID in the “PUBLISHED” or “REJECTED” state
    owning_org not included for ids in the RESERVED state
*/
async function getCveId (req, res, next) {
  const CONSTANTS = getConstants()

  try {
    const auth = req.ctx.authenticated
    const id = req.ctx.params.id
    const cveIdRepo = req.ctx.repositories.getCveIdRepository()
    const orgRepo = req.ctx.repositories.getOrgRepository()

    const agt = setAggregateObj({ cve_id: id })
    let result = await cveIdRepo.aggregate(agt)
    result = result.length > 0 ? result[0] : null

    if (!result || result.state === CONSTANTS.CVE_STATES.AVAILABLE) {
      return res.status(404).json(error.cveIdNotFound(id))
    }

    let finalResult = {}
    let loggerUuid = 'unauthenticated-user'
    let orgShortName = ''
    let orgUUID = null
    let isSecretariat = false

    if (auth) {
      loggerUuid = req.ctx.uuid
      orgShortName = req.ctx.org
      orgUUID = await orgRepo.getOrgUUID(orgShortName) // orgShortName is not null
      isSecretariat = await orgRepo.isSecretariatUUID(orgUUID)
    }

    // Secretariat and owning org are allowed to see complete results
    if (isSecretariat || (orgShortName === result.owning_cna)) {
      finalResult = result
    } else { // otherwise, remove Requested by information, and redact owning_cna for RESERVED ids
      finalResult = {
        cve_id: result.cve_id,
        cve_year: result.cve_year,
        state: result.state
      }
      if (result.state === CONSTANTS.CVE_STATES.RESERVED) {
        finalResult.owning_cna = '[REDACTED]'
      } else {
        finalResult.owning_cna = result.owning_cna
        finalResult.dateUpdated = result.time.modified
      }
    }

    logger.info({ uuid: loggerUuid, message: id + ' record was sent to the user.', cveId: finalResult })
    return res.status(200).json(finalResult)
  } catch (err) {
    next(err)
  }
}

// Called by PUT /cve-id/:id
async function modifyCveId (req, res, next) {
  try {
    const id = req.ctx.params.id
    let state
    let newOrgShortName
    let orgUUID
    const orgRepo = req.ctx.repositories.getOrgRepository()
    const cveIdRepo = req.ctx.repositories.getCveIdRepository()
    const userRepo = req.ctx.repositories.getUserRepository()
    const cveRepo = req.ctx.repositories.getCveRepository()
    const org = await orgRepo.findOneByShortName(req.ctx.org)

    // Get remaining org quota
    const totalReserved = await cveIdRepo.countDocuments({ owning_cna: org.UUID, state: 'RESERVED' })
    const remainingQuota = (org.policies.id_quota - totalReserved)

    // Check for existing record - await only allowed at top level so cannot
    // move inside of it statement below
    const cve = await cveRepo.findOneByCveId(id)
    const queryKeys = Object.keys(req.ctx.query)
    // Object.keys(req.ctx.query).forEach(k => {
    for (let i = 0; i < queryKeys.length; i++) {
      const k = queryKeys[i]
      const key = k.toLowerCase()

      // Ok to change owning_cna if there is an existing record, but not state
      if (key === 'state') {
        if (cve) {
          return res.status(403).json(error.cannotChangeCveIdWithRecord(id))
        }
        state = req.ctx.query.state

        // Don't allow state transition to RESERVED if org has no remaining quota
        if (state === 'RESERVED' && remainingQuota <= 0) {
          return res.status(403).json(error.modifyCveIdNoQuota())
        }
      } else if (key === 'org') {
        newOrgShortName = req.ctx.query.org
      }
    }
    // })

    let result = await cveIdRepo.findOneByCveId(id)
    if (!result) {
      return res.status(404).json(error.cveIdNotFound(id))
    }

    const cveId = JSON.parse(JSON.stringify(result)) // clone cve id returned from the db
    delete cveId._id
    delete cveId.time

    if (state) {
      cveId.state = state
    }

    if (newOrgShortName) {
      orgUUID = await orgRepo.getOrgUUID(newOrgShortName)

      if (!orgUUID) {
        logger.info({ uuid: req.ctx.uuid, message: id + ' could not be reassigned to ' + newOrgShortName + ' in MongoDB because it does not exist.' })
        return res.status(404).json(error.orgDne(newOrgShortName, 'org', 'query'))
      }

      cveId.owning_cna = orgUUID
    }

    await cveIdRepo.findOneAndUpdate({ cve_id: id }, cveId) // update cve id
    const agt = setAggregateObj({ cve_id: id })
    result = await cveIdRepo.aggregate(agt)
    result = result.length > 0 ? result[0] : null

    const responseMessage = {
      message: id + ' was successfully updated.',
      updated: result
    }

    const payload = {
      action: 'update_cveid',
      change: id + ' was successfully updated.',
      req_UUID: req.ctx.uuid,
      org_UUID: await orgRepo.getOrgUUID(req.ctx.org),
      cve_id: result
    }
    payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID)
    logger.info(JSON.stringify(payload))
    return res.status(200).json(responseMessage)
  } catch (err) {
    next(err)
  }
}

// Called by POST /cve-id-range/:year
async function createCveIdRange (req, res, next) {
  try {
    const CONSTANTS = getConstants()
    const year = req.ctx.params.year
    const cveIdRangeRepo = req.ctx.repositories.getCveIdRangeRepository()
    const orgRepo = req.ctx.repositories.getOrgRepository()
    const userRepo = req.ctx.repositories.getUserRepository()
    const result = await cveIdRangeRepo.findOne({ cve_year: year })

    if (result) {
      logger.info({ uuid: req.ctx.uuid, message: 'CVE Id Range document for year ' + year + ' already exists.' })
      return res.status(400).json(error.yearRangeExists(year))
    }

    const defaultDoc = CONSTANTS.DEFAULT_CVE_ID_RANGE
    defaultDoc.cve_year = year
    await cveIdRangeRepo.findOneAndUpdate({ cve_year: year }, defaultDoc, { upsert: true })
    const payload = {
      action: 'create_cveIdRange',
      change: 'CVE Id Range document for year ' + year + ' was created.',
      req_UUID: req.ctx.uuid,
      org_UUID: await orgRepo.getOrgUUID(req.ctx.org)
    }
    payload.user_UUID = await userRepo.getUserUUID(req.ctx.user, payload.org_UUID)

    logger.info(JSON.stringify(payload))
    return res.status(200).send()
  } catch (err) {
    next(err)
  }
}

async function priorityReservation (year, amount, shortName, orgShortName, requester, availableIds, res, req) {
  const CONSTANTS = getConstants()
  const cveIdRangeRepo = req.ctx.repositories.getCveIdRangeRepository()
  const reqUUID = req.ctx.uuid
  let result = await cveIdRangeRepo.findOne({ cve_year: year })
  let isFull = false

  // Cve Id Range for 'year' does not exists
  if (!result) {
    logger.info({ uuid: req.ctx.uuid, message: 'CVE IDs for year ' + year + ' cannot be reserved at this time.' })
    res.header(CONSTANTS.QUOTA_HEADER, availableIds)
    return res.status(403).json(error.cannotReserveForYear(year))
  }

  const endRange = parseInt(result.ranges.priority.end)
  result = await cveIdRangeRepo.findOneAndUpdate({ $and: [{ cve_year: year }, { 'ranges.priority.top_id': { $lt: endRange } }] }, { $inc: { 'ranges.priority.top_id': amount } }, { new: true })

  // priority id block is full, reserve id in sequential block
  if (!result) {
    isFull = true
    logger.info({ uuid: req.ctx.uuid, message: 'Priority id block is full for year ' + year + ', reserving id in sequential block.' })
    await sequentialReservation(year, amount, shortName, orgShortName, requester, availableIds, true, res, req)
  }

  if (!isFull) {
    const orgRepo = req.ctx.repositories.getOrgRepository()
    const userRepo = req.ctx.repositories.getUserRepository()
    const cveIdRepo = req.ctx.repositories.getCveIdRepository()
    const id = generateSequentialIds(year, result.ranges.priority.top_id, amount)
    const owningOrgUUID = await orgRepo.getOrgUUID(shortName)
    const orgUUID = await orgRepo.getOrgUUID(orgShortName)
    const requesterUUID = (await userRepo.findOneByUserNameAndOrgUUID(requester, orgUUID)).UUID

    let cveIdDocuments = []
    const cveIdDocumentsUUID = []
    const cveIdUUID = new CveId()
    const cveId = new CveId()

    cveIdUUID.cve_id = cveId.cve_id = id[0]
    cveIdUUID.cve_year = cveId.cve_year = year
    cveIdUUID.state = cveId.state = 'RESERVED'
    cveIdUUID.owning_cna = owningOrgUUID // the org who gets assigned the reserved CVE IDs
    cveId.owning_cna = shortName
    cveIdUUID.reserved = cveId.reserved = Date.now()
    cveIdUUID.requested_by = {
      cna: orgUUID, // the org who requested the CVE IDs
      user: requesterUUID
    }
    cveId.requested_by = {
      cna: orgShortName, // the org who requested the CVE IDs
      user: requester
    }

    cveIdDocumentsUUID.push(cveIdUUID) // list of cve ids with UUIDs
    cveIdDocuments.push(cveId) // list of cve ids with short name and username

    // Create a CveId document (reserve the CVE ID)
    await cveIdRepo.insertMany(cveIdDocumentsUUID) // reserve a cve id
    cveIdDocuments = cleanOutputSequential(cveIdDocuments)
    const payload = {
      action: 'priority-reservation',
      change: 'A priority CVE ID was reserved for \'' + shortName + '\' organization on behalf of \'' + orgShortName + '\' organization.',
      req_UUID: reqUUID,
      org_UUID: orgUUID,
      user_UUID: requesterUUID,
      amount: amount,
      start: cveIdDocumentsUUID[0].cve_id,
      stop: cveIdDocumentsUUID[cveIdDocumentsUUID.length - 1].cve_id
    }
    logger.info(JSON.stringify(payload))
    const q = availableIds - amount
    res.header(CONSTANTS.QUOTA_HEADER, q)
    return res.status(200).json({ cve_ids: cveIdDocuments, meta: { remaining_quota: q } })
  }
}

async function sequentialReservation (year, amount, shortName, orgShortName, requester, availableIds, isPriority, res, req) {
  const CONSTANTS = getConstants()
  const cveIdRangeRepo = req.ctx.repositories.getCveIdRangeRepository()
  const reqUUID = req.ctx.uuid
  let result = await cveIdRangeRepo.findOne({ cve_year: year })
  let isFull = false

  // Cve Id Range for 'year' does not exists
  if (!result) {
    logger.info({ uuid: req.ctx.uuid, message: 'CVE IDs for year ' + year + ' cannot be reserved at this time.' })
    res.header(CONSTANTS.QUOTA_HEADER, availableIds)
    return res.status(403).json(error.cannotReserveForYear(year))
  }

  const topId = parseInt(result.ranges.general.top_id)
  amount = parseInt(amount)
  result = await cveIdRangeRepo.findOneAndUpdate({ $and: [{ cve_year: year }, { 'ranges.general.end': { $gt: (topId + amount - 1) } }] }, { $inc: { 'ranges.general.top_id': amount } }, { new: true })
  // The cve id block is full for sequential CVE IDs
  if (!result) {
    isFull = true

    if (isPriority) {
      logger.error(JSON.stringify({ message: 'The cve id priority and sequential blocks are full for year ' + year + '. No more priority or sequential ids can be reserved at this time.' }))
    } else {
      logger.error(JSON.stringify({ message: 'The cve id sequential block is full for year ' + year + '. No more sequential ids can be reserved at this time.' }))
    }

    res.header(CONSTANTS.QUOTA_HEADER, availableIds)
    return res.status(403).json(error.yearRangeFull(year))
  }

  if (!isFull) {
    const orgRepo = req.ctx.repositories.getOrgRepository()
    const userRepo = req.ctx.repositories.getUserRepository()
    const cveIdRepo = req.ctx.repositories.getCveIdRepository()
    const ids = generateSequentialIds(year, result.ranges.general.top_id, amount)
    const owningOrgUUID = await orgRepo.getOrgUUID(shortName)
    const orgUUID = await orgRepo.getOrgUUID(orgShortName)
    const requesterUUID = (await userRepo.findOneByUserNameAndOrgUUID(requester, orgUUID)).UUID
    let cveIdDocuments = []
    const cveIdDocumentsUUID = []
    let cveIdUUID
    let cveId

    ids.forEach(id => {
      cveIdUUID = new CveId()
      cveId = new CveId()
      cveIdUUID.cve_id = cveId.cve_id = id
      cveIdUUID.cve_year = cveId.cve_year = year
      cveIdUUID.state = cveId.state = 'RESERVED'
      cveIdUUID.owning_cna = owningOrgUUID // the org who gets assigned the reserved CVE IDs
      cveId.owning_cna = shortName
      cveIdUUID.reserved = cveId.reserved = Date.now()
      cveIdUUID.requested_by = {
        cna: orgUUID, // the org who requested the CVE IDs
        user: requesterUUID
      }
      cveId.requested_by = {
        cna: orgShortName, // the org who requested the CVE IDs
        user: requester
      }

      cveIdDocumentsUUID.push(cveIdUUID) // list of cve ids with UUIDs
      cveIdDocuments.push(cveId) // list of cve ids with short name and username
    })

    // Create multiple CveId documents (reserve the CVE IDs)
    await cveIdRepo.insertMany(cveIdDocumentsUUID) // reserve cve ids
    cveIdDocuments = cleanOutputSequential(cveIdDocuments)
    const payload = {
      action: 'sequential-reservation',
      req_UUID: reqUUID,
      org_UUID: orgUUID,
      user_UUID: requesterUUID,
      amount: amount,
      start: cveIdDocumentsUUID[0].cve_id,
      stop: cveIdDocumentsUUID[cveIdDocumentsUUID.length - 1].cve_id
    }

    if (isPriority) {
      payload.change = 'The cve id priority block is full for year ' + year + '. A sequential CVE ID was reserved for \'' + shortName + '\' organization on behalf of \'' + orgShortName + '\' organization.'
    } else {
      payload.change = 'Sequential CVE IDs were reserved for \'' + shortName + '\' organization on behalf of \'' + orgShortName + '\' organization.'
    }

    logger.info(JSON.stringify(payload))
    const q = availableIds - amount
    res.header(CONSTANTS.QUOTA_HEADER, q)
    return res.status(200).json({ cve_ids: cveIdDocuments, meta: { remaining_quota: q } })
  }
}

async function nonSequentialReservation (year, amount, shortName, orgShortName, requester, availableIds, res, req) {
  const CONSTANTS = getConstants()
  const cveIdRepo = req.ctx.repositories.getCveIdRepository()
  const cveIdRangeRepo = req.ctx.repositories.getCveIdRangeRepository()
  const reqUUID = req.ctx.uuid
  let isFull = false
  let available
  amount = parseInt(amount)
  let availableLimit = Math.max(3 * amount, CONSTANTS.DEFAULT_AVAILABLE_POOL)
  let result = await cveIdRangeRepo.findOne({ cve_year: year })

  // Cve Id Range for 'year' does not exists
  if (!result) {
    logger.info({ uuid: req.ctx.uuid, message: 'CVE IDs for year ' + year + ' cannot be reserved at this time.' })
    res.header(CONSTANTS.QUOTA_HEADER, availableIds)
    return res.status(403).json(error.cannotReserveForYear(year))
  }

  available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit }) // get available ids

  // Case 1: Not enough IDs in the 'AVAILABLE' pool
  if (available.length < availableLimit) {
    result = await incrementNonSequentialPool(availableLimit, result.ranges.general.end, result.ranges.general.top_id, amount, available.length, year, cveIdRangeRepo)
    isFull = result.isFull

    if (isFull) {
      logger.error(JSON.stringify({ message: 'The cve id non-sequential block is full for year ' + year + '. No more sequential ids can be reserved at this time.' }))
      res.header(CONSTANTS.QUOTA_HEADER, availableIds)
      return res.status(403).json(error.yearRangeFull(year))
    }

    await allocateAvailableCveIds(result.ids, year, req) // Pool was incremented. Create 'AVAILABLE' cve ids.
    available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit }) // get available ids
  }

  // Case 2: Enough IDs in the 'AVAILABLE' pool
  const orgRepo = req.ctx.repositories.getOrgRepository()
  const userRepo = req.ctx.repositories.getUserRepository()
  let index
  let counter = 0
  const cveIdDocuments = []
  const cveIdDocumentsUUID = []
  const owningOrgUUID = await orgRepo.getOrgUUID(shortName)
  const orgUUID = await orgRepo.getOrgUUID(orgShortName)
  const requesterUUID = (await userRepo.findOneByUserNameAndOrgUUID(requester, orgUUID)).UUID
  const q = availableIds - amount

  while ((counter < amount) && !isFull) {
    index = getRandomInt(0, available.length) // get random index in the available array
    result = await reserveNonSequentialCveId(index, available, year, shortName, orgShortName, requester, owningOrgUUID, orgUUID, requesterUUID, cveIdRepo)

    if (result.isReserved) {
      logger.info({ uuid: req.ctx.uuid, message: result.cveId.cve_id + ' was reserved' })
      cveIdDocuments.push(result.cveId) // add reserved cve id to the array of reserved cve ids
      cveIdDocumentsUUID.push(result.cveIdUUID) // add reserved cve id UUID to the array of reserved cve ids
      available.splice(index, 1) // remove reserved cve id from the 'AVAILABLE' pool
      counter++
    } else {
      available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit }) // get available ids
      availableLimit = Math.max(3 * (amount - counter), CONSTANTS.DEFAULT_AVAILABLE_POOL) // recalculate the available limit since some ids might have been reserved

      // Case 1: Not enough IDs in the 'AVAILABLE' pool
      if (available.length < availableLimit) {
        result = await cveIdRangeRepo.findOne({ cve_year: year })
        result = await incrementNonSequentialPool(availableLimit, result.ranges.general.end, result.ranges.general.top_id, amount - counter, available.length, year, cveIdRangeRepo)
        isFull = result.isFull

        if (isFull) {
          logger.info({ uuid: req.ctx.uuid, message: 'Only ' + counter + ' cve ids were reserved because there are not enough ids in the CVE ID non-sequential block. Non-sequential CVE IDs were reserved for \'' + shortName + '\' organization on behalf of \'' + orgShortName + '\' organization.', cve_ids: cveIdDocumentsUUID.map((doc) => { return doc.cve_id }) })
          res.header(CONSTANTS.QUOTA_HEADER, q)
          return res.status(206).json(error.reservedPartialAmount(counter, cveIdDocuments), { meta: { remaining_quota: q } })
        }

        await allocateAvailableCveIds(result.ids, year, req) // Pool was incremented. Create 'AVAILABLE' cve ids.
        available = await cveIdRepo.find({ cve_year: year, state: 'AVAILABLE' }, { limit: availableLimit }) // get available ids
      }
    }
  }

  const reservedCveIds = cveIdDocumentsUUID.map((doc) => {
    return doc.cve_id
  })

  if (!isFull) {
    const payload = {
      action: 'nonsequential-reservation',
      change: 'Non-sequential CVE IDs were reserved for \'' + shortName + '\' organization on behalf of \'' + orgShortName + '\' organization.',
      req_UUID: reqUUID,
      org_UUID: orgUUID,
      user_UUID: requesterUUID,
      amount: amount,
      cve_ids: reservedCveIds
    }

    logger.info(JSON.stringify(payload))
    res.header(CONSTANTS.QUOTA_HEADER, q)
    return res.status(200).json({ cve_ids: cveIdDocuments, meta: { remaining_quota: q } })
  }
}

async function incrementNonSequentialPool (availableLimit, end, top, amount, available, year, cveIdRR) {
  let increment = availableLimit - available + amount // the amount is the increment amount
  const cveIdRangeRepo = cveIdRR
  const endRange = parseInt(end)
  const topId = parseInt(top)

  // Cap increment to end of general CVE ID block
  if (endRange < (topId + increment)) {
    increment = endRange - topId
  }

  const result = {
    isFull: false,
    ids: null
  }

  if (increment > 0) {
    let r = await cveIdRangeRepo.findOneAndUpdate({ $and: [{ cve_year: year }, { 'ranges.general.end': { $gte: increment } }] }, { $inc: { 'ranges.general.top_id': increment } }, { new: true })

    // Cap increment because it went over end of general block
    if (r.ranges.general.top_id > endRange) {
      increment = increment - (r.ranges.general.top_id - endRange) // adjusting real increment
      r = await cveIdRangeRepo.findOneAndUpdate({ cve_year: year }, { $set: { 'ranges.general.top_id': endRange } }, { new: true }) // Cap top_id to end of the block
    }

    if (increment > 0) {
      result.ids = generateSequentialIds(year, r.ranges.general.top_id, increment)
    } else if (available < amount) { // You hit the end of the boundary
      result.isFull = true
    }
  } else if (available < amount) { // You hit the end of the boundary
    result.isFull = true
  }

  return result
}

async function allocateAvailableCveIds (ids, year, req) {
  if (ids) {
    const cveIdRepo = req.ctx.repositories.getCveIdRepository()
    const cveIdDocuments = []
    let cveId

    ids.forEach(id => {
      cveId = new CveId()
      cveId.cve_id = id
      cveId.cve_year = year
      cveId.state = 'AVAILABLE'
      cveId.owning_cna = 'N/A' // the org who gets assigned the reserved CVE IDs
      cveId.reserved = Date.now()
      cveId.requested_by = {
        cna: 'N/A', // the org who requested the CVE IDs
        user: 'N/A'
      }

      cveIdDocuments.push(cveId)
    })

    await cveIdRepo.insertMany(cveIdDocuments) // Save the 'AVAILABLE' cve ids
    logger.info({ uuid: req.ctx.uuid, message: 'The AVAILABLE pool was incremented by ' + cveIdDocuments.length + ' cve ids.' })
  }
}

async function reserveNonSequentialCveId (index, available, year, shortName, orgShortName, requester, owningOrgUUID, orgUUID, requesterUUID, cveIdR) {
  // Update available Cve Id
  const cveIdUUID = new CveId()
  const cveId = new CveId()
  const cveIdRepo = cveIdR
  cveIdUUID.cve_id = cveId.cve_id = available[index].cve_id
  cveIdUUID.cve_year = cveId.cve_year = year
  cveIdUUID.state = cveId.state = 'RESERVED'
  cveIdUUID.owning_cna = owningOrgUUID
  cveId.owning_cna = shortName // the org who gets assigned the reserved CVE IDs
  cveIdUUID.reserved = cveId.reserved = Date.now()
  cveIdUUID.requested_by = {
    cna: orgUUID, // the org who requested the CVE IDs
    user: requesterUUID
  }
  cveId.requested_by = {
    cna: orgShortName, // the org who requested the CVE IDs
    user: requester
  }

  const result = {
    cveId: cveId,
    cveIdUUID: cveIdUUID,
    isReserved: await cveIdRepo.findOneAndUpdate({ cve_id: cveIdUUID.cve_id, state: 'AVAILABLE' }, cveIdUUID, { new: true })
  }

  return result
}

async function getPayload (req, org) {
  const payload = {
    id_quota: org.policies.id_quota
  }
  const cveIdRepo = req.ctx.repositories.getCveIdRepository()

  const result = await cveIdRepo.countDocuments({ owning_cna: org.UUID, state: 'RESERVED' })
  payload.total_reserved = result
  payload.available = (payload.id_quota - payload.total_reserved)

  return payload
}

function cleanOutputSequential (cveIdDocuments) {
  cveIdDocuments.forEach((obj) => {
    const cveId = JSON.parse(JSON.stringify(obj))
    delete cveId.time

    if (cveId.__v !== undefined) {
      delete cveId.__v
    }
  })

  return cveIdDocuments
}

function generateSequentialIds (year, topId, amount) {
  const start = topId - amount + 1 // before the pool 'AVAILABLE' pool increment
  const end = topId // after the 'AVAILABLE' pool increment
  const ids = []

  for (let i = start; i < end + 1; i++) {
    ids.push('CVE-' + year + '-' + String(i).padStart(4, '0'))
  }

  return ids
}

function getRandomInt (min, max) {
  min = Math.ceil(min)
  max = Math.floor(max)

  return Math.floor(Math.random() * (max - min) + min) // The maximum is exclusive and the minimum is inclusive
}

function setAggregateObj (query) {
  return [
    {
      $match: query
    },
    {
      $lookup: {
        from: 'Org',
        localField: 'owning_cna',
        foreignField: 'UUID',
        as: 'ownerCna'
      }
    },
    {
      $unwind: '$ownerCna'
    },
    {
      $project: {
        _id: false,
        cve_id: true,
        cve_year: true,
        state: true,
        owning_cna: '$ownerCna.short_name',
        requested_by: true,
        reserved: true,
        time: true
      }
    },
    {
      $lookup: {
        from: 'User',
        localField: 'requested_by.user',
        foreignField: 'UUID',
        as: 'result'
      }
    },
    {
      $unwind: '$result'
    },
    {
      $project: {
        cve_id: true,
        cve_year: true,
        state: true,
        owning_cna: true,
        'requested_by.cna': true,
        'requested_by.user': '$result.username',
        reserved: true,
        time: true
      }
    },
    {
      $lookup: {
        from: 'Org',
        localField: 'requested_by.cna',
        foreignField: 'UUID',
        as: 'result'
      }
    },
    {
      $unwind: '$result'
    },
    {
      $project: {
        cve_id: true,
        cve_year: true,
        state: true,
        owning_cna: true,
        'requested_by.cna': '$result.short_name',
        'requested_by.user': true,
        reserved: true,
        time: true
      }
    }
  ]
}

// Smaller aggregation used in getFilteredCveId function
function setMinAggregateObj (query) {
  return [
    {
      $match: query
    },

    {
      $project: {
        _id: false,
        __v: false
      }
    }
  ]
}

module.exports = {
  CVEID_GET_FILTER: getFilteredCveId,
  CVEID_RESERVE: reserveCveId,
  CVEID_GET_SINGLE: getCveId,
  CVEID_UPDATE_SINGLE: modifyCveId,
  CVEID_RANGE_CREATE: createCveIdRange
}
