/**
 * Persist My Block (library) cssVars from Gutenberg / frontend inspector.
 * Edits are queued and flushed on post/page save (or FE editor Save).
 */
import apiFetch from '@wordpress/api-fetch';
import { normalize } from '../cssVars';
import {
	cssVarsFromButtonStyles,
	findLocalizedBlockMeta,
	getButtonLibrary,
	patchButtonLibraryEntryStyles,
	refreshButtonLibrary,
	syncButtonLibraryStylesFromCssVars,
} from './buttonLibrary';

const saveTimers = new Map();
/** @type {Map<number, { cssVars: object, defaultAttrs?: object }>} */
const pendingById = new Map();

/**
 * @param {unknown} entry
 * @return {{ cssVars: object, defaultAttrs?: object }}
 */
function asPending( entry ) {
	if ( entry && typeof entry === 'object' && ! Array.isArray( entry ) ) {
		const hasCss = Object.prototype.hasOwnProperty.call( entry, 'cssVars' );
		const hasAttrs = Object.prototype.hasOwnProperty.call(
			entry,
			'defaultAttrs'
		);
		if ( hasCss || hasAttrs ) {
			return {
				...( hasCss ? { cssVars: normalize( entry.cssVars || {} ) } : {} ),
				...( hasAttrs ? { defaultAttrs: entry.defaultAttrs } : {} ),
			};
		}
	}
	return { cssVars: normalize( entry || {} ) };
}

/**
 * @return {{ restUrl?: string, nonce?: string, blocks?: Array }}
 */
function editorConfig() {
	if ( typeof window === 'undefined' ) {
		return {};
	}
	return (
		window.jocbmEditor ||
		window.jocbmFront ||
		window.jocbmAdmin ||
		{}
	);
}

/**
 * Keep localized block meta in sync so other surfaces see fresh library styles
 * and (when provided) My Block content defaults for linked placements.
 *
 * @param {number} userBlockId
 * @param {object} cssVars
 * @param {object|null} [defaultAttrs] When set, live-wire My Block content defaults.
 */
export function syncLocalizedLibraryCssVars(
	userBlockId,
	cssVars,
	defaultAttrs = null
) {
	const id = Number( userBlockId );
	if ( ! id ) {
		return;
	}
	const next = normalize( cssVars || {} );
	const cfg = editorConfig();
	const lists = [ cfg.blocks ];
	if ( cfg.blocksByName && typeof cfg.blocksByName === 'object' ) {
		lists.push( Object.values( cfg.blocksByName ) );
	}
	lists.forEach( ( list ) => {
		( Array.isArray( list ) ? list : [] ).forEach( ( row ) => {
			if ( Number( row?.id ) === id ) {
				row.cssVars = next;
				if ( defaultAttrs != null && typeof defaultAttrs === 'object' ) {
					row.defaultAttrs = defaultAttrs;
				}
			}
		} );
	} );
	// Heroes using Style reference read from buttonLibrary — keep it live.
	syncButtonLibraryStylesFromCssVars( id, next );
	if ( typeof window !== 'undefined' ) {
		window.dispatchEvent(
			new CustomEvent( 'joc-blockora-morph-library-css', {
				detail: {
					id,
					cssVars: next,
					...( defaultAttrs != null
						? { defaultAttrs }
						: {} ),
				},
			} )
		);
	}
}

/**
 * Queue library cssVars (and optional defaultAttrs) to persist on the next save.
 *
 * @param {number} userBlockId
 * @param {object} cssVars
 * @param {{ defaultAttrs?: object }} [options]
 */
export function queueLibraryCssVars( userBlockId, cssVars, options = {} ) {
	const id = Number( userBlockId );
	if ( ! id ) {
		return;
	}
	const next = normalize( cssVars || {} );
	const prev = pendingById.has( id ) ? asPending( pendingById.get( id ) ) : {};
	const defaultAttrs =
		options.defaultAttrs != null
			? options.defaultAttrs
			: prev.defaultAttrs;
	pendingById.set( id, {
		cssVars: next,
		...( defaultAttrs != null ? { defaultAttrs } : {} ),
	} );
	syncLocalizedLibraryCssVars( id, next, defaultAttrs ?? null );
	if ( typeof window !== 'undefined' ) {
		window.dispatchEvent(
			new CustomEvent( 'joc-blockora-morph-library-pending', {
				detail: { id, cssVars: next, defaultAttrs },
			} )
		);
	}
}

/**
 * Queue a Style-reference Button My Block update from a hero button style bag.
 * Persists on a short debounce (does not wait for post Update).
 *
 * @param {number} userBlockId
 * @param {object} styles
 */
export function queueReferencedButtonLibraryStyles( userBlockId, styles ) {
	const id = Number( userBlockId );
	if ( ! id ) {
		return;
	}
	const meta = findLocalizedBlockMeta( id );
	const catalog = getButtonLibrary().find( ( b ) => Number( b?.id ) === id );
	// Prefer live DB/meta cssVars; fall back to reconstructing from catalog styles.
	const baseVars =
		meta?.cssVars ||
		( catalog?.styles
			? cssVarsFromButtonStyles( catalog.styles, {} )
			: {} );
	const cssVars = cssVarsFromButtonStyles( styles || {}, baseVars );
	// Debounced PUT — legacy helper (Style reference Design is no longer
	// written from Gutenberg placements; kept for optional admin tooling).
	saveLibraryCssVars( id, cssVars, { delay: 400 } );
	patchButtonLibraryEntryStyles( id, styles || {} );
}

/**
 * @return {boolean}
 */
export function hasPendingLibraryCssVars() {
	return pendingById.size > 0;
}

/**
 * @param {number} id
 * @param {{ cssVars: object, defaultAttrs?: object }|object} payload
 * @param {object} handlers
 * @return {Promise<object>}
 */
async function putLibraryUpdate( id, payload, handlers = {} ) {
	const { onStart, onDone, onError } = handlers;
	const pending = asPending( payload );
	onStart?.( id );
	try {
		const data = {};
		if ( Object.prototype.hasOwnProperty.call( pending, 'cssVars' ) ) {
			data.cssVars = pending.cssVars;
		}
		if ( Object.prototype.hasOwnProperty.call( pending, 'defaultAttrs' ) ) {
			data.defaultAttrs = pending.defaultAttrs;
		}
		if ( ! Object.keys( data ).length ) {
			onDone?.( id, pending.cssVars || {} );
			return pending.cssVars || {};
		}
		const saved = await apiFetch( {
			path: `/joc-blockora-morph/v1/user-blocks/${ id }`,
			method: 'PUT',
			data,
		} );
		const next = normalize(
			saved?.cssVars || pending.cssVars || {}
		);
		const nextAttrs =
			saved?.defaultAttrs != null
				? saved.defaultAttrs
				: pending.defaultAttrs;
		syncLocalizedLibraryCssVars( id, next, nextAttrs ?? null );
		onDone?.( id, next );
		return next;
	} catch ( error ) {
		onError?.( error );
		throw error;
	}
}

/**
 * Persist all queued library cssVars (e.g. on Update / Publish / FE Save).
 *
 * @param {object} [handlers]
 * @return {Promise<object[]>}
 */
export async function flushPendingLibraryCssVars( handlers = {} ) {
	saveTimers.forEach( ( timer ) => window.clearTimeout( timer ) );
	saveTimers.clear();

	const entries = [ ...pendingById.entries() ];
	if ( ! entries.length ) {
		return [];
	}
	pendingById.clear();

	const results = [];
	const errors = [];
	for ( const [ id, payload ] of entries ) {
		try {
			results.push( await putLibraryUpdate( id, payload, handlers ) );
		} catch ( error ) {
			// Re-queue failed payloads so a later save can retry.
			pendingById.set( id, asPending( payload ) );
			errors.push( error );
		}
	}
	// Authoritative Style reference bags after persist (icons / attrs included).
	if ( results.length ) {
		refreshButtonLibrary().catch( () => {} );
	}
	if ( typeof window !== 'undefined' ) {
		window.dispatchEvent(
			new CustomEvent( 'joc-blockora-morph-library-flushed', {
				detail: {
					ok: errors.length === 0,
					count: results.length,
					errors,
				},
			} )
		);
	}
	if ( errors.length ) {
		throw errors[ 0 ];
	}
	return results;
}

/**
 * Debounced PUT of library cssVars (legacy / optional immediate paths).
 *
 * @param {number} userBlockId
 * @param {object} cssVars
 * @param {object} [options]
 * @param {Function} [options.onStart]
 * @param {Function} [options.onDone]
 * @param {Function} [options.onError]
 * @param {number} [options.delay]
 * @param {boolean} [options.immediate]
 * @param {object} [options.defaultAttrs]
 * @return {Promise<object>|void}
 */
export function saveLibraryCssVars(
	userBlockId,
	cssVars,
	{
		onStart,
		onDone,
		onError,
		delay = 450,
		immediate = false,
		defaultAttrs,
	} = {}
) {
	const id = Number( userBlockId );
	if ( ! id ) {
		return immediate
			? Promise.reject( new Error( 'Missing user block id' ) )
			: undefined;
	}
	queueLibraryCssVars( id, cssVars, { defaultAttrs } );

	if ( saveTimers.has( id ) ) {
		window.clearTimeout( saveTimers.get( id ) );
		saveTimers.delete( id );
	}

	const run = () => {
		const payload = pendingById.get( id ) || { cssVars: normalize( cssVars || {} ) };
		pendingById.delete( id );
		return putLibraryUpdate( id, payload, { onStart, onDone, onError } );
	};

	if ( immediate ) {
		return run();
	}

	const timer = window.setTimeout( () => {
		saveTimers.delete( id );
		run();
	}, delay );
	saveTimers.set( id, timer );
}
