/**
 * TinyMCE-backed WYSIWYG field for Blockora Morph admin.
 * Text color lives in the TinyMCE toolbar (theme palette + custom).
 *
 * TinyMCE mutates the DOM around its textarea, so the editor host is isolated
 * in a memoized child that never re-renders (avoids React wiping the iframe).
 */
import { memo, useCallback, useEffect, useId, useMemo, useRef } from '@wordpress/element';
import { useThemeTokens } from '../../hooks/useThemeTokens';
import {
	normalizeAllowedCommands,
	sanitizeAllowedHtml,
	sanitizePasteHtml,
	tinymceValidElements,
	toolbarFromAllowedCommands,
} from '../../shared/allowedHtml';
import { DEFAULT_COLORS } from '../../styleFieldDefaults';

/**
 * Resolve a CSS color (hex / rgb / var(…)) to 6-digit hex without '#'.
 *
 * @param {string} color
 * @return {string|null}
 */
function resolveColorToHex( color ) {
	const raw = String( color || '' ).trim();
	if ( ! raw ) {
		return null;
	}
	const hex = raw.match( /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i );
	if ( hex ) {
		let h = hex[ 1 ].toLowerCase();
		if ( h.length === 3 ) {
			h = h
				.split( '' )
				.map( ( ch ) => ch + ch )
				.join( '' );
		}
		return h;
	}
	if ( typeof document === 'undefined' ) {
		return null;
	}
	try {
		const host =
			document.getElementById( 'joc-blockora-morph-app' ) || document.body;
		const probe = document.createElement( 'span' );
		probe.style.cssText =
			'position:absolute;left:-9999px;top:0;visibility:hidden;color:' +
			raw;
		host.appendChild( probe );
		const computed = getComputedStyle( probe ).color;
		host.removeChild( probe );
		const rgb = computed.match(
			/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i
		);
		if ( ! rgb ) {
			return null;
		}
		return [ rgb[ 1 ], rgb[ 2 ], rgb[ 3 ] ]
			.map( ( n ) => {
				const h = Number( n ).toString( 16 );
				return h.length === 1 ? `0${ h }` : h;
			} )
			.join( '' );
	} catch ( e ) {
		return null;
	}
}

/**
 * TinyMCE textcolor_map: [ hexWithoutHash, name, ... ]
 *
 * @param {Array<{ name?: string, color: string }>} colors
 * @return {string[]}
 */
function toTextcolorMap( colors ) {
	const map = [];
	const seen = new Set();
	( colors || [] ).forEach( ( c, i ) => {
		const h = resolveColorToHex( c.color );
		if ( ! h || seen.has( h ) ) {
			return;
		}
		seen.add( h );
		map.push( h, c.name || `Color ${ i + 1 }` );
	} );
	return map;
}

/**
 * Brand / theme colors first, then defaults for any missing hex.
 *
 * @param {Array<{ name?: string, color: string }>} themeColors
 * @return {Array<{ name?: string, color: string }>}
 */
function mergePalette( themeColors ) {
	const out = [];
	const seen = new Set();
	[ ...( themeColors || [] ), ...DEFAULT_COLORS ].forEach( ( c ) => {
		const h = resolveColorToHex( c.color );
		if ( ! h || seen.has( h ) ) {
			return;
		}
		seen.add( h );
		out.push( c );
	} );
	return out.length ? out : DEFAULT_COLORS;
}

/**
 * Imperative TinyMCE mount — must not re-render after init.
 */
const WysiwygHost = memo(
	function WysiwygHost( { editorId, apiRef } ) {
		const containerRef = useRef( null );

		useEffect( () => {
			const container = containerRef.current;
			const wpEditor = window.wp?.editor;
			if ( ! container || ! wpEditor?.initialize ) {
				return undefined;
			}

			const textarea = document.createElement( 'textarea' );
			textarea.id = editorId;
			textarea.rows = 6;
			textarea.className = 'bm-field-input bm-wysiwyg-field__textarea';
			textarea.value = apiRef.current?.value || '';
			container.appendChild( textarea );

			const commands = apiRef.current?.allowedCommand ?? null;
			const textcolorMap = toTextcolorMap(
				mergePalette( apiRef.current?.paletteColors )
			);
			const toolbar1 = toolbarFromAllowedCommands( commands );
			const validElements = tinymceValidElements( commands );
			const hasHeading =
				commands == null ||
				( Array.isArray( commands ) && commands.includes( 'heading' ) );
			const clean = ( html ) =>
				sanitizeAllowedHtml( html, commands );

			const qtButtons = [];
			const cmdSet = new Set(
				commands == null
					? [
							'bold',
							'italic',
							'link',
							'bullist',
							'numlist',
							'strikethrough',
					  ]
					: commands
			);
			if ( cmdSet.has( 'bold' ) ) {
				qtButtons.push( 'strong' );
			}
			if ( cmdSet.has( 'italic' ) ) {
				qtButtons.push( 'em' );
			}
			if ( cmdSet.has( 'link' ) ) {
				qtButtons.push( 'link' );
			}
			if ( cmdSet.has( 'bullist' ) ) {
				qtButtons.push( 'ul', 'li' );
			}
			if ( cmdSet.has( 'numlist' ) ) {
				qtButtons.push( 'ol', 'li' );
			}
			if ( cmdSet.has( 'strikethrough' ) ) {
				qtButtons.push( 'del' );
			}
			qtButtons.push( 'close' );

			wpEditor.initialize( editorId, {
				tinymce: {
					wpautop: false,
					forced_root_block: false,
					plugins:
						'charmap,colorpicker,hr,lists,paste,tabfocus,textcolor,wordpress,wplink,wptextpattern',
					toolbar1,
					valid_elements: validElements,
					...( hasHeading
						? {
								block_formats:
									'NA=div;H1=h1;H2=h2;H3=h3;H4=h4;H5=h5;H6=h6',
						  }
						: {} ),
					invalid_elements:
						'script,iframe,object,embed,img,video,audio,svg,canvas,form,input,button,textarea,select,style,link,meta,base,noscript,template,math,frame,frameset,applet,portal,marquee,plaintext,xmp',
					paste_as_text: false,
					paste_preprocess( _plugin, args ) {
						args.content = sanitizePasteHtml(
							args.content || '',
							apiRef.current.allowedCommand
						);
					},
					textcolor_map:
						textcolorMap.length > 0 ? textcolorMap : undefined,
					height: 220,
					menubar: false,
					branding: false,
					setup( editor ) {
						let ready = false;

						editor.on( 'init', () => {
							apiRef.current.syncing = true;
							editor.setContent(
								clean( apiRef.current?.value || '' )
							);
							apiRef.current.syncing = false;
							ready = true;

							/*
							 * TinyMCE’s inline link toolbar captures `#…_ifr` at
							 * preinit — often null in the Gutenberg sidebar —
							 * so it paints over the canvas. Prefer the modal.
							 */
							editor.addCommand( 'WP_Link', () => {
								if ( window.wpLink?.open ) {
									window.wpLink.open( editor.id );
								}
							} );
						} );

						// Hide broken inline link toolbars (preview / input).
						editor.on( 'wptoolbar', ( event ) => {
							const el = event.toolbar?.getEl?.();
							if (
								el &&
								( el.querySelector( '.wp-link-input' ) ||
									el.querySelector( '.wp-link-preview' ) )
							) {
								event.toolbar = null;
							}
						} );

						/*
						 * forced_root_block:false makes Enter insert <br> globally.
						 * In lists, force a real list-item split instead.
						 */
						editor.on( 'keydown', ( event ) => {
							const key = event.key || event.keyCode;
							const isEnter =
								key === 'Enter' || key === 13 || key === 'NumpadEnter';
							if ( ! isEnter || event.shiftKey ) {
								return;
							}
							const node = editor.selection?.getNode?.();
							const li = node
								? editor.dom.getParent( node, 'li' )
								: null;
							if ( ! li ) {
								return;
							}
							event.preventDefault();
							event.stopPropagation();
							editor.execCommand( 'InsertParagraph' );
						} );

						const emit = () => {
							if ( ! ready || apiRef.current?.syncing ) {
								return;
							}
							const next = clean( editor.getContent() || '' );
							if ( next === ( apiRef.current?.value || '' ) ) {
								return;
							}
							apiRef.current?.onChange?.( next );
						};

						editor.on( 'change keyup undo redo PastePostProcess', emit );
					},
				},
				quicktags: {
					buttons: qtButtons.join( ',' ),
				},
				mediaButtons: false,
			} );

			return () => {
				try {
					if ( window.tinymce?.get( editorId ) ) {
						window.tinymce.get( editorId ).remove();
					}
					wpEditor.remove?.( editorId );
				} catch ( e ) {
					// ignore cleanup races
				}
				container.innerHTML = '';
			};
		}, [ editorId, apiRef ] );

		return (
			<div
				ref={ containerRef }
				className="bm-wysiwyg-field__host"
			/>
		);
	},
	() => true
);

/**
 * @param {object}   props
 * @param {string}   props.label
 * @param {string}   props.value
 * @param {Function} props.onChange
 * @param {string[]|null} [props.allowedCommand]
 */
export default function WysiwygField( {
	label,
	value = '',
	onChange,
	allowedCommand = null,
} ) {
	const reactId = useId();
	const commands = useMemo(
		() => normalizeAllowedCommands( allowedCommand ),
		// Serialize for stable compare when parent passes a fresh array.
		// eslint-disable-next-line react-hooks/exhaustive-deps
		[ JSON.stringify( allowedCommand ?? null ) ]
	);
	const configKey = commands == null ? 'all' : commands.join( '-' );
	const editorId = `bm-wysiwyg-${ reactId.replace( /:/g, '' ) }-${ configKey }`;
	const { themeColors } = useThemeTokens();
	const paletteColors = mergePalette( themeColors );

	const clean = useCallback(
		( html ) => sanitizeAllowedHtml( html, commands ),
		[ commands ]
	);

	const apiRef = useRef( {
		value,
		onChange,
		paletteColors,
		allowedCommand: commands,
		syncing: false,
	} );
	apiRef.current.value = value;
	apiRef.current.onChange = onChange;
	apiRef.current.paletteColors = paletteColors;
	apiRef.current.allowedCommand = commands;

	// Sync external value changes (load/reset) without remounting TinyMCE.
	useEffect( () => {
		const editor = window.tinymce?.get?.( editorId );
		if ( ! editor ) {
			return;
		}
		const current = editor.getContent() || '';
		const next = clean( value || '' );
		if ( next === current ) {
			return;
		}
		apiRef.current.syncing = true;
		editor.setContent( next );
		apiRef.current.syncing = false;
	}, [ editorId, value, clean ] );

	return (
		<div className="bm-wysiwyg-field block">
			{ label ? <span className="bm-field-label">{ label }</span> : null }
			<WysiwygHost
				key={ configKey }
				editorId={ editorId }
				apiRef={ apiRef }
			/>
		</div>
	);
}
