import { Category, MenuItem, MenuItemCategory } from '@backend/domain'
import { CategoryRepository, MenuItemCategoryRepository, MenuItemRepository } from '@backend/repository'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import { In } from 'typeorm'
import { Transactional } from 'typeorm-transactional'
import { MenuItemMapper } from '../mappers'
import {
	CreateMenuItemRequest,
	ImportMenuItemRequest,
	MenuItemCategoryRequest,
	MenuItemResponse,
	UpdateMenuItemRequest,
	UpdateSortOrderRequest,
} from '../types'

@Injectable()
export class MenuItemService {
	constructor(
		private readonly _menuItemRepository: MenuItemRepository,
		private readonly _categoryRepository: CategoryRepository,
		private readonly _itemCategoryRepository: MenuItemCategoryRepository,
	) {}

	async findAll(): Promise<MenuItemResponse[]> {
		const items = await this._menuItemRepository.find({
			relations: { categories: { category: { parentCategory: true } } },
		})

		return items.map((item) => MenuItemMapper.entityToResponse(item))
	}

	async findById(id: string): Promise<MenuItemResponse> {
		const item = await this._menuItemRepository.findOne({
			where: { id },
			relations: { categories: { category: { parentCategory: true } } },
		})
		if (!item) {
			throw new NotFoundException('Item not found')
		}

		return MenuItemMapper.entityToResponse(item)
	}

	@Transactional()
	async save(request: CreateMenuItemRequest): Promise<MenuItemResponse> {
		const item: Partial<MenuItem> = {
			name: request.name,
			description: request.description,
			price: request.price,
			currency: request.currency,
			amount: request.amount,
			unit: request.unit,
			allergens: request.allergens,
			coverUrl: request.coverUrl,
		}

		const entity = await this._menuItemRepository.save(item)

		if (request.subcategories) request.categories.push(...request.subcategories)
		await this.saveItemCategories(entity, request.categories)

		const res = (await this._menuItemRepository.findOne({
			where: { id: entity.id },
			relations: { categories: { category: { parentCategory: true } } },
		}))!

		return MenuItemMapper.entityToResponse(res)
	}

	@Transactional()
	async updateById(id: string, request: UpdateMenuItemRequest): Promise<MenuItemResponse> {
		if (id !== request.id) {
			throw new BadRequestException()
		}

		const existing = await this._menuItemRepository.findOne({
			where: { id },
			relations: { categories: { category: true } },
		})
		if (!existing) {
			throw new NotFoundException('Item not found')
		}

		const item: Partial<MenuItem> = {
			name: request.name,
			description: request.description,
			price: request.price,
			currency: request.currency,
			amount: request.amount,
			unit: request.unit,
			allergens: request.allergens,
			coverUrl: request.coverUrl,
		}
		await this._menuItemRepository.update({ id }, item)

		// Delete all existing categories
		const existingCategoryIds = existing.categories.map((_category) => _category.id)
		await this._itemCategoryRepository.delete({ id: In(existingCategoryIds) })

		// Save new categories
		if (request.subcategories) request.categories.push(...request.subcategories)
		await this.saveItemCategories(existing, request.categories)

		const res = (await this._menuItemRepository.findOne({
			where: { id },
			relations: { categories: { category: { parentCategory: true } } },
		}))!

		return MenuItemMapper.entityToResponse(res)
	}

	async deleteById(id: string): Promise<void> {
		const exists = await this._menuItemRepository.exist({ where: { id } })
		if (!exists) {
			throw new NotFoundException('Item not found')
		}

		await this._menuItemRepository.softDelete({ id })
	}

	async toggleArchive(id: string, isArchived: boolean): Promise<void> {
		const exists = await this._menuItemRepository.exist({ where: { id } })
		if (!exists) {
			throw new NotFoundException('Item not found')
		}

		await this._menuItemRepository.update({ id }, { isArchived })
	}

	async toggleVisible(id: string, categoryId: string, isVisible: boolean): Promise<void> {
		const exists = await this._menuItemRepository.exist({ where: { id } })
		if (!exists) {
			throw new NotFoundException('Item not found')
		}

		const itemCategory = await this._itemCategoryRepository.findOne({
			where: { item: { id }, category: { id: categoryId } },
			select: { id: true },
		})
		if (!itemCategory) {
			throw new BadRequestException()
		}

		await this._itemCategoryRepository.update({ id: itemCategory.id }, { isVisible })
	}

	@Transactional()
	async updateSortOrder(request: UpdateSortOrderRequest[], categoryId: string): Promise<void> {
		const sortOrders = request.map((x) => x.sortOrder)
		if (new Set(sortOrders).size !== sortOrders.length) {
			throw new BadRequestException('Sort orders must be unique')
		}

		for (const { id, sortOrder } of request) {
			const itemCategory = await this._itemCategoryRepository.findOne({
				where: { item: { id }, category: { id: categoryId } },
				select: { id: true },
			})
			if (itemCategory !== null) {
				await this._itemCategoryRepository.update({ id: itemCategory.id }, { sortOrder })
			}
		}
	}

	@Transactional()
	async import(request: ImportMenuItemRequest[]): Promise<void> {
		const categories = await this.createCategories(request)
		await this.createItems(request, categories)
	}

	private async createCategories(items: ImportMenuItemRequest[]): Promise<Category[]> {
		const existingCategories = await this._categoryRepository.find({ select: { id: true, name: true } })
		const existingCategoryNames = existingCategories.map((category) => category.name)

		const newCategoryNames = [
			...new Set(
				items
					.map((item) => item.categories)
					.reduce((current, all) => [...current, ...all])
					.filter((categoryName) => !existingCategoryNames.includes(categoryName)),
			).values(),
		]
		if (newCategoryNames.length === 0) return existingCategories

		const newCategories: Partial<Category>[] = newCategoryNames.map((name, index) => ({
			name,
			sortOrder: existingCategoryNames.length + index + 1,
		}))

		await this._categoryRepository.save(newCategories)
		return this._categoryRepository.find({ select: { id: true, name: true } })
	}

	private async createItems(items: ImportMenuItemRequest[], categories: Category[]): Promise<void> {
		for (const item of items) {
			const saveItem: Partial<MenuItem> = {
				name: item.name,
				description: item.description,
				price: item.price,
				currency: item.currency,
				amount: item.amount,
				unit: item.unit,
			}
			const entity = await this._menuItemRepository.save(saveItem)
			const itemCategories: MenuItemCategoryRequest[] = item.categories.map((name) => {
				const category = categories.find((category) => category.name === name)!
				return { categoryId: category.id, sortOrder: 1 }
			})
			await this.saveItemCategories(entity, itemCategories)
		}
	}

	private async saveItemCategories(item: MenuItem, requestCategories: MenuItemCategoryRequest[]): Promise<void> {
		const categoryIds = requestCategories.map((_category) => _category.categoryId)
		const categories = await this._categoryRepository.find({
			where: { id: In(categoryIds) },
			select: { id: true, sortOrder: true },
		})

		const itemCategories = categories.map((_category) => {
			const sortOrder = requestCategories.find((_itemCategory) => _itemCategory.categoryId === _category.id)!.sortOrder
			return { item, category: _category, sortOrder } as MenuItemCategory
		})
		await this._itemCategoryRepository.save(itemCategories)
	}
}
