import type { FastifyRequest, FastifyReply, FastifyInstance } from "fastify"
import jwt from "@fastify/jwt"
import fastifyPlugin from "fastify-plugin"
import type { User } from "@shared/types"
import configService from "@c/config/configService"
import type { WorkSessionDoc } from "@/corePlugins/raclette__core/backend/datatypes/workSession/workSession.schema"
import {
  ADMIN_SYSTEM_TAG_QUERY,
  type AdminTaggableUser,
  userHasAdminTag,
} from "@shared/helper/adminTag"
import { ForbiddenError } from "@/utils/errors"

const WORK_SESSION_COLLECTION = "raclette__core-worksession"
const WORK_SESSION_HEADER = "x-work-session-id"

/**
 * Middleware: allow either authenticated user (JWT) or valid work session (X-Work-Session-Id).
 * When using this, the route states that it handles permission checks itself (e.g. compare req.workSession with query).
 * - If JWT present and valid: sets req.user (normal auth).
 * - If no/invalid JWT but X-Work-Session-Id present: loads work session, validates (active, not expired),
 *   optionally checks that workSession.pluginData includes the given pluginKey, then sets req.workSession and req.isWorkSession.
 */
export const authenticatedOrWorksession = async (
  req: FastifyRequest,
  res: FastifyReply,
  fastify: FastifyInstance,
  pluginKey: string,
) => {
  fastify.log.debug(
    `[Auth] authenticatedOrWorksession: ${pluginKey} ${req.method} ${req.url}`,
  )

  const { requireAuthentication } = configService.getConfig()?.global ?? {
    requireAuthentication: true,
  }
  const isAdminTagRequired = configService.isAdminTagRequired()

  if (!requireAuthentication) {
    const mockUser: Partial<User> = { _id: "SYSTEM" }
    req.user = mockUser as User
    return
  }

  // 1) Try JWT first
  try {
    await req.jwtVerify()
    const payload = req.user as {
      type?: string
      sessionId?: string
      _id?: string
    }
    fastify.log.debug(
      `[Auth] JWT verified, payload type: ${payload.type || "user"}, sessionId: ${payload.sessionId || "none"}, _id: ${payload._id || "none"}`,
    )

    if (payload.type === "workSession" && payload.sessionId) {
      fastify.log.info(
        `[Auth] Work session JWT detected, sessionId: ${payload.sessionId}, pluginKey: ${pluginKey}`,
      )
      // Work session JWT: load session and validate
      const workSession = await loadAndValidateWorkSession(
        fastify,
        payload.sessionId,
        pluginKey,
      )
      if (!workSession) {
        fastify.log.warn(
          `[Auth] Work session validation failed for sessionId: ${payload.sessionId}`,
        )
        return res
          .status(401)
          .send({ message: "Invalid or expired work session" })
      }
      fastify.log.info(
        `[Auth] Work session authenticated: sessionId=${payload.sessionId}, pluginKey=${pluginKey}`,
      )
      req.workSession = workSession
      req.isWorkSession = true

      // Set projectId from workSession.project if available
      if (workSession.project) {
        req.projectId = workSession.project
        fastify.log.debug(`[Auth] Work session projectId: ${req.projectId}`)
      }
      return
    }
    // Normal user JWT: fetch user and set req.user
    fastify.log.debug(
      `[Auth] Normal user JWT, fetching user: ${(req.user as any)?._id}`,
    )
    const { _id } = req.user
    const adminTag = await fastify.database
      .collection("raclette__core-tag")
      .findOne(ADMIN_SYSTEM_TAG_QUERY)
    const user = await fastify.database
      .collection("raclette__core-user")
      .findOne({ _id }, { projection: { password: 0, resetKey: 0 } })
    if (!user) {
      fastify.log.error("User not found")
      return res.status(401).send({ message: "User not found" })
    }
    const isAdmin = userHasAdminTag(user as AdminTaggableUser, adminTag)
    if (isAdminTagRequired && !isAdmin) {
      fastify.log.error("User not allowed in workbench")
      return res.status(403).send({ message: "User not allowed in workbench" })
    }
    if (isAdmin) user.isAdmin = true
    const account = await fastify.database
      .collection("raclette__core-account")
      .findOne({ _id: user.account })
    user.account = account
    req.user = user
    // Set projectId from user's lastProjectId (for requestData.project in payload wrappers)
    if (user.ux?.lastProjectId) {
      req.projectId = user.ux.lastProjectId
      fastify.log.debug(`[Auth] User projectId: ${req.projectId}`)
    }
    return
  } catch (error: any) {
    // JWT missing or invalid: try work session via header
    fastify.log.debug(
      `[Auth] JWT verification failed: ${error.message}, trying work session header`,
    )
  }

  // 2) Try X-Work-Session-Id header
  const sessionId = req.headers[WORK_SESSION_HEADER] as string | undefined
  if (sessionId) {
    const workSession = await loadAndValidateWorkSession(
      fastify,
      sessionId,
      pluginKey,
    )
    if (workSession) {
      req.workSession = workSession
      req.isWorkSession = true

      // Set projectId from workSession.project if available
      // workaround for how projects/spaces are currently handled - tbd
      if (workSession.project) {
        req.projectId = workSession.project
      }
      return
    } else {
      fastify.log.warn(
        `[Auth] Work session validation failed for header sessionId: ${sessionId}`,
      )
    }
  }

  return res.status(401).send({ message: "Authentication required" })
}

async function loadAndValidateWorkSession(
  fastify: FastifyInstance,
  sessionId: string,
  pluginKey: string,
): Promise<WorkSessionDoc | null> {
  fastify.log.debug(
    `[WorkSession Auth] Loading and validating work session: sessionId=${sessionId}, pluginKey=${pluginKey}`,
  )

  if (!sessionId || typeof sessionId !== "string") {
    fastify.log.warn(`[WorkSession Auth] Invalid sessionId: ${sessionId}`)
    return null
  }

  fastify.log.debug(`[WorkSession Auth] Querying database for work session`)
  const doc = await fastify.database
    .collection(WORK_SESSION_COLLECTION)
    .findOne({ _id: sessionId })

  if (!doc) {
    fastify.log.warn(`[WorkSession Auth] Work session not found: ${sessionId}`)
    return null
  }

  if (!doc.isActive) {
    fastify.log.warn(
      `[WorkSession Auth] Work session is not active: ${sessionId}`,
    )
    return null
  }

  const expiry = doc.expiryDate ? new Date(doc.expiryDate) : null
  if (expiry && expiry.getTime() < Date.now()) {
    fastify.log.warn(
      `[WorkSession Auth] Work session expired: ${sessionId}, expiryDate: ${expiry.toISOString()}`,
    )
    return null
  }

  // Check that this plugin is part of the work session (pluginData keys are "pluginKey:widgetName")
  const pluginData = doc.pluginData || {}
  const pluginKeys = Object.keys(pluginData)

  const hasPlugin =
    pluginKeys.some((k) => k.startsWith(pluginKey + ":")) ||
    Object.prototype.hasOwnProperty.call(pluginData, pluginKey)

  if (!hasPlugin) {
    fastify.log.warn(
      `[WorkSession Auth] Plugin ${pluginKey} not found in work session pluginData: ${pluginKeys.join(", ")}`,
    )
    return null
  }

  fastify.log.info(
    `[WorkSession Auth] Work session validated successfully: sessionId=${sessionId}, pluginKey=${pluginKey}`,
  )
  return doc as WorkSessionDoc
}

export const authenticate = async (
  req: FastifyRequest,
  res: FastifyReply,
  fastify: FastifyInstance,
) => {
  {
    const { requireAuthentication } = configService.getConfig()?.global ?? {
      requireAuthentication: true,
    }
    const isAdminTagRequired = configService.isAdminTagRequired()
    // Skip JWT verification and database queries, if global.requireAuthentication is configured to false
    if (!requireAuthentication) {
      const mockUser: Partial<User> = {
        _id: "SYSTEM",
      }

      req.user = mockUser as User
      return
    }

    try {
      await req.jwtVerify()
      const payload = req.user as { type?: string }
      if (payload.type === "workSession") {
        return res
          .status(401)
          .send({ message: "Work session token not allowed on this route" })
      }
      // fetch current user data from the db
      const { _id } = req.user
      const adminTag = await fastify.database
        .collection("raclette__core-tag")
        .findOne(ADMIN_SYSTEM_TAG_QUERY)
      // IMPORTANT
      // due to some very weird behaviour, mongoose always returns empty results
      // so we are using mongodb directly until we fix this
      const user = await fastify.database
        .collection("raclette__core-user")
        .findOne(
          { _id },
          { projection: { password: 0, resetKey: 0 } }, // Exclude sensitive fields
        )
      if (!user) {
        fastify.log.error("User not found")
        throw new Error("User not found")
      }
      const isAdmin = userHasAdminTag(user as AdminTaggableUser, adminTag)
      if (isAdminTagRequired && !isAdmin) {
        fastify.log.error("User not allowed in workbench")
        throw new Error("User not allowed in workbench")
      }
      if (isAdmin) {
        user.isAdmin = true
      }

      const account = await fastify.database
        .collection("raclette__core-account")
        .findOne({ _id: user.account })

      user.account = account

      req.user = user
    } catch (error) {
      return res.send(error)
    }
  }
}
export const checkPermissions = async (
  req: FastifyRequest,
  res: FastifyReply,
  fastify: FastifyInstance,
  permissions: Array<String>,
) => {
  {
    try {
      if (!permissions.length) {
        fastify.log.error("No permissions given for permission check")
        throw new Error("No Permissions")
      }
      // TODO implement proper Permission lookup here
      permissions.forEach((permissionStr) => {
        if (permissionStr === "user.isAdmin") {
          if (!req.user.isAdmin) {
            throw new ForbiddenError("User not permitted to action")
          }
        }
      })
    } catch (error: any) {
      fastify.log.error(error.message)
      return res.send(error.message)
    }
  }
}
export default fastifyPlugin(async (fastify: FastifyInstance, opts = {}) => {
  // provide some sensible options
  const defaults = {
    secret: false,
    decode: { complete: true },
    sign: {
      iss: "api.raclettejs.info",
      expiresIn: "30d",
    },
    verify: { allowedIss: "api.raclettejs.info" },
  }

  // merge proved opts with defaults
  const mergedOptions = { ...defaults, ...opts }

  /* register the plugin */
  fastify.register(jwt, mergedOptions)

  // decorate the fastify instance, so we can call it anywhere
  fastify.decorate("authenticate", (req, res) => {
    return authenticate(req, res, fastify)
  })

  fastify.decorate(
    "authenticatedOrWorksession",
    (pluginKey: string) => (req: FastifyRequest, res: FastifyReply) =>
      authenticatedOrWorksession(req, res, fastify, pluginKey),
  )
  fastify.decorate(
    "checkPermissions",
    (permissionsArr: Array<String>) =>
      (req: FastifyRequest, res: FastifyReply) =>
        checkPermissions(req, res, fastify, permissionsArr),
  )
})
