import { NextResponse } from "next/server"

import { authRequired, mfaRequired } from "@/constants/routes/authRequired"
import { login } from "@/constants/routes/handleAuth"
import { getInternalNextURL, getPublicNextURL } from "@/server/utils"

import { auth } from "@/auth"
import { findLang } from "@/utils/languages"

import type { NextMiddleware } from "next/server"

import type { MiddlewareMatcher } from "@/types/middleware"

/**
 * AppRouteHandlerFnContext is the context that is passed to the handler as
 * the second argument. This is only done for Route handlers (route.js) and
 * not for middleware. Middleware`s second argument is `event` of type
 * `NextFetchEvent`.
 *
 * Auth.js uses the same pattern for both Route handlers and Middleware,
 * the auth()-wrapper:
 *
 *   auth((req) => { ... })
 *
 * But there is a difference between middleware and route handlers, route
 * handlers get passed a context which middleware do not get (they get a
 * NextFetchEvent instead). Using the same function for both works runtime
 * because Auth.js handles this properly. But fails in typings as the second
 * argument doesn't match for middleware.
 *
 * We want to avoid using ts-expect-error because that hides other errors
 * not related to this typing error and ts-expect-error cannot be scoped either.
 *
 * So we type assert this export to NextMiddleware. The lesser of all evils.
 *
 * https://github.com/nextauthjs/next-auth/blob/3c035ec62f2f21d7cab65504ba83fb1a9a13be01/packages/next-auth/src/lib/index.ts#L265
 * https://authjs.dev/reference/nextjs
 */
export const middleware = auth(async (request) => {
  const lang = findLang(request.nextUrl.pathname)!

  const isLoggedIn = !!request.auth
  const hasError = request.auth?.error

  // Inside auth() we need an internal request for rewrites.
  // @see getInternalNextURL()
  const nextUrlInternal = getInternalNextURL(request)

  const nextUrlPublic = getPublicNextURL(request)

  /**
   * Function to validate MFA from token data
   * @returns boolean
   */
  function isMFAInvalid() {
    const isMFATokenValid = request.auth
      ? request.auth.token.mfa_expires_at > Date.now()
      : false
    return !(request.auth?.token.mfa_scope && isMFATokenValid)
  }
  const isMFAPath = mfaRequired.includes(request.nextUrl.pathname)

  if (isLoggedIn && isMFAPath && isMFAInvalid()) {
    const headers = new Headers(request.headers)
    headers.set("x-returnurl", nextUrlPublic.href)
    headers.set("x-login-source", "mfa")
    return NextResponse.rewrite(new URL(`/${lang}/login`, nextUrlInternal), {
      request: {
        headers,
      },
    })
  }

  if (isLoggedIn && !hasError) {
    const headers = new Headers(request.headers)
    headers.set("x-continue", "1")
    return NextResponse.next({
      headers,
    })
  }

  const headers = new Headers()
  headers.append(
    "set-cookie",
    `redirectTo=${encodeURIComponent(nextUrlPublic.href)}; Path=/; HttpOnly; SameSite=Lax`
  )

  const loginUrl = login[lang]
  const redirectUrl = new URL(loginUrl, nextUrlPublic)
  const redirectOpts = {
    headers,
  }
  console.log(`[authRequired] redirecting to: ${redirectUrl}`, redirectOpts)
  return NextResponse.redirect(redirectUrl, redirectOpts)
}) as NextMiddleware // See comment above

export const matcher: MiddlewareMatcher = (request) => {
  return authRequired.includes(request.nextUrl.pathname)
}
