import {
	useCallback,
	useEffect,
	useMemo,
	useRef,
	useState,
	type KeyboardEvent as ReactKeyboardEvent,
} from 'react';
import { createBlock, type BlockInstance } from '@wordpress/blocks';
import {
	BlockEditorProvider,
	BlockList,
	BlockTools,
	BlockInspector,
	Inserter,
	WritingFlow,
	ObserveTyping,
	BlockEditorKeyboardShortcuts,
} from '@wordpress/block-editor';
import { Button, Popover, SlotFillProvider } from '@wordpress/components';
import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
import { useDebounce, useStateWithHistory } from '@wordpress/compose';
import { cog, undo as undoIcon, redo as redoIcon } from '@wordpress/icons';

// Default rich-text formats (bold, italic, link, inline code, …) register as a
// side effect of importing the package.
import '@wordpress/format-library';

// Side-effect import: must run AFTER @wordpress/block-editor so our
// editor.MediaUpload filter registers last and replaces the bundled
// wp.media-based one (see the module).
import '../lib/media/registerMediaUpload';

// The editor island owns the editor + component + block styles. Reading pages
// import only the *frontend* block styles (Decision 0003) — never these.
import '@wordpress/components/build-style/style.css';
import '@wordpress/block-editor/build-style/style.css';
import '@wordpress/block-editor/build-style/content.css';
// (@wordpress/format-library ships no `build-style` export subpath; the inline
// format UI is styled by the components + block-editor stylesheets above.)
import '@wordpress/block-library/build-style/common.css';
import '@wordpress/block-library/build-style/style.css';
import '@wordpress/block-library/build-style/editor.css';
import '@wordpress/block-library/build-style/theme.css';

import { registerSkyPressBlocks } from '../lib/blocks/serialize';
import { ALLOWED_BLOCKS } from '../lib/blocks/allowlist';
import type { MediaUploadHandler } from '../lib/media/mediaUpload';
import { registerMentionFormat } from '../lib/editor/mention-format';
import { registerMentionAutocompleter } from '../lib/editor/mention-autocompleter';
import { shouldHandleEditorUndo } from '../lib/editor/undo-shortcut';
import EditorErrorBoundary from './EditorErrorBoundary';
import type { BlockNode } from '../lib/blocks/render';

export const SPIKE_BLOCKS_KEY = 'skypress:spike:blocks';

/** Rebuild editor block instances from stored block nodes (for editing, SP5). */
function toEditorBlocks( nodes: BlockNode[] ): BlockInstance[] {
	return nodes.map( ( node ) =>
		createBlock(
			node.name,
			{ ...node.attributes },
			toEditorBlocks( node.innerBlocks ?? [] )
		)
	);
}

interface SkyEditorProps {
	/** Receives the live block tree on every change (for the publish flow, SP2). */
	onChange?: ( blocks: BlockInstance[] ) => void;
	/** Custom Gutenberg media handler — uploads to the PDS as a blob (SP3). */
	mediaUpload?: MediaUploadHandler;
	/** Existing article content to load when editing (SP5). */
	initialBlocks?: BlockNode[];
}

/**
 * Standalone Gutenberg editor, composed directly from `@wordpress/block-editor`
 * (Decision 0021 — we no longer wrap `@automattic/isolated-block-editor`). The
 * `BlockEditorProvider` holds the block tree; `BlockTools` + `WritingFlow` +
 * `ObserveTyping` + `BlockList` are the canvas; undo/redo is app-level via
 * `useStateWithHistory`. Restricted to the curated block set (Decision 0002).
 *
 * The live block tree is forwarded on every change via `onChange` (the structured
 * array — not HTML) so the Studio can publish it, and persisted as a local draft.
 *
 * Rendered with `client:only="react"` so its (heavy) bundle never reaches reading
 * pages (Decision 0001 / 0003).
 */
export default function SkyEditor( { onChange, mediaUpload, initialBlocks }: SkyEditorProps ) {
	// Register the curated block set + the `@`-mention format/autocompleter, and
	// build the initial editor blocks — once per mount, before `BlockList` first
	// renders (all idempotent). Studio remounts SkyEditor (keyed) when switching
	// articles, so reading `initialBlocks` at mount is sufficient.
	const initialValue = useMemo( () => {
		registerSkyPressBlocks();
		registerMentionFormat();
		registerMentionAutocompleter();
		return initialBlocks && initialBlocks.length > 0 ? toEditorBlocks( initialBlocks ) : [];
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [] );

	const {
		value,
		setValue: setBlocks,
		undo,
		redo,
		hasUndo,
		hasRedo,
	} = useStateWithHistory< BlockInstance[] >( initialValue );
	// `@wordpress/compose` types `value` as `unknown` (its generic isn't propagated
	// to the return) even though it round-trips exactly what `setValue` was given.
	const blocks = value as BlockInstance[];

	// Empty until the first change. Kept in the DOM as an `aria-live` region so
	// "Draft saved" updates are announced; the `:empty` rule clips it out of view
	// (not `display: none`, which would drop it from the a11y tree) while idle.
	const [ status, setStatus ] = useState< string >( '' );
	const [ showInspector, setShowInspector ] = useState( false );
	const inspectorToggleRef = useRef< HTMLButtonElement >( null );
	// The block canvas, handed to `BlockTools` so the floating block toolbar
	// anchors to — and scroll-follows — the selected block.
	const contentRef = useRef< HTMLDivElement >( null );
	// Set by undo/redo so the value-sync effect below forwards the restored tree.
	const pendingForward = useRef( false );

	// Forward the live tree to the publish flow on every change. This is a cheap
	// Studio state update, which the publish action reads at click time, so it
	// must stay current (not debounced).
	const notify = useCallback(
		( next: BlockInstance[] ) => {
			onChange?.( next );
		},
		[ onChange ]
	);

	// Persisting the local draft serializes the whole block tree and writes
	// `localStorage` — too costly to run on every keystroke (`onInput` fires per
	// character). Debounce it so rapid typing coalesces into one write; the
	// "Draft saved" status reflects the write that actually lands.
	const persist = useDebounce(
		useCallback( ( next: BlockInstance[] ) => {
			window.localStorage.setItem( SPIKE_BLOCKS_KEY, JSON.stringify( next ) );
			setStatus( `Draft saved · ${ next.length } block(s)` );
		}, [] ),
		800
	);
	// Flush any pending draft write when the editor unmounts (Studio remounts it
	// on publish/article switch) so the last edits in the debounce window survive.
	useEffect( () => () => persist.flush(), [ persist ] );

	const forward = useCallback(
		( next: BlockInstance[] ) => {
			notify( next );
			persist( next );
		},
		[ notify, persist ]
	);

	// `onInput` = transient edits (typing) → staged into the pending history record;
	// `onChange` = persistent edits (insert/remove/move) → a new undo step. Both keep
	// the publish state current.
	const onInput = useCallback(
		( next: BlockInstance[] ) => {
			setBlocks( next, true );
			forward( next );
		},
		[ setBlocks, forward ]
	);
	const onChangeBlocks = useCallback(
		( next: BlockInstance[] ) => {
			setBlocks( next, false );
			forward( next );
		},
		[ setBlocks, forward ]
	);

	// `undo`/`redo` swap the controlled `value`, but `BlockEditorProvider` doesn't
	// re-fire `onInput`/`onChange` for a controlled-value change (it would loop) —
	// so the draft + publish flow would otherwise miss it. Flag the change here and
	// forward the restored tree once `value` reflects it.
	const handleUndo = useCallback( () => {
		pendingForward.current = true;
		undo();
	}, [ undo ] );
	const handleRedo = useCallback( () => {
		pendingForward.current = true;
		redo();
	}, [ redo ] );
	useEffect( () => {
		if ( ! pendingForward.current ) {
			return;
		}
		pendingForward.current = false;
		forward( value as BlockInstance[] );
	}, [ value, forward ] );

	const settings = useMemo(
		() => ( {
			// A custom mediaUpload routes uploads to the PDS as a blob (SP3).
			...( mediaUpload ? { mediaUpload } : {} ),
			// Restrict the inserter/transforms to the curated set. The registry is
			// already pruned to it (registerSkyPressBlocks); this scopes the UI too.
			allowedBlockTypes: [ ...ALLOWED_BLOCKS ],
			// Float the selected block's toolbar by the block (vs. docking it in a
			// fixed bar). `BlockTools` renders the floating toolbar into the in-editor
			// `Popover.Slot`, anchored via `__unstableContentRef` below. It shows on a
			// block with content — an unmodified empty paragraph gets the side inserter
			// instead (Gutenberg's `useShowBlockTools`).
			hasFixedToolbar: false,
		} ),
		[ mediaUpload ]
	);

	// Undo/redo from the keyboard. Capture phase so we win over contentEditable's
	// native history before rich-text handles the key.
	const onKeyDownCapture = useCallback(
		( event: ReactKeyboardEvent ) => {
			const mod = event.metaKey || event.ctrlKey;
			if ( ! mod || event.key.toLowerCase() !== 'z' ) {
				return;
			}
			// Don't hijack undo from a text field in the chrome (inspector Alt text,
			// link popover URL) — let its native undo win there.
			if ( ! shouldHandleEditorUndo( event.target, contentRef.current ) ) {
				return;
			}
			event.preventDefault();
			if ( event.shiftKey ) {
				handleRedo();
			} else {
				handleUndo();
			}
		},
		[ handleUndo, handleRedo ]
	);

	return (
		<EditorErrorBoundary>
			<ShortcutProvider>
				<SlotFillProvider>
					<BlockEditorProvider
						value={ blocks }
						onInput={ onInput }
						onChange={ onChangeBlocks }
						settings={ settings }
					>
						{ /* eslint-disable-next-line jsx-a11y/no-static-element-interactions */ }
						<div className="skypress-editor" onKeyDownCapture={ onKeyDownCapture }>
							<div className="skypress-editor__toolbar">
								<Inserter rootClientId={ undefined } />
								<div className="skypress-editor__toolbar-spacer" />
								<Button
									icon={ undoIcon }
									label="Undo"
									onClick={ handleUndo }
									disabled={ ! hasUndo }
								/>
								<Button
									icon={ redoIcon }
									label="Redo"
									onClick={ handleRedo }
									disabled={ ! hasRedo }
								/>
								<Button
									ref={ inspectorToggleRef }
									icon={ cog }
									label="Block settings"
									isPressed={ showInspector }
									aria-expanded={ showInspector }
									onClick={ () => setShowInspector( ( open ) => ! open ) }
								/>
								{ showInspector && (
									<Popover
										anchor={ inspectorToggleRef.current }
										placement="bottom-end"
										onClose={ () => setShowInspector( false ) }
										className="skypress-editor__inspector"
									>
										<BlockInspector />
									</Popover>
								) }
							</div>

							<div
								className="skypress-editor__body editor-styles-wrapper"
								ref={ contentRef }
							>
								<BlockTools __unstableContentRef={ contentRef }>
									<BlockEditorKeyboardShortcuts.Register />
									<WritingFlow>
										<ObserveTyping>
											<BlockList />
										</ObserveTyping>
									</WritingFlow>
								</BlockTools>
							</div>

							<Popover.Slot />

							<p className="skypress-editor__status" aria-live="polite">
								{ status }
							</p>
						</div>
					</BlockEditorProvider>
				</SlotFillProvider>
			</ShortcutProvider>
		</EditorErrorBoundary>
	);
}
