import { Reservation } from '@backend/domain'
import { DateUtils } from '@core/utils'
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { addSeconds, isAfter, isBefore, subSeconds } from 'date-fns'
import { FindManyOptions, In, LessThan, MoreThan, Repository } from 'typeorm'
import { UserModifiableEntityRepository } from './generic'

@Injectable()
export class ReservationRepository extends UserModifiableEntityRepository<Reservation> {
	constructor(@InjectRepository(Reservation) override readonly _repository: Repository<Reservation>) {
		super(_repository)
	}

	/**
	 * Serializes booking creation per tenant using a Postgres advisory lock that is
	 * held until the surrounding transaction ends. Must be called inside an active
	 * transaction (e.g. via @Transactional()).
	 */
	async acquireBookingLock(tenantId: string): Promise<void> {
		await this.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [tenantId])
	}

	/**
	 * Find all reservations that overlap the given time interval
	 * The search is time exlusive, i.e. reservations that start or end at the exact
	 * same time as provided start or end time are filtered out
	 * e.g if start is 18:00 it doesn't return reservations that end at 18:00 and similarly
	 * if end is 22:00 it doesn't return reservations that start at 22:00
	 *
	 * @param start Start time of the interval
	 * @param end End time of the interval
	 * @param options Additional options to pass to the repository find method
	 * @returns A list of reservations that overlap the given time interval
	 */
	async findByInterval(start: Date, end: Date, options?: FindManyOptions<Reservation>): Promise<Reservation[]> {
		return super
			.find({
				...options,
				where: {
					...options?.where,
					startDate: LessThan(end),
					endDate: MoreThan(start),
				},
			})
			.then((reservations) =>
				reservations.filter(
					(reservation) =>
						// Time exclusivity filter
						isBefore(start, subSeconds(reservation.endDate, 1)) && isAfter(end, addSeconds(reservation.startDate, 1)),
				),
			)
	}

	/** Find all reservations that start or end in same day as provided reservation */
	async findByDay(date: Date, options?: FindManyOptions<Reservation>): Promise<Reservation[]> {
		const start = DateUtils.normalizeDay(date)
		const end = DateUtils.normalizeDayEnd(date)

		return this.findByInterval(start, end, options)
	}

	async findByDayAndTables(
		tableIds?: string[],
		date?: Date,
		options?: FindManyOptions<Reservation>,
	): Promise<Reservation[]> {
		const filter: FindManyOptions<Reservation> = { ...options }

		if (tableIds) {
			filter.where = { ...filter.where, tables: { id: In(tableIds) } }
		}

		if (date) {
			return this.findByDay(date, filter)
		}

		return super.find(filter)
	}
}
