import type { FastifyInstance } from "fastify"
import type { Socket } from "socket.io"
import { validate } from "uuid"
import { createSystemEventHandlers } from "./createSystemEventHandlers"
import configService from "@c/config/configService"
import { callDataTypeOperation } from "@c/contracting/contractRegistrar"
import { buildWorkSessionJoinPayload } from "./workSessionJoinPayload"

interface JoinPayload {
  roomId?: string
  bootstrap?: boolean
}

interface ProjectResponse {
  _id: string
  [key: string]: any
}

/**
 * Check if socket is authenticated
 */
const isSocketAuthenticated = (
  socket: Socket,
  requiresAuth: boolean,
): boolean => {
  if (!requiresAuth) return true
  return socket.decoded_token !== undefined || socket.isWorkSession === true
}

/**
 * Guard for explicit room joins. Server-assigned readMain joins skip this.
 */
const assertCanJoinProject = async (
  fastify: FastifyInstance,
  socket: Socket,
  spaceId: string,
): Promise<ProjectResponse> => {
  const project = await loadProject(fastify, spaceId)
  if (!project) {
    throw new Error("Space not found")
  }

  // TODO: here we add the guard if a user is allowed to join a specific space or not

  throw new Error("Not allowed to join this space")
}

/**
 * Load project by ID or assign main project when roomId is omitted
 */
const loadProject = async (
  fastify: FastifyInstance,
  projectId?: string,
): Promise<ProjectResponse | null> => {
  try {
    if (projectId) {
      return await callDataTypeOperation(
        "raclette__core",
        "project",
        "read",
        fastify,
        projectId,
      )
    }
    return await callDataTypeOperation(
      "raclette__core",
      "project",
      "readMain",
      fastify,
    )
  } catch (error) {
    fastify.log.warn(
      `Failed to load project: ${error instanceof Error ? error.message : String(error)}`,
    )
    return null
  }
}

/**
 * Load composition for work session
 */
const loadWorkSessionComposition = async (
  fastify: FastifyInstance,
  workSession: any,
): Promise<any | null> => {
  // Use composition from workSession if available
  if (workSession.composition) {
    fastify.log.info(
      `[Socket] Using shared composition from workSession ${workSession._id}`,
    )
    return workSession.composition
  }

  // Fallback: Load via compositionId
  try {
    const composition = await callDataTypeOperation(
      "raclette__core",
      "composition",
      "read",
      fastify,
      workSession.compositionId,
    )

    if (!configService.isAutoSendEnabled("compositions") || !composition) {
      return null
    }

    return composition
  } catch (error) {
    fastify.log.warn(
      `Error loading composition for workSession: ${error instanceof Error ? error.message : String(error)}`,
    )
    return null
  }
}

/**
 * Load user's active work sessions and join their rooms
 */
const joinOwnedWorkSessions = async (
  fastify: FastifyInstance,
  socket: Socket,
  userId: string,
): Promise<void> => {
  const now = new Date()

  try {
    const ownedSessions = await callDataTypeOperation(
      "raclette__core",
      "workSession",
      "readAll",
      fastify,
      {
        userId,
        isActive: true,
      },
    )

    const validSessions = Array.isArray(ownedSessions)
      ? ownedSessions.filter((s: any) => {
          if (!s.expiryDate) return false
          return new Date(s.expiryDate) > now
        })
      : []

    const ownedSessionIds = validSessions.map((s: any) => s._id)
    socket.data.ownedSessions = ownedSessionIds

    ownedSessionIds.forEach((sessionId: string) => {
      socket.join(`session_${sessionId}`)
    })

    if (ownedSessionIds.length > 0) {
      fastify.log.info(
        `[Socket] User ${socket.id} joined ${ownedSessionIds.length} owned session rooms`,
      )
    }
  } catch (error) {
    fastify.log.warn(
      `Error loading owned workSessions: ${error instanceof Error ? error.message : String(error)}`,
    )
  }
}

/**
 * Build response data for authenticated user join
 */
const buildAuthenticatedUserResponse = async (
  fastify: FastifyInstance,
  project: ProjectResponse,
): Promise<any> => {
  const responseData: any = { project }

  if (configService.isAutoSendEnabled("compositions")) {
    responseData.compositions = await callDataTypeOperation(
      "raclette__core",
      "composition",
      "readAll",
      fastify,
      { isDeleted: false },
    )
  }

  if (configService.isAutoSendEnabled("interactionLinks")) {
    responseData.interactionLinks = await callDataTypeOperation(
      "raclette__core",
      "interactionLink",
      "readAll",
      fastify,
      { isDeleted: false },
    )
  }

  if (configService.isAutoSendEnabled("projectConfig")) {
    responseData.projectConfig = {}
  }

  const customData = configService.getCustomSocketData()
  if (customData && typeof customData === "object") {
    Object.entries(customData).forEach(([key, value]) => {
      responseData[key] = value
    })
  }

  return responseData
}

/**
 * Validate work session (UUID, exists, active, not expired, interactionLink exists).
 * Shared by socket auth middleware and handleWorkSessionJoin.
 */
export const validateWorkSession = async (
  fastify: FastifyInstance,
  sessionId: string,
): Promise<any> => {
  fastify.log.debug(
    `[WorkSession Validation] Starting validation for sessionId: ${sessionId}`,
  )

  // Validate UUID format
  if (!validate(sessionId)) {
    fastify.log.warn(
      `[WorkSession Validation] Invalid session ID format: ${sessionId}`,
    )
    throw new Error("Invalid session ID format")
  }

  // Load work session
  const workSession = await callDataTypeOperation(
    "raclette__core",
    "workSession",
    "read",
    fastify,
    sessionId,
  )

  if (!workSession) {
    throw new Error("Work session not found")
  }

  // Validate work session is active
  if (!workSession.isActive) {
    throw new Error("Work session is not active")
  }

  // Validate work session is not expired
  const now = new Date()
  const expiryDate = workSession.expiryDate
    ? new Date(workSession.expiryDate)
    : null

  if (expiryDate && expiryDate <= now) {
    throw new Error("Work session has expired")
  }

  // Validate composition exists
  const composition = await callDataTypeOperation(
    "raclette__core",
    "composition",
    "read",
    fastify,
    workSession.compositionId,
  )

  if (!composition) {
    throw new Error("Composition not found for work session")
  }

  fastify.log.debug(
    `[WorkSession Validation] Validation successful for sessionId: ${sessionId}`,
  )
  return workSession
}

/**
 * Handle work session join
 */
const handleWorkSessionJoin = async (
  fastify: FastifyInstance,
  socket: Socket,
): Promise<void> => {
  const workSession = socket.workSession
  if (!workSession) {
    socket.emit("error", {
      error: { msg: "Work session not found" },
    })
    return
  }

  const sessionId = workSession._id
  fastify.log.info(
    `[WorkSession Join] Handling join for socket: ${socket.id}, sessionId: ${sessionId}`,
  )

  // Re-validate work session (similar to login validating credentials)
  try {
    fastify.log.debug(
      `[WorkSession Join] Re-validating work session before join`,
    )
    const validatedSession = await validateWorkSession(fastify, sessionId)
    // Use validated session (in case it was updated)
    Object.assign(workSession, validatedSession)
    fastify.log.debug(`[WorkSession Join] Work session validated successfully`)
  } catch (error: any) {
    fastify.log.error(
      `[WorkSession Join] Work session validation failed: ${error.message}, stack: ${error.stack}`,
    )
    socket.emit("error", {
      error: { msg: error.message || "Work session validation failed" },
    })
    return
  }

  fastify.log.debug(
    `[WorkSession Join] Joining socket to room: session_${sessionId}`,
  )
  socket.join(`session_${sessionId}`)
  socket.data.session = sessionId

  fastify.log.debug(`[WorkSession Join] Loading composition for work session`)
  const composition = await loadWorkSessionComposition(fastify, workSession)
  if (!composition) {
    fastify.log.warn(
      `[WorkSession Join] No composition available for session: ${sessionId}`,
    )
    socket.emit("error", {
      error: { msg: "No composition available for this session" },
    })
    return
  }
  fastify.log.debug(`[WorkSession Join] Composition loaded`)

  const joinPayloadData = buildWorkSessionJoinPayload(workSession, composition)

  // Create JWT token for work session (analogous to login token creation)
  const now = new Date()
  fastify.log.info(
    `[WorkSession Join] Creating JWT token for work session: ${sessionId}`,
  )
  const tokenPayload = {
    type: "workSession",
    sessionId: sessionId,
    userId: workSession.userId,
    permissions: workSession.permissions || 4, // Default to read-only (4)
    iat: Math.floor(Date.now() / 1000),
  }

  fastify.log.debug(
    `[WorkSession Join] Token payload: ${JSON.stringify(tokenPayload)}`,
  )

  // Token expires when work session expires (similar to login using config expiry)
  const expiryDate = workSession.expiryDate
    ? new Date(workSession.expiryDate)
    : null
  const expiresInSeconds = expiryDate
    ? Math.max(0, Math.floor((expiryDate.getTime() - now.getTime()) / 1000))
    : 7 * 24 * 60 * 60 // Default to 7 days if no expiry

  fastify.log.debug(
    `[WorkSession Join] Token expiry: ${expiresInSeconds}s (${expiresInSeconds / 3600}h), expiryDate: ${expiryDate?.toISOString() || "none"}`,
  )

  const token = fastify.jwt.sign(tokenPayload, {
    expiresIn: expiresInSeconds > 0 ? `${expiresInSeconds}s` : "7d", // Fallback to 7d if invalid
  })
  const decoded = fastify.jwt.decode(token)

  fastify.log.info(
    `[WorkSession Join] JWT token created successfully for sessionId: ${sessionId}, token length: ${token.length}, expiresIn: ${expiresInSeconds}s`,
  )
  fastify.log.debug(
    `[WorkSession Join] Decoded token: ${JSON.stringify(decoded)}`,
  )

  const responseData = {
    ...joinPayloadData,
    token,
    decoded,
  }

  fastify.log.info(
    `[WorkSession Join] Emitting joinConfirmation to socket: ${socket.id}`,
  )
  socket.emit("joinConfirmation", responseData)
  fastify.log.info(
    `[WorkSession Join] Work session join complete: socket=${socket.id}, sessionId=${sessionId}, room=session_${sessionId}, tokenCreated=${!!token}`,
  )
}

/**
 * Handle authenticated user join
 */
const handleAuthenticatedUserJoin = async (
  fastify: FastifyInstance,
  socket: Socket,
  payload: JoinPayload,
): Promise<void> => {
  const explicitSpaceId = payload.roomId
  const bootstrap = payload.bootstrap ?? !explicitSpaceId
  let spaceId = explicitSpaceId
  let project: ProjectResponse | null = null

  if (explicitSpaceId) {
    try {
      project = await assertCanJoinProject(fastify, socket, explicitSpaceId)
      spaceId = explicitSpaceId
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "Access denied"
      const code = message === "Space not found" ? "NOT_FOUND" : "FORBIDDEN"
      socket.emit("joinFailed", {
        roomId: explicitSpaceId,
        code,
        message,
      })
      return
    }
  } else {
    // per default we let every user join the default space for the app
    project = await loadProject(fastify)
    if (!project) {
      socket.emit("joinFailed", {
        code: "NOT_FOUND",
        message: "No default space found. Something seems wrongly configured!",
      })
      return
    }
    spaceId = project._id
  }

  await socket.join(spaceId)
  if (!socket.data.projects) {
    socket.data.projects = new Set<string>()
  }
  socket.data.projects.add(spaceId)

  const decodedToken = socket.decoded_token as { _id?: string }
  const userId = decodedToken?._id
  if (bootstrap && userId) {
    await joinOwnedWorkSessions(fastify, socket, userId)
  }

  const responseData =
    bootstrap && project
      ? await buildAuthenticatedUserResponse(fastify, project)
      : project
        ? { project }
        : {}

  socket.emit("joinConfirmation", {
    ...responseData,
    roomId: spaceId,
    bootstrap,
  })
  fastify.log.info(
    `[Socket] Socket ${socket.id} joined room ${spaceId}, rooms: ${[...socket.rooms].join(", ")}`,
  )
}

/**
 * Register socket event handlers
 */
const registerSocketEvents = (fastify: FastifyInstance): void => {
  const { wss } = fastify
  const eventbusHandler = createSystemEventHandlers(fastify)
  const requiresAuth = configService.isAuthRequired()

  wss.on("connection", (socket: Socket) => {
    fastify.log.info(`[Socket] Connected ${socket.id}`)
    socket.emit("socketEstablished", { id: socket.id })

    if (!isSocketAuthenticated(socket, requiresAuth) && requiresAuth) {
      fastify.log.warn(`[Socket] Unauthenticated connection ${socket.id}`)
      socket.emit("authenticationRequired", {
        message: "Authentication required",
      })
    }

    socket.on("join", async (payload: JoinPayload = {}) => {
      if (requiresAuth && !isSocketAuthenticated(socket, requiresAuth)) {
        socket.emit("authenticationFailed", {
          message: "Authentication required to join room",
        })
        return
      }

      if (socket.isWorkSession === true && socket.workSession) {
        await handleWorkSessionJoin(fastify, socket)
      } else {
        await handleAuthenticatedUserJoin(fastify, socket, payload)
      }
    })

    socket.on("disconnect", () => {
      fastify.log.info(`[Socket] Disconnected ${socket.id}`)
    })

    socket.on("leave", (room: string) => {
      socket.leave(room)
      socket.data.projects?.delete(room)
      socket.emit("leaveConfirmation", room)
      wss.in(room).emit("memberLeft", {
        room,
        msg: `member left ${socket.id}`,
      })
    })
  })

  wss.on("connection_error", (error: Error) => {
    fastify.log.error({ msg: "[Socket] Connection error", error })
  })

  // Register system event handlers
  const systemEvents = {
    broadcastDataUpdated: eventbusHandler.broadcastDataUpdated,
    broadcastDataCreated: eventbusHandler.broadcastDataCreated,
    broadcastDataDeleted: eventbusHandler.broadcastDataDeleted,
    broadcastTagCreated: eventbusHandler.broadcastTagCreated,
    broadcastTagDeleted: eventbusHandler.broadcastTagDeleted,
    broadcastUserCreated: eventbusHandler.broadcastUserCreated,
    broadcastUserDeleted: eventbusHandler.broadcastUserDeleted,
    broadcastInteractionLinkCreated:
      eventbusHandler.broadcastInteractionLinkCreated,
    broadcastInteractionLinkDeleted:
      eventbusHandler.broadcastInteractionLinkDeleted,
    broadcastCompositionCreated: eventbusHandler.broadcastCompositionCreated,
    broadcastCompositionDeleted: eventbusHandler.broadcastCompositionDeleted,
    broadcastProjectCreated: eventbusHandler.broadcastProjectCreated,
    broadcastProjectUpdated: eventbusHandler.broadcastProjectUpdated,
    broadcastProjectConfigUpdated:
      eventbusHandler.broadcastProjectConfigUpdated,
    broadcastNewVersionAvailable: eventbusHandler.broadcastNewVersionAvailable,
  }

  Object.entries(systemEvents).forEach(([event, handler]) => {
    fastify.eventbus.on(event, handler)
  })

  // Set up socket broadcasting for plugin events via EventEmitter hook
  setupPluginEventSocketBroadcasting(fastify)
}

/**
 * Set up socket broadcasting for plugin-prefixed events
 * Registers a broadcast hook in the EventEmitter that automatically
 * broadcasts plugin events (containing "--") to the project room
 */
const setupPluginEventSocketBroadcasting = (fastify: FastifyInstance): void => {
  fastify.eventbus.setBroadcastHook((eventName: string, data: unknown) => {
    // Broadcast to project room (fire-and-forget)
    callDataTypeOperation("raclette__core", "project", "readMain", fastify)
      .then((mainProject) => {
        if (mainProject?._id) {
          if (data?.workSessionId) {
            fastify.wss.in(data.workSessionId).emit(eventName, data)
            fastify.log.debug(
              `[Socket] Broadcasted plugin event ${eventName} to workSessionId room ${data.workSessionId}`,
            )
          } else {
            fastify.wss.in(mainProject._id).emit(eventName, data)
            fastify.log.debug(
              `[Socket] Broadcasted plugin event ${eventName} to project room ${mainProject._id}`,
            )
          }
        } else {
          throw new Error("no project for broadcast")
        }
      })
      .catch((error: unknown) => {
        const errorMessage =
          error instanceof Error ? error.message : String(error)
        fastify.log.warn(`Failed to send broadcast. ${errorMessage}`)
      })
  })
}

export default registerSocketEvents
