import { Reservation, Table, TableCombination } from '@backend/domain'
import { uniqBy } from 'es-toolkit'

/**
 * Calculates all party sizes that can be accommodated by the given list of tables
 * in a map of how many tables can accommodate each party size.
 *
 * Party sizes larger than the biggest single table can only be served by joining tables,
 * so the configured table combinations are added on top. Combinations are purely additive:
 * they never change the counts for party sizes that a single table already covers, which
 * keeps the availability feed and BatchAvailabilityLookup in sync for the existing sizes.
 * @param tables the tables to calculate the party sizes for.
 * @param combinations the configured table combinations (each with its `tables` loaded).
 * @returns a map of all party sizes and how many tables/combinations are available for each
 */
export function getAllPartySizes(tables: Table[], combinations: TableCombination[] = []): Map<number, number> {
	if (tables.length === 0) return new Map()

	const capacities = tables.map((table) => table.capacity)
	const maxSingleCapacity = Math.max(...capacities)
	const result = new Map<number, number>()

	for (let partySize = 1; partySize <= maxSingleCapacity; partySize++) {
		result.set(partySize, capacities.filter((capacity) => capacity >= partySize).length)
	}

	// Only party sizes above the biggest single table need combinations — below that they
	// are already covered, and adding combinations there would diverge feed and lookup.
	if (combinations.length > 0) {
		const maxCombinationCapacity = Math.max(...combinations.map((combination) => combination.capacity))

		for (let partySize = maxSingleCapacity + 1; partySize <= maxCombinationCapacity; partySize++) {
			const combinationCount = combinations.filter((combination) => combination.capacity >= partySize).length

			if (combinationCount > 0) result.set(partySize, combinationCount)
		}
	}

	return result
}

/**
 * Counts how many tables (or table combinations) that can seat the given party size are
 * still open for a time slot, given the reservations overlapping that slot. Reservations
 * must be loaded with their `tables` relation.
 *
 * Single source of truth for slot availability — used by both the Google availability feed
 * (spots_open) and the Booking Server BatchAvailabilityLookup, so both always agree.
 *
 * For party sizes that fit a single table the original per-table calculation is used
 * unchanged. For larger party sizes availability comes from table combinations: only
 * mutually disjoint, fully-free combinations are counted (greedy packing), so we never
 * report more simultaneous large-group slots than can actually be seated → no overbooking.
 * @param tables all tables of the restaurant.
 * @param overlappingReservations reservations overlapping the slot, with `tables` loaded.
 * @param partySize the requested party size; when omitted, all tables are considered.
 * @param combinations the configured table combinations (each with its `tables` loaded).
 * @returns the number of open tables/combinations that can seat the party size.
 */
export function getOpenTableCount(
	tables: Table[],
	overlappingReservations: Reservation[],
	partySize?: number,
	combinations: TableCombination[] = [],
): number {
	const occupiedTables = uniqBy(
		overlappingReservations.flatMap((reservation) => reservation.tables ?? []),
		(table) => table.id,
	)

	const maxSingleCapacity = tables.length > 0 ? Math.max(...tables.map((table) => table.capacity)) : 0

	// Party sizes that fit a single table keep the original calculation — unchanged source
	// of truth shared by the feed and the lookup.
	if (!partySize || partySize <= maxSingleCapacity) {
		const fitsPartySize = (table: Table) => (partySize ? table.capacity >= partySize : true)
		return tables.filter(fitsPartySize).length - occupiedTables.filter(fitsPartySize).length
	}

	// Party size exceeds every single table: availability comes from combinations only.
	const occupiedTableIds = new Set(occupiedTables.map((table) => table.id))
	const usedTableIds = new Set<string>()
	let openCombinations = 0

	// Smallest matching combinations first so the greedy packing keeps larger ones free.
	const matchingCombinations = combinations
		.filter((combination) => combination.capacity >= partySize)
		.sort((a, b) => a.capacity - b.capacity)

	for (const combination of matchingCombinations) {
		const memberTables = combination.tables ?? []
		if (memberTables.length === 0) continue

		const isFree = memberTables.every((table) => !occupiedTableIds.has(table.id) && !usedTableIds.has(table.id))
		if (!isFree) continue

		for (const table of memberTables) usedTableIds.add(table.id)
		openCombinations++
	}

	return openCombinations
}
