import { eq, sql } from "drizzle-orm";
import type { db as prodDb } from "../../db/index.ts";
import { categories, globalItems, items } from "../../db/schema.ts";
import { getOrCreateUncategorized } from "./category.service.ts";

type Db = typeof prodDb;

// ─── CSV serialisation helpers ────────────────────────────────────────────────

function escapeField(value: string | number | null | undefined): string {
	if (value === null || value === undefined) return "";
	const str = String(value);
	// Wrap in quotes if the field contains a comma, double-quote, or newline
	if (
		str.includes(",") ||
		str.includes('"') ||
		str.includes("\n") ||
		str.includes("\r")
	) {
		return `"${str.replace(/"/g, '""')}"`;
	}
	return str;
}

function buildCsvRow(fields: (string | number | null | undefined)[]): string {
	return fields.map(escapeField).join(",");
}

// ─── CSV parsing helpers ───────────────────────────────────────────────────────

function parseCsvLine(line: string): string[] {
	const fields: string[] = [];
	let i = 0;

	while (i <= line.length) {
		if (i === line.length) {
			// End of line — push empty trailing field only if we were expecting one
			// (handled by the loop condition above + break below)
			break;
		}

		if (line[i] === '"') {
			// Quoted field
			let field = "";
			i++; // skip opening quote
			while (i < line.length) {
				if (line[i] === '"') {
					if (i + 1 < line.length && line[i + 1] === '"') {
						// Escaped quote
						field += '"';
						i += 2;
					} else {
						// Closing quote
						i++;
						break;
					}
				} else {
					field += line[i];
					i++;
				}
			}
			fields.push(field);
			// Skip comma separator
			if (i < line.length && line[i] === ",") i++;
		} else {
			// Unquoted field — read until comma or end of line
			const start = i;
			while (i < line.length && line[i] !== ",") i++;
			fields.push(line.slice(start, i));
			if (i < line.length) i++; // skip comma
		}
	}

	return fields;
}

function parseCsv(content: string): { headers: string[]; rows: string[][] } {
	const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
	const nonEmpty = lines.filter((l) => l.trim() !== "");
	if (nonEmpty.length === 0) return { headers: [], rows: [] };
	const headers = parseCsvLine(nonEmpty[0]);
	const rows = nonEmpty.slice(1).map(parseCsvLine);
	return { headers, rows };
}

// ─── Export ───────────────────────────────────────────────────────────────────

export async function exportItemsCsv(db: Db, userId: number): Promise<string> {
	const rows = await db
		.select({
			name: sql<string>`COALESCE(
				CASE WHEN ${items.globalItemId} IS NOT NULL
					THEN ${globalItems.brand} || ' ' || ${globalItems.model}
					ELSE ${items.name}
				END,
				${items.name}
			)`.as("name"),
			quantity: items.quantity,
			weightGrams: sql<number | null>`COALESCE(
				CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END,
				${items.weightGrams}
			)`.as("weight_grams"),
			priceCents: sql<number | null>`COALESCE(
				CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END,
				${items.priceCents}
			)`.as("price_cents"),
			categoryName: categories.name,
			notes: items.notes,
			productUrl: items.productUrl,
		})
		.from(items)
		.innerJoin(categories, eq(items.categoryId, categories.id))
		.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
		.where(eq(items.userId, userId));

	const header =
		"name,quantity,weightGrams,priceCents,category,notes,productUrl";
	const dataLines = rows.map((row) =>
		buildCsvRow([
			row.name,
			row.quantity,
			row.weightGrams,
			row.priceCents,
			row.categoryName,
			row.notes,
			row.productUrl,
		]),
	);

	return [header, ...dataLines].join("\n");
}

// ─── Import ───────────────────────────────────────────────────────────────────

export interface ImportResult {
	imported: number;
	createdCategories: string[];
	errors: string[];
}

export async function importItemsCsv(
	db: Db,
	userId: number,
	csvContent: string,
): Promise<ImportResult> {
	const { headers, rows } = parseCsv(csvContent);

	const result: ImportResult = {
		imported: 0,
		createdCategories: [],
		errors: [],
	};

	if (headers.length === 0) return result;

	// Normalise header names for lookup (case-insensitive)
	const headerIndex = (name: string): number =>
		headers.findIndex((h) => h.trim().toLowerCase() === name.toLowerCase());

	const nameIdx = headerIndex("name");
	const quantityIdx = headerIndex("quantity");
	const weightIdx = headerIndex("weightGrams");
	const priceIdx = headerIndex("priceCents");
	const categoryIdx = headerIndex("category");
	const notesIdx = headerIndex("notes");
	const urlIdx = headerIndex("productUrl");

	// Build a local category cache (name → id) seeded from user's categories
	const categoryCache = new Map<string, number>();
	const existingCategories = await db
		.select({ id: categories.id, name: categories.name })
		.from(categories)
		.where(eq(categories.userId, userId));
	for (const cat of existingCategories) {
		categoryCache.set(cat.name.toLowerCase(), cat.id);
	}

	for (let rowNum = 0; rowNum < rows.length; rowNum++) {
		const row = rows[rowNum];
		const lineNum = rowNum + 2; // 1-based, +1 for header

		try {
			const name = nameIdx >= 0 ? row[nameIdx]?.trim() : undefined;
			if (!name) {
				result.errors.push(`Row ${lineNum}: missing required field "name"`);
				continue;
			}

			// Category resolution — scoped to this user
			let categoryId: number;
			const rawCategory = categoryIdx >= 0 ? row[categoryIdx]?.trim() : "";
			const categoryName = rawCategory || "Uncategorized";
			const cacheKey = categoryName.toLowerCase();

			if (categoryCache.has(cacheKey)) {
				categoryId = categoryCache.get(cacheKey)!;
			} else if (cacheKey === "uncategorized") {
				// Use the shared helper for Uncategorized
				categoryId = await getOrCreateUncategorized(db, userId);
				categoryCache.set(cacheKey, categoryId);
			} else {
				// Create a new category for this user
				const [inserted] = await db
					.insert(categories)
					.values({ name: categoryName, icon: "package", userId })
					.returning();
				categoryId = inserted.id;
				categoryCache.set(cacheKey, categoryId);
				result.createdCategories.push(categoryName);
			}

			// Parse optional numeric fields
			const rawQuantity = quantityIdx >= 0 ? row[quantityIdx]?.trim() : "";
			const quantity = rawQuantity ? Number.parseInt(rawQuantity, 10) : 1;
			if (rawQuantity && Number.isNaN(quantity)) {
				result.errors.push(
					`Row ${lineNum}: invalid quantity "${rawQuantity}", skipping`,
				);
				continue;
			}

			const rawWeight = weightIdx >= 0 ? row[weightIdx]?.trim() : "";
			const weightGrams = rawWeight ? Number.parseFloat(rawWeight) : null;
			if (rawWeight && Number.isNaN(weightGrams as number)) {
				result.errors.push(
					`Row ${lineNum}: invalid weightGrams "${rawWeight}", skipping`,
				);
				continue;
			}

			const rawPrice = priceIdx >= 0 ? row[priceIdx]?.trim() : "";
			const priceCents = rawPrice ? Number.parseInt(rawPrice, 10) : null;
			if (rawPrice && Number.isNaN(priceCents as number)) {
				result.errors.push(
					`Row ${lineNum}: invalid priceCents "${rawPrice}", skipping`,
				);
				continue;
			}

			const notes = notesIdx >= 0 ? row[notesIdx]?.trim() || null : null;
			const productUrl = urlIdx >= 0 ? row[urlIdx]?.trim() || null : null;

			await db.insert(items).values({
				name,
				quantity,
				weightGrams,
				priceCents,
				categoryId,
				userId,
				notes,
				productUrl,
				imageFilename: null,
				imageSourceUrl: null,
			});

			result.imported++;
		} catch (err) {
			result.errors.push(`Row ${lineNum}: ${(err as Error).message}`);
		}
	}

	return result;
}
