import { Request, Response } from 'express';
import db, { handlePrismaError } from '../../../../handlers/db.js'; // Database
import log from '../../../../handlers/log.js';
import { parseDynamicSortBy } from '../../../../helpers/prisma_helpers.js';
import { schema_get, schema_post, schema_patch, schema_del } from './transaction_schema.js';
import { Prisma } from '@prisma/client';

class AbortError extends Error {
	constructor(public http_status: number, public status: string, public errorcode: string, public message: string, public details?: any) {
		super(message);
		this.name = 'AbortError';
	}
}

// MARK: GET transaction
async function get(req: Request, res: Response) {
	const { error, value } = schema_get.validate(req.query);
	if (error) {
		log.api?.debug('GET transaction Error:', req.query, value, error.details[0].message);
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
	} else {
		log.api?.debug('GET transaction Success:', req.query, value);

		if (value.id !== undefined || value.user_id !== undefined) {
			// get by id or user_id
			await db
				.$transaction([
					// Same query for count and findMany
					db.transactions.count({
						where: {
							OR: [{ id: value.id }, { userId: value.user_id }],
							paid: value.paid
						},
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
						skip: value.skip,
						take: value.take
					}),
					db.transactions.findMany({
						where: {
							OR: [{ id: value.id }, { userId: value.user_id }],
							paid: value.paid
						},
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
						skip: value.skip,
						take: value.take
					})
				])
				.then(([count, result]) => {
					if (result.length !== 0) {
						res.status(200).json({ count, result });
					} else {
						res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified transaction' });
					}
				})
				.catch((err) => {
					handlePrismaError(err, res, 'GET transaction');
				});
		} else {
			// get all
			await db
				.$transaction([
					// Same query for count and findMany
					db.transactions.count({
						where: {
							paid: value.paid
						},
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
						skip: value.skip,
						take: value.take
					}),
					db.transactions.findMany({
						where: {
							paid: value.paid
						},
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
						skip: value.skip,
						take: value.take
					})
				])
				.then(([count, result]) => {
					if (result.length !== 0) {
						res.status(200).json({ count, result });
					} else {
						res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified transaction' });
					}
				})
				.catch((err) => {
					handlePrismaError(err, res, 'GET transaction');
				});
		}
	}
}

// MARK: CREATE transaction
async function post(req: Request, res: Response) {
	const { error, value } = schema_post.validate(req.body);
	if (error) {
		log.api?.debug('POST transaction Error:', req.body, value, error.details[0].message);
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
	} else {
		log.api?.debug('POST transaction Success:', req.body, value);

		const products: Array<number> = value.products;
		const outOfStockProducts: { id: number; name: string }[] = [];
		const notFoundProducts: { id: number; name: string }[] = [];
		const salesData: { productId: number; price: number }[] = [];
		let total = new Prisma.Decimal(0);

		// Start Prisma transaction
		await db
			.$transaction(async (prisma) => {
				// Iterate over all products for this transaction(not prisma)
				for (let i = 0; i < products.length; i++) {
					log.api?.debug('Product:', i + 1, 'of', products.length, '(Loop)');

					// Get product (price, stock, name)
					const product = await prisma.products.findUnique({
						where: { id: products[i] },
						select: { price: true, stock: true, name: true }
					});

					// Check if product exists
					if (product) {
						log.api?.debug('Price:', product.price, '[Name:' + product.name + ']', '[ID:' + products[i] + ']');

						if (product.stock > 0) {
							// Add price of current product to total
							total = total.add(product.price);

							// Add product to salesData -> Later generate sales entry for each product
							salesData.push({
								productId: products[i],
								price: Number(product.price)
							});

							// Reduce stock by 1
							await prisma.products.update({
								where: { id: products[i] },
								data: { stock: { decrement: 1 } }
							});
						} else {
							// Product is out of stock
							outOfStockProducts.push({ id: products[i], name: product.name });
						}
					} else {
						// Product not found
						notFoundProducts.push({ id: products[i], name: 'unknown' });
					}
				}

				log.api?.debug('Total:', total.toFixed(2));

				// Abort the Prisma transaction if there are not existing products
				if (notFoundProducts.length > 0) {
					log.api?.debug('Aborting. missing products:', notFoundProducts);

					throw new AbortError(
						400, // http_status
						'ERROR', // status
						'NOT_FOUND', // errorcode
						'Some of the products included in the transaction do not exist, therefore the transaction has not been processed.', // message
						notFoundProducts // details
					);
				}

				// Abort the Prisma transaction if there are products with insufficient stock
				if (outOfStockProducts.length > 0) {
					log.api?.debug('Aborting. out of stock products:', outOfStockProducts);
					throw new AbortError(
						400, // http_status
						'ERROR', // status
						'OUT_OF_STOCK', // errorcode
						'Some of the products included in the transaction are out of stock, therefore the transaction has not been processed.', // message
						outOfStockProducts // details
					);
				}

				// Create transaction with salesData
				const transaction = await prisma.transactions.create({
					data: {
						userId: value.user_id,
						total: total,
						paid: false,
						sales: {
							create: salesData
						}
					},
					select: {
						id: true
					}
				});

				// Everything went well
				res.status(201).json({ status: 'CREATED', message: 'Successfully created transaction', id: transaction.id });
			})
			.catch((err) => {
				if (err instanceof AbortError) {
					res.status(err.http_status).json({
						status: err.status,
						errorcode: err.errorcode,
						message: err.message,
						details: err.details
					});
				} else {
					handlePrismaError(err, res, 'POST transaction');
				}
			});
	}
}

// MARK: UPDATE transaction
async function patch(req: Request, res: Response) {
	const { error, value } = schema_patch.validate(req.body);
	if (error) {
		log.api?.debug('PATCH transaction Error:', req.body, value, error.details[0].message);
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
	} else {
		log.api?.debug('PATCH transaction Success:', req.body, value);
		await db.transactions
			.update({
				where: {
					id: value.id
				},
				data: {
					userId: value.user_id,
					paid: value.paid,
					paidAt: value.paid ? new Date() : undefined
				},
				select: {
					id: true
				}
			})
			.then((result) => {
				res.status(200).json({ status: 'UPDATED', message: 'Successfully updated transaction', id: result.id });
			})
			.catch((err) => {
				handlePrismaError(err, res, 'PATCH transaction');
			});
	}
}

// MARK: DELETE transaction
async function del(req: Request, res: Response) {
	const { error, value } = schema_del.validate(req.body);
	if (error) {
		log.api?.debug('DEL transaction Error:', req.body, value, error.details[0].message);
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
	} else {
		log.api?.debug('DEL transaction Success:', req.body, value);
		await db.transactions
			.delete({
				where: {
					id: value.id
				}
			})
			.then((result) => {
				res.status(200).json({ status: 'DELETED', message: 'Successfully deleted transaction', id: result.id });
			})
			.catch((err) => {
				handlePrismaError(err, res, 'DEL transaction');
			});
	}
}

export default { get, post, patch, del };
