/**
 * Enter key behavior for Morph rich contenteditables.
 *
 * - Inside <li>: normal Enter → new <li> (or exit list when empty)
 * - Shift+Enter (anywhere) or Enter outside lists → <br> (never <p>/<div>)
 *
 * Do not rely on the browser default inside lists — in nested contenteditables
 * Chrome/Safari often insert <br> (or <div> later sanitized to <br>) instead.
 */

/**
 * @param {Document} doc
 * @return {Selection|null}
 */
function getDocSelection( doc ) {
	try {
		return doc?.getSelection?.() || null;
	} catch ( e ) {
		return null;
	}
}

/**
 * @param {Document} doc
 * @return {HTMLLIElement|null}
 */
export function getSelectionListItem( doc ) {
	const sel = getDocSelection( doc );
	if ( ! sel || ! sel.rangeCount ) {
		return null;
	}
	let node = sel.anchorNode;
	if ( ! node ) {
		return null;
	}
	if ( node.nodeType === 3 ) {
		node = node.parentElement;
	}
	if ( ! node || typeof node.closest !== 'function' ) {
		return null;
	}
	return node.closest( 'li' );
}

/**
 * @param {Document} doc
 * @return {boolean}
 */
export function selectionIsInListItem( doc ) {
	return Boolean( getSelectionListItem( doc ) );
}

/**
 * @param {Element} li
 * @return {boolean}
 */
function listItemIsEmpty( li ) {
	const text = String( li.textContent || '' )
		.replace( /\u200B/g, '' )
		.replace( /\u00a0/g, ' ' )
		.trim();
	if ( text ) {
		return false;
	}
	return ! li.querySelector(
		'img, svg, video, iframe, table, ul, ol, a[href]'
	);
}

/**
 * Place caret at the start of a node (or inside an empty li).
 *
 * Empty <li><br></li> caret placement is unreliable in Chrome when the list is
 * contenteditable (flex + ::before checks) — use a ZWSP text node as host.
 *
 * @param {Document} doc
 * @param {Node}     node
 */
function placeCaretAtStart( doc, node ) {
	const sel = getDocSelection( doc );
	if ( ! sel || ! node ) {
		return;
	}

	const editable =
		node.nodeType === 1
			? /** @type {Element} */ ( node ).closest?.(
					'[contenteditable="true"]'
			  ) ||
			  ( /** @type {Element} */ ( node ).isContentEditable
					? node
					: null )
			: node.parentElement?.closest?.( '[contenteditable="true"]' );
	if (
		editable &&
		typeof editable.focus === 'function' &&
		doc.activeElement !== editable
	) {
		try {
			editable.focus( { preventScroll: true } );
		} catch ( e ) {
			try {
				editable.focus();
			} catch ( e2 ) {
				// ignore
			}
		}
	}

	const range = doc.createRange();

	if ( node.nodeType === 1 ) {
		const el = /** @type {Element} */ ( node );
		const onlyBr =
			el.childNodes.length === 1 && el.firstChild?.nodeName === 'BR';
		const empty = el.childNodes.length === 0;
		if ( empty || onlyBr || listItemIsEmpty( el ) ) {
			while ( el.firstChild ) {
				el.removeChild( el.firstChild );
			}
			const zwsp = doc.createTextNode( '\u200B' );
			el.appendChild( zwsp );
			range.setStart( zwsp, 0 );
			range.collapse( true );
		} else if ( el.firstChild?.nodeType === 3 ) {
			range.setStart( el.firstChild, 0 );
			range.collapse( true );
		} else {
			range.setStart( el, 0 );
			range.collapse( true );
		}
	} else {
		range.selectNodeContents( node );
		range.collapse( true );
	}

	try {
		sel.removeAllRanges();
		sel.addRange( range );
	} catch ( e ) {
		// ignore
	}
}

/**
 * Re-assert caret after layout (Chrome sometimes drops the first setSelection).
 *
 * @param {Document} doc
 * @param {Node}     node
 */
function placeCaretAtStartSoon( doc, node ) {
	placeCaretAtStart( doc, node );
	const win = doc.defaultView || window;
	if ( typeof win.requestAnimationFrame !== 'function' ) {
		return;
	}
	win.requestAnimationFrame( () => {
		placeCaretAtStart( doc, node );
		win.requestAnimationFrame( () => {
			placeCaretAtStart( doc, node );
		} );
	} );
}

/**
 * Place caret at the end of a node.
 *
 * @param {Document} doc
 * @param {Node}     node
 */
function placeCaretAtEnd( doc, node ) {
	const sel = getDocSelection( doc );
	if ( ! sel ) {
		return;
	}
	const range = doc.createRange();
	range.selectNodeContents( node );
	range.collapse( false );
	sel.removeAllRanges();
	sel.addRange( range );
}

/**
 * Whether the list element itself is the contenteditable root (checklist fields
 * like pricing features). Nested ul/ol inside a rich-text div inherit
 * isContentEditable — do NOT treat those as roots.
 *
 * @param {Element|null} list
 * @return {boolean}
 */
function listIsEditableRoot( list ) {
	if ( ! list || list.nodeType !== 1 ) {
		return false;
	}
	const ce = list.getAttribute?.( 'contenteditable' );
	return ce === 'true' || ce === '';
}

/**
 * Whether the collapsed caret is at the very start of `li`.
 *
 * @param {Document}      doc
 * @param {HTMLLIElement} li
 * @return {boolean}
 */
function caretIsAtStartOfListItem( doc, li ) {
	const sel = getDocSelection( doc );
	if ( ! sel || ! sel.isCollapsed || ! sel.rangeCount ) {
		return false;
	}
	// Empty checklist rows only host a ZWSP — treat any caret as "at start".
	if ( listItemIsEmpty( li ) ) {
		return true;
	}
	const range = sel.getRangeAt( 0 );
	const probe = doc.createRange();
	probe.selectNodeContents( li );
	probe.collapse( true );
	try {
		return (
			range.compareBoundaryPoints( Range.START_TO_START, probe ) === 0
		);
	} catch ( e ) {
		return false;
	}
}

/**
 * Leave a nested list on Backspace: remove the empty item / unwrap content,
 * drop an empty ul/ol, and put the caret on the text before the list.
 *
 * @param {Document}      doc
 * @param {HTMLLIElement} li
 * @return {boolean}
 */
function leaveListItemBackward( doc, li ) {
	const list = li.parentElement;
	if ( ! list || ! /^(UL|OL)$/i.test( list.tagName ) ) {
		return false;
	}
	const parent = list.parentNode;
	if ( ! parent ) {
		return false;
	}

	const wasEmpty = listItemIsEmpty( li );

	if ( wasEmpty ) {
		const before = list.previousSibling;
		let nextLi = li.nextElementSibling;
		while ( nextLi && nextLi.tagName !== 'LI' ) {
			nextLi = nextLi.nextElementSibling;
		}
		li.remove();
		const listGone = ! list.querySelector( 'li' );
		if ( listGone ) {
			list.remove();
		}

		if ( ! listGone && nextLi && parent.contains( nextLi ) ) {
			placeCaretAtStart( doc, nextLi );
			return true;
		}

		const sel = getDocSelection( doc );
		if ( sel ) {
			const range = doc.createRange();
			if ( before && parent.contains( before ) ) {
				if ( before.nodeType === 3 ) {
					range.setStart( before, before.textContent?.length || 0 );
				} else {
					range.selectNodeContents( before );
					range.collapse( false );
				}
			} else if ( parent.childNodes.length ) {
				range.selectNodeContents( parent );
				range.collapse( true );
			} else {
				const zwsp = doc.createTextNode( '\u200B' );
				parent.appendChild( zwsp );
				range.setStart( zwsp, 0 );
			}
			range.collapse( true );
			sel.removeAllRanges();
			sel.addRange( range );
		}
		return true;
	}

	// Outdent: move this item's content out before the list.
	const frag = doc.createDocumentFragment();
	const marker = doc.createTextNode( '' );
	frag.appendChild( marker );
	while ( li.firstChild ) {
		frag.appendChild( li.firstChild );
	}
	parent.insertBefore( frag, list );
	li.remove();
	if ( ! list.querySelector( 'li' ) ) {
		list.remove();
	}

	const sel = getDocSelection( doc );
	if ( sel ) {
		const range = doc.createRange();
		range.setStartAfter( marker );
		range.collapse( true );
		sel.removeAllRanges();
		sel.addRange( range );
	}
	try {
		marker.remove();
	} catch ( e ) {
		// ignore
	}
	return true;
}

/**
 * Exit an empty list item (Enter on blank <li>) — leave the list forward.
 *
 * When the <ul>/<ol> itself is contenteditable (e.g. pricing features), never
 * leave the list — remove the empty item or keep a single blank <li>.
 *
 * @param {Document}       doc
 * @param {HTMLLIElement}  li
 * @return {boolean}
 */
function exitEmptyListItem( doc, li ) {
	const list = li.parentElement;
	if ( ! list || ! /^(UL|OL)$/i.test( list.tagName ) ) {
		return false;
	}

	if ( listIsEditableRoot( list ) ) {
		const items = Array.from( list.children ).filter(
			( el ) => el.tagName === 'LI'
		);
		if ( items.length <= 1 ) {
			li.innerHTML = '';
			li.appendChild( doc.createTextNode( '\u200B' ) );
			placeCaretAtStartSoon( doc, li );
			return true;
		}
		const prev = li.previousElementSibling;
		const next = li.nextElementSibling;
		li.remove();
		const focus =
			( prev && prev.tagName === 'LI' && prev ) ||
			( next && next.tagName === 'LI' && next ) ||
			null;
		if ( focus ) {
			placeCaretAtEnd( doc, focus );
		}
		return true;
	}

	const parent = list.parentNode;
	if ( ! parent ) {
		return false;
	}

	const br = doc.createElement( 'br' );
	if ( list.nextSibling ) {
		parent.insertBefore( br, list.nextSibling );
	} else {
		parent.appendChild( br );
	}

	li.remove();
	if ( ! list.querySelector( 'li' ) ) {
		list.remove();
	}

	const sel = getDocSelection( doc );
	if ( sel ) {
		const range = doc.createRange();
		range.setStartAfter( br );
		range.collapse( true );
		sel.removeAllRanges();
		sel.addRange( range );
	}
	return true;
}

/**
 * Backspace at the start of a <li>: merge into the previous item, or leave the
 * list backward (focus text above) when this is the first / last remaining item.
 *
 * @param {Document} doc
 * @return {boolean}
 */
export function deleteListItemAtCaretStart( doc ) {
	const li = getSelectionListItem( doc );
	if ( ! li || ! caretIsAtStartOfListItem( doc, li ) ) {
		return false;
	}

	const list = li.parentElement;
	if ( ! list || ! /^(UL|OL)$/i.test( list.tagName ) ) {
		return false;
	}

	const prevLi = (() => {
		let n = li.previousElementSibling;
		while ( n && n.tagName !== 'LI' ) {
			n = n.previousElementSibling;
		}
		return n && n.tagName === 'LI' ? n : null;
	})();

	if ( prevLi ) {
		const join = doc.createRange();
		join.selectNodeContents( prevLi );
		join.collapse( false );
		const marker = doc.createTextNode( '' );
		join.insertNode( marker );

		const frag = doc.createDocumentFragment();
		while ( li.firstChild ) {
			frag.appendChild( li.firstChild );
		}
		if ( marker.nextSibling ) {
			prevLi.insertBefore( frag, marker.nextSibling );
		} else {
			prevLi.appendChild( frag );
		}
		li.remove();

		const sel = getDocSelection( doc );
		if ( sel ) {
			const range = doc.createRange();
			range.setStartAfter( marker );
			range.collapse( true );
			sel.removeAllRanges();
			sel.addRange( range );
		}
		try {
			marker.remove();
		} catch ( e ) {
			// ignore
		}
		return true;
	}

	// First item (also the last remaining item when alone).
	if ( listIsEditableRoot( list ) ) {
		if ( ! listItemIsEmpty( li ) ) {
			return false;
		}
		return exitEmptyListItem( doc, li );
	}

	return leaveListItemBackward( doc, li );
}

/**
 * Split the current <li> at the caret into two list items.
 *
 * @param {Document} doc
 * @return {boolean}
 */
export function splitListItemAtCaret( doc ) {
	const sel = getDocSelection( doc );
	const li = getSelectionListItem( doc );
	if ( ! sel || ! sel.rangeCount || ! li ) {
		return false;
	}

	const list = li.parentElement;
	if ( ! list || ! /^(UL|OL)$/i.test( list.tagName ) ) {
		return false;
	}

	if ( listItemIsEmpty( li ) ) {
		return exitEmptyListItem( doc, li );
	}

	let range = sel.getRangeAt( 0 );
	// Clamp caret into this <li> (flex/::before can report odd endpoints).
	if ( ! li.contains( range.commonAncestorContainer ) ) {
		range = doc.createRange();
		range.selectNodeContents( li );
		range.collapse( false );
	}
	range.deleteContents();

	const after = range.cloneRange();
	after.selectNodeContents( li );
	try {
		after.setStart( range.endContainer, range.endOffset );
	} catch ( e ) {
		after.selectNodeContents( li );
		after.collapse( false );
	}
	const trailing = after.extractContents();

	const newLi = doc.createElement( 'li' );
	if ( trailing && trailing.childNodes.length ) {
		newLi.appendChild( trailing );
		if ( listItemIsEmpty( newLi ) ) {
			while ( newLi.firstChild ) {
				newLi.removeChild( newLi.firstChild );
			}
			newLi.appendChild( doc.createTextNode( '\u200B' ) );
		}
	} else {
		newLi.appendChild( doc.createTextNode( '\u200B' ) );
	}

	if ( listItemIsEmpty( li ) ) {
		while ( li.firstChild ) {
			li.removeChild( li.firstChild );
		}
		li.appendChild( doc.createTextNode( '\u200B' ) );
	}

	if ( li.nextSibling ) {
		list.insertBefore( newLi, li.nextSibling );
	} else {
		list.appendChild( newLi );
	}

	placeCaretAtStartSoon( doc, newLi );
	return true;
}

/**
 * Insert a soft line break (<br>).
 *
 * @param {Document} doc
 */
function insertSoftBreak( doc ) {
	try {
		if ( doc.execCommand( 'insertLineBreak' ) ) {
			return;
		}
	} catch ( e ) {
		// fall through
	}
	doc.execCommand( 'insertHTML', false, '<br>' );
}

/**
 * @param {KeyboardEvent} event
 * @param {Document}      [doc]
 * @return {boolean} true when Enter was handled (caller should sync)
 */
export function handleRichTextEnter( event, doc = document ) {
	if ( ! event || event.key !== 'Enter' ) {
		return false;
	}

	const inList = selectionIsInListItem( doc );

	// New list item (or exit empty item).
	if ( inList && ! event.shiftKey ) {
		event.preventDefault();
		if ( typeof event.stopPropagation === 'function' ) {
			event.stopPropagation();
		}
		if ( ! splitListItemAtCaret( doc ) ) {
			// Fallback: try legacy command, then soft break.
			try {
				if ( ! doc.execCommand( 'insertParagraph' ) ) {
					insertSoftBreak( doc );
				}
			} catch ( e ) {
				insertSoftBreak( doc );
			}
		}
		return true;
	}

	// Shift+Enter in lists, or Enter outside lists → <br>.
	event.preventDefault();
	if ( typeof event.stopPropagation === 'function' ) {
		event.stopPropagation();
	}
	insertSoftBreak( doc );
	return true;
}

/**
 * @param {KeyboardEvent} event
 * @param {Document}      [doc]
 * @return {boolean} true when Backspace was handled (caller should sync)
 */
export function handleRichTextBackspace( event, doc = document ) {
	if ( ! event || event.key !== 'Backspace' ) {
		return false;
	}
	if ( event.shiftKey || event.metaKey || event.ctrlKey || event.altKey ) {
		return false;
	}
	if ( ! selectionIsInListItem( doc ) ) {
		return false;
	}
	if ( ! deleteListItemAtCaretStart( doc ) ) {
		return false;
	}
	event.preventDefault();
	if ( typeof event.stopPropagation === 'function' ) {
		event.stopPropagation();
	}
	return true;
}
