import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
import { categories, financialAccounts, transactions } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { excludeTransactionsFromExcludedAccounts } from "@/shared/lib/accounts/query-filters";
import { db } from "@/shared/lib/db";
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
import type {
	CategoryReportData,
	CategoryReportFilters,
	CategoryReportItem,
	MonthlyData,
} from "@/shared/lib/types/reports";
import { safeToNumber as toNumber } from "@/shared/utils/number";
import { calculatePercentageChange, generatePeriodRange } from "./utils";

/**
 * Fetches category report data for multiple periods
 *
 * @param userId - User ID to filter data
 * @param filters - Report filters (startPeriod, endPeriod, categoryIds)
 * @returns Complete category report data
 */
export async function fetchCategoryReport(
	userId: string,
	filters: CategoryReportFilters,
): Promise<CategoryReportData> {
	const { startPeriod, endPeriod, categoryIds } = filters;

	// Generate all periods in the range
	const periods = generatePeriodRange(startPeriod, endPeriod);

	const adminPayerId = await getAdminPayerId(userId);
	if (!adminPayerId) {
		return { categories: [], periods, totals: new Map(), grandTotal: 0 };
	}

	// Build WHERE conditions
	const whereConditions = [
		eq(transactions.userId, userId),
		eq(transactions.payerId, adminPayerId),
		inArray(transactions.period, periods),
		or(eq(categories.type, "despesa"), eq(categories.type, "receita")),
		or(
			isNull(transactions.note),
			sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
		),
		excludeTransactionsFromExcludedAccounts(),
	];

	// Add optional category filter
	if (categoryIds && categoryIds.length > 0) {
		whereConditions.push(inArray(categories.id, categoryIds));
	}

	// Query to get aggregated data by category and period
	const rows = await db
		.select({
			categoryId: categories.id,
			categoryName: categories.name,
			categoryIcon: categories.icon,
			categoryType: categories.type,
			period: transactions.period,
			total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
		})
		.from(transactions)
		.innerJoin(categories, eq(transactions.categoryId, categories.id))
		.leftJoin(
			financialAccounts,
			eq(transactions.accountId, financialAccounts.id),
		)
		.where(and(...whereConditions))
		.groupBy(
			categories.id,
			categories.name,
			categories.icon,
			categories.type,
			transactions.period,
		);

	// Process results into CategoryReportData structure
	const categoryMap = new Map<string, CategoryReportItem>();
	const periodTotalsMap = new Map<string, number>();

	// Initialize period totals
	for (const period of periods) {
		periodTotalsMap.set(period, 0);
	}

	// Process each row
	for (const row of rows) {
		const amount = Math.abs(toNumber(row.total));
		const { categoryId, categoryName, categoryIcon, categoryType, period } =
			row;

		// Get or create category item
		if (!categoryMap.has(categoryId)) {
			categoryMap.set(categoryId, {
				categoryId,
				name: categoryName,
				icon: categoryIcon,
				type: categoryType as "despesa" | "receita",
				monthlyData: new Map<string, MonthlyData>(),
				total: 0,
			});
		}

		const categoryItem = categoryMap.get(categoryId);
		if (!categoryItem) continue;

		// Add monthly data (will calculate percentage later)
		categoryItem.monthlyData.set(period, {
			period,
			amount,
			previousAmount: 0, // Will be filled in next step
			percentageChange: null, // Will be calculated in next step
		});

		// Update category total
		categoryItem.total += amount;

		// Update period total
		const currentPeriodTotal = periodTotalsMap.get(period) ?? 0;
		periodTotalsMap.set(period, currentPeriodTotal + amount);
	}

	// Calculate percentage changes (compare with previous period)
	for (const categoryItem of categoryMap.values()) {
		const sortedPeriods = Array.from(categoryItem.monthlyData.keys()).sort();

		for (let i = 0; i < sortedPeriods.length; i++) {
			const period = sortedPeriods[i];
			const monthlyData = categoryItem.monthlyData.get(period);
			if (!monthlyData) continue;

			if (i > 0) {
				// Get previous period data
				const prevPeriod = sortedPeriods[i - 1];
				const prevMonthlyData = categoryItem.monthlyData.get(prevPeriod);
				const previousAmount = prevMonthlyData?.amount ?? 0;

				// Update with previous amount and calculate percentage
				monthlyData.previousAmount = previousAmount;
				monthlyData.percentageChange = calculatePercentageChange(
					monthlyData.amount,
					previousAmount,
				);
			} else {
				// First period - no comparison
				monthlyData.previousAmount = 0;
				monthlyData.percentageChange = null;
			}
		}
	}

	// Fill in missing periods with zero values
	for (const categoryItem of categoryMap.values()) {
		for (const period of periods) {
			if (!categoryItem.monthlyData.has(period)) {
				// Find previous period data for percentage calculation
				const periodIndex = periods.indexOf(period);
				let previousAmount = 0;

				if (periodIndex > 0) {
					const prevPeriod = periods[periodIndex - 1];
					const prevData = categoryItem.monthlyData.get(prevPeriod);
					previousAmount = prevData?.amount ?? 0;
				}

				categoryItem.monthlyData.set(period, {
					period,
					amount: 0,
					previousAmount,
					percentageChange: calculatePercentageChange(0, previousAmount),
				});
			}
		}
	}

	// Convert to array and sort
	const categoryList = Array.from(categoryMap.values());

	// Sort: despesas first (by total desc), then receitas (by total desc)
	categoryList.sort((a, b) => {
		// First by type: despesa comes before receita
		if (a.type !== b.type) {
			return a.type === "despesa" ? -1 : 1;
		}
		// Then by total (descending)
		return b.total - a.total;
	});

	// Calculate grand total
	let grandTotal = 0;
	for (const categoryItem of categoryList) {
		grandTotal += categoryItem.total;
	}

	return {
		categories: categoryList,
		periods,
		totals: periodTotalsMap,
		grandTotal,
	};
}
