import type { Editor } from '@tiptap/core';
import type { Node as ProseMirrorNode } from '@tiptap/pm/model';
import { m } from '$lib/paraglide/messages.js';

const SVG_NS = 'http://www.w3.org/2000/svg';

/**
 * Opens the re-edit dropdown for a saved @mention. The host editor supplies
 * this; the NodeView calls it when the pencil is activated.
 *
 * @param getRect    anchor for the dropdown — the mention token's on-screen rect
 * @param displayName the stored display text, used to pre-fill the search input
 * @param pos        the mention node's document position, captured at open time
 *                   so the eventual re-link targets exactly this node
 */
export type RelinkRequest = (
	getRect: () => DOMRect | null,
	displayName: string,
	pos: number
) => void;

type NodeViewArgs = {
	node: ProseMirrorNode;
	editor: Editor;
	getPos: () => number | undefined;
};

// Static developer markup — no user data reaches the DOM here, so building the
// glyph element-by-element (never innerHTML) keeps the "user text is textContent
// only" rule honest across the whole NodeView.
function buildPencilIcon(): SVGSVGElement {
	const svg = document.createElementNS(SVG_NS, 'svg');
	svg.setAttribute('viewBox', '0 0 24 24');
	svg.setAttribute('fill', 'none');
	svg.setAttribute('stroke', 'currentColor');
	svg.setAttribute('stroke-width', '2');
	svg.setAttribute('aria-hidden', 'true');
	svg.setAttribute('class', 'h-4 w-4');
	const tip = document.createElementNS(SVG_NS, 'path');
	tip.setAttribute('d', 'M12 20h9');
	tip.setAttribute('stroke-linecap', 'round');
	const body = document.createElementNS(SVG_NS, 'path');
	body.setAttribute('d', 'M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z');
	body.setAttribute('stroke-linejoin', 'round');
	svg.append(tip, body);
	return svg;
}

/**
 * Tiptap NodeView for a person @mention. Renders the `@displayName` token text
 * (as `textContent` — the name is user-influenced, never `innerHTML`) plus a
 * `contenteditable="false"` pencil button that opens the re-edit dropdown (#628).
 *
 * The pencil sits in a fixed-width slot revealed on whole-token hover and
 * keyboard focus — an instant opacity swap (respects prefers-reduced-motion),
 * so following words never shift and the caret model stays stable.
 */
export function createMentionNodeView(requestRelink: RelinkRequest) {
	return ({ node, editor, getPos }: NodeViewArgs) => {
		// Tracks the node's current attrs across update()s so the pencil always
		// opens with the live displayName (relink only swaps personId today, but
		// this keeps openRelink honest if displayName ever becomes mutable).
		let currentNode = node;

		const dom = document.createElement('span');
		dom.className = 'mention-token group/mention';
		dom.setAttribute('data-type', 'mention');
		dom.setAttribute('data-person-id', node.attrs.personId ?? '');
		dom.setAttribute('data-display-name', node.attrs.displayName ?? '');

		const text = document.createElement('span');
		text.className = 'underline decoration-ink/50 underline-offset-2 text-brand-navy font-medium';
		text.textContent = `@${node.attrs.displayName}`;
		dom.appendChild(text);

		// Fixed-width slot — always present so revealing the pencil never reflows
		// the following text (NFR no-reflow). Only opacity changes on reveal.
		const slot = document.createElement('span');
		slot.className =
			'mention-edit-slot ml-0.5 inline-flex w-4 shrink-0 items-center justify-center align-middle';

		const button = document.createElement('button');
		button.type = 'button';
		button.contentEditable = 'false';
		button.tabIndex = 0;
		button.setAttribute('aria-label', m.person_mention_edit_label());
		// Visible glyph stays ~16px; the 44px tap target comes from padding pulled
		// back with negative margin so the larger hit box does not reflow the line.
		// While hidden the button must be pointer-events-none — opacity-0 alone
		// still hit-tests, and the 44px box overhangs the adjacent text, so a click
		// in the gap between two mentions would otherwise land on this invisible
		// button and spuriously open the dropdown (AC-8). Pointer events are
		// re-enabled together with the opacity reveal on hover/focus.
		button.className = [
			'mention-edit-btn',
			'-mx-3 -my-2 inline-flex h-11 w-11 items-center justify-center rounded-sm text-ink-2',
			'pointer-events-none opacity-0 transition-none',
			'group-hover/mention:pointer-events-auto group-hover/mention:opacity-100',
			'group-focus-within/mention:pointer-events-auto group-focus-within/mention:opacity-100',
			'focus:pointer-events-auto focus:opacity-100',
			'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-navy',
			'disabled:cursor-not-allowed'
		].join(' ');
		button.appendChild(buildPencilIcon());
		slot.appendChild(button);
		dom.appendChild(slot);

		const openRelink = (event: Event) => {
			event.preventDefault();
			event.stopPropagation();
			// AC-7: inert when the editor is disabled — independent of the wrapper's
			// pointer-events-none, on both the pointer and the keyboard path.
			if (!editor.isEditable) return;
			const pos = getPos();
			if (pos === undefined) return;
			requestRelink(() => text.getBoundingClientRect(), currentNode.attrs.displayName ?? '', pos);
		};

		// Prevent mousedown from moving the editor selection before the click
		// fires — keeps the captured pos valid. click + Enter/Space activate.
		button.addEventListener('mousedown', (event) => event.preventDefault());
		button.addEventListener('click', openRelink);
		button.addEventListener('keydown', (event) => {
			if (event.key === 'Enter' || event.key === ' ') openRelink(event);
		});

		const syncEditable = () => {
			const editable = editor.isEditable;
			button.disabled = !editable;
			if (editable) {
				button.removeAttribute('aria-disabled');
				button.tabIndex = 0;
			} else {
				button.setAttribute('aria-disabled', 'true');
				button.tabIndex = -1;
			}
		};
		syncEditable();
		// editor.setEditable() emits "update" (not a ProseMirror transaction), so
		// we listen on "update" to re-sync the pencil's inert state when `disabled`
		// flips mid-session. "update" also skips selection-only changes, so this
		// fires far less often than a "transaction" listener would.
		editor.on('update', syncEditable);

		return {
			dom,
			// The mention is an atom leaf — its DOM is fully owned here, so PM must
			// ignore our mutations (token textContent / disabled attr) rather than
			// trying to read them back into the document.
			ignoreMutation: () => true,
			// Pencil events are ours; everything else (caret placement on the token)
			// flows to ProseMirror.
			stopEvent: (event: Event) => button.contains(event.target as Node),
			update(updatedNode: ProseMirrorNode) {
				if (updatedNode.type.name !== 'mention') return false;
				currentNode = updatedNode;
				text.textContent = `@${updatedNode.attrs.displayName}`;
				dom.setAttribute('data-person-id', updatedNode.attrs.personId ?? '');
				dom.setAttribute('data-display-name', updatedNode.attrs.displayName ?? '');
				return true;
			},
			destroy() {
				editor.off('update', syncEditable);
			}
		};
	};
}
