import { z } from "zod"

import { toLang } from "@/server/utils"

import { sortRoomConfigs } from "@/utils/sort"
import { nullableStringValidator } from "@/utils/zod/stringValidator"

import { occupancySchema } from "./schemas/availability/occupancy"
import { productTypeSchema } from "./schemas/availability/productType"
import { citySchema } from "./schemas/city"
import {
  attributesSchema,
  includedSchema,
  relationshipsSchema as hotelRelationshipsSchema,
} from "./schemas/hotel"
import { addressSchema } from "./schemas/hotel/address"
import { detailedFacilitiesSchema } from "./schemas/hotel/detailedFacility"
import { locationSchema } from "./schemas/hotel/location"
import { imageSchema } from "./schemas/image"
import { locationCitySchema } from "./schemas/location/city"
import { locationHotelSchema } from "./schemas/location/hotel"
import {
  ancillaryPackageSchema,
  breakfastPackageSchema,
  packageSchema,
} from "./schemas/packages"
import { relationshipsSchema } from "./schemas/relationships"
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"

import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { RateEnum } from "@/types/enums/rate"
import { RateTypeEnum } from "@/types/enums/rateType"
import type {
  AdditionalData,
  City,
  NearbyHotel,
  Restaurant,
  Room,
} from "@/types/hotel"
import type {
  Product,
  RateDefinition,
} from "@/types/trpc/routers/hotel/roomAvailability"

// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
export const hotelSchema = z
  .object({
    data: z.object({
      attributes: attributesSchema,
      id: z.string(),
      language: z.string().transform((val) => {
        const lang = toLang(val)
        if (!lang) {
          throw new Error("Invalid language")
        }
        return lang
      }),
      relationships: hotelRelationshipsSchema,
      type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
    }),
    // NOTE: We can pass an "include" param to the hotel API to retrieve
    // additional data for an individual hotel.
    included: includedSchema,
  })
  .transform(({ data: { attributes, ...data }, included }) => {
    const additionalData =
      included.find(
        (inc): inc is AdditionalData => inc!.type === "additionalData"
      ) ?? ({} as AdditionalData)
    const cities = included.filter((inc): inc is City => inc!.type === "cities")
    const nearbyHotels = included.filter(
      (inc): inc is NearbyHotel => inc!.type === "hotels"
    )
    const restaurants = included.filter(
      (inc): inc is Restaurant => inc!.type === "restaurants"
    )
    const roomCategories = included.filter(
      (inc): inc is Room => inc!.type === "roomcategories"
    )
    return {
      additionalData,
      cities,
      hotel: {
        ...data,
        ...attributes,
      },
      nearbyHotels,
      restaurants,
      roomCategories,
    }
  })

export const hotelsAvailabilitySchema = z.object({
  data: z.array(
    z.object({
      attributes: z.object({
        bookingCode: z.string().nullish(),
        checkInDate: z.string(),
        checkOutDate: z.string(),
        hotelId: z.number(),
        occupancy: occupancySchema,
        productType: productTypeSchema,
        status: z.string(),
      }),
      relationships: relationshipsSchema.optional(),
      type: z.string().optional(),
    })
  ),
})

function getRate(rate: RateDefinition) {
  switch (rate.cancellationRule) {
    case "CancellableBefore6PM":
      return RateEnum.flex
    case "Changeable":
      return RateEnum.change
    case "NotCancellable":
      return RateEnum.save
    default:
      console.info(
        `Unknown cancellationRule [${rate.cancellationRule}]. This should never happen!`
      )
      return null
  }
}

/**
 * This is used for custom sorting further down
 * to guarantee correct order of rates
 */
const cancellationRules = {
  CancellableBefore6PM: 2,
  Changeable: 1,
  NotCancellable: 0,
} as const

export const roomsAvailabilitySchema = z
  .object({
    data: z.object({
      attributes: z.object({
        bookingCode: nullableStringValidator,
        checkInDate: z.string(),
        checkOutDate: z.string(),
        hotelId: z.number(),
        mustBeGuaranteed: z.boolean().optional(),
        occupancy: occupancySchema.optional(),
        packages: z.array(packageSchema).optional().default([]),
        rateDefinitions: z.array(rateDefinitionSchema),
        roomConfigurations: z.array(roomConfigurationSchema),
      }),
    }),
  })
  .transform(({ data: { attributes } }) => {
    const rateDefinitions = attributes.rateDefinitions
    const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => {
      // @ts-expect-error - index of cancellationRule TS
      acc[val.rateCode] = cancellationRules[val.cancellationRule]
      return acc
    }, {})

    function getProductRateCode(product: Product) {
      if ("corporateCheque" in product) {
        return product.corporateCheque.rateCode
      }
      if ("redemption" in product && product.redemption) {
        return product.redemption.rateCode
      }
      if ("voucher" in product) {
        return product.voucher.rateCode
      }
      if ("public" in product && product.public) {
        return product.public.rateCode
      }
      if ("member" in product && product.member) {
        return product.member.rateCode
      }
      return ""
    }

    function sortProductsBasedOnCancellationRule(a: Product, b: Product) {
      // @ts-expect-error - index
      const lookUpA = cancellationRuleLookup[getProductRateCode(a)]
      // @ts-expect-error - index
      const lookUpB = cancellationRuleLookup[getProductRateCode(b)]
      return lookUpA - lookUpB
    }

    function findRateDefintion(rateCode: string) {
      return rateDefinitions.find(
        (rateDefinition) => rateDefinition.rateCode === rateCode
      )
    }

    function getRateDetails(product: Product) {
      let rateCode = ""
      if ("corporateCheque" in product) {
        rateCode = product.corporateCheque.rateCode
      } else if ("redemption" in product && product.redemption) {
        rateCode = product.redemption.rateCode
      } else if ("voucher" in product && product.voucher) {
        rateCode = product.voucher.rateCode
      } else if ("public" in product && product.public) {
        rateCode = product.public.rateCode
      } else if ("member" in product && product.member) {
        rateCode = product.member.rateCode
      }

      if (!rateCode) {
        return null
      }

      const rateDefinition = findRateDefintion(rateCode)

      if (!rateDefinition) {
        return null
      }

      const rate = getRate(rateDefinition)
      if (!rate) {
        return null
      }

      if (attributes.bookingCode) {
        product.bookingCode = attributes.bookingCode
      } else {
        product.bookingCode = undefined
      }
      product.rate = rate
      product.rateDefinition = rateDefinition

      return product
    }

    const roomConfigurations = attributes.roomConfigurations
      .map((room) => {
        if (room.products.length) {
          const breakfastIncluded = []
          const breakfastIncludedMember = []
          for (const product of room.products) {
            if ("corporateCheque" in product) {
              const rateDetails = getRateDetails(product)
              if (rateDetails) {
                breakfastIncluded.push(
                  rateDetails.rateDefinition.breakfastIncluded
                )
                room.code.push({
                  ...rateDetails,
                  corporateCheque: product.corporateCheque,
                })
              }
              continue
            }

            if ("voucher" in product) {
              const rateDetails = getRateDetails(product)
              if (rateDetails) {
                breakfastIncluded.push(
                  rateDetails.rateDefinition.breakfastIncluded
                )
                room.code.push({
                  ...rateDetails,
                  voucher: product.voucher,
                })
              }
              continue
            }

            // Redemption is an array
            if (Array.isArray(product)) {
              if (product.length) {
                for (const redemption of product) {
                  const rateDetails = getRateDetails(redemption)
                  if (rateDetails) {
                    breakfastIncluded.push(
                      rateDetails.rateDefinition.breakfastIncluded
                    )
                    room.redemptions.push({
                      ...redemption,
                      ...rateDetails,
                    })
                  }
                }
              }
              continue
            }

            if (
              ("member" in product && product.member) ||
              ("public" in product && product.public)
            ) {
              const memberRate = product.member
              const publicRate = product.public
              const rateDetails = getRateDetails(product)
              const rateDetailsMember = getRateDetails({
                ...product,
                public: null,
              })

              if (rateDetails) {
                if (publicRate) {
                  breakfastIncluded.push(
                    rateDetails.rateDefinition.breakfastIncluded
                  )
                }
                if (rateDetailsMember) {
                  breakfastIncludedMember.push(
                    rateDetailsMember.rateDefinition.breakfastIncluded
                  )
                  rateDetails.rateDefinitionMember =
                    rateDetailsMember.rateDefinition
                }
                switch (rateDetails.rateDefinition.rateType) {
                  case RateTypeEnum.PublicPromotion:
                    room.campaign.push({
                      ...rateDetails,
                      member: memberRate,
                      public: publicRate,
                    })
                    break
                  case RateTypeEnum.Regular:
                    room.regular.push({
                      ...rateDetails,
                      member: memberRate,
                      public: publicRate,
                    })
                    break
                  default:
                    room.code.push({
                      ...rateDetails,
                      member: memberRate,
                      public: publicRate,
                    })
                }
                continue
              }
            }
          }

          room.breakfastIncludedInAllRates =
            !!breakfastIncluded.length && breakfastIncluded.every(Boolean)
          room.breakfastIncludedInAllRatesMember =
            !!breakfastIncludedMember.length &&
            breakfastIncludedMember.every(Boolean)

          // CancellationRule is the same for public and member per product
          // Sorting to guarantee order based on rate
          room.campaign.sort(sortProductsBasedOnCancellationRule)
          room.code.sort(sortProductsBasedOnCancellationRule)
          room.redemptions.sort(sortProductsBasedOnCancellationRule)
          room.regular.sort(sortProductsBasedOnCancellationRule)

          const hasCampaignProducts = room.campaign.length
          const hasCodeProducts = room.code.length
          const hasRedemptionProducts = room.redemptions.length
          const hasRegularProducts = room.regular.length
          if (
            !hasCampaignProducts &&
            !hasCodeProducts &&
            !hasRedemptionProducts &&
            !hasRegularProducts
          ) {
            room.status = AvailabilityEnum.NotAvailable
          }
        }

        return room
      })
      .sort(sortRoomConfigs)

    return {
      ...attributes,
      roomConfigurations,
    }
  })

export const citiesByCountrySchema = z.object({
  data: z.array(
    citySchema.transform((data) => {
      return {
        ...data.attributes,
        id: data.id,
        type: data.type,
      }
    })
  ),
})

export const countriesSchema = z.object({
  data: z
    .array(
      z.object({
        attributes: z.object({
          currency: z.string().default("N/A"),
          name: z.string(),
        }),
        hotelInformationSystemId: z.number().optional(),
        id: z.string().optional().default(""),
        language: z.string().optional(),
        type: z.literal("countries"),
      })
    )
    .transform((data) => {
      return data.map((country) => {
        return {
          ...country.attributes,
          hotelInformationSystemId: country.hotelInformationSystemId,
          id: country.id,
          language: country.language,
          type: country.type,
        }
      })
    }),
})

export type Cities = z.infer<typeof citiesSchema>
export const citiesSchema = z
  .object({
    data: z.array(citySchema),
  })
  .transform(({ data }) => {
    if (data.length) {
      const city = data[0]
      return {
        ...city.attributes,
        id: city.id,
        type: city.type,
      }
    }
    return null
  })

export const locationsSchema = z.object({
  data: z
    .array(
      z
        .discriminatedUnion("type", [locationCitySchema, locationHotelSchema])
        .transform((location) => {
          if (location.type === "cities") {
            return {
              ...location.attributes,
              country: location.attributes.countryName || "",
              id: location.id,
              type: location.type,
            }
          }
          return {
            ...location.attributes,
            id: location.id,
            relationships: {
              city: {
                cityIdentifier: "",
                ianaTimeZoneId: "",
                id: "",
                isPublished: false,
                keywords: [],
                name: "",
                timeZoneId: "",
                type: "cities",
                url: location?.relationships?.city?.links?.related ?? "",
              },
            },
            type: location.type,
            operaId: location.attributes.operaId ?? "",
          }
        })
    )
    .transform((data) =>
      data
        .filter((node) => !!node && node.isPublished)
        .filter((node) => {
          if (node.type === "hotels") {
            if (!node.operaId) {
              return false
            }
          } else {
            if (!node.cityIdentifier) {
              return false
            }
          }
          return true
        })
        .sort((a, b) => {
          if (a.type === b.type) {
            return a.name.localeCompare(b.name)
          } else {
            return a.type === "cities" ? -1 : 1
          }
        })
    ),
})

export const breakfastPackagesSchema = z
  .object({
    data: z.object({
      attributes: z
        .object({
          hotelId: z.number(),
          packages: z.array(breakfastPackageSchema),
        })
        .or(z.object({ packages: z.tuple([]) })),
      type: z.literal("breakfastpackage"),
    }),
  })
  .transform(({ data }) =>
    data.attributes.packages.filter((pkg) => pkg.code?.match(/^(BRF\d+)$/gm))
  )

export const ancillaryPackagesSchema = z
  .object({
    data: z.object({
      attributes: z.object({
        hotelId: z.number(),
        ancillaries: z.array(ancillaryPackageSchema),
      }),
    }),
  })
  .transform(({ data }) =>
    data.attributes.ancillaries
      .map((ancillary) => ({
        categoryName: ancillary.categoryName,
        ancillaryContent: ancillary.ancillaryContent
          .filter((item) => item.status === "Available")
          .map((item) => ({
            hotelId: data.attributes.hotelId,
            id: item.id,
            title: item.title,
            description: item.descriptions.html,
            imageUrl: item.images[0]?.imageSizes.small || undefined,
            price: {
              total: item.variants.ancillary.price.totalPrice,
              currency: item.variants.ancillary.price.currency,
            },
            points: item.variants.ancillaryLoyalty?.points,
            loyaltyCode: item.variants.ancillaryLoyalty?.code,
            requiresDeliveryTime: item.requiresDeliveryTime,
            categoryName: ancillary.categoryName,
          })),
      }))
      .filter((ancillary) => ancillary.ancillaryContent.length > 0)
  )

export const packagesSchema = z
  .object({
    data: z
      .object({
        attributes: z.object({
          hotelId: z.number(),
          packages: z.array(packageSchema).default([]),
        }),
      })
      .optional(),
  })
  .transform(({ data }) => data?.attributes.packages)

export const getHotelIdsSchema = z
  .object({
    data: z.array(
      z.object({
        attributes: z.object({
          isPublished: z.boolean(),
          isActive: z.boolean(),
        }),
        id: z.string(),
      })
    ),
  })
  .transform(({ data }) => {
    const filteredHotels = data.filter((hotel) => hotel.attributes.isPublished)
    return filteredHotels.map((hotel) => hotel.id)
  })

export const getNearbyHotelIdsSchema = z
  .object({
    data: z.array(
      z.object({
        attributes: z.object({
          isPublished: z.boolean(),
          isActive: z.boolean(),
        }),
        id: z.string(),
      })
    ),
  })
  .transform(({ data }) => {
    const filteredHotels = data.filter(
      (hotel) => hotel.attributes.isPublished && hotel.attributes.isActive
    )
    return filteredHotels.map((hotel) => hotel.id)
  })

export const roomFeaturesSchema = z
  .object({
    data: z.object({
      attributes: z.object({
        hotelId: z.number(),
        roomFeatures: z
          .array(
            z.object({
              roomTypeCode: z.string(),
              features: z.array(
                z.object({
                  inventory: z.number(),
                  code: z.enum([
                    RoomPackageCodeEnum.PET_ROOM,
                    RoomPackageCodeEnum.ALLERGY_ROOM,
                    RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
                  ]),
                })
              ),
            })
          )
          .default([]),
      }),
    }),
  })
  .transform((data) => {
    return data.data.attributes.roomFeatures
  })

export const destinationPagesHotelDataSchema = z
  .object({
    data: z.object({
      id: z.string(),
      name: z.string(),
      location: locationSchema,
      cityIdentifier: z.string().optional(),
      tripadvisor: z.number().optional(),
      detailedFacilities: detailedFacilitiesSchema,
      galleryImages: z
        .array(imageSchema)
        .nullish()
        .transform((arr) => (arr ? arr.filter(Boolean) : [])),
      address: addressSchema,
      hotelType: z.string(),
      type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
      url: z.string().optional(),
      hotelContent: z
        .object({
          texts: z.object({
            descriptions: z.object({
              short: z.string().optional(),
            }),
          }),
        })
        .optional(),
    }),
  })
  .transform(({ data: { hotelContent, ...data } }) => {
    return {
      hotel: {
        ...data,
        hotelDescription: hotelContent?.texts.descriptions?.short,
      },
      url: data.url ?? "",
    }
  })
