"use client"

import { usePathname, useSearchParams } from "next/navigation"
import { parseAsInteger, useQueryState } from "nuqs"
import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from "react"
import { type IntlShape, useIntl } from "react-intl"

import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { logger } from "@scandic-hotels/common/logger"
import { type RouterOutput, trpc } from "@scandic-hotels/trpc/client"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel"
import { selectRateRoomsAvailabilityInputSchema } from "@scandic-hotels/trpc/routers/hotels/availability/selectRate/rooms/schema"

import { useIsLoggedIn } from "../../../hooks/useIsLoggedIn"
import useLang from "../../../hooks/useLang"
import { BookingCodeFilterEnum } from "../../../stores/bookingCode-filter"
import {
  parseSelectRateSearchParams,
  searchParamsToRecord,
  serializeBookingSearchParams,
} from "../../../utils/url"
import { clearRooms } from "../clearRooms"
import { DebugButton } from "../DebugButton"
import { findUnavailableSelectedRooms } from "../findUnavailableSelectedRooms"
import { getSelectedPackages } from "../getSelectedPackages"
import { getTotalPrice } from "../getTotalPrice"
import { includeRoomInfo } from "../includeRoomInfo"
import { isRateSelected as isRateSelected_Inner } from "../isRateSelected"
import { calculateNumberOfNights } from "./calculateNumberOfNights"
import { getLowestRoomPrice } from "./getLowestRoomPrice"

import type { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"

import type { SelectRateBooking } from "../../../types/components/selectRate/selectRate"
import type { Price } from "../../../types/price"
import type {
  AvailabilityWithRoomInfo,
  DefaultRoomPackage,
  Rate,
  RoomPackage,
  SelectedRate,
  SelectRateContext,
} from "../types"

const SelectRateContext = createContext<SelectRateContext>(
  {} as SelectRateContext
)
SelectRateContext.displayName = "SelectRateContext"

type SelectRateContextProps = {
  children: React.ReactNode
  hotelData: NonNullable<RouterOutput["hotel"]["get"]>
}

export function SelectRateProvider({
  children,
  hotelData,
}: SelectRateContextProps) {
  const lang = useLang()
  const searchParams = useSearchParams()
  const updateBooking = useUpdateBooking()
  const isUserLoggedIn = useIsLoggedIn()
  const intl = useIntl()

  const [activeRoomIndex, setInternalActiveRoomIndex] = useQueryState<number>(
    "activeRoomIndex",
    parseAsInteger.withDefault(0)
  )

  const [_bookingCodeFilter, setBookingCodeFilter] =
    useState<BookingCodeFilterEnum>(BookingCodeFilterEnum.Discounted)

  const selectRateBooking = parseSelectRateSearchParams(
    searchParamsToRecord(searchParams)
  )

  const selectRateInput = selectRateRoomsAvailabilityInputSchema.safeParse({
    booking: selectRateBooking,
    lang,
  })

  const hotelId = selectRateInput.data?.booking.hotelId ?? hotelData.hotel.id

  const hotelQuery = trpc.hotel.get.useQuery(
    { hotelId: hotelId!, language: lang, isCardOnlyPayment: false },
    { enabled: !!hotelId, initialData: hotelData, refetchOnWindowFocus: false }
  )

  const availabilityQuery = trpc.hotel.availability.selectRate.rooms.useQuery(
    selectRateInput.data!,
    {
      retry(failureCount, error) {
        if (error.data?.code === "BAD_REQUEST") {
          return false
        }

        return failureCount <= 2
      },
      enabled: selectRateInput.success,
      refetchOnWindowFocus: false,
    }
  )

  const availablePackages: (DefaultRoomPackage | RoomPackage)[][] | undefined =
    useMemo(() => {
      const defaults = getDefaultRoomPackages(intl)

      return availabilityQuery.data
        ?.filter((x) => "packages" in x)
        .map((x) => {
          const p = x.packages.filter((x) => isRoomPackage(x))
          return [
            ...p,
            ...defaults.filter(
              (def) => !p.some((pkg) => pkg.code === def.code)
            ),
          ].sort((a, b) => a.description.localeCompare(b.description))
        })
    }, [availabilityQuery.data, intl])

  const roomAvailability: (AvailabilityWithRoomInfo | null)[][] =
    useMemo(() => {
      return (
        availabilityQuery.data
          ?.map((x, ix) => {
            if ("roomConfigurations" in x === false) {
              return undefined
            }

            const { roomConfigurations } = x
            return includeRoomInfo({
              roomConfigurations,
              roomCategories: hotelQuery.data?.roomCategories ?? [],
              selectedPackages: getSelectedPackages(
                availablePackages?.at(ix),
                selectRateInput.data?.booking.rooms[ix]?.packages ?? []
              ),
            })
          })
          .filter((x) => !!x) ?? []
      )
    }, [
      availabilityQuery.data,
      hotelQuery.data?.roomCategories,
      availablePackages,
      selectRateInput.data?.booking.rooms,
    ])

  const isRateSelected = useCallback(
    ({
      roomIndex,
      rate,
      roomTypeCode,
    }: {
      roomIndex: number
      rate: Rate
      roomTypeCode: string | null | undefined
    }) => {
      const selectedRate = selectRateBooking?.rooms?.[roomIndex].rateCode
      const selectedRoomTypeCode =
        selectRateBooking?.rooms?.[roomIndex].roomTypeCode

      const isSelected = isRateSelected_Inner({
        selectedRateCode: selectedRate,
        selectedRoomTypeCode,
        rate,
        roomTypeCode,
      })

      return isSelected
    },
    [selectRateBooking?.rooms]
  )

  const roomCount = selectRateInput.data?.booking?.rooms?.length ?? 1

  const selectedRates: SelectedRate[] = useMemo(() => {
    return (selectRateBooking?.rooms ?? []).map((_, ix) => {
      const selectedRatesPerRoom = roomAvailability.at(ix)?.flatMap((room) => {
        if (!room) return undefined

        const allRates: Rate[] = [
          ...room.regular.map((reg) => ({
            ...reg,
            type: "regular" as const,
          })),
          ...room.campaign.map((camp) => ({
            ...camp,
            type: "campaign" as const,
          })),
          ...room.redemptions.map((red) => ({
            ...red,
            type: "redemption" as const,
          })),
          ...room.code.map((cod) => ({
            ...cod,
            type: "code" as const,
          })),
        ]

        return allRates
          .map((rate) => ({
            ...rate,
            roomInfo: room,
            isSelected: isRateSelected({
              roomIndex: ix,
              rate: rate,
              roomTypeCode: room.roomTypeCode,
            }),
          }))
          .filter((x) => x.isSelected)
      })

      if (selectedRatesPerRoom && selectedRatesPerRoom.length > 1) {
        console.error(`Multiple selected rates found for room index ${ix}:`)
      }

      const selectedRate = selectedRatesPerRoom?.at(0)
      return selectedRate
    })
  }, [selectRateBooking?.rooms, isRateSelected, roomAvailability])

  const totalPrice = getTotalPrice({
    selectedRates: selectedRates.map((rate, ix) => ({
      rate,
      roomConfiguration: roomAvailability[ix]?.[0],
    })),
    isMember: isUserLoggedIn,
  })

  const getPriceForRoom = useCallback(
    (roomIndex: number): Price | null => {
      if (roomIndex < 0 || roomIndex >= selectedRates.length) {
        console.warn("Room index out of bounds:", roomIndex)
        return null
      }

      const rate = selectedRates[roomIndex]
      if (!rate) {
        return null
      }

      return getTotalPrice({
        selectedRates: [
          { rate, roomConfiguration: roomAvailability[roomIndex]?.[0] },
        ],
        isMember: isUserLoggedIn && roomIndex === 0,
        addAdditionalCost: false,
      })
    },
    [selectedRates, roomAvailability, isUserLoggedIn]
  )

  const setActiveRoomIndex = useCallback(
    (roomIndex: number | "deselect" | "next") => {
      if (roomIndex === "deselect" || roomIndex == "next") {
        if (roomCount === 1) {
          setInternalActiveRoomIndex(0)
          return
        }

        const isLastRoom = activeRoomIndex >= roomCount - 1
        if (isLastRoom) {
          setInternalActiveRoomIndex(-1)
          return
        }

        const nextRoomWithoutRate = selectedRates.findIndex((rate, ix) => {
          return ix !== activeRoomIndex && (!rate || !rate.isSelected)
        })

        setInternalActiveRoomIndex(nextRoomWithoutRate)
        return
      }

      if (roomIndex < 0 || roomIndex >= roomCount) {
        logger.warn("Room index out of bounds:", roomIndex)
        return
      }

      setInternalActiveRoomIndex(roomIndex)
    },
    [roomCount, activeRoomIndex, setInternalActiveRoomIndex, selectedRates]
  )

  const getPackagesForRoom: SelectRateContext["getPackagesForRoom"] = (
    roomIndex
  ) => {
    const availableForRoom = availablePackages?.[roomIndex] ?? []
    const selectedPackages = getSelectedPackages(
      availableForRoom,
      selectRateInput.data?.booking.rooms[roomIndex]?.packages ?? []
    )

    return {
      selectedPackages,
      availablePackages: availableForRoom,
    }
  }

  const bookingCodeFilter =
    _bookingCodeFilter === BookingCodeFilterEnum.Discounted &&
    !selectRateInput.data?.booking.bookingCode
      ? BookingCodeFilterEnum.All
      : _bookingCodeFilter

  const roomAvailabilityWithAdjustedRoomCount: (AvailabilityWithRoomInfo | null)[][] =
    roomAvailability.map((availability, roomIndex) => {
      if (roomIndex === 0) {
        return availability
      }

      return availability.map((room) => {
        if (!room) {
          return room
        }

        const sameRoomTypeSelectedPreviouslyCount =
          selectRateBooking?.rooms
            .slice(0, roomIndex)
            .filter((x) => x.roomTypeCode === room.roomTypeCode).length ?? 0
        const newRoomsLeft = Math.max(
          room.roomsLeft - sameRoomTypeSelectedPreviouslyCount,
          0
        )

        return {
          ...room,
          roomsLeft: newRoomsLeft,
          status:
            newRoomsLeft === 0
              ? AvailabilityEnum.NotAvailable
              : AvailabilityEnum.Available,
        } as typeof room
      })
    })

  const roomIndexesToDeselect = findUnavailableSelectedRooms({
    selectedRates,
    roomAvailabilityWithAdjustedRoomCount,
  })

  const cleared = clearRooms({
    selectRateBooking,
    roomIndexesToClear: roomIndexesToDeselect,
  })

  if (cleared.hasUpdated) {
    updateBooking(cleared.selectRateBooking)
  }

  return (
    <SelectRateContext.Provider
      value={{
        hotel: {
          data: hotelQuery.data,
          isFetching: hotelQuery.isFetching,
          isError: hotelQuery.isError,
          isSuccess: hotelQuery.isSuccess,
          error: hotelQuery.error,
        },
        availability: {
          data: availabilityQuery.data,
          isFetching: availabilityQuery.isFetching,
          isError: availabilityQuery.isError,
          isSuccess: availabilityQuery.isSuccess,
          error: availabilityQuery.error,
        },
        isFetching: availabilityQuery.isFetching || hotelQuery.isFetching,
        isError: availabilityQuery.isError || hotelQuery.isError,
        isSuccess: availabilityQuery.isSuccess && hotelQuery.isSuccess,
        getAvailabilityForRoom: (roomIndex: number) =>
          getAvailabilityForRoom(
            roomIndex,
            roomAvailabilityWithAdjustedRoomCount
          ),
        getLowestRoomPrice: () =>
          getLowestRoomPrice(roomAvailabilityWithAdjustedRoomCount),
        isRateSelected,
        getPackagesForRoom,
        bookingCodeFilter,
        input: {
          data: selectRateInput.data,
          hasError: !selectRateInput.success,
          nights: calculateNumberOfNights(
            selectRateInput.data?.booking.fromDate,
            selectRateInput.data?.booking.toDate
          ),
          errorCode: selectRateInput.error?.errors[0].message,
          bookingCode: selectRateInput.data?.booking.bookingCode,
          roomCount: roomCount,
          isMultiRoom: roomCount > 1,
        },
        selectedRates: {
          vat: hotelQuery.data?.hotel.vat ?? 0,
          rates: selectedRates,
          totalPrice,
          getPriceForRoom,
          rateSelectedForRoom: (roomIndex: number) => {
            return !!selectedRates[roomIndex]
          },
          forRoom: (roomIndex: number) => {
            return selectedRates[roomIndex]
          },
          state:
            selectedRates.length === 0
              ? "NONE_SELECTED"
              : selectedRates.every((x) => !!x)
                ? "ALL_SELECTED"
                : "PARTIALLY_SELECTED",
        },
        activeRoomIndex: activeRoomIndex,
        actions: {
          setActiveRoom: setActiveRoomIndex,
          selectPackages: ({ roomIndex, packages }) => {
            const updatedRoom = selectRateBooking?.rooms?.[roomIndex]
            if (!updatedRoom) {
              console.error("No room found at index", roomIndex)
              // TODO: What to do here?
              return
            }

            updatedRoom.packages = packages
            updateBooking(selectRateBooking)
            setActiveRoomIndex(roomIndex)
          },
          selectBookingCodeFilter: (filter: BookingCodeFilterEnum) => {
            setBookingCodeFilter(filter)
          },
          selectRate: ({
            roomIndex,
            rateCode,
            counterRateCode,
            roomTypeCode,
            bookingCode,
          }) => {
            const updatedRoom = selectRateBooking?.rooms?.[roomIndex]
            if (!updatedRoom) {
              console.error("No room found at index", roomIndex)
              // TODO: What to do here?
              return
            }

            updatedRoom.rateCode = rateCode
            updatedRoom.roomTypeCode = roomTypeCode
            updatedRoom.counterRateCode = counterRateCode || null
            updatedRoom.bookingCode = bookingCode || null

            updateBooking(selectRateBooking)
            setActiveRoomIndex("next")
          },
          removeBookingCode: () => {
            if (!selectRateInput.data) {
              return
            }

            const clearedBooking: SelectRateBooking = {
              hotelId: selectRateInput.data.booking.hotelId,
              fromDate: selectRateInput.data.booking.fromDate,
              toDate: selectRateInput.data.booking.toDate,
              rooms: selectRateInput.data.booking.rooms.map((room) => ({
                ...room,
                bookingCode: null,
              })),
            }

            updateBooking(clearedBooking)
            setActiveRoomIndex(0)
          },
        },
      }}
    >
      {children}
      <DebugButton />
    </SelectRateContext.Provider>
  )
}

export const useSelectRateContext = () => useContext(SelectRateContext)

const getDefaultRoomPackages = (intl: IntlShape): DefaultRoomPackage[] =>
  [
    {
      code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
      description: intl.formatMessage({
        defaultMessage: "Accessible room",
      }),
    },
    {
      code: RoomPackageCodeEnum.ALLERGY_ROOM,
      description: intl.formatMessage({
        defaultMessage: "Allergy-friendly room",
      }),
    },
    {
      code: RoomPackageCodeEnum.PET_ROOM,
      description: intl.formatMessage({
        defaultMessage: "Pet-friendly room",
      }),
    },
  ].map((pkg) => ({
    ...pkg,
    type: "default",
    localPrice: { currency: CurrencyEnum.Unknown, price: 0, totalPrice: 0 },
    requestedPrice: { currency: CurrencyEnum.Unknown, price: 0, totalPrice: 0 },
    itemCode: "",
    inventories: [],
  }))

function getAvailabilityForRoom(
  roomIndex: number,
  roomAvailability: (AvailabilityWithRoomInfo | null)[][] | undefined
): AvailabilityWithRoomInfo[] | undefined {
  if (
    !roomAvailability ||
    roomIndex < 0 ||
    roomIndex >= roomAvailability.length
  ) {
    return undefined
  }

  return roomAvailability[roomIndex]
    ?.filter((x) => !!x)
    .sort((a, b) => {
      if (!a || !b) {
        return 0
      }

      if (
        a.status === AvailabilityEnum.NotAvailable &&
        b.status !== AvailabilityEnum.NotAvailable
      ) {
        return 1
      }

      if (
        a.status !== AvailabilityEnum.NotAvailable &&
        b.status === AvailabilityEnum.NotAvailable
      ) {
        return -1
      }

      return 0
    })
}

function useUpdateBooking() {
  const pathname = usePathname()

  return function updateBooking(booking: SelectRateBooking) {
    const newUrl = new URL(pathname, window.location.origin)

    // TODO: Handle existing search params
    newUrl.search = serializeBookingSearchParams(booking).toString()
    // router.replace(newUrl.toString(), { scroll: false })
    window.history.replaceState({}, "", newUrl.toString())
  }
}

function isRoomPackage(x: {
  code: BreakfastPackageEnum | RoomPackageCodeEnum
}): x is { code: RoomPackageCodeEnum } {
  return Object.values(RoomPackageCodeEnum).includes(
    x.code as RoomPackageCodeEnum
  )
}
