const {
  getApisWithFullAccess,
  getApisForSession,
  getMasterApisWithFullAccess,
} = require("./budibaseApi")
const getDatastore = require("./datastore")
const getDatabaseManager = require("./databaseManager")
const { $, splitKey } = require("@budibase/core").common
const { keyBy, values } = require("lodash/fp")
const {
  masterAppPackage,
  applictionVersionPackage,
  applictionVersionPublicPaths,
} = require("../utilities/createAppPackage")
const { determineVersionId } = require("./runtimePackages")

const isMaster = appname => appname === "_master"

module.exports = async context => {
  const { config } = context
  const datastoreModule = getDatastore(config)

  const databaseManager = getDatabaseManager(
    datastoreModule,
    config.datastoreConfig
  )

  const masterDatastore = datastoreModule.getDatastore(
    databaseManager.masterDatastoreConfig
  )

  const bb = await getMasterApisWithFullAccess(context)

  let applications
  const loadApplications = async () => {
    const apps = await bb.indexApi.listItems("/all_applications")
    applications = $(apps, [keyBy("name")])
  }
  await loadApplications()

  const getInstanceDatastore = instanceDatastoreConfig =>
    datastoreModule.getDatastore(instanceDatastoreConfig)

  const getCustomSessionId = (appname, sessionId) =>
    isMaster(appname)
      ? bb.recordApi.customId("mastersession", sessionId)
      : bb.recordApi.customId("session", sessionId)

  const getApplication = async (nameOrKey, isRetry = false) => {
    if (applications[nameOrKey]) return applications[nameOrKey]

    for (let name in applications) {
      const a = applications[name]
      if (a.key === nameOrKey) return a
      if (a.id === nameOrKey) return a
    }

    if (isRetry) return

    await loadApplications()

    return await getApplication(nameOrKey, true)
  }

  const getSession = async (sessionId, appname) => {
    const customSessionId = getCustomSessionId(appname, sessionId)
    if (isMaster(appname)) {
      return await bb.recordApi.load(`/sessions/${customSessionId}`)
    } else {
      const app = await getApplication(appname)
      return await bb.recordApi.load(
        `/applications/${app.id}/sessions/${customSessionId}`
      )
    }
  }

  const deleteSession = async (sessionId, appname) => {
    const customSessionId = getCustomSessionId(appname, sessionId)
    if (isMaster(appname)) {
      return await bb.recordApi.delete(`/sessions/${customSessionId}`)
    } else {
      const app = await getApplication(appname)
      return await bb.recordApi.delete(
        `/applications/${app.id}/sessions/${customSessionId}`
      )
    }
  }

  const createAppUser = async (appname, instance, user, password) => {
    if (isMaster(appname)) {
      throw new Error("This method is for creating app users - not on master!")
    }

    const versionId = determineVersionId(instance.version)
    const dsConfig = JSON.parse(instance.datastoreconfig)
    const appPackage = await applictionVersionPackage(
      context,
      appname,
      versionId,
      instance.key
    )

    const bbInstance = await getApisWithFullAccess(
      datastoreModule.getDatastore(dsConfig),
      appPackage
    )

    await bbInstance.authApi.createUser(user, password)
  }

  const authenticate = async (
    sessionId,
    appname,
    username,
    password,
    instanceName = "default"
  ) => {
    if (isMaster(appname)) {
      const authUser = await bb.authApi.authenticate(username, password)
      if (!authUser) {
        return null
      }

      const session = bb.recordApi.getNew("/sessions", "mastersession")
      bb.recordApi.setCustomId(session, sessionId)
      session.user_json = JSON.stringify(authUser)
      session.username = username
      await bb.recordApi.save(session)
      return session
    }

    const app = await getApplication(appname)

    const userInMaster = await getUser(app.id, username)
    if (!userInMaster) return null

    const instance = await bb.recordApi.load(userInMaster.instance.key)

    const versionId = determineVersionId(instance.version)

    const dsConfig = JSON.parse(instance.datastoreconfig)
    const appPackage = await applictionVersionPackage(
      context,
      appname,
      versionId,
      instance.key
    )
    const bbInstance = await getApisWithFullAccess(
      datastoreModule.getDatastore(dsConfig),
      appPackage
    )

    const authUser = await bbInstance.authApi.authenticate(username, password)

    if (!authUser) {
      return null
    }

    const session = bb.recordApi.getNew(
      `/applications/${app.id}/sessions`,
      "session"
    )
    bb.recordApi.setCustomId(session, sessionId)
    session.user_json = JSON.stringify(authUser)
    session.instanceDatastoreConfig = instance.datastoreconfig
    session.instanceKey = instance.key
    session.username = username
    session.instanceVersion = instance.version.key
    await bb.recordApi.save(session)
    return session
  }

  const getInstanceApiForSession = async (appname, sessionId) => {
    if (isMaster(appname)) {
      const customId = bb.recordApi.customId("mastersession", sessionId)
      const masterPkg = masterAppPackage(context)
      try {
        const session = await bb.recordApi.load(`/sessions/${customId}`)
        return {
          instance: await getApisForSession(
            masterDatastore,
            masterAppPackage(context),
            session
          ),
          publicPath: masterPkg.mainUiPath,
          sharedPath: masterPkg.sharedPath,
        }
      } catch (_) {
        return {
          instance: null,
          publicPath: masterPkg.unauthenticatedUiPath,
          sharedPath: masterPkg.sharedPath,
        }
      }
    } else {
      const app = await getApplication(appname)
      const customId = bb.recordApi.customId("session", sessionId)
      try {
        const session = await bb.recordApi.load(
          `/applications/${app.id}/sessions/${customId}`
        )
        const dsConfig = JSON.parse(session.instanceDatastoreConfig)
        const instanceDatastore = getInstanceDatastore(dsConfig)

        const versionId = determineVersionId(session.instanceVersion)

        const appPackage = await applictionVersionPackage(
          context,
          appname,
          versionId,
          session.instanceKey
        )

        return {
          instance: await getApisForSession(
            instanceDatastore,
            appPackage,
            session
          ),
          publicPath: appPackage.mainUiPath,
          sharedPath: appPackage.sharedPath,
        }
      } catch (_) {
        const versionId = determineVersionId(app.defaultVersion)
        const appPublicPaths = applictionVersionPublicPaths(
          context,
          app.name,
          versionId
        )
        return {
          instance: null,
          publicPath: appPublicPaths.unauthenticatedUiPath,
          sharedPath: appPublicPaths.sharedPath,
        }
      }
    }
  }

  const getUser = async (appId, username) => {
    const userId = bb.recordApi.customId("user", username)
    try {
      return await bb.recordApi.load(`/applications/${appId}/users/${userId}`)
    } catch (_) {
      //empty
      return
    }
  }

  const getFullAccessInstanceApiForUsername = async (appname, username) => {
    if (isMaster(appname)) {
      return bb
    } else {
      const app = await getApplication(appname)
      const user = await getUser(app.id, username)

      if (!user) return null

      const dsConfig = JSON.parse(user.instance.datastoreconfig)
      const instanceDatastore = getInstanceDatastore(dsConfig)

      const versionId = determineVersionId(
        (await bb.recordApi.load(user.instance.key)).version
      )

      const appPackage = await applictionVersionPackage(
        context,
        appname,
        versionId,
        user.instance.key
      )

      return await getApisWithFullAccess(instanceDatastore, appPackage)
    }
  }

  const removeSessionsForUser = async (appname, username) => {
    if (isMaster(appname)) {
      const sessions = await bb.indexApi.listItems("/mastersessions_by_user", {
        rangeStartParams: { username },
        rangeEndParams: { username },
        searchPhrase: `username:${username}`,
      })

      for (let session of sessions) {
        await bb.recordApi.delete(session.key)
      }
    } else {
      const app = await getApplication(appname)
      const sessions = await bb.indexApi.listItems(
        `/applications/${app.id}/sessions_by_user`,
        {
          rangeStartParams: { username },
          rangeEndParams: { username },
          searchPhrase: `username:${username}`,
        }
      )

      for (let session of sessions) {
        await bb.recordApi.delete(session.key)
      }
    }
  }

  const disableUser = async (app, username) => {
    await removeSessionsForUser(app.name, username)
    const userInMaster = await getUser(app.id, username)
    userInMaster.active = false
    await bb.recordApi.save(userInMaster)
  }

  const enableUser = async (app, username) => {
    const userInMaster = await getUser(app.id, username)
    userInMaster.active = true
    await bb.recordApi.save(userInMaster)
  }

  const listApplications = () => values(applications)

  return {
    getApplication,
    getSession,
    deleteSession,
    authenticate,
    getInstanceApiForSession,
    getFullAccessInstanceApiForUsername,
    removeSessionsForUser,
    disableUser,
    enableUser,
    getUser,
    createAppUser,
    bbMaster: bb,
    listApplications,
  }
}