import { CustomerEvent, CustomerEventType, Order, OrderState, Reservation } from '@backend/domain'
import { CustomerEventRepository, OrderRepository, ReservationRepository, ReviewsRepository } from '@backend/repository'
import { ReservationState } from '@core/types'
import { DateUtils } from '@core/utils'
import { Injectable } from '@nestjs/common'
import { differenceInDays, subDays } from 'date-fns'
import { groupBy } from 'es-toolkit/compat'
import { Between, In } from 'typeorm'
import { ChartDataPoint, StatisticsResponse } from '../types'

@Injectable()
export class StatisticsService {
	constructor(
		private readonly _orderRepository: OrderRepository,
		private readonly _reviewsRepository: ReviewsRepository,
		private readonly _reservationRepository: ReservationRepository,
		private readonly _customerEventsRepository: CustomerEventRepository,
	) {}

	async getStatisticsData(): Promise<StatisticsResponse> {
		const weekStart = subDays(DateUtils.normalizeDay(new Date()), 7)
		const weekEnd = DateUtils.normalizeDayEnd(new Date())

		const orders = await this._orderRepository.find({
			where: { createdAt: Between(weekStart, weekEnd), state: OrderState.done },
			relations: { items: { menuItem: true } },
			select: { items: { name: true, quantity: true, menuItem: { id: true } } },
		})
		const topDishes = this.calculateTopDishes(orders)

		const customerEvents = await this._customerEventsRepository.find({
			where: {
				createdAt: Between(weekStart, weekEnd),
				type: In([
					CustomerEventType.order_requested,
					CustomerEventType.payment_request,
					CustomerEventType.waiter_requested,
				]),
			},
			select: { createdAt: true, type: true },
		})
		const customerEventsData = this.mapCustomerEvents(customerEvents)

		const reservations = await this._reservationRepository.find({
			where: { startDate: Between(weekStart, weekEnd), state: ReservationState.done },
			select: { startDate: true },
		})
		const rushHourData = this.mapRushHourData(reservations)

		const reviews = await this._reviewsRepository.findOne({})

		const response: StatisticsResponse = {
			topDishes,
			rushHourData,
			customerEventsData,
			reviews: reviews?.reviews,
			ratings: reviews ? { averageRating: reviews?.rating, totalRatings: reviews?.totalRatings } : undefined,
		}
		return response
	}

	private calculateTopDishes(orders: Order[]): string[] {
		const orderItems = orders
			.map((order) => order.items.map((item) => ({ id: item.menuItem.id, name: item.name, quantity: item.quantity })))
			.reduce((previous, current) => [...current, ...previous], [])

		const groupItems = groupBy(orderItems, 'id')
		const resultItems: Array<{ id: string; name: string; quantity: number }> = []

		for (const itemId in groupItems) {
			const itemsByGroup = groupItems[itemId]
			const totalQuantity = itemsByGroup.map((item) => item.quantity).reduce((current, sum) => current + sum, 0)
			resultItems.push({ id: itemId, name: itemsByGroup[0].name, quantity: totalQuantity })
		}

		return resultItems
			.sort((aItem, bItem) => aItem.quantity - bItem.quantity)
			.map((item) => item.name)
			.slice(0, 5)
	}

	private mapRushHourData(reservations: Reservation[]): ChartDataPoint[] {
		const reservationsByDay = groupBy(reservations, (reservation) => reservation.startDate.getDay())
		const result: Array<{ label: string; data: number[] }> = []

		for (const day in reservationsByDay) {
			const reservationsByHour = groupBy(reservationsByDay[day], (reservation) => reservation.startDate.getHours())

			const dataPoints = Array.from<number>({ length: 24 }).fill(0)
			for (const hour in reservationsByHour) {
				dataPoints[Number(hour)] = reservationsByHour[hour].length
			}

			result.push({ label: day, data: dataPoints })
		}

		return result
	}

	private mapCustomerEvents(events: CustomerEvent[]): ChartDataPoint[] {
		const groupEvents = groupBy(events, 'type')
		const result: Array<{ label: string; data: number[] }> = []

		for (const eventType in groupEvents) {
			const eventsByType = groupEvents[eventType]
			const eventsByDay = groupBy(eventsByType, (event) => 6 - differenceInDays(new Date(), event.createdAt))

			const dataPoints = Array.from<number>({ length: 7 }).fill(0)
			for (const day in eventsByDay) {
				dataPoints[Number(day)] = eventsByDay[day].length
			}

			result.push({ label: this.getCustomerEventTypeText(eventType), data: dataPoints })
		}

		return result
	}

	private getCustomerEventTypeText(type: string): string {
		switch (type) {
			case CustomerEventType.payment_request: {
				return 'Pay'
			}
			case CustomerEventType.order_requested: {
				return 'Order'
			}
			case CustomerEventType.waiter_requested: {
				return 'Call Waiter'
			}
			default: {
				return ''
			}
		}
	}
}
