import type { FlightTicket } from "../data/entities";
import { Logger } from "@pkg/logger";
import { round } from "$lib/core/num.utils";
import { type Result } from "@pkg/result";
import { type LimitedOrderWithTicketInfoModel } from "$lib/domains/order/data/entities";
import { getJustDateString } from "@pkg/logic/core/date.utils";

type OrderWithUsageInfo = {
	order: LimitedOrderWithTicketInfoModel;
	usePartialAmount: boolean;
};

export class TicketWithOrderSkiddaddler {
	THRESHOLD_DIFF_PERCENTAGE = 15;
	ELEVATED_THRESHOLD_DIFF_PERCENTAGE = 50;
	tickets: FlightTicket[];

	private reservedOrders: number[];
	private allActiveOrders: LimitedOrderWithTicketInfoModel[];
	private urgentActiveOrders: LimitedOrderWithTicketInfoModel[];
	// Stores oids that have their amount divided into smaller amounts for being reused for multiple tickets
	// this record keeps track of the oid, with how much remainder is left to be divided
	private reservedPartialOrders: Record<number, number>;
	private minTicketPrice: number;
	private maxTicketPrice: number;

	constructor(
		tickets: FlightTicket[],
		allActiveOrders: LimitedOrderWithTicketInfoModel[],
	) {
		this.tickets = tickets;
		this.reservedOrders = [];
		this.reservedPartialOrders = {};
		this.minTicketPrice = 0;
		this.maxTicketPrice = 0;
		this.allActiveOrders = [];
		this.urgentActiveOrders = [];

		this.loadupActiveOrders(allActiveOrders);
	}

	async magic(): Promise<Result<boolean>> {
		// Sorting the orders by price in ascending order
		this.allActiveOrders.sort((a, b) => b.basePrice - a.basePrice);

		this.loadMinMaxPrices();

		for (const ticket of this.tickets) {
			if (this.areAllOrdersUsedUp()) {
				break;
			}

			const suitableOrders = this.getSuitableOrders(ticket);
			if (!suitableOrders) {
				continue;
			}
			console.log("--------- suitable orders ---------");
			console.log(suitableOrders);
			console.log("-----------------------------------");
			const [discountAmt, newDisplayPrice] = this.calculateNewAmounts(
				ticket,
				suitableOrders,
			);
			ticket.priceDetails.discountAmount = discountAmt;
			ticket.priceDetails.displayPrice = newDisplayPrice;

			const oids = Array.from(
				new Set(suitableOrders.map((o) => o.order.id)),
			).toSorted();
			ticket.refOIds = oids;
			this.reservedOrders.push(...oids);
		}

		Logger.debug(`Assigned ${this.reservedOrders.length} orders to tickets`);
		return { data: true };
	}

	private loadupActiveOrders(data: LimitedOrderWithTicketInfoModel[]) {
		const now = new Date();
		const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());

		// Removing all orders which have tickets with departure date of yesterday and older
		this.allActiveOrders = data.filter((o) => {
			return (
				new Date(
					getJustDateString(new Date(o.flightTicketInfo.departureDate)),
				) >= today
			);
		});
		this.urgentActiveOrders = [];
		const threeDaysFromNowMs = 3 * 24 * 3600 * 1000;
		for (const order of this.allActiveOrders) {
			const depDate = new Date(order.flightTicketInfo.departureDate);
			if (now.getTime() + threeDaysFromNowMs > depDate.getTime()) {
				this.urgentActiveOrders.push(order);
			}
		}

		Logger.info(`Found ${this.allActiveOrders.length} active orders`);
		Logger.info(`We got ${this.urgentActiveOrders.length} urgent orders`);
	}

	private loadMinMaxPrices() {
		this.minTicketPrice = 100000;
		this.maxTicketPrice = 0;

		for (const ticket of this.tickets) {
			const _dPrice = ticket.priceDetails.displayPrice;
			if (_dPrice < this.minTicketPrice) {
				this.minTicketPrice = _dPrice;
			}
			if (_dPrice > this.maxTicketPrice) {
				this.maxTicketPrice = _dPrice;
			}
		}
	}

	private areAllOrdersUsedUp() {
		if (this.urgentActiveOrders.length === 0) {
			return this.reservedOrders.length >= this.allActiveOrders.length;
		}
		const areUrgentOrdersUsedUp =
			this.reservedOrders.length >= this.urgentActiveOrders.length;
		return areUrgentOrdersUsedUp;
	}

	/**
	 * An order is used up in 2 cases
	 *  1. it's not being partially fulfulled and it's found in the used orders list
	 *  2. it's being partially fulfilled and it's found in both the used orders list and sub chunked orders list
	 */
	private isOrderUsedUp(oid: number, displayPrice: number) {
		if (this.reservedPartialOrders.hasOwnProperty(oid)) {
			return this.reservedPartialOrders[oid] < displayPrice;
		}
		return this.reservedOrders.includes(oid);
	}

	private calculateNewAmounts(
		ticket: FlightTicket,
		orders: OrderWithUsageInfo[],
	) {
		if (orders.length < 1) {
			return [
				ticket.priceDetails.discountAmount,
				ticket.priceDetails.displayPrice,
			];
		}

		const totalAmount = orders.reduce((sum, { order, usePartialAmount }) => {
			return (
				sum +
				(usePartialAmount ? order.pricePerPassenger : order.displayPrice)
			);
		}, 0);

		const discountAmt = round(ticket.priceDetails.displayPrice - totalAmount);
		return [discountAmt, totalAmount];
	}

	/**
	 * Suitable orders are those orders that are ideally within the threshold and are not already reserved. So there's a handful of cases, which other sub methods are handling
	 */
	private getSuitableOrders(
		ticket: FlightTicket,
	): OrderWithUsageInfo[] | undefined {
		const activeOrders =
			this.urgentActiveOrders.length > 0
				? this.urgentActiveOrders
				: this.allActiveOrders;
		const cases = [
			(t: any, a: any) => {
				return this.findFirstSuitableDirectOId(t, a);
			},
			(t: any, a: any) => {
				return this.findFirstSuitablePartialOId(t, a);
			},
			(t: any, a: any) => {
				return this.findManySuitableOIds(t, a);
			},
			(t: any, a: any) => {
				return this.findFirstSuitableDirectOIdElevated(t, a);
			},
		];
		let c = 1;
		for (const each of cases) {
			const out = each(ticket, activeOrders);
			if (out) {
				console.log(`Case ${c} worked, returning it's response`);
				return out;
			}
			c++;
		}
		console.log("NO CASE WORKED, WUT, yee");
	}

	private findFirstSuitableDirectOId(
		ticket: FlightTicket,
		activeOrders: LimitedOrderWithTicketInfoModel[],
	): OrderWithUsageInfo[] | undefined {
		let ord: LimitedOrderWithTicketInfoModel | undefined;
		for (const o of activeOrders) {
			const [op, tp] = [o.displayPrice, ticket.priceDetails.displayPrice];
			const diff = Math.abs(op - tp);
			const diffPtage = (diff / tp) * 100;
			if (this.isOrderSuitable(o.id, op, tp, diffPtage)) {
				ord = o;
				break;
			}
		}
		if (!ord) {
			return;
		}
		return [{ order: ord, usePartialAmount: false }];
	}

	private findFirstSuitablePartialOId(
		ticket: FlightTicket,
		activeOrders: LimitedOrderWithTicketInfoModel[],
	): OrderWithUsageInfo[] | undefined {
		const tp = ticket.priceDetails.displayPrice;

		let ord: LimitedOrderWithTicketInfoModel | undefined;
		for (const o of activeOrders) {
			const op = o.pricePerPassenger;
			const diff = op - tp;
			const diffPtage = (diff / tp) * 100;
			if (this.isOrderSuitable(o.id, op, tp, diffPtage)) {
				ord = o;
				break;
			}
		}
		if (!ord) {
			return;
		}
		this.upsertPartialOrderToCache(
			ord.id,
			ord.pricePerPassenger,
			ord.fullfilledPrice,
		);
		return [{ order: ord, usePartialAmount: true }];
	}

	private findManySuitableOIds(
		ticket: FlightTicket,
		activeOrders: LimitedOrderWithTicketInfoModel[],
	): OrderWithUsageInfo[] | undefined {
		const targetPrice = ticket.priceDetails.displayPrice;

		const validOrderOptions = activeOrders.flatMap((order) => {
			const options: OrderWithUsageInfo[] = [];

			// Add full price option if suitable
			if (
				!this.isOrderUsedUp(order.id, order.displayPrice) &&
				order.displayPrice <= targetPrice
			) {
				options.push({ order, usePartialAmount: false });
			}

			// Add partial price option if suitable
			if (
				!this.isOrderUsedUp(order.id, order.pricePerPassenger) &&
				order.pricePerPassenger <= targetPrice
			) {
				options.push({ order, usePartialAmount: true });
			}

			return options;
		});

		if (validOrderOptions.length === 0) return undefined;
		// Helper function to get the effective price of an order
		const getEffectivePrice = (ordInfo: {
			order: LimitedOrderWithTicketInfoModel;
			usePerPassenger: boolean;
		}) => {
			return ordInfo.usePerPassenger
				? ordInfo.order.pricePerPassenger
				: ordInfo.order.displayPrice;
		};

		const sumCombination = (combo: OrderWithUsageInfo[]): number => {
			return combo.reduce(
				(sum, { order, usePartialAmount }) =>
					sum +
					(usePartialAmount
						? order.pricePerPassenger
						: order.displayPrice),
				0,
			);
		};

		let bestCombination: OrderWithUsageInfo[] = [];
		let bestDiff = targetPrice;

		// Try combinations of different sizes
		for (
			let size = 1;
			size <= Math.min(validOrderOptions.length, 3);
			size++
		) {
			const combinations = this.getCombinations(validOrderOptions, size);

			for (const combo of combinations) {
				const sum = sumCombination(combo);
				const diff = targetPrice - sum;

				if (
					diff >= 0 &&
					(diff / targetPrice) * 100 <=
						this.THRESHOLD_DIFF_PERCENTAGE &&
					diff < bestDiff
				) {
					// Verify we're not using the same order twice
					const orderIds = new Set(combo.map((c) => c.order.id));
					if (orderIds.size === combo.length) {
						bestDiff = diff;
						bestCombination = combo;
					}
				}
			}
		}

		if (bestCombination.length === 0) return undefined;

		// Update partial orders cache
		bestCombination.forEach(({ order, usePartialAmount }) => {
			if (usePartialAmount) {
				this.upsertPartialOrderToCache(
					order.id,
					order.pricePerPassenger,
					order.fullfilledPrice,
				);
			}
		});

		return bestCombination;
	}

	/**
	 * In case no other cases worked, but we have an order with far lower price amount,
	 * then we juss use it but bump up it's amount to match the order
	 */
	private findFirstSuitableDirectOIdElevated(
		ticket: FlightTicket,
		activeOrders: LimitedOrderWithTicketInfoModel[],
	): OrderWithUsageInfo[] | undefined {
		const targetPrice = ticket.priceDetails.displayPrice;

		// Find the first unreserved order with the smallest price difference
		let bestOrder: LimitedOrderWithTicketInfoModel | undefined;
		let smallestPriceDiff = Number.MAX_VALUE;

		for (const order of activeOrders) {
			if (this.isOrderUsedUp(order.id, order.displayPrice)) {
				continue;
			}

			// Check both regular price and per-passenger price
			const prices = [order.displayPrice];
			if (order.pricePerPassenger) {
				prices.push(order.pricePerPassenger);
			}

			for (const price of prices) {
				const diff = Math.abs(targetPrice - price);
				if (diff < smallestPriceDiff) {
					smallestPriceDiff = diff;
					bestOrder = order;
				}
			}
		}

		if (!bestOrder) {
			return undefined;
		}

		// Calculate if we should use partial amount
		const usePartial =
			bestOrder.pricePerPassenger &&
			Math.abs(targetPrice - bestOrder.pricePerPassenger) <
				Math.abs(targetPrice - bestOrder.displayPrice);

		// The price will be elevated to match the ticket price
		// We'll use the original price ratio to determine the new display price
		const originalPrice = usePartial
			? bestOrder.pricePerPassenger
			: bestOrder.displayPrice;

		// Only elevate if the price difference is reasonable
		const priceDiffPercentage =
			(Math.abs(targetPrice - originalPrice) / targetPrice) * 100;
		if (priceDiffPercentage > this.ELEVATED_THRESHOLD_DIFF_PERCENTAGE) {
			return undefined;
		}

		// if (usePartial) {
		// bestOrder.pricePerPassenger =
		// } else {
		// }

		return [{ order: bestOrder, usePartialAmount: !!usePartial }];
	}

	private getCombinations<T>(arr: T[], size: number): T[][] {
		if (size === 0) return [[]];
		if (arr.length === 0) return [];

		const first = arr[0];
		const rest = arr.slice(1);

		const combosWithoutFirst = this.getCombinations(rest, size);
		const combosWithFirst = this.getCombinations(rest, size - 1).map(
			(combo) => [first, ...combo],
		);
		return [...combosWithoutFirst, ...combosWithFirst];
	}

	private isOrderSuitable(
		orderId: number,
		orderPrice: number,
		ticketPrice: number,
		diffPercentage: number,
	) {
		return (
			diffPercentage > 0 &&
			diffPercentage <= this.THRESHOLD_DIFF_PERCENTAGE &&
			orderPrice <= ticketPrice &&
			!this.isOrderUsedUp(orderId, orderPrice)
		);
	}

	private upsertPartialOrderToCache(
		oid: number,
		price: number,
		_defaultPrice: number = 0,
	) {
		if (!this.reservedPartialOrders[oid]) {
			this.reservedPartialOrders[oid] = _defaultPrice;
			return;
		}
		this.reservedPartialOrders[oid] += price;
	}
}
