/**
 * Expanding text field with FormatToolbar.
 * Shared treatment for text / textarea / wysiwyg / textWysiwyg content fields.
 *
 * Inline surface shows formatted HTML (contenteditable) while remaining typeable;
 * FormatToolbar provides field-aware formatting.
 *
 * Schema options:
 * - `allowedCommand` — restricts toolbar + paste/sanitize tags
 * - `disableAdvanceEditor` — plain text only (strip all tags; no format toolbar)
 */
import {
	useCallback,
	useLayoutEffect,
	useMemo,
	useRef,
	useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
	allowedCommandsFromField,
	isAdvanceEditorDisabled,
	normalizeAllowedCommands,
	sanitizeAllowedHtml,
	sanitizePasteHtml,
	stripToPlainText,
	toolbarCommandsFromField,
} from '../../shared/allowedHtml';
import {
	isRedoKeyEvent,
	isUndoKeyEvent,
} from '../../shared/attributeHistory';
import { handleRichTextBackspace, handleRichTextEnter } from '../../shared/richEnter';
import FormatToolbar from './FormatToolbar';

/**
 * Content field types that use TextWysiwygField.
 *
 * @param {string} type
 * @return {boolean}
 */
export function isTextWysiwygType( type ) {
	return (
		type === 'textWysiwyg' ||
		type === 'textWysywig' ||
		type === 'text' ||
		type === 'textarea' ||
		type === 'wysiwyg' ||
		type === 'richtext'
	);
}

/**
 * @param {HTMLElement|null} el
 */
function autosizeSurface( el ) {
	if ( ! el ) {
		return;
	}
	el.style.height = 'auto';
	const next = Math.max( el.scrollHeight, 40 );
	el.style.height = `${ next }px`;
}

/**
 * Whether the contenteditable is visually empty (placeholder can show).
 *
 * @param {HTMLElement} el
 * @return {boolean}
 */
function isSurfaceEmpty( el ) {
	const text = ( el.textContent || '' ).replace( /\u00a0/g, ' ' ).trim();
	if ( text ) {
		return false;
	}
	const html = ( el.innerHTML || '' )
		.replace( /<br\s*\/?>/gi, '' )
		.replace( /&nbsp;/gi, '' )
		.trim();
	return html === '';
}

/**
 * Prefer HTML clipboard data, fall back to plain text — always sanitize.
 * Paste never keeps style attributes or media / scripts.
 *
 * @param {DataTransfer|null|undefined} clipboard
 * @param {boolean} plainOnly
 * @param {(html: string) => string} sanitize
 * @return {string}
 */
function clipboardToSafeHtml( clipboard, plainOnly, sanitize ) {
	const html = clipboard?.getData( 'text/html' ) || '';
	const plain = clipboard?.getData( 'text/plain' ) || '';
	if ( plainOnly ) {
		return stripToPlainText( plain || html );
	}
	return sanitize( html || plain );
}

/**
 * @param {object}   props
 * @param {string}   [props.label]
 * @param {string}   [props.value]
 * @param {Function} props.onChange
 * @param {string}   [props.help]
 * @param {boolean}  [props.hideLabel]
 * @param {string}   [props.placeholder]
 * @param {string[]|null} [props.allowedCommand]
 * @param {boolean}  [props.disableAdvanceEditor]
 * @param {object}   [props.field] Schema field
 */
export default function TextWysiwygField( {
	label = '',
	value = '',
	onChange,
	help = '',
	hideLabel = false,
	placeholder = '',
	allowedCommand = undefined,
	disableAdvanceEditor = undefined,
	field = null,
} ) {
	const surfaceRef = useRef( null );
	const [ empty, setEmpty ] = useState( true );
	const sessionPastRef = useRef( [] );
	const sessionFutureRef = useRef( [] );
	const sessionCursorRef = useRef( '' );
	const sessionTimerRef = useRef( 0 );
	const originHtmlRef = useRef( '' );

	const advanceDisabled = isAdvanceEditorDisabled(
		field,
		disableAdvanceEditor
	);

	const commands = useMemo( () => {
		if ( advanceDisabled ) {
			return [];
		}
		if ( allowedCommand !== undefined ) {
			return normalizeAllowedCommands( allowedCommand );
		}
		return allowedCommandsFromField( field );
	}, [
		advanceDisabled,
		allowedCommand,
		field?.allowedCommand,
		field?.allowedCommands,
		field?.allowed_commands,
	] );

	const toolbarCommands = useMemo( () => {
		if ( advanceDisabled ) {
			return [];
		}
		return toolbarCommandsFromField(
			field,
			allowedCommand !== undefined ? allowedCommand : undefined
		);
	}, [
		advanceDisabled,
		allowedCommand,
		field?.allowedCommand,
		field?.allowedCommands,
		field?.allowed_commands,
		field?.disableAdvanceEditor,
		field?.disableAdvancedEditor,
	] );

	const sanitize = useCallback(
		( html ) =>
			advanceDisabled
				? stripToPlainText( html )
				: sanitizeAllowedHtml( html, commands ),
		[ advanceDisabled, commands ]
	);

	const sanitizePaste = useCallback(
		( html ) =>
			advanceDisabled
				? stripToPlainText( html )
				: sanitizePasteHtml( html, commands ),
		[ advanceDisabled, commands ]
	);

	const stringValue = useMemo( () => {
		let raw = '';
		if ( value == null ) {
			raw = '';
		} else if ( typeof value === 'string' ) {
			raw = value;
		} else if ( Array.isArray( value ) ) {
			// Legacy features repeater → HTML list items for the surface.
			raw = value
				.map( ( item ) => {
					const text =
						typeof item === 'string'
							? item
							: item && typeof item === 'object'
							? String( item.text ?? item.title ?? '' )
							: '';
					const escaped = text
						.replace( /&/g, '&amp;' )
						.replace( /</g, '&lt;' )
						.replace( />/g, '&gt;' );
					return `<li>${ escaped }</li>`;
				} )
				.join( '' );
		} else {
			raw = String( value );
		}
		return sanitize( raw );
	}, [ sanitize, value ] );
	const placeholderText =
		placeholder || __( 'Type content…', 'joc-blockora-morph' );

	useLayoutEffect( () => {
		const el = surfaceRef.current;
		if ( ! el ) {
			return;
		}
		// Avoid clobbering the caret while the user is typing.
		if ( document.activeElement !== el ) {
			if ( el.innerHTML !== stringValue ) {
				el.innerHTML = stringValue;
			}
			originHtmlRef.current = stringValue;
			sessionCursorRef.current = stringValue;
			sessionPastRef.current = [];
			sessionFutureRef.current = [];
		}
		setEmpty( isSurfaceEmpty( el ) );
		autosizeSurface( el );
	}, [ stringValue ] );

	const readSurface = useCallback( () => {
		const el = surfaceRef.current;
		if ( ! el ) {
			return '';
		}
		return sanitize(
			advanceDisabled ? el.textContent || '' : el.innerHTML
		);
	}, [ advanceDisabled, sanitize ] );

	const writeSurface = useCallback(
		( html ) => {
			const el = surfaceRef.current;
			if ( ! el ) {
				return;
			}
			if ( advanceDisabled ) {
				el.textContent = html;
			} else {
				el.innerHTML = html;
			}
			setEmpty( isSurfaceEmpty( el ) );
			autosizeSurface( el );
			onChange?.( sanitize( html ) );
		},
		[ advanceDisabled, onChange, sanitize ]
	);

	const pushSessionHistory = useCallback( () => {
		const cleaned = readSurface();
		if ( cleaned === sessionCursorRef.current ) {
			return;
		}
		sessionPastRef.current.push( sessionCursorRef.current );
		if ( sessionPastRef.current.length > 80 ) {
			sessionPastRef.current.shift();
		}
		sessionCursorRef.current = cleaned;
		sessionFutureRef.current = [];
	}, [ readSurface ] );

	const scheduleSessionHistory = useCallback( () => {
		if ( sessionTimerRef.current ) {
			window.clearTimeout( sessionTimerRef.current );
		}
		sessionTimerRef.current = window.setTimeout( () => {
			sessionTimerRef.current = 0;
			pushSessionHistory();
		}, 280 );
	}, [ pushSessionHistory ] );

	const undoSession = useCallback( () => {
		if ( sessionTimerRef.current ) {
			window.clearTimeout( sessionTimerRef.current );
			sessionTimerRef.current = 0;
			pushSessionHistory();
		}
		if ( ! sessionPastRef.current.length ) {
			if ( sessionCursorRef.current === originHtmlRef.current ) {
				return false;
			}
			sessionFutureRef.current.push( sessionCursorRef.current );
			sessionCursorRef.current = originHtmlRef.current;
			writeSurface( originHtmlRef.current );
			return true;
		}
		sessionFutureRef.current.push( sessionCursorRef.current );
		sessionCursorRef.current = sessionPastRef.current.pop();
		writeSurface( sessionCursorRef.current );
		return true;
	}, [ pushSessionHistory, writeSurface ] );

	const redoSession = useCallback( () => {
		if ( sessionTimerRef.current ) {
			window.clearTimeout( sessionTimerRef.current );
			sessionTimerRef.current = 0;
			pushSessionHistory();
		}
		if ( ! sessionFutureRef.current.length ) {
			return false;
		}
		sessionPastRef.current.push( sessionCursorRef.current );
		sessionCursorRef.current = sessionFutureRef.current.pop();
		writeSurface( sessionCursorRef.current );
		return true;
	}, [ pushSessionHistory, writeSurface ] );

	const emitSanitized = useCallback(
		( { rewrite = false } = {} ) => {
			const el = surfaceRef.current;
			if ( ! el ) {
				return;
			}
			const cleaned = sanitize(
				advanceDisabled ? el.textContent || '' : el.innerHTML
			);
			if ( rewrite && el.innerHTML !== cleaned ) {
				el.innerHTML = cleaned;
			}
			setEmpty( isSurfaceEmpty( el ) );
			autosizeSurface( el );
			onChange?.( cleaned );
		},
		[ advanceDisabled, onChange, sanitize ]
	);

	const onSurfaceInput = useCallback( () => {
		emitSanitized( { rewrite: false } );
		scheduleSessionHistory();
	}, [ emitSanitized, scheduleSessionHistory ] );

	const onSurfaceBlur = useCallback( () => {
		emitSanitized( { rewrite: true } );
	}, [ emitSanitized ] );

	const onSurfacePaste = useCallback(
		( event ) => {
			event.preventDefault();
			event.stopPropagation();
			const cleaned = clipboardToSafeHtml(
				event.clipboardData,
				advanceDisabled,
				sanitizePaste
			);
			if ( cleaned ) {
				document.execCommand(
					advanceDisabled ? 'insertText' : 'insertHTML',
					false,
					cleaned
				);
			}
			emitSanitized( { rewrite: true } );
			scheduleSessionHistory();
		},
		[ advanceDisabled, emitSanitized, sanitizePaste, scheduleSessionHistory ]
	);

	const onSurfaceKeyDown = useCallback(
		( event ) => {
			if ( isUndoKeyEvent( event ) ) {
				event.preventDefault();
				event.stopPropagation();
				undoSession();
				return;
			}
			if ( isRedoKeyEvent( event ) ) {
				event.preventDefault();
				event.stopPropagation();
				redoSession();
				return;
			}
			if ( advanceDisabled ) {
				return;
			}
			const surfaceDoc =
				surfaceRef.current?.ownerDocument || document;
			if ( handleRichTextBackspace( event, surfaceDoc ) ) {
				emitSanitized( { rewrite: false } );
				scheduleSessionHistory();
				return;
			}
			if ( event.key !== 'Enter' ) {
				return;
			}
			// Lists: Enter → new <li>; Shift+Enter / non-list → <br>.
			if ( handleRichTextEnter( event, surfaceDoc ) ) {
				emitSanitized( { rewrite: false } );
				scheduleSessionHistory();
			}
		},
		[
			advanceDisabled,
			emitSanitized,
			redoSession,
			scheduleSessionHistory,
			undoSession,
		]
	);

	const onSurfaceDrop = useCallback( ( event ) => {
		const types = Array.from( event.dataTransfer?.types || [] );
		if ( types.includes( 'Files' ) || event.dataTransfer?.files?.length ) {
			event.preventDefault();
			event.stopPropagation();
		}
	}, [] );

	const onSurfaceDragOver = useCallback( ( event ) => {
		const types = Array.from( event.dataTransfer?.types || [] );
		if ( types.includes( 'Files' ) ) {
			event.preventDefault();
			event.dataTransfer.dropEffect = 'none';
		}
	}, [] );

	const getTarget = useCallback( () => surfaceRef.current, [] );

	const onToolbarChange = useCallback( () => {
		// Don't rewrite the live DOM — that jumps the caret to the start.
		emitSanitized( { rewrite: false } );
	}, [ emitSanitized ] );

	const showToolbar =
		! advanceDisabled &&
		( toolbarCommands == null ||
			( Array.isArray( toolbarCommands ) && toolbarCommands.length > 0 ) );

	return (
		<div
			className={
				'bm-text-wysiwyg' +
				( advanceDisabled ? ' bm-text-wysiwyg--plain' : '' )
			}
		>
			{ ! hideLabel && label ? (
				<div className="bm-text-wysiwyg__head">
					<span className="bm-text-wysiwyg__label bm-field-label">
						{ label }
					</span>
					{ help ? (
						<p className="bm-text-wysiwyg__help">{ help }</p>
					) : null }
				</div>
			) : help ? (
				<p className="bm-text-wysiwyg__help">{ help }</p>
			) : null }

			{ showToolbar ? (
				<FormatToolbar
					commands={ toolbarCommands }
					getTarget={ getTarget }
					onChange={ onToolbarChange }
				/>
			) : null }

			<div
				ref={ surfaceRef }
				className={
					'bm-text-wysiwyg__input bm-field-input' +
					( empty ? ' is-empty' : '' )
				}
				contentEditable
				role="textbox"
				aria-multiline={ advanceDisabled ? 'false' : 'true' }
				aria-label={ label || __( 'Content', 'joc-blockora-morph' ) }
				data-placeholder={ placeholderText }
				onInput={ onSurfaceInput }
				onBlur={ onSurfaceBlur }
				onPaste={ onSurfacePaste }
				onKeyDownCapture={ onSurfaceKeyDown }
				onDrop={ onSurfaceDrop }
				onDragOver={ onSurfaceDragOver }
				suppressContentEditableWarning
			/>
		</div>
	);
}
