<script lang="ts">
import { onMount, onDestroy, mount, unmount } from 'svelte';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import { Mention } from '@tiptap/extension-mention';
import { m } from '$lib/paraglide/messages.js';
import type { components } from '$lib/generated/api';
import type { PersonMention } from '$lib/types';
import { deserialize, serialize } from '$lib/utils/mentionSerializer';
import MentionDropdown from './MentionDropdown.svelte';

type Person = components['schemas']['Person'];

type Props = {
	value: string;
	mentionedPersons: PersonMention[];
	placeholder?: string;
	disabled?: boolean;
	onfocus?: () => void;
	onblur?: () => void;
	onSelectionChange?: (text: string | null) => void;
};

let {
	value = $bindable(''),
	mentionedPersons = $bindable([]),
	placeholder = '',
	disabled = false,
	onfocus,
	onblur,
	onSelectionChange
}: Props = $props();

let editorEl: HTMLDivElement;
let editor: Editor | null = null;

// Single reactive state object shared with MentionDropdown. Mutating these
// fields propagates to the mounted dropdown via Svelte's $state proxy —
// this is required because Svelte 5's `mount()` does NOT return prop
// accessors; setting `instance.items = ...` does not update the component.
let dropdownState = $state<{
	items: Person[];
	command: (item: Person) => void;
	clientRect: (() => DOMRect | null) | null;
}>({
	items: [],
	command: () => {},
	clientRect: null
});

type DropdownExports = {
	onKeyDown: (event: KeyboardEvent) => boolean;
};

onMount(() => {
	// Custom Mention node: uses personId / displayName instead of the
	// default id / label attribute names so the mentionSerializer can
	// round-trip correctly without attribute remapping.
	const CustomMention = Mention.extend({
		addAttributes() {
			return {
				personId: {
					default: null,
					parseHTML: (el) => el.getAttribute('data-person-id'),
					renderHTML: (attrs) => ({ 'data-person-id': attrs.personId })
				},
				displayName: {
					default: null,
					parseHTML: (el) => el.getAttribute('data-display-name'),
					renderHTML: (attrs) => ({ 'data-display-name': attrs.displayName })
				},
				mentionSuggestionChar: {
					default: '@',
					parseHTML: (el) => el.getAttribute('data-mention-suggestion-char'),
					renderHTML: (attrs) => ({
						'data-mention-suggestion-char': attrs.mentionSuggestionChar
					})
				}
			};
		}
	});

	editor = new Editor({
		element: editorEl,
		// Initial editable state honors the `disabled` prop. The reactive
		// $effect below keeps it in sync if the prop flips after mount —
		// without this, a keyboard user can tab into the contenteditable
		// even when the wrapper has pointer-events-none (WCAG 2.1.1).
		editable: !disabled,
		extensions: [
			StarterKit.configure({
				heading: false,
				bold: false,
				italic: false,
				strike: false,
				code: false,
				blockquote: false,
				codeBlock: false,
				bulletList: false,
				orderedList: false,
				hardBreak: false,
				horizontalRule: false
			}),
			CustomMention.configure({
				renderHTML({ node }) {
					// Underline color matches the read-mode .person-mention rule
					// (ink at ~50% alpha) — brand-mint on white fails WCAG 1.4.11
					// Non-Text Contrast (≈1.7:1, needs 3:1). Leonie #5621.
					return [
						'span',
						{
							'data-type': 'mention',
							'data-person-id': node.attrs.personId,
							'data-display-name': node.attrs.displayName,
							class:
								'mention-token underline decoration-ink/50 underline-offset-2 text-brand-navy font-medium'
						},
						`@${node.attrs.displayName}`
					];
				},
				renderText({ node }) {
					return `@${node.attrs.displayName}`;
				},
				suggestion: {
					char: '@',
					items: async ({ query }: { query: string }) => {
						if (!query) return [];
						try {
							const res = await fetch(`/api/persons?q=${encodeURIComponent(query)}`);
							if (!res.ok) return [];
							return ((await res.json()) as Person[]).slice(0, 5);
						} catch {
							return [];
						}
					},
					// AC-1 fix: insert the typed query as displayName, not person.displayName.
					command({ editor: ed, range, props }) {
						const p = props as unknown as { personId: string; displayName: string };
						const nodeAfter = ed.view.state.selection.$to.nodeAfter;
						if (nodeAfter?.text?.startsWith(' ')) range.to += 1;
						ed.chain()
							.focus()
							.insertContentAt(range, [
								{
									type: 'mention',
									attrs: { personId: p.personId, displayName: p.displayName }
								},
								{ type: 'text', text: ' ' }
							])
							.run();
					},
					render() {
						let component: object | null = null;
						let exports: DropdownExports | null = null;

						// Tiptap's SuggestionProps types `command` against the default
						// MentionNodeAttrs (id/label). Our custom Mention extension uses
						// personId/displayName, so we cast the renderProps locally.
						type LooseRenderProps = {
							items: unknown;
							command: (props: { personId: string; displayName: string }) => void;
							query: string;
							clientRect?: (() => DOMRect | null) | null;
						};

						const updateState = (renderProps: LooseRenderProps) => {
							dropdownState.items = renderProps.items as Person[];
							// AC-1: pass typed query as displayName, not person.displayName
							dropdownState.command = (item: Person) =>
								renderProps.command({
									personId: item.id,
									displayName: renderProps.query
								});
							dropdownState.clientRect = renderProps.clientRect ?? null;
						};

						return {
							onStart(renderProps) {
								updateState(renderProps as unknown as LooseRenderProps);
								const mounted = mount(MentionDropdown, {
									target: document.body,
									props: { model: dropdownState }
								});
								component = mounted as object;
								exports = mounted as unknown as DropdownExports;
							},
							onUpdate(renderProps) {
								updateState(renderProps as unknown as LooseRenderProps);
							},
							onKeyDown({ event }) {
								// Escape is handled by the suggestion plugin itself.
								if (event.key === 'Escape') return false;
								return exports?.onKeyDown(event) ?? false;
							},
							onExit() {
								if (component) {
									unmount(component);
									component = null;
									exports = null;
								}
							}
						};
					}
				}
			})
		],
		content: deserialize(value, mentionedPersons),
		editorProps: {
			attributes: {
				role: 'textbox',
				'aria-multiline': 'true',
				'aria-label': m.transcription_editor_aria_label(),
				...(placeholder ? { 'data-placeholder': placeholder } : {}),
				class: [
					'min-h-[120px] px-1 py-2.5',
					'font-serif text-base leading-relaxed text-ink',
					'focus:outline-none',
					'tiptap-editor-inner'
				].join(' ')
			}
		},
		onUpdate({ editor: ed }) {
			const { text, mentionedPersons: mp } = serialize(ed.getJSON());
			value = text;
			mentionedPersons = mp;
		},
		onFocus() {
			onfocus?.();
		},
		onBlur() {
			onblur?.();
		},
		onSelectionUpdate({ editor: ed }) {
			const { from, to } = ed.state.selection;
			onSelectionChange?.(from !== to ? ed.state.doc.textBetween(from, to) : null);
		}
	});
});

onDestroy(() => {
	editor?.destroy();
});

// Keep editor in sync with the reactive `disabled` prop. Tiptap's setEditable
// flips contenteditable on the inner DOM and stops accepting input — matches
// the textarea's old `disabled` semantics for keyboard users (WCAG 2.1.1).
//
// Guard: setEditable triggers a ProseMirror transaction which fires onUpdate;
// onUpdate writes through bind:value / bind:mentionedPersons. Without this
// idempotence check, the effect would loop on every prop pass-through.
$effect(() => {
	const shouldBeEditable = !disabled;
	if (editor && editor.isEditable !== shouldBeEditable) {
		editor.setEditable(shouldBeEditable);
	}
});
</script>

<div
	class="relative rounded-sm border border-transparent focus-within:border-brand-mint focus-within:ring-2 focus-within:ring-brand-mint/40"
	class:opacity-50={disabled}
	class:pointer-events-none={disabled}
	aria-disabled={disabled ? 'true' : undefined}
	bind:this={editorEl}
></div>
