import { getHotel } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"

import { getLang } from "@/i18n/serverContext"

import { generateChildrenString } from "../utils"

import type {
  AlternativeHotelsAvailabilityInput,
  AvailabilityInput,
} from "@/types/components/hotelReservation/selectHotel/availabilityInput"
import type {
  CategorizedFilters,
  HotelFilter,
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { AdditionalData, Hotel } from "@/types/hotel"
import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability"
import type {
  HotelLocation,
  Location,
} from "@/types/trpc/routers/hotel/locations"

interface AvailabilityResponse {
  availability: HotelsAvailabilityItem[]
}

export interface HotelResponse {
  availability: HotelsAvailabilityItem
  hotel: Hotel
  additionalData: AdditionalData
}

type Result = AvailabilityResponse | null
type SettledResult = PromiseSettledResult<Result>[]

async function enhanceHotels(hotels: HotelsAvailabilityItem[]) {
  const language = await getLang()
  return await Promise.allSettled(
    hotels.map(async (availability) => {
      const hotelData = await getHotel({
        hotelId: availability.hotelId.toString(),
        isCardOnlyPayment: false,
        language,
      })

      if (!hotelData) {
        return null
      }

      return {
        availability,
        hotel: hotelData.hotel,
        additionalData: hotelData.additionalData,
      }
    })
  )
}

async function fetchAlternativeHotels(
  hotelId: string,
  input: AlternativeHotelsAvailabilityInput
) {
  const caller = await serverClient()
  const alternativeHotelIds = await caller.hotel.nearbyHotelIds({
    hotelId,
  })

  if (!alternativeHotelIds) {
    return null
  }

  return await caller.hotel.availability.hotelsByHotelIds({
    ...input,
    hotelIds: alternativeHotelIds,
  })
}

async function fetchAvailableHotels(input: AvailabilityInput) {
  const caller = await serverClient()
  return await caller.hotel.availability.hotelsByCity(input)
}

async function fetchBookingCodeAvailableHotels(input: AvailabilityInput) {
  const caller = await serverClient()
  return await caller.hotel.availability.hotelsByCityWithBookingCode(input)
}

function getFulfilledResponses<T>(result: PromiseSettledResult<T | null>[]) {
  const fulfilledResponses: NonNullable<T>[] = []
  for (const res of result) {
    if (res.status === "fulfilled" && res.value) {
      fulfilledResponses.push(res.value)
    }
  }
  return fulfilledResponses
}

function getHotelAvailabilityItems(hotels: AvailabilityResponse[]) {
  return hotels.map((hotel) => hotel.availability)
}

// Filter out hotels that are unavailable for
// at least one room.
function sortAndFilterHotelsByAvailability(
  fulfilledHotels: HotelsAvailabilityItem[][]
) {
  const availableHotels = new Map<
    HotelsAvailabilityItem["hotelId"],
    HotelsAvailabilityItem
  >()
  const unavailableHotels = new Map<
    HotelsAvailabilityItem["hotelId"],
    HotelsAvailabilityItem
  >()
  const unavailableHotelIds = new Set<HotelsAvailabilityItem["hotelId"]>()

  for (const availabilityHotels of fulfilledHotels) {
    for (const hotel of availabilityHotels) {
      if (hotel.status === AvailabilityEnum.Available) {
        if (availableHotels.has(hotel.hotelId)) {
          const currentAddedHotel = availableHotels.get(hotel.hotelId)
          // Make sure the cheapest version of the room is the one
          // we keep so that it matches the cheapest room on select-rate
          if (
            (hotel.productType?.public &&
              currentAddedHotel?.productType?.public &&
              hotel.productType.public.localPrice.pricePerNight <
                currentAddedHotel.productType.public.localPrice
                  .pricePerNight) ||
            (hotel.productType?.member &&
              currentAddedHotel?.productType?.member &&
              hotel.productType.member.localPrice.pricePerNight <
                currentAddedHotel.productType.member.localPrice.pricePerNight)
          ) {
            availableHotels.set(hotel.hotelId, hotel)
          }
        } else {
          availableHotels.set(hotel.hotelId, hotel)
        }
      } else {
        unavailableHotels.set(hotel.hotelId, hotel)
        unavailableHotelIds.add(hotel.hotelId)
      }
    }
  }

  for (const [hotelId] of unavailableHotelIds.entries()) {
    if (availableHotels.has(hotelId)) {
      availableHotels.delete(hotelId)
    }
  }

  return [
    Array.from(availableHotels.values()),
    Array.from(unavailableHotels.values()),
  ].flat()
}

type GetHotelsInput = {
  fromDate: string
  toDate: string
  rooms: {
    adults: number
    childrenInRoom?: Child[]
  }[]
  isAlternativeFor: HotelLocation | null
  bookingCode: string | undefined
  city: Location
  redemption: boolean
}

export async function getHotels({
  rooms,
  fromDate,
  toDate,
  isAlternativeFor,
  bookingCode,
  city,
  redemption,
}: GetHotelsInput) {
  let availableHotelsResponse: SettledResult = []
  if (isAlternativeFor) {
    availableHotelsResponse = await Promise.allSettled(
      rooms.map(async (room) => {
        return fetchAlternativeHotels(isAlternativeFor.id, {
          adults: room.adults,
          bookingCode,
          children: room.childrenInRoom
            ? generateChildrenString(room.childrenInRoom)
            : undefined,
          redemption,
          roomStayEndDate: toDate,
          roomStayStartDate: fromDate,
        })
      })
    )
  } else if (bookingCode) {
    availableHotelsResponse = await Promise.allSettled(
      rooms.map(async (room) => {
        return fetchBookingCodeAvailableHotels({
          adults: room.adults,
          bookingCode,
          children: room.childrenInRoom
            ? generateChildrenString(room.childrenInRoom)
            : undefined,
          cityId: city.id,
          roomStayStartDate: fromDate,
          roomStayEndDate: toDate,
        })
      })
    )
  } else {
    availableHotelsResponse = await Promise.allSettled(
      rooms.map(
        async (room) =>
          await fetchAvailableHotels({
            adults: room.adults,
            children: room.childrenInRoom
              ? generateChildrenString(room.childrenInRoom)
              : undefined,
            cityId: city.id,
            redemption,
            roomStayEndDate: toDate,
            roomStayStartDate: fromDate,
          })
      )
    )
  }

  const fulfilledAvailabilities = getFulfilledResponses<AvailabilityResponse>(
    availableHotelsResponse
  )
  const availablilityItems = getHotelAvailabilityItems(fulfilledAvailabilities)
  const availableHotels = sortAndFilterHotelsByAvailability(availablilityItems)
  if (!availableHotels.length) {
    return []
  }
  const hotelsResponse = await enhanceHotels(availableHotels)
  const hotels = getFulfilledResponses<HotelResponse>(hotelsResponse)

  return hotels
}

const hotelSurroundingsFilterNames = [
  "Hotel surroundings",
  "Hotel omgivelser",
  "Hotelumgebung",
  "Hotellia lähellä",
  "Hotellomgivelser",
  "Omgivningar",
]

const hotelFacilitiesFilterNames = [
  "Hotel facilities",
  "Hotellfaciliteter",
  "Hotelfaciliteter",
  "Hotel faciliteter",
  "Hotel-Infos",
  "Hotellin palvelut",
]

export function getFiltersFromHotels(
  hotels: HotelResponse[]
): CategorizedFilters {
  const defaultFilters = { facilityFilters: [], surroundingsFilters: [] }
  if (!hotels.length) {
    return defaultFilters
  }

  const filters = hotels.flatMap(({ hotel }) =>
    hotel.detailedFacilities.map(
      (facility) =>
        <HotelFilter>{
          ...facility,
          hotelId: hotel.operaId,
          hotelIds: [hotel.operaId],
        }
    )
  )

  const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
  const filterList: HotelFilter[] = uniqueFilterIds
    .map((filterId) => {
      const filter = filters.find((f) => f.id === filterId)

      // List and include all hotel Ids having same filter / amenity
      if (filter) {
        filter.hotelIds = filters
          .filter((f) => f.id === filterId)
          .map((f) => f.hotelId)
      }
      return filter
    })
    .filter((filter): filter is HotelFilter => filter !== undefined)
    .sort((a, b) => b.sortOrder - a.sortOrder)

  return filterList.reduce<CategorizedFilters>((filters, filter) => {
    if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) {
      filters.surroundingsFilters.push(filter)
    }

    if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) {
      filters.facilityFilters.push(filter)
    }

    return filters
  }, defaultFilters)
}
