import { Elysia } from "elysia";
import { db } from "@core/drizzle";
import { bilibiliMetadata, latestVideoSnapshot, songs } from "@core/drizzle/main/schema";
import { eq, like, or } from "drizzle-orm";
import type { BilibiliMetadataType, ProducerType, SongType } from "@core/drizzle/outerSchema";
import { BiliVideoSchema, SongSchema } from "@elysia/lib/schema";
import { z } from "zod";

interface SongSearchResult {
	type: "song";
	data: SongType;
	rank: number;
}

interface ProducerSearchResult {
	type: "producer";
	data: ProducerType;
	rank: number;
}

interface BiliVideoSearchResult {
	type: "bili-video";
	data: BilibiliMetadataType;
	rank: number; // 0 to 1
}

const getSongSearchResult = async (searchQuery: string) => {
	const data = await db
		.select()
		.from(songs)
		.innerJoin(latestVideoSnapshot, eq(songs.aid, latestVideoSnapshot.aid))
		.where(like(songs.name, `%${searchQuery}%`));

	const results = data
		.map((song) => {
			if (!song.songs.name) return null;
			const occurrences = (song.songs.name.match(new RegExp(searchQuery, "gi")) || []).length;
			const viewsLog = Math.log10(song.latest_video_snapshot.views + 1);
			return {
				type: "song" as "song",
				data: song,
				occurrences,
				viewsLog
			};
		})
		.filter((d) => d !== null);

	// If no results, return empty array
	if (results.length === 0) return [];

	// Normalize occurrences and viewsLog
	const maxOccurrences = Math.max(...results.map((r) => r.occurrences));
	const minViewsLog = Math.min(...results.map((r) => r.viewsLog));
	const maxViewsLog = Math.max(...results.map((r) => r.viewsLog));
	const viewsLogRange = maxViewsLog - minViewsLog || 1; // Prevent division by zero

	// Calculate weighted rank (0-1 range)
	// Weight: 0.6 for occurrences, 0.4 for viewsLog
	const normalizedResults = results.map((result) => {
		const normalizedOccurrences = maxOccurrences > 0 ? result.occurrences / maxOccurrences : 0;
		const normalizedViewsLog = (result.viewsLog - minViewsLog) / viewsLogRange;

		// Weighted combination
		const rank = normalizedOccurrences * 0.6 + normalizedViewsLog * 0.4;

		return {
			type: result.type,
			data: result.data.songs,
			rank: Math.min(Math.max(rank, 0), 1) // Ensure rank is between 0 and 1
		};
	});

	return normalizedResults;
};

const getVideoSearchResult = async (searchQuery: string) => {
	const extractAVID = (query: string): number | null => {
		const avMatch = query.match(/av(\d+)/i);
		if (avMatch) {
			return Number.parseInt(avMatch[1]);
		}
		return 0;
	};
	const results = await db
		.select()
		.from(bilibiliMetadata)
		.innerJoin(latestVideoSnapshot, eq(bilibiliMetadata.aid, latestVideoSnapshot.aid))
		.where(
			or(
				eq(bilibiliMetadata.bvid, searchQuery),
				eq(bilibiliMetadata.aid, extractAVID(searchQuery) || 0)
			)
		);
	return results.map((video) => ({
		type: "bili-video" as "bili-video",
		data: { views: video.latest_video_snapshot.views, ...video.bilibili_metadata },
		rank: 1 // Exact match
	}));
};

const BiliVideoDataSchema = BiliVideoSchema.extend({
	views: z.number()
});

export const searchHandler = new Elysia({ prefix: "/search" }).get(
	"/result",
	async ({ query }) => {
		const searchQuery = query.query;
		const [songResults, videoResults] = await Promise.all([
			getSongSearchResult(searchQuery),
			getVideoSearchResult(searchQuery)
		]);

		const combinedResults = [...songResults, ...videoResults];
		return combinedResults.sort((a, b) => b.rank - a.rank);
	},
	{
		response: {
			200: z.array(
				z.union([
					z.object({
						type: z.literal("song"),
						data: SongSchema,
						rank: z.number()
					}),
					z.object({
						type: z.literal("bili-video"),
						data: BiliVideoDataSchema,
						rank: z.number()
					})
				])
			),
			404: z.object({
				message: z.string()
			})
		},
		query: z.object({
			query: z.string()
		})
	}
);
