import { Cookies, Headers } from "../constants"
import { getCookie, clearCookie, openJwt } from "../utils"
import { getUser } from "../cache/user"
import { getSession, updateSessionTTL } from "../security/sessions"
import { buildMatcherRegex, matches } from "./matchers"
import { SEPARATOR } from "../db/constants"
import { ViewName } from "../db/utils"
import { queryGlobalView } from "../db/views"
import { getGlobalDB, doInTenant } from "../tenancy"
import { decrypt } from "../security/encryption"
const identity = require("../context/identity")
const env = require("../environment")

const ONE_MINUTE = env.SESSION_UPDATE_PERIOD || 60 * 1000

interface FinaliseOpts {
  authenticated?: boolean
  internal?: boolean
  publicEndpoint?: boolean
  version?: string
  user?: any
}

function timeMinusOneMinute() {
  return new Date(Date.now() - ONE_MINUTE).toISOString()
}

function finalise(ctx: any, opts: FinaliseOpts = {}) {
  ctx.publicEndpoint = opts.publicEndpoint || false
  ctx.isAuthenticated = opts.authenticated || false
  ctx.user = opts.user
  ctx.internal = opts.internal || false
  ctx.version = opts.version
}

async function checkApiKey(apiKey: string, populateUser?: Function) {
  if (apiKey === env.INTERNAL_API_KEY) {
    return { valid: true }
  }
  const decrypted = decrypt(apiKey)
  const tenantId = decrypted.split(SEPARATOR)[0]
  return doInTenant(tenantId, async () => {
    const db = getGlobalDB()
    // api key is encrypted in the database
    const userId = await queryGlobalView(
      ViewName.BY_API_KEY,
      {
        key: apiKey,
      },
      db
    )
    if (userId) {
      return {
        valid: true,
        user: await getUser(userId, tenantId, populateUser),
      }
    } else {
      throw "Invalid API key"
    }
  })
}

/**
 * This middleware is tenancy aware, so that it does not depend on other middlewares being used.
 * The tenancy modules should not be used here and it should be assumed that the tenancy context
 * has not yet been populated.
 */
export = (
  noAuthPatterns = [],
  opts: { publicAllowed: boolean; populateUser?: Function } = {
    publicAllowed: false,
  }
) => {
  const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : []
  return async (ctx: any, next: any) => {
    let publicEndpoint = false
    const version = ctx.request.headers[Headers.API_VER]
    // the path is not authenticated
    const found = matches(ctx, noAuthOptions)
    if (found) {
      publicEndpoint = true
    }
    try {
      // check the actual user is authenticated first, try header or cookie
      const headerToken = ctx.request.headers[Headers.TOKEN]
      const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken)
      const apiKey = ctx.request.headers[Headers.API_KEY]
      const tenantId = ctx.request.headers[Headers.TENANT_ID]
      let authenticated = false,
        user = null,
        internal = false
      if (authCookie && !apiKey) {
        const sessionId = authCookie.sessionId
        const userId = authCookie.userId
        let session
        try {
          // getting session handles error checking (if session exists etc)
          session = await getSession(userId, sessionId)
          if (opts && opts.populateUser) {
            user = await getUser(
              userId,
              session.tenantId,
              opts.populateUser(ctx)
            )
          } else {
            user = await getUser(userId, session.tenantId)
          }
          user.csrfToken = session.csrfToken
          if (session?.lastAccessedAt < timeMinusOneMinute()) {
            // make sure we denote that the session is still in use
            await updateSessionTTL(session)
          }
          authenticated = true
        } catch (err: any) {
          authenticated = false
          console.error("Auth Error", err?.message || err)
          // remove the cookie as the user does not exist anymore
          clearCookie(ctx, Cookies.Auth)
        }
      }
      // this is an internal request, no user made it
      if (!authenticated && apiKey) {
        const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
        const { valid, user: foundUser } = await checkApiKey(
          apiKey,
          populateUser
        )
        if (valid && foundUser) {
          authenticated = true
          user = foundUser
        } else if (valid) {
          authenticated = true
          internal = true
        }
      }
      if (!user && tenantId) {
        user = { tenantId }
      } else if (user) {
        delete user.password
      }
      // be explicit
      if (authenticated !== true) {
        authenticated = false
      }
      // isAuthenticated is a function, so use a variable to be able to check authed state
      finalise(ctx, { authenticated, user, internal, version, publicEndpoint })

      if (user && user.email) {
        return identity.doInUserContext(user, next)
      } else {
        return next()
      }
    } catch (err: any) {
      // invalid token, clear the cookie
      if (err && err.name === "JsonWebTokenError") {
        clearCookie(ctx, Cookies.Auth)
      }
      // allow configuring for public access
      if ((opts && opts.publicAllowed) || publicEndpoint) {
        finalise(ctx, { authenticated: false, version, publicEndpoint })
        return next()
      } else {
        ctx.throw(err.status || 403, err)
      }
    }
  }
}
