/**
 * Button Style reference catalog.
 *
 * Localized `buttonLibrary` is only an initial snapshot. Soft REST refresh runs
 * after idle when the tab is visible; hidden tabs pause via
 * @jayoncode/browser-lifecycle. Mutations (download / delete / save) call
 * refreshButtonLibrary() so the picker stays current.
 */
import { createBrowserLifecycle } from '@jayoncode/browser-lifecycle';
import apiFetch from '@wordpress/api-fetch';
import { useEffect, useState } from '@wordpress/element';
import { effective, normalize } from '../cssVars';

const EVENT = 'joc-blockora-morph:button-library-changed';

/** @type {Array|undefined} undefined = use localize; array = live memory */
let memory;
/** @type {Promise<Array>|null} */
let inflight = null;
/** @type {number} */
let lastFetchedAt = 0;
/** Min gap between visibility-driven refreshes (ms). Explicit refresh always runs. */
const FOCUS_MIN_INTERVAL_MS = 60_000;
/** Soft delay before background refresh after mount (ms). */
const MOUNT_IDLE_DELAY_MS = 2_000;

/**
 * @param {import('@jayoncode/browser-lifecycle').BrowserLifecycle|null} lifecycle
 * @return {boolean}
 */
function isPageVisible( lifecycle ) {
	if ( lifecycle ) {
		try {
			return lifecycle.getSnapshot().visibility === 'visible';
		} catch ( err ) {
			// Fall through.
		}
	}
	if ( typeof document === 'undefined' ) {
		return true;
	}
	if ( ! document.visibilityState ) {
		return true;
	}
	return document.visibilityState === 'visible';
}

function configHosts() {
	if ( typeof window === 'undefined' ) {
		return [];
	}
	return [
		window.jocbmAdmin,
		window.jocbmEditor,
		window.jocbmFront,
	].filter( Boolean );
}

function readLocalized() {
	for ( const cfg of configHosts() ) {
		if ( Array.isArray( cfg.buttonLibrary ) ) {
			return cfg.buttonLibrary;
		}
	}
	return [];
}

function bumpLocalized( items ) {
	configHosts().forEach( ( cfg ) => {
		cfg.buttonLibrary = items;
	} );
}

/**
 * Map button cssVars (+ optional defaultAttrs) into the Style reference bag.
 * Mirrors MarkupInterpolator::button_styles_from_vars_attrs.
 *
 * @param {object} cssVars
 * @param {object} [attrs]
 * @return {object}
 */
export function buttonStylesFromVarsAttrs( cssVars, attrs = {} ) {
	const vars = effective( cssVars || {}, 'desktop' );
	return {
		bg: String( vars[ '--bm-label-bg' ] ?? vars[ '--bm-accent' ] ?? '' ),
		color: String( vars[ '--bm-label-color' ] ?? vars[ '--bm-text' ] ?? '' ),
		fontFamily: String(
			vars[ '--bm-label-font-family' ] ?? vars[ '--bm-font-family' ] ?? ''
		),
		fontSize: String( vars[ '--bm-label-font-size' ] ?? '' ),
		bgHover: String( vars[ '--bm-label-bg-hover' ] ?? '' ),
		colorHover: String( vars[ '--bm-label-color-hover' ] ?? '' ),
		size: String( vars[ '--bm-label-size' ] ?? 'medium' ),
		padY: String( vars[ '--bm-label-pad-y' ] ?? '' ),
		padX: String( vars[ '--bm-label-pad-x' ] ?? '' ),
		borderRadius: String( vars[ '--bm-label-border-radius' ] ?? '' ),
		borderWidth: String( vars[ '--bm-label-border-width' ] ?? '' ),
		borderStyle: String( vars[ '--bm-label-border-style' ] ?? '' ),
		borderColor: String( vars[ '--bm-label-border-color' ] ?? '' ),
		borderWidthMode: String(
			vars[ '--bm-label-border-width-mode' ] ?? ''
		),
		borderTopWidth: String( vars[ '--bm-label-border-top-width' ] ?? '' ),
		borderRightWidth: String(
			vars[ '--bm-label-border-right-width' ] ?? ''
		),
		borderBottomWidth: String(
			vars[ '--bm-label-border-bottom-width' ] ?? ''
		),
		borderLeftWidth: String( vars[ '--bm-label-border-left-width' ] ?? '' ),
		borderTopLeftRadius: String(
			vars[ '--bm-label-border-top-left-radius' ] ?? ''
		),
		borderTopRightRadius: String(
			vars[ '--bm-label-border-top-right-radius' ] ?? ''
		),
		borderBottomRightRadius: String(
			vars[ '--bm-label-border-bottom-right-radius' ] ?? ''
		),
		borderBottomLeftRadius: String(
			vars[ '--bm-label-border-bottom-left-radius' ] ?? ''
		),
		shadow: String(
			vars[ '--bm-label-shadow' ] ?? vars[ '--bm-label-shadow-hover' ] ?? ''
		),
		shadowPreset: String( vars[ '--bm-label-shadow-preset' ] ?? 'none' ),
		shadowColor: String( vars[ '--bm-label-shadow-color' ] ?? '' ),
		icon: String( attrs.icon ?? '' ),
		iconPosition: String( attrs.iconPosition ?? 'left' ),
		iconSize: String( vars[ '--bm-label-icon-size' ] ?? '20px' ),
		iconColor: String( vars[ '--bm-label-icon-color' ] ?? '' ),
	};
}

/**
 * Write a Style reference / hero-button style bag back onto Button My Block cssVars.
 *
 * @param {object} styles  Normalized button style bag (bg, color, …)
 * @param {object} [baseCssVars] Existing library cssVars to merge into
 * @return {object}
 */
export function cssVarsFromButtonStyles( styles = {}, baseCssVars = {} ) {
	const s =
		styles && typeof styles === 'object'
			? styles
			: {};
	const desktop = {
		...( normalize( baseCssVars || {} ).desktop || {} ),
	};
	const set = ( key, value ) => {
		if ( value == null ) {
			return;
		}
		const str = String( value );
		if ( str === '' && key !== '--bm-label-font-family' ) {
			return;
		}
		desktop[ key ] = str;
	};
	set( '--bm-label-bg', s.bg );
	set( '--bm-label-color', s.color );
	set( '--bm-label-font-family', s.fontFamily );
	set( '--bm-label-font-size', s.fontSize );
	set( '--bm-label-bg-hover', s.bgHover );
	set( '--bm-label-color-hover', s.colorHover );
	set( '--bm-label-size', s.size || 'medium' );
	set( '--bm-label-pad-y', s.padY );
	set( '--bm-label-pad-x', s.padX );
	set( '--bm-label-border-radius', s.borderRadius );
	set( '--bm-label-border-width', s.borderWidth );
	set( '--bm-label-border-style', s.borderStyle );
	set( '--bm-label-border-color', s.borderColor );
	set( '--bm-label-border-width-mode', s.borderWidthMode );
	set( '--bm-label-border-top-width', s.borderTopWidth );
	set( '--bm-label-border-right-width', s.borderRightWidth );
	set( '--bm-label-border-bottom-width', s.borderBottomWidth );
	set( '--bm-label-border-left-width', s.borderLeftWidth );
	set( '--bm-label-border-top-left-radius', s.borderTopLeftRadius );
	set( '--bm-label-border-top-right-radius', s.borderTopRightRadius );
	set( '--bm-label-border-bottom-right-radius', s.borderBottomRightRadius );
	set( '--bm-label-border-bottom-left-radius', s.borderBottomLeftRadius );
	set( '--bm-label-shadow', s.shadow );
	set( '--bm-label-shadow-preset', s.shadowPreset );
	set( '--bm-label-shadow-color', s.shadowColor );
	set( '--bm-label-icon-size', s.iconSize );
	set( '--bm-label-icon-color', s.iconColor );
	return normalize( {
		...normalize( baseCssVars || {} ),
		desktop,
	} );
}

/**
 * Locate localized user-block meta (defaultAttrs / slug) for a library id.
 *
 * @param {number} userBlockId
 * @return {object|null}
 */
export function findLocalizedBlockMeta( userBlockId ) {
	const id = Number( userBlockId );
	if ( ! id ) {
		return null;
	}
	for ( const cfg of configHosts() ) {
		const lists = [ cfg.blocks ];
		if ( cfg.blocksByName && typeof cfg.blocksByName === 'object' ) {
			lists.push( Object.values( cfg.blocksByName ) );
		}
		for ( const list of lists ) {
			for ( const row of Array.isArray( list ) ? list : [] ) {
				if ( Number( row?.id ) === id ) {
					return row;
				}
			}
		}
	}
	return null;
}

/**
 * Keep Style reference catalog styles in sync when a Button My Block Design changes.
 *
 * @param {number} userBlockId
 * @param {object} cssVars
 * @return {boolean} True when a catalog entry was patched.
 */
export function syncButtonLibraryStylesFromCssVars( userBlockId, cssVars ) {
	const id = Number( userBlockId );
	if ( ! id ) {
		return false;
	}
	const lib = getButtonLibrary();
	const idx = lib.findIndex( ( b ) => Number( b?.id ) === id );
	if ( idx < 0 ) {
		return false;
	}
	const existing = lib[ idx ] || {};
	const meta = findLocalizedBlockMeta( id );
	const attrs = {
		...( typeof meta?.defaultAttrs === 'object' && meta.defaultAttrs
			? meta.defaultAttrs
			: {} ),
		// Prefer catalog icon fields already derived for Style reference.
		icon:
			meta?.defaultAttrs?.icon ??
			existing.styles?.icon ??
			'',
		iconPosition:
			meta?.defaultAttrs?.iconPosition ??
			existing.styles?.iconPosition ??
			'left',
	};
	const styles = buttonStylesFromVarsAttrs( normalize( cssVars || {} ), attrs );
	// Preserve icon when Design-only cssVars updates omit defaultAttrs.
	if ( ! String( styles.icon || '' ).trim() && existing.styles?.icon ) {
		styles.icon = existing.styles.icon;
		styles.iconPosition =
			existing.styles.iconPosition || styles.iconPosition;
	}
	const next = lib.slice();
	next[ idx ] = { ...existing, styles };
	setButtonLibrary( next );
	return true;
}

/**
 * Patch Style reference catalog entry directly from a hero button style bag.
 *
 * @param {number} userBlockId
 * @param {object} styles
 * @return {boolean}
 */
export function patchButtonLibraryEntryStyles( userBlockId, styles ) {
	const id = Number( userBlockId );
	if ( ! id ) {
		return false;
	}
	const lib = getButtonLibrary();
	const idx = lib.findIndex( ( b ) => Number( b?.id ) === id );
	if ( idx < 0 ) {
		return false;
	}
	const existing = lib[ idx ] || {};
	const bag = {
		...( existing.styles && typeof existing.styles === 'object'
			? existing.styles
			: {} ),
		...( styles && typeof styles === 'object' ? styles : {} ),
	};
	const next = lib.slice();
	next[ idx ] = {
		...existing,
		styles: {
			bg: String( bag.bg ?? '' ),
			color: String( bag.color ?? '' ),
			fontFamily: String( bag.fontFamily ?? '' ),
			fontSize: String( bag.fontSize ?? '' ),
			bgHover: String( bag.bgHover ?? '' ),
			colorHover: String( bag.colorHover ?? '' ),
			size: String( bag.size ?? 'medium' ),
			padY: String( bag.padY ?? '' ),
			padX: String( bag.padX ?? '' ),
			borderRadius: String( bag.borderRadius ?? '' ),
			shadow: String( bag.shadow ?? '' ),
			shadowPreset: String( bag.shadowPreset ?? 'none' ),
			shadowColor: String( bag.shadowColor ?? '' ),
			icon: String( bag.icon ?? '' ),
			iconPosition:
				String( bag.iconPosition || 'left' ).toLowerCase() === 'right'
					? 'right'
					: 'left',
			iconSize: String( bag.iconSize ?? '20px' ),
			iconColor: String( bag.iconColor ?? '' ),
		},
	};
	setButtonLibrary( next );
	return true;
}

/**
 * Synchronous snapshot (memory after refresh, else page-load localize).
 *
 * @return {Array}
 */
export function getButtonLibrary() {
	if ( memory !== undefined ) {
		return memory;
	}
	return readLocalized();
}

/**
 * Publish a fresh library list to memory, localize bags, and subscribers.
 *
 * @param {Array} items
 */
export function setButtonLibrary( items ) {
	memory = Array.isArray( items ) ? items : [];
	bumpLocalized( memory );
	if ( typeof window !== 'undefined' ) {
		window.dispatchEvent(
			new CustomEvent( EVENT, { detail: memory } )
		);
	}
}

/**
 * @param {(items: Array) => void} callback
 * @return {() => void} Unsubscribe
 */
export function subscribeButtonLibrary( callback ) {
	if ( typeof window === 'undefined' || typeof callback !== 'function' ) {
		return () => {};
	}
	const handler = ( event ) => {
		callback( Array.isArray( event.detail ) ? event.detail : getButtonLibrary() );
	};
	window.addEventListener( EVENT, handler );
	return () => window.removeEventListener( EVENT, handler );
}

/**
 * Fetch live button library (no browser cache). Safe to call after delete/import.
 *
 * @param {{ force?: boolean }} [options] Reserved for callers; always bypasses throttle.
 * @return {Promise<Array>}
 */
export async function refreshButtonLibrary( options = {} ) {
	void options;
	if ( inflight ) {
		return inflight;
	}

	inflight = ( async () => {
		try {
			const stamp = Date.now();
			const data = await apiFetch( {
				path: `/joc-blockora-morph/v1/button-library?_=${ stamp }`,
			} );
			const items = Array.isArray( data?.items )
				? data.items
				: Array.isArray( data )
				? data
				: [];
			setButtonLibrary( items );
			lastFetchedAt = stamp;
			// Other editor tabs (Gutenberg) re-fetch Style reference catalog.
			if ( typeof window !== 'undefined' && window.localStorage ) {
				try {
					window.localStorage.setItem(
						'joc-bm-button-library-rev',
						String( stamp )
					);
				} catch ( err ) {
					// ignore
				}
			}
			return items;
		} catch ( err ) {
			// Keep last known list on network failure.
			return getButtonLibrary();
		} finally {
			inflight = null;
		}
	} )();

	return inflight;
}

/**
 * React helper — paints localized snapshot immediately, then soft-refreshes
 * when the tab is visible (idle/delayed). Pauses on `page:hidden` and resumes
 * on `page:visible` via @jayoncode/browser-lifecycle. Cross-tab updates use
 * `storage`; import/delete/save call refreshButtonLibrary().
 *
 * @param {{ refreshOnMount?: boolean, refreshOnFocus?: boolean }} [options]
 * @return {Array}
 */
export function useButtonLibrary( {
	refreshOnMount = true,
	refreshOnFocus = true,
} = {} ) {
	const [ items, setItems ] = useState( () => getButtonLibrary() );

	useEffect( () => {
		const unsub = subscribeButtonLibrary( setItems );
		let delayTimer = 0;
		let idleId = 0;
		let pendingWhileHidden = false;
		/** @type {import('@jayoncode/browser-lifecycle').BrowserLifecycle|null} */
		let lifecycle = null;
		/** @type {Array<() => void>} */
		const offs = [];

		const clearScheduled = () => {
			if ( delayTimer ) {
				window.clearTimeout( delayTimer );
				delayTimer = 0;
			}
			if (
				idleId &&
				typeof window.cancelIdleCallback === 'function'
			) {
				window.cancelIdleCallback( idleId );
				idleId = 0;
			}
		};

		const runRefresh = () => {
			if ( ! isPageVisible( lifecycle ) ) {
				pendingWhileHidden = true;
				return;
			}
			pendingWhileHidden = false;
			refreshButtonLibrary().then( setItems );
		};

		const scheduleSoftRefresh = () => {
			clearScheduled();
			if ( ! isPageVisible( lifecycle ) ) {
				pendingWhileHidden = true;
				return;
			}
			const kick = () => {
				delayTimer = 0;
				idleId = 0;
				runRefresh();
			};
			if ( typeof window.requestIdleCallback === 'function' ) {
				idleId = window.requestIdleCallback( kick, {
					timeout: MOUNT_IDLE_DELAY_MS,
				} );
			} else {
				delayTimer = window.setTimeout( kick, MOUNT_IDLE_DELAY_MS );
			}
		};

		const onPageHidden = () => {
			if ( delayTimer || idleId ) {
				pendingWhileHidden = true;
			}
			clearScheduled();
		};

		const onPageVisible = () => {
			if ( pendingWhileHidden ) {
				pendingWhileHidden = false;
				runRefresh();
				return;
			}

			if ( ! refreshOnFocus ) {
				return;
			}
			if (
				lastFetchedAt &&
				Date.now() - lastFetchedAt < FOCUS_MIN_INTERVAL_MS
			) {
				return;
			}
			runRefresh();
		};

		if ( typeof window !== 'undefined' ) {
			try {
				lifecycle = createBrowserLifecycle( { autoStart: true } );
				offs.push( lifecycle.on( 'page:hidden', onPageHidden ) );
				offs.push( lifecycle.on( 'page:visible', onPageVisible ) );
			} catch ( err ) {
				lifecycle = null;
			}
		}

		if ( refreshOnMount ) {
			scheduleSoftRefresh();
		}

		// Cross-tab: another admin/editor wrote a new revision.
		const onStorage = ( event ) => {
			if ( event?.key !== 'joc-bm-button-library-rev' ) {
				return;
			}
			clearScheduled();
			runRefresh();
		};

		if ( typeof window !== 'undefined' ) {
			window.addEventListener( 'storage', onStorage );
		}

		return () => {
			unsub();
			clearScheduled();
			offs.forEach( ( off ) => {
				try {
					off();
				} catch ( err ) {
					// ignore
				}
			} );
			if ( lifecycle ) {
				try {
					lifecycle.dispose();
				} catch ( err ) {
					// ignore
				}
			}
			if ( typeof window !== 'undefined' ) {
				window.removeEventListener( 'storage', onStorage );
			}
		};
	}, [ refreshOnMount, refreshOnFocus ] );

	return items;
}

export function getLibraryStylesMap() {
	const map = {};
	getButtonLibrary().forEach( ( b ) => {
		const id = Number( b?.id );
		if ( Number.isFinite( id ) && id !== 0 && b.styles ) {
			map[ id ] = b.styles;
		}
	} );
	return map;
}
