/**
 * Icon library registry (mirrors PHP IconRegistry).
 * Stored refs: `libraryId:iconId` (legacy bare dashicon → dashicons:{slug}).
 */

import { BLOCK_ICONS } from './dashicons';
import { LUCIDE_CATALOG } from './lucideCatalog';

/**
 * @typedef {{ id: string, label: string, keywords?: string[] }} IconCatalogItem
 * @typedef {{
 *   id: string,
 *   label: string,
 *   enabled: boolean,
 *   builtin?: boolean,
 *   type: 'font-class'|'svg-sprite'|'svg-inline'|'css-mask',
 *   stylesheetUrl?: string,
 *   classTemplate?: string,
 *   textContent?: boolean,
 *   spriteUrl?: string,
 *   catalog?: IconCatalogItem[],
 * }} IconLibrary
 */

/**
 * @return {IconLibrary[]}
 */
export function defaultIconLibraries() {
	return [
		{
			id: 'dashicons',
			label: 'Dashicons',
			enabled: true,
			builtin: true,
			type: 'font-class',
			stylesheetUrl: '',
			classTemplate: 'dashicons dashicons-{id}',
			textContent: false,
			spriteUrl: '',
			catalog: BLOCK_ICONS.map( ( id ) => ( {
				id,
				label: id,
				keywords: [ id ],
			} ) ),
		},
		{
			id: 'fa',
			label: 'Font Awesome',
			enabled: false,
			builtin: true,
			type: 'font-class',
			stylesheetUrl: '',
			classTemplate: 'fa-solid fa-{id}',
			textContent: false,
			spriteUrl: '',
			catalog: FA_STARTER,
		},
		{
			id: 'material',
			label: 'Material Icons',
			enabled: false,
			builtin: true,
			type: 'font-class',
			stylesheetUrl: '',
			classTemplate: 'material-icons',
			textContent: true,
			spriteUrl: '',
			catalog: MATERIAL_STARTER,
		},
		{
			id: 'lucide',
			label: 'Lucide',
			enabled: true,
			builtin: true,
			type: 'svg-inline',
			stylesheetUrl: '',
			classTemplate: '',
			textContent: false,
			spriteUrl: '',
			catalog: LUCIDE_CATALOG,
		},
	];
}

const FA_STARTER = [
	'house',
	'user',
	'envelope',
	'phone',
	'arrow-right',
	'arrow-left',
	'check',
	'xmark',
	'plus',
	'minus',
	'star',
	'heart',
	'cart-shopping',
	'magnifying-glass',
	'gear',
	'download',
	'upload',
	'link',
	'location-dot',
	'calendar',
	'clock',
	'play',
	'pause',
	'chevron-right',
	'chevron-left',
].map( ( id ) => ( { id, label: id, keywords: [ id ] } ) );

const MATERIAL_STARTER = [
	'home',
	'person',
	'mail',
	'call',
	'arrow_forward',
	'arrow_back',
	'check',
	'close',
	'add',
	'remove',
	'star',
	'favorite',
	'shopping_cart',
	'search',
	'settings',
	'download',
	'upload',
	'link',
	'location_on',
	'calendar_today',
	'schedule',
	'play_arrow',
	'pause',
	'chevron_right',
	'chevron_left',
	'info',
	'warning',
].map( ( id ) => ( {
	id,
	label: id,
	keywords: [ id.replace( /_/g, ' ' ) ],
} ) );

/**
 * Libraries from localized config (admin / editor / front).
 *
 * @return {IconLibrary[]}
 */
export function getIconLibraries() {
	if ( typeof window === 'undefined' ) {
		return defaultIconLibraries();
	}
	const cfg =
		window.jocbmAdmin ||
		window.jocbmEditor ||
		window.jocbmFront ||
		{};
	const list = cfg.iconLibraries?.libraries || cfg.iconLibraries;
	if ( Array.isArray( list ) && list.length ) {
		return list;
	}
	return defaultIconLibraries();
}

/**
 * @return {IconLibrary[]}
 */
export function getEnabledIconLibraries() {
	return getIconLibraries().filter(
		( l ) =>
			l &&
			( l.enabled || l.id === 'dashicons' || l.id === 'lucide' )
	);
}

/**
 * @param {string} id
 * @return {IconLibrary|null}
 */
export function getIconLibrary( id ) {
	const key = String( id || '' )
		.trim()
		.toLowerCase();
	return getIconLibraries().find( ( l ) => l.id === key ) || null;
}

/**
 * @param {string} ref
 * @return {{ libraryId: string, iconId: string }|null}
 */
export function parseIconRef( ref ) {
	const raw = String( ref || '' ).trim();
	if ( ! raw ) {
		return null;
	}
	if ( raw.includes( ':' ) ) {
		const [ libraryId, ...rest ] = raw.split( ':' );
		const iconId = rest.join( ':' ).trim();
		const lib = String( libraryId || '' )
			.trim()
			.toLowerCase()
			.replace( /[^a-z0-9_-]/g, '' );
		if ( ! lib || ! /^[a-z0-9_-]+$/i.test( iconId ) ) {
			return null;
		}
		return { libraryId: lib, iconId };
	}
	if ( /^[a-z0-9-]+$/i.test( raw ) ) {
		return { libraryId: 'dashicons', iconId: raw };
	}
	return null;
}

/**
 * @param {string} ref
 * @return {string}
 */
export function normalizeIconRef( ref ) {
	const parsed = parseIconRef( ref );
	if ( ! parsed ) {
		return '';
	}
	return `${ parsed.libraryId }:${ parsed.iconId }`;
}

/**
 * @param {string} ref
 * @return {boolean}
 */
export function isValidIconRef( ref ) {
	return Boolean( parseIconRef( ref ) );
}

/**
 * Escape attribute text.
 *
 * @param {string} value
 * @return {string}
 */
function escAttr( value ) {
	return String( value || '' )
		.replace( /&/g, '&amp;' )
		.replace( /"/g, '&quot;' )
		.replace( /</g, '&lt;' )
		.replace( />/g, '&gt;' );
}

/**
 * Normalize Lucide (stroke) SVG for UI — single-line, fill none, fixed box.
 * Only mutates the root <svg> tag so child shape geometry (e.g. rect width/height) stays intact.
 * Gutenberg applies `fill: currentColor` on block icons; without root fill=none, Lucide looks solid/broken.
 *
 * @param {string} svg
 * @return {string}
 */
export function normalizeInlineSvg( svg ) {
	let out = String( svg || '' ).trim();
	if ( ! /^<svg\b/i.test( out ) ) {
		return '';
	}
	out = out.replace( /\s+/g, ' ' );
	const open = out.match( /^<svg\b[^>]*>/i );
	if ( ! open ) {
		return '';
	}
	let tag = open[ 0 ];
	tag = tag.replace(
		/\s(fill|stroke|width|height|stroke-width|stroke-linecap|stroke-linejoin)="[^"]*"/gi,
		''
	);
	tag = tag.replace(
		/^<svg\b/i,
		'<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"'
	);
	return tag + out.slice( open[ 0 ].length );
}

/** @type {Map<string, string>} */
const inlineSvgCache = new Map();
/** @type {Map<string, Promise<string>>} */
const inlineSvgPending = new Map();

/**
 * Rest base + nonce from localized config.
 *
 * @return {{ restUrl: string, nonce: string }}
 */
function restConfig() {
	const cfg =
		( typeof window !== 'undefined' &&
			( window.jocbmAdmin ||
				window.jocbmEditor ||
				window.jocbmFront ||
				window.jocbmFrontendEditor ) ) ||
		{};
	return {
		restUrl: String( cfg.restUrl || '' ).replace( /\/?$/, '/' ),
		nonce: String( cfg.nonce || '' ),
	};
}

/**
 * Fetch one svg-inline icon (cached). Used by the editor picker — public
 * frontend resolves SVGs in PHP and never needs this.
 *
 * @param {string} libraryId
 * @param {string} iconId
 * @return {Promise<string>}
 */
export function fetchInlineIconSvg( libraryId, iconId ) {
	const lib = String( libraryId || '' ).trim();
	const id = String( iconId || '' ).trim();
	const key = `${ lib }:${ id }`;
	if ( ! lib || ! id ) {
		return Promise.resolve( '' );
	}
	if ( inlineSvgCache.has( key ) ) {
		return Promise.resolve( inlineSvgCache.get( key ) || '' );
	}
	if ( inlineSvgPending.has( key ) ) {
		return inlineSvgPending.get( key );
	}

	const { restUrl, nonce } = restConfig();
	if ( ! restUrl ) {
		return Promise.resolve( '' );
	}

	const url =
		restUrl +
		'icons/svg?library=' +
		encodeURIComponent( lib ) +
		'&id=' +
		encodeURIComponent( id );

	const pending = fetch( url, {
		credentials: 'same-origin',
		headers: nonce
			? {
					'X-WP-Nonce': nonce,
			  }
			: {},
	} )
		.then( ( res ) => ( res.ok ? res.json() : null ) )
		.then( ( data ) => {
			const svg = normalizeInlineSvg( String( data?.svg || '' ) );
			if ( svg ) {
				inlineSvgCache.set( key, svg );
			}
			inlineSvgPending.delete( key );
			return svg;
		} )
		.catch( () => {
			inlineSvgPending.delete( key );
			return '';
		} );

	inlineSvgPending.set( key, pending );
	return pending;
}

/**
 * Sync lookup of a previously fetched inline SVG.
 *
 * @param {string} ref
 * @return {string}
 */
export function getCachedInlineIconSvg( ref ) {
	const parsed = parseIconRef( ref );
	if ( ! parsed ) {
		return '';
	}
	return (
		normalizeInlineSvg(
			inlineSvgCache.get( `${ parsed.libraryId }:${ parsed.iconId }` ) || ''
		) || ''
	);
}

/**
 * Replace pending svg-inline placeholders with fetched Lucide (etc.) markup.
 * Safe to call after interpolate → innerHTML / srcdoc paint.
 *
 * @param {ParentNode|null|undefined} root
 * @return {Promise<number>} Number of icons hydrated.
 */
export function hydratePendingInlineIcons( root ) {
	if ( ! root || typeof root.querySelectorAll !== 'function' ) {
		return Promise.resolve( 0 );
	}
	const nodes = Array.from(
		root.querySelectorAll( '[data-bm-icon-ref].bm-icon--pending' )
	);
	if ( ! nodes.length ) {
		return Promise.resolve( 0 );
	}

	/** @type {Map<string, HTMLElement[]>} */
	const byRef = new Map();
	nodes.forEach( ( el ) => {
		const ref = String( el.getAttribute( 'data-bm-icon-ref' ) || '' ).trim();
		if ( ! ref ) {
			return;
		}
		if ( ! byRef.has( ref ) ) {
			byRef.set( ref, [] );
		}
		byRef.get( ref ).push( el );
	} );

	return Promise.all(
		Array.from( byRef.entries() ).map( async ( [ ref, els ] ) => {
			const parsed = parseIconRef( ref );
			if ( ! parsed ) {
				return 0;
			}
			await fetchInlineIconSvg( parsed.libraryId, parsed.iconId );
			let done = 0;
			els.forEach( ( el ) => {
				if ( ! el.isConnected ) {
					return;
				}
				const style = el.getAttribute( 'style' ) || '';
				const extra = Array.from( el.classList )
					.filter(
						( c ) =>
							c !== 'bm-icon' &&
							c !== 'bm-icon--pending' &&
							c !== 'bm-icon--svg'
					)
					.join( ' ' );
				const html = renderIconHtml( ref, style, extra );
				if ( ! html || html.includes( 'bm-icon--pending' ) ) {
					return;
				}
				const wrap = el.ownerDocument.createElement( 'span' );
				wrap.innerHTML = html;
				const next = wrap.firstElementChild;
				if ( next ) {
					el.replaceWith( next );
					done += 1;
				}
			} );
			return done;
		} )
	).then( ( counts ) => counts.reduce( ( sum, n ) => sum + n, 0 ) );
}

/**
 * Build icon HTML (matches PHP IconRegistry::render_html).
 *
 * @param {string} ref
 * @param {string} [style]
 * @param {string} [extraClass]
 * @param {IconLibrary[]} [libraries]
 * @return {string}
 */
export function renderIconHtml(
	ref,
	style = '',
	extraClass = '',
	libraries = null
) {
	const parsed = parseIconRef( ref );
	if ( ! parsed ) {
		return '';
	}
	const libs = Array.isArray( libraries ) ? libraries : getIconLibraries();
	let lib = libs.find( ( l ) => l.id === parsed.libraryId ) || null;
	if ( ! lib || ! lib.enabled ) {
		if ( parsed.libraryId === 'dashicons' || ! lib ) {
			lib = {
				type: 'font-class',
				classTemplate: 'dashicons dashicons-{id}',
				textContent: false,
			};
		} else if ( parsed.libraryId === 'lucide' ) {
			lib = {
				type: 'svg-inline',
				textContent: false,
			};
		} else {
			return '';
		}
	}

	const type = lib.type || 'font-class';
	const iconId = parsed.iconId;
	const extra = String( extraClass || '' ).trim();
	const styleAttr = String( style || '' ).trim();

	if ( type === 'svg-sprite' ) {
		const sprite = String( lib.spriteUrl || '' ).trim();
		if ( ! sprite ) {
			return '';
		}
		const cls = [ 'bm-icon', 'bm-icon--svg', extra ].filter( Boolean ).join( ' ' );
		return `<svg class="${ escAttr( cls ) }" style="${ escAttr(
			styleAttr
		) }" aria-hidden="true" focusable="false"><use href="${ escAttr(
			sprite
		) }#${ escAttr( iconId ) }"></use></svg>`;
	}

	if ( type === 'svg-inline' ) {
		const cached = normalizeInlineSvg( getCachedInlineIconSvg( ref ) );
		if ( ! cached ) {
			// Placeholder until hydratePendingInlineIcons() fetches SVG markup.
			const cls = [ 'bm-icon', 'bm-icon--pending', extra ]
				.filter( Boolean )
				.join( ' ' );
			return `<span class="${ escAttr( cls ) }" style="${ escAttr(
				styleAttr
			) }" data-bm-icon-ref="${ escAttr(
				`${ parsed.libraryId }:${ iconId }`
			) }" aria-hidden="true"></span>`;
		}
		// Apply class/style onto root <svg> only — never strip child attributes.
		const open = cached.match( /^<svg\b[^>]*>/i );
		if ( ! open ) {
			return '';
		}
		let tag = open[ 0 ].replace(
			/\s(class|style|aria-hidden|focusable)="[^"]*"/gi,
			''
		);
		const cls = [ 'bm-icon', 'bm-icon--svg', extra ].filter( Boolean ).join( ' ' );
		tag = tag.replace(
			/^<svg\b/i,
			`<svg class="${ escAttr( cls ) }" style="${ escAttr(
				styleAttr
			) }" aria-hidden="true" focusable="false"`
		);
		return tag + cached.slice( open[ 0 ].length );
	}

	if ( type === 'css-mask' ) {
		const maskTpl = String( lib.classTemplate || '' );
		const maskUrl = maskTpl.replace( /\{id\}/g, encodeURIComponent( iconId ) );
		let maskStyle = styleAttr;
		if ( maskUrl ) {
			maskStyle += `${
				maskStyle ? ';' : ''
			}mask-image:url(${ maskUrl });-webkit-mask-image:url(${ maskUrl });mask-size:contain;-webkit-mask-size:contain;mask-repeat:no-repeat;background-color:currentColor`;
		}
		const cls = [ 'bm-icon', 'bm-icon--mask', extra ].filter( Boolean ).join( ' ' );
		return `<span class="${ escAttr( cls ) }" style="${ escAttr(
			maskStyle
		) }" aria-hidden="true"></span>`;
	}

	const tpl = String( lib.classTemplate || 'dashicons dashicons-{id}' );
	const className = [ tpl.replace( /\{id\}/g, iconId ), extra ]
		.filter( Boolean )
		.join( ' ' )
		.trim();
	const textNode = lib.textContent
		? String( iconId )
				.replace( /&/g, '&amp;' )
				.replace( /</g, '&lt;' )
				.replace( />/g, '&gt;' )
		: '';
	return `<span class="${ escAttr( className ) }" style="${ escAttr(
		styleAttr
	) }" aria-hidden="true">${ textNode }</span>`;
}

/**
 * Ensure library stylesheets are present in document head (picker / admin).
 * Resolves root-relative URLs so Gutenberg (/wp-admin/…) loads the correct CSS.
 *
 * @param {IconLibrary[]} [libraries]
 */
export function ensureIconStylesheets( libraries = null ) {
	if ( typeof document === 'undefined' ) {
		return;
	}
	const libs = ( Array.isArray( libraries ) ? libraries : getEnabledIconLibraries() ).filter(
		( l ) => l && l.enabled
	);
	libs.forEach( ( lib ) => {
		if ( lib.id === 'dashicons' ) {
			return;
		}
		const primary = absolutizeAssetUrl( String( lib.stylesheetUrl || '' ).trim() );
		if ( ! primary ) {
			return;
		}
		expandStylesheetUrls( primary ).forEach( ( href, i ) => {
			const id = `bm-icon-lib-${ lib.id }${ i ? `-${ i }` : '' }`;
			let link = document.getElementById( id );
			if ( ! link ) {
				link = document.createElement( 'link' );
				link.id = id;
				link.rel = 'stylesheet';
				document.head.appendChild( link );
			}
			if ( link.getAttribute( 'href' ) !== href ) {
				link.setAttribute( 'href', href );
			}
		} );
	} );
}

/**
 * FA fontawesome.css needs solid.css for @font-face.
 *
 * @param {string} url
 * @return {string[]}
 */
export function expandStylesheetUrls( url ) {
	const primary = String( url || '' ).trim();
	if ( ! primary ) {
		return [];
	}
	const urls = [ primary ];
	const m = primary.match( /^(.*)\/fontawesome(\.min)?\.css(\?.*)?$/i );
	if ( m ) {
		urls.push( `${ m[ 1 ] }/solid${ m[ 2 ] || '' }.css${ m[ 3 ] || '' }` );
	}
	return [ ...new Set( urls ) ];
}

/**
 * @param {string} href
 * @return {string}
 */
export function absolutizeAssetUrl( href ) {
	const raw = String( href || '' ).trim();
	if ( ! raw || typeof window === 'undefined' ) {
		return raw;
	}
	if ( /^https?:\/\//i.test( raw ) ) {
		return raw;
	}
	if ( raw.startsWith( '//' ) ) {
		return `${ window.location.protocol }${ raw }`;
	}
	if ( raw.startsWith( '/' ) ) {
		return `${ window.location.origin }${ raw }`;
	}
	try {
		return new URL( raw, window.location.origin ).href;
	} catch ( e ) {
		return raw;
	}
}

/**
 * Class names for a library + icon id (no HTML).
 *
 * @param {IconLibrary|null|undefined} lib
 * @param {string} iconId
 * @return {string}
 */
export function iconClassNames( lib, iconId ) {
	const tpl = String(
		lib?.classTemplate || 'dashicons dashicons-{id}'
	);
	return tpl.replace( /\{id\}/g, String( iconId || '' ) ).trim();
}

/**
 * Collect svg-inline icon refs (e.g. lucide:…) from nested attrs / values.
 *
 * @param {unknown} value
 * @return {string[]}
 */
export function collectInlineIconRefs( value ) {
	const refs = new Set();
	const walk = ( v ) => {
		if ( typeof v === 'string' ) {
			const p = parseIconRef( v );
			if ( ! p ) {
				return;
			}
			const libs = getIconLibraries();
			const lib = libs.find( ( l ) => l.id === p.libraryId );
			const type =
				lib?.type ||
				( p.libraryId === 'lucide' ? 'svg-inline' : '' );
			if ( type === 'svg-inline' ) {
				refs.add( `${ p.libraryId }:${ p.iconId }` );
			}
			return;
		}
		if ( ! v || typeof v !== 'object' ) {
			return;
		}
		if ( Array.isArray( v ) ) {
			v.forEach( walk );
			return;
		}
		Object.keys( v ).forEach( ( key ) => {
			if ( key === 'icon' && typeof v[ key ] === 'string' ) {
				walk( v[ key ] );
			} else {
				walk( v[ key ] );
			}
		} );
	};
	walk( value );
	return Array.from( refs );
}

/**
 * Prefetch svg-inline icons so interpolate can emit real SVGs (not pending).
 *
 * @param {unknown} value
 * @return {Promise<number>} Number of icons fetched into cache.
 */
export function prefetchInlineIconsFromValue( value ) {
	const refs = collectInlineIconRefs( value );
	if ( ! refs.length ) {
		return Promise.resolve( 0 );
	}
	return Promise.all(
		refs.map( ( ref ) => {
			const parsed = parseIconRef( ref );
			if ( ! parsed ) {
				return Promise.resolve( '' );
			}
			return fetchInlineIconSvg( parsed.libraryId, parsed.iconId );
		} )
	).then( ( svgs ) => svgs.filter( Boolean ).length );
}

/**
 * Collect library ids from nested attrs.
 *
 * @param {unknown} value
 * @return {string[]}
 */
export function libraryIdsFromAttrs( value ) {
	const ids = new Set();
	const walk = ( v ) => {
		if ( typeof v === 'string' ) {
			const p = parseIconRef( v );
			if ( p ) {
				ids.add( p.libraryId );
			}
			return;
		}
		if ( ! v || typeof v !== 'object' ) {
			return;
		}
		if ( Array.isArray( v ) ) {
			v.forEach( walk );
			return;
		}
		Object.keys( v ).forEach( ( key ) => {
			if ( key === 'icon' && typeof v[ key ] === 'string' ) {
				walk( v[ key ] );
			} else {
				walk( v[ key ] );
			}
		} );
	};
	walk( value );
	return Array.from( ids );
}
