import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
	return twMerge(clsx(inputs));
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };

export function createQueryParams(
	data: Record<string, string | string[] | number | boolean | undefined | null>
) {
	const params = new URLSearchParams();

	for (const [key, value] of Object.entries(data)) {
		if (value === undefined || value === null) continue;

		if (Array.isArray(value)) {
			value.forEach((item) => {
				if (item !== null && item !== undefined) {
					params.append(key, String(item));
				}
			});
		} else {
			params.append(key, String(value));
		}
	}

	return params;
}

export function formatFileSize(bytes: number, decimals = 1, binary = true) {
	if (!bytes) return '0 B';

	// Use 1024 for binary (default) or 1000 for decimal
	const divisor = binary ? 1024 : 1000;

	// Units for binary and decimal representations
	const units = binary
		? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
		: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

	// Find the appropriate unit
	const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(divisor)), units.length - 1);
	const size = bytes / Math.pow(divisor, exponent);

	// Format the number with appropriate decimals
	const formattedSize = size.toFixed(
		// Don't show decimals for bytes
		exponent === 0 ? 0 : decimals
	);

	return `${formattedSize} ${units[exponent]}`;
}

/**
 * Presentation for a book identifier.
 *
 * The backend stores identifiers as free-form name/value pairs, so this only
 * decides how to label and link the ones we recognise. Anything unknown falls
 * back to the stored name and renders as plain text.
 */
const IDENTIFIER_TYPES: Record<string, { label: string; url?: (value: string) => string }> = {
	'isbn-13': { label: 'ISBN-13', url: (v) => `https://openlibrary.org/isbn/${isbnDigits(v)}` },
	'isbn-10': { label: 'ISBN-10', url: (v) => `https://openlibrary.org/isbn/${isbnDigits(v)}` },
	isbn: { label: 'ISBN', url: (v) => `https://openlibrary.org/isbn/${isbnDigits(v)}` },
	doi: { label: 'DOI', url: (v) => `https://doi.org/${encodeURI(v.trim())}` },
	asin: { label: 'ASIN', url: (v) => `https://www.amazon.com/dp/${encodeURIComponent(v.trim())}` }
};

/** Order the ones worth reading first; everything else keeps its own order. */
const IDENTIFIER_ORDER = ['isbn-13', 'isbn-10', 'isbn', 'doi', 'asin'];

function isbnDigits(value: string) {
	return value.replace(/[^0-9Xx]/g, '');
}

function identifierKey(name: string) {
	return name
		.trim()
		.toLowerCase()
		.replace(/[\s_]+/g, '-');
}

export function describeIdentifier(name: string, value: string) {
	const known = IDENTIFIER_TYPES[identifierKey(name)];

	return {
		label: known?.label ?? name.toUpperCase(),
		href: known?.url?.(value)
	};
}

export function sortIdentifiers(identifiers: Record<string, string>) {
	return Object.entries(identifiers).sort(([a], [b]) => {
		const ia = IDENTIFIER_ORDER.indexOf(identifierKey(a));
		const ib = IDENTIFIER_ORDER.indexOf(identifierKey(b));
		if (ia === -1 && ib === -1) return 0;
		if (ia === -1) return 1;
		if (ib === -1) return -1;
		return ia - ib;
	});
}

export function getFileType(filename: string) {
	const parts = filename.split('.');
	if (parts.length < 2) return 'UNKNOWN';
	const extension = parts[parts.length - 1];
	return extension.toUpperCase();
}

/**
 * The formats there is a reader for. Everything else is download-only.
 *
 * A library imported from elsewhere carries MOBI, AZW3 and CBZ files, which are
 * legitimate to store and to download but have nowhere to open — the reader routes
 * are `read/epub` and `read/pdf` and there is no third one.
 */
const READABLE_FILE_TYPES = ['EPUB', 'PDF'];

export function isReadable(filename: string) {
	return READABLE_FILE_TYPES.includes(getFileType(filename));
}

export const pluck = (array: [], key: string) => array.map((obj) => obj?.[key]);
