import { OpenReservationFilter, Reservation, ReservationSource } from '@backend/domain'
import { ReservationRepository, SettingsRepository } from '@backend/repository'
import { NotificationEventType, PropertyUpdateMap, ReservationState } from '@core/types'
import { InjectQueue } from '@nestjs/bullmq'
import { Injectable, Logger } from '@nestjs/common'
import { map, sumBy } from 'es-toolkit/compat'
import { In } from 'typeorm'
import { GOOGLE_RESERVATIONS_QUEUES, GoogleReatimeUpdateQueue } from '../../../core'
import { EventService } from '../../../global'
import { ReservationEmailEvent, ReservationUpdates } from '../types'
import { ReservationEmailService } from './email.service'

type NotifyReservationUpdateParams = {
	updatedReservation: Reservation
	oldReservation?: Reservation
	updates?: ReservationUpdates
	/** Semantic event for email selection. Defaults to a state-based derivation for backward compatibility. */
	event?: ReservationEmailEvent
}

@Injectable()
export class ReservationUtilsService {
	private readonly logger = new Logger(ReservationUtilsService.name)

	constructor(
		private readonly _eventService: EventService,
		private readonly _emailService: ReservationEmailService,
		private readonly _settingsRepository: SettingsRepository,
		private readonly _reservationRepository: ReservationRepository,
		@InjectQueue(GOOGLE_RESERVATIONS_QUEUES.SEND_REALTIME_UPDATE)
		private readonly _realtimeUpdateQueue: GoogleReatimeUpdateQueue,
	) {}

	/**
	 * Checks if the given {@link Reservation} can be auto approved
	 * @param reservation The reservation to check
	 * @returns true if the reservation can be auto approved
	 */
	async canAutoApproveReservation(reservation: Reservation): Promise<boolean> {
		// If reservation has not been assigned tables
		if (!reservation.tables) return false

		const settings = await this._settingsRepository
			.findOne({ select: { reservation: {} } })
			.then((settings) => settings?.reservation)

		// If auto approving reservation is disabled
		if (!settings || !settings.autoApproveReservations.enabled) {
			return false
		}

		// If no of people exceeds guestLimitPerReservation
		if (reservation.noOfPersons > settings.autoApproveReservations.guestLimitPerReservation) {
			return false
		}

		// Find no of expected guests at same time
		const expectedGuests = await this._reservationRepository
			.findByInterval(reservation.startDate, reservation.endDate, {
				where: { state: In(OpenReservationFilter) },
				select: { noOfPersons: true },
			})
			.then((reservations) => sumBy(map(reservations, 'noOfPersons'), 'noOfPersons'))

		// If overall no of guests exceeds guestLimitOverall
		if (reservation.noOfPersons + expectedGuests > settings.autoApproveReservations.guestLimitOverall) {
			return false
		}

		// If all conditions match
		return true
	}

	/**
	 * Notifies about the new reservation
	 * @param reservation The reservation that was created
	 */
	notifyNewReservation(reservation: Reservation): void {
		this._eventService.send(NotificationEventType.reservations_updated)

		const event =
			reservation.state === ReservationState.approved
				? ReservationEmailEvent.createdConfirmed
				: ReservationEmailEvent.createdPending

		// Fire-and-forget: a failing email must never become an unhandled
		// rejection — that terminates the Node process and kills the task
		this._emailService.notifyEvent(event, reservation).catch((error) =>
			this.logger.error({
				message: 'Failed to send email for new reservation',
				reservationId: reservation.id,
				tenantId: reservation.tenantId,
				error: error instanceof Error ? error.message : error,
			}),
		)
	}

	/**
	 * Notifies about reservation update
	 * @param reservation The reservation that was updated
	 */
	async notifyReservationUpdate({
		updatedReservation,
		oldReservation,
		updates,
		event,
	}: NotifyReservationUpdateParams): Promise<void> {
		this._eventService.send(NotificationEventType.reservations_updated)

		if (!updates) {
			if (!oldReservation) {
				this.logger.error("One of 'oldReservation' or 'updates' must be provided")
				return
			}

			updates = this.createReservationUpdateMap(updatedReservation, oldReservation)
		}

		await this._emailService.notifyEvent(event ?? this.deriveEvent(updatedReservation, updates), updatedReservation, updates)

		if (updatedReservation.source === ReservationSource.google) {
			await this.addToRealtimeUpdateQueue(updatedReservation.id, updates)
		}
	}

	/** Fallback mapping for callers that do not pass an explicit event. */
	private deriveEvent(reservation: Reservation, updates: ReservationUpdates): ReservationEmailEvent {
		const stateUpdate = updates.find((update) => update?.field === 'state')
		if (stateUpdate) {
			switch (stateUpdate.newValue as ReservationState) {
				case ReservationState.approved: {
					return ReservationEmailEvent.confirmed
				}
				case ReservationState.rejected: {
					return ReservationEmailEvent.cancelledByRestaurant
				}
				case ReservationState.unconfirmed: {
					return ReservationEmailEvent.pending
				}
				default: {
					return ReservationEmailEvent.updated
				}
			}
		}
		return ReservationEmailEvent.updated
	}

	private createReservationUpdateMap(reservation: Reservation, oldReservation: Reservation): ReservationUpdates {
		const updates: ReservationUpdates = []

		for (const fieldName of Object.keys(reservation)) {
			const field = fieldName as keyof Reservation
			const oldValue = oldReservation[field]
			const newValue = reservation[field]

			if (newValue !== oldValue) {
				updates.push({ field, oldValue, newValue } as PropertyUpdateMap<Reservation>)
			}
		}
		return updates
	}

	private async addToRealtimeUpdateQueue(reservationId: string, updates: ReservationUpdates): Promise<void> {
		await this._realtimeUpdateQueue.add('google_reservation_update', { reservationId, updates })
	}
}
