import * as api from "@/lib/api"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { createRefIdPlugin } from "@/server/plugins/refIdToConfirmationNumber"
import { createCounter } from "@/server/telemetry"
import {
  router,
  safeProtectedServiceProcedure,
  serviceProcedure,
} from "@/server/trpc"
import { toApiLang } from "@/server/utils"

import { getBookedHotelRoom } from "@/utils/booking"

import { encrypt } from "../../../utils/encryption"
import { getHotel } from "../hotels/utils"
import {
  createRefIdInput,
  findBookingInput,
  getBookingInput,
  getBookingStatusInput,
  getLinkedReservationsInput,
} from "./input"
import { createBookingSchema } from "./output"
import { findBooking, getBooking } from "./utils"

const refIdPlugin = createRefIdPlugin()

export const bookingQueryRouter = router({
  get: safeProtectedServiceProcedure
    .input(getBookingInput)
    .concat(refIdPlugin.toConfirmationNumber)
    .use(async ({ ctx, input, next }) => {
      const lang = input.lang ?? ctx.lang

      return next({
        ctx: {
          lang,
        },
      })
    })
    .query(async function ({ ctx }) {
      const { confirmationNumber, lang, serviceToken } = ctx

      const getBookingCounter = createCounter("trpc.booking", "get")
      const metricsGetBooking = getBookingCounter.init({ confirmationNumber })

      metricsGetBooking.start()

      const booking = await getBooking(confirmationNumber, lang, serviceToken)

      if (!booking) {
        metricsGetBooking.dataError(
          `Fail to get booking data for ${confirmationNumber}`,
          { confirmationNumber }
        )
        return null
      }

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

      if (!hotelData) {
        metricsGetBooking.dataError(
          `Failed to get hotel data for ${booking.hotelId}`,
          {
            hotelId: booking.hotelId,
          }
        )

        throw serverErrorByStatus(404)
      }

      metricsGetBooking.success()

      return {
        ...hotelData,
        booking,
        room: getBookedHotelRoom(
          hotelData.roomCategories,
          booking.roomTypeCode
        ),
      }
    }),
  findBooking: safeProtectedServiceProcedure
    .input(findBookingInput)
    .query(async function ({
      ctx,
      input: { confirmationNumber, lastName, firstName, email },
    }) {
      const findBookingCounter = createCounter("trpc.booking", "findBooking")
      const metricsFindBooking = findBookingCounter.init({ confirmationNumber })

      metricsFindBooking.start()

      const booking = await findBooking(
        confirmationNumber,
        ctx.lang,
        ctx.serviceToken,
        lastName,
        firstName,
        email
      )

      if (!booking) {
        metricsFindBooking.dataError(
          `Fail to find booking data for ${confirmationNumber}`,
          { confirmationNumber }
        )
        return null
      }

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

      if (!hotelData) {
        metricsFindBooking.dataError(
          `Failed to find hotel data for ${booking.hotelId}`,
          {
            hotelId: booking.hotelId,
          }
        )

        throw serverErrorByStatus(404)
      }

      metricsFindBooking.success()

      return {
        ...hotelData,
        booking,
        room: getBookedHotelRoom(
          hotelData.roomCategories,
          booking.roomTypeCode
        ),
      }
    }),
  linkedReservations: safeProtectedServiceProcedure
    .input(getLinkedReservationsInput)
    .concat(refIdPlugin.toConfirmationNumber)
    .use(async ({ ctx, input, next }) => {
      const lang = input.lang ?? ctx.lang
      return next({
        ctx: {
          lang,
        },
      })
    })
    .query(async function ({ ctx }) {
      const { confirmationNumber, lang, serviceToken } = ctx

      const getLinkedReservationsCounter = createCounter(
        "trpc.booking",
        "linkedReservations"
      )
      const metricsGetLinkedReservations = getLinkedReservationsCounter.init({
        confirmationNumber,
      })

      metricsGetLinkedReservations.start()

      const booking = await getBooking(confirmationNumber, lang, serviceToken)

      if (!booking) {
        return []
      }

      const linkedReservationsResults = await Promise.allSettled(
        booking.linkedReservations.map((linkedReservation) =>
          getBooking(linkedReservation.confirmationNumber, lang, serviceToken)
        )
      )

      const linkedReservations = []
      for (const linkedReservationsResult of linkedReservationsResults) {
        if (linkedReservationsResult.status === "fulfilled") {
          if (linkedReservationsResult.value) {
            linkedReservations.push(linkedReservationsResult.value)
          } else {
            metricsGetLinkedReservations.dataError(
              `Unexpected value for linked reservation`
            )
          }
        } else {
          metricsGetLinkedReservations.dataError(
            `Failed to get linked reservation`
          )
        }
      }

      metricsGetLinkedReservations.success()

      return linkedReservations
    }),
  status: serviceProcedure
    .input(getBookingStatusInput)
    .concat(refIdPlugin.toConfirmationNumber)
    .query(async function ({ ctx, input }) {
      const lang = input.lang ?? ctx.lang
      const { confirmationNumber } = ctx
      const language = toApiLang(lang)

      const getBookingStatusCounter = createCounter("trpc.booking", "status")
      const metricsGetBookingStatus = getBookingStatusCounter.init({
        confirmationNumber,
      })

      metricsGetBookingStatus.start()

      const apiResponse = await api.get(
        api.endpoints.v1.Booking.status(confirmationNumber),
        {
          headers: {
            Authorization: `Bearer ${ctx.serviceToken}`,
          },
        },
        {
          language,
        }
      )

      if (!apiResponse.ok) {
        await metricsGetBookingStatus.httpError(apiResponse)
        throw serverErrorByStatus(apiResponse.status, apiResponse)
      }

      const apiJson = await apiResponse.json()
      const verifiedData = createBookingSchema.safeParse(apiJson)
      if (!verifiedData.success) {
        metricsGetBookingStatus.validationError(verifiedData.error)
        throw badRequestError()
      }

      metricsGetBookingStatus.success()

      const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry

      return {
        booking: verifiedData.data,
        sig: encrypt(expire.toString()),
      }
    }),
  createRefId: serviceProcedure
    .input(createRefIdInput)
    .mutation(async function ({ input }) {
      const { confirmationNumber, lastName } = input
      const encryptedRefId = encrypt(`${confirmationNumber},${lastName}`)

      if (!encryptedRefId) {
        throw serverErrorByStatus(422, "Was not able to encrypt ref id")
      }

      return {
        refId: encryptedRefId,
      }
    }),
})
