import { sql, type Transaction } from "kysely";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB, Tables } from "~/db/tables";
import { actorId } from "~/features/auth/core/user.server";
import {
	concatUserSubmittedImagePrefix,
	customAvatarUrl,
} from "~/utils/kysely.server";
import { seededRandom } from "~/utils/random";
import type { ListedArt } from "./art-types";

export function unlinkSelfFromArt(artId: number) {
	return db
		.deleteFrom("ArtUserMetadata")
		.where("artId", "=", artId)
		.where("userId", "=", actorId())
		.execute();
}

function getDailySeed() {
	const today = new Date();
	const year = today.getFullYear();
	const month = today.getMonth() + 1;
	const day = today.getDate();
	return `${year}-${month}-${day}`;
}

export async function findShowcaseArts(): Promise<ListedArt[]> {
	const arts = await db
		.selectFrom((eb) =>
			eb
				// each author's most recent art (showcase ones first) via SQLite's
				// max() + bare column rule, packed into one integer since createdAt
				// always stays below the isShowcase component
				.selectFrom("Art")
				.innerJoin("User", "User.id", "Art.authorId")
				.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
				.select(({ fn }) => [
					"Art.id as artId",
					fn
						.max(
							sql<number>`"Art"."isShowcase" * 10000000000 + "Art"."createdAt"`,
						)
						.as("packedShowcaseCreatedAt"),
				])
				.groupBy("Art.authorId")
				.as("BestOfAuthor"),
		)
		.innerJoin("Art", "Art.id", "BestOfAuthor.artId")
		.innerJoin("User", "User.id", "Art.authorId")
		.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
		.select((eb) => [
			"Art.id",
			"Art.createdAt",
			"Art.isShowcase",
			"User.id as userId",
			"User.discordId",
			"User.username",
			"User.discordAvatar",
			"User.commissionsOpen",
			customAvatarUrl(eb).as("customAvatarUrl"),
			concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
				"url",
			),
		])
		.orderBy("Art.isShowcase", "desc")
		.orderBy("Art.createdAt", "desc")
		.orderBy("User.id", "asc")
		.execute();

	const mappedArts = arts.map((a) => ({
		id: a.id,
		createdAt: a.createdAt,
		url: a.url,
		isShowcase: Boolean(a.isShowcase),
		author: {
			commissionsOpen: a.commissionsOpen,
			discordAvatar: a.discordAvatar,
			customAvatarUrl: a.customAvatarUrl,
			discordId: a.discordId,
			username: a.username,
		},
	}));

	const { seededShuffle } = seededRandom(getDailySeed());
	return seededShuffle(mappedArts);
}

export async function findShowcaseArtsByTag(
	tagId: Tables["ArtTag"]["id"],
): Promise<ListedArt[]> {
	const arts = await db
		.selectFrom("TaggedArt")
		.innerJoin("Art", "Art.id", "TaggedArt.artId")
		.innerJoin("User", "User.id", "Art.authorId")
		.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
		.select((eb) => [
			"Art.id",
			"Art.createdAt",
			"Art.isShowcase",
			"User.id as userId",
			"User.discordId",
			"User.username",
			"User.discordAvatar",
			"User.commissionsOpen",
			customAvatarUrl(eb).as("customAvatarUrl"),
			concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
				"url",
			),
		])
		.where("TaggedArt.tagId", "=", tagId)
		.orderBy("Art.isShowcase", "desc")
		.orderBy("Art.createdAt", "desc")
		.execute();

	const encounteredUserIds = new Set<number>();

	return arts
		.filter((row) => {
			if (encounteredUserIds.has(row.userId)) {
				return false;
			}

			encounteredUserIds.add(row.userId);

			return true;
		})
		.map((a) => ({
			id: a.id,
			createdAt: a.createdAt,
			url: a.url,
			isShowcase: Boolean(a.isShowcase),
			author: {
				commissionsOpen: a.commissionsOpen,
				discordAvatar: a.discordAvatar,
				customAvatarUrl: a.customAvatarUrl,
				discordId: a.discordId,
				username: a.username,
			},
		}));
}

export async function findRecentlyUploadedArts(): Promise<ListedArt[]> {
	const arts = await db
		.selectFrom("Art")
		.innerJoin("User", "User.id", "Art.authorId")
		.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
		.select((eb) => [
			"Art.id",
			"Art.createdAt",
			"Art.isShowcase",
			"User.discordId",
			"User.username",
			"User.discordAvatar",
			"User.commissionsOpen",
			customAvatarUrl(eb).as("customAvatarUrl"),
			concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
				"url",
			),
		])
		.orderBy("Art.createdAt", "desc")
		.limit(100)
		.execute();

	return arts.map((a) => ({
		id: a.id,
		createdAt: a.createdAt,
		url: a.url,
		isShowcase: Boolean(a.isShowcase),
		author: {
			commissionsOpen: a.commissionsOpen,
			discordAvatar: a.discordAvatar,
			customAvatarUrl: a.customAvatarUrl,
			discordId: a.discordId,
			username: a.username,
		},
	}));
}

export async function findAllTags() {
	return db.selectFrom("ArtTag").select(["id", "name"]).execute();
}

export async function deleteOrphanTags() {
	const result = await db
		.deleteFrom("ArtTag")
		.where("id", "not in", db.selectFrom("TaggedArt").select("TaggedArt.tagId"))
		.executeTakeFirst();

	return Number(result.numDeletedRows);
}

export async function findArtsByUserId(
	userId: number,
	{ includeAuthored = true, includeTagged = true } = {},
): Promise<ListedArt[]> {
	const taggedButNotAuthored = includeTagged
		? await db
				.selectFrom("Art")
				.innerJoin("ArtUserMetadata", "ArtUserMetadata.artId", "Art.id")
				.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
				.innerJoin("User", "User.id", "Art.authorId")
				.select(({ eb }) => [
					"Art.id",
					"Art.description",
					"Art.createdAt",
					"Art.isShowcase",
					concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
						"url",
					),
					"User.discordId",
					"User.username",
					"User.discordAvatar",
					"User.commissionsOpen",
					customAvatarUrl(eb).as("customAvatarUrl"),
					jsonArrayFrom(
						eb
							.selectFrom("TaggedArt")
							.innerJoin("ArtTag", "ArtTag.id", "TaggedArt.tagId")
							.select(["ArtTag.id", "ArtTag.name"])
							.whereRef("TaggedArt.artId", "=", "Art.id"),
					).as("tags"),
					jsonArrayFrom(
						eb
							.selectFrom("ArtUserMetadata")
							.innerJoin(
								"User as LinkedUser",
								"LinkedUser.id",
								"ArtUserMetadata.userId",
							)
							.select([
								"LinkedUser.id",
								"LinkedUser.discordId",
								"LinkedUser.username",
								"LinkedUser.customUrl",
							])
							.whereRef("ArtUserMetadata.artId", "=", "Art.id"),
					).as("linkedUsers"),
				])
				.where("ArtUserMetadata.userId", "=", userId)
				.where("Art.authorId", "!=", userId)
				.execute()
		: [];

	const authored = includeAuthored
		? await db
				.selectFrom("Art")
				.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
				.select(({ eb }) => [
					"Art.id",
					"Art.description",
					"Art.createdAt",
					"Art.isShowcase",
					concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
						"url",
					),
					jsonArrayFrom(
						eb
							.selectFrom("TaggedArt")
							.innerJoin("ArtTag", "ArtTag.id", "TaggedArt.tagId")
							.select(["ArtTag.id", "ArtTag.name"])
							.whereRef("TaggedArt.artId", "=", "Art.id"),
					).as("tags"),
					jsonArrayFrom(
						eb
							.selectFrom("ArtUserMetadata")
							.innerJoin(
								"User as LinkedUser",
								"LinkedUser.id",
								"ArtUserMetadata.userId",
							)
							.select([
								"LinkedUser.id",
								"LinkedUser.discordId",
								"LinkedUser.username",
								"LinkedUser.customUrl",
							])
							.whereRef("ArtUserMetadata.artId", "=", "Art.id"),
					).as("linkedUsers"),
				])
				.where("Art.authorId", "=", userId)
				.execute()
		: [];

	const combined = [
		...taggedButNotAuthored.map((row) => ({
			id: row.id,
			url: row.url,
			description: row.description ?? undefined,
			createdAt: row.createdAt,
			isShowcase: Boolean(row.isShowcase),
			tags: row.tags.length > 0 ? row.tags : undefined,
			linkedUsers: row.linkedUsers.length > 0 ? row.linkedUsers : undefined,
			author: {
				discordId: row.discordId,
				username: row.username,
				discordAvatar: row.discordAvatar,
				customAvatarUrl: row.customAvatarUrl,
				commissionsOpen: row.commissionsOpen ?? undefined,
			},
		})),
		...authored.map((row) => ({
			id: row.id,
			url: row.url,
			description: row.description ?? undefined,
			createdAt: row.createdAt,
			isShowcase: Boolean(row.isShowcase),
			tags: row.tags.length > 0 ? row.tags : undefined,
			linkedUsers: row.linkedUsers.length > 0 ? row.linkedUsers : undefined,
			author: undefined,
		})),
	];

	return combined.sort((a, b) => b.createdAt - a.createdAt);
}

export async function deleteById(id: number) {
	return db.deleteFrom("Art").where("id", "=", id).execute();
}

type TagsToAdd = Array<Partial<Pick<Tables["ArtTag"], "name" | "id">>>;

type InsertArtArgs = Pick<Tables["Art"], "description"> &
	Pick<Tables["UserSubmittedImage"], "url" | "validatedAt"> & {
		linkedUsers: number[];
		tags: TagsToAdd;
	};

export async function insert(args: InsertArtArgs) {
	const authorId = actorId();
	return await db.transaction().execute(async (trx) => {
		const img = await trx
			.insertInto("UnvalidatedUserSubmittedImage")
			.values({
				submitterUserId: authorId,
				url: args.url,
				validatedAt: args.validatedAt,
			})
			.returningAll()
			.executeTakeFirstOrThrow();

		const hasExistingArt = await trx
			.selectFrom("Art")
			.select("id")
			.where("authorId", "=", authorId)
			.executeTakeFirst();

		const art = await trx
			.insertInto("Art")
			.values({
				authorId,
				description: args.description,
				imgId: img.id,
				isShowcase: hasExistingArt ? 0 : 1,
			})
			.returningAll()
			.executeTakeFirstOrThrow();

		if (args.linkedUsers.length > 0) {
			await trx
				.insertInto("ArtUserMetadata")
				.values(args.linkedUsers.map((userId) => ({ artId: art.id, userId })))
				.execute();
		}

		await insertTags(trx, {
			tags: args.tags,
			authorId,
			artId: art.id,
		});

		return art;
	});
}

type UpdateArtArgs = Pick<Tables["Art"], "description" | "isShowcase"> & {
	linkedUsers: number[];
	tags: TagsToAdd;
};

export async function update(id: number, args: UpdateArtArgs) {
	return await db.transaction().execute(async (trx) => {
		const { authorId } = await trx
			.selectFrom("Art")
			.select("authorId")
			.where("id", "=", id)
			.executeTakeFirstOrThrow();

		if (args.isShowcase) {
			await trx
				.updateTable("Art")
				.set({ isShowcase: 0 })
				.where("authorId", "=", authorId)
				.execute();
		}

		await trx
			.updateTable("Art")
			.set({
				description: args.description,
				isShowcase: args.isShowcase ? 1 : 0,
			})
			.where("id", "=", id)
			.execute();

		await trx.deleteFrom("ArtUserMetadata").where("artId", "=", id).execute();

		if (args.linkedUsers.length > 0) {
			await trx
				.insertInto("ArtUserMetadata")
				.values(args.linkedUsers.map((userId) => ({ artId: id, userId })))
				.execute();
		}

		await trx.deleteFrom("TaggedArt").where("artId", "=", id).execute();

		await insertTags(trx, {
			tags: args.tags,
			authorId,
			artId: id,
		});

		return id;
	});
}

async function insertTags(
	trx: Transaction<DB>,
	{
		tags,
		authorId,
		artId,
	}: {
		tags: TagsToAdd;
		authorId: number;
		artId: number;
	},
) {
	for (const tag of tags) {
		let tagId = tag.id;
		if (!tagId) {
			if (!tag.name) {
				throw new Error("tag name must be provided if no id");
			}

			const newTag = await trx
				.insertInto("ArtTag")
				.values({
					name: tag.name,
					authorId,
				})
				.returningAll()
				.executeTakeFirstOrThrow();
			tagId = newTag.id;
		}

		await trx.insertInto("TaggedArt").values({ artId, tagId }).execute();
	}
}
