import { REDEMPTION } from "@/constants/booking"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { badRequestError, unauthorizedError } from "@/server/errors/trpc"
import { getCityPageUrls } from "@/server/routers/contentstack/destinationCityPage/utils"
import { getVerifiedUser } from "@/server/routers/user/utils"
import { createCounter } from "@/server/telemetry"
import {
  contentStackBaseWithServiceProcedure,
  publicProcedure,
  router,
  safeProtectedServiceProcedure,
  serviceProcedure,
} from "@/server/trpc"
import { toApiLang } from "@/server/utils"

import { getCacheClient } from "@/services/dataCache"

import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
import { meetingRoomsSchema } from "./schemas/meetingRoom"
import {
  ancillaryPackageInputSchema,
  breakfastPackageInputSchema,
  cityCoordinatesInputSchema,
  enterDetailsRoomsAvailabilityInputSchema,
  getAdditionalDataInputSchema,
  getDestinationsMapDataInput,
  getHotelsByCityIdentifierInput,
  getHotelsByCountryInput,
  getHotelsByCSFilterInput,
  getHotelsByHotelIdsAvailabilityInputSchema,
  getLocationsInput,
  getLocationsUrlsInput,
  getMeetingRoomsInputSchema,
  hotelInputSchema,
  hotelsAvailabilityInputSchema,
  myStayRoomAvailabilityInputSchema,
  nearbyHotelIdsInput,
  roomPackagesInputSchema,
  selectRateRoomAvailabilityInputSchema,
  selectRateRoomsAvailabilityInputSchema,
} from "./input"
import {
  ancillaryPackagesSchema,
  breakfastPackagesSchema,
  getNearbyHotelIdsSchema,
} from "./output"
import {
  getBedTypes,
  getCitiesByCountry,
  getCountries,
  getHotel,
  getHotelIdsByCityId,
  getHotelIdsByCityIdentifier,
  getHotelIdsByCountry,
  getHotelsAvailabilityByCity,
  getHotelsAvailabilityByHotelIds,
  getHotelsByHotelIds,
  getLocations,
  getPackages,
  getRoomsAvailability,
  getSelectedRoomAvailability,
  mergeRoomTypes,
  selectRateRedirectURL,
} from "./utils"

import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { RateEnum } from "@/types/enums/rate"
import { RateTypeEnum } from "@/types/enums/rateType"
import type { DestinationPagesHotelData, HotelDataWithUrl } from "@/types/hotel"
import type { Room } from "@/types/providers/details/room"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"

export const hotelQueryRouter = router({
  availability: router({
    hotelsByCity: safeProtectedServiceProcedure
      .input(hotelsAvailabilityInputSchema)
      .use(async ({ ctx, input, next }) => {
        if (input.redemption) {
          if (ctx.session?.token.access_token) {
            const verifiedUser = await getVerifiedUser({ session: ctx.session })
            if (!verifiedUser?.error) {
              return next({
                ctx: {
                  token: ctx.session.token.access_token,
                  userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
                },
                input,
              })
            }
          }
          throw unauthorizedError()
        }
        return next({
          ctx: {
            token: ctx.serviceToken,
          },
          input,
        })
      })
      .query(async ({ ctx, input }) => {
        const { lang } = ctx
        const apiLang = toApiLang(lang)
        const {
          cityId,
          roomStayStartDate,
          roomStayEndDate,
          adults,
          children,
          bookingCode,
          redemption,
        } = input

        // In case of redemption do not cache result
        if (redemption) {
          return getHotelsAvailabilityByCity(
            input,
            apiLang,
            ctx.token,
            ctx.userPoints
          )
        }

        const cacheClient = await getCacheClient()
        return await cacheClient.cacheOrGet(
          `${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
          async () => {
            return getHotelsAvailabilityByCity(input, apiLang, ctx.token)
          },
          env.CACHE_TIME_CITY_SEARCH
        )
      }),
    hotelsByHotelIds: serviceProcedure
      .input(getHotelsByHotelIdsAvailabilityInputSchema)
      .query(async ({ input, ctx }) => {
        const { lang } = ctx
        const apiLang = toApiLang(lang)
        return getHotelsAvailabilityByHotelIds(input, apiLang, ctx.serviceToken)
      }),

    enterDetails: safeProtectedServiceProcedure
      .input(enterDetailsRoomsAvailabilityInputSchema)
      .use(async ({ ctx, input, next }) => {
        if (input.booking.searchType === REDEMPTION) {
          if (ctx.session?.token.access_token) {
            const verifiedUser = await getVerifiedUser({ session: ctx.session })
            if (!verifiedUser?.error) {
              return next({
                ctx: {
                  token: ctx.session.token.access_token,
                  userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
                },
              })
            }
          }
          throw unauthorizedError()
        }
        return next({
          ctx: {
            token: ctx.serviceToken,
          },
        })
      })
      .query(async function ({ ctx, input }) {
        const availability = await getRoomsAvailability(
          input,
          ctx.token,
          ctx.serviceToken,
          ctx.userPoints
        )

        const hotelData = await getHotel(
          {
            hotelId: input.booking.hotelId,
            isCardOnlyPayment: false,
            language: input.lang || ctx.lang,
          },
          ctx.serviceToken
        )

        const selectedRooms = []
        for (const [idx, room] of availability.entries()) {
          if (!room || "error" in room) {
            console.info(`Availability failed: ${room.error}`)
            console.error(room.details)
            selectedRooms.push(null)
            continue
          }
          const bookingRoom = input.booking.rooms[idx]
          const selected = getSelectedRoomAvailability(
            bookingRoom.rateCode,
            room.rateDefinitions,
            room.roomConfigurations,
            bookingRoom.roomTypeCode,
            ctx.userPoints
          )
          if (!selected) {
            console.error("Unable to find selected room")
            selectedRooms.push(null)
            continue
          }

          const {
            rateDefinition,
            rateDefinitions,
            product,
            rooms,
            selectedRoom,
          } = selected

          const bedTypes = getBedTypes(
            rooms,
            selectedRoom.roomType,
            hotelData?.roomCategories
          )

          const counterRateCode = input.booking.rooms[idx].counterRateCode
          let memberRateDefinition = undefined
          if ("member" in product && product.member && counterRateCode) {
            memberRateDefinition = rateDefinitions.find(
              (rate) => rate.rateCode === counterRateCode && rate.isMemberRate
            )
          }

          const selectedPackages = input.booking.rooms[idx].packages
          selectedRooms.push({
            bedTypes,
            breakfastIncluded: rateDefinition.breakfastIncluded,
            cancellationText: rateDefinition.cancellationText,
            isAvailable: selectedRoom.status === AvailabilityEnum.Available,
            isFlexRate: product.rate === RateEnum.flex,
            memberMustBeGuaranteed: memberRateDefinition?.mustBeGuaranteed,
            mustBeGuaranteed: rateDefinition.mustBeGuaranteed,
            packages: room.packages.filter((pkg) =>
              selectedPackages?.includes(pkg.code)
            ),
            rate: product.rate,
            rateDefinitionTitle: rateDefinition.title,
            rateDetails: rateDefinition.generalTerms,
            memberRateDetails: memberRateDefinition?.generalTerms,
            // Send rate Title when it is a booking code rate
            rateTitle:
              rateDefinition.rateType !== RateTypeEnum.Regular
                ? rateDefinition.title
                : undefined,
            rateType: rateDefinition.rateType,
            roomRate: product,
            roomType: selectedRoom.roomType,
            roomTypeCode: selectedRoom.roomTypeCode,
          })
        }

        const totalBedsAvailableForRoomTypeCode: Record<string, number> = {}
        for (const selectedRoom of selectedRooms) {
          if (selectedRoom) {
            if (!totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode]) {
              totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] =
                selectedRoom.bedTypes.reduce(
                  (total, bedType) => total + bedType.roomsLeft,
                  0
                )
            }
          }
        }

        for (const [idx, selectedRoom] of selectedRooms.entries()) {
          if (selectedRoom) {
            const totalBedsLeft =
              totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode]
            if (totalBedsLeft <= 0) {
              selectedRooms[idx] = null
              continue
            }
            totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] =
              totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] - 1
          }
        }

        if (selectedRooms.some((sr) => !sr)) {
          return selectRateRedirectURL(input, selectedRooms.map(Boolean))
        }

        // Make TS show appropriate type
        return selectedRooms.filter((sr): sr is Room => !!sr)
      }),
    myStay: safeProtectedServiceProcedure
      .input(myStayRoomAvailabilityInputSchema)
      .use(async ({ ctx, input, next }) => {
        if (input.booking.searchType === REDEMPTION) {
          if (ctx.session?.token.access_token) {
            const verifiedUser = await getVerifiedUser({ session: ctx.session })
            if (!verifiedUser?.error) {
              return next({
                ctx: {
                  token: ctx.session.token.access_token,
                  userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
                },
              })
            }
          }
          throw unauthorizedError()
        }
        return next({
          ctx: {
            token: ctx.serviceToken,
          },
        })
      })
      .query(async function ({ ctx, input }) {
        const [availability] = await getRoomsAvailability(
          {
            booking: {
              ...input.booking,
              rooms: [input.booking.room],
            },
            lang: input.lang,
          },
          ctx.token,
          ctx.serviceToken,
          ctx.userPoints
        )

        if (!availability || "error" in availability) {
          return null
        }

        const bookingRoom = input.booking.room
        const selected = getSelectedRoomAvailability(
          bookingRoom.rateCode,
          availability.rateDefinitions,
          availability.roomConfigurations,
          bookingRoom.roomTypeCode,
          ctx.userPoints
        )

        if (!selected) {
          console.error("Unable to find selected room")
          return null
        }

        return {
          product: selected.product,
          selectedRoom: selected.selectedRoom,
        }
      }),
    selectRate: router({
      room: safeProtectedServiceProcedure
        .input(selectRateRoomAvailabilityInputSchema)
        .use(async ({ ctx, input, next }) => {
          if (input.booking.searchType === REDEMPTION) {
            if (ctx.session?.token.access_token) {
              const verifiedUser = await getVerifiedUser({
                session: ctx.session,
              })
              if (!verifiedUser?.error) {
                return next({
                  ctx: {
                    token: ctx.session.token.access_token,
                    userPoints:
                      verifiedUser?.data.membership?.currentPoints ?? 0,
                  },
                })
              }
            }
            throw unauthorizedError()
          }
          return next({
            ctx: {
              token: ctx.serviceToken,
            },
          })
        })
        .query(async function ({ ctx, input }) {
          const [availability] = await getRoomsAvailability(
            {
              booking: {
                ...input.booking,
                rooms: [input.booking.room],
              },
              lang: input.lang,
            },
            ctx.token,
            ctx.serviceToken,
            ctx.userPoints
          )

          if (!availability || "error" in availability) {
            return null
          }

          const roomConfigurations = mergeRoomTypes(
            availability.roomConfigurations
          )

          return {
            ...availability,
            roomConfigurations,
          }
        }),

      rooms: safeProtectedServiceProcedure
        .input(selectRateRoomsAvailabilityInputSchema)
        .use(async ({ ctx, input, next }) => {
          if (input.booking.searchType === REDEMPTION) {
            if (ctx.session?.token.access_token) {
              const verifiedUser = await getVerifiedUser({
                session: ctx.session,
              })
              if (!verifiedUser?.error) {
                return next({
                  ctx: {
                    token: ctx.session.token.access_token,
                    userPoints:
                      verifiedUser?.data.membership?.currentPoints ?? 0,
                  },
                })
              }
            }
            throw unauthorizedError()
          }
          return next({
            ctx: {
              token: ctx.serviceToken,
            },
          })
        })
        .query(async function ({ ctx, input }) {
          input.booking.rooms = input.booking.rooms.map((room) => ({
            ...room,
            bookingCode: room.bookingCode || input.booking.bookingCode,
          }))

          const availability = await getRoomsAvailability(
            input,
            ctx.token,
            ctx.serviceToken,
            ctx.userPoints
          )

          for (const room of availability) {
            if (!room || "error" in room) {
              continue
            }

            room.roomConfigurations = mergeRoomTypes(room.roomConfigurations)
          }

          return availability
        }),
    }),

    hotelsByCityWithBookingCode: serviceProcedure
      .input(hotelsAvailabilityInputSchema)
      .query(async ({ input, ctx }) => {
        const { lang } = ctx
        const apiLang = toApiLang(lang)

        const bookingCodeAvailabilityResponse =
          await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)

        // Get regular availability of hotels which don't have availability with booking code.
        const unavailableHotelIds = bookingCodeAvailabilityResponse.availability
          .filter((hotel) => {
            return hotel.status === "NotAvailable"
          })
          .flatMap((hotel) => {
            return hotel.hotelId
          })

        // All hotels have availability with booking code no need to fetch regular prices.
        // return response as is without any filtering as below.
        if (!unavailableHotelIds || !unavailableHotelIds.length) {
          return bookingCodeAvailabilityResponse
        }

        const unavailableHotelsInput = {
          ...input,
          bookingCode: "",
          hotelIds: unavailableHotelIds,
        }
        const unavailableHotels = await getHotelsAvailabilityByHotelIds(
          unavailableHotelsInput,
          apiLang,
          ctx.serviceToken
        )

        // No regular rates available due to network or API failure (no need to filter & merge).
        if (!unavailableHotels) {
          return bookingCodeAvailabilityResponse
        }

        // Filtering the response hotels to merge bookingCode rates and regular rates in single response.
        return {
          availability: bookingCodeAvailabilityResponse.availability
            .filter((hotel) => {
              return hotel.status === "Available"
            })
            .concat(unavailableHotels.availability),
        }
      }),
  }),
  get: serviceProcedure
    .input(hotelInputSchema)
    .query(async ({ ctx, input }) => {
      const { hotelId, language } = input

      const [hotelData, hotelPages] = await Promise.all([
        getHotel(input, ctx.serviceToken),
        getHotelPageUrls(language),
      ])
      const hotelPage = hotelPages.find((page) => page.hotelId === hotelId)

      return hotelData
        ? {
            ...hotelData,
            url: hotelPage?.url ?? null,
          }
        : null
    }),
  hotels: router({
    byCountry: router({
      get: contentStackBaseWithServiceProcedure
        .input(getHotelsByCountryInput)
        .query(async ({ ctx, input }) => {
          const { lang, serviceToken } = ctx
          const { country } = input

          const hotelIds = await getHotelIdsByCountry({
            country,
            serviceToken: ctx.serviceToken,
          })

          return await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
        }),
    }),
    byCityIdentifier: router({
      get: contentStackBaseWithServiceProcedure
        .input(getHotelsByCityIdentifierInput)
        .query(async ({ ctx, input }) => {
          const { lang, serviceToken } = ctx
          const { cityIdentifier } = input

          const hotelIds = await getHotelIdsByCityIdentifier(
            cityIdentifier,
            serviceToken
          )

          return await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
        }),
    }),
    byCSFilter: router({
      get: contentStackBaseWithServiceProcedure
        .input(getHotelsByCSFilterInput)
        .query(async function ({ ctx, input }) {
          const { locationFilter, hotelsToInclude } = input

          const language = ctx.lang
          let hotelsToFetch: string[] = []

          const getHotelsByCSFilterCounter = createCounter(
            "trpc.hotel.hotels",
            "byCSFilter"
          )
          const metricsGetHotelsByCSFilter = getHotelsByCSFilterCounter.init({
            input,
            language,
          })

          metricsGetHotelsByCSFilter.start()

          if (hotelsToInclude.length) {
            hotelsToFetch = hotelsToInclude
          } else if (locationFilter?.city) {
            const locations = await getLocations({
              lang: language,
              serviceToken: ctx.serviceToken,
              citiesByCountry: null,
            })
            if (!locations || "error" in locations) {
              return []
            }

            const cityId = locations
              .filter(
                (loc): loc is CityLocation =>
                  "type" in loc && loc.type === "cities"
              )
              .find((loc) => loc.cityIdentifier === locationFilter.city)?.id

            if (!cityId) {
              metricsGetHotelsByCSFilter.dataError(
                `CityId not found for cityIdentifier: ${locationFilter.city}`,
                {
                  cityIdentifier: locationFilter.city,
                }
              )

              return []
            }

            const hotelIds = await getHotelIdsByCityId({
              cityId,
              serviceToken: ctx.serviceToken,
            })

            if (!hotelIds?.length) {
              metricsGetHotelsByCSFilter.dataError(
                `No hotelIds found for cityId: ${cityId}`,
                {
                  cityId,
                }
              )

              return []
            }

            const filteredHotelIds = hotelIds.filter(
              (id) => !locationFilter.excluded.includes(id)
            )

            hotelsToFetch = filteredHotelIds
          } else if (locationFilter?.country) {
            const hotelIds = await getHotelIdsByCountry({
              country: locationFilter.country,
              serviceToken: ctx.serviceToken,
            })

            if (!hotelIds?.length) {
              metricsGetHotelsByCSFilter.dataError(
                `No hotelIds found for country: ${locationFilter.country}`,
                {
                  country: locationFilter.country,
                }
              )

              return []
            }

            const filteredHotelIds = hotelIds.filter(
              (id) => !locationFilter.excluded.includes(id)
            )

            hotelsToFetch = filteredHotelIds
          }

          if (!hotelsToFetch.length) {
            metricsGetHotelsByCSFilter.dataError(
              `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
              input
            )

            return []
          }
          const hotelPages = await getHotelPageUrls(language)
          const hotels = await Promise.all(
            hotelsToFetch.map(async (hotelId) => {
              const hotelData = await getHotel(
                { hotelId, isCardOnlyPayment: false, language },
                ctx.serviceToken
              )
              const hotelPage = hotelPages.find(
                (page) => page.hotelId === hotelId
              )

              return hotelData
                ? {
                    ...hotelData,
                    url: hotelPage?.url ?? null,
                  }
                : null
            })
          )

          metricsGetHotelsByCSFilter.success()

          return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
        }),
    }),
    getDestinationsMapData: serviceProcedure
      .input(getDestinationsMapDataInput)
      .query(async function ({ input, ctx }) {
        const lang = input?.lang ?? ctx.lang
        const warmup = input?.warmup ?? false

        const fetchHotels = async () => {
          const countries = await getCountries({
            // Countries need to be in English regardless of incoming lang because
            // we use the names as input for API endpoints.
            lang: Lang.en,
            serviceToken: ctx.serviceToken,
          })

          if (!countries) {
            throw new Error("Unable to fetch countries")
          }

          const countryNames = countries.data.map((country) => country.name)
          const hotelData: DestinationPagesHotelData[] = (
            await Promise.all(
              countryNames.map(async (country) => {
                const hotelIds = await getHotelIdsByCountry({
                  country,
                  serviceToken: ctx.serviceToken,
                })

                const hotels = await getHotelsByHotelIds({
                  hotelIds,
                  lang: lang,
                  serviceToken: ctx.serviceToken,
                })

                return hotels
              })
            )
          ).flat()

          return hotelData
        }

        const cacheClient = await getCacheClient()
        return await cacheClient.cacheOrGet(
          `${lang}:getDestinationsMapData`,
          fetchHotels,
          "max",
          {
            cacheStrategy: warmup ? "fetch-then-cache" : "cache-first",
          }
        )
      }),
  }),

  nearbyHotelIds: serviceProcedure
    .input(nearbyHotelIdsInput)
    .query(async function ({ ctx, input }) {
      const { lang } = ctx
      const apiLang = toApiLang(lang)

      const { hotelId } = input
      const params: Record<string, string | number> = {
        language: apiLang,
      }
      const cacheClient = await getCacheClient()
      return cacheClient.cacheOrGet(
        `${apiLang}:nearbyHotels:${hotelId}`,
        async () => {
          const nearbyHotelsCounter = createCounter(
            "trpc.hotel",
            "nearbyHotelIds"
          )
          const metricsNearbyHotels = nearbyHotelsCounter.init({
            params,
            hotelId,
          })

          metricsNearbyHotels.start()

          const apiResponse = await api.get(
            api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
            {
              headers: {
                Authorization: `Bearer ${ctx.serviceToken}`,
              },
            },
            params
          )
          if (!apiResponse.ok) {
            await metricsNearbyHotels.httpError(apiResponse)
            return null
          }

          const apiJson = await apiResponse.json()
          const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
          if (!validateHotelData.success) {
            metricsNearbyHotels.validationError(validateHotelData.error)
            throw badRequestError()
          }

          metricsNearbyHotels.success()

          return validateHotelData.data.map((id: string) => parseInt(id, 10))
        },
        env.CACHE_TIME_HOTELS
      )
    }),
  locations: router({
    get: serviceProcedure.input(getLocationsInput).query(async function ({
      ctx,
      input,
    }) {
      const lang = input.lang ?? ctx.lang
      const cacheClient = await getCacheClient()
      return await cacheClient.cacheOrGet(
        `${lang}:getLocations`,
        async () => {
          const countries = await getCountries({
            lang: lang,
            serviceToken: ctx.serviceToken,
          })

          if (!countries) {
            throw new Error("Unable to fetch countries")
          }
          const countryNames = countries.data.map((country) => country.name)
          const citiesByCountry = await getCitiesByCountry({
            countries: countryNames,
            serviceToken: ctx.serviceToken,
            lang,
          })

          const locations = await getLocations({
            lang,
            serviceToken: ctx.serviceToken,
            citiesByCountry,
          })

          if (!locations || "error" in locations) {
            throw new Error("Unable to fetch locations")
          }

          return locations
        },
        "max"
      )
    }),
    urls: publicProcedure
      .input(getLocationsUrlsInput)
      .query(async ({ input }) => {
        const { lang } = input

        const locationsUrlsCounter = createCounter(
          "trpc.hotel.locations",
          "urls"
        )
        const metricsLocationsUrls = locationsUrlsCounter.init({
          lang,
        })

        metricsLocationsUrls.start()

        const [hotelPageUrlsResult, cityPageUrlsResult] =
          await Promise.allSettled([
            getHotelPageUrls(lang),
            getCityPageUrls(lang),
          ])

        if (
          hotelPageUrlsResult.status === "rejected" ||
          cityPageUrlsResult.status === "rejected"
        ) {
          metricsLocationsUrls.dataError(`Failed to get data for page URLs`, {
            hotelPageUrlsResult,
            cityPageUrlsResult,
          })

          return null
        }

        metricsLocationsUrls.success()

        return {
          hotels: hotelPageUrlsResult.value,
          cities: cityPageUrlsResult.value,
        }
      }),
  }),
  map: router({
    city: serviceProcedure
      .input(cityCoordinatesInputSchema)
      .query(async function ({ input }) {
        const apiKey = process.env.GOOGLE_STATIC_MAP_KEY
        const { city, hotel } = input

        async function fetchCoordinates(address: string) {
          const cacheClient = await getCacheClient()
          return await cacheClient.cacheOrGet(
            `coordinates:${address}`,
            async function () {
              const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
              const response = await fetch(url)
              const data = await response.json()

              if (data.status !== "OK") {
                console.error(`Geocode error: ${data.status}`)
                return null
              }

              const location = data.results[0]?.geometry?.location
              if (!location) {
                console.error("No location found in geocode response")
                return null
              }

              return location
            },
            "1d"
          )
        }

        let location = await fetchCoordinates(city)
        if (!location) {
          location = await fetchCoordinates(`${city}, ${hotel.address}`)
        }

        if (!location) {
          throw new Error("Unable to fetch coordinates")
        }

        return location
      }),
  }),
  meetingRooms: safeProtectedServiceProcedure
    .input(getMeetingRoomsInputSchema)
    .query(async function ({ ctx, input }) {
      const { hotelId, language } = input

      const params: Record<string, string | string[]> = {
        hotelId,
        language,
      }
      const meetingRoomsCounter = createCounter("trpc.hotel", "meetingRooms")
      const metricsMeetingRooms = meetingRoomsCounter.init({
        params,
      })

      metricsMeetingRooms.start()

      const cacheClient = await getCacheClient()
      return cacheClient.cacheOrGet(
        `${language}:hotels:meetingRooms:${hotelId}`,
        async () => {
          const apiResponse = await api.get(
            api.endpoints.v1.Hotel.Hotels.meetingRooms(input.hotelId),
            {
              headers: {
                Authorization: `Bearer ${ctx.serviceToken}`,
              },
            },
            params
          )

          if (!apiResponse.ok) {
            if (apiResponse.status === 404) {
              // This is expected when the hotel does not have meeting rooms
              metricsMeetingRooms.success()
              return []
            }

            await metricsMeetingRooms.httpError(apiResponse)
            throw new Error("Failed to fetch meeting rooms", {
              cause: apiResponse,
            })
          }

          const apiJson = await apiResponse.json()
          const validatedMeetingRooms = meetingRoomsSchema.safeParse(apiJson)

          if (!validatedMeetingRooms.success) {
            metricsMeetingRooms.validationError(validatedMeetingRooms.error)
            throw badRequestError()
          }

          metricsMeetingRooms.success()

          return validatedMeetingRooms.data.data
        },
        env.CACHE_TIME_HOTELS
      )
    }),
  additionalData: safeProtectedServiceProcedure
    .input(getAdditionalDataInputSchema)
    .query(async function ({ ctx, input }) {
      const { hotelId, language } = input

      const params: Record<string, string | string[]> = {
        hotelId,
        language,
      }

      const additionalDataCounter = createCounter(
        "trpc.hotel",
        "additionalData"
      )
      const metricsAdditionalData = additionalDataCounter.init({
        params,
      })

      metricsAdditionalData.start()

      const cacheClient = await getCacheClient()
      return cacheClient.cacheOrGet(
        `${language}:hotels:additionalData:${hotelId}`,
        async () => {
          const apiResponse = await api.get(
            api.endpoints.v1.Hotel.Hotels.additionalData(input.hotelId),
            {
              headers: {
                Authorization: `Bearer ${ctx.serviceToken}`,
              },
            },
            params
          )

          if (!apiResponse.ok) {
            await metricsAdditionalData.httpError(apiResponse)
            throw new Error("Unable to fetch additional data for hotel")
          }

          const apiJson = await apiResponse.json()
          const validatedAdditionalData =
            additionalDataSchema.safeParse(apiJson)

          if (!validatedAdditionalData.success) {
            metricsAdditionalData.validationError(validatedAdditionalData.error)
            throw badRequestError()
          }

          metricsAdditionalData.success()

          return validatedAdditionalData.data
        },
        env.CACHE_TIME_HOTELS
      )
    }),
  packages: router({
    get: serviceProcedure
      .input(roomPackagesInputSchema)
      .query(async ({ ctx, input }) => {
        return getPackages(input, ctx.serviceToken)
      }),
    breakfast: safeProtectedServiceProcedure
      .input(breakfastPackageInputSchema)
      .query(async function ({ ctx, input }) {
        const { lang } = ctx

        const apiLang = toApiLang(lang)
        const params = {
          Adults: input.adults,
          EndDate: dt(input.toDate).format("YYYY-MM-DD"),
          StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
          language: apiLang,
        }

        const breakfastCounter = createCounter(
          "trpc.hotel.packages",
          "breakfast"
        )
        const metricsBreakfast = breakfastCounter.init({
          params,
          hotelId: input.hotelId,
        })

        metricsBreakfast.start()

        const cacheClient = await getCacheClient()
        const breakfastPackages = await cacheClient.cacheOrGet(
          `${apiLang}:adults${input.adults}:startDate:${params.StartDate}:endDate:${params.EndDate}:hotel:${input.hotelId}`,
          async () => {
            const apiResponse = await api.get(
              api.endpoints.v1.Package.Breakfast.hotel(input.hotelId),
              {
                headers: {
                  Authorization: `Bearer ${ctx.serviceToken}`,
                },
              },
              params
            )

            if (!apiResponse.ok) {
              await metricsBreakfast.httpError(apiResponse)
              throw new Error("Unable to fetch breakfast packages")
            }

            const apiJson = await apiResponse.json()
            const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson)
            if (!breakfastPackages.success) {
              metricsBreakfast.validationError(breakfastPackages.error)
              throw new Error("Unable to parse breakfast packages")
            }

            return breakfastPackages.data
          },
          "1h"
        )
        // Since the BRF0 package is out of scope for release we'll disable this handling
        // of membership levels for now, to be reanabled once needed

        // if (ctx.session?.token) {
        //   const apiUser = await getVerifiedUser({ session: ctx.session })
        //   if (apiUser && !("error" in apiUser)) {
        //     const user = parsedUser(apiUser.data, false)
        //     if (
        //       user.membership &&
        //       ["L6", "L7"].includes(user.membership.membershipLevel)
        //     ) {
        //       const freeBreakfastPackage = breakfastPackages.find(
        //         (pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
        //       )
        //       if (freeBreakfastPackage?.localPrice) {
        //         return [freeBreakfastPackage]
        //       }
        //     }
        //   }
        // }

        metricsBreakfast.success()

        return breakfastPackages.filter(
          (pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
        )
      }),
    ancillary: safeProtectedServiceProcedure
      .input(ancillaryPackageInputSchema)
      .query(async function ({ ctx, input }) {
        const { lang } = ctx

        const apiLang = toApiLang(lang)
        const params = {
          EndDate: dt(input.toDate).format("YYYY-MM-DD"),
          StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
          language: apiLang,
        }

        const ancillaryCounter = createCounter(
          "trpc.hotel.packages",
          "ancillary"
        )
        const metricsAncillary = ancillaryCounter.init({
          params,
          hotelId: input.hotelId,
        })

        metricsAncillary.start()

        const cacheClient = await getCacheClient()
        const result = await cacheClient.cacheOrGet(
          `${apiLang}:hotel:${input.hotelId}:ancillaries:startDate:${params.StartDate}:endDate:${params.EndDate}`,
          async () => {
            const apiResponse = await api.get(
              api.endpoints.v1.Package.Ancillary.hotel(input.hotelId),
              {
                headers: {
                  Authorization: `Bearer ${ctx.serviceToken}`,
                },
              },
              params
            )

            if (!apiResponse.ok) {
              await metricsAncillary.httpError(apiResponse)
              return null
            }

            const apiJson = await apiResponse.json()
            const ancillaryPackages = ancillaryPackagesSchema.safeParse(apiJson)
            if (!ancillaryPackages.success) {
              metricsAncillary.validationError(ancillaryPackages.error)
              return null
            }

            return ancillaryPackages.data
          },
          "1h"
        )

        metricsAncillary.success()

        return result
      }),
  }),
})
