import type { Composition } from "@shared/types/core/Composition.types"
import type { InteractionLink } from "@shared/types/core/InteractionLink.types"
import log from "@racletteCore/lib/logger"
import { handleServerItem } from "@racletteCore/lib/data/dataApi"
import * as userNotifier from "@racletteCore/lib/userNotifier"
import type { ProjectConfiguration } from "../store/types"
import type { Socket } from "socket.io-client"
import { extractSessionIdFromUrl } from "@racletteCore/lib/workSessionUrl"
import { setHttpToken, setWorkSessionId } from "@racletteCore/lib/httpClient"
import { isActiveSocket, setActiveSocket } from "./socketRegistry"
import { startProjectBootstrap } from "./socketBootstrap"

/**
 * Listen to socket events received from the backend.
 *
 * @param $socket - socket.io client instance
 * @param $store - mini-rx store (dispatch actions)
 * @param $eventbus - global event bus
 *
 * Project bootstrap after joinConfirmation is handled in ./socketBootstrap.ts.
 */
const registerSocketEvents = ($socket: Socket, $store, $eventbus) => {
  setActiveSocket($socket)

  /** Ignore events from a replaced socket instance (see socketRegistry + main.ts teardown). */
  const guardActive = (socket: Socket) => isActiveSocket(socket)

  // --- Connection lifecycle ---------------------------------------------------

  /** Fired when a connection is established; triggers room join. */
  $socket.on("connect", () => {
    if (!guardActive($socket)) {
      return
    }
    log.socket("onConnect", "Socket connection established")
    userNotifier.setServerConnected()
    $eventbus.emit("socket/join/room")
  })

  /** Enable offline mode here if project config disabled it. */
  $socket.on("disconnect", (reason) => {
    if (!guardActive($socket)) {
      log.socket("onDisconnect", `ignored stale socket instance: ${reason}`)
      return
    }
    log.socket(
      "onDisconnect",
      `Socket disconnected (${reason}). Enable offline mode here if project conf disabled it`,
    )
    userNotifier.setServerDisconnected()
  })

  $socket.on("connect_error", (err) => {
    if (!guardActive($socket)) {
      return
    }
    log.socket("onConnectError", err)
    userNotifier.setServerDisconnected()
  })

  $socket.on("close", () => {
    if (!guardActive($socket)) {
      return
    }
    log.socket("onClose", "socket connection lost")
    userNotifier.setServerDisconnected()
  })

  /**
   * Reconnect attempt in progress (status 2).
   * connect handler will set online status and re-join the room.
   */
  $socket.on("reconnect", (attempt) => {
    if (!guardActive($socket)) {
      return
    }
    log.socket("onReconnect", `Socket reconnect (attempt ${attempt})`)
    userNotifier.setServerConnecting()
  })

  /** Server assigned this socket an id
   * payload: { id: string } */
  $socket.on("socketEstablished", (data) => {
    log.socket("onSocketEstablished", data)
    $store.dispatch({
      type: "backend/socket/status",
      payload: { socketId: data.id },
    })
  })

  // --- Entity sync (server → store / event bus) ------------------------------

  $socket.on("tagDeleted", (payload) => {
    const { appSessionId, body, type } = payload

    log.socket("onTagDeleted", {
      body,
      type,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    handleServerItem(payload, true, false)
    $eventbus.emit("socket_deletedTag", {
      offline: false,
      hard: true,
      body,
    })
  })

  $socket.on("tagCreated", (payload) => {
    const { appSessionId, body, type } = payload

    log.socket("onTagCreated", {
      body,
      type,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    handleServerItem(payload, true, false)
    $eventbus.emit("socket_createdTag", {
      offline: false,
      hard: true,
      body,
    })
  })

  $socket.on("userCreated", (payload) => {
    const { appSessionId, body, type } = payload

    log.socket("onUserCreated", {
      body,
      type,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    $eventbus.emit("socket_createdUser", { offline: false, body })
  })

  $socket.on("userDeleted", (payload) => {
    const { appSessionId, body, type } = payload
    log.socket("onUserDeleted", {
      body,
      type,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    $eventbus.emit("socket_deletedUser", { offline: false, body })
  })

  $socket.on("projectUpdated", (payload) => {
    const { appSessionId, body, type } = payload

    log.socket("onProjectUpdated", {
      body,
      type,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    handleServerItem(payload, true, false)
  })

  $socket.on("projectConfigUpdated", (payload) => {
    const { appSessionId, body, type } = payload

    log.socket("onProjectConfigUpdated", {
      body,
      type,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    handleServerItem(payload, true, false)
  })

  /**
   * Reports or updates the list of available projects.
   * Usually sent after the client requested projects the user has access to.
   */
  $socket.on("getProjectList", (data) => {
    if (!data || data.length < 1) {
      return
    }
    log.socket("onGetProjectList", data)
    $store.dispatch({ type: "projects/add", payload: data })
  })

  // --- Room join / leave ------------------------------------------------------

  $socket.on("joinFailed", (payload: { roomId?: string; code?: string; message?: string }) => {
    if (!guardActive($socket)) {
      return
    }
    log.socket("onJoinFailed", payload)
    // Room-scoped failure: transport may still work. A snackbar alone is not enough —
    // surfacing this as a hard error would blank the page while HTTP/socket stay up.
    // TODO: targeted UX once space-switch / permission flows are defined.
    if (!$socket.connected) {
      userNotifier.setServerDisconnected()
    }
  })

  /**
   * Entered a project room (new scope). bootstrap=false is a light re-join after reconnect.
   * Work sessions: no project in payload, but compositions/interactionLinks (+ optional token).
   */
  $socket.on(
    "joinConfirmation",
    (payload: {
      roomId?: string
      bootstrap?: boolean
      project?: { _id: string; account: string }
      projectConfig?: ProjectConfiguration
      compositions?: Composition[]
      interactionLinks?: InteractionLink[]
      token?: string // Work session JWT — replaces x-work-session-id header
    }) => {
      if (!guardActive($socket)) {
        return
      }

      const {
        project,
        projectConfig,
        compositions,
        interactionLinks,
        token,
        bootstrap = true,
      } = payload

      const isWorkSession = !project && compositions && compositions.length > 0

      if (isWorkSession) {
        const sessionId = extractSessionIdFromUrl() || "work_session"
        const resolveId = "join_session_" + sessionId

        log.socket("onJoinConfirmation", "Work session join", payload)

        if (token) {
          setHttpToken(token)
          setWorkSessionId(null)
          log.socket("onJoinConfirmation", "Work session token set", {
            hasToken: !!token,
          })
        }

        startProjectBootstrap(
          { store: $store, eventbus: $eventbus },
          resolveId,
          {
            compositions,
            interactionLinks,
            projectConfig,
            roomId: sessionId,
            afterSetup: async () => {
              // Fullscreen work session — hide chrome; URL stays /:workSession/id={uuid}
              // and the router resolves composition from store.
              $store.dispatch({
                type: "ui/update",
                payload: {
                  availableInterfaces: [],
                  navigationOpen: false,
                },
              })
            },
          },
        )

        return
      }

      if (!project) {
        log.socket("onJoinConfirmation", "no Room")
        return
      }

      const roomId = payload.roomId ?? project._id

      if (!roomId) {
        log.socket("onJoinConfirmation", "no Room id")
        return
      }

      if (bootstrap === false) {
        log.socket(
          "onJoinConfirmation",
          `Joined room without bootstrap: ${roomId}`,
        )
        userNotifier.setServerConnected()
        return
      }

      log.socket("onJoinConfirmation", payload)

      startProjectBootstrap(
        { store: $store, eventbus: $eventbus },
        "join_" + roomId,
        {
          project,
          compositions,
          interactionLinks,
          projectConfig,
          roomId,
        },
      )
    },
  )

  /**
   * Backend reports leaving a room — response to our request or forced by server.
   */
  $socket.on("leaveConfirmation", (data) => {
    log.socket("onLeaveConfirmation", data)
  })

  /** Backend has accepted our auth token on the socket. */
  $socket.on("login", () =>
    log.socket("onLogin", "Socket connection successfully authenticated"),
  )

  // --- Generic data sync ------------------------------------------------------

  $socket.on("dataCreated", (payload) => {
    const { body, appSessionId } = payload

    log.socket("dataCreated", {
      payload,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    handleServerItem(payload, true, false)
    $eventbus.emit("socket_dataCreated", body)
  })

  $socket.on("dataDeleted", (payload) => {
    const { body, appSessionId } = payload

    log.socket("dataDeleted", {
      payload,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    handleServerItem(payload, true, false)
    $eventbus.emit("socket_dataDeleted", body)
  })

  /** Incremental data update from the backend. */
  $socket.on("data", (payload) => {
    const { body, appSessionId } = payload

    log.socket("onData", {
      payload,
      isOwnRequest: appSessionId === $socket.appSessionId,
    })
    handleServerItem(payload, true, false)
    $eventbus.emit("socket/data/update", { offline: false, body })
  })

  /** New deployed frontend version available. */
  $socket.on("newVersionAvailable", () => {
    log.socket("onNewVersionAvailable", {})
    userNotifier.newVersionAvailable()
  })
}

export default registerSocketEvents
export { registerSocketEvents }
