import {
	useCallback,
	useEffect,
	useMemo,
	useRef,
	useState,
} from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { __, sprintf, _n } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import {
	Button,
	Card,
	CardBody,
	Notice,
	DropdownMenu,
	MenuGroup,
	MenuItem,
	TextControl,
	SelectControl,
	ComboboxControl,
	FormTokenField,
} from '@wordpress/components';
import { STORE_KEY } from '../store';
import queryContext from '../queryContext';
import {
	fetchListCached,
	invalidateListCache,
	isListCacheFresh,
} from '../listCache';
import {
	ArrowDown,
	ArrowUp,
	Edit3,
	Eye,
	Filter,
	MoreHorizontal,
	Plus,
	RotateCw,
	Search,
	Trash2,
	X,
} from 'lucide-react';
import RecordFormModal from './RecordFormModal';
import BulkEditModal from './BulkEditModal';
import ConfirmDialog from './ConfirmDialog';
import ColumnManager from './ColumnManager';

/**
 * Turn a column key into a human label (fallback for keys without a label).
 */
const humanizeKey = ( key ) =>
	String( key )
		.replace( /[_-]+/g, ' ' )
		.replace( /\b\w/g, ( c ) => c.toUpperCase() );

/**
 * Markup a cell is allowed to contain, as tag → permitted attributes.
 *
 * Cell HTML is a closed vocabulary: a status/language badge and a thumbnail.
 */
const CELL_HTML_ALLOWED = {
	SPAN: [ 'class' ],
	IMG: [ 'src', 'alt', 'class' ],
	STRONG: [],
	EM: [],
	BR: [],
};

/**
 * Reduce a transformer's HTML to that vocabulary.
 *
 * The PHP transformers escape their values, so this is a second line rather
 * than the only one — but it means a transformer that forgets cannot put a
 * script tag or an event handler into the grid. Unknown elements are unwrapped
 * rather than dropped, so their text still shows.
 */
const sanitizeCellHtml = ( html ) => {
	if ( typeof DOMParser === 'undefined' ) {
		return '';
	}
	const doc = new DOMParser().parseFromString(
		`<body>${ html }</body>`,
		'text/html'
	);
	doc.body.querySelectorAll( '*' ).forEach( ( el ) => {
		const allowed = CELL_HTML_ALLOWED[ el.tagName ];
		if ( ! allowed ) {
			el.replaceWith( ...el.childNodes );
			return;
		}
		[ ...el.attributes ].forEach( ( attr ) => {
			const name = attr.name.toLowerCase();
			const unsafeUrl =
				( name === 'src' || name === 'href' ) &&
				/^\s*(javascript|data|vbscript):/i.test( attr.value );
			if ( ! allowed.includes( name ) || unsafeUrl ) {
				el.removeAttribute( attr.name );
			}
		} );
	} );
	return doc.body.innerHTML;
};

/**
 * Render a table cell value. Supports column transformers that return
 * `{ value, isHtml }` (e.g. an image preview) as well as plain scalars.
 */
const renderCellValue = ( value ) => {
	if ( value === undefined || value === null || value === '' ) {
		return '-';
	}

	// Multi-value fields (checkboxes / multiselect / pivots) arrive as arrays —
	// render scalars comma-joined, and objects by their name/label/title.
	if ( Array.isArray( value ) ) {
		const parts = value
			.map( ( v ) =>
				v && typeof v === 'object' ? v.name ?? v.label ?? v.title ?? '' : v
			)
			.filter( ( v ) => v !== '' && v !== null && v !== undefined );
		return parts.length ? parts.join( ', ' ) : '-';
	}

	if ( typeof value === 'object' ) {
		if ( value.isHtml && typeof value.value === 'string' ) {
			return (
				<span
					// eslint-disable-next-line react/no-danger
					dangerouslySetInnerHTML={ {
						__html: sanitizeCellHtml( value.value ),
					} }
				/>
			);
		}

		if ( 'value' in value ) {
			return String( value.value );
		}

		return '-';
	}

	// Render image URLs / data-URIs (e.g. marker & category icons) as thumbnails.
	if (
		typeof value === 'string' &&
		( value.startsWith( 'data:image/' ) ||
			/\.(svg|png|jpe?g|gif|webp)(\?.*)?$/i.test( value ) )
	) {
		return (
			<img src={ value } alt="" className="cliqthemes-ui-cell-thumb" />
		);
	}

	return String( value );
};

const sanitiseFilterValue = ( value ) => {
	if ( Array.isArray( value ) ) {
		const cleaned = value
			.map( ( item ) => {
				if ( typeof item === 'string' ) {
					return item.trim();
				}

				if ( item === undefined || item === null ) {
					return '';
				}

				return String( item );
			} )
			.filter( ( item ) => item !== '' );

		return cleaned.length > 0 ? cleaned : undefined;
	}

	if ( value === undefined || value === null ) {
		return undefined;
	}

	if ( typeof value === 'string' ) {
		const trimmed = value.trim();

		return trimmed === '' ? undefined : trimmed;
	}

	return value;
};

const normaliseFilters = ( filters ) => {
	const normalised = {};

	Object.entries( filters ?? {} ).forEach( ( [ key, rawValue ] ) => {
		const value = sanitiseFilterValue( rawValue );

		if ( value !== undefined ) {
			normalised[ key ] = value;
		}
	} );

	return normalised;
};

const buildQueryString = ( query ) => {
	const params = new URLSearchParams();
	params.set( 'per_page', String( query.perPage ?? 20 ) );
	params.set( 'page', String( query.page ?? 1 ) );

	if ( query.orderBy ) {
		params.set( 'order_by', query.orderBy );
		params.set( 'order', query.order ?? 'asc' );
	}

	Object.entries( query.filters ?? {} ).forEach( ( [ key, value ] ) => {
		if ( Array.isArray( value ) ) {
			value.forEach( ( item ) => {
				params.append( `filters[${ key }][]`, String( item ) );
			} );
			return;
		}

		if ( value && typeof value === 'object' ) {
			Object.entries( value ).forEach( ( [ childKey, childValue ] ) => {
				params.append(
					`filters[${ key }][${ childKey }]`,
					String( childValue )
				);
			} );
			return;
		}

		params.append( `filters[${ key }]`, String( value ) );
	} );

	// Generic query context (e.g. a host's multilingual `lang`), forwarded to
	// the provider as context[key]=value. The framework assigns no meaning.
	Object.entries( queryContext.get() ).forEach( ( [ key, value ] ) => {
		params.append( `context[${ key }]`, String( value ) );
	} );

	return params.toString();
};

// The active query context (e.g. multilingual `lang`) as a `?context[k]=v` string,
// so single-record GET / create / update carry the SAME scoping the list uses —
// letting a provider e.g. load/save the active language's translation.
const contextQueryString = () => {
	const params = new URLSearchParams();
	Object.entries( queryContext.get() ).forEach( ( [ key, value ] ) => {
		params.append( `context[${ key }]`, String( value ) );
	} );
	const qs = params.toString();
	return qs ? `?${ qs }` : '';
};

const RecordsView = ( { providerId } ) => {
	const { provider, records, status, error, rest, pagination } = useSelect(
		( select ) => {
			const store = select( STORE_KEY );

			return {
				provider: store.getProvider( providerId ),
				records: store.getRecords( providerId ),
				status: store.getStatus( providerId ),
				error: store.getError( providerId ),
				rest: store.getRestConfig(),
				pagination: store.getPagination( providerId ),
			};
		},
		[ providerId ]
	);

	const { setRecords, setStatus, setError, updateProvider, setPagination } =
		useDispatch( STORE_KEY );
	const noticesStore = useDispatch( 'core/notices' );

	const notify = useCallback(
		( noticeStatus, message, options = {} ) => {
			const dispatcher = noticesStore?.createNotice
				? noticesStore
				: window.wp?.data?.dispatch?.( 'core/notices' );

			if ( dispatcher?.createNotice ) {
				dispatcher.createNotice( noticeStatus, message, {
					type: 'snackbar',
					...options,
				} );
			}
		},
		[ noticesStore ]
	);

	const listRecords = useMemo(
		() => ( Array.isArray( records ) ? records : [] ),
		[ records ]
	);

	const [ selectedIds, setSelectedIds ] = useState( [] );
	const [ isFormOpen, setFormOpen ] = useState( false );
	const [ formMode, setFormMode ] = useState( 'create' );
	const [ currentRecord, setCurrentRecord ] = useState( null );
	const [ isConfirmOpen, setConfirmOpen ] = useState( false );
	const [ pendingIds, setPendingIds ] = useState( [] );
	const [ isSaving, setIsSaving ] = useState( false );
	const [ isDeleting, setIsDeleting ] = useState( false );
	const [ isBulkOpen, setBulkOpen ] = useState( false );
	const [ isBulkSaving, setBulkSaving ] = useState( false );
	const queryRef = useRef( {
		page: 1,
		perPage: pagination?.per_page ?? 20,
		orderBy: '',
		order: 'asc',
		filters: {},
	} );
	const [ query, setQuery ] = useState( () => queryRef.current );
	const [ activeFilters, setActiveFilters ] = useState(
		() => queryRef.current.filters ?? {}
	);
	const previousProviderIdRef = useRef( null );
	const [ isFiltersExpanded, setFiltersExpanded ] = useState( false );

	const formSchema = useMemo( () => provider?.form ?? {}, [ provider ] );
	// A read-only provider (e.g. Leads) can be viewed but not created/edited:
	// the Add button is hidden and the record modal opens as a disabled view.
	const readOnly = !! provider?.readOnly;
	const formFields = useMemo( () => {
		if ( Array.isArray( formSchema?.fields ) && formSchema.fields.length ) {
			return formSchema.fields;
		}

		if ( ! Array.isArray( formSchema?.sections ) ) {
			return [];
		}

		const collected = [];
		formSchema.sections.forEach( ( section ) => {
			( section.groups ?? [] ).forEach( ( group ) => {
				( group.fields ?? [] ).forEach( ( field ) => {
					collected.push( field );
				} );
			} );
		} );

		return collected;
	}, [ formSchema ] );
	const primaryKey = formSchema.primaryKey ?? provider?.primaryKey ?? 'id';
	const recordTypeLabel = formSchema.label ?? provider?.label ?? __( 'Record', 'cliq-store-locator' );

	const submitLabels = {
		create:
			formSchema.submitLabels?.create ??
			sprintf(
				// translators: %s is a record type, e.g. "Category".
				__( 'Create %s', 'cliq-store-locator' ),
				recordTypeLabel
			),
		update:
			formSchema.submitLabels?.update ??
			sprintf(
				// translators: %s is a record type, e.g. "Marker".
				__( 'Update %s', 'cliq-store-locator' ),
				recordTypeLabel
			),
	};
	const editorTitle = readOnly
		? sprintf(
				// translators: %s is the record type, e.g. "Lead".
				__( 'View %s', 'cliq-store-locator' ),
				recordTypeLabel
		  ).trim()
		: formMode === 'create'
			? submitLabels.create
			: submitLabels.update;

	const updateQueryState = useCallback( ( nextQuery ) => {
		queryRef.current = nextQuery;
		setQuery( nextQuery );
	}, [] );

	const fetchRecords = useCallback(
		async ( overrides = {} ) => {
			if ( ! provider || ! rest?.namespace ) {
				return;
			}

			// `force` is a fetch directive, not part of the query — pull it out so
			// it never leaks into queryRef (which would make every later fetch
			// forced) or the query string.
			const { force = false, ...queryOverrides } = overrides;

			const baseQuery = queryRef.current;
			const nextQuery = {
				...baseQuery,
				...queryOverrides,
				filters:
					queryOverrides.filters !== undefined
						? queryOverrides.filters
						: baseQuery.filters,
			};

			nextQuery.filters = normaliseFilters( nextQuery.filters );
			nextQuery.page = Math.max(
				1,
				Number.parseInt( nextQuery.page ?? 1, 10 ) || 1
			);
			nextQuery.perPage = Math.max(
				1,
				Number.parseInt( nextQuery.perPage ?? 20, 10 ) || 20
			);

			const basePath = `${ rest.namespace }/${ provider.route }`;
			const queryString = buildQueryString( nextQuery );
			const path = queryString
				? `${ basePath }?${ queryString }`
				: basePath;

			// Only show the loading skeleton when we'll actually hit the network;
			// a cache hit (tab revisit) resolves synchronously, so flashing the
			// skeleton would just cause a visible flicker.
			const willHitNetwork = force || ! isListCacheFresh( path );
			if ( willHitNetwork ) {
				setStatus( providerId, 'loading' );
			}
			setError( providerId, null );

			try {
				const response = await fetchListCached(
					path,
					() => apiFetch( { path } ),
					{ force }
				);
				setRecords( providerId, response?.records ?? [] );
				updateProvider( providerId, {
					columns: response?.columns ?? provider.columns,
					filters: response?.filters ?? provider.filters,
				} );

				if ( response?.pagination ) {
					setPagination( providerId, response.pagination );
					nextQuery.page = response.pagination.page ?? nextQuery.page;
					nextQuery.perPage =
						response.pagination.per_page ?? nextQuery.perPage;
				} else {
					setPagination( providerId, {
						total: ( response?.records ?? [] ).length,
						per_page: nextQuery.perPage,
						page: nextQuery.page,
						total_pages: 1,
					} );
				}

				updateQueryState( nextQuery );
				setActiveFilters( nextQuery.filters );
				setStatus( providerId, 'ready' );
			} catch ( fetchError ) {
				const message =
					fetchError?.message ??
					__( 'Unable to fetch records.', 'cliq-store-locator' );
				setError( providerId, message );
				setStatus( providerId, 'error' );
			}
		},
		[
			provider,
			rest,
			providerId,
			setStatus,
			setError,
			setRecords,
			updateProvider,
			setPagination,
			updateQueryState,
		]
	);

	useEffect( () => {
		if ( ! provider ) {
			return;
		}

		if ( previousProviderIdRef.current === providerId ) {
			return;
		}

		previousProviderIdRef.current = providerId;

		const defaults = {};
		Object.entries( provider.filters ?? {} ).forEach(
			( [ key, definition ] ) => {
				if (
					definition &&
					Object.prototype.hasOwnProperty.call(
						definition,
						'default'
					)
				) {
					const defaultValue = sanitiseFilterValue(
						definition.default
					);
					if ( defaultValue !== undefined ) {
						defaults[ key ] = defaultValue;
					}
				}
			}
		);

		const initialOrder =
			( provider?.defaultSort?.order ?? 'asc' ).toLowerCase() === 'desc'
				? 'desc'
				: 'asc';
		const baseQuery = {
			page: 1,
			perPage: provider?.perPage ?? queryRef.current.perPage ?? 20,
			orderBy: provider?.defaultSort?.orderBy ?? '',
			order: initialOrder,
			filters: defaults,
		};

		updateQueryState( baseQuery );
		setActiveFilters( defaults );
		// Advanced filters stay collapsed by default (behind the Filter button);
		// only auto-open when a filter already has a default value applied.
		setFiltersExpanded( Object.keys( defaults ).length > 0 );
		setSelectedIds( [] );

		// Reset any open form/confirm so switching providers never shows a
		// stale record (e.g. a store form rendered for the brand provider).
		setFormOpen( false );
		setCurrentRecord( null );
		setFormMode( 'create' );
		setConfirmOpen( false );
		setPendingIds( [] );

		setPagination( providerId, {
			total: 0,
			per_page: baseQuery.perPage,
			page: 1,
			total_pages: 1,
		} );
		setStatus( providerId, 'idle' );
	}, [ providerId, provider, updateQueryState, setStatus, setPagination ] );

	useEffect( () => {
		if ( ! provider ) {
			return;
		}

		if ( status === 'idle' ) {
			fetchRecords();
		}
	}, [ provider, status, fetchRecords ] );

	// Refetch (page 1) whenever the global query context changes — e.g. the host
	// switches the active language via a toolbar control.
	useEffect( () => {
		return queryContext.subscribe( () => {
			if ( provider ) {
				fetchRecords( { page: 1 } );
			}
		} );
	}, [ provider, fetchRecords ] );

	const filterEntries = useMemo(
		() => Object.entries( provider?.filters ?? {} ),
		[ provider?.filters ]
	);

	// The first filter defined with operator 'search' becomes the always-visible
	// keyword box next to "Add New"; the rest live behind the Filters button.
	const searchKey = useMemo( () => {
		const found = filterEntries.find(
			( [ , def ] ) => ( def?.operator ?? '' ) === 'search'
		);
		return found ? found[ 0 ] : null;
	}, [ filterEntries ] );

	const advancedFilterEntries = useMemo(
		() => filterEntries.filter( ( [ key ] ) => key !== searchKey ),
		[ filterEntries, searchKey ]
	);
	const hasFilters = advancedFilterEntries.length > 0;

	const searchTimer = useRef( null );
	const runSearch = useCallback(
		( rawValue ) => {
			if ( ! searchKey ) {
				return;
			}
			setActiveFilters( ( prev ) => {
				const next = { ...prev };
				if ( rawValue ) {
					next[ searchKey ] = rawValue;
				} else {
					delete next[ searchKey ];
				}
				if ( searchTimer.current ) {
					clearTimeout( searchTimer.current );
				}
				searchTimer.current = setTimeout( () => {
					fetchRecords( { page: 1, filters: next } );
				}, 350 );
				return next;
			} );
		},
		[ searchKey, fetchRecords ]
	);

	useEffect( () => {
		if ( ! hasFilters ) {
			setFiltersExpanded( false );
		}
	}, [ hasFilters ] );

	useEffect( () => {
		if ( ! hasFilters ) {
			return;
		}

		const allowedKeys = new Set( filterEntries.map( ( [ key ] ) => key ) );
		setActiveFilters( ( prev ) => {
			const next = {};
			Object.entries( prev ).forEach( ( [ key, value ] ) => {
				if ( allowedKeys.has( key ) ) {
					next[ key ] = value;
				}
			} );

			if ( Object.keys( next ).length === Object.keys( prev ).length ) {
				return prev;
			}

			return next;
		} );
	}, [ filterEntries, hasFilters ] );

	const updateFilterValue = useCallback( ( key, value ) => {
		setActiveFilters( ( prev ) => {
			const sanitised = sanitiseFilterValue( value );

			if ( sanitised === undefined ) {
				if ( ! Object.prototype.hasOwnProperty.call( prev, key ) ) {
					return prev;
				}

				const next = { ...prev };
				delete next[ key ];
				return next;
			}

			const previousValue = prev[ key ];
			const isSame =
				Array.isArray( previousValue ) || Array.isArray( sanitised )
					? JSON.stringify( previousValue ) ===
					  JSON.stringify( sanitised )
					: previousValue === sanitised;

			if ( isSame ) {
				return prev;
			}

			return {
				...prev,
				[ key ]: sanitised,
			};
		} );
	}, [] );

	const appliedFiltersCount = useMemo(
		() =>
			Object.keys( normaliseFilters( query.filters ?? {} ) ).filter(
				( key ) => key !== searchKey
			).length,
		[ query.filters, searchKey ]
	);

	const filtersAreDirty = useMemo( () => {
		const applied = normaliseFilters( query.filters ?? {} );
		const pending = normaliseFilters( activeFilters );

		return JSON.stringify( applied ) !== JSON.stringify( pending );
	}, [ activeFilters, query.filters ] );

	const applyFilters = useCallback( () => {
		fetchRecords( { page: 1, filters: activeFilters } );
	}, [ activeFilters, fetchRecords ] );

	const resetFilters = useCallback( () => {
		setActiveFilters( {} );
		fetchRecords( { page: 1, filters: {} } );
	}, [ fetchRecords ] );

	const renderFilterControl = useCallback(
		( key, definition ) => {
			const label = definition?.label ?? key;
			const type = (
				definition?.control ??
				definition?.type ??
				'text'
			).toLowerCase();
			// Options may be a list ([{value,label}] / strings) OR a { value: label }
			// map (how PHP assoc arrays serialize) — normalise both.
			const rawOptions = definition?.options;
			const options = Array.isArray( rawOptions )
				? rawOptions
				: rawOptions && typeof rawOptions === 'object'
				? Object.entries( rawOptions ).map( ( [ value, label ] ) => ( {
						value,
						label,
				  } ) )
				: [];
			const normalisedOptions = options
				.map( ( option ) => {
					if ( typeof option === 'string' ) {
						return { label: option, value: option };
					}

					const value =
						option?.value ??
						option?.id ??
						option?.slug ??
						option?.name ??
						'';
					const optionLabel =
						option?.label ??
						option?.name ??
						option?.title ??
						String( value );

					return {
						label: optionLabel,
						value,
					};
				} )
				.filter(
					( option ) =>
						option.value !== undefined && option.value !== null
				)
				.map( ( option ) => ( {
					label: String( option.label ),
					value: String( option.value ),
				} ) );

			const value = activeFilters[ key ];
			const isMultiple =
				definition?.multiple !== undefined
					? Boolean( definition?.multiple )
					: type === 'multi-select' || type === 'multiselect';

			if ( isMultiple ) {
				const tokens = Array.isArray( value )
					? value.map( ( item ) => {
							const match = normalisedOptions.find(
								( option ) =>
									String( option.value ) === String( item )
							);
							return match?.label ?? String( item );
					  } )
					: [];
				const suggestions = normalisedOptions.map(
					( option ) => option.label
				);
				const mapLabelToValue = normalisedOptions.reduce(
					( acc, option ) => {
						acc.set( option.label, option.value );
						return acc;
					},
					new Map()
				);

				return (
					<FormTokenField
						__next40pxDefaultSize
						__nextHasNoMarginBottom
						__experimentalExpandOnFocus
						__experimentalShowHowTo={ false }
						label={ label }
						value={ tokens }
						suggestions={ suggestions }
						onChange={ ( tokensValue ) => {
							const mapped = tokensValue
								.map(
									( token ) =>
										mapLabelToValue.get( token ) ?? token
								)
								.filter( ( token ) => token !== '' );
							updateFilterValue( key, mapped );
						} }
					/>
				);
			}

			// Any option-backed filter is a searchable combobox dropdown.
			if ( normalisedOptions.length ) {
				return (
					<ComboboxControl
						label={ label }
						value={ typeof value === 'string' ? value : '' }
						options={ normalisedOptions }
						allowReset
						onChange={ ( newValue ) =>
							updateFilterValue( key, newValue || '' )
						}
						__next40pxDefaultSize
						__nextHasNoMarginBottom
					/>
				);
			}

			return (
				<TextControl
					label={ label }
					value={ typeof value === 'string' ? value : '' }
					onChange={ ( newValue ) =>
						updateFilterValue( key, newValue )
					}
					placeholder={ definition?.placeholder ?? '' }
					__nextHasNoMarginBottom
				/>
			);
		},
		[ activeFilters, updateFilterValue ]
	);

	const columns = useMemo( () => provider?.columns ?? {}, [ provider ] );

	// All selectable columns: the provider's declared columns plus any scalar
	// data keys present in the records (so custom fields can be shown too).
	const allColumnKeys = useMemo( () => {
		const keys = [ ...Object.keys( columns ) ];
		const seen = new Set( keys );
		listRecords.forEach( ( record ) => {
			Object.keys( record ?? {} ).forEach( ( key ) => {
				if (
					seen.has( key ) ||
					key === primaryKey ||
					key === 'id' ||
					/_preview$/.test( key )
				) {
					return;
				}
				const value = record[ key ];
				// Selectable as a column if scalar, OR an array of scalars (a
				// multi-value custom field like checkboxes/multiselect). Arrays of
				// objects and nested objects stay excluded — too complex for a cell.
				const isScalar = value === null || typeof value !== 'object';
				const isScalarArray =
					Array.isArray( value ) &&
					value.every( ( v ) => v === null || typeof v !== 'object' );
				if ( isScalar || isScalarArray ) {
					seen.add( key );
					keys.push( key );
				}
			} );
		} );
		return keys;
	}, [ columns, listRecords, primaryKey ] );

	const columnLabel = useCallback(
		( key ) => columns[ key ]?.label ?? humanizeKey( key ),
		[ columns ]
	);

	const prefsKey = providerId ? `cliqui:columns:${ providerId }` : null;
	const [ columnPrefs, setColumnPrefs ] = useState( { order: [], hidden: [] } );

	useEffect( () => {
		if ( ! prefsKey ) {
			return;
		}
		try {
			const raw = window.localStorage.getItem( prefsKey );
			setColumnPrefs(
				raw ? JSON.parse( raw ) : { order: [], hidden: [] }
			);
		} catch ( e ) {
			setColumnPrefs( { order: [], hidden: [] } );
		}
	}, [ prefsKey ] );

	const updateColumnPrefs = useCallback(
		( next ) => {
			setColumnPrefs( next );
			if ( prefsKey ) {
				try {
					window.localStorage.setItem(
						prefsKey,
						JSON.stringify( next )
					);
				} catch ( e ) {
					// Ignore storage failures (private mode, quota, etc.).
				}
			}
		},
		[ prefsKey ]
	);

	// Until the admin customises columns, show only the provider's declared
	// columns; record-derived keys (incl. custom fields) are available but
	// hidden by default so the table doesn't explode in width.
	const effectivePrefs = useMemo( () => {
		const hasStored =
			( columnPrefs.order?.length ?? 0 ) > 0 ||
			( columnPrefs.hidden?.length ?? 0 ) > 0;
		if ( hasStored ) {
			return columnPrefs;
		}
		const baseKeys = Object.keys( columns );
		return {
			order: baseKeys,
			hidden: allColumnKeys.filter( ( k ) => ! baseKeys.includes( k ) ),
		};
	}, [ columnPrefs, columns, allColumnKeys ] );

	const columnKeys = useMemo( () => {
		const hidden = new Set( effectivePrefs.hidden ?? [] );
		const order = ( effectivePrefs.order ?? [] ).filter( ( k ) =>
			allColumnKeys.includes( k )
		);
		const rest = allColumnKeys.filter( ( k ) => ! order.includes( k ) );
		return [ ...order, ...rest ].filter( ( k ) => ! hidden.has( k ) );
	}, [ allColumnKeys, effectivePrefs ] );

	const handleSort = useCallback(
		( columnKey ) => {
			const column = columns[ columnKey ] ?? {};
			if ( column.sortable === false ) {
				return;
			}

			const isCurrent = query.orderBy === columnKey;
			let nextOrder = 'asc';

			if ( isCurrent ) {
				nextOrder = query.order === 'asc' ? 'desc' : 'asc';
			}

			fetchRecords( { orderBy: columnKey, order: nextOrder, page: 1 } );
		},
		[ columns, fetchRecords, query.order, query.orderBy ]
	);

	const perPage = query.perPage ?? pagination?.per_page ?? 20;
	const currentPage = query.page ?? pagination?.page ?? 1;
	const totalRecords = pagination?.total ?? listRecords.length;
	const totalPages = Math.max(
		1,
		pagination?.total_pages ??
			Math.ceil( Math.max( totalRecords, 1 ) / perPage )
	);

	const goToPreviousPage = useCallback( () => {
		if ( currentPage > 1 ) {
			fetchRecords( { page: currentPage - 1 } );
		}
	}, [ currentPage, fetchRecords ] );

	const goToNextPage = useCallback( () => {
		if ( currentPage < totalPages ) {
			fetchRecords( { page: currentPage + 1 } );
		}
	}, [ currentPage, totalPages, fetchRecords ] );

	const handlePerPageChange = useCallback(
		( value ) => {
			const nextValue = Number.parseInt( value, 10 );

			if ( ! Number.isNaN( nextValue ) && nextValue > 0 ) {
				fetchRecords( { perPage: nextValue, page: 1 } );
			}
		},
		[ fetchRecords ]
	);

	const paginationSummary = useMemo( () => {
		if ( totalRecords === 0 ) {
			return __( 'No results', 'cliq-store-locator' );
		}

		const start = ( currentPage - 1 ) * perPage + 1;
		const end = Math.min( totalRecords, currentPage * perPage );

		return sprintf(
			/* translators: 1: Start result number, 2: end result number, 3: total results. */
			__( 'Showing %1$d–%2$d of %3$d', 'cliq-store-locator' ),
			start,
			end,
			totalRecords
		);
	}, [ currentPage, perPage, totalRecords ] );

	const initialFormValues = useMemo( () => {
		if ( ! formFields.length ) {
			return {};
		}

		const defaults = formFields.reduce(
			( acc, field ) => ( {
				...acc,
				[ field.name ]: field.default ?? '',
			} ),
			{}
		);

		if ( formMode === 'edit' && currentRecord ) {
			return {
				...defaults,
				...currentRecord,
			};
		}

		return defaults;
	}, [ formFields, formMode, currentRecord ] );

	// Reflect the open record in the URL hash (#/<provider>/edit/<id> or .../new) so
	// a refresh reopens it. App.js only rewrites the FIRST hash segment, so this
	// sub-path survives. `routedRef` tracks what we last opened FROM the URL to
	// avoid a write→read→open loop.
	const routedRef = useRef( null );
	const setRecordHash = useCallback(
		( segment ) => {
			if ( ! providerId ) {
				return;
			}
			const base = `/${ providerId }`;
			const next = segment ? `${ base }/${ segment }` : base;
			if ( ( window.location.hash || '' ).replace( /^#/, '' ) !== next ) {
				window.location.hash = next;
			}
		},
		[ providerId ]
	);

	// Forget any URL-routed record when switching providers (refs persist across
	// the reused component instance, so reset to avoid a stale cross-provider id).
	useEffect( () => {
		routedRef.current = null;
	}, [ providerId ] );

	const openCreateModal = () => {
		setFormMode( 'create' );
		setCurrentRecord( null );
		setFormOpen( true );
		routedRef.current = '__new__';
		setRecordHash( 'new' );
	};

	const openEditModal = ( record ) => {
		setFormMode( 'edit' );
		setCurrentRecord( record );
		setFormOpen( true );
		const id = record?.[ primaryKey ] ?? record?.id;
		routedRef.current = id !== undefined && id !== null ? String( id ) : null;
		if ( id === undefined || id === null ) {
			return;
		}
		setRecordHash( `edit/${ encodeURIComponent( id ) }` );
		// Grid rows carry DISPLAY-transformed values (status badges, category
		// names, media previews). Re-fetch the RAW single record so the form binds
		// real values — otherwise saving writes the display shapes back and drops
		// pivots (names ≠ ids) and non-slug columns. The single-record endpoint
		// returns raw values; list rows do not.
		if ( rest?.namespace && provider?.route ) {
			apiFetch( {
				path: `${ rest.namespace }/${ provider.route }/${ encodeURIComponent(
					id
				) }${ contextQueryString() }`,
			} )
				.then( ( res ) => {
					const raw = res?.record ?? res;
					if ( raw && typeof raw === 'object' ) {
						setCurrentRecord( raw );
					}
				} )
				.catch( () => {} );
		}
	};

	const openDeleteConfirm = ( ids ) => {
		const list = Array.isArray( ids ) ? ids : [ ids ];
		setPendingIds( list );
		setConfirmOpen( true );
	};

	const closeModals = () => {
		setFormOpen( false );
		setConfirmOpen( false );
		setCurrentRecord( null );
		setPendingIds( [] );
		setIsSaving( false );
		setIsDeleting( false );
		routedRef.current = null;
		setRecordHash( null );
	};

	// Open the record named in the URL hash (deep link / refresh while editing).
	// Resolves from the loaded page first, else fetches the single record.
	useEffect( () => {
		if ( ! providerId || isFormOpen ) {
			return;
		}
		const parts = ( window.location.hash || '' )
			.replace( /^#\/?/, '' )
			.split( '/' );
		if ( parts[ 0 ] !== providerId ) {
			return;
		}
		if ( parts[ 1 ] === 'new' && routedRef.current !== '__new__' ) {
			openCreateModal();
			return;
		}
		if ( parts[ 1 ] !== 'edit' || ! parts[ 2 ] ) {
			return;
		}
		const id = decodeURIComponent( parts[ 2 ] );
		if ( routedRef.current === id ) {
			return;
		}
		const found = listRecords.find(
			( r ) => String( r?.[ primaryKey ] ?? r?.id ) === String( id )
		);
		if ( found ) {
			openEditModal( found );
		} else if ( status === 'ready' && rest?.namespace && provider?.route ) {
			routedRef.current = id;
			apiFetch( {
				path: `${ rest.namespace }/${ provider.route }/${ encodeURIComponent( id ) }${ contextQueryString() }`,
			} )
				.then( ( res ) => {
					const rec = res?.record ?? res;
					if ( rec && typeof rec === 'object' ) {
						openEditModal( rec );
					}
				} )
				.catch( () => {} );
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [ providerId, listRecords, status, isFormOpen ] );

	const handleFormSubmit = async ( values ) => {
		if ( ! provider || ! rest?.namespace ) {
			return;
		}

		setIsSaving( true );
		setError( providerId, null );

		const resourcePath = `${ rest.namespace }/${ provider.route }`;
		const isCreate = formMode === 'create';
		const recordId = values[ primaryKey ] ?? currentRecord?.[ primaryKey ];

		// Carry the active query context (e.g. multilingual `lang`) so the provider
		// can save into the right scope — e.g. write a translation sibling row.
		const ctxQuery = contextQueryString();
		const options = {
			path: ( isCreate
				? resourcePath
				: `${ resourcePath }/${ encodeURIComponent( recordId ) }` ) + ctxQuery,
			method: isCreate ? 'POST' : 'PUT',
			data: values,
		};

		try {
			await apiFetch( options );
			const recordLabel = formSchema.label ?? provider.label;
			const successMessage = isCreate
				? sprintf(
						/* translators: %s: Record label. */
						__( '%s created.', 'cliq-store-locator' ),
						recordLabel
				  )
				: sprintf(
						/* translators: %s: Record label. */
						__( '%s updated.', 'cliq-store-locator' ),
						recordLabel
				  );

			notify( 'success', successMessage, {
				type: 'snackbar',
			} );
			setFormOpen( false );
			setIsSaving( false );
			invalidateListCache( `${ rest.namespace }/${ provider.route }` );
			fetchRecords( { force: true } );
		} catch ( fetchError ) {
			const message =
				fetchError?.message ??
				__( 'Unable to save record.', 'cliq-store-locator' );
			notify( 'error', message, { type: 'snackbar' } );
			setIsSaving( false );
		}
	};

	const handleDelete = async () => {
		if ( ! provider || ! rest?.namespace || pendingIds.length === 0 ) {
			setConfirmOpen( false );
			return;
		}

		setIsDeleting( true );
		const resourcePath = `${ rest.namespace }/${ provider.route }`;

		try {
			await Promise.all(
				pendingIds.map( ( id ) =>
					apiFetch( {
						path: `${ resourcePath }/${ encodeURIComponent( id ) }`,
						method: 'DELETE',
					} )
				)
			);

			notify(
				'success',
				sprintf(
					/* translators: %d: Number of deleted records. */
					_n(
						'%d record deleted.',
						'%d records deleted.',
						pendingIds.length,
						'cliq-store-locator'
					),
					pendingIds.length
				),
				{ type: 'snackbar' }
			);

			setSelectedIds( ( prev ) =>
				prev.filter( ( id ) => ! pendingIds.includes( id ) )
			);
			setIsDeleting( false );
			setConfirmOpen( false );
			invalidateListCache( `${ rest.namespace }/${ provider.route }` );
			fetchRecords( { force: true } );
		} catch ( fetchError ) {
			const message =
				fetchError?.message ??
				__( 'Unable to delete record.', 'cliq-store-locator' );
			notify( 'error', message, { type: 'snackbar' } );
			setIsDeleting( false );
		}
	};

	const bulkActions = useMemo(
		() =>
			Array.isArray( formSchema?.bulkActions )
				? formSchema.bulkActions.filter( ( action ) => action?.id )
				: [],
		[ formSchema ]
	);

	const handleBulkApply = async ( { action, field, value } ) => {
		if (
			! provider ||
			! rest?.namespace ||
			selectedIds.length === 0 ||
			! field?.name
		) {
			return;
		}

		setBulkSaving( true );
		const resourcePath = `${ rest.namespace }/${ provider.route }`;

		try {
			await apiFetch( {
				path: `${ resourcePath }/bulk${ contextQueryString() }`,
				method: 'PUT',
				data: {
					ids: selectedIds,
					action: action.id,
					field: field.name,
					value,
					mode: action.mode ?? 'replace',
				},
			} );

			notify(
				'success',
				sprintf(
					/* translators: %d: Number of updated records. */
					_n(
						'%d record updated.',
						'%d records updated.',
						selectedIds.length,
						'cliq-store-locator'
					),
					selectedIds.length
				),
				{ type: 'snackbar' }
			);
			setBulkOpen( false );
			setSelectedIds( [] );
			invalidateListCache( `${ rest.namespace }/${ provider.route }` );
			await fetchRecords( { force: true } );
		} catch ( fetchError ) {
			notify(
				'error',
				fetchError?.message ??
					__( 'Unable to update the selected records.', 'cliq-store-locator' ),
				{ type: 'snackbar' }
			);
		} finally {
			setBulkSaving( false );
		}
	};

	const allRowIds = useMemo(
		() =>
			listRecords.map( ( record, index ) => {
				const value = record?.[ primaryKey ] ?? record?.id ?? index;
				return value?.toString();
			} ),
		[ listRecords, primaryKey ]
	);

	const toggleSelectAll = ( checked ) => {
		if ( checked ) {
			setSelectedIds( allRowIds );
		} else {
			setSelectedIds( [] );
		}
	};

	const toggleRow = ( rowId, checked ) => {
		setSelectedIds( ( prev ) => {
			if ( checked ) {
				return prev.includes( rowId ) ? prev : [ ...prev, rowId ];
			}

			return prev.filter( ( id ) => id !== rowId );
		} );
	};

	const isAllSelected =
		allRowIds.length > 0 && selectedIds.length === allRowIds.length;

	const emptyState = (
		<div className="cliqthemes-ui-empty">
			<p>
				{ __(
					'Select a data provider to view its records.',
					'cliq-store-locator'
				) }
			</p>
		</div>
	);

	const content = provider ? (
		<Card className="cliqthemes-ui-card">
			<div className="cliqthemes-ui-card-toolbar">
				<div>
					<h2 className="cliqthemes-ui-card-title">
						{ isFormOpen ? editorTitle : provider.label }
					</h2>
					{ ! isFormOpen && (
						<span className="cliqthemes-ui-card-subtitle">
							{ columnKeys.length }{ ' ' }
							{ columnKeys.length === 1
								? __( 'column', 'cliq-store-locator' )
								: __( 'columns', 'cliq-store-locator' ) }
						</span>
					) }
				</div>
				<div className="cliqthemes-ui-toolbar-actions">
					{ searchKey && ! isFormOpen && (
						<div className="cliqthemes-ui-searchbox">
							<Search size={ 15 } strokeWidth={ 1.9 } className="cliqthemes-ui-searchbox__icon" />
							<input
								type="search"
								className="cliqthemes-ui-searchbox__input"
								placeholder={
									provider?.filters?.[ searchKey ]?.label ||
									__( 'Search…', 'cliq-store-locator' )
								}
								value={ activeFilters[ searchKey ] ?? '' }
								onChange={ ( event ) => runSearch( event.target.value ) }
							/>
						</div>
					) }
					{ isFormOpen ? (
						<Button
							variant="tertiary"
							className="cliqthemes-ui-button cliqthemes-ui-button--ghost"
							onClick={ closeModals }
							disabled={ isSaving }
						>
							<X
								size={ 16 }
								strokeWidth={ 1.8 }
								className="cliqthemes-ui-button__icon"
							/>
							{ __( 'Cancel', 'cliq-store-locator' ) }
						</Button>
					) : (
						! readOnly && (
							<Button
								variant="primary"
								className="cliqthemes-ui-button cliqthemes-ui-button--primary"
								onClick={ openCreateModal }
							>
								<Plus
									size={ 16 }
									strokeWidth={ 1.8 }
									className="cliqthemes-ui-button__icon"
								/>
								{ __( 'Add New', 'cliq-store-locator' ) }
							</Button>
						)
					) }
					{ hasFilters && ! isFormOpen && (
						<Button
							variant="tertiary"
							className={ `cliqthemes-ui-button cliqthemes-ui-button--ghost cliqthemes-ui-button--filters${
								isFiltersExpanded ? ' is-active' : ''
							}` }
							onClick={ () =>
								setFiltersExpanded( ( prev ) => ! prev )
							}
							aria-expanded={ isFiltersExpanded }
						>
							<Filter
								size={ 16 }
								strokeWidth={ 1.8 }
								className="cliqthemes-ui-button__icon"
							/>
							{ __( 'Filters', 'cliq-store-locator' ) }
							{ appliedFiltersCount > 0 && (
								<span className="cliqthemes-ui-filter-count">
									{ appliedFiltersCount }
								</span>
							) }
						</Button>
					) }
					{ ! isFormOpen && allColumnKeys.length > 0 && (
						<ColumnManager
							columns={ allColumnKeys }
							label={ columnLabel }
							prefs={ effectivePrefs }
							onChange={ updateColumnPrefs }
						/>
					) }
					<Button
						variant="secondary"
						className="cliqthemes-ui-button cliqthemes-ui-button--secondary"
						onClick={ () => fetchRecords( { force: true } ) }
						isBusy={ status === 'loading' }
						disabled={ isFormOpen }
					>
						<RotateCw
							size={ 16 }
							strokeWidth={ 2 }
							className="cliqthemes-ui-button__icon"
						/>
						{ __( 'Refresh', 'cliq-store-locator' ) }
					</Button>
				</div>
			</div>

			<CardBody>
				{ isFormOpen ? (
					<RecordFormModal
						isOpen={ isFormOpen }
						display="inline"
						readOnly={ readOnly }
						submitLabel={
							formMode === 'create'
								? submitLabels.create
								: submitLabels.update
						}
						fields={ formFields }
						sections={ formSchema.sections ?? [] }
						initialValues={ initialFormValues }
						isSaving={ isSaving }
						onSubmit={ handleFormSubmit }
						onCancel={ closeModals }
					/>
				) : (
					<>
						{ status === 'error' && error && (
							<Notice status="error" isDismissible={ false }>
								{ error }
							</Notice>
						) }

						{ hasFilters && ! isFormOpen && isFiltersExpanded && (
							<div
								className={ `cliqthemes-ui-filters${
									isFiltersExpanded ? ' is-expanded' : ''
								}` }
							>
								<div className="cliqthemes-ui-filters__header">
									<span className="cliqthemes-ui-filters__summary">
										{ appliedFiltersCount > 0
											? sprintf(
													/* translators: %d: Number of filters currently applied. */
													_n(
														'%d filter applied',
														'%d filters applied',
														appliedFiltersCount,
														'cliq-store-locator'
													),
													appliedFiltersCount
											  )
											: __(
													'No filters applied',
													'cliq-store-locator'
											  ) }
									</span>
									<div className="cliqthemes-ui-filters__actions">
										<Button
											variant="secondary"
											className="cliqthemes-ui-button cliqthemes-ui-button--secondary"
											onClick={ applyFilters }
											disabled={ ! filtersAreDirty }
										>
											{ __(
												'Apply Filters',
												'cliq-store-locator'
											) }
										</Button>
										<Button
											variant="tertiary"
											className="cliqthemes-ui-button cliqthemes-ui-button--ghost"
											onClick={ resetFilters }
											disabled={
												appliedFiltersCount === 0 &&
												! filtersAreDirty
											}
										>
											{ __( 'Reset', 'cliq-store-locator' ) }
										</Button>
									</div>
								</div>
								{ isFiltersExpanded && (
									<div className="cliqthemes-ui-filters__controls">
										{ advancedFilterEntries.map(
											( [ key, definition ] ) => (
												<div
													key={ key }
													className="cliqthemes-ui-filters__control"
												>
													{ renderFilterControl(
														key,
														definition
													) }
												</div>
											)
										) }
									</div>
								) }
							</div>
						) }

						{ selectedIds.length > 0 && (
								<div className="cliqthemes-ui-bulk-actions">
							{ bulkActions.length > 0 && (
								<Button
									variant="secondary"
									className="cliqthemes-ui-button cliqthemes-ui-button--secondary"
									onClick={ () => setBulkOpen( true ) }
								>
									<Edit3
										size={ 16 }
										strokeWidth={ 1.8 }
										className="cliqthemes-ui-button__icon"
									/>
									{ __( 'Bulk edit', 'cliq-store-locator' ) }
								</Button>
							) }
							<Button
								variant="secondary"
								className="cliqthemes-ui-button cliqthemes-ui-button--danger"
								isDestructive
								disabled={ selectedIds.length === 0 }
								onClick={ () =>
									openDeleteConfirm( selectedIds )
								}
							>
								<Trash2
									size={ 16 }
									strokeWidth={ 1.8 }
									className="cliqthemes-ui-button__icon"
								/>
								{ __( 'Delete Selected', 'cliq-store-locator' ) }
							</Button>
							<span className="cliqthemes-ui-selection-meta">
								{ selectedIds.length > 0
									? sprintf(
											/* translators: %d: Number of selected items. */
											_n(
												'%d item selected',
												'%d items selected',
												selectedIds.length,
												'cliq-store-locator'
											),
											selectedIds.length
									  )
									: __(
											'No items selected.',
											'cliq-store-locator'
									  ) }
							</span>
						</div>
							) }

						{ status !== 'loading' &&
							listRecords.length === 0 &&
							! error && (
								<p className="cliqthemes-ui-empty-state">
									{ __(
										'No records found for this provider.',
										'cliq-store-locator'
									) }
								</p>
							) }

						{ ( status === 'loading' || listRecords.length > 0 ) && (
							<>
								<div className="cliqthemes-ui-table-wrapper">
									<table className="cliqthemes-ui-table">
										<thead>
											<tr>
												<th className="cliqthemes-ui-table-checkbox">
													<input
														type="checkbox"
														aria-label={ __(
															'Select all',
															'cliq-store-locator'
														) }
														checked={
															isAllSelected
														}
														onChange={ ( event ) =>
															toggleSelectAll(
																event.target
																	.checked
															)
														}
													/>
												</th>
												{ columnKeys.map( ( key ) => {
													const column =
														columns[ key ] ?? {};
													const isSorted =
														query.orderBy === key;
													const isSortable =
														column.sortable !==
														false;

													return (
														<th
															key={ key }
															className={
																isSortable
																	? 'is-sortable'
																	: undefined
															}
														>
															{ isSortable ? (
																<button
																	type="button"
																	className={ `cliqthemes-ui-table-sort${
																		isSorted
																			? ' is-sorted'
																			: ''
																	}` }
																	onClick={ () =>
																		handleSort(
																			key
																		)
																	}
																>
																	<span>
																		{ column.label ??
																			humanizeKey( key ) }
																	</span>
																	{ isSorted &&
																		( query.order ===
																		'asc' ? (
																			<ArrowUp
																				size={
																					14
																				}
																				strokeWidth={
																					1.8
																				}
																			/>
																		) : (
																			<ArrowDown
																				size={
																					14
																				}
																				strokeWidth={
																					1.8
																				}
																			/>
																		) ) }
																</button>
															) : (
																column.label ??
																humanizeKey( key )
															) }
														</th>
													);
												} ) }
												<th className="cliqthemes-ui-table-actions">
													{ __(
														'Actions',
														'cliq-store-locator'
													) }
												</th>
											</tr>
										</thead>
										<tbody>
											{ status === 'loading' &&
												Array.from( { length: 6 } ).map(
													( _, rowIndex ) => (
														<tr
															key={ `skeleton-${ rowIndex }` }
															className="cliqthemes-ui-skel-row"
															aria-hidden="true"
														>
															<td className="cliqthemes-ui-table-checkbox">
																<span className="cliqthemes-ui-skel cliqthemes-ui-skel--check" />
															</td>
															{ columnKeys.map( ( key ) => (
																<td key={ key }>
																	<span className="cliqthemes-ui-skel" />
																</td>
															) ) }
															<td className="cliqthemes-ui-table-actions">
																<span className="cliqthemes-ui-skel cliqthemes-ui-skel--actions" />
															</td>
														</tr>
													)
												) }
											{ status !== 'loading' &&
												listRecords.map(
												( record, index ) => {
													const rowIdValue =
														record?.[
															primaryKey
														] ??
														record?.id ??
														index;
													const rowId =
														rowIdValue?.toString();
													const isSelected =
														selectedIds.includes(
															rowId
														);

													return (
														<tr
															key={
																rowId ?? index
															}
															className={
																isSelected
																	? 'is-selected'
																	: ''
															}
														>
															<td className="cliqthemes-ui-table-checkbox">
																<input
																	type="checkbox"
																	aria-label={ __(
																		'Select row',
																		'cliq-store-locator'
																	) }
																	checked={
																		isSelected
																	}
																	onChange={ (
																		event
																	) =>
																		toggleRow(
																			rowId,
																			event
																				.target
																				.checked
																		)
																	}
																/>
															</td>
															{ columnKeys.map(
																( key ) => (
																	<td
																		key={
																			key
																		}
																	>
																		{ renderCellValue( record?.[ key ] ) }
																	</td>
																)
															) }
															<td className="cliqthemes-ui-table-actions">
																<div className="cliqthemes-ui-rowacts">
																	<button
																		type="button"
																		className="cliqthemes-ui-iconact"
																		data-tip={ readOnly ? __( 'View', 'cliq-store-locator' ) : __( 'Edit', 'cliq-store-locator' ) }
																		aria-label={ readOnly ? __( 'View', 'cliq-store-locator' ) : __( 'Edit', 'cliq-store-locator' ) }
																		onClick={ () => openEditModal( record ) }
																	>
																		{ readOnly ? <Eye size={ 16 } strokeWidth={ 1.8 } /> : <Edit3 size={ 16 } strokeWidth={ 1.8 } /> }
																	</button>
																	<button
																		type="button"
																		className="cliqthemes-ui-iconact cliqthemes-ui-iconact--danger"
																		data-tip={ __( 'Delete', 'cliq-store-locator' ) }
																		aria-label={ __( 'Delete', 'cliq-store-locator' ) }
																		onClick={ () => openDeleteConfirm( rowId ) }
																	>
																		<Trash2 size={ 16 } strokeWidth={ 1.8 } />
																	</button>
																</div>
															</td>
														</tr>
													);
												}
											) }
										</tbody>
									</table>
								</div>
								<div className="cliqthemes-ui-table-footer">
									<div className="cliqthemes-ui-pagination">
										<Button
											variant="tertiary"
											className="cliqthemes-ui-button cliqthemes-ui-button--ghost"
											onClick={ goToPreviousPage }
											disabled={
												currentPage <= 1 ||
												status === 'loading'
											}
										>
											{ __(
												'Previous',
												'cliq-store-locator'
											) }
										</Button>
										<span className="cliqthemes-ui-pagination__status">
											{ sprintf(
												/* translators: 1: Current page, 2: total pages. */
												__(
													'Page %1$d of %2$d',
													'cliq-store-locator'
												),
												currentPage,
												totalPages
											) }
										</span>
										<Button
											variant="tertiary"
											className="cliqthemes-ui-button cliqthemes-ui-button--ghost"
											onClick={ goToNextPage }
											disabled={
												currentPage >= totalPages ||
												status === 'loading'
											}
										>
											{ __( 'Next', 'cliq-store-locator' ) }
										</Button>
									</div>
									<div className="cliqthemes-ui-pagination__controls">
										<SelectControl
											label={ __(
												'Rows per page',
												'cliq-store-locator'
											) }
											hideLabelFromVision
											value={ String( perPage ) }
											options={ [ 10, 20, 50, 100 ].map(
												( value ) => ( {
													label: String( value ),
													value: String( value ),
												} )
											) }
											onChange={ handlePerPageChange }
											__nextHasNoMarginBottom
										/>
										<span className="cliqthemes-ui-pagination__summary">
											{ paginationSummary }
										</span>
									</div>
								</div>
							</>
						) }
					</>
				) }
			</CardBody>
			{ isBulkOpen && (
				<BulkEditModal
					actions={ bulkActions }
					count={ selectedIds.length }
					isSaving={ isBulkSaving }
					onApply={ handleBulkApply }
					onCancel={ () => setBulkOpen( false ) }
				/>
			) }
			<ConfirmDialog
				isOpen={ isConfirmOpen }
				title={ __( 'Delete Records?', 'cliq-store-locator' ) }
				message={ _n(
					'Are you sure you want to delete this record? This action cannot be undone.',
					'Are you sure you want to delete these records? This action cannot be undone.',
					pendingIds.length,
					'cliq-store-locator'
				) }
				confirmLabel={ __( 'Delete', 'cliq-store-locator' ) }
				isBusy={ isDeleting }
				onConfirm={ handleDelete }
				onCancel={ closeModals }
			/>
		</Card>
	) : (
		emptyState
	);

	return content;
};

export default RecordsView;
