import type { Book } from '$lib/schema';

/**
 * What kind of thing a metadata field holds, which is the only thing that decides
 * what you can do with it when two records disagree.
 */
export type FieldKind = 'text' | 'number' | 'date' | 'list' | 'keyed' | 'longtext';

/** An action offered on a field, beyond replacing it outright. */
export type FieldAction = 'replace' | 'merge' | 'append';

export interface FieldSpec {
	/** The key sent in `BookMetadataUpdate`. */
	key: string;
	label: string;
	kind: FieldKind;
	group: string;
	/** Extra actions past `replace`, which every field has. */
	extra: FieldAction[];
}

/**
 * The fields a merge can resolve, in the order and grouping the edit form uses.
 *
 * Deliberately a spec rather than markup: the merge workbench and, later, the
 * provider review screen both render from this, so a field cannot exist in one and
 * not the other. `cover` and `files` are absent because they are not choices —
 * files always come across and the cover has its own endpoint.
 */
export const MERGE_FIELDS: FieldSpec[] = [
	{ key: 'title', label: 'Title', kind: 'text', group: 'Identity', extra: [] },
	{ key: 'subtitle', label: 'Subtitle', kind: 'text', group: 'Identity', extra: [] },
	{ key: 'edition', label: 'Edition', kind: 'number', group: 'Identity', extra: [] },
	{ key: 'series', label: 'Series', kind: 'text', group: 'Identity', extra: [] },
	{ key: 'series_position', label: 'No.', kind: 'text', group: 'Identity', extra: [] },
	{ key: 'language', label: 'Language', kind: 'text', group: 'Identity', extra: [] },

	// Order is meaningful for authors, so the second list is appended rather than
	// interleaved; tags are a set, so they merge.
	{
		key: 'authors',
		label: 'Authors',
		kind: 'list',
		group: 'People and subjects',
		extra: ['append']
	},
	{ key: 'tags', label: 'Tags', kind: 'list', group: 'People and subjects', extra: ['merge'] },

	{ key: 'publisher', label: 'Publisher', kind: 'text', group: 'Publication', extra: [] },
	{ key: 'published_date', label: 'Published', kind: 'date', group: 'Publication', extra: [] },
	{ key: 'pages', label: 'Pages', kind: 'number', group: 'Publication', extra: [] },
	{
		key: 'identifiers',
		label: 'Identifiers',
		kind: 'keyed',
		group: 'Publication',
		extra: ['merge']
	},

	{
		key: 'description',
		label: 'Description',
		kind: 'longtext',
		group: 'Description',
		extra: ['append']
	}
];

/** A field's value, in the shape `BookMetadataUpdate` expects to receive it. */
export type FieldValue = string | number | string[] | Record<string, string> | null;

/** Read one field off a book, flattening the relations the API returns as objects. */
export function readField(book: Book, key: string): FieldValue {
	switch (key) {
		case 'authors':
			return book.authors.map((author) => author.name);
		case 'tags':
			return book.tags.map((tag) => tag.name);
		case 'publisher':
			return book.publisher?.name ?? null;
		case 'series':
			return book.series?.title ?? null;
		case 'identifiers':
			return book.identifiers ?? {};
		default:
			return (book as unknown as Record<string, FieldValue>)[key] ?? null;
	}
}

/** Whether a field holds nothing, and so has no decision attached to it. */
export function isEmpty(value: FieldValue): boolean {
	if (value === null || value === undefined || value === '') return true;
	if (Array.isArray(value)) return value.length === 0;
	if (typeof value === 'object') return Object.keys(value).length === 0;
	return false;
}

/** Whether two field values say the same thing, order included for lists. */
export function isSame(left: FieldValue, right: FieldValue): boolean {
	if (isEmpty(left) && isEmpty(right)) return true;
	return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
}

/**
 * Apply an action to a pair of values and return what the target becomes.
 *
 * `merge` on a keyed collection is per name and the target wins a clash, because
 * `Identifier` is unique on `(name, book_id)` — a book cannot hold both its print
 * and its ebook ISBN, so the second one has nowhere to go.
 */
export function applyAction(
	action: FieldAction,
	kind: FieldKind,
	target: FieldValue,
	incoming: FieldValue
): FieldValue {
	if (action === 'replace') return incoming;

	if (kind === 'list') {
		const current = Array.isArray(target) ? target : [];
		const extra = Array.isArray(incoming) ? incoming : [];
		// Order preserved, duplicates dropped — works for both append and merge.
		return [...new Set([...current, ...extra])];
	}

	if (kind === 'keyed') {
		return { ...(incoming as Record<string, string>), ...(target as Record<string, string>) };
	}

	if (kind === 'longtext') {
		const current = typeof target === 'string' ? target.trim() : '';
		const extra = typeof incoming === 'string' ? incoming.trim() : '';
		return [current, extra].filter(Boolean).join('\n\n');
	}

	return incoming;
}

/** How a value reads in the inert reference column. */
export function displayValue(value: FieldValue): string {
	if (isEmpty(value)) return '';
	if (Array.isArray(value)) return value.join(', ');
	if (typeof value === 'object') {
		return Object.entries(value as Record<string, string>)
			.map(([name, id]) => `${name}: ${id}`)
			.join('  ·  ');
	}
	return String(value);
}
