import {
	ApprovedOpenReservationFilter,
	OpenReservationFilter,
	Table,
	TableSubscriber,
	TableType,
} from '@backend/domain'
import {
	ReservationRepository,
	TableCombinationRepository,
	TableRepository,
	TableSubscriberRepository,
	TableTokenRepository,
	UserRepository,
} from '@backend/repository'
import { ReservationState } from '@core/types'
import { removeNulls, sortTablesByCapacityAsc, sortTablesByPriority, sortTablesByRoomPlan } from '@core/utils'
import { Injectable, NotFoundException } from '@nestjs/common'
import { differenceBy, filter, includes, map, orderBy, some, sumBy } from 'es-toolkit/compat'
import { In, MoreThanOrEqual, Not } from 'typeorm'
import { Transactional } from 'typeorm-transactional'
import { TableMapper } from '../mappers'
import {
	FindAvailableTableByIdRequest,
	FindAvailableTablesByTimeSlotRequest,
	FindAvailableTablesByTimeSlotResponse,
	SaveTableRequest,
	TableResponse,
} from '../types'

@Injectable()
export class TableService {
	constructor(
		private readonly _userRepository: UserRepository,
		private readonly _tableRepository: TableRepository,
		private readonly _tableTokenRepository: TableTokenRepository,
		private readonly _reservationRepository: ReservationRepository,
		private readonly _tableSubscriberRepository: TableSubscriberRepository,
		private readonly _tableCombinationRepository: TableCombinationRepository,
	) {}

	async findAll(): Promise<TableResponse[]> {
		const tables = await this._tableRepository.find({ relations: { subscribers: { user: true } } })
		return map(tables, (table) => TableMapper.entityToResponse(table))
	}

	async findById(id: string): Promise<TableResponse> {
		const table = await this._tableRepository.findOne({ where: { id }, relations: { subscribers: { user: true } } })
		if (!table) {
			throw new NotFoundException('Table not found')
		}

		return TableMapper.entityToResponse(table)
	}

	@Transactional()
	async saveRoomPlan(tables: SaveTableRequest[]): Promise<TableResponse[]> {
		const existingTables = await this._tableRepository.find()
		const updatedTables = tables.filter((_table) => !!_table.id)

		const deletedTableIds = differenceBy(existingTables, updatedTables, 'id').map((_table) => _table.id)
		await this._tableRepository.softDelete({ id: In(deletedTableIds) })

		const users = await this._userRepository.find()
		for (const _table of tables) {
			const table: Partial<Table> = {
				id: _table.id,
				name: _table.name,
				capacity: _table.capacity,
				width: _table.width,
				height: _table.height,
				columnStart: _table.columnStart,
				rowStart: _table.rowStart,
				shape: _table.shape,
				priority: _table.priority,
			}
			const entity = await this._tableRepository.save(table)

			// Remove all existing subscribers
			if (_table.id) {
				await this._tableSubscriberRepository.delete({ table: { id: _table.id } })
			}
			// Add new subscribers
			if (_table.subscribers && _table.subscribers.length > 0) {
				const subscribers = _table.subscribers.map((userId) => {
					const user = users.find((_user) => _user.id === userId)!
					return { user, table: entity } as Partial<TableSubscriber>
				})
				await this._tableSubscriberRepository.save(subscribers)
			}
			// Generate token for new table
			if (!_table.id) {
				await this._tableTokenRepository.save({ tableId: entity.id })
			}
		}

		return this._tableRepository
			.find({ relations: { subscribers: { user: true } } })
			.then((tables) => map(tables, (table) => TableMapper.entityToResponse(table)))
	}

	async findAllSubscribedTablesByUserId(userId: string): Promise<TableResponse[]> {
		const subscriptions = await this._tableSubscriberRepository.find({
			where: { user: { id: userId } },
			relations: { table: true },
		})
		return subscriptions.map((subscription) => TableMapper.entityToResponse(subscription.table))
	}

	@Transactional()
	async saveUserTableSubscriptions(tableIds: string[], userId: string): Promise<void> {
		const user = await this._userRepository.findOneBy({ id: userId })
		if (!user) {
			throw new NotFoundException('User not found')
		}

		// Delete all existing subscriptions
		await this._tableSubscriberRepository.delete({ user: { id: userId } })

		if (tableIds.length === 0) return
		// Save new subscriptions
		const subscribers: Partial<TableSubscriber>[] = []
		for (const tableId of tableIds) {
			const table = await this._tableRepository.findOneBy({ id: tableId })
			if (table !== null) {
				subscribers.push({ user, table })
			}
		}
		await this._tableSubscriberRepository.save(subscribers)
	}

	async findAvailableTablesByTimeSlot(
		request: FindAvailableTablesByTimeSlotRequest,
	): Promise<FindAvailableTablesByTimeSlotResponse[]> {
		request.start = new Date(request.start)
		request.end = new Date(request.end)

		const { start, end, noOfPersons } = request

		// Find all open reservations with overlapping time
		const reservations = await this._reservationRepository.findByInterval(start, end, {
			where: { state: In(OpenReservationFilter) },
			relations: { tables: true },
		})
		const unconfirmedReservations = reservations.filter((res) => res.state === ReservationState.unconfirmed)

		const occupiedTables = reservations
			.filter((res) => res.tables && ApprovedOpenReservationFilter.includes(res.state))
			.flatMap((res) => map(res.tables, 'id'))
			.filter(removeNulls)

		const maxCapacity = await this._tableRepository.findMaxCapacity()

		const response: FindAvailableTablesByTimeSlotResponse[] = []

		// Find single tables that fit noOfPersons
		if (maxCapacity && noOfPersons <= maxCapacity) {
			const availableTables = await this._tableRepository.find({
				where: { id: Not(In(occupiedTables)), capacity: MoreThanOrEqual(noOfPersons) },
			})

			const sortedTables = sortTablesByCapacityAsc(sortTablesByPriority(sortTablesByRoomPlan(availableTables)))

			let tables: FindAvailableTablesByTimeSlotResponse[] = sortedTables.map((table) => ({
				id: table.id,
				name: table.name,
				capacity: table.capacity,
				type: TableType.table,
				hasUnconfirmedReservation: some(unconfirmedReservations, (reservation) =>
					includes(map(reservation.tables, 'id'), table.id),
				),
			}))
			// Tables with unconfirmed reservations at bottom
			tables = orderBy(tables, ['hasUnconfirmedReservation'], ['asc'])
			response.push(...tables)
		}

		let combinations = await this._tableCombinationRepository
			.find({ relations: { tables: true } })
			.then((combinations) => filter(combinations, (combination) => combination.capacity >= noOfPersons))

		// Filter out occupied tables from combinations
		combinations = map(combinations, (combination) => {
			combination.tables = filter(combination.tables, (table) => !includes(occupiedTables, table.id))
			return combination
		})

		combinations = filter(combinations, (combination) => sumBy(combination.tables, 'capacity') >= noOfPersons)

		// Sort combinations by capacity
		const sortedCombinations: FindAvailableTablesByTimeSlotResponse[] = orderBy(
			combinations,
			['capacity'],
			['asc'],
		).map((combination) => ({
			id: combination.id,
			name: combination.name,
			tableNames: combination.tables.map((table) => table.name),
			capacity: combination.capacity,
			type: TableType.tableCombination,
		}))

		response.push(...sortedCombinations)

		// Put combinations with unconfirmed reservations at bottom
		return orderBy(response, ['hasUnconfirmedReservation'], ['asc'])
	}

	async findAvailableTablesById(
		request: FindAvailableTableByIdRequest,
	): Promise<FindAvailableTablesByTimeSlotResponse> {
		const { id, type } = request

		if (type === TableType.table) {
			const table = await this._tableRepository.findOneBy({ id })
			if (!table) throw new NotFoundException('Table not found')

			return {
				id: table.id,
				name: table.name,
				capacity: table.capacity,
				type: TableType.table,
				hasUnconfirmedReservation: false,
			}
		} else {
			const combination = await this._tableCombinationRepository.findOneBy({ id })
			if (!combination) throw new NotFoundException('Table combination not found')

			return {
				id: combination.id,
				name: combination.name,
				capacity: combination.capacity,
				type: TableType.tableCombination,
				hasUnconfirmedReservation: false,
			}
		}
	}
}
