import { randomUUID } from "node:crypto";
import { db } from "@kk/db";
import {
	drinkkaart,
	drinkkaartTopup,
	drinkkaartTransaction,
} from "@kk/db/schema";
import { user } from "@kk/db/schema/auth";
import { ORPCError } from "@orpc/server";
import { and, desc, eq, gte, lte, sql } from "drizzle-orm";
import { z } from "zod";
import { adminProcedure, protectedProcedure } from "../index";
import { sendDeductionEmail } from "../lib/drinkkaart-email";
import {
	formatCents,
	generateQrSecret,
	signQrToken,
	verifyQrToken,
} from "../lib/drinkkaart-utils";

// ---------------------------------------------------------------------------
// Internal helper: get or create the drinkkaart row for a user
// ---------------------------------------------------------------------------

async function getOrCreateDrinkkaart(userId: string) {
	const rows = await db
		.select()
		.from(drinkkaart)
		.where(eq(drinkkaart.userId, userId))
		.limit(1);

	if (rows[0]) return rows[0];

	const now = new Date();
	const newCard = {
		id: randomUUID(),
		userId,
		balance: 0,
		version: 0,
		qrSecret: generateQrSecret(),
		createdAt: now,
		updatedAt: now,
	};
	await db.insert(drinkkaart).values(newCard);
	return newCard;
}

// ---------------------------------------------------------------------------
// Router procedures
// ---------------------------------------------------------------------------

export const drinkkaartRouter = {
	// -------------------------------------------------------------------------
	// 4.1 getMyDrinkkaart
	// -------------------------------------------------------------------------
	getMyDrinkkaart: protectedProcedure.handler(async ({ context }) => {
		const userId = context.session.user.id;
		const card = await getOrCreateDrinkkaart(userId);

		const [topups, transactions] = await Promise.all([
			db
				.select()
				.from(drinkkaartTopup)
				.where(eq(drinkkaartTopup.drinkkaartId, card.id))
				.orderBy(desc(drinkkaartTopup.paidAt))
				.limit(50),
			db
				.select()
				.from(drinkkaartTransaction)
				.where(eq(drinkkaartTransaction.drinkkaartId, card.id))
				.orderBy(desc(drinkkaartTransaction.createdAt))
				.limit(50),
		]);

		return {
			id: card.id,
			balance: card.balance,
			balanceFormatted: formatCents(card.balance),
			createdAt: card.createdAt,
			updatedAt: card.updatedAt,
			topups: topups.map((t) => ({
				id: t.id,
				type: t.type,
				amountCents: t.amountCents,
				balanceBefore: t.balanceBefore,
				balanceAfter: t.balanceAfter,
				reason: t.reason,
				paidAt: t.paidAt,
			})),
			transactions: transactions.map((t) => ({
				id: t.id,
				type: t.type,
				amountCents: t.amountCents,
				balanceBefore: t.balanceBefore,
				balanceAfter: t.balanceAfter,
				note: t.note,
				reversedBy: t.reversedBy,
				reverses: t.reverses,
				createdAt: t.createdAt,
			})),
		};
	}),

	// -------------------------------------------------------------------------
	// 4.2 getMyQrToken
	// -------------------------------------------------------------------------
	getMyQrToken: protectedProcedure.handler(async ({ context }) => {
		const userId = context.session.user.id;
		const card = await getOrCreateDrinkkaart(userId);

		const authSecret = context.env.BETTER_AUTH_SECRET;
		const { token, expiresAt } = await signQrToken(
			{ drinkkaartId: card.id, userId },
			card.qrSecret,
			authSecret,
		);

		return { token, expiresAt };
	}),

	// -------------------------------------------------------------------------
	// 4.3 getTopUpCheckoutUrl
	// -------------------------------------------------------------------------
	getTopUpCheckoutUrl: protectedProcedure
		.input(
			z.object({
				amountCents: z
					.number()
					.int()
					.min(100, "Minimumbedrag is € 1,00")
					.max(50000, "Maximumbedrag is € 500,00"),
			}),
		)
		.handler(async ({ input, context }) => {
			const { env: serverEnv, session } = context;

			if (
				!serverEnv.LEMON_SQUEEZY_API_KEY ||
				!serverEnv.LEMON_SQUEEZY_STORE_ID ||
				!serverEnv.LEMON_SQUEEZY_VARIANT_ID
			) {
				throw new ORPCError("INTERNAL_SERVER_ERROR", {
					message: "LemonSqueezy is niet geconfigureerd",
				});
			}

			const card = await getOrCreateDrinkkaart(session.user.id);

			const response = await fetch(
				"https://api.lemonsqueezy.com/v1/checkouts",
				{
					method: "POST",
					headers: {
						Accept: "application/vnd.api+json",
						"Content-Type": "application/vnd.api+json",
						Authorization: `Bearer ${serverEnv.LEMON_SQUEEZY_API_KEY}`,
					},
					body: JSON.stringify({
						data: {
							type: "checkouts",
							attributes: {
								custom_price: input.amountCents,
								product_options: {
									name: "Drinkkaart Opladen",
									description: `Opwaardering van ${formatCents(input.amountCents)}`,
									redirect_url: `${serverEnv.BETTER_AUTH_URL}/account?topup=success`,
								},
								checkout_data: {
									email: session.user.email,
									name: session.user.name,
									custom: {
										type: "drinkkaart_topup",
										drinkkaartId: card.id,
										userId: session.user.id,
									},
								},
								checkout_options: {
									embed: false,
									locale: "nl",
								},
							},
							relationships: {
								store: {
									data: {
										type: "stores",
										id: serverEnv.LEMON_SQUEEZY_STORE_ID,
									},
								},
								variant: {
									data: {
										type: "variants",
										id: serverEnv.LEMON_SQUEEZY_VARIANT_ID,
									},
								},
							},
						},
					}),
				},
			);

			if (!response.ok) {
				const errorData = await response.json();
				console.error("LemonSqueezy Drinkkaart checkout error:", errorData);
				throw new ORPCError("INTERNAL_SERVER_ERROR", {
					message: "Kon checkout niet aanmaken",
				});
			}

			const data = (await response.json()) as {
				data?: { attributes?: { url?: string } };
			};
			const checkoutUrl = data.data?.attributes?.url;
			if (!checkoutUrl) {
				throw new ORPCError("INTERNAL_SERVER_ERROR", {
					message: "Geen checkout URL ontvangen",
				});
			}

			return { checkoutUrl };
		}),

	// -------------------------------------------------------------------------
	// 4.4 resolveQrToken (admin)
	// -------------------------------------------------------------------------
	resolveQrToken: adminProcedure
		.input(z.object({ token: z.string().min(1) }))
		.handler(async ({ input, context }) => {
			let drinkkaartId: string;
			let userId: string;

			// Decode payload first to get the drinkkaartId for key lookup
			const parts = input.token.split(".");
			if (parts.length !== 2) {
				throw new ORPCError("BAD_REQUEST", {
					message: "Ongeldig QR-token formaat",
				});
			}

			let rawPayload: { drinkkaartId: string; userId: string; exp: number };
			try {
				rawPayload = JSON.parse(
					atob((parts[0] as string).replace(/-/g, "+").replace(/_/g, "/")),
				);
				drinkkaartId = rawPayload.drinkkaartId;
				userId = rawPayload.userId;
			} catch {
				throw new ORPCError("BAD_REQUEST", { message: "Ongeldig QR-token" });
			}

			// Fetch the card to get the per-user secret
			const card = await db
				.select()
				.from(drinkkaart)
				.where(eq(drinkkaart.id, drinkkaartId))
				.limit(1)
				.then((r) => r[0]);

			if (!card) {
				throw new ORPCError("NOT_FOUND", {
					message: "Drinkkaart niet gevonden",
				});
			}

			// Verify HMAC and expiry
			try {
				await verifyQrToken(
					input.token,
					card.qrSecret,
					context.env.BETTER_AUTH_SECRET,
				);
			} catch (err: unknown) {
				const msg = (err as Error).message;
				if (msg === "EXPIRED") {
					throw new ORPCError("GONE", { message: "QR-token is verlopen" });
				}
				throw new ORPCError("UNAUTHORIZED", { message: "Ongeldig QR-token" });
			}

			// Fetch user
			const cardUser = await db
				.select({ id: user.id, name: user.name, email: user.email })
				.from(user)
				.where(eq(user.id, userId))
				.limit(1)
				.then((r) => r[0]);

			if (!cardUser) {
				throw new ORPCError("NOT_FOUND", {
					message: "Gebruiker niet gevonden",
				});
			}

			return {
				drinkkaartId: card.id,
				userId: cardUser.id,
				userName: cardUser.name,
				userEmail: cardUser.email,
				balance: card.balance,
				balanceFormatted: formatCents(card.balance),
			};
		}),

	// -------------------------------------------------------------------------
	// 4.5 deductBalance (admin)
	// -------------------------------------------------------------------------
	deductBalance: adminProcedure
		.input(
			z.object({
				drinkkaartId: z.string().min(1),
				amountCents: z.number().int().min(1),
				note: z.string().max(200).optional(),
			}),
		)
		.handler(async ({ input, context }) => {
			const adminId = context.session.user.id;

			const card = await db
				.select()
				.from(drinkkaart)
				.where(eq(drinkkaart.id, input.drinkkaartId))
				.limit(1)
				.then((r) => r[0]);

			if (!card) {
				throw new ORPCError("NOT_FOUND", {
					message: "Drinkkaart niet gevonden",
				});
			}

			if (card.balance < input.amountCents) {
				throw new ORPCError("UNPROCESSABLE_CONTENT", {
					message: `Onvoldoende saldo. Huidig saldo: ${formatCents(card.balance)}`,
				});
			}

			const balanceBefore = card.balance;
			const balanceAfter = balanceBefore - input.amountCents;
			const now = new Date();

			// Optimistic lock update
			const result = await db
				.update(drinkkaart)
				.set({
					balance: balanceAfter,
					version: card.version + 1,
					updatedAt: now,
				})
				.where(
					and(
						eq(drinkkaart.id, input.drinkkaartId),
						eq(drinkkaart.version, card.version),
					),
				);

			if (result.rowsAffected === 0) {
				throw new ORPCError("CONFLICT", {
					message: "Gelijktijdige wijziging gedetecteerd. Probeer opnieuw.",
				});
			}

			const transactionId = randomUUID();
			await db.insert(drinkkaartTransaction).values({
				id: transactionId,
				drinkkaartId: card.id,
				userId: card.userId,
				adminId,
				amountCents: input.amountCents,
				balanceBefore,
				balanceAfter,
				type: "deduction",
				note: input.note ?? null,
				createdAt: now,
			});

			// Fire-and-forget deduction email
			const cardUser = await db
				.select({ email: user.email, name: user.name })
				.from(user)
				.where(eq(user.id, card.userId))
				.limit(1)
				.then((r) => r[0]);

			if (cardUser) {
				sendDeductionEmail({
					to: cardUser.email,
					firstName: cardUser.name.split(" ")[0] ?? cardUser.name,
					amountCents: input.amountCents,
					newBalanceCents: balanceAfter,
				}).catch((err) =>
					console.error("Failed to send deduction email:", err),
				);
			}

			return {
				transactionId,
				balanceBefore,
				balanceAfter,
				balanceFormatted: formatCents(balanceAfter),
			};
		}),

	// -------------------------------------------------------------------------
	// 4.6 adminCreditBalance (admin)
	// -------------------------------------------------------------------------
	adminCreditBalance: adminProcedure
		.input(
			z.object({
				drinkkaartId: z.string().min(1),
				amountCents: z.number().int().min(1),
				reason: z.string().min(1).max(200),
			}),
		)
		.handler(async ({ input, context }) => {
			const adminId = context.session.user.id;

			const card = await db
				.select()
				.from(drinkkaart)
				.where(eq(drinkkaart.id, input.drinkkaartId))
				.limit(1)
				.then((r) => r[0]);

			if (!card) {
				throw new ORPCError("NOT_FOUND", {
					message: "Drinkkaart niet gevonden",
				});
			}

			const balanceBefore = card.balance;
			const balanceAfter = balanceBefore + input.amountCents;
			const now = new Date();

			const result = await db
				.update(drinkkaart)
				.set({
					balance: balanceAfter,
					version: card.version + 1,
					updatedAt: now,
				})
				.where(
					and(
						eq(drinkkaart.id, input.drinkkaartId),
						eq(drinkkaart.version, card.version),
					),
				);

			if (result.rowsAffected === 0) {
				throw new ORPCError("CONFLICT", {
					message: "Gelijktijdige wijziging gedetecteerd. Probeer opnieuw.",
				});
			}

			const topupId = randomUUID();
			await db.insert(drinkkaartTopup).values({
				id: topupId,
				drinkkaartId: card.id,
				userId: card.userId,
				amountCents: input.amountCents,
				balanceBefore,
				balanceAfter,
				type: "admin_credit",
				adminId,
				reason: input.reason,
				paidAt: now,
			});

			return {
				topupId,
				balanceBefore,
				balanceAfter,
				balanceFormatted: formatCents(balanceAfter),
			};
		}),

	// -------------------------------------------------------------------------
	// 4.7 reverseTransaction (admin)
	// -------------------------------------------------------------------------
	reverseTransaction: adminProcedure
		.input(
			z.object({
				transactionId: z.string().min(1),
				note: z.string().max(200).optional(),
			}),
		)
		.handler(async ({ input, context }) => {
			const adminId = context.session.user.id;

			const original = await db
				.select()
				.from(drinkkaartTransaction)
				.where(eq(drinkkaartTransaction.id, input.transactionId))
				.limit(1)
				.then((r) => r[0]);

			if (!original) {
				throw new ORPCError("NOT_FOUND", {
					message: "Transactie niet gevonden",
				});
			}

			if (original.type !== "deduction") {
				throw new ORPCError("BAD_REQUEST", {
					message: "Alleen afschrijvingen kunnen worden teruggedraaid",
				});
			}

			if (original.reversedBy) {
				throw new ORPCError("CONFLICT", {
					message: "Deze transactie is al teruggedraaid",
				});
			}

			const card = await db
				.select()
				.from(drinkkaart)
				.where(eq(drinkkaart.id, original.drinkkaartId))
				.limit(1)
				.then((r) => r[0]);

			if (!card) {
				throw new ORPCError("NOT_FOUND", {
					message: "Drinkkaart niet gevonden",
				});
			}

			const balanceBefore = card.balance;
			const balanceAfter = balanceBefore + original.amountCents;
			const now = new Date();

			const result = await db
				.update(drinkkaart)
				.set({
					balance: balanceAfter,
					version: card.version + 1,
					updatedAt: now,
				})
				.where(
					and(eq(drinkkaart.id, card.id), eq(drinkkaart.version, card.version)),
				);

			if (result.rowsAffected === 0) {
				throw new ORPCError("CONFLICT", {
					message: "Gelijktijdige wijziging gedetecteerd. Probeer opnieuw.",
				});
			}

			const reversalId = randomUUID();

			await db.insert(drinkkaartTransaction).values({
				id: reversalId,
				drinkkaartId: card.id,
				userId: card.userId,
				adminId,
				amountCents: original.amountCents,
				balanceBefore,
				balanceAfter,
				type: "reversal",
				reverses: original.id,
				note: input.note ?? null,
				createdAt: now,
			});

			// Mark original as reversed
			await db
				.update(drinkkaartTransaction)
				.set({ reversedBy: reversalId })
				.where(eq(drinkkaartTransaction.id, original.id));

			return {
				reversalId,
				balanceBefore,
				balanceAfter,
				balanceFormatted: formatCents(balanceAfter),
			};
		}),

	// -------------------------------------------------------------------------
	// 4.8 getTransactionLog (admin) — paginated audit log
	// -------------------------------------------------------------------------
	getTransactionLog: adminProcedure
		.input(
			z.object({
				page: z.number().int().min(1).default(1),
				pageSize: z.number().int().min(1).max(200).default(50),
				userId: z.string().optional(),
				adminId: z.string().optional(),
				type: z.enum(["deduction", "reversal"]).optional(),
				dateFrom: z.string().optional(),
				dateTo: z.string().optional(),
			}),
		)
		.handler(async ({ input }) => {
			// Build conditions
			const conditions = [];
			if (input.userId) {
				conditions.push(eq(drinkkaartTransaction.userId, input.userId));
			}
			if (input.adminId) {
				conditions.push(eq(drinkkaartTransaction.adminId, input.adminId));
			}
			if (input.type) {
				conditions.push(eq(drinkkaartTransaction.type, input.type));
			}
			if (input.dateFrom) {
				conditions.push(
					gte(drinkkaartTransaction.createdAt, new Date(input.dateFrom)),
				);
			}
			if (input.dateTo) {
				conditions.push(
					lte(drinkkaartTransaction.createdAt, new Date(input.dateTo)),
				);
			}

			const where = conditions.length > 0 ? and(...conditions) : undefined;

			// Paginated results with user names
			const offset = (input.page - 1) * input.pageSize;

			const rows = await db
				.select({
					id: drinkkaartTransaction.id,
					type: drinkkaartTransaction.type,
					userId: drinkkaartTransaction.userId,
					adminId: drinkkaartTransaction.adminId,
					amountCents: drinkkaartTransaction.amountCents,
					balanceBefore: drinkkaartTransaction.balanceBefore,
					balanceAfter: drinkkaartTransaction.balanceAfter,
					note: drinkkaartTransaction.note,
					reversedBy: drinkkaartTransaction.reversedBy,
					reverses: drinkkaartTransaction.reverses,
					createdAt: drinkkaartTransaction.createdAt,
				})
				.from(drinkkaartTransaction)
				.where(where)
				.orderBy(desc(drinkkaartTransaction.createdAt))
				.limit(input.pageSize)
				.offset(offset);

			// Collect unique user IDs
			const userIds = [
				...new Set([
					...rows.map((r) => r.userId),
					...rows.map((r) => r.adminId),
				]),
			];

			const users =
				userIds.length > 0
					? await db
							.select({ id: user.id, name: user.name, email: user.email })
							.from(user)
							.where(
								userIds.length === 1
									? eq(user.id, userIds[0] as string)
									: sql`${user.id} IN (${sql.join(
											userIds.map((id) => sql`${id}`),
											sql`, `,
										)})`,
							)
					: [];

			const userMap = new Map(users.map((u) => [u.id, u]));

			// Count total
			const countResult = await db
				.select({ count: sql<number>`count(*)` })
				.from(drinkkaartTransaction)
				.where(where);
			const total = Number(countResult[0]?.count ?? 0);

			return {
				transactions: rows.map((r) => ({
					id: r.id,
					type: r.type,
					userId: r.userId,
					userName: userMap.get(r.userId)?.name ?? "Onbekend",
					userEmail: userMap.get(r.userId)?.email ?? "",
					adminId: r.adminId,
					adminName: userMap.get(r.adminId)?.name ?? "Onbekend",
					amountCents: r.amountCents,
					balanceBefore: r.balanceBefore,
					balanceAfter: r.balanceAfter,
					note: r.note,
					reversedBy: r.reversedBy,
					reverses: r.reverses,
					createdAt: r.createdAt,
				})),
				total,
				page: input.page,
				pageSize: input.pageSize,
			};
		}),

	// -------------------------------------------------------------------------
	// Extra: getDrinkkaartByUserId (admin) — for manual credit user search
	// -------------------------------------------------------------------------
	getDrinkkaartByUserId: adminProcedure
		.input(z.object({ userId: z.string().min(1) }))
		.handler(async ({ input }) => {
			const cardUser = await db
				.select({ id: user.id, name: user.name, email: user.email })
				.from(user)
				.where(eq(user.id, input.userId))
				.limit(1)
				.then((r) => r[0]);

			if (!cardUser) {
				throw new ORPCError("NOT_FOUND", {
					message: "Gebruiker niet gevonden",
				});
			}

			const card = await getOrCreateDrinkkaart(input.userId);

			return {
				drinkkaartId: card.id,
				userId: cardUser.id,
				userName: cardUser.name,
				userEmail: cardUser.email,
				balance: card.balance,
				balanceFormatted: formatCents(card.balance),
			};
		}),

	// -------------------------------------------------------------------------
	// Extra: searchUsers (admin) — for manual credit user search UI
	// -------------------------------------------------------------------------
	searchUsers: adminProcedure
		.input(z.object({ query: z.string().min(1) }))
		.handler(async ({ input }) => {
			const term = `%${input.query}%`;
			const results = await db
				.select({ id: user.id, name: user.name, email: user.email })
				.from(user)
				.where(
					sql`lower(${user.name}) LIKE lower(${term}) OR lower(${user.email}) LIKE lower(${term})`,
				)
				.limit(20);
			return results;
		}),
};
