const { getBuiltinPermissions } = require("@devlego/backend-core/permissions")
const {
  isBuiltin,
  getDBRoleID,
  getExternalRoleID,
  getBuiltinRoles,
  checkForRoleResourceArray,
} = require("@devlego/backend-core/roles")
const { getRoleParams } = require("../../db/utils")
const {
  CURRENTLY_SUPPORTED_LEVELS,
  getBasePermissions,
} = require("../../utilities/security")
const { removeFromArray } = require("../../utilities")
const { getAppDB } = require("@devlego/backend-core/context")

const PermissionUpdateType = {
  REMOVE: "remove",
  ADD: "add",
}

const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS

// utility function to stop this repetition - permissions always stored under roles
async function getAllDBRoles(db) {
  const body = await db.allDocs(
    getRoleParams(null, {
      include_docs: true,
    })
  )
  return body.rows.map(row => row.doc)
}

async function updatePermissionOnRole(
  appId,
  { roleId, resourceId, level },
  updateType
) {
  const db = getAppDB()
  const remove = updateType === PermissionUpdateType.REMOVE
  const isABuiltin = isBuiltin(roleId)
  const dbRoleId = getDBRoleID(roleId)
  const dbRoles = await getAllDBRoles(db)
  const docUpdates = []

  // the permission is for a built in, make sure it exists
  if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
    const builtin = getBuiltinRoles()[roleId]
    builtin._id = getDBRoleID(builtin._id)
    dbRoles.push(builtin)
  }

  // now try to find any roles which need updated, e.g. removing the
  // resource from another role and then adding to the new role
  for (let role of dbRoles) {
    let updated = false
    const rolePermissions = role.permissions ? role.permissions : {}
    // make sure its an array, also handle migrating
    if (
      !rolePermissions[resourceId] ||
      !Array.isArray(rolePermissions[resourceId])
    ) {
      rolePermissions[resourceId] =
        typeof rolePermissions[resourceId] === "string"
          ? [rolePermissions[resourceId]]
          : []
    }
    // handle the removal/updating the role which has this permission first
    // the updating (role._id !== dbRoleId) is required because a resource/level can
    // only be permitted in a single role (this reduces hierarchy confusion and simplifies
    // the general UI for this, rather than needing to show everywhere it is used)
    if (
      (role._id !== dbRoleId || remove) &&
      rolePermissions[resourceId].indexOf(level) !== -1
    ) {
      removeFromArray(rolePermissions[resourceId], level)
      updated = true
    }
    // handle the adding, we're on the correct role, at it to this
    if (!remove && role._id === dbRoleId) {
      const set = new Set(rolePermissions[resourceId])
      rolePermissions[resourceId] = [...set.add(level)]
      updated = true
    }
    // handle the update, add it to bulk docs to perform at end
    if (updated) {
      role.permissions = rolePermissions
      docUpdates.push(role)
    }
  }

  const response = await db.bulkDocs(docUpdates)
  return response.map(resp => {
    resp._id = getExternalRoleID(resp.id)
    delete resp.id
    return resp
  })
}

exports.fetchBuiltin = function (ctx) {
  ctx.body = Object.values(getBuiltinPermissions())
}

exports.fetchLevels = function (ctx) {
  // for now only provide the read/write perms externally
  ctx.body = SUPPORTED_LEVELS
}

exports.fetch = async function (ctx) {
  const db = getAppDB()
  const roles = await getAllDBRoles(db)
  let permissions = {}
  // create an object with structure role ID -> resource ID -> level
  for (let role of roles) {
    if (!role.permissions) {
      continue
    }
    const roleId = getExternalRoleID(role._id)
    for (let [resource, levelArr] of Object.entries(role.permissions)) {
      const levels = Array.isArray(levelArr) ? [levelArr] : levelArr
      const perms = {}
      levels.forEach(level => (perms[level] = roleId))
      permissions[resource] = perms
    }
  }
  // apply the base permissions
  const finalPermissions = {}
  for (let [resource, permission] of Object.entries(permissions)) {
    const basePerms = getBasePermissions(resource)
    finalPermissions[resource] = Object.assign(basePerms, permission)
  }
  ctx.body = finalPermissions
}

exports.getResourcePerms = async function (ctx) {
  const resourceId = ctx.params.resourceId
  const db = getAppDB()
  const body = await db.allDocs(
    getRoleParams(null, {
      include_docs: true,
    })
  )
  const roles = body.rows.map(row => row.doc)
  let permissions = {}
  for (let level of SUPPORTED_LEVELS) {
    // update the various roleIds in the resource permissions
    for (let role of roles) {
      const rolePerms = checkForRoleResourceArray(role.permissions, resourceId)
      if (
        rolePerms &&
        rolePerms[resourceId] &&
        rolePerms[resourceId].indexOf(level) !== -1
      ) {
        permissions[level] = getExternalRoleID(role._id)
      }
    }
  }
  ctx.body = Object.assign(getBasePermissions(resourceId), permissions)
}

exports.addPermission = async function (ctx) {
  ctx.body = await updatePermissionOnRole(
    ctx.appId,
    ctx.params,
    PermissionUpdateType.ADD
  )
}

exports.removePermission = async function (ctx) {
  ctx.body = await updatePermissionOnRole(
    ctx.appId,
    ctx.params,
    PermissionUpdateType.REMOVE
  )
}
