import { $store } from "@racletteCore"
import type { RouteLocationNormalized } from "vue-router"
import {
  DEFAULT_SLOT_NAME,
  parseRouteToCompositionSlots,
  type CompositionSlot,
} from "@racletteOrchestrator/router/routeParserHelper"
import { getCompositionIdByPath } from "@racletteCore/store/reducers/compositions/selectors"
import logger from "@racletteCore/lib/logger"

// TODO: find better filename

export const dispatchSetCompositionAs = (
  slot: string,
  compositionPath?: string,
  compositionParams?: Record<string, string>,
) => {
  if (compositionPath) {
    const compositionId = getCompositionIdByPath(compositionPath)

    if (compositionId) {
      $store.dispatch({
        type: "ui/setCompositionAs",
        payload: {
          slot,
          compositionId,
        },
      })

      if (compositionParams && Object.keys(compositionParams).length) {
        $store.dispatch({
          type: "compositions/update",
          payload: {
            compositionId,
            compositionParams,
          },
        })
      }
    } else {
      logger.warning(
        "compositionNotFound",
        `No composition for path "${compositionPath}" (slot "${slot}"). ` +
          "Route may be ahead of socket bootstrap or the composition was removed.",
      )
    }
  } else {
    $store.dispatch({
      type: "ui/unsetComposition",
      payload: {
        slot,
      },
    })
  }
}

/**
 * Compares two arrays of composition slots and returns the differences
 * Returns an object with three arrays: toRemove, toAdd, toUpdate
 */
const getCompositionSlotChanges = (
  oldSlots: CompositionSlot[],
  newSlots: CompositionSlot[],
) => {
  // Helper: Create a comparison key for slots (ignoring slotParams)
  const getSlotKey = (slot: CompositionSlot) =>
    `${slot.slotName}:${slot.slotValue}`

  // Helper: Check if slotParams are different
  const hasDifferentSlotParams = (
    slotA: CompositionSlot,
    slotB: CompositionSlot,
  ) => {
    const paramsA = slotA.slotParams || {}
    const paramsB = slotB.slotParams || {}
    return JSON.stringify(paramsA) !== JSON.stringify(paramsB)
  }

  const oldSlotMap = new Map(oldSlots.map((slot) => [getSlotKey(slot), slot]))
  const newSlotMap = new Map(newSlots.map((slot) => [getSlotKey(slot), slot]))

  const toRemove: CompositionSlot[] = []
  const toAdd: CompositionSlot[] = []
  const toUpdate: CompositionSlot[] = []

  // Find slots to remove (exist in old but not in new)
  for (const [key, oldSlot] of oldSlotMap) {
    if (!newSlotMap.has(key)) {
      toRemove.push(oldSlot)
    }
  }

  // Find slots to add or update (exist in new)
  for (const [key, newSlot] of newSlotMap) {
    if (!oldSlotMap.has(key)) {
      // Completely new slot
      toAdd.push(newSlot)
    } else {
      // Slot exists in both, check if slotParams changed
      const oldSlot = oldSlotMap.get(key)!
      if (hasDifferentSlotParams(oldSlot, newSlot)) {
        toUpdate.push(newSlot)
      }
    }
  }

  return { toRemove, toAdd, toUpdate }
}

/**
 * Same slot name, different composition path — swap in one `setCompositionAs`
 * instead of unset + set (avoids a frame with no active composition).
 */
const partitionSlotChanges = (
  toRemove: CompositionSlot[],
  toAdd: CompositionSlot[],
) => {
  const replacements: CompositionSlot[] = []
  const stillToRemove: CompositionSlot[] = []
  const stillToAdd: CompositionSlot[] = []

  const addBySlotName = new Map(
    toAdd.map((slot) => [slot.slotName, slot]),
  )

  for (const removed of toRemove) {
    const replacement = addBySlotName.get(removed.slotName)
    if (replacement) {
      replacements.push(replacement)
      addBySlotName.delete(removed.slotName)
    } else {
      stillToRemove.push(removed)
    }
  }

  stillToAdd.push(...addBySlotName.values())

  return { replacements, stillToRemove, stillToAdd }
}

export const getCompositionSlotsForRoute = (
  route: RouteLocationNormalized,
  options?: { useDefaultSlotFallback?: boolean },
) => {
  const hasPageIdParameter = Boolean(route.params.pageId?.length)
  const useDefaultSlotFallback =
    options?.useDefaultSlotFallback ?? !hasPageIdParameter

  return parseRouteToCompositionSlots(
    route.fullPath,
    undefined,
    useDefaultSlotFallback,
  )
}

export const applyCompositionSlots = (slots: CompositionSlot[]) => {
  slots.forEach((slot) =>
    dispatchSetCompositionAs(slot.slotName, slot.slotValue, slot.slotParams),
  )
}

export const updateActiveCompositions = (
  to: RouteLocationNormalized,
  from: RouteLocationNormalized,
) => {
  const isInitialRouting = from.name === undefined

  const compositionSlots = getCompositionSlotsForRoute(to)
  const beforeCompositionSlots = getCompositionSlotsForRoute(from, {
    useDefaultSlotFallback:
      !isInitialRouting && !Boolean(from.params.pageId?.length),
  })

  const { toRemove, toAdd, toUpdate } = getCompositionSlotChanges(
    beforeCompositionSlots,
    compositionSlots,
  )

  const { replacements, stillToRemove, stillToAdd } = partitionSlotChanges(
    toRemove,
    toAdd,
  )

  // Same slot, new composition path — set directly (no unset in between)
  replacements.forEach((slot) =>
    dispatchSetCompositionAs(slot.slotName, slot.slotValue, slot.slotParams),
  )

  // Update existing slots with new slotParams (without removing/adding)
  toUpdate.forEach((slot) => {
    const compositionId = getCompositionIdByPath(slot.slotValue)
    if (
      compositionId &&
      slot.slotParams &&
      Object.keys(slot.slotParams).length
    ) {
      $store.dispatch({
        type: "compositions/update",
        payload: {
          compositionId,
          compositionParams: slot.slotParams,
        },
      })
    }
  })

  // New slots (slot name was not active before)
  stillToAdd.forEach((slot) =>
    dispatchSetCompositionAs(slot.slotName, slot.slotValue, slot.slotParams),
  )

  // Slots removed without a replacement on the same slot name
  stillToRemove.forEach((slot) => dispatchSetCompositionAs(slot.slotName))
}

export const handleRouteChange = (
  to: RouteLocationNormalized,
  from: RouteLocationNormalized,
) => {
  if (to.name === "app.space.page") {
    updateActiveCompositions(to, from)
  } else {
    dispatchSetCompositionAs(DEFAULT_SLOT_NAME)
  }
}

// check, if the user is navigating inside of the dynamic page ("app.space.page") and if the default slot has a change
export const checkForDefaultSlotChange = (
  to: RouteLocationNormalized,
  from: RouteLocationNormalized,
) => {
  if (to.name === "app.space.page" && from.name === "app.space.page") {
    const defaultSlotValueTo = getDefaultSlotValue(to.fullPath)
    const defaultSlotValueFrom = getDefaultSlotValue(from.fullPath)

    if (defaultSlotValueTo === defaultSlotValueFrom) {
      return false
    }
  }

  return true
}

export const getDefaultSlotValue = (url: string) =>
  parseRouteToCompositionSlots(url, undefined, true).find(
    (slot) => slot.slotName === DEFAULT_SLOT_NAME,
  )?.slotValue

/**
 * Re-apply composition slots from the current route without a navigation event.
 * Used after socket project bootstrap clears ui.page while the URL is unchanged.
 */
export const applyCompositionSlotsFromRoute = (
  route: RouteLocationNormalized,
) => {
  if (route.name !== "app.space.page") {
    return
  }

  applyCompositionSlots(getCompositionSlotsForRoute(route))
}
