import './admin.css';
import { designPresets, type DesignPreset } from './design-presets';
import {
	BROWSER_FONTS,
	buildGoogleFontsCssUrl,
	collectGoogleFontsFromStyle,
	cssFontFamilyValue,
	DEFAULT_FONT,
	filterFontFamilies,
	googleFontsCatalog,
	sanitizeWeight,
	weightsForFamily,
} from './google-fonts';
import { isHiddenForViewport } from './notification-presentation';

type BasicAnalyticsModule = typeof import('./analytics/basic');

( function () {
	'use strict';
	const cfg = window.voidPulseAdmin;
	const root = document.getElementById( 'voidpulse-app' );
	if ( ! root || ! cfg ) {
		return;
	}
	const PREVIEW_FONT_LINK_ID = 'voidpulse-preview-fonts';
	const FALLBACK_WEIGHTS = [ 400, 500, 550, 600, 700 ];
	function availableWeights( family, current ) {
		const catalogWeights = weightsForFamily(
			googleFontsCatalog,
			family || ''
		);
		const weights = new Set(
			family && catalogWeights.length ? catalogWeights : FALLBACK_WEIGHTS
		);
		if ( current !== null && current !== undefined && current !== '' ) {
			const sanitized = sanitizeWeight( current, 400 );
			if ( ! family || catalogWeights.indexOf( sanitized ) >= 0 ) {
				weights.add( sanitized );
			}
		}
		return Array.from( weights ).sort( function ( a, b ) {
			return a - b;
		} );
	}
	const requestedTab = new URLSearchParams( window.location.search ).get(
		'tab'
	);
	const initialTab =
		[
			'dashboard',
			'campaigns',
			'analytics',
			'settings',
			'upgrade',
		].indexOf( requestedTab ) >= 0
			? requestedTab
			: 'dashboard';
	const state: any = {
		campaign: null,
		analytics: null,
		tab: initialTab,
		dirty: false,
		preview: 'desktop',
		notice: '',
		noticeType: 'success',
		diagnostics: null,
		diagnosticsLoading: false,
		dataLifecycle: null,
		dataLifecycleLoading: false,
		analyticsConfirm: false,
		analyticsRange: 30,
		analyticsLoading: false,
		previewData: cfg.preview,
		productRuleProducts: [],
		productSearchResults: [],
		productSearchQuery: '',
		productSearchLoading: false,
		productSearchFocused: false,
	};
	let toastTimer = 0;
	let productSearchTimer = 0;
	let productSearchGeneration = 0;
	let basicAnalyticsModule: BasicAnalyticsModule | null = null;
	let analyticsRenderGeneration = 0;
	let fontPickerOutsideBound = false;

	function resetBasicAnalytics() {
		analyticsRenderGeneration += 1;
		if ( basicAnalyticsModule ) {
			basicAnalyticsModule.unmountBasicAnalytics();
		}
	}

	function mountBasicAnalyticsOnDemand() {
		const generation = analyticsRenderGeneration;
		const node = document.getElementById( 'voidpulse-basic-analytics' );
		if ( ! node ) {
			return;
		}
		import( /* webpackChunkName: 'admin-analytics' */ './analytics/basic' )
			.then( function ( analyticsModule ) {
				basicAnalyticsModule = analyticsModule;
				if (
					generation !== analyticsRenderGeneration ||
					state.tab !== 'analytics' ||
					! node.isConnected
				) {
					return;
				}
				analyticsModule.mountBasicAnalytics( node, state.analytics );
			} )
			.catch( function () {
				if (
					generation === analyticsRenderGeneration &&
					state.tab === 'analytics' &&
					node.isConnected
				) {
					node.innerHTML =
						'<div class="voidpulse-inline-error" role="alert">The analytics chart could not be loaded. Refresh the page to try again.</div>';
				}
			} );
	}

	function api( path, options: any = {} ) {
		options = options || {};
		options.headers = Object.assign(
			{ 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce },
			options.headers || {}
		);
		return fetch( cfg.root + path, options ).then( function ( response ) {
			return response.json().then( function ( body ) {
				if ( ! response.ok ) {
					throw new Error( body.message || 'Request failed' );
				}
				return body;
			} );
		} );
	}
	function esc( value ) {
		return String(
			value === null || value === undefined ? '' : value
		).replace( /[&<>"']/g, function ( char ) {
			return {
				'&': '&amp;',
				'<': '&lt;',
				'>': '&gt;',
				'"': '&quot;',
				"'": '&#039;',
			}[ char ];
		} );
	}
	function contextualUpgrade(
		eyebrow,
		title,
		copy,
		button = 'Compare plans'
	) {
		if ( cfg.pro ) {
			return '';
		}
		return (
			'<section class="voidpulse-card voidpulse-contextual-upgrade"><div><span class="voidpulse-eyebrow">' +
			esc( eyebrow ) +
			'</span><h2>' +
			esc( title ) +
			'</h2><p>' +
			esc( copy ) +
			'</p></div><button class="button" data-action="upgrade">' +
			esc( button ) +
			'</button></section>'
		);
	}
	function toast() {
		if ( ! state.notice ) {
			return '';
		}
		const isError = state.noticeType === 'error';
		let title = 'All set';
		let iconName = 'check';
		if ( state.noticeType === 'info' ) {
			title = 'Update';
			iconName = 'info';
		}
		if ( isError ) {
			title = 'Something needs attention';
			iconName = 'alert';
		}
		return (
			'<div class="voidpulse-toast-region" aria-live="polite"><div class="voidpulse-toast voidpulse-toast--' +
			esc( state.noticeType ) +
			'" role="' +
			( isError ? 'alert' : 'status' ) +
			'"><span class="voidpulse-toast-icon">' +
			icon( iconName ) +
			'</span><div class="voidpulse-toast-copy"><strong>' +
			esc( title ) +
			'</strong><p>' +
			esc( state.notice ) +
			'</p></div><button type="button" class="voidpulse-toast-close" data-action="dismiss-toast" aria-label="Dismiss notification">×</button></div></div>'
		);
	}
	function dismissToast() {
		if ( toastTimer ) {
			window.clearTimeout( toastTimer );
			toastTimer = 0;
		}
		state.notice = '';
		const node = root.querySelector( '.voidpulse-toast' );
		if ( node ) {
			node.classList.add( 'is-leaving' );
			window.setTimeout( function () {
				node.parentElement?.remove();
			}, 180 );
		}
	}
	function armToast() {
		if ( toastTimer ) {
			window.clearTimeout( toastTimer );
		}
		if ( state.notice ) {
			toastTimer = window.setTimeout( dismissToast, 5000 );
		}
	}
	function icon( name ) {
		const paths = {
			campaign:
				'<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M7 9h10M7 13h6"/>',
			eye: '<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z"/><circle cx="12" cy="12" r="2.5"/>',
			cursor: '<path d="m5 3 14 9-6 2-3 6Z"/>',
			trend: '<path d="m3 17 6-6 4 4 8-9"/><path d="M15 6h6v6"/>',
			check: '<path d="m5 12 4 4L19 6"/>',
			shield: '<path d="M12 3 4 6v5c0 5 3.4 8.7 8 10 4.6-1.3 8-5 8-10V6Z"/><path d="m9 12 2 2 4-4"/>',
			server: '<rect x="3" y="4" width="18" height="6" rx="2"/><rect x="3" y="14" width="18" height="6" rx="2"/><path d="M7 7h.01M7 17h.01"/>',
			play: '<path d="m8 5 11 7-11 7Z"/>',
			alert: '<path d="M12 3 2.5 20h19Z"/><path d="M12 9v4M12 17h.01"/>',
			info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/>',
		};
		return (
			'<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
			( paths[ name ] || paths.check ) +
			'</svg>'
		);
	}
	function field( label, key, type, options: any[] = [] ) {
		const value = get( key );
		let input;
		if ( type === 'select' ) {
			input =
				'<select data-key="' +
				key +
				'">' +
				options
					.map( function ( item ) {
						return (
							'<option value="' +
							esc( item[ 0 ] ) +
							'"' +
							( String( value ) === String( item[ 0 ] )
								? ' selected'
								: '' ) +
							'>' +
							esc( item[ 1 ] ) +
							'</option>'
						);
					} )
					.join( '' ) +
				'</select>';
		} else if ( type === 'checkbox' ) {
			input =
				'<span class="voidpulse-switch"><input type="checkbox" data-key="' +
				key +
				'"' +
				( value ? ' checked' : '' ) +
				'><span></span></span>';
		} else if ( type === 'color' ) {
			const color =
				typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test( value )
					? value
					: '#ffffff';
			input =
				'<div class="voidpulse-color-control"><input class="voidpulse-color-swatch" aria-label="' +
				esc( label ) +
				' colour picker" type="color" data-key="' +
				key +
				'" value="' +
				esc( color ) +
				'"><input class="voidpulse-color-hex" aria-label="' +
				esc( label ) +
				' hexadecimal value" type="text" data-key="' +
				key +
				'" value="' +
				esc( color ) +
				'" maxlength="7" pattern="#[0-9a-fA-F]{6}"></div>';
		} else {
			input =
				'<input type="' +
				type +
				'" data-key="' +
				key +
				'" value="' +
				esc( value ) +
				'">';
		}
		return (
			'<label class="voidpulse-field voidpulse-field--' +
			type +
			'"><span>' +
			esc( label ) +
			'</span>' +
			input +
			'</label>'
		);
	}
	function numberField( label, key, min, max ) {
		const value = get( key );
		return (
			'<label class="voidpulse-field voidpulse-field--number"><span>' +
			esc( label ) +
			'</span><input type="number" min="' +
			min +
			'" max="' +
			max +
			'" data-key="' +
			key +
			'" value="' +
			esc( value ) +
			'"></label>'
		);
	}
	function customiseHeading( title ) {
		return (
			'<div class="voidpulse-customise-subsection"><strong>' +
			esc( title ) +
			'</strong></div>'
		);
	}
	function fontPickerField( label, key ) {
		const value = get( key ) || DEFAULT_FONT;
		return (
			'<label class="voidpulse-field voidpulse-field--font"><span>' +
			esc( label ) +
			'</span><div class="voidpulse-font-picker" data-font-key="' +
			esc( key ) +
			'"><input type="search" class="voidpulse-font-picker-search" placeholder="System or browser font" value="' +
			esc( value ) +
			'" autocomplete="off" aria-label="' +
			esc( label ) +
			'"><input type="hidden" data-key="' +
			esc( key ) +
			'" value="' +
			esc( value ) +
			'"><div class="voidpulse-font-picker-menu" hidden></div></div></label>'
		);
	}
	function weightField( label, fontKey, weightKey, fallback ) {
		const family = get( fontKey ) || DEFAULT_FONT;
		const current = sanitizeWeight( get( weightKey ), fallback );
		const weights = availableWeights( family, current );
		const selected =
			weights.indexOf( current ) >= 0 ? current : weights[ 0 ];
		return (
			'<label class="voidpulse-field voidpulse-field--select"><span>' +
			esc( label ) +
			'</span><select data-key="' +
			esc( weightKey ) +
			'">' +
			weights
				.map( function ( weight ) {
					return (
						'<option value="' +
						weight +
						'"' +
						( weight === selected ? ' selected' : '' ) +
						'>' +
						weight +
						'</option>'
					);
				} )
				.join( '' ) +
			'</select></label>'
		);
	}
	function typographyRoleFields( role, title ) {
		const labels = {
			label: {
				font: 'Type font',
				weight: 'Type weight',
				size: 'Type size',
				min: 8,
				max: 18,
				fallback: 500,
			},
			headline: {
				font: 'Title font',
				weight: 'Title weight',
				size: 'Title size',
				min: 10,
				max: 28,
				fallback: 500,
			},
			message: {
				font: 'Message font',
				weight: 'Message weight',
				size: 'Message size',
				min: 10,
				max: 24,
				fallback: 500,
			},
		}[ role ];
		return (
			'<div class="voidpulse-typography-role"><strong class="voidpulse-typography-role-title">' +
			esc( title ) +
			'</strong><div class="voidpulse-typography-role-fields">' +
			fontPickerField( labels.font, 'style.' + role + '_font' ) +
			weightField(
				labels.weight,
				'style.' + role + '_font',
				'style.' + role + '_weight',
				labels.fallback
			) +
			numberField(
				labels.size,
				'style.' + role + '_size',
				labels.min,
				labels.max
			) +
			'</div></div>'
		);
	}
	function syncPreviewFonts() {
		const url = buildGoogleFontsCssUrl(
			collectGoogleFontsFromStyle(
				state.campaign && state.campaign.style
			),
			googleFontsCatalog
		);
		let link = document.getElementById(
			PREVIEW_FONT_LINK_ID
		) as HTMLLinkElement | null;
		if ( ! url ) {
			if ( link ) {
				link.remove();
			}
			return;
		}
		if ( ! link ) {
			link = document.createElement( 'link' );
			link.id = PREVIEW_FONT_LINK_ID;
			link.rel = 'stylesheet';
			document.head.appendChild( link );
		}
		if ( link.getAttribute( 'href' ) !== url ) {
			link.setAttribute( 'href', url );
		}
	}
	function previewFontVars( style ) {
		const roles: Array< [ string, number ] > = [
			[ 'label', 500 ],
			[ 'headline', 500 ],
			[ 'message', 500 ],
		];
		return roles
			.map( function ( entry ) {
				const role = entry[ 0 ];
				const defaultWeight = entry[ 1 ];
				const familyCss = cssFontFamilyValue(
					String( style[ role + '_font' ] || '' )
				).replace( /"/g, "'" );
				const weight = sanitizeWeight(
					style[ role + '_weight' ],
					defaultWeight
				);
				let css = '';
				if ( familyCss ) {
					// Single quotes so the value survives the HTML style="" attribute.
					css += '--voidpulse-' + role + '-font:' + familyCss + ';';
				}
				css += '--voidpulse-' + role + '-weight:' + weight + ';';
				return css;
			} )
			.join( '' );
	}
	function syncUnsavedIndicator() {
		const badge = root.querySelector( '.voidpulse-unsaved' );
		if ( ! badge ) {
			return;
		}
		badge.textContent = state.dirty
			? 'Unsaved changes'
			: 'All changes saved';
		badge.classList.toggle( 'is-dirty', state.dirty );
	}
	function productRules() {
		const orders = state.campaign.orders;
		const selectedIds = Array.isArray( orders.product_ids )
			? orders.product_ids
			: [];
		const selected = selectedIds.map( function ( id ) {
			return (
				state.productRuleProducts.find( function ( product ) {
					return Number( product.id ) === Number( id );
				} ) || { id, name: 'Product #' + id, sku: '' }
			);
		} );
		const selectedLookup = selectedIds.map( Number );
		const results = state.productSearchResults.filter(
			function ( product ) {
				return selectedLookup.indexOf( Number( product.id ) ) < 0;
			}
		);
		let selectedMarkup =
			'<p class="voidpulse-product-picker-empty">' +
			( orders.product_rule === 'include'
				? 'No products selected. This campaign will not collect sales until you add one.'
				: 'No exclusions yet. Search for products you do not want to show.' ) +
			'</p>';
		if ( selected.length ) {
			selectedMarkup = selected
				.map( function ( product ) {
					return (
						'<span class="voidpulse-product-chip"><span><strong>' +
						esc( product.name ) +
						'</strong>' +
						( product.sku
							? '<small>SKU ' + esc( product.sku ) + '</small>'
							: '' ) +
						'</span><button type="button" data-remove-product="' +
						esc( product.id ) +
						'" aria-label="Remove ' +
						esc( product.name ) +
						'">×</button></span>'
					);
				} )
				.join( '' );
		}
		let resultMarkup = '<p>Enter at least two characters to search.</p>';
		if ( state.productSearchLoading ) {
			resultMarkup = '<p>Searching products…</p>';
		} else if ( state.productSearchQuery.length >= 2 ) {
			resultMarkup = results.length
				? results
						.map( function ( product ) {
							return (
								'<button type="button" role="option" data-add-product="' +
								esc( product.id ) +
								'"><span><strong>' +
								esc( product.name ) +
								'</strong>' +
								( product.sku
									? '<small>SKU ' +
									  esc( product.sku ) +
									  '</small>'
									: '' ) +
								'</span><b>Add</b></button>'
							);
						} )
						.join( '' )
				: '<p>No matching published products.</p>';
		}
		const picker =
			orders.product_rule === 'all'
				? '<p class="voidpulse-product-rule-summary">Every eligible published product may power a notification.</p>'
				: '<div class="voidpulse-product-picker"><div class="voidpulse-product-chips">' +
				  selectedMarkup +
				  '</div><label class="voidpulse-product-search"><span>Find products</span><input type="search" data-product-search value="' +
				  esc( state.productSearchQuery ) +
				  '" placeholder="Search by product name or SKU" autocomplete="off" aria-controls="voidpulse-product-search-results"></label><div id="voidpulse-product-search-results" class="voidpulse-product-results" role="listbox">' +
				  resultMarkup +
				  '</div></div>';
		return (
			'<div class="voidpulse-product-rules">' +
			field(
				'Products allowed to appear',
				'orders.product_rule',
				'select',
				[
					[ 'all', 'All eligible products' ],
					[ 'include', 'Only selected products' ],
					[ 'exclude', 'All except selected products' ],
				]
			) +
			picker +
			'</div>'
		);
	}
	function loadProductRuleProducts() {
		const ids =
			state.campaign &&
			state.campaign.orders &&
			Array.isArray( state.campaign.orders.product_ids )
				? state.campaign.orders.product_ids
				: [];
		if ( ! ids.length ) {
			state.productRuleProducts = [];
			return Promise.resolve( [] );
		}
		return api(
			'products?include=' + encodeURIComponent( ids.join( ',' ) )
		)
			.then( function ( products ) {
				state.productRuleProducts = products;
				if ( state.tab === 'campaigns' ) {
					render();
				}
				return products;
			} )
			.catch( function () {
				return [];
			} );
	}
	function searchProducts( query ) {
		const generation = ++productSearchGeneration;
		state.productSearchQuery = query;
		state.productSearchResults = [];
		if ( productSearchTimer ) {
			window.clearTimeout( productSearchTimer );
		}
		if ( query.trim().length < 2 ) {
			state.productSearchLoading = false;
			render();
			return;
		}
		state.productSearchLoading = true;
		render();
		productSearchTimer = window.setTimeout( function () {
			api( 'products?search=' + encodeURIComponent( query.trim() ) )
				.then( function ( products ) {
					if ( generation !== productSearchGeneration ) {
						return;
					}
					state.productSearchResults = products;
				} )
				.catch( function ( error ) {
					if ( generation === productSearchGeneration ) {
						state.notice = error.message;
						state.noticeType = 'error';
					}
				} )
				.finally( function () {
					if ( generation === productSearchGeneration ) {
						state.productSearchLoading = false;
						render();
					}
				} );
		}, 250 );
	}
	function get( path ) {
		return path.split( '.' ).reduce( function ( object, key ) {
			return object && object[ key ];
		}, state.campaign );
	}
	function set( path, value ) {
		const parts = path.split( '.' );
		let target = state.campaign;
		parts.slice( 0, -1 ).forEach( function ( key ) {
			target = target[ key ];
		} );
		target[ parts[ parts.length - 1 ] ] = value;
		state.dirty = true;
		syncUnsavedIndicator();
	}
	function previewValues() {
		const c = state.campaign;
		const data = state.previewData;
		const identities = data.identities;
		const locations = data.locations;
		return {
			customer: identities[ c.identity ] || identities.first_name,
			location: Object.prototype.hasOwnProperty.call(
				locations,
				c.location
			)
				? locations[ c.location ]
				: locations.city,
			product: data.product,
			time_ago: data.time_ago,
		};
	}
	function productInitials( product ) {
		const initials = String( product )
			.trim()
			.split( /\s+/ )
			.filter( Boolean )
			.slice( 0, 2 )
			.map( function ( word ) {
				return word.charAt( 0 ).toUpperCase();
			} )
			.join( '' );
		return initials || 'SP';
	}
	function previewImage() {
		const c = state.campaign;
		const data = state.previewData;
		if ( ! c.style.show_image ) {
			return '';
		}
		const className =
			'voidpulse-preview-image voidpulse-image--' +
			esc( c.style.image_shape );
		if ( data.product_image ) {
			return (
				'<img class="' +
				className +
				'" src="' +
				esc( data.product_image ) +
				'" alt="">'
			);
		}
		return (
			'<div class="' +
			className +
			'">' +
			esc( productInitials( data.product ) ) +
			'</div>'
		);
	}
	function previewMessage() {
		const values = previewValues();
		return state.campaign.message
			.replace(
				/\{(customer|location|product|time_ago)\}/g,
				function ( match, token ) {
					return values[ token ];
				}
			)
			.replace( /\s+/g, ' ' )
			.replace( ' from purchased ', ' purchased ' )
			.trim();
	}
	function preview() {
		const c = state.campaign;
		const data = state.previewData;
		const message = previewMessage();
		const position =
			state.preview === 'mobile'
				? c.style.mobile_position
				: c.style.desktop_position;
		const labelSize = Number( c.style.label_size || 10 );
		const headlineSize = Number(
			c.style.headline_size || Number( c.style.font_size || 14 ) + 1
		);
		const messageSize = Number(
			c.style.message_size || c.style.font_size || 14
		);
		syncPreviewFonts();
		const previewHidden = isHiddenForViewport(
			c.display || {},
			state.preview === 'mobile'
		);
		return (
			'<div class="voidpulse-preview-stage voidpulse-preview--' +
			state.preview +
			' voidpulse-preview-position--' +
			esc( position ) +
			'"><span class="voidpulse-sample-label">' +
			esc( data.label ) +
			'</span><div class="voidpulse-preview-surface">' +
			( previewHidden
				? '<p class="voidpulse-preview-hidden">Hidden on this viewport</p>'
				: '<article class="voidpulse-preview-card voidpulse-template--' +
				  esc( c.template ) +
				  ' voidpulse-preview-animation--' +
				  esc( c.timing.animation ) +
				  '" style="--bg:' +
				  esc( c.style.background ) +
				  ';--text:' +
				  esc( c.style.text ) +
				  ';--muted:' +
				  esc( c.style.muted_text ) +
				  ';--accent:' +
				  esc( c.style.accent ) +
				  ';--border:' +
				  esc( c.style.border ) +
				  ';--radius:' +
				  Number( c.style.radius ) +
				  'px;--width:' +
				  Number( c.style.width ) +
				  'px;--size:' +
				  messageSize +
				  'px;--label-color:' +
				  esc( c.style.label_color || c.style.accent ) +
				  ';--headline-color:' +
				  esc( c.style.headline_color || c.style.text ) +
				  ';--message-color:' +
				  esc(
						c.style.message_color ||
							c.style.text ||
							c.style.muted_text
				  ) +
				  ';--label-size:' +
				  labelSize +
				  'px;--headline-size:' +
				  headlineSize +
				  'px;--message-size:' +
				  messageSize +
				  'px;--animation-duration:' +
				  Number( c.timing.animation_duration ) +
				  'ms;--animation-easing:' +
				  ( c.timing.animation_easing === 'spring'
						? 'cubic-bezier(0.22, 1.35, 0.36, 1)'
						: esc( c.timing.animation_easing ) ) +
				  ';' +
				  previewFontVars( c.style ) +
				  '">' +
				  previewImage() +
				  '<div class="voidpulse-preview-copy">' +
				  ( c.style.show_label
						? '<span class="voidpulse-preview-label">Recent store activity</span>'
						: '' ) +
				  ( c.style.show_headline
						? '<strong class="voidpulse-preview-headline">' +
						  esc( c.name || 'Recent sales' ) +
						  '</strong>'
						: '' ) +
				  '<p class="voidpulse-preview-message">' +
				  esc( message ) +
				  '</p></div>' +
				  ( c.style.show_close
						? '<button type="button" aria-label="Close preview">×</button>'
						: '' ) +
				  '</article>' ) +
			'</div></div>'
		);
	}
	function stats() {
		const a = state.analytics || { impressions: 0, clicks: 0, ctr: 0 };
		const items = [
			[
				'campaign',
				'Active campaigns',
				state.campaign.status === 'active' ? '1' : '0',
				'Campaigns currently live',
			],
			[
				'eye',
				'Notifications displayed',
				a.impressions,
				'Total impressions',
			],
			[
				'cursor',
				'Notification clicks',
				a.clicks,
				'Customer engagement',
			],
			[ 'trend', 'Click-through rate', a.ctr + '%', 'Clicks per view' ],
		];
		return (
			'<div class="voidpulse-stats">' +
			items
				.map( function ( item ) {
					return (
						'<article><div class="voidpulse-stat-icon">' +
						icon( item[ 0 ] ) +
						'</div><span>' +
						esc( item[ 1 ] ) +
						'</span><strong>' +
						esc( item[ 2 ] ) +
						'</strong><small>' +
						esc( item[ 3 ] ) +
						'</small></article>'
					);
				} )
				.join( '' ) +
			'</div>'
		);
	}
	function dashboard() {
		const c = state.campaign;
		const steps = [
			[
				true,
				'Connect your store',
				'WooCommerce is connected and ready.',
			],
			[
				Boolean( state.campaign.updated_at ),
				'Create your campaign',
				'Add a message using genuine order activity.',
			],
			[
				Boolean( state.campaign.updated_at ),
				'Choose your design',
				'Choose a layout and colour preset.',
			],
			[
				state.campaign.status === 'active',
				'Preview and publish',
				'Review the notification and make it live.',
			],
		];
		const complete = steps.filter( function ( step ) {
			return step[ 0 ];
		} ).length;
		const identityLabels = {
			first_name: 'First name only',
			first_initial: 'First name + initial',
			anonymous: 'Anonymous customer',
			someone: 'Someone',
			customer: 'Generic customer',
		};
		const locationLabels = {
			city: 'City',
			region: 'Region or state',
			country: 'Country',
			city_country: 'City and country',
			hidden: 'Location hidden',
		};
		const scopeLabels = {
			storefront: 'Entire storefront',
			products: 'Product pages',
			archives: 'Shop and archives',
		};
		const campaignPanel = cfg.pro
			? '<div id="voidpulse-pro-dashboard-campaigns"></div>'
			: '<article class="voidpulse-card voidpulse-campaign-summary"><div class="voidpulse-card-head"><div><span class="voidpulse-card-kicker">Current campaign</span><h2>' +
			  esc( c.name ) +
			  '</h2><p>Recent WooCommerce purchase activity</p></div><span class="voidpulse-status voidpulse-status--' +
			  esc( c.status ) +
			  '">' +
			  esc( c.status ) +
			  '</span></div><div class="voidpulse-campaign-message"><span>Message preview</span><strong>' +
			  esc( previewMessage() ) +
			  '</strong></div><dl class="voidpulse-campaign-details"><div><dt>Layout</dt><dd>' +
			  esc( c.template ) +
			  '</dd></div><div><dt>Customer details</dt><dd>' +
			  esc( identityLabels[ c.identity ] ) +
			  ' · ' +
			  esc( locationLabels[ c.location ] ) +
			  '</dd></div><div><dt>Display area</dt><dd>' +
			  esc( scopeLabels[ c.display.scope ] ) +
			  '</dd></div><div><dt>Animation & timing</dt><dd>' +
			  esc( c.timing.animation ) +
			  ' · ' +
			  esc( c.timing.duration ) +
			  's visible · ' +
			  esc( c.timing.gap ) +
			  's gap</dd></div></dl><div class="voidpulse-card-actions"><button class="button button-primary" data-action="edit">Edit campaign</button>' +
			  ( c.status === 'active'
					? '<button class="button" data-action="pause">Pause campaign</button>'
					: '<button class="button" data-action="activate">Activate campaign</button>' ) +
			  '</div></article>';
		return (
			stats() +
			'<div class="voidpulse-dashboard-grid' +
			( cfg.pro ? ' voidpulse-dashboard-grid--pro' : '' ) +
			'"><article class="voidpulse-card voidpulse-setup"><div class="voidpulse-setup-head"><div><span class="voidpulse-card-kicker">Getting started</span><h2>Setup checklist</h2></div><strong>' +
			complete +
			' of ' +
			steps.length +
			'</strong></div><div class="voidpulse-progress"><span style="width:' +
			( complete / steps.length ) * 100 +
			'%"></span></div><ol class="voidpulse-checklist">' +
			steps
				.map( function ( step, index ) {
					return (
						'<li class="' +
						( step[ 0 ] ? 'done' : '' ) +
						'"><span class="voidpulse-step-marker">' +
						( step[ 0 ] ? icon( 'check' ) : index + 1 ) +
						'</span><div><strong>' +
						esc( step[ 1 ] ) +
						'</strong><small>' +
						esc( step[ 2 ] ) +
						'</small></div></li>'
					);
				} )
				.join( '' ) +
			'</ol></article>' +
			campaignPanel +
			'</div>' +
			( cfg.pro
				? ''
				: contextualUpgrade(
						'Grow with Pro',
						'Run the right social proof for every audience',
						'Unlock unlimited campaigns, scheduling, advanced targeting and deeper attribution when you are ready to scale.',
						'See what Pro adds'
				  ) )
		);
	}
	function designPresetChoices() {
		const styleKeys: Array< keyof DesignPreset[ 'style' ] > = [
			'background',
			'text',
			'muted_text',
			'accent',
			'border',
		];
		return (
			'<div class="voidpulse-design-presets"><div class="voidpulse-design-presets-head"><div><strong>Design presets</strong><span>Choose a complete colour system, then fine-tune it below.</span></div><small>14 light · 13 dark</small></div><div class="voidpulse-design-presets-grid">' +
			designPresets
				.map( function ( preset ) {
					const selected = styleKeys.every( function ( key ) {
						return (
							String(
								state.campaign.style[ key ]
							).toUpperCase() ===
							String( preset.style[ key ] ).toUpperCase()
						);
					} );
					const swatches = [
						preset.style.background,
						preset.style.text,
						preset.style.muted_text,
						preset.style.accent,
						preset.style.border,
					];
					return (
						'<button type="button" class="voidpulse-design-preset' +
						( selected ? ' is-selected' : '' ) +
						'" data-design-preset="' +
						esc( preset.id ) +
						'" aria-pressed="' +
						( selected ? 'true' : 'false' ) +
						'"><span class="voidpulse-design-preset-preview" style="--preset-bg:' +
						esc( preset.style.background ) +
						';--preset-text:' +
						esc( preset.style.text ) +
						';--preset-muted:' +
						esc( preset.style.muted_text ) +
						';--preset-accent:' +
						esc( preset.style.accent ) +
						';--preset-border:' +
						esc( preset.style.border ) +
						'"><i></i><span><b></b><em></em></span></span><span class="voidpulse-design-preset-meta"><strong>' +
						esc( preset.name ) +
						'</strong><small>' +
						esc( preset.mode ) +
						'</small></span><span class="voidpulse-design-preset-swatches">' +
						swatches
							.map( function ( colour ) {
								return (
									'<i style="background:' +
									esc( colour ) +
									'"></i>'
								);
							} )
							.join( '' ) +
						'</span><span class="voidpulse-design-preset-check">' +
						icon( 'check' ) +
						'</span></button>'
					);
				} )
				.join( '' ) +
			'</div></div>'
		);
	}
	function builder() {
		if ( cfg.pro ) {
			return '<div id="voidpulse-pro-workspace"></div>';
		}
		const c = state.campaign;
		return (
			'<div class="voidpulse-page-intro voidpulse-page-intro--compact"><div><span class="voidpulse-eyebrow">Campaign editor</span><h2>Build your sale notification</h2><p>Changes appear in the preview instantly. Save when everything looks right.</p></div><span class="voidpulse-unsaved' +
			( state.dirty ? ' is-dirty' : '' ) +
			'">' +
			( state.dirty ? 'Unsaved changes' : 'All changes saved' ) +
			'</span></div><div class="voidpulse-builder"><div class="voidpulse-settings">' +
			'<section class="voidpulse-card"><div class="voidpulse-section-head"><span>01</span><div><h2>Campaign details</h2><p>Name your campaign so it is easy to recognise.</p></div></div>' +
			field( 'Campaign name', 'name', 'text' ) +
			field( 'Order', 'timing.order', 'select', [
				[ 'newest', 'Newest first' ],
				[ 'random', 'Random' ],
			] ) +
			'</section>' +
			'<section class="voidpulse-card"><div class="voidpulse-section-head"><span>02</span><div><h2>Message</h2><p>Use dynamic tokens to personalise real store activity.</p></div></div><label class="voidpulse-field"><span>Notification message</span><textarea data-key="message" rows="3">' +
			esc( c.message ) +
			'</textarea></label><div class="voidpulse-tokens">' +
			[ 'customer', 'location', 'product', 'time_ago' ]
				.map( function ( token ) {
					return (
						'<button type="button" data-token="' +
						token +
						'">{' +
						token +
						'}</button>'
					);
				} )
				.join( '' ) +
			'</div></section>' +
			'<section class="voidpulse-card"><div class="voidpulse-section-head"><span>03</span><div><h2>Customer privacy</h2><p>Control how much customer detail appears publicly.</p></div></div><div class="voidpulse-fields">' +
			field( 'Identity', 'identity', 'select', [
				[ 'first_name', 'First name only' ],
				[ 'first_initial', 'First name + surname initial' ],
				[ 'anonymous', 'Anonymous customer' ],
				[ 'someone', 'Someone' ],
				[ 'customer', 'Customer' ],
			] ) +
			field( 'Location', 'location', 'select', [
				[ 'city', 'City' ],
				[ 'region', 'Region or state' ],
				[ 'country', 'Country' ],
				[ 'city_country', 'City and country' ],
				[ 'hidden', 'Hide location' ],
			] ) +
			'</div></section>' +
			'<section class="voidpulse-card"><div class="voidpulse-section-head"><span>04</span><div><h2>Design</h2><p>Start with a curated preset or fine-tune every visual detail.</p></div></div>' +
			designPresetChoices() +
			'<div class="voidpulse-design-customize"><strong>Customise preset</strong><span>Changes update the live preview instantly.</span></div><div class="voidpulse-fields">' +
			customiseHeading( 'Colors' ) +
			field( 'Background', 'style.background', 'color' ) +
			field( 'Message', 'style.message_color', 'color' ) +
			field( 'Accent', 'style.accent', 'color' ) +
			field( 'Border', 'style.border', 'color' ) +
			customiseHeading( 'Typography' ) +
			( c.style.show_label
				? typographyRoleFields( 'label', 'Activity label' )
				: '' ) +
			( c.style.show_headline
				? typographyRoleFields( 'headline', 'Headline' )
				: '' ) +
			typographyRoleFields( 'message', 'Message' ) +
			customiseHeading( 'Layout' ) +
			field( 'Layout', 'template', 'select', [
				[ 'clean', 'Clean — no shadow' ],
				[ 'modern', 'Modern — soft shadow' ],
				[ 'minimal', 'Minimal — left border' ],
			] ) +
			numberField( 'Width', 'style.width', 280, 600 ) +
			numberField( 'Border radius', 'style.radius', 0, 32 ) +
			field( 'Image shape', 'style.image_shape', 'select', [
				[ 'rounded', 'Rounded' ],
				[ 'square', 'Square' ],
				[ 'circle', 'Circle' ],
			] ) +
			customiseHeading( 'Display' ) +
			field( 'Show product image', 'style.show_image', 'checkbox' ) +
			field( 'Show close button', 'style.show_close', 'checkbox' ) +
			field( 'Show activity label', 'style.show_label', 'checkbox' ) +
			field( 'Show headline', 'style.show_headline', 'checkbox' ) +
			field( 'Desktop position', 'style.desktop_position', 'select', [
				[ 'bottom-left', 'Bottom left' ],
				[ 'bottom-right', 'Bottom right' ],
			] ) +
			field( 'Mobile position', 'style.mobile_position', 'select', [
				[ 'bottom-left', 'Bottom left' ],
				[ 'bottom-right', 'Bottom right' ],
			] ) +
			'</div></section>' +
			'<section class="voidpulse-card"><div class="voidpulse-section-head"><span>05</span><div><h2>Animation &amp; timing</h2><p>Choose how notifications enter, then control their display rhythm.</p></div></div><div class="voidpulse-fields">' +
			field( 'Animation', 'timing.animation', 'select', [
				[ 'fade', 'Fade' ],
				[ 'slide', 'Slide up' ],
				[ 'scale', 'Scale in' ],
				[ 'bounce', 'Bounce' ],
				[ 'flip', 'Flip in' ],
			] ) +
			field(
				'Animation speed (milliseconds)',
				'timing.animation_duration',
				'number'
			) +
			field( 'Animation easing', 'timing.animation_easing', 'select', [
				[ 'ease-out', 'Smooth ease out' ],
				[ 'ease-in-out', 'Ease in and out' ],
				[ 'linear', 'Linear' ],
				[ 'spring', 'Spring' ],
			] ) +
			field(
				'Initial delay (seconds)',
				'timing.initial_delay',
				'number'
			) +
			field( 'Display duration (seconds)', 'timing.duration', 'number' ) +
			field( 'Gap (seconds)', 'timing.gap', 'number' ) +
			field( 'Maximum per page', 'timing.maximum', 'number' ) +
			field( 'Loop notifications', 'timing.loop', 'checkbox' ) +
			field( 'Pause in hidden tab', 'timing.pause_hidden', 'checkbox' ) +
			'</div></section>' +
			'<section class="voidpulse-card"><div class="voidpulse-section-head"><span>06</span><div><h2>Display rules</h2><p>Choose where this campaign can appear.</p></div></div><div class="voidpulse-fields">' +
			field( 'Scope', 'display.scope', 'select', [
				[ 'storefront', 'Entire storefront' ],
				[ 'products', 'Product pages only' ],
				[ 'archives', 'Shop and product archives' ],
			] ) +
			field( 'Hide on cart', 'display.hide_cart', 'checkbox' ) +
			field( 'Hide on checkout', 'display.hide_checkout', 'checkbox' ) +
			field( 'Hide on account', 'display.hide_account', 'checkbox' ) +
			field(
				'Hide on order received',
				'display.hide_received',
				'checkbox'
			) +
			field( 'Hide on mobile', 'display.hide_mobile', 'checkbox' ) +
			field( 'Hide on desktop', 'display.hide_desktop', 'checkbox' ) +
			'</div></section>' +
			'<section class="voidpulse-card"><div class="voidpulse-section-head"><span>07</span><div><h2>Order activity</h2><p>Control which genuine purchases can power notifications.</p></div></div><div class="voidpulse-fields">' +
			field( 'History days', 'orders.days', 'number' ) +
			field( 'Maximum orders', 'orders.maximum', 'number' ) +
			field(
				'Include guest orders',
				'orders.include_guests',
				'checkbox'
			) +
			field(
				'Include hidden products',
				'orders.include_hidden_products',
				'checkbox'
			) +
			'</div><fieldset class="voidpulse-statuses"><legend>Eligible order statuses</legend><label><input type="checkbox" data-order-status="wc-processing"' +
			( c.orders.statuses.indexOf( 'wc-processing' ) >= 0
				? ' checked'
				: '' ) +
			'> Processing</label><label><input type="checkbox" data-order-status="wc-completed"' +
			( c.orders.statuses.indexOf( 'wc-completed' ) >= 0
				? ' checked'
				: '' ) +
			'> Completed</label></fieldset>' +
			productRules() +
			'</section>' +
			'<div class="voidpulse-actions"><button class="button" data-action="reset">Reset defaults</button><button class="button" data-action="export">Export JSON</button><label class="button">Import JSON<input hidden type="file" accept=".json,application/json" data-import></label><span class="voidpulse-spacer"></span><button class="button" data-action="save">Save draft</button><button class="button button-primary" data-action="publish"' +
			( ! cfg.canPublish ? ' disabled' : '' ) +
			'>Publish</button></div></div>' +
			'<aside class="voidpulse-preview"><div class="voidpulse-preview-head"><div><span class="voidpulse-eyebrow">Live preview</span><h2>Recent sale</h2></div><div class="voidpulse-preview-tools"><button class="button voidpulse-simulate" type="button" data-action="simulate">' +
			icon( 'play' ) +
			'Simulate</button><div class="voidpulse-segmented">' +
			[ 'desktop', 'tablet', 'mobile' ]
				.map( function ( mode ) {
					return (
						'<button class="' +
						( state.preview === mode ? 'active' : '' ) +
						'" data-preview="' +
						mode +
						'">' +
						mode +
						'</button>'
					);
				} )
				.join( '' ) +
			'</div></div></div>' +
			preview() +
			'</aside></div>'
		);
	}
	const ANALYTICS_RANGES = [
		[ 7, 'Last 7 days' ],
		[ 30, 'Last 30 days' ],
		[ 90, 'Last 90 days' ],
		[ 180, 'Last 180 days' ],
		[ 365, 'Last 365 days' ],
	];
	function analyticsRangePicker() {
		return (
			'<div class="voidpulse-analytics-toolbar"><label class="voidpulse-field voidpulse-field--select"><span>Reporting range</span><select data-analytics-range' +
			( state.analyticsLoading ? ' disabled' : '' ) +
			'>' +
			ANALYTICS_RANGES.map( function ( option ) {
				return (
					'<option value="' +
					option[ 0 ] +
					'"' +
					( Number( state.analyticsRange ) === option[ 0 ]
						? ' selected'
						: '' ) +
					'>' +
					esc( option[ 1 ] ) +
					'</option>'
				);
			} ).join( '' ) +
			'</select></label></div>'
		);
	}
	function analytics() {
		if ( cfg.pro ) {
			return '<div id="voidpulse-pro-analytics"></div>';
		}
		return (
			analyticsRangePicker() +
			'<div id="voidpulse-basic-analytics"></div>' +
			contextualUpgrade(
				'Advanced analytics',
				'Understand which campaigns influence revenue',
				'Pro adds order attribution, revenue reporting, CSV exports and campaign-level performance.',
				'Compare analytics'
			)
		);
	}
	function diagnosticContent() {
		if ( state.diagnosticsLoading || ! state.diagnostics ) {
			return '<div class="voidpulse-diagnostic-loading"><span></span><div><strong>Checking your environment</strong><small>Loading current plugin and store information…</small></div></div>';
		}
		if ( state.diagnostics.error ) {
			return (
				'<div class="voidpulse-inline-error">' +
				esc( state.diagnostics.error ) +
				'</div>'
			);
		}
		const labels = {
			event_cache_count: 'Cached activities',
			last_refresh: 'Last cache refresh',
			next_refresh: 'Next scheduled refresh',
			schema_version: 'Database schema',
			asset_version: 'Plugin assets',
			hpos_enabled: 'WooCommerce HPOS',
		};
		function displayValue( key, value ) {
			if ( key === 'hpos_enabled' ) {
				return value ? 'Enabled' : 'Disabled';
			}
			if ( [ 'last_refresh', 'next_refresh' ].indexOf( key ) >= 0 ) {
				return value
					? new Date( Number( value ) * 1000 ).toLocaleString()
					: 'Not scheduled';
			}
			return value;
		}
		return (
			'<dl>' +
			Object.keys( state.diagnostics )
				.map( function ( key ) {
					if ( cfg.pro && 'schema_version' === key ) {
						return '';
					}
					if (
						'next_refresh' === key &&
						! state.diagnostics[ key ]
					) {
						return (
							'<div class="voidpulse-diagnostic-warning"><dt>' +
							esc( labels[ key ] ) +
							'</dt><dd><strong>Not scheduled</strong><button type="button" class="button button-small" data-action="repair-schedule">Repair schedule</button></dd></div>'
						);
					}
					return (
						'<div><dt>' +
						esc( labels[ key ] || key.replace( /_/g, ' ' ) ) +
						'</dt><dd>' +
						esc( displayValue( key, state.diagnostics[ key ] ) ) +
						'</dd></div>'
					);
				} )
				.join( '' ) +
			'</dl>'
		);
	}
	function formatBytes( value ) {
		const bytes = Math.max( 0, Number( value ) || 0 );
		if ( bytes < 1024 ) {
			return bytes + ' B';
		}
		if ( bytes < 1024 * 1024 ) {
			return ( bytes / 1024 ).toFixed( 1 ) + ' KB';
		}
		return ( bytes / ( 1024 * 1024 ) ).toFixed( 1 ) + ' MB';
	}
	function dataLifecycleContent() {
		const data = state.dataLifecycle;
		if ( state.dataLifecycleLoading || ! data ) {
			return '<article class="voidpulse-card"><div class="voidpulse-diagnostic-loading"><span></span><div><strong>Loading data controls</strong><small>Calculating plugin storage usage…</small></div></div></article>';
		}
		const retention = data.detailed_available
			? '<label class="voidpulse-field voidpulse-field--select"><span>Detailed analytics retention</span><select data-lifecycle-key="retention_days">' +
			  [
					[ 30, '30 days' ],
					[ 90, '90 days' ],
					[ 180, '180 days' ],
					[ 365, '365 days' ],
			  ]
					.map( function ( option ) {
						return (
							'<option value="' +
							option[ 0 ] +
							'"' +
							( Number( data.retention_days ) === option[ 0 ]
								? ' selected'
								: '' ) +
							'>' +
							option[ 1 ] +
							'</option>'
						);
					} )
					.join( '' ) +
			  '</select></label>'
			: '<div class="voidpulse-data-pro-note"><strong>Detailed retention</strong><span>Available with Pro conversion analytics.</span></div>';
		return (
			'<article class="voidpulse-card voidpulse-data-lifecycle"><div class="voidpulse-section-head"><span>' +
			icon( 'shield' ) +
			'</span><div><h2>Data lifecycle</h2><p>Control storage retention and what happens to plugin data.</p></div></div><div class="voidpulse-data-usage"><div><span>Database usage</span><strong>' +
			esc( formatBytes( data.database_bytes ) ) +
			'</strong></div><div><span>Stored records</span><strong>' +
			esc( data.database_records ) +
			'</strong></div><div><span>Plugin tables</span><strong>' +
			esc( data.database_tables ) +
			'</strong></div></div><div class="voidpulse-setting-group"><div><strong>Uninstall cleanup</strong><p>Delete campaigns, analytics and plugin tables only when the plugin is uninstalled.</p></div><label class="voidpulse-field voidpulse-field--checkbox"><span>Delete data on uninstall</span><span class="voidpulse-switch"><input type="checkbox" data-lifecycle-key="delete_on_uninstall"' +
			( data.delete_on_uninstall ? ' checked' : '' ) +
			'><span></span></span></label></div><div class="voidpulse-data-retention">' +
			retention +
			'</div><div class="voidpulse-data-actions"><button type="button" class="button button-primary" data-action="save-data-lifecycle">Save data settings</button></div><div class="voidpulse-danger-zone"><div><strong>Analytics history</strong><p>Permanently clear notification views, clicks and attributed conversion history. Campaigns are not affected.</p></div>' +
			( state.analyticsConfirm
				? '<div class="voidpulse-confirm-actions"><button type="button" class="button" data-action="cancel-clear-analytics">Cancel</button><button type="button" class="button voidpulse-danger-button" data-action="confirm-clear-analytics">Permanently clear</button></div>'
				: '<button type="button" class="button voidpulse-danger-button" data-action="clear-analytics">Clear analytics history</button>' ) +
			'</div></article>'
		);
	}
	function settings() {
		const frequencyCap = Number( state.campaign.performance.frequency_cap );
		const frequencyCapValues = [ 0, 1, 3, 5, 10, 20, 50 ];
		if (
			Number.isFinite( frequencyCap ) &&
			frequencyCap >= 0 &&
			! frequencyCapValues.includes( frequencyCap )
		) {
			frequencyCapValues.push( frequencyCap );
			frequencyCapValues.sort( function ( left, right ) {
				return left - right;
			} );
		}
		const frequencyCapOptions = frequencyCapValues.map( function ( value ) {
			return [ value, value === 0 ? 'Unlimited' : String( value ) ];
		} );
		return (
			'<div class="voidpulse-page-intro voidpulse-page-intro--compact"><div><span class="voidpulse-eyebrow">Global settings</span><h2>Performance and system health</h2><p>Manage activity caching and review the plugin environment.</p></div><button class="button button-primary" data-action="save-settings">Save settings</button></div><div class="voidpulse-settings-layout"><div class="voidpulse-settings-main"><article class="voidpulse-card"><div class="voidpulse-section-head"><span>' +
			icon( 'server' ) +
			'</span><div><h2>Performance</h2><p>Choose how many genuine order activities are kept ready for notifications.</p></div></div>' +
			field(
				'Event cache size',
				'performance.event_cache_size',
				'number'
			) +
			'<div class="voidpulse-setting-group"><div><strong>Administrator visibility</strong><p>Keep notifications visible while you are logged in so you can verify the storefront experience.</p></div>' +
			field(
				'Show notifications to administrators',
				'performance.show_to_admins',
				'checkbox'
			) +
			'</div></article><article class="voidpulse-card voidpulse-delivery-card"><div class="voidpulse-section-head"><span>' +
			icon( 'shield' ) +
			'</span><div><h2>Visitor delivery</h2><p>Control notification fatigue and reduce unnecessary storefront work.</p></div></div><div class="voidpulse-delivery-grid"><div class="voidpulse-setting-group"><div><strong>Frequency cap</strong><p>Unlimited by default. Choose a cap to limit notifications during a rolling 30-minute window.</p></div>' +
			field(
				'Notifications per visitor',
				'performance.frequency_cap',
				'select',
				frequencyCapOptions
			) +
			'</div><div class="voidpulse-setting-group"><div><strong>Dismissal snooze</strong><p>Choose how long notifications stay hidden after a visitor closes one.</p></div>' +
			field(
				'Hide after dismissal',
				'performance.dismissal_snooze',
				'select',
				[
					[ 'session', 'Until the session ends' ],
					[ 'day', 'For 24 hours' ],
					[ 'week', 'For 7 days' ],
				]
			) +
			'</div><div class="voidpulse-setting-group"><div><strong>Repeat protection</strong><p>Avoid showing the same order or campaign repeatedly during one session.</p></div>' +
			field(
				'Prevent repeated activity',
				'performance.repeat_protection',
				'checkbox'
			) +
			'</div><div class="voidpulse-setting-group"><div><strong>Cross-page pacing</strong><p>Preserve the notification gap when a shopper navigates to another page.</p></div>' +
			field(
				'Keep pacing between pages',
				'performance.cross_page_pacing',
				'checkbox'
			) +
			'</div><div class="voidpulse-setting-group"><div><strong>Optimized loading</strong><p>Start notification work when the browser is idle, with a short safety timeout.</p></div>' +
			field(
				'Defer storefront processing',
				'performance.optimized_loading',
				'checkbox'
			) +
			'</div><div class="voidpulse-setting-group"><div><strong>Data-saver mode</strong><p>Skip product images when a visitor asks their browser to reduce data usage.</p></div>' +
			field(
				'Respect reduced-data preferences',
				'performance.data_saver',
				'checkbox'
			) +
			'</div></div></article>' +
			( cfg.pro ? '<div id="voidpulse-pro-settings-main"></div>' : '' ) +
			dataLifecycleContent() +
			'</div><aside class="voidpulse-settings-side"><article class="voidpulse-card voidpulse-system-card"><div class="voidpulse-section-head"><span>' +
			icon( 'server' ) +
			'</span><div><h2>System health</h2><p>Live environment details.</p></div></div><div id="voidpulse-diagnostics">' +
			diagnosticContent() +
			'</div></article><article class="voidpulse-card voidpulse-maintenance"><h2>Cache maintenance</h2><p>Clear stored activity if you need to refresh notification data immediately.</p><button class="button" data-action="clear-cache">Clear cached activity</button></article>' +
			( cfg.pro ? '<div id="voidpulse-pro-settings-side"></div>' : '' ) +
			contextualUpgrade(
				'Pro controls',
				'Target and schedule every campaign',
				'Add audience rules, schedules, team permissions and secure integrations.',
				'Explore Pro controls'
			) +
			'</aside></div>'
		);
	}
	function upgrade() {
		const features = [
			[ 'Campaigns', 'Recent sales notification', 'Unlimited campaigns' ],
			[
				'Notification types',
				'Recent WooCommerce sales',
				'Sales, reviews, product totals, milestones, live viewers, low stock, countdowns and announcements',
			],
			[ 'Notification layouts', '3 layouts', '19 layouts' ],
			[
				'Design controls',
				'Colours, typography, position, animation and timing',
				'Included',
				true,
			],
			[ 'Custom Google Fonts', 'Included', 'Included', true ],
			[ 'Custom font uploads', '—', 'Site-wide font library' ],
			[
				'Display targeting',
				'Storefront page types',
				'Advanced rules and scheduling',
			],
			[
				'Analytics',
				'Views, clicks, dismissals and CTR over 7 to 365 days',
				'Attributed orders, revenue and CSV export',
			],
			[
				'Sales momentum',
				'Recent individual purchases',
				'Product sales totals and milestones',
			],
			[
				'WordPress dashboard widget',
				'7-day analytics widget',
				'Included',
				true,
			],
			[ 'Import and export', 'Included', 'Included', true ],
			[ 'Experiments', '—', 'A/B campaign experiments' ],
			[
				'Integrations',
				'WooCommerce orders',
				'Secure external sale webhooks',
			],
			[
				'Team controls',
				'Administrator and shop manager',
				'Granular campaign roles',
			],
			[ 'Support', 'Community support', 'Priority product support' ],
		];
		return (
			'<section class="voidpulse-upgrade"><div class="voidpulse-upgrade-hero"><span class="voidpulse-eyebrow">VoidPulse Pro</span><h2>Turn more store activity into confident purchases</h2><p>Unlock unlimited campaigns, advanced targeting and deeper analytics as your store grows.</p><a class="button button-primary voidpulse-upgrade-cta" href="' +
			esc( cfg.upgradeUrl || '#' ) +
			'">Upgrade to Pro</a><small>Secure checkout and licence management by Freemius.</small></div><div class="voidpulse-comparison-wrap"><table class="voidpulse-comparison"><thead><tr><th scope="col">Feature</th><th scope="col">Free</th><th scope="col"><span>Recommended</span>Pro</th></tr></thead><tbody>' +
			features
				.map( function ( feature ) {
					return (
						'<tr><th scope="row">' +
						esc( feature[ 0 ] ) +
						'</th><td class="' +
						( feature[ 3 ] ? 'is-included' : '' ) +
						'">' +
						( feature[ 3 ] ? icon( 'check' ) : '' ) +
						esc( feature[ 1 ] ) +
						'</td><td>' +
						icon( 'check' ) +
						esc( feature[ 2 ] ) +
						'</td></tr>'
					);
				} )
				.join( '' ) +
			'</tbody><tfoot><tr><th scope="row">Choose your plan</th><td><strong>Free</strong><small>For getting started</small></td><td><a class="button button-primary" href="' +
			esc( cfg.upgradeUrl || '#' ) +
			'">Upgrade to Pro</a></td></tr></tfoot></table></div></section>'
		);
	}
	function loadingSkeleton() {
		const statCards = Array.from( { length: 4 }, function () {
			return '<div class="voidpulse-skeleton-card voidpulse-skeleton-stat"><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span></div>';
		} ).join( '' );
		return (
			'<div class="voidpulse-admin-skeleton" aria-busy="true" aria-label="Preparing Social Proof Notifications"><div aria-hidden="true">' +
			'<div class="voidpulse-skeleton-brand"><span class="voidpulse-skeleton-block voidpulse-skeleton-logo"></span><div class="voidpulse-skeleton-brand-copy"><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span></div></div>' +
			'<div class="voidpulse-skeleton-tabs"><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span></div>' +
			'<div class="voidpulse-skeleton-stats">' +
			statCards +
			'</div><div class="voidpulse-skeleton-dashboard"><div class="voidpulse-skeleton-card voidpulse-skeleton-setup"><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span></div><div class="voidpulse-skeleton-card voidpulse-skeleton-campaign"><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span><span class="voidpulse-skeleton-block"></span></div></div>' +
			'</div></div>'
		);
	}
	function render() {
		resetBasicAnalytics();
		if ( ! state.campaign ) {
			root.innerHTML = loadingSkeleton();
			return;
		}
		const canUpgrade = ! cfg.pro;
		if ( state.tab === 'upgrade' && ! canUpgrade ) {
			state.tab = 'dashboard';
		}
		const tabs = [
			[ 'dashboard', 'Dashboard' ],
			[ 'campaigns', cfg.pro ? 'Campaigns' : 'Campaign editor' ],
			[ 'analytics', 'Analytics' ],
			[ 'settings', 'Settings' ],
		];
		if ( canUpgrade ) {
			tabs.push( [ 'upgrade', 'Upgrade' ] );
		}
		const content = {
			dashboard,
			campaigns: builder,
			analytics,
			settings,
			upgrade,
		}[ state.tab ]();
		root.innerHTML =
			'<header class="voidpulse-header"><div class="voidpulse-brand"><img class="voidpulse-brand-icon" src="' +
			esc( cfg.iconUrl ) +
			'" alt=""><div><span class="voidpulse-eyebrow">WooCommerce social proof</span><h1>Social Proof Notifications</h1><p>Genuine store activity, clearly presented.</p></div></div></header><nav class="voidpulse-tabs" aria-label="Social Proof Notifications sections">' +
			tabs
				.map( function ( tab ) {
					return (
						'<button data-tab="' +
						tab[ 0 ] +
						'" class="' +
						( state.tab === tab[ 0 ] ? 'active' : '' ) +
						'">' +
						tab[ 1 ] +
						'</button>'
					);
				} )
				.join( '' ) +
			'</nav>' +
			'<main>' +
			content +
			'</main>' +
			toast();
		bind();
		armToast();
		if ( state.tab === 'analytics' && ! cfg.pro ) {
			mountBasicAnalyticsOnDemand();
		}
		if ( state.tab === 'settings' && ! state.diagnostics ) {
			loadDiagnostics();
		}
		if ( state.tab === 'settings' && ! state.dataLifecycle ) {
			loadDataLifecycle();
		}
		window.dispatchEvent( new CustomEvent( 'voidpulse:render' ) );
	}
	function loadDiagnostics() {
		if ( state.diagnosticsLoading ) {
			return;
		}
		state.diagnosticsLoading = true;
		api( 'diagnostics' )
			.then( function ( data ) {
				state.diagnostics = data;
			} )
			.catch( function ( error ) {
				state.diagnostics = { error: error.message };
			} )
			.finally( function () {
				state.diagnosticsLoading = false;
				if ( state.tab === 'settings' ) {
					render();
				}
			} );
	}
	function loadDataLifecycle() {
		if ( state.dataLifecycleLoading ) {
			return;
		}
		state.dataLifecycleLoading = true;
		api( 'data-lifecycle' )
			.then( function ( data ) {
				state.dataLifecycle = data;
			} )
			.catch( function ( error ) {
				state.notice = error.message;
				state.noticeType = 'error';
			} )
			.finally( function () {
				state.dataLifecycleLoading = false;
				if ( state.tab === 'settings' ) {
					render();
				}
			} );
	}
	function save( status, successMessage = '' ) {
		state.campaign.status = status;
		api( 'campaign', {
			method: 'POST',
			body: JSON.stringify( { campaign: state.campaign } ),
		} )
			.then( function ( result ) {
				state.campaign = result.campaign;
				state.dirty = false;
				state.notice =
					successMessage ||
					( status === 'active'
						? 'Campaign published.'
						: 'Draft saved.' );
				state.noticeType = 'success';
				render();
			} )
			.catch( function ( error ) {
				state.notice = error.message;
				state.noticeType = 'error';
				render();
			} );
	}
	function bind() {
		root.querySelectorAll( '[data-tab]' ).forEach( function ( node: any ) {
			node.onclick = function () {
				state.tab = node.dataset.tab;
				render();
			};
		} );
		root.querySelectorAll( '[data-key]' ).forEach( function ( node: any ) {
			if ( node.type === 'hidden' ) {
				return;
			}
			const applyFieldValue = function () {
				let value = node.value;
				if ( node.type === 'checkbox' ) {
					value = node.checked;
				} else if ( node.type === 'number' ) {
					value = Number( node.value );
				} else if (
					node.tagName === 'SELECT' &&
					/_(weight|size)$/.test( String( node.dataset.key || '' ) )
				) {
					value = Number( node.value );
				}
				set( node.dataset.key, value );
				if ( node.dataset.key === 'style.message_size' ) {
					set( 'style.font_size', value );
				}
				if ( node.dataset.key === 'orders.product_rule' ) {
					state.productSearchQuery = '';
					state.productSearchResults = [];
					state.productSearchFocused = false;
					render();
					return;
				}
				// These toggles add or remove their own typography controls.
				if (
					node.dataset.key === 'style.show_label' ||
					node.dataset.key === 'style.show_headline'
				) {
					render();
					return;
				}
				if (
					node.classList.contains( 'voidpulse-color-swatch' ) ||
					( node.classList.contains( 'voidpulse-color-hex' ) &&
						/^#[0-9a-f]{6}$/i.test( value ) )
				) {
					root.querySelectorAll(
						'[data-key="' + node.dataset.key + '"]'
					).forEach( function ( matching: any ) {
						if ( matching !== node ) {
							matching.value = value;
						}
					} );
				}
				if ( state.tab === 'campaigns' ) {
					const stage = root.querySelector(
						'.voidpulse-preview-stage'
					);
					if ( stage ) {
						stage.outerHTML = preview();
					} else {
						syncPreviewFonts();
					}
				}
			};
			node.oninput = applyFieldValue;
			node.onchange = applyFieldValue;
		} );
		root.querySelectorAll( '.voidpulse-font-picker' ).forEach( function (
			picker: any
		) {
			const search = picker.querySelector(
				'.voidpulse-font-picker-search'
			);
			const hidden = picker.querySelector( 'input[type="hidden"]' );
			const menu = picker.querySelector( '.voidpulse-font-picker-menu' );
			if ( ! search || ! hidden || ! menu ) {
				return;
			}
			function closeMenu() {
				menu.hidden = true;
				menu.innerHTML = '';
				search.value = hidden.value || '';
			}
			function applyFont( family ) {
				const key = picker.dataset.fontKey;
				const role = String( key || '' )
					.replace( 'style.', '' )
					.replace( '_font', '' );
				hidden.value = family;
				search.value = family;
				set( key, family );
				const weightKey = 'style.' + role + '_weight';
				const current = sanitizeWeight( get( weightKey ), 500 );
				const weights = availableWeights( family, current );
				if ( weights.indexOf( current ) < 0 ) {
					set(
						weightKey,
						weights.indexOf( 400 ) >= 0 ? 400 : weights[ 0 ]
					);
				}
				menu.hidden = true;
				render();
			}
			function renderMenu( query ) {
				const needle = String( query || '' )
					.trim()
					.toLowerCase();
				const browserMatches = BROWSER_FONTS.filter(
					function ( entry ) {
						if ( ! needle ) {
							return true;
						}
						return (
							entry.family.toLowerCase().includes( needle ) ||
							entry.label.toLowerCase().includes( needle )
						);
					}
				);
				const matches = filterFontFamilies(
					googleFontsCatalog,
					query || ''
				);
				let html =
					'<button type="button" class="voidpulse-font-picker-option' +
					( ! hidden.value ? ' is-selected' : '' ) +
					'" data-font-family="">System default</button>';
				if ( browserMatches.length ) {
					html +=
						'<span class="voidpulse-font-picker-section">Browser fonts</span>';
					html += browserMatches
						.map( function ( entry ) {
							return (
								'<button type="button" class="voidpulse-font-picker-option' +
								( hidden.value === entry.family
									? ' is-selected'
									: '' ) +
								'" data-font-family="' +
								esc( entry.family ) +
								'" style="font-family:' +
								cssFontFamilyValue( entry.family ).replace(
									/"/g,
									"'"
								) +
								'">' +
								esc( entry.label ) +
								'</button>'
							);
						} )
						.join( '' );
				}
				if ( matches.length ) {
					html +=
						'<span class="voidpulse-font-picker-section">Google fonts</span>';
					html += matches
						.map( function ( entry ) {
							return (
								'<button type="button" class="voidpulse-font-picker-option' +
								( hidden.value === entry.family
									? ' is-selected'
									: '' ) +
								'" data-font-family="' +
								esc( entry.family ) +
								'" style="font-family:\'' +
								esc( entry.family ).replace( /'/g, '' ) +
								'\', sans-serif">' +
								esc( entry.family ) +
								'</button>'
							);
						} )
						.join( '' );
				}
				if ( ! browserMatches.length && ! matches.length && needle ) {
					html +=
						'<span class="voidpulse-font-picker-empty">No fonts match</span>';
				}
				menu.innerHTML = html;
				menu.hidden = false;
				menu.querySelectorAll( '[data-font-family]' ).forEach(
					function ( option: any ) {
						option.onclick = function () {
							applyFont(
								option.dataset.fontFamily || DEFAULT_FONT
							);
						};
					}
				);
			}
			search.onfocus = function () {
				renderMenu( search.value );
			};
			search.oninput = function () {
				renderMenu( search.value );
			};
			search.onkeydown = function ( event ) {
				if ( event.key === 'Escape' ) {
					closeMenu();
				}
				if ( event.key === 'Enter' ) {
					event.preventDefault();
					const needle = String( search.value || '' )
						.trim()
						.toLowerCase();
					if ( ! needle ) {
						applyFont( DEFAULT_FONT );
						return;
					}
					const browserExact = BROWSER_FONTS.find(
						function ( entry ) {
							return (
								entry.family.toLowerCase() === needle ||
								entry.label.toLowerCase() === needle
							);
						}
					);
					if ( browserExact ) {
						applyFont( browserExact.family );
						return;
					}
					const exact = filterFontFamilies(
						googleFontsCatalog,
						search.value
					).find( function ( entry ) {
						return entry.family.toLowerCase() === needle;
					} );
					if ( exact ) {
						applyFont( exact.family );
					}
				}
			};
		} );
		if ( ! fontPickerOutsideBound ) {
			fontPickerOutsideBound = true;
			document.addEventListener( 'mousedown', function ( event ) {
				root.querySelectorAll( '.voidpulse-font-picker' ).forEach(
					function ( picker: any ) {
						const menu = picker.querySelector(
							'.voidpulse-font-picker-menu'
						);
						if (
							! menu ||
							menu.hidden ||
							picker.contains( event.target as Node )
						) {
							return;
						}
						menu.hidden = true;
						menu.innerHTML = '';
						const search = picker.querySelector(
							'.voidpulse-font-picker-search'
						);
						if ( search ) {
							const hidden = picker.querySelector(
								'input[type="hidden"]'
							);
							search.value = ( hidden && hidden.value ) || '';
						}
					}
				);
			} );
		}
		root.querySelectorAll( '[data-preview]' ).forEach( function (
			node: any
		) {
			node.onclick = function () {
				state.preview = node.dataset.preview;
				render();
			};
		} );
		root.querySelectorAll( '[data-order-status]' ).forEach( function (
			node: any
		) {
			node.onchange = function () {
				const selected = Array.from(
					root.querySelectorAll( '[data-order-status]:checked' )
				).map( function ( item: any ) {
					return item.dataset.orderStatus;
				} );
				if ( ! selected.length ) {
					node.checked = true;
					return;
				}
				state.campaign.orders.statuses = selected;
				state.dirty = true;
				syncUnsavedIndicator();
			};
		} );
		const productSearch: any = root.querySelector(
			'[data-product-search]'
		);
		if ( productSearch ) {
			productSearch.oninput = function () {
				state.productSearchFocused = true;
				searchProducts( productSearch.value );
			};
			if ( state.productSearchFocused ) {
				productSearch.focus();
				productSearch.setSelectionRange(
					productSearch.value.length,
					productSearch.value.length
				);
			}
		}
		root.querySelectorAll( '[data-add-product]' ).forEach( function (
			node: any
		) {
			node.onclick = function () {
				const id = Number( node.dataset.addProduct );
				if (
					! id ||
					state.campaign.orders.product_ids
						.map( Number )
						.indexOf( id ) >= 0
				) {
					return;
				}
				if ( state.campaign.orders.product_ids.length >= 100 ) {
					state.notice =
						'Product rules support up to 100 selected products.';
					state.noticeType = 'error';
					render();
					return;
				}
				const product = state.productSearchResults.find(
					function ( item ) {
						return Number( item.id ) === id;
					}
				);
				state.campaign.orders.product_ids.push( id );
				if ( product ) {
					state.productRuleProducts.push( product );
				}
				state.productSearchQuery = '';
				state.productSearchResults = [];
				state.productSearchFocused = false;
				state.dirty = true;
				render();
			};
		} );
		root.querySelectorAll( '[data-remove-product]' ).forEach( function (
			node: any
		) {
			node.onclick = function () {
				const id = Number( node.dataset.removeProduct );
				state.campaign.orders.product_ids =
					state.campaign.orders.product_ids.filter(
						function ( productId ) {
							return Number( productId ) !== id;
						}
					);
				state.productRuleProducts = state.productRuleProducts.filter(
					function ( product ) {
						return Number( product.id ) !== id;
					}
				);
				state.dirty = true;
				render();
			};
		} );
		root.querySelectorAll( '[data-token]' ).forEach( function (
			node: any
		) {
			node.onclick = function () {
				state.campaign.message += ' {' + node.dataset.token + '}';
				state.dirty = true;
				render();
			};
		} );
		root.querySelectorAll( '[data-design-preset]' ).forEach( function (
			node: any
		) {
			node.onclick = function () {
				const preset = designPresets.find( function ( item ) {
					return item.id === node.dataset.designPreset;
				} );
				if ( ! preset ) {
					return;
				}
				state.campaign.style = Object.assign(
					{},
					state.campaign.style,
					{
						background: preset.style.background,
						text: preset.style.text,
						muted_text: preset.style.muted_text,
						accent: preset.style.accent,
						border: preset.style.border,
						label_color: preset.style.accent,
						headline_color: preset.style.text,
						message_color: preset.style.text,
					}
				);
				state.dirty = true;
				render();
			};
		} );
		const analyticsRangeNode: any = root.querySelector(
			'[data-analytics-range]'
		);
		if ( analyticsRangeNode ) {
			analyticsRangeNode.onchange = function () {
				const range = Number( analyticsRangeNode.value );
				if ( ! range || range === Number( state.analyticsRange ) ) {
					return;
				}
				state.analyticsRange = range;
				state.analyticsLoading = true;
				resetBasicAnalytics();
				render();
				api( 'analytics?range=' + range )
					.then( function ( data ) {
						state.analytics = data;
					} )
					.catch( function ( error ) {
						state.notice =
							error.message ||
							'The analytics range could not be loaded.';
						state.noticeType = 'error';
					} )
					.then( function () {
						state.analyticsLoading = false;
						render();
					} );
			};
		}
		root.querySelectorAll( '[data-lifecycle-key]' ).forEach( function (
			node: any
		) {
			node.onchange = function () {
				const key = node.dataset.lifecycleKey;
				state.dataLifecycle[ key ] =
					node.type === 'checkbox'
						? Boolean( node.checked )
						: Number( node.value );
			};
		} );
		const action = function ( name, fn ) {
			const node: any = root.querySelector(
				'[data-action="' + name + '"]'
			);
			if ( node ) {
				node.onclick = fn;
			}
		};
		action( 'edit', function () {
			state.tab = 'campaigns';
			render();
		} );
		action( 'upgrade', function () {
			state.tab = 'upgrade';
			render();
		} );
		action( 'dismiss-toast', dismissToast );
		action( 'pause', function () {
			save( 'paused' );
		} );
		action( 'activate', function () {
			save( 'active' );
		} );
		action( 'save', function () {
			save( 'draft' );
		} );
		action( 'save-settings', function () {
			save( state.campaign.status, 'Settings saved.' );
		} );
		action( 'publish', function () {
			save( 'active' );
		} );
		action( 'simulate', function () {
			const card: any = root.querySelector( '.voidpulse-preview-card' );
			if ( ! card ) {
				return;
			}
			card.classList.remove( 'is-simulating' );
			void card.offsetWidth;
			card.classList.add( 'is-simulating' );
		} );
		action( 'reset', function () {
			api( 'campaign/defaults' )
				.then( function ( defaults ) {
					state.campaign = defaults;
					state.productRuleProducts = [];
					state.productSearchQuery = '';
					state.productSearchResults = [];
					state.dirty = true;
					state.notice = 'Defaults restored. Save when ready.';
					state.noticeType = 'info';
					render();
				} )
				.catch( function ( error ) {
					state.notice = error.message;
					state.noticeType = 'error';
					render();
				} );
		} );
		action( 'export', function () {
			api( 'campaign/export' )
				.then( function ( doc ) {
					const blob = new Blob( [ JSON.stringify( doc, null, 2 ) ], {
						type: 'application/json',
					} );
					const link = document.createElement( 'a' );
					link.href = URL.createObjectURL( blob );
					link.download = 'voidpulse-campaign.json';
					link.click();
					URL.revokeObjectURL( link.href );
				} )
				.catch( function ( error ) {
					state.notice = error.message;
					state.noticeType = 'error';
					render();
				} );
		} );
		action( 'clear-cache', function () {
			api( 'cache', { method: 'DELETE' } )
				.then( function () {
					state.notice = 'Cached activity cleared.';
					state.noticeType = 'success';
					render();
				} )
				.catch( function ( error ) {
					state.notice = error.message;
					state.noticeType = 'error';
					render();
				} );
		} );
		action( 'repair-schedule', function () {
			api( 'schedule/repair', { method: 'POST' } )
				.then( function () {
					state.diagnostics = null;
					state.notice = 'Background refresh schedule repaired.';
					state.noticeType = 'success';
					render();
				} )
				.catch( function ( error ) {
					state.notice = error.message;
					state.noticeType = 'error';
					render();
				} );
		} );
		action( 'save-data-lifecycle', function () {
			api( 'data-lifecycle', {
				method: 'POST',
				body: JSON.stringify( {
					settings: {
						delete_on_uninstall:
							state.dataLifecycle.delete_on_uninstall,
						retention_days: state.dataLifecycle.retention_days,
					},
				} ),
			} )
				.then( function ( data ) {
					state.dataLifecycle = data;
					state.notice = 'Data settings saved.';
					state.noticeType = 'success';
					render();
				} )
				.catch( function ( error ) {
					state.notice = error.message;
					state.noticeType = 'error';
					render();
				} );
		} );
		action( 'clear-analytics', function () {
			state.analyticsConfirm = true;
			render();
		} );
		action( 'cancel-clear-analytics', function () {
			state.analyticsConfirm = false;
			render();
		} );
		action( 'confirm-clear-analytics', function () {
			api( 'analytics/data', {
				method: 'DELETE',
				body: JSON.stringify( { confirm: true } ),
			} )
				.then( function () {
					return Promise.all( [
						api( 'analytics?range=' + state.analyticsRange ),
						api( 'data-lifecycle' ),
					] );
				} )
				.then( function ( values ) {
					state.analytics = values[ 0 ];
					state.dataLifecycle = values[ 1 ];
					state.analyticsConfirm = false;
					state.notice = 'Analytics history cleared.';
					state.noticeType = 'success';
					render();
				} )
				.catch( function ( error ) {
					state.notice = error.message;
					state.noticeType = 'error';
					render();
				} );
		} );
		const importer: any = root.querySelector( '[data-import]' );
		if ( importer ) {
			importer.onchange = function () {
				const file = importer.files[ 0 ];
				if ( ! file || file.size > 1048576 ) {
					return;
				}
				file.text()
					.then( JSON.parse )
					.then( function ( document ) {
						return api( 'campaign/import', {
							method: 'POST',
							body: JSON.stringify( { document } ),
						} );
					} )
					.then( function ( result ) {
						state.campaign = result.campaign;
						state.productRuleProducts = [];
						state.dirty = false;
						state.notice = 'Campaign imported as a draft.';
						state.noticeType = 'success';
						render();
						loadProductRuleProducts();
					} )
					.catch( function ( error ) {
						state.notice = error.message;
						state.noticeType = 'error';
						render();
					} );
			};
		}
	}
	window.addEventListener( 'beforeunload', function ( event ) {
		if ( state.dirty ) {
			event.preventDefault();
			event.returnValue = '';
		}
	} );
	Promise.all( [
		api( 'campaign' ),
		api( 'analytics?range=' + state.analyticsRange ),
	] )
		.then( function ( values ) {
			state.campaign = values[ 0 ];
			state.analytics = values[ 1 ];
			render();
			loadProductRuleProducts();
		} )
		.catch( function ( error ) {
			root.innerHTML =
				'<div class="voidpulse-load-error" role="alert"><strong>Social Proof Notifications could not load</strong><p>' +
				esc( error.message ) +
				'</p><a class="button" href="' +
				esc( window.location.href ) +
				'">Try again</a></div>';
		} );
} )();
