/**
 * Gutenberg left panel: browse JBM Library + My Blocks with live thumbs,
 * search/category filters, and native canvas drag-insert (variant modal intact).
 */
import apiFetch from '@wordpress/api-fetch';
import { Draggable, SelectControl, TextControl } from '@wordpress/components';
import {
	createPortal,
	useCallback,
	useEffect,
	useMemo,
	useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { useDispatch, useSelect } from '@wordpress/data';
import { createBlock, getBlockType } from '@wordpress/blocks';
import { brandLogoUrl, brandBlockIcon } from '../shared/brandLogo';
import LibraryCardThumb from '../admin/components/LibraryCardThumb';
import { getEditorMeta } from './libraryVariants';

const PANEL_WIDTH = 360;

function tagTone( slug = '' ) {
	const key = String( slug ).toLowerCase();
	if ( /hero|landing|banner/.test( key ) ) {
		return 'primary';
	}
	if ( /shop|commerce|product|pricing/.test( key ) ) {
		return 'success';
	}
	if ( /social|testimonial|proof/.test( key ) ) {
		return 'secondary';
	}
	if ( /form|checkout|contact/.test( key ) ) {
		return 'warning';
	}
	if ( /faq|accordion|tabs/.test( key ) ) {
		return 'tertiary';
	}
	return 'muted';
}

function editorRestPath( path ) {
	const meta = getEditorMeta();
	const base = String(
		meta.restUrl || '/wp-json/joc-blockora-morph/v1/'
	).replace( /\/?$/, '/' );
	const clean = String( path || '' ).replace( /^\//, '' );
	if ( base.includes( '/wp-json/' ) ) {
		const ns = base.split( '/wp-json/' )[ 1 ] || 'joc-blockora-morph/v1/';
		return `/${ ns.replace( /\/?$/, '' ) }/${ clean }`.replace(
			/\/+/g,
			'/'
		);
	}
	return `/joc-blockora-morph/v1/${ clean }`;
}

function shapeLibraryItem( block ) {
	const variants = Array.isArray( block?.variants ) ? block.variants : [];
	return {
		...block,
		key: block.name || `lib-${ block.slug }`,
		title: block.title || block.name || '',
		categorySlug: block.categorySlug || '',
		categoryName: block.categoryName || '',
		variantCount: variants.length,
		draggable: Boolean( block?.name && getBlockType( block.name ) ),
		isProLocked: Boolean( block?.isPro ) && ! getEditorMeta().isPro,
		source: 'library',
	};
}

function shapeMyBlockItem( row ) {
	const slug = String( row?.slug || '' );
	const name = slug ? `joc-blockora-morph/${ slug }` : '';
	const registered = Boolean( name && getBlockType( name ) );
	const categorySlugs = Array.isArray( row?.categorySlugs )
		? row.categorySlugs.filter( Boolean )
		: [];
	const categorySlug =
		row.categorySlug || categorySlugs[ 0 ] || '';
	return {
		name,
		key: `my-${ row.id || slug }`,
		title: row.name || slug,
		slug,
		id: row.id,
		markup: row.markup || '',
		cssVars: row.cssVars || {},
		defaultAttrs: row.defaultAttrs || {},
		fieldSchema: row.fieldSchema || [],
		categorySlug,
		categorySlugs,
		categoryName: row.categoryName || '',
		variantCount: 0,
		isEnabled: Boolean( row.isEnabled ),
		draggable: registered && Boolean( row.isEnabled ),
		isProLocked: false,
		source: 'my',
	};
}

function variantLabel( count ) {
	return count === 1
		? __( '1 variant', 'joc-blockora-morph' )
		: `${ count } ${ __( 'variants', 'joc-blockora-morph' ) }`;
}

/**
 * Keep one portal host node alive inside Gutenberg's React-owned DOM.
 *
 * The node identity never changes, so the portal is never torn down when the
 * editor re-renders and drops our injected element — it is simply re-attached.
 *
 * @param {string}   className Class name for the host element.
 * @param {Function} place     Attaches the node; returns false while its
 *                             target container does not exist yet.
 */
function useStableSlot( className, place ) {
	const [ host, setHost ] = useState( null );

	useEffect( () => {
		const node = document.createElement( 'div' );
		node.className = className;

		let disposed = false;
		let frame = 0;

		const sync = () => {
			frame = 0;
			if ( disposed ) {
				return;
			}
			// Our own writes would otherwise re-trigger the observer.
			observer.disconnect();
			let placed = false;
			try {
				placed = place( node ) !== false;
			} finally {
				if ( ! disposed ) {
					observer.observe( document.body, {
						childList: true,
						subtree: true,
					} );
				}
			}
			if ( placed ) {
				setHost( ( prev ) => ( prev === node ? prev : node ) );
			}
		};

		const observer = new MutationObserver( () => {
			if ( disposed || frame ) {
				return;
			}
			frame = window.requestAnimationFrame( sync );
		} );

		sync();

		return () => {
			disposed = true;
			if ( frame ) {
				window.cancelAnimationFrame( frame );
			}
			observer.disconnect();
			node.remove();
		};
	}, [] );

	return host;
}

function dropStaleSlots( node, className ) {
	document.querySelectorAll( `.${ className }` ).forEach( ( el ) => {
		if ( el !== node ) {
			el.remove();
		}
	} );
}

function placeToolbarSlot( node ) {
	// Prefer a sibling of NavigableToolbar (`.editor-document-tools`), not a
	// child — a plain <button> inside that toolbar triggers the WP 5.6
	// "custom components as toolbar controls" deprecation.
	const tools =
		document.querySelector( '.editor-document-tools' ) ||
		document.querySelector( '.edit-post-header-toolbar' );
	if ( ! tools?.parentNode ) {
		return false;
	}
	dropStaleSlots( node, 'jocbm-block-browser-toolbar-slot' );

	const parent = tools.parentNode;
	if ( node.parentNode === parent && node.previousSibling === tools ) {
		return true;
	}
	parent.insertBefore( node, tools.nextSibling );
	return true;
}

function placePanelSlot( node ) {
	const body = document.querySelector(
		'.interface-interface-skeleton__body'
	);
	if ( ! body ) {
		return false;
	}
	dropStaleSlots( node, 'jocbm-block-browser-host' );

	if ( node.parentNode === body && ! node.previousSibling ) {
		return true;
	}
	body.insertBefore( node, body.firstChild );
	return true;
}

function useDocumentToolsMount() {
	return useStableSlot(
		'jocbm-block-browser-toolbar-slot',
		placeToolbarSlot
	);
}

function usePanelMount() {
	return useStableSlot( 'jocbm-block-browser-host', placePanelSlot );
}

function BrowserCard( { item } ) {
	const canDrag = item.draggable && ! item.isProLocked;
	const blocks = useMemo( () => {
		if ( ! canDrag || ! item.name ) {
			return [];
		}
		try {
			return [ createBlock( item.name ) ];
		} catch ( e ) {
			return [];
		}
	}, [ canDrag, item.name ] );

	const className =
		'jocbm-block-browser__card' +
		( canDrag ? ' is-draggable' : '' ) +
		( item.isProLocked ? ' is-locked' : '' ) +
		( ! item.draggable ? ' is-disabled' : '' );

	const inner = ( dragProps = {} ) => (
		<div
			className={ className }
			title={ item.title }
			draggable={ dragProps.draggable }
			onDragStart={ dragProps.onDragStart }
			onDragEnd={ dragProps.onDragEnd }
		>
			<div className="jocbm-block-browser__card-media">
				<LibraryCardThumb block={ item } />
				<span
					className={ `jocbm-block-browser__badge is-${ tagTone(
						item.categorySlug
					) }` }
				>
					{ item.categoryName ||
						item.categorySlug ||
						__( 'Block', 'joc-blockora-morph' ) }
				</span>
				{ item.variantCount > 0 ? (
					<span className="jocbm-block-browser__variants">
						{ variantLabel( item.variantCount ) }
					</span>
				) : null }
				{ item.isProLocked ? (
					<span className="jocbm-block-browser__lock">
						{ __( 'Pro', 'joc-blockora-morph' ) }
					</span>
				) : null }
			</div>
			<div className="jocbm-block-browser__card-body">
				<strong className="jocbm-block-browser__card-title">
					{ item.title }
				</strong>
				{ ! item.isEnabled && item.source === 'my' ? (
					<span className="jocbm-block-browser__card-meta">
						{ __( 'Disabled', 'joc-blockora-morph' ) }
					</span>
				) : null }
			</div>
		</div>
	);

	if ( ! canDrag || ! blocks.length ) {
		return inner();
	}

	// Draggable clones this node's markup as the drag chip; without it the
	// component falls back to `elementId` lookups and throws.
	const dragChip = (
		<div className="jocbm-block-browser__chip">
			{ brandLogoUrl() ? (
				<img src={ brandLogoUrl() } alt="" width={ 18 } height={ 18 } />
			) : null }
			<span>{ item.title }</span>
		</div>
	);

	return (
		<Draggable
			__experimentalTransferDataType="wp-blocks"
			__experimentalDragComponent={ dragChip }
			transferData={ { type: 'inserter', blocks } }
			cloneClassname="jocbm-block-browser__drag-clone"
			onDragStart={ ( event ) => {
				event.dataTransfer.effectAllowed = 'copy';
				document.body.classList.add( 'jocbm-block-browser-dragging' );
			} }
			onDragEnd={ () => {
				document.body.classList.remove(
					'jocbm-block-browser-dragging'
				);
			} }
		>
			{ ( { onDraggableStart, onDraggableEnd } ) =>
				inner( {
					draggable: true,
					onDragStart: onDraggableStart,
					onDragEnd: onDraggableEnd,
				} )
			}
		</Draggable>
	);
}

function BlockBrowserPanel( { open, onClose } ) {
	const [ tab, setTab ] = useState( 'library' );
	const [ query, setQuery ] = useState( '' );
	const [ category, setCategory ] = useState( 'all' );
	const [ myBlocks, setMyBlocks ] = useState( [] );
	const [ categories, setCategories ] = useState( [] );
	const [ loadingMy, setLoadingMy ] = useState( false );

	const libraryItems = useMemo(
		() => ( getEditorMeta().libraryBlocks || [] ).map( shapeLibraryItem ),
		// Recompute when panel opens so newly registered types are visible.
		// eslint-disable-next-line react-hooks/exhaustive-deps
		[ open ]
	);

	useEffect( () => {
		if ( ! open ) {
			return undefined;
		}
		let cancelled = false;
		setLoadingMy( true );
		Promise.all( [
			apiFetch( { path: editorRestPath( 'user-blocks' ) } ).catch(
				() => ( { items: [] } )
			),
			apiFetch( { path: editorRestPath( 'categories' ) } ).catch(
				() => ( { items: [] } )
			),
		] )
			.then( ( [ users, cats ] ) => {
				if ( cancelled ) {
					return;
				}
				const rows = Array.isArray( users?.items )
					? users.items
					: Array.isArray( users )
					? users
					: [];
				setMyBlocks( rows.map( shapeMyBlockItem ) );
				const catRows = Array.isArray( cats?.items )
					? cats.items
					: Array.isArray( cats )
					? cats
					: [];
				setCategories( catRows );
			} )
			.finally( () => {
				if ( ! cancelled ) {
					setLoadingMy( false );
				}
			} );
		return () => {
			cancelled = true;
		};
	}, [ open ] );

	const categoryOptions = useMemo( () => {
		const source = tab === 'library' ? libraryItems : myBlocks;
		const map = new Map();
		source.forEach( ( item ) => {
			const slugs = [
				item.categorySlug,
				...( Array.isArray( item.categorySlugs )
					? item.categorySlugs
					: [] ),
			].filter( Boolean );
			slugs.forEach( ( slug ) => {
				if ( map.has( slug ) ) {
					return;
				}
				map.set(
					slug,
					item.categoryName ||
						categories.find( ( c ) => c.slug === slug )?.name ||
						slug
				);
			} );
		} );
		return [
			{
				label: __( 'All categories', 'joc-blockora-morph' ),
				value: 'all',
			},
			...Array.from( map.entries() )
				.sort( ( a, b ) =>
					String( a[ 1 ] ).localeCompare( String( b[ 1 ] ), undefined, {
						sensitivity: 'base',
					} )
				)
				.map( ( [ value, label ] ) => ( { value, label } ) ),
		];
	}, [ tab, libraryItems, myBlocks, categories ] );

	// Drop a category filter that no longer exists for the active tab.
	useEffect( () => {
		if ( category === 'all' ) {
			return;
		}
		const stillValid = categoryOptions.some(
			( opt ) => opt.value === category
		);
		if ( ! stillValid ) {
			setCategory( 'all' );
		}
	}, [ category, categoryOptions ] );

	const items = useMemo( () => {
		const source = tab === 'library' ? libraryItems : myBlocks;
		const q = query.trim().toLowerCase();
		return source.filter( ( item ) => {
			if ( category !== 'all' ) {
				const slugs = Array.isArray( item.categorySlugs )
					? item.categorySlugs
					: [];
				const match =
					item.categorySlug === category ||
					slugs.includes( category );
				if ( ! match ) {
					return false;
				}
			}
			if ( ! q ) {
				return true;
			}
			const hay = `${ item.title || '' } ${ item.categoryName || '' } ${
				item.categorySlug || ''
			} ${ item.slug || '' }`.toLowerCase();
			return hay.includes( q );
		} );
	}, [ tab, libraryItems, myBlocks, query, category ] );

	return (
		<aside
			className={
				'jocbm-block-browser' + ( open ? ' is-open' : '' )
			}
			style={ { width: PANEL_WIDTH } }
			aria-label={ __( 'JBM blocks', 'joc-blockora-morph' ) }
			aria-hidden={ ! open }
		>
			<header className="jocbm-block-browser__header">
				<div className="jocbm-block-browser__brand">
					<img
						src={ brandLogoUrl() }
						alt=""
						width={ 22 }
						height={ 22 }
					/>
					<strong>{ __( 'JBM Blocks', 'joc-blockora-morph' ) }</strong>
				</div>
				<button
					type="button"
					className="jocbm-block-browser__close"
					onClick={ onClose }
					aria-label={ __( 'Close', 'joc-blockora-morph' ) }
				>
					<span
						className="dashicons dashicons-no-alt"
						aria-hidden="true"
					/>
				</button>
			</header>

			<div className="jocbm-block-browser__tabs" role="tablist">
				<button
					type="button"
					role="tab"
					aria-selected={ tab === 'library' }
					className={
						'jocbm-block-browser__tab' +
						( tab === 'library' ? ' is-active' : '' )
					}
					onClick={ () => setTab( 'library' ) }
				>
					{ __( 'JBM Library', 'joc-blockora-morph' ) }
				</button>
				<button
					type="button"
					role="tab"
					aria-selected={ tab === 'my' }
					className={
						'jocbm-block-browser__tab' +
						( tab === 'my' ? ' is-active' : '' )
					}
					onClick={ () => setTab( 'my' ) }
				>
					{ __( 'My Blocks', 'joc-blockora-morph' ) }
				</button>
			</div>

			<div className="jocbm-block-browser__filters">
				<TextControl
					className="jocbm-block-browser__search"
					label={ __( 'Search', 'joc-blockora-morph' ) }
					hideLabelFromVision
					placeholder={ __( 'Search blocks…', 'joc-blockora-morph' ) }
					value={ query }
					onChange={ setQuery }
					__nextHasNoMarginBottom
					__next40pxDefaultSize
				/>
				<SelectControl
					className="jocbm-block-browser__category"
					label={ __( 'Category', 'joc-blockora-morph' ) }
					hideLabelFromVision
					value={ category }
					options={ categoryOptions }
					onChange={ setCategory }
					__nextHasNoMarginBottom
					__next40pxDefaultSize
				/>
			</div>

			<div className="jocbm-block-browser__list">
				{ tab === 'my' && loadingMy ? (
					<p className="jocbm-block-browser__empty">
						{ __( 'Loading…', 'joc-blockora-morph' ) }
					</p>
				) : null }
				{ tab === 'library' || ! loadingMy
					? items.map( ( item ) => (
							<BrowserCard key={ item.key } item={ item } />
					  ) )
					: null }
				{ ( tab === 'library' || ! loadingMy ) && ! items.length ? (
					<p className="jocbm-block-browser__empty">
						{ __( 'No blocks found.', 'joc-blockora-morph' ) }
					</p>
				) : null }
			</div>

			<p className="jocbm-block-browser__hint">
				{ __(
					'Drag a block into the canvas to insert it.',
					'joc-blockora-morph'
				) }
			</p>
		</aside>
	);
}

function BlockBrowserRoot() {
	const [ open, setOpen ] = useState( false );
	const toolbarHost = useDocumentToolsMount();
	const panelHost = usePanelMount();
	const editorDispatch = useDispatch( 'core/editor' );
	const setIsListViewOpened = editorDispatch?.setIsListViewOpened;
	const listViewOpen = useSelect( ( select ) => {
		try {
			return Boolean( select( 'core/editor' )?.isListViewOpened?.() );
		} catch ( e ) {
			return false;
		}
	}, [] );

	const toggle = useCallback( () => {
		setOpen( ( prev ) => {
			const next = ! prev;
			if ( next && typeof setIsListViewOpened === 'function' ) {
				setIsListViewOpened( false );
			}
			return next;
		} );
	}, [ setIsListViewOpened ] );

	useEffect( () => {
		if ( listViewOpen && open ) {
			setOpen( false );
		}
	}, [ listViewOpen, open ] );

	useEffect( () => {
		document.body.classList.toggle( 'jocbm-block-browser-open', open );
		return () => {
			document.body.classList.remove( 'jocbm-block-browser-open' );
		};
	}, [ open ] );

	const logo = brandLogoUrl();

	return (
		<>
			{ toolbarHost
				? createPortal(
						<button
							type="button"
							className={
								'jocbm-block-browser-toggle' +
								( open ? ' is-active' : '' )
							}
							onClick={ toggle }
							aria-pressed={ open }
							aria-label={ __(
								'JBM blocks library',
								'joc-blockora-morph'
							) }
							title={ __(
								'JBM blocks library',
								'joc-blockora-morph'
							) }
						>
							{ logo ? (
								<img
									src={ logo }
									alt=""
									width={ 32 }
									height={ 32 }
									draggable={ false }
								/>
							) : (
								brandBlockIcon( 32 )
							) }
						</button>,
						toolbarHost
				  )
				: null }
			{ panelHost
				? createPortal(
						<BlockBrowserPanel
							open={ open }
							onClose={ () => setOpen( false ) }
						/>,
						panelHost
				  )
				: null }
		</>
	);
}

registerPlugin( 'joc-blockora-morph-block-browser', {
	render: BlockBrowserRoot,
	icon: brandBlockIcon( 20 ),
} );
