import { Order, OrderItem, OrderItemState, OrderState, Reservation } from '@backend/domain'
import {
	MenuItemRepository,
	OrderItemRepository,
	OrderRepository,
	ReservationRepository,
	SettingsRepository,
	TableRepository,
} from '@backend/repository'
import { NotificationEventType, ReservationState } from '@core/types'
import {
	BadRequestException,
	Injectable,
	InternalServerErrorException,
	NotAcceptableException,
	UnprocessableEntityException,
} from '@nestjs/common'
import { addMinutes, subMinutes } from 'date-fns'
import { map, omit } from 'es-toolkit/compat'
import { Between, In } from 'typeorm'
import { Transactional } from 'typeorm-transactional'
import { EventService } from '../../../global'
import { OrderMapper } from '../mappers'
import {
	CheckinRequest,
	CreateOrderItemRequest,
	OrderResponse,
	PayOrderItemRequest,
	UpdateItemsStateRequest,
} from '../types'
import { OrderEmailService } from './email.service'

const DEFAULT_TAX_RATE = 19

@Injectable()
export class OrderService {
	constructor(
		private readonly _eventService: EventService,
		private readonly _emailService: OrderEmailService,
		private readonly _tableRepository: TableRepository,
		private readonly _orderRepository: OrderRepository,
		private readonly _settingsRepository: SettingsRepository,
		private readonly _menuItemRepository: MenuItemRepository,
		private readonly _orderItemRepository: OrderItemRepository,
		private readonly _reservationRepository: ReservationRepository,
	) {}

	async findAllOpenOrders(): Promise<OrderResponse[]> {
		const orders = await this._orderRepository.find({
			where: { state: OrderState.open },
			relations: { tables: true, reservation: true, items: { menuItem: true } },
		})

		return orders.map((order) => OrderMapper.entityToResponse(order))
	}

	async findByReservationIds(reservationIds: string[]): Promise<OrderResponse[]> {
		const orders = await this._orderRepository.find({
			where: { reservation: { id: In(reservationIds) } },
			relations: { tables: true, reservation: true, items: { menuItem: true } },
		})

		return orders.map((order) => OrderMapper.entityToResponse(order))
	}

	@Transactional()
	async checkin(request: CheckinRequest, customerRequest = false): Promise<OrderResponse> {
		if (!request.reservationId && !request.tableIds && !request.tableId) {
			throw new UnprocessableEntityException('One of tableId, tableIds or reservationId must be provided')
		}

		// Check if verification is required for reservation
		if (customerRequest) {
			const reservation = await this._reservationRepository.findOne({
				where: {
					tables: { id: In(request.tableIds ?? [request.tableId]) },
					startDate: Between(subMinutes(new Date(), 30), addMinutes(new Date(), 30)),
					state: ReservationState.approved,
				},
			})
			if (reservation) {
				throw new NotAcceptableException('Reservation verification required')
			}
		}

		let reservation: Reservation | undefined

		if (request.reservationId) {
			const foundReservation = await this._reservationRepository.findOne({
				where: { id: request.reservationId },
				relations: { tables: true, order: { items: true, tables: true } },
			})
			if (!foundReservation) {
				throw new UnprocessableEntityException('Reservation not found')
			}
			if (foundReservation.order && foundReservation.order.state !== OrderState.cancelled) {
				throw new UnprocessableEntityException('Reservation already has an order')
			}
			if (foundReservation.state === ReservationState.unconfirmed) {
				throw new UnprocessableEntityException('Reservation has not been confirmed yet')
			}
			if (foundReservation.state === ReservationState.rejected) {
				throw new UnprocessableEntityException('Reservation has been rejected')
			}
			if (foundReservation.state === ReservationState.seated) {
				throw new UnprocessableEntityException('Reservation is already open')
			}
			if (foundReservation.state === ReservationState.done) {
				throw new UnprocessableEntityException('Reservation has already been closed')
			}

			reservation = foundReservation
		}

		const tableIds = reservation ? map(reservation?.tables, 'id') : (request.tableIds ?? [request.tableId])
		if (tableIds.length === 0) {
			throw new UnprocessableEntityException('Table not provided for order')
		}

		const tables = await this._tableRepository.find({ where: { id: In(tableIds) } })
		if (tables.length !== tableIds.length) {
			throw new UnprocessableEntityException('Invalid tables')
		}

		const alreadyOpenOrder = await this._orderRepository.findOne({
			where: { tables: { id: In(tableIds) }, state: OrderState.open },
			relations: { tables: true, items: { menuItem: true } },
		})

		if (alreadyOpenOrder) {
			if (customerRequest) {
				return OrderMapper.entityToResponse(alreadyOpenOrder)
			}
			throw new BadRequestException('Table already has an open order')
		}

		if (reservation) {
			await this._reservationRepository.update({ id: reservation.id }, { state: ReservationState.seated })

			// If check-in of cancelled order
			if (!customerRequest && reservation.order?.state === OrderState.cancelled) {
				await this._orderRepository.update({ id: reservation.order.id }, { state: OrderState.open })
				return OrderMapper.entityToResponse(reservation.order)
			}
		}

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

		const order: Partial<Order> = {
			currencyCode,
			outstandingAmount: 0,
			paidAmount: 0,
			baseAmount: 0,
			// TODO! Get this value from company preferences
			taxPercentage: DEFAULT_TAX_RATE,
			taxAmount: 0,
			totalAmount: 0,
			state: OrderState.open,
			tables,
			reservation,
		}

		const entity = await this._orderRepository.save(order)

		this._eventService.send(NotificationEventType.orders_updated)
		if (request.reservationId) {
			this._eventService.send(NotificationEventType.reservations_updated)
		}

		return OrderMapper.entityToResponse(entity)
	}

	@Transactional()
	async addOrderItems(orderId: string, request: CreateOrderItemRequest[]): Promise<OrderResponse> {
		if (request.length === 0) {
			throw new BadRequestException('Request must contain atleast 1 order item')
		}

		const order = await this._orderRepository.findOne({ where: { id: orderId }, relations: { items: true } })
		if (!order) {
			throw new BadRequestException('Order not found')
		}
		if (order.state !== OrderState.open) {
			throw new BadRequestException('Order is not open')
		}

		const menuItems = await this._menuItemRepository.find({
			where: { id: In(request.map((item) => item.menuItemId)) },
			select: { id: true, name: true, unit: true, price: true },
		})
		const orderItems: Partial<OrderItem>[] = []

		for (const requestItem of request) {
			const menuItem = menuItems.find((item) => item.id === requestItem.menuItemId)
			if (!menuItem) {
				throw new BadRequestException('Menu item not found')
			}

			const orderItem: Partial<OrderItem> = {
				name: menuItem.name,
				quantity: requestItem.quantity,
				quantityUnit: menuItem.unit,
				unitPrice: menuItem.price,
				paidQuantity: 0,
				paid: false,
				state: OrderItemState.pending,
				order,
				menuItem,
			}
			orderItems.push(orderItem)
		}

		const items = await this._orderItemRepository.save(orderItems)
		order.setItems(items)

		await this._orderRepository.update({ id: order.id }, omit(order, 'items'))

		const orderEntity = await this._orderRepository.findOne({
			where: { id: orderId },
			relations: { tables: true, items: { menuItem: true } },
		})

		if (!orderEntity) {
			throw new InternalServerErrorException('Order not found after update')
		}

		this._eventService.send(NotificationEventType.orders_updated)

		return OrderMapper.entityToResponse(orderEntity)
	}

	@Transactional()
	async payItemsViaCash(orderId: string, request: PayOrderItemRequest[]): Promise<void> {
		if (request.length === 0) {
			throw new UnprocessableEntityException('Please provide atleast 1 item')
		}

		const order = await this._orderRepository.findOne({ where: { id: orderId }, relations: { items: true } })
		if (!order) {
			throw new UnprocessableEntityException('Order not found')
		}

		const orderItems = await this._orderItemRepository.find({ where: { id: In(request.map(({ id }) => id)) } })
		const itemsToUpdate: Partial<OrderItem>[] = []
		for (const requestItem of request) {
			const orderItem = orderItems.find((item) => item.id === requestItem.id)
			if (!orderItem) {
				throw new UnprocessableEntityException('Order item not found')
			}

			try {
				orderItem.payQuantity(requestItem.quantity)
			} catch (error) {
				throw new UnprocessableEntityException(error.message)
			}

			itemsToUpdate.push(orderItem)
		}

		await this._orderItemRepository.save(orderItems)

		// Replace existing order items with updated values
		const items = order.items.map(
			(orderItem) => orderItems.find((requestItem) => requestItem.id === orderItem.id) || orderItem,
		)
		order.setItems(items)

		await this._orderRepository.update({ id: orderId }, omit(order, 'items'))
		this._eventService.send(NotificationEventType.orders_updated)
	}

	@Transactional()
	async checkout(id: string): Promise<void> {
		const order = await this._orderRepository.findOne({
			where: { id },
			relations: { reservation: true },
		})

		if (!order) {
			throw new BadRequestException('Order not found')
		}
		if (order.state !== OrderState.open) {
			throw new BadRequestException('Only open orders can be checkedout')
		}
		if (order.outstandingAmount > 0) {
			throw new BadRequestException('Order cannot be checkedout with outstanding amount')
		}

		await this._orderRepository.update({ id }, { state: OrderState.done })

		this._eventService.send(NotificationEventType.orders_updated)

		if (order.reservation) {
			await this._reservationRepository.update({ id: order.reservation.id }, { state: ReservationState.done })
			this._eventService.send(NotificationEventType.reservations_updated)
			await this._emailService.sendFeedbackEmail(order.reservation)
		}
	}

	@Transactional()
	async cancelOrder(id: string): Promise<void> {
		const order = await this._orderRepository.findOne({
			where: { id },
			relations: { reservation: true },
		})
		if (!order) {
			throw new BadRequestException('Order not found')
		}
		if (order.state !== OrderState.open) {
			throw new BadRequestException('Only open orders can be cancelled')
		}

		await this._orderRepository.update({ id }, { state: OrderState.cancelled })

		this._eventService.send(NotificationEventType.orders_updated)

		if (order.reservation) {
			await this._reservationRepository.update({ id: order.reservation.id }, { state: ReservationState.approved })
			this._eventService.send(NotificationEventType.reservations_updated)
		}
	}

	@Transactional()
	async reOpenOrder(id: string): Promise<void> {
		const order = await this._orderRepository.findOne({
			where: { id: id },
			relations: { reservation: true, tables: true },
		})
		if (!order) {
			throw new BadRequestException('Order not found')
		}
		if (order.state !== OrderState.done) {
			throw new BadRequestException('Only completed orders can be re-opened')
		}

		const alreadyOpenOrder = await this._orderRepository.exist({
			where: { tables: { id: In(map(order.tables, 'id')) }, state: OrderState.open },
		})
		if (alreadyOpenOrder) {
			throw new BadRequestException('Table already has an open order')
		}

		await this._orderRepository.update({ id }, { state: OrderState.open })
		this._eventService.send(NotificationEventType.orders_updated)

		if (order.reservation) {
			await this._reservationRepository.update({ id: order.reservation.id }, { state: ReservationState.seated })
			this._eventService.send(NotificationEventType.reservations_updated)
		}
	}

	async updateItemsState(orderId: string, request: UpdateItemsStateRequest): Promise<void> {
		if (request.itemIds.length === 0) {
			throw new BadRequestException('Please provide atleast 1 item')
		}

		const order = await this._orderRepository.findOne({ where: { id: orderId }, select: { state: true } })
		if (!order) {
			throw new BadRequestException('Order not found')
		}
		if (order.state !== OrderState.open) {
			throw new BadRequestException('Order is not open')
		}

		await this._orderItemRepository.update({ id: In(request.itemIds) }, { state: request.state })
		this._eventService.send(NotificationEventType.orders_updated)
	}

	async updateItemCourse(orderId: string, itemId: string, course: number): Promise<void> {
		if (course <= 0) {
			throw new BadRequestException('Course must be greater than 0')
		}

		const order = await this._orderRepository.findOne({ where: { id: orderId }, select: { state: true } })
		if (!order) {
			throw new BadRequestException('Order not found')
		}
		if (order.state !== OrderState.open) {
			throw new BadRequestException('Order is not open')
		}

		await this._orderItemRepository.update({ id: itemId }, { course })
		this._eventService.send(NotificationEventType.orders_updated)
	}
}
