/* GravityOps Search — Shortcode Builder */
/* global jQuery */
(function () {
	'use strict';

	/* ------------------------------------------------------------------ */
	/* Shared: runs on builder tab AND saved shortcodes tab                */
	/* ------------------------------------------------------------------ */

	function copyToClipboard(text, onSuccess) {
		if (navigator.clipboard) {
			navigator.clipboard
				.writeText(text)
				.then(onSuccess)
				.catch(function () {});
		}
	}

	document.querySelectorAll('.gospb-saved-code').forEach(function (el) {
		el.addEventListener('click', function () {
			copyToClipboard(el.textContent, function () {
				el.classList.add('gospb-copied');
				setTimeout(function () {
					el.classList.remove('gospb-copied');
				}, 500);
			});
		});
	});

	document.querySelectorAll('[data-gospb-edit]').forEach(function (btn) {
		btn.addEventListener('click', function () {
			try {
				localStorage.setItem('gospb_draft', btn.dataset.gospbEdit);
			} catch (e) {}
			window.location.href = btn.dataset.gospbBuilderUrl;
		});
	});

	document.querySelectorAll('.gospb-delete-sc').forEach(function (btn) {
		btn.addEventListener('click', function () {
			if (!confirm('Delete this saved shortcode?')) {
				return;
			}
			const body = new URLSearchParams();
			body.append('action', 'gospb_delete_shortcode');
			body.append('nonce', btn.dataset.nonce);
			body.append('id', btn.dataset.id);
			fetch(btn.dataset.ajax, {
				method: 'POST',
				credentials: 'same-origin',
				body,
			}).then(function () {
				btn.closest('.gospb-saved-card').remove();
			});
		});
	});

	/* ------------------------------------------------------------------ */
	/* Builder: only runs when #gospb-app is present                        */
	/* ------------------------------------------------------------------ */

	const root = document.getElementById('gospb-app');
	if (!root) {
		return;
	}

	const AJAX = root.dataset.ajax;
	const NONCE = root.dataset.nonce;

	/* ------------------------------------------------------------------ */
	/* Constants                                                            */
	/* ------------------------------------------------------------------ */

	const ENTRY_PROPERTIES = [
		{ id: 'id', label: 'Entry ID', type: 'number' },
		{ id: 'form_id', label: 'Form ID', type: 'number' },
		{ id: 'date_created', label: 'Date Created', type: 'date' },
		{ id: 'date_updated', label: 'Date Updated', type: 'date' },
		{ id: 'created_by', label: 'Created By (User)', type: 'number' },
		{ id: 'is_starred', label: 'Is Starred', type: 'boolean' },
		{ id: 'is_read', label: 'Is Read', type: 'boolean' },
		{ id: 'ip', label: 'IP Address', type: 'text' },
		{ id: 'source_url', label: 'Source URL', type: 'text' },
		{ id: 'user_agent', label: 'User Agent', type: 'text' },
		{ id: 'payment_status', label: 'Payment Status', type: 'text' },
		{ id: 'payment_date', label: 'Payment Date', type: 'date' },
		{ id: 'payment_amount', label: 'Payment Amount', type: 'number' },
		{ id: 'transaction_id', label: 'Transaction ID', type: 'text' },
		{ id: 'currency', label: 'Currency', type: 'text' },
		{ id: 'status', label: 'Status', type: 'text' },
	];

	const ALL_OPERATORS = [
		{ value: '=', label: '= (equals)' },
		{ value: '!=', label: '!= (not equal)' },
		{ value: 'contains', label: 'contains' },
		{ value: 'like', label: 'like (SQL wildcards)' },
		{ value: 'in', label: 'in (array)' },
		{ value: 'not in', label: 'not in (array)' },
		{ value: 'gt', label: '> (greater than)' },
		{ value: 'lt', label: '< (less than)' },
		{ value: 'gt=', label: '>= (greater or equal)' },
		{ value: 'lt=', label: '<= (less or equal)' },
	];

	function operatorsForType(type) {
		switch (type) {
			case 'number':
			case 'currency':
				return ['=', '!=', 'gt', 'lt', 'gt=', 'lt=', 'in', 'not in'];
			case 'date':
			case 'datetime':
				return ['=', '!=', 'gt', 'lt', 'gt=', 'lt='];
			case 'select':
			case 'radio':
			case 'checkbox':
			case 'multi_choice':
			case 'image_choice':
				return ['=', '!=', 'in', 'not in', 'contains'];
			case 'boolean':
				return ['=', '!='];
			default:
				return ['=', '!=', 'contains', 'like', 'in', 'not in'];
		}
	}

	const HTML_TAGS = [
		{ value: '', label: '— No wrapper —' },
		{ value: 'a', label: '<a> link' },
		{ value: 'span', label: '<span>' },
		{ value: 'strong', label: '<strong>' },
		{ value: 'em', label: '<em>' },
		{ value: 'div', label: '<div>' },
		{ value: 'p', label: '<p>' },
		{ value: 'li', label: '<li> (list item)' },
		{ value: 'tr', label: '<tr> (table row)' },
		{ value: 'td', label: '<td> (table cell)' },
		{ value: 'th', label: '<th> (table header)' },
		{ value: 'h3', label: '<h3>' },
		{ value: 'h4', label: '<h4>' },
		{ value: 'br', label: '<br> (self-closing)' },
	];

	const SEPARATOR_PRESETS = [
		{ value: '', label: '— Default —' },
		{ value: ', ', label: 'Comma (, )' },
		{ value: '; ', label: 'Semicolon (; )' },
		{ value: ' ', label: 'Space' },
		{ value: '<br>', label: '<br> line break' },
		{ value: '</li><li>', label: '</li><li> (between list items)' },
		{
			value: '</td></tr><tr><td>',
			label: '</td></tr><tr><td> (between rows)',
		},
		{ value: '__none__', label: '__none__ (no separator)' },
	];

	const STARTER_TEMPLATES = [
		{
			name: 'Latest entry by email',
			state() {
				return Object.assign({}, defaultState(), {
					target: { mode: 'forms', form_ids: [] },
					search_rows: [{ field_id: '', operator: '=', value: '' }],
					sort: {
						key: 'id',
						direction: 'DESC',
						is_num: true,
						secondary_key: '',
						secondary_direction: 'DESC',
					},
					limit: '1',
					display: {
						mode: 'simple',
						simple_field_ids: ['id'],
						template_blocks: [],
					},
				});
			},
		},
		{
			name: 'Count of matching entries',
			state() {
				return Object.assign({}, defaultState(), {
					limit: 'all',
					display: {
						mode: 'template',
						simple_field_ids: [],
						template_blocks: [
							{ type: 'text', value: 'Total: ' },
							{ type: 'special', value: 'num_results' },
						],
					},
				});
			},
		},
		{
			name: 'Discover meta keys for a form',
			state() {
				return Object.assign({}, defaultState(), {
					display: {
						mode: 'simple',
						simple_field_ids: ['meta'],
						template_blocks: [],
					},
				});
			},
		},
	];

	/* ------------------------------------------------------------------ */
	/* State                                                                */
	/* ------------------------------------------------------------------ */

	function defaultState() {
		return {
			target: { mode: 'forms', form_ids: [] },
			search_rows: [],
			search_mode: 'all',
			search_empty: false,
			field_match: 'first',
			sort: {
				key: 'id',
				direction: 'DESC',
				is_num: true,
				secondary_key: '',
				secondary_direction: 'DESC',
			},
			limit: '1',
			unique: false,
			separator: '',
			default_values: [''],
			link: false,
			display: {
				mode: 'simple',
				simple_field_ids: [],
				template_blocks: [],
			},
		};
	}

	let state = loadDraft() || defaultState();
	let formsList = [];
	const schemaCache = {};
	let presets = {};

	function saveDraft() {
		try {
			localStorage.setItem('gospb_draft', JSON.stringify(state));
		} catch (e) {}
	}

	function loadDraft() {
		try {
			return JSON.parse(localStorage.getItem('gospb_draft') || 'null');
		} catch (e) {
			return null;
		}
	}

	/* ------------------------------------------------------------------ */
	/* AJAX helpers                                                         */
	/* ------------------------------------------------------------------ */

	function ajax(action, data) {
		const body = new URLSearchParams();
		body.append('action', action);
		body.append('nonce', NONCE);
		Object.keys(data || {}).forEach(function (k) {
			body.append(k, data[k]);
		});
		return fetch(AJAX, { method: 'POST', credentials: 'same-origin', body })
			.then(function (r) {
				return r.json();
			})
			.then(function (j) {
				if (!j || !j.success) {
					throw new Error(j && j.data ? j.data : 'ajax_error');
				}
				return j.data;
			});
	}

	/* ------------------------------------------------------------------ */
	/* Schema helpers                                                       */
	/* ------------------------------------------------------------------ */

	function loadForms() {
		return ajax('gospb_list_forms', {}).then(function (list) {
			formsList = list || [];
		});
	}

	function loadSchema(formId) {
		formId = parseInt(formId, 10);
		if (!formId || formId <= 0) {
			return Promise.resolve(null);
		}
		if (schemaCache[formId]) {
			return Promise.resolve(schemaCache[formId]);
		}
		return ajax('gospb_get_form', { form_id: formId }).then(function (s) {
			schemaCache[formId] = s;
			return s;
		});
	}

	function primaryFormId() {
		if (state.target.mode === 'all') {
			return null;
		}
		return state.target.form_ids.length
			? parseInt(state.target.form_ids[0], 10)
			: null;
	}

	function primarySchema() {
		const id = primaryFormId();
		return id ? schemaCache[id] : null;
	}

	function allSelectedSchemas() {
		return (state.target.form_ids || [])
			.map(function (id) {
				return schemaCache[id] || null;
			})
			.filter(Boolean);
	}

	function getUniqueFieldTypes(schemas) {
		const seen = {};
		schemas.forEach(function (schema) {
			(schema.fields || []).forEach(function (f) {
				if (f.type) {
					seen[f.type] = true;
				}
			});
		});
		return Object.keys(seen).sort();
	}

	function fieldOptionsForSchema(schema, opts) {
		opts = opts || {};
		const includeGlobal = !!opts.includeGlobal;
		const out = [];
		if (includeGlobal) {
			out.push({
				value: '',
				label: '— Search ANY field (global) —',
				type: 'text',
				group: 'Special',
			});
		}
		ENTRY_PROPERTIES.forEach(function (p) {
			out.push({
				value: p.id,
				label: p.label + ' [' + p.id + ']',
				type: p.type,
				group: 'Entry properties',
			});
		});
		if (!schema) {
			return out;
		}
		const groupName = 'Fields — ' + schema.title;
		schema.fields.forEach(function (f) {
			const baseLabel = (f.label || '(no label)') + ' [' + f.id + ']';
			if (
				(f.type === 'name' || f.type === 'address') &&
				f.inputs &&
				f.inputs.length
			) {
				f.inputs.forEach(function (inp) {
					out.push({
						value: inp.id,
						label:
							(f.label || f.type) +
							' — ' +
							inp.label +
							' [' +
							inp.id +
							']',
						type: 'text',
						group: groupName,
						isMulti: true,
						parentLabel: f.label,
					});
				});
				out.push({
					value: f.id,
					label: baseLabel + ' (combined)',
					type: 'text',
					group: groupName,
					isMulti: true,
					displayOnly: true,
				});
			} else if (f.type === 'checkbox' || f.inputType === 'checkbox') {
				out.push({
					value: f.id,
					label: baseLabel + ' (checkbox)',
					type: 'checkbox',
					group: groupName,
					isMulti: true,
					choices: f.choices,
				});
			} else {
				out.push({
					value: f.id,
					label: baseLabel,
					type: f.type || 'text',
					group: groupName,
					choices: f.choices,
				});
			}
		});
		if (schema.meta_keys && schema.meta_keys.length) {
			const metaGroup = 'Entry Meta — ' + schema.title;
			schema.meta_keys.forEach(function (key) {
				out.push({
					value: key,
					label: key,
					type: 'text',
					group: metaGroup,
				});
			});
		}
		return out;
	}

	function fieldMetaById(schema, id) {
		return fieldOptionsForSchema(schema).find(function (o) {
			return o.value === String(id);
		});
	}

	/* ------------------------------------------------------------------ */
	/* Serialiser                                                           */
	/* ------------------------------------------------------------------ */

	function attr(name, value) {
		if (
			value === '' ||
			value === null ||
			value === undefined ||
			value === false
		) {
			return '';
		}
		const s = String(value);
		if (s.indexOf('"') === -1) {
			return ' ' + name + '="' + s + '"';
		}
		if (s.indexOf("'") === -1) {
			return ' ' + name + "='" + s + "'";
		}
		return ' ' + name + '="' + s.replace(/"/g, '') + '"';
	}

	function buildSearchString() {
		return state.search_rows
			.filter(function (r) {
				return (r.field_mode || 'id') === 'id';
			})
			.map(function (r) {
				return r.field_id || '';
			})
			.join(',');
	}

	function buildLabelString() {
		return state.search_rows
			.filter(function (r) {
				return r.field_mode === 'label';
			})
			.map(function (r) {
				return r.field_id || '';
			})
			.join(',');
	}

	function buildTypeString() {
		return state.search_rows
			.filter(function (r) {
				return r.field_mode === 'type';
			})
			.map(function (r) {
				return r.field_id || '';
			})
			.join(',');
	}

	function buildOperatorsString() {
		const idRows = state.search_rows.filter(function (r) {
			return (r.field_mode || 'id') === 'id';
		});
		const labelRows = state.search_rows.filter(function (r) {
			return r.field_mode === 'label';
		});
		const typeRows = state.search_rows.filter(function (r) {
			return r.field_mode === 'type';
		});
		const ops = idRows
			.concat(labelRows)
			.concat(typeRows)
			.map(function (r) {
				return r.operator || '=';
			});
		if (
			ops.every(function (o) {
				return o === '=';
			})
		) {
			return '';
		}
		return ops.join(',');
	}

	function parseArrayValue(val) {
		if (Array.isArray(val)) {
			return val;
		}
		return val ? String(val).split(',') : [];
	}

	function buildContent() {
		const idRows = state.search_rows.filter(function (r) {
			return (r.field_mode || 'id') === 'id';
		});
		const labelRows = state.search_rows.filter(function (r) {
			return r.field_mode === 'label';
		});
		const typeRows = state.search_rows.filter(function (r) {
			return r.field_mode === 'type';
		});
		return idRows
			.concat(labelRows)
			.concat(typeRows)
			.map(function (r) {
				if (r.operator === 'in' || r.operator === 'not in') {
					const items = parseArrayValue(r.value);
					const cleaned = items
						.map(function (s) {
							return String(s).replace(/'/g, '').trim();
						})
						.filter(Boolean);
					return "array('" + cleaned.join("','") + "')";
				}
				return r.value === null || r.value === undefined
					? ''
					: String(r.value);
			})
			.join('|');
	}

	function buildDisplay() {
		const d = state.display;
		if (d.mode === 'simple') {
			return (d.simple_field_ids || []).filter(Boolean).join(',');
		}
		return d.template_blocks.map(renderTemplateBlock).join('');
	}

	function buildAttrString(attrs) {
		let s = '';
		Object.keys(attrs || {}).forEach(function (k) {
			if (
				attrs[k] === '' ||
				attrs[k] === null ||
				attrs[k] === undefined
			) {
				return;
			}
			s += ' ' + k + "='" + String(attrs[k]).replace(/'/g, '') + "'";
		});
		return s;
	}

	function renderTemplateBlock(b) {
		switch (b.type) {
			case 'text':
				return b.value || '';
			case 'placeholder':
				return renderPlaceholder(b);
			case 'special':
				return '{' + (b.value || 'num_results') + '}';
			case 'html': {
				const tag = b.tag || 'span';
				if (!tag) {
					return (b.children || []).map(renderTemplateBlock).join('');
				}
				const attrs = b.attrs || {};
				const attrStr = buildAttrString(attrs);
				if (b.selfClosing || tag === 'br') {
					return '<' + tag + attrStr + ' />';
				}
				const inner = (b.children || [])
					.map(renderTemplateBlock)
					.join('');
				return '<' + tag + attrStr + '>' + inner + '</' + tag + '>';
			}
			case 'shortcode':
				return renderNestedShortcode(b);
			default:
				return '';
		}
	}

	function renderPlaceholder(b) {
		const id = String(b.field_id || '');
		if (!id) {
			return '';
		}
		const def = b.default ? ';' + b.default : '';
		let format = b.format || 'auto';
		if (format === 'auto') {
			format = /^\d+(\.\d+)?$/.test(id) ? 'curly' : 'gos_curly';
		}
		switch (format) {
			case 'gos_curly':
				return '{gos:' + id + def + '}';
			case 'gos_plain':
				return 'gos:' + id;
			case 'curly':
			default:
				return '{' + id + def + '}';
		}
	}

	function renderNestedShortcode(b) {
		const tag = b.tag || 'gravops_search';
		const attrs = b.attrs || {};
		const inner = b.inner_template || '';
		const attrStr = buildAttrString(attrs);
		if (b.selfClosing) {
			return '{{' + tag + attrStr + '}}';
		}
		return '{{' + tag + attrStr + '}}' + inner + '{{/' + tag + '}}';
	}

	function buildShortcode() {
		const a = [];
		let target;
		if (state.target.mode === 'all' || !state.target.form_ids.length) {
			target = '0';
		} else {
			target = state.target.form_ids.join(',');
		}
		a.push(attr('target', target));

		const searchStr = buildSearchString();
		if (searchStr) {
			a.push(attr('search', searchStr));
		}
		const labelStr = buildLabelString();
		if (labelStr) {
			a.push(attr('search_label', labelStr));
		}
		const typeStr = buildTypeString();
		if (typeStr) {
			a.push(attr('search_type', typeStr));
		}
		const opStr = buildOperatorsString();
		if (opStr) {
			a.push(attr('operators', opStr));
		}
		const hasLabelOrType = state.search_rows.some(function (r) {
			return r.field_mode === 'label' || r.field_mode === 'type';
		});
		if (
			hasLabelOrType &&
			state.field_match &&
			state.field_match !== 'first'
		) {
			a.push(attr('field_match', state.field_match));
		}
		if (state.search_mode && state.search_mode !== 'all') {
			a.push(attr('search_mode', state.search_mode));
		}
		if (state.search_empty) {
			a.push(attr('search_empty', 'true'));
		}

		const display = buildDisplay();
		if (display) {
			a.push(attr('display', display));
		}

		if (state.sort.key && state.sort.key !== 'id') {
			a.push(attr('sort_key', state.sort.key));
		}
		if (state.sort.direction && state.sort.direction !== 'DESC') {
			a.push(attr('sort_direction', state.sort.direction));
		}
		if (state.sort.is_num === false) {
			a.push(attr('sort_is_num', 'false'));
		}
		if (state.sort.secondary_key) {
			a.push(attr('secondary_sort_key', state.sort.secondary_key));
			if (
				state.sort.secondary_direction &&
				state.sort.secondary_direction !== 'DESC'
			) {
				a.push(
					attr(
						'secondary_sort_direction',
						state.sort.secondary_direction
					)
				);
			}
		}

		if (state.limit && state.limit !== '1') {
			a.push(attr('limit', state.limit));
		}
		if (state.unique) {
			a.push(attr('unique', 'true'));
		}
		if (state.separator) {
			a.push(attr('separator', state.separator));
		}

		const defs = (state.default_values || []).filter(function (v) {
			return v !== '' && v !== null && v !== undefined;
		});
		if (defs.length) {
			a.push(attr('default', defs.join('||')));
		}
		if (state.link) {
			a.push(attr('link', 'true'));
		}

		const content = buildContent();
		const opening = '[gravops_search' + a.join('') + ']';
		if (!content && !searchStr) {
			return opening + '[/gravops_search]';
		}
		return opening + content + '[/gravops_search]';
	}

	/* ------------------------------------------------------------------ */
	/* Validator                                                            */
	/* ------------------------------------------------------------------ */

	function validate() {
		const warns = [];
		const schema = primarySchema();

		if (
			state.target.mode === 'forms' &&
			state.target.form_ids.length === 0
		) {
			warns.push({
				level: 'error',
				msg: 'Pick at least one form, or switch to "All Forms".',
			});
		}
		if (state.target.mode === 'forms' && state.target.form_ids.length > 1) {
			const primaryId = state.target.form_ids[0];
			const primaryMeta = formsList.find(function (f) {
				return f.id === primaryId;
			});
			const primaryName = primaryMeta
				? '"' + primaryMeta.title + '" (#' + primaryId + ')'
				: '#' + primaryId;
			warns.push({
				level: 'info',
				msg:
					'Multi-form target: field-aware UI features use ' +
					primaryName +
					' as the primary form. Ensure field IDs mean the same thing across all selected forms.',
			});
		}

		const hasLabelOrTypeRows = state.search_rows.some(function (r) {
			return r.field_mode === 'label' || r.field_mode === 'type';
		});
		if (
			hasLabelOrTypeRows &&
			state.target.mode === 'forms' &&
			state.target.form_ids.length > 1 &&
			state.search_mode !== 'any'
		) {
			warns.push({
				level: 'warn',
				msg: 'Label/type filters across multiple forms resolve to different field IDs per form — consider setting match mode to "ANY condition" so entries match whichever form\'s field resolves.',
			});
		}

		state.search_rows.forEach(function (r, i) {
			if (
				(r.field_mode === 'label' || r.field_mode === 'type') &&
				!String(r.field_id || '').trim()
			) {
				warns.push({
					level: 'error',
					msg:
						'Search row #' +
						(i + 1) +
						' (By ' +
						(r.field_mode === 'label' ? 'Label' : 'Type') +
						') has no ' +
						(r.field_mode === 'label' ? 'label' : 'type') +
						' set.',
				});
			}
			if (typeof r.value === 'string' && r.value.indexOf('|') !== -1) {
				warns.push({
					level: 'error',
					msg:
						'Search row #' +
						(i + 1) +
						' value contains a pipe (|). The shortcode cannot escape pipes — this will produce wrong results.',
				});
			}
			if (r.operator === 'in' || r.operator === 'not in') {
				const items = parseArrayValue(r.value);
				if (!items.length) {
					warns.push({
						level: 'warn',
						msg:
							'Search row #' +
							(i + 1) +
							' uses "' +
							r.operator +
							'" but has no values.',
					});
				}
				items.forEach(function (v) {
					if (String(v).indexOf("'") !== -1) {
						warns.push({
							level: 'warn',
							msg:
								'Search row #' +
								(i + 1) +
								' has a value containing a single quote. Single quotes will be stripped.',
						});
					}
				});
			}
		});

		if (state.display.mode === 'template') {
			const blocks = state.display.template_blocks;
			const firstPh = blocks.find(function (b) {
				return b.type === 'placeholder';
			});
			if (firstPh && !firstPh.default) {
				warns.push({
					level: 'warn',
					msg:
						'First placeholder ({' +
						firstPh.field_id +
						'}) has no default. Entries where this resolves empty will be skipped entirely.',
				});
			}
			blocks.forEach(function (b) {
				if (
					b.type === 'placeholder' &&
					b.format === 'gos_plain' &&
					!/^\d+$/.test(b.field_id)
				) {
					warns.push({
						level: 'error',
						msg:
							'Bare "gos:' +
							b.field_id +
							'" only works for numeric field IDs. Use {gos:' +
							b.field_id +
							'} instead.',
					});
				}
			});
		}

		const defs = (state.default_values || []).filter(function (v) {
			return v !== '';
		});
		const dispCount =
			state.display.mode === 'simple'
				? state.display.simple_field_ids.length
				: state.display.template_blocks.filter(function (b) {
						return b.type === 'placeholder';
					}).length;
		if (defs.length > 1 && dispCount > 0 && defs.length !== dispCount) {
			warns.push({
				level: 'info',
				msg:
					'You set ' +
					defs.length +
					' default values but display has ' +
					dispCount +
					' field(s). Defaults map by position — extras are unused.',
			});
		}

		if (state.sort.key && schema && state.sort.key !== 'id') {
			const isProp = ENTRY_PROPERTIES.some(function (p) {
				return p.id === state.sort.key;
			});
			if (!fieldMetaById(schema, state.sort.key) && !isProp) {
				warns.push({
					level: 'info',
					msg:
						'Sort key "' +
						state.sort.key +
						'" is not a known field or entry property in form #' +
						schema.id +
						'.',
				});
			}
		}
		if (state.sort.direction === 'RAND' && state.sort.secondary_key) {
			warns.push({
				level: 'info',
				msg: 'Secondary sort is ignored when primary direction is RAND.',
			});
		}
		if (String(state.limit).toLowerCase() === 'all') {
			warns.push({
				level: 'info',
				msg: 'limit="all" loops in pages of 25 until exhausted. Cache the page if results are large.',
			});
		}
		if (
			state.search_empty &&
			state.search_rows.some(function (r) {
				return (
					r.value !== '' && r.value !== null && r.value !== undefined
				);
			})
		) {
			warns.push({
				level: 'warn',
				msg: 'search_empty is on but some rows have values. The flag only changes behavior when content is blank.',
			});
		}

		(function walk(blocks) {
			(blocks || []).forEach(function (b) {
				if (b.type === 'html' && b.attrs) {
					Object.keys(b.attrs).forEach(function (k) {
						const v = String(b.attrs[k] || '');
						if (v.indexOf('"') !== -1 && v.indexOf("'") !== -1) {
							warns.push({
								level: 'error',
								msg:
									'HTML attribute "' +
									k +
									'" contains both single and double quotes — one set will be stripped.',
							});
						}
					});
				}
				if (b.children) {
					walk(b.children);
				}
			});
		})(state.display.template_blocks);

		return warns;
	}

	/* ------------------------------------------------------------------ */
	/* DOM helpers                                                          */
	/* ------------------------------------------------------------------ */

	function el(tag, attrs, children) {
		const e = document.createElement(tag);
		Object.keys(attrs || {}).forEach(function (k) {
			if (k === 'class') {
				e.className = attrs[k];
			} else if (k === 'style') {
				e.style.cssText = attrs[k];
			} else if (k.startsWith('on') && typeof attrs[k] === 'function') {
				const fn = attrs[k];
				e.addEventListener(k.slice(2), function (ev) {
					fn(ev);
				});
			} else if (k === 'html') {
				e.innerHTML = attrs[k];
			} else if (attrs[k] === true) {
				e.setAttribute(k, k);
			} else if (
				attrs[k] === false ||
				attrs[k] === null ||
				attrs[k] === undefined
			) {
				// skip
			} else {
				e.setAttribute(k, attrs[k]);
			}
		});
		(children || []).forEach(function (c) {
			if (c === null || c === undefined) {
				return;
			}
			if (typeof c === 'string') {
				e.appendChild(document.createTextNode(c));
			} else {
				e.appendChild(c);
			}
		});
		return e;
	}

	function rerender() {
		saveDraft();
		root.innerHTML = '';
		root.appendChild(renderMain());
		root.appendChild(renderSide());
	}

	/* ------------------------------------------------------------------ */
	/* Main column                                                          */
	/* ------------------------------------------------------------------ */

	function renderMain() {
		const main = el('div', { class: 'gospb-main' });
		main.appendChild(renderTopSection());
		main.appendChild(renderMiddleSection());
		main.appendChild(renderBottomSection());
		return main;
	}

	function renderTopSection() {
		const card = el('div', { class: 'gospb-card' });
		card.appendChild(
			el('h2', {}, [
				'Attributes',
				el('span', { class: 'gospb-help' }, [
					'target, sort, limit, etc.',
				]),
			])
		);

		const targetRow = el('div', { class: 'gospb-row' }, [
			el('label', {}, ['Target form(s)']),
		]);
		const targetWrap = el('div', { class: 'gospb-inline' });
		const allFormsCb = el('input', {
			type: 'checkbox',
			id: 'gospb-all-forms',
		});
		allFormsCb.checked = state.target.mode === 'all';
		allFormsCb.addEventListener('change', function () {
			state.target.mode = allFormsCb.checked ? 'all' : 'forms';
			rerender();
		});
		targetWrap.appendChild(allFormsCb);
		targetWrap.appendChild(
			el('label', { for: 'gospb-all-forms' }, ['All forms (target="0")'])
		);

		if (state.target.mode === 'forms') {
			const box = el('div', {
				style: 'flex: 1 1 100%; max-width: 100%; display:flex; flex-direction:column; gap:6px;',
			});

			if (state.target.form_ids.length) {
				const summary = el('div', { class: 'gospb-selected-forms' });
				summary.appendChild(
					el('div', { class: 'gospb-selected-forms__label' }, [
						'Selected (' + state.target.form_ids.length + ')',
						state.target.form_ids.length > 1
							? el(
									'span',
									{
										class: 'gospb-help',
										style: 'float:none; margin-left:6px;',
									},
									[
										'Primary form drives field-aware UI features. Click "Make primary" to change.',
									]
								)
							: null,
					])
				);
				state.target.form_ids.forEach(function (fid, idx) {
					const meta = formsList.find(function (f) {
						return f.id === fid;
					});
					const title = meta ? meta.title : '(unknown form)';
					const isPrimary = idx === 0;
					const chip = el('div', {
						class:
							'gospb-form-chip' +
							(isPrimary ? ' gospb-form-chip--primary' : ''),
					});
					if (isPrimary) {
						chip.appendChild(
							el(
								'span',
								{
									class: 'gospb-form-chip__star',
									title: 'Primary form',
								},
								['★']
							)
						);
					}
					chip.appendChild(
						el('span', { class: 'gospb-form-chip__title' }, [title])
					);
					chip.appendChild(
						el('span', { class: 'gospb-pill' }, ['#' + fid])
					);
					if (isPrimary) {
						chip.appendChild(
							el('span', { class: 'gospb-form-chip__role' }, [
								'Primary',
							])
						);
					} else {
						chip.appendChild(
							el(
								'button',
								{
									class: 'gospb-btn gospb-form-chip__action',
									title: 'Use this form for field-aware UI features',
									onclick() {
										state.target.form_ids = [fid].concat(
											state.target.form_ids.filter(
												function (x) {
													return x !== fid;
												}
											)
										);
										loadSchema(fid).then(rerender);
									},
								},
								['Make primary']
							)
						);
					}
					chip.appendChild(
						el(
							'button',
							{
								class: 'gospb-btn gospb-btn-danger gospb-form-chip__action',
								title: 'Remove from target',
								onclick() {
									state.target.form_ids =
										state.target.form_ids.filter(
											function (x) {
												return x !== fid;
											}
										);
									rerender();
								},
							},
							['×']
						)
					);
					summary.appendChild(chip);
				});
				box.appendChild(summary);
			}

			const checks = el('div', { class: 'gospb-multi-checks' });
			if (!formsList.length) {
				checks.appendChild(
					el('div', { class: 'gospb-empty' }, ['Loading forms…'])
				);
			} else {
				const sorted = formsList.slice().sort(function (a, b) {
					return (a.title || '')
						.toLowerCase()
						.localeCompare((b.title || '').toLowerCase());
				});
				sorted.forEach(function (f) {
					const row = el('label');
					const cb = el('input', { type: 'checkbox', value: f.id });
					cb.checked = state.target.form_ids.indexOf(f.id) !== -1;
					cb.addEventListener('change', function () {
						const id = parseInt(cb.value, 10);
						if (cb.checked) {
							if (state.target.form_ids.indexOf(id) === -1) {
								state.target.form_ids.push(id);
							}
							loadSchema(id).then(rerender);
						} else {
							state.target.form_ids =
								state.target.form_ids.filter(function (x) {
									return x !== id;
								});
							rerender();
						}
					});
					row.appendChild(cb);
					row.appendChild(
						document.createTextNode(' ' + f.title + ' ')
					);
					row.appendChild(
						el('span', { class: 'gospb-pill' }, ['#' + f.id])
					);
					checks.appendChild(row);
				});
			}
			box.appendChild(checks);
			targetWrap.appendChild(box);
		}

		targetRow.appendChild(targetWrap);
		card.appendChild(targetRow);

		card.appendChild(
			rowSelect(
				'Match mode',
				state.search_mode,
				[
					{ value: 'all', label: 'ALL conditions must match' },
					{ value: 'any', label: 'ANY condition can match' },
				],
				function (v) {
					state.search_mode = v;
					rerender();
				}
			)
		);

		card.appendChild(
			rowCheckbox(
				'Search empty values',
				state.search_empty,
				'When ON and content is blank, finds entries where the searched fields are empty.',
				function (v) {
					state.search_empty = v;
					rerender();
				}
			)
		);

		const schema = primarySchema();
		const sortOpts = sortOptionsFor(schema);
		card.appendChild(
			rowSelect('Sort by', state.sort.key, sortOpts, function (v) {
				state.sort.key = v;
				const meta = sortOpts.find(function (o) {
					return o.value === v;
				});
				if (
					meta &&
					meta.numeric !== null &&
					meta.numeric !== undefined
				) {
					state.sort.is_num = !!meta.numeric;
				}
				rerender();
			})
		);
		card.appendChild(
			rowSelect(
				'Sort direction',
				state.sort.direction,
				[
					{ value: 'DESC', label: 'DESC (newest/highest first)' },
					{ value: 'ASC', label: 'ASC (oldest/lowest first)' },
					{ value: 'RAND', label: 'RAND (random)' },
				],
				function (v) {
					state.sort.direction = v;
					rerender();
				}
			)
		);
		card.appendChild(
			rowCheckbox(
				'Sort numerically',
				!!state.sort.is_num,
				'Treats values as numbers, not strings. Auto-set for numeric fields.',
				function (v) {
					state.sort.is_num = v;
					rerender();
				}
			)
		);
		if (state.sort.direction !== 'RAND') {
			card.appendChild(
				rowSelect(
					'Secondary sort by',
					state.sort.secondary_key,
					[{ value: '', label: '— None —' }].concat(sortOpts),
					function (v) {
						state.sort.secondary_key = v;
						rerender();
					}
				)
			);
			if (state.sort.secondary_key) {
				card.appendChild(
					rowSelect(
						'Secondary direction',
						state.sort.secondary_direction,
						[
							{ value: 'DESC', label: 'DESC' },
							{ value: 'ASC', label: 'ASC' },
						],
						function (v) {
							state.sort.secondary_direction = v;
							rerender();
						}
					)
				);
			}
		}

		const limitRow = el('div', { class: 'gospb-row' }, [
			el('label', {}, ['Limit']),
		]);
		const limitWrap = el('div', { class: 'gospb-inline' });
		const allCb = el('input', { type: 'checkbox' });
		allCb.checked = String(state.limit).toLowerCase() === 'all';
		allCb.addEventListener('change', function () {
			state.limit = allCb.checked ? 'all' : '1';
			rerender();
		});
		limitWrap.appendChild(allCb);
		limitWrap.appendChild(el('label', {}, ['All']));
		if (!allCb.checked) {
			const num = el('input', {
				type: 'number',
				min: '1',
				value: state.limit,
				style: 'width:90px;',
			});
			num.addEventListener('input', function () {
				state.limit = num.value;
				updatePreview();
			});
			limitWrap.appendChild(num);
		}
		limitRow.appendChild(limitWrap);
		card.appendChild(limitRow);

		card.appendChild(
			rowCheckbox(
				'Unique results only',
				state.unique,
				'Returns only unique formatted results.',
				function (v) {
					state.unique = v;
					rerender();
				}
			)
		);

		const sepOpts = SEPARATOR_PRESETS.slice();
		const isPreset = sepOpts.some(function (o) {
			return o.value === state.separator;
		});
		const sepRow = el('div', { class: 'gospb-row' }, [
			el('label', {}, ['Result separator']),
		]);
		const sepWrap = el('div', {
			class: 'gospb-inline',
			style: 'flex:1; gap:6px;',
		});
		const sepSel = el('select', { style: 'flex: 0 0 auto;' });
		sepOpts.forEach(function (o) {
			const opt = el('option', { value: o.value }, [o.label]);
			if (o.value === state.separator) {
				opt.selected = true;
			}
			sepSel.appendChild(opt);
		});
		const customOpt = el('option', { value: '__custom__' }, ['Custom…']);
		if (!isPreset && state.separator) {
			customOpt.selected = true;
		}
		sepSel.appendChild(customOpt);
		sepSel.addEventListener('change', function () {
			if (sepSel.value === '__custom__') {
				state.separator = state.separator || '';
			} else {
				state.separator = sepSel.value;
			}
			rerender();
		});
		sepWrap.appendChild(sepSel);
		if (sepSel.value === '__custom__' || (!isPreset && state.separator)) {
			const custom = el('input', {
				type: 'text',
				value: state.separator || '',
				placeholder: 'Enter custom separator (HTML allowed)',
				style: 'flex:1;',
			});
			custom.addEventListener('input', function () {
				state.separator = custom.value;
				updatePreview();
			});
			sepWrap.appendChild(custom);
		}
		sepRow.appendChild(sepWrap);
		card.appendChild(sepRow);

		const defRow = el(
			'div',
			{ class: 'gospb-row', style: 'align-items: start;' },
			[el('label', {}, ['Default value(s)'])]
		);
		const defWrap = el('div', {});
		(state.default_values.length ? state.default_values : ['']).forEach(
			function (v, i) {
				const line = el('div', {
					class: 'gospb-inline',
					style: 'margin-bottom:4px;',
				});
				const inp = el('input', {
					type: 'text',
					value: v,
					placeholder:
						'Used when ' +
						(i === 0
							? 'no entries match (or first display field empty)'
							: 'display field #' + (i + 1) + ' is empty'),
					style: 'flex:1;',
				});
				inp.addEventListener('input', function () {
					state.default_values[i] = inp.value;
					updatePreview();
				});
				line.appendChild(inp);
				line.appendChild(
					el(
						'button',
						{
							class: 'gospb-btn gospb-btn-danger',
							onclick() {
								state.default_values.splice(i, 1);
								if (!state.default_values.length) {
									state.default_values = [''];
								}
								rerender();
							},
						},
						['×']
					)
				);
				defWrap.appendChild(line);
			}
		);
		defWrap.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn',
					onclick() {
						state.default_values.push('');
						rerender();
					},
				},
				['+ Add fallback']
			)
		);
		defRow.appendChild(defWrap);
		card.appendChild(defRow);

		// noinspection HtmlUnknownTarget
		card.appendChild(
			rowCheckbox(
				'Wrap each result in admin entry link',
				state.link,
				'Adds an <a href="…admin entry view…"> wrapper around each result.',
				function (v) {
					state.link = v;
					rerender();
				}
			)
		);

		return card;
	}

	function sortOptionsFor(schema) {
		const opts = [];
		ENTRY_PROPERTIES.forEach(function (p) {
			opts.push({
				value: p.id,
				label: p.label + ' [' + p.id + ']',
				numeric: p.type === 'number',
			});
		});
		if (schema) {
			schema.fields.forEach(function (f) {
				const numeric = f.type === 'number' || f.type === 'currency';
				opts.push({
					value: f.id,
					label:
						'[Field] ' +
						(f.label || '(no label)') +
						' [' +
						f.id +
						']',
					numeric,
				});
			});
		}
		return opts;
	}

	function rowSelect(label, value, options, onChange) {
		const r = el('div', { class: 'gospb-row' }, [el('label', {}, [label])]);
		const sel = el('select');
		options.forEach(function (o) {
			const opt = el('option', { value: o.value }, [o.label]);
			if (String(o.value) === String(value)) {
				opt.selected = true;
			}
			sel.appendChild(opt);
		});
		sel.addEventListener('change', function () {
			onChange(sel.value);
		});
		r.appendChild(sel);
		return r;
	}

	function rowCheckbox(label, checked, hint, onChange) {
		const r = el('div', { class: 'gospb-row' }, [el('label', {}, [label])]);
		const wrap = el('div', { class: 'gospb-inline' });
		const cb = el('input', { type: 'checkbox' });
		cb.checked = !!checked;
		cb.addEventListener('change', function () {
			onChange(cb.checked);
		});
		wrap.appendChild(cb);
		if (hint) {
			wrap.appendChild(
				el('span', { style: 'color:#646970;font-size:12px;' }, [hint])
			);
		}
		r.appendChild(wrap);
		return r;
	}

	/* ------------------------------------------------------------------ */
	/* Search filters section                                               */
	/* ------------------------------------------------------------------ */

	function renderMiddleSection() {
		const card = el('div', { class: 'gospb-card' });
		card.appendChild(
			el('h2', {}, [
				'Search filters',
				el('span', { class: 'gospb-help' }, [
					'Each row maps by position to one pipe-separated value',
				]),
			])
		);

		const list = el('div', {});
		const schema = primarySchema();
		const fieldOpts = fieldOptionsForSchema(schema, {
			includeGlobal: true,
		});

		if (!state.search_rows.length) {
			list.appendChild(
				el('div', { class: 'gospb-empty' }, [
					'No filters yet. Use the buttons below to start, or leave empty to display fields without filtering.',
				])
			);
		}
		state.search_rows.forEach(function (row, idx) {
			list.appendChild(renderSearchRow(row, idx, fieldOpts));
		});
		card.appendChild(list);

		const hasLabelOrTypeRowsNow = state.search_rows.some(function (r) {
			return r.field_mode === 'label' || r.field_mode === 'type';
		});
		if (hasLabelOrTypeRowsNow) {
			card.appendChild(
				rowSelect(
					'Field match',
					state.field_match || 'first',
					[
						{ value: 'first', label: 'First match per form' },
						{
							value: 'all',
							label: 'All matches (every field with that label/type)',
						},
					],
					function (v) {
						state.field_match = v;
						rerender();
					}
				)
			);
		}

		const addBtnRow = el('div', {
			class: 'gospb-inline',
			style: 'margin-top:6px;',
		});
		addBtnRow.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn',
					onclick() {
						state.search_rows.push({
							field_id: '',
							operator: '=',
							value: '',
							field_mode: 'id',
						});
						rerender();
					},
				},
				['+ Add filter']
			)
		);
		addBtnRow.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn',
					onclick() {
						state.search_rows.push({
							field_id: '',
							operator: '=',
							value: '',
							field_mode: 'label',
						});
						rerender();
					},
				},
				['+ By label']
			)
		);
		addBtnRow.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn',
					onclick() {
						state.search_rows.push({
							field_id: '',
							operator: '=',
							value: '',
							field_mode: 'type',
						});
						rerender();
					},
				},
				['+ By type']
			)
		);
		card.appendChild(addBtnRow);

		return card;
	}

	function renderSearchRow(row, idx, fieldOpts) {
		const wrap = el('div', { class: 'gospb-search-row' });
		wrap.appendChild(
			el(
				'div',
				{
					class: 'gospb-handle',
					draggable: true,
					ondragstart(e) {
						e.dataTransfer.setData('text/gospb-row', String(idx));
					},
					ondragover(e) {
						e.preventDefault();
					},
					ondrop(e) {
						e.preventDefault();
						const from = parseInt(
							e.dataTransfer.getData('text/gospb-row'),
							10
						);
						if (isNaN(from) || from === idx) {
							return;
						}
						const moved = state.search_rows.splice(from, 1)[0];
						state.search_rows.splice(idx, 0, moved);
						rerender();
					},
				},
				[String(idx + 1)]
			)
		);

		// Mode selector: By ID / By Label / By Type
		const mode = row.field_mode || 'id';
		const modeSel = el('select');
		[
			{ value: 'id', label: 'By ID' },
			{ value: 'label', label: 'By Label' },
			{ value: 'type', label: 'By Type' },
		].forEach(function (m) {
			const opt = el('option', { value: m.value }, [m.label]);
			if (mode === m.value) {
				opt.selected = true;
			}
			modeSel.appendChild(opt);
		});
		modeSel.addEventListener('change', function () {
			row.field_mode = modeSel.value;
			row.field_id = '';
			row.operator = '=';
			rerender();
		});
		wrap.appendChild(modeSel);

		// Field/label/type input — varies by mode
		let fieldMeta = null;
		if (mode === 'id') {
			const fieldSel = el('select');
			const groups = {};
			fieldOpts.forEach(function (o) {
				if (o.displayOnly) {
					return;
				}
				(groups[o.group || ''] = groups[o.group || ''] || []).push(o);
			});
			Object.keys(groups).forEach(function (gname) {
				const og = el('optgroup', { label: gname });
				groups[gname].forEach(function (o) {
					const opt = el('option', { value: o.value }, [o.label]);
					if (String(o.value) === String(row.field_id)) {
						opt.selected = true;
					}
					og.appendChild(opt);
				});
				fieldSel.appendChild(og);
			});
			fieldSel.addEventListener('change', function () {
				row.field_id = fieldSel.value;
				const meta = fieldOpts.find(function (o) {
					return o.value === fieldSel.value;
				});
				const allowed = meta ? operatorsForType(meta.type) : ['='];
				if (allowed.indexOf(row.operator) === -1) {
					row.operator = allowed[0];
				}
				rerender();
			});
			wrap.appendChild(fieldSel);
			fieldMeta = fieldOpts.find(function (o) {
				return o.value === row.field_id;
			});
		} else if (mode === 'label') {
			const inp = el('input', {
				type: 'text',
				value: row.field_id || '',
				placeholder: 'Field label (e.g. Email Address)',
			});
			inp.addEventListener('input', function () {
				row.field_id = inp.value;
				updatePreview();
			});
			wrap.appendChild(inp);
		} else {
			const types = getUniqueFieldTypes(allSelectedSchemas());
			if (types.length > 0) {
				const typeSel = el('select');
				const blankOpt = el('option', { value: '' }, [
					'— Select type —',
				]);
				if (!row.field_id) {
					blankOpt.selected = true;
				}
				typeSel.appendChild(blankOpt);
				types.forEach(function (t) {
					const opt = el('option', { value: t }, [t]);
					if (t === row.field_id) {
						opt.selected = true;
					}
					typeSel.appendChild(opt);
				});
				typeSel.addEventListener('change', function () {
					row.field_id = typeSel.value;
					updatePreview();
				});
				wrap.appendChild(typeSel);
			} else {
				const inp = el('input', {
					type: 'text',
					value: row.field_id || '',
					placeholder: 'Field type (e.g. email, text, select)',
				});
				inp.addEventListener('input', function () {
					row.field_id = inp.value;
					updatePreview();
				});
				wrap.appendChild(inp);
			}
		}

		const allowedOps = fieldMeta
			? operatorsForType(fieldMeta.type)
			: ALL_OPERATORS.map(function (o) {
					return o.value;
				});
		const opSel = el('select');
		ALL_OPERATORS.filter(function (o) {
			return allowedOps.indexOf(o.value) !== -1;
		}).forEach(function (o) {
			const opt = el('option', { value: o.value }, [o.label]);
			if (o.value === row.operator) {
				opt.selected = true;
			}
			opSel.appendChild(opt);
		});
		opSel.addEventListener('change', function () {
			row.operator = opSel.value;
			rerender();
		});
		wrap.appendChild(opSel);

		wrap.appendChild(renderValueInput(row, fieldMeta));

		wrap.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn gospb-btn-danger',
					onclick() {
						state.search_rows.splice(idx, 1);
						rerender();
					},
				},
				['Remove']
			)
		);

		return wrap;
	}

	function renderValueInput(row, meta) {
		const op = row.operator;
		if (op === 'in' || op === 'not in') {
			const inp = el('input', {
				type: 'text',
				value: Array.isArray(row.value)
					? row.value.join(', ')
					: row.value || '',
				placeholder: 'comma,separated,values',
			});
			inp.addEventListener('input', function () {
				row.value = inp.value
					.split(',')
					.map(function (s) {
						return s.trim();
					})
					.filter(Boolean);
				updatePreview();
			});
			return inp;
		}
		const type = meta ? meta.type : 'text';
		if (
			meta &&
			meta.choices &&
			meta.choices.length &&
			(op === '=' || op === '!=')
		) {
			const sel = el('select');
			sel.appendChild(el('option', { value: '' }, ['— Choose —']));
			meta.choices.forEach(function (c) {
				const opt = el('option', { value: c.value }, [
					c.text + (c.value !== c.text ? ' [' + c.value + ']' : ''),
				]);
				if (String(c.value) === String(row.value)) {
					opt.selected = true;
				}
				sel.appendChild(opt);
			});
			sel.addEventListener('change', function () {
				row.value = sel.value;
				updatePreview();
			});
			return sel;
		}
		if (type === 'number' || type === 'currency') {
			const inp = el('input', {
				type: 'number',
				value: row.value || '',
				step: 'any',
			});
			inp.addEventListener('input', function () {
				row.value = inp.value;
				updatePreview();
			});
			return inp;
		}
		if (type === 'date') {
			const inp = el('input', { type: 'date', value: row.value || '' });
			inp.addEventListener('input', function () {
				row.value = inp.value;
				updatePreview();
			});
			return inp;
		}
		if (type === 'boolean') {
			const sel = el('select');
			[
				['', '— Choose —'],
				['1', 'Yes / true'],
				['0', 'No / false'],
			].forEach(function (pair) {
				const opt = el('option', { value: pair[0] }, [pair[1]]);
				if (String(pair[0]) === String(row.value)) {
					opt.selected = true;
				}
				sel.appendChild(opt);
			});
			sel.addEventListener('change', function () {
				row.value = sel.value;
				updatePreview();
			});
			return sel;
		}
		const inp = el('input', {
			type: 'text',
			value: row.value || '',
			placeholder: op === 'like' ? 'wildcards: % and _' : 'value',
		});
		inp.addEventListener('input', function () {
			row.value = inp.value;
			updatePreview();
		});
		return inp;
	}

	/* ------------------------------------------------------------------ */
	/* Display section                                                      */
	/* ------------------------------------------------------------------ */

	function renderBlockAdders(onAdd) {
		const wrap = el('div', { class: 'gospb-add-block' });
		[
			{ label: '+ Text', type: 'text' },
			{ label: '+ Field placeholder', type: 'placeholder' },
			{ label: '+ HTML wrapper', type: 'html' },
			{ label: '+ Nested shortcode', type: 'shortcode' },
			{ label: '+ Special ({num_results})', type: 'special' },
		].forEach(function (b) {
			wrap.appendChild(
				el(
					'button',
					{
						class: 'gospb-btn',
						onclick() {
							onAdd(b.type);
						},
					},
					[b.label]
				)
			);
		});
		return wrap;
	}

	function renderBottomSection() {
		const card = el('div', { class: 'gospb-card' });
		card.appendChild(
			el('h2', {}, [
				'Display',
				el('span', { class: 'gospb-help' }, [
					'What gets rendered for each matching entry',
				]),
			])
		);

		const tabs = el('div', { class: 'gospb-mode-tabs' });
		[
			{ id: 'simple', label: 'Simple field list' },
			{ id: 'template', label: 'Template builder' },
		].forEach(function (t) {
			const b = el(
				'button',
				{ class: state.display.mode === t.id ? 'active' : '' },
				[t.label]
			);
			b.addEventListener('click', function () {
				state.display.mode = t.id;
				rerender();
			});
			tabs.appendChild(b);
		});
		card.appendChild(tabs);

		if (state.display.mode === 'simple') {
			card.appendChild(renderSimpleDisplay());
		} else {
			card.appendChild(renderTemplateDisplay());
		}

		return card;
	}

	function renderSimpleDisplay() {
		const wrap = el('div', {});
		const schema = primarySchema();
		const opts = fieldOptionsForSchema(schema);

		wrap.appendChild(
			el(
				'p',
				{ style: 'margin: 0 0 8px; color:#646970; font-size:12px;' },
				[
					'Pick one or more fields/properties. The plugin will output their raw values, separated by commas (or by your chosen separator for multiple entries).',
				]
			)
		);

		const checks = el('div', { class: 'gospb-multi-checks' });
		const groups = {};
		opts.forEach(function (o) {
			(groups[o.group] = groups[o.group] || []).push(o);
		});
		Object.keys(groups).forEach(function (gname) {
			checks.appendChild(
				el(
					'div',
					{
						style: 'font-weight:600;color:#1d2327;margin-top:6px;font-size:12px;',
					},
					[gname]
				)
			);
			groups[gname].forEach(function (o) {
				const lbl = el('label');
				const cb = el('input', { type: 'checkbox', value: o.value });
				cb.checked =
					state.display.simple_field_ids.indexOf(o.value) !== -1;
				cb.addEventListener('change', function () {
					if (cb.checked) {
						state.display.simple_field_ids.push(o.value);
					} else {
						state.display.simple_field_ids =
							state.display.simple_field_ids.filter(function (v) {
								return v !== o.value;
							});
					}
					updatePreview();
				});
				lbl.appendChild(cb);
				lbl.appendChild(document.createTextNode(' ' + o.label));
				checks.appendChild(lbl);
			});
		});
		checks.appendChild(
			el(
				'div',
				{
					style: 'font-weight:600;color:#1d2327;margin-top:6px;font-size:12px;',
				},
				['Special']
			)
		);
		[
			{
				value: 'meta',
				label: 'meta (introspection — list every available key)',
			},
			{
				value: 'num_results',
				label: 'num_results (count of matched entries)',
			},
		].forEach(function (o) {
			const lbl = el('label');
			const cb = el('input', { type: 'checkbox', value: o.value });
			cb.checked = state.display.simple_field_ids.indexOf(o.value) !== -1;
			cb.addEventListener('change', function () {
				if (cb.checked) {
					state.display.simple_field_ids.push(o.value);
				} else {
					state.display.simple_field_ids =
						state.display.simple_field_ids.filter(function (v) {
							return v !== o.value;
						});
				}
				updatePreview();
			});
			lbl.appendChild(cb);
			lbl.appendChild(document.createTextNode(' ' + o.label));
			checks.appendChild(lbl);
		});
		wrap.appendChild(checks);

		if (primaryFormId()) {
			const disc = el(
				'button',
				{
					class: 'gospb-btn',
					style: 'margin-top:8px;',
					onclick() {
						const fid = primaryFormId();
						const sc =
							'[gravops_search target="' +
							fid +
							'" display="meta" limit="1"][/gravops_search]';
						runShortcode(sc).then(function (html) {
							const out =
								document.getElementById('gospb-meta-out');
							if (out) {
								out.innerHTML =
									'<strong>Available meta keys for form #' +
									fid +
									':</strong><br>' +
									html;
							}
						});
					},
				},
				['Discover available meta keys for this form']
			);
			wrap.appendChild(disc);
			wrap.appendChild(
				el('div', {
					id: 'gospb-meta-out',
					class: 'gospb-test-out',
					style: 'margin-top:6px;',
				})
			);
		}

		return wrap;
	}

	function renderTemplateDisplay() {
		const wrap = el('div', {});
		wrap.appendChild(
			el(
				'p',
				{ style: 'margin: 0 0 8px; color:#646970; font-size:12px;' },
				[
					'Stack blocks to build the per-entry output. Field placeholders insert {id}, {gos:id} etc. HTML blocks can wrap children. Nested shortcodes are emitted as {{…}} so the plugin converts them safely.',
				]
			)
		);

		wrap.appendChild(
			renderBlockList(state.display.template_blocks, function (list) {
				state.display.template_blocks = list;
				rerender();
			})
		);

		wrap.appendChild(
			renderBlockAdders(function (type) {
				state.display.template_blocks.push(makeBlock(type));
				rerender();
			})
		);

		return wrap;
	}

	function makeBlock(type) {
		switch (type) {
			case 'text':
				return { type: 'text', value: '' };
			case 'placeholder':
				return {
					type: 'placeholder',
					field_id: '',
					format: 'auto',
					default: '',
				};
			case 'html':
				return {
					type: 'html',
					tag: 'span',
					attrs: {},
					children: [],
					selfClosing: false,
				};
			case 'shortcode':
				return {
					type: 'shortcode',
					tag: 'gravops_search',
					attrs: {},
					inner_template: '',
					selfClosing: false,
				};
			case 'special':
				return { type: 'special', value: 'num_results' };
			default:
				return { type: 'text', value: '' };
		}
	}

	function renderBlockList(list, onChange) {
		const wrap = el('div', { class: 'gospb-block-list' });
		if (!list.length) {
			wrap.appendChild(
				el('div', { class: 'gospb-empty' }, ['No blocks yet.'])
			);
		}
		list.forEach(function (b, i) {
			wrap.appendChild(renderBlock(b, list, i, onChange));
		});
		return wrap;
	}

	function renderBlock(b, list, idx, onParentChange) {
		const wrap = el('div', { class: 'gospb-block' });
		wrap.appendChild(
			el(
				'div',
				{
					class: 'gospb-handle',
					draggable: true,
					ondragstart(e) {
						e.dataTransfer.setData('text/plain', String(idx));
					},
					ondragover(e) {
						e.preventDefault();
					},
					ondrop(e) {
						e.preventDefault();
						const from = parseInt(
							e.dataTransfer.getData('text/plain'),
							10
						);
						if (isNaN(from) || from === idx) {
							return;
						}
						const moved = list.splice(from, 1)[0];
						list.splice(idx, 0, moved);
						onParentChange(list);
					},
				},
				['⋮⋮']
			)
		);

		wrap.appendChild(
			el('span', { class: 'gospb-block-type' }, [
				labelForBlockType(b.type),
			])
		);

		const editor = el('div', {});
		renderBlockEditor(b, editor);
		wrap.appendChild(editor);

		wrap.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn gospb-btn-danger',
					onclick() {
						list.splice(idx, 1);
						onParentChange(list);
					},
				},
				['×']
			)
		);

		if (b.type === 'html' && !b.selfClosing) {
			const childArea = el('div', { class: 'gospb-block-children' });
			childArea.appendChild(
				el(
					'div',
					{
						style: 'font-size:11px;color:#646970;margin-bottom:4px;',
					},
					['Children of <' + b.tag + '>:']
				)
			);
			childArea.appendChild(
				renderBlockList(b.children || [], function (list2) {
					b.children = list2;
					rerender();
				})
			);
			childArea.appendChild(
				renderBlockAdders(function (type) {
					b.children = b.children || [];
					b.children.push(makeBlock(type));
					rerender();
				})
			);
			wrap.appendChild(childArea);
		}

		return wrap;
	}

	function labelForBlockType(t) {
		return (
			{
				text: 'Text',
				placeholder: 'Field',
				html: 'HTML',
				shortcode: 'Shortcode',
				special: 'Special',
			}[t] || t
		);
	}

	function renderBlockEditor(b, container) {
		container.innerHTML = '';
		switch (b.type) {
			case 'text': {
				const inp = el('input', {
					type: 'text',
					value: b.value || '',
					placeholder:
						'Plain text (literal { and } not allowed — they conflict with placeholder syntax)',
				});
				inp.addEventListener('input', function () {
					b.value = inp.value;
					updatePreview();
				});
				container.appendChild(inp);
				break;
			}
			case 'placeholder': {
				const schema = primarySchema();
				const opts = fieldOptionsForSchema(schema);
				const wrap = el('div', {
					class: 'gospb-inline',
					style: 'flex-wrap:wrap; gap:4px;',
				});

				const sel = el('select', { style: 'flex:1; min-width:200px;' });
				sel.appendChild(
					el('option', { value: '' }, ['— Pick field —'])
				);
				const pGroups = {};
				opts.forEach(function (o) {
					(pGroups[o.group] = pGroups[o.group] || []).push(o);
				});
				Object.keys(pGroups).forEach(function (gname) {
					const og = el('optgroup', { label: gname });
					pGroups[gname].forEach(function (o) {
						const opt = el('option', { value: o.value }, [
							o.label + (o.displayOnly ? ' (display only)' : ''),
						]);
						if (String(o.value) === String(b.field_id)) {
							opt.selected = true;
						}
						og.appendChild(opt);
					});
					sel.appendChild(og);
				});
				sel.addEventListener('change', function () {
					b.field_id = sel.value;
					b.format = /^\d+(\.\d+)?$/.test(sel.value)
						? 'curly'
						: 'gos_curly';
					rerender();
				});
				wrap.appendChild(sel);

				const fmt = el('select', { style: 'flex:0 0 auto;' });
				[
					{ value: 'auto', label: 'Auto format' },
					{ value: 'curly', label: '{id} (numeric)' },
					{
						value: 'gos_curly',
						label: '{gos:id} (text props / merge-tag-safe)',
					},
					{
						value: 'gos_plain',
						label: 'gos:id (nested shortcodes only)',
					},
				].forEach(function (o) {
					const opt = el('option', { value: o.value }, [o.label]);
					if (o.value === (b.format || 'auto')) {
						opt.selected = true;
					}
					fmt.appendChild(opt);
				});
				fmt.addEventListener('change', function () {
					b.format = fmt.value;
					updatePreview();
				});
				wrap.appendChild(fmt);

				const def = el('input', {
					type: 'text',
					value: b.default || '',
					placeholder: 'Default if empty (optional)',
					style: 'flex:1; min-width:140px;',
				});
				def.addEventListener('input', function () {
					b.default = def.value;
					updatePreview();
				});
				wrap.appendChild(def);

				container.appendChild(wrap);
				break;
			}
			case 'html': {
				const wrap = el('div', {
					class: 'gospb-inline',
					style: 'flex-wrap:wrap; gap:4px;',
				});
				const tagSel = el('select', { style: 'flex:0 0 110px;' });
				HTML_TAGS.forEach(function (o) {
					const opt = el('option', { value: o.value }, [o.label]);
					if (o.value === b.tag) {
						opt.selected = true;
					}
					tagSel.appendChild(opt);
				});
				tagSel.addEventListener('change', function () {
					b.tag = tagSel.value;
					b.selfClosing = b.tag === 'br';
					rerender();
				});
				wrap.appendChild(tagSel);

				const attrFields = htmlAttrsForTag(b.tag);
				attrFields.forEach(function (a) {
					const inp = el('input', {
						type: 'text',
						value: (b.attrs && b.attrs[a.name]) || '',
						placeholder: a.placeholder,
						style: 'flex:1; min-width:120px;',
					});
					inp.addEventListener('input', function () {
						b.attrs = b.attrs || {};
						b.attrs[a.name] = inp.value;
						updatePreview();
					});
					wrap.appendChild(inp);
				});
				container.appendChild(wrap);
				container.appendChild(
					el(
						'div',
						{
							style: 'font-size:11px;color:#646970;margin-top:4px;',
						},
						[
							'Attribute values use single quotes inside display="…". Single quotes inside values will be stripped.',
						]
					)
				);
				break;
			}
			case 'shortcode': {
				const wrap = el('div', {
					class: 'gospb-inline',
					style: 'flex-wrap:wrap; gap:4px;',
				});
				const tag = el('input', {
					type: 'text',
					value: b.tag || 'gravops_search',
					placeholder: 'shortcode tag',
					style: 'flex:0 0 180px;',
				});
				tag.addEventListener('input', function () {
					b.tag = tag.value;
					updatePreview();
				});
				wrap.appendChild(tag);
				const sc = el('input', { type: 'checkbox' });
				sc.checked = !!b.selfClosing;
				sc.addEventListener('change', function () {
					b.selfClosing = sc.checked;
					rerender();
				});
				wrap.appendChild(sc);
				wrap.appendChild(el('label', {}, ['Self-closing']));
				container.appendChild(wrap);

				const attrsWrap = el('div', { style: 'margin-top:4px;' });
				attrsWrap.appendChild(
					el('div', { style: 'font-size:11px;color:#646970;' }, [
						'Attributes (single-quoted in output):',
					])
				);
				const attrs = (b.attrs = b.attrs || {});
				Object.keys(attrs).forEach(function (k) {
					const ln = el('div', {
						class: 'gospb-inline',
						style: 'gap:4px;margin-top:3px;',
					});
					const kn = el('input', {
						type: 'text',
						value: k,
						placeholder: 'attr name',
						style: 'flex:0 0 130px;',
					});
					const vn = el('input', {
						type: 'text',
						value: attrs[k],
						placeholder: 'value',
						style: 'flex:1;',
					});
					kn.addEventListener('change', function () {
						if (kn.value !== k) {
							attrs[kn.value] = attrs[k];
							delete attrs[k];
							rerender();
						}
					});
					vn.addEventListener('input', function () {
						attrs[kn.value] = vn.value;
						updatePreview();
					});
					ln.appendChild(kn);
					ln.appendChild(vn);
					ln.appendChild(
						el(
							'button',
							{
								class: 'gospb-btn gospb-btn-danger',
								onclick() {
									delete attrs[k];
									rerender();
								},
							},
							['×']
						)
					);
					attrsWrap.appendChild(ln);
				});
				attrsWrap.appendChild(
					el(
						'button',
						{
							class: 'gospb-btn',
							style: 'margin-top:4px;',
							onclick() {
								attrs[
									'attr' + (Object.keys(attrs).length + 1)
								] = '';
								rerender();
							},
						},
						['+ Add attribute']
					)
				);
				container.appendChild(attrsWrap);

				if (!b.selfClosing) {
					const innerWrap = el('div', { style: 'margin-top:6px;' });
					innerWrap.appendChild(
						el('div', { style: 'font-size:11px;color:#646970;' }, [
							'Inner content. For gravitymath formulas, use ~gos.13~ or ~gos.13.sum~.',
						])
					);
					const innerInp = el('input', {
						type: 'text',
						value: b.inner_template || '',
						placeholder: 'inner content',
						style: 'width:100%;',
					});
					innerInp.addEventListener('input', function () {
						b.inner_template = innerInp.value;
						updatePreview();
					});
					innerWrap.appendChild(innerInp);

					const schema = primarySchema();
					if (schema && schema.fields.length) {
						const refSel = el('select', {
							style: 'margin-top:4px;',
						});
						refSel.appendChild(
							el('option', { value: '' }, [
								'Quick-insert outer field reference…',
							])
						);
						schema.fields.forEach(function (f) {
							refSel.appendChild(
								el('option', { value: f.id }, [
									(f.label || '(no label)') +
										' [' +
										f.id +
										']',
								])
							);
						});
						refSel.addEventListener('change', function () {
							if (!refSel.value) {
								return;
							}
							const ref = /^\d+$/.test(refSel.value)
								? 'gos:' + refSel.value
								: '{gos:' + refSel.value + '}';
							b.inner_template = (b.inner_template || '') + ref;
							rerender();
						});
						innerWrap.appendChild(refSel);
					}
					container.appendChild(innerWrap);
				}
				break;
			}
			case 'special': {
				const sel = el('select');
				[
					{ value: 'num_results', label: '{num_results} (numeric)' },
					{
						value: 'gos:num_results',
						label: '{gos:num_results} (merge-tag-safe)',
					},
				].forEach(function (o) {
					const opt = el('option', { value: o.value }, [o.label]);
					if (o.value === (b.value || 'num_results')) {
						opt.selected = true;
					}
					sel.appendChild(opt);
				});
				sel.addEventListener('change', function () {
					b.value = sel.value;
					updatePreview();
				});
				container.appendChild(sel);
				break;
			}
		}
	}

	function htmlAttrsForTag(tag) {
		switch (tag) {
			case 'a':
				return [
					{ name: 'href', placeholder: 'href (e.g. mailto:{13})' },
					{ name: 'class', placeholder: 'class' },
					{ name: 'target', placeholder: 'target (_blank)' },
				];
			case 'span':
			case 'div':
			case 'p':
			case 'li':
			case 'tr':
			case 'td':
			case 'th':
				return [
					{ name: 'class', placeholder: 'class' },
					{ name: 'style', placeholder: 'inline style' },
				];
			case 'h3':
			case 'h4':
				return [{ name: 'class', placeholder: 'class' }];
			default:
				return [];
		}
	}

	/* ------------------------------------------------------------------ */
	/* Sidebar                                                              */
	/* ------------------------------------------------------------------ */

	function renderSide() {
		const side = el('div', { class: 'gospb-side' });

		/* Live shortcode preview */
		const previewCard = el('div', { class: 'gospb-card' });
		previewCard.appendChild(el('h2', {}, ['Live shortcode']));
		const pre = el('pre', { id: 'gospb-preview', class: 'gospb-preview' });
		pre.textContent = buildShortcode();
		previewCard.appendChild(pre);

		const btnRow = el('div', {
			class: 'gospb-inline',
			style: 'margin-top:8px;',
		});
		const showBtn = el(
			'button',
			{
				class: 'gospb-btn gospb-btn-primary',
				onclick() {
					pre.textContent = buildShortcode();
					renderWarnings();
				},
			},
			['Display Shortcode']
		);
		const copyBtn = el(
			'button',
			{
				class: 'gospb-btn',
				onclick() {
					copyToClipboard(buildShortcode(), function () {
						copyBtn.textContent = '✓ Copied';
						setTimeout(function () {
							copyBtn.textContent = 'Copy Shortcode';
						}, 1500);
					});
				},
			},
			['Copy Shortcode']
		);
		btnRow.appendChild(showBtn);
		btnRow.appendChild(copyBtn);
		previewCard.appendChild(btnRow);
		side.appendChild(previewCard);

		/* Validation */
		const wCard = el('div', { class: 'gospb-card' });
		wCard.appendChild(el('h2', {}, ['Validation']));
		wCard.appendChild(
			el('div', { id: 'gospb-warnings', class: 'gospb-warnings' })
		);
		side.appendChild(wCard);

		/* Test run */
		const tCard = el('div', { class: 'gospb-card' });
		tCard.appendChild(
			el('h2', {}, [
				'Test run',
				el('span', { class: 'gospb-help' }, [
					'Renders the shortcode now',
				]),
			])
		);
		const runBtn = el(
			'button',
			{
				class: 'gospb-btn',
				onclick() {
					const out = document.getElementById('gospb-test-out');
					out.innerHTML = '<em>Running…</em>';
					runShortcode(buildShortcode())
						.then(function (html) {
							out.innerHTML = html || '<em>(empty result)</em>';
						})
						.catch(function (err) {
							out.innerHTML =
								'<span style="color:#b32d2e;">Error: ' +
								(err.message || err) +
								'</span>';
						});
				},
			},
			['▶ Run shortcode']
		);
		tCard.appendChild(runBtn);
		tCard.appendChild(
			el('div', {
				id: 'gospb-test-out',
				class: 'gospb-test-out',
				style: 'margin-top:6px;',
			})
		);
		tCard.appendChild(
			el('p', { style: 'font-size:11px;color:#646970;margin:6px 0 0;' }, [
				"Note: the plugin doesn't enforce permissions itself. Anyone who can view the page where you place the shortcode can see these results.",
			])
		);
		side.appendChild(tCard);

		/* Save shortcode */
		const scCard = el('div', { class: 'gospb-card' });
		scCard.appendChild(el('h2', {}, ['Save shortcode']));
		const scRow = el('div', { class: 'gospb-inline', style: 'gap:6px;' });
		const scInp = el('input', {
			type: 'text',
			placeholder: 'Shortcode name',
			style: 'flex:1;',
		});
		const scMsg = el('div', {
			style: 'font-size:11px;color:#646970;margin-top:4px;',
		});
		scRow.appendChild(scInp);
		scRow.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn gospb-btn-primary',
					onclick() {
						if (!scInp.value.trim()) {
							return;
						}
						ajax('gospb_save_shortcode', {
							name: scInp.value.trim(),
							shortcode: buildShortcode(),
							state: JSON.stringify(state),
						})
							.then(function () {
								scMsg.textContent =
									'✓ Saved. View it in the Saved Shortcodes tab.';
								scMsg.style.color = '#99D328';
								scInp.value = '';
								setTimeout(function () {
									scMsg.textContent = '';
								}, 3000);
							})
							.catch(function () {
								scMsg.textContent =
									'Save failed — check the shortcode is valid.';
								scMsg.style.color = '#b32d2e';
							});
					},
				},
				['Save']
			)
		);
		scCard.appendChild(scRow);
		scCard.appendChild(scMsg);
		side.appendChild(scCard);

		/* Presets */
		const pCard = el('div', { class: 'gospb-card' });
		pCard.appendChild(el('h2', {}, ['Presets']));
		const pList = el('div', { id: 'gospb-presets-list' });
		Object.keys(presets).forEach(function (name) {
			const ln = el('div', {
				class: 'gospb-inline',
				style: 'margin-bottom:3px;',
			});
			ln.appendChild(
				el(
					'button',
					{
						class: 'gospb-btn',
						style: 'flex:1;',
						onclick() {
							state = JSON.parse(JSON.stringify(presets[name]));
							rerender();
						},
					},
					[name]
				)
			);
			ln.appendChild(
				el(
					'button',
					{
						class: 'gospb-btn gospb-btn-danger',
						onclick() {
							ajax('gospb_delete_preset', { name }).then(
								function () {
									delete presets[name];
									rerender();
								}
							);
						},
					},
					['×']
				)
			);
			pList.appendChild(ln);
		});
		if (!Object.keys(presets).length) {
			pList.appendChild(
				el('div', { class: 'gospb-empty' }, ['No saved presets yet.'])
			);
		}
		pCard.appendChild(pList);

		const saveRow = el('div', {
			class: 'gospb-inline',
			style: 'margin-top:6px;',
		});
		const nameInp = el('input', {
			type: 'text',
			placeholder: 'Preset name',
			style: 'flex:1;',
		});
		saveRow.appendChild(nameInp);
		saveRow.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn gospb-btn-primary',
					onclick() {
						if (!nameInp.value) {
							return;
						}
						ajax('gospb_save_preset', {
							name: nameInp.value,
							state: JSON.stringify(state),
						})
							.then(function () {
								return ajax('gospb_get_presets', {});
							})
							.then(function (all) {
								presets = all || {};
								rerender();
							});
					},
				},
				['Save current']
			)
		);
		pCard.appendChild(saveRow);

		pCard.appendChild(
			el(
				'div',
				{
					style: 'margin-top:8px;font-size:11px;font-weight:600;color:#646970;',
				},
				['Starter templates:']
			)
		);
		STARTER_TEMPLATES.forEach(function (t) {
			pCard.appendChild(
				el(
					'button',
					{
						class: 'gospb-btn',
						style: 'margin-top:3px;width:100%;text-align:left;',
						onclick() {
							state = t.state();
							rerender();
						},
					},
					[t.name]
				)
			);
		});

		pCard.appendChild(
			el(
				'button',
				{
					class: 'gospb-btn gospb-btn-danger',
					style: 'margin-top:8px;width:100%;',
					onclick() {
						// eslint-disable-next-line no-alert,no-undef
						if (!confirm('Reset the builder to a blank state?')) {
							return;
						}
						state = defaultState();
						rerender();
					},
				},
				['Reset everything']
			)
		);

		side.appendChild(pCard);

		setTimeout(renderWarnings, 0);

		return side;
	}

	function renderWarnings() {
		const target = document.getElementById('gospb-warnings');
		if (!target) {
			return;
		}
		target.innerHTML = '';
		const warns = validate();
		if (!warns.length) {
			target.appendChild(
				el('div', { class: 'gospb-warn info' }, [
					'✓ No issues detected.',
				])
			);
			return;
		}
		warns.forEach(function (w) {
			target.appendChild(
				el('div', { class: 'gospb-warn ' + w.level }, [w.msg])
			);
		});
	}

	function updatePreview() {
		const pre = document.getElementById('gospb-preview');
		if (pre) {
			pre.textContent = buildShortcode();
		}
		renderWarnings();
		saveDraft();
	}

	function runShortcode(sc) {
		return ajax('gospb_test_run', { shortcode: sc }).then(function (d) {
			return d.html;
		});
	}

	/* ------------------------------------------------------------------ */
	/* Boot                                                                 */
	/* ------------------------------------------------------------------ */

	Promise.all([
		loadForms(),
		ajax('gospb_get_presets', {})
			.then(function (p) {
				presets = p || {};
			})
			.catch(function () {}),
	])
		.then(function () {
			const ids =
				state.target.mode === 'forms' ? state.target.form_ids : [];
			return Promise.all(ids.map(loadSchema));
		})
		.then(rerender)
		.catch(function (err) {
			root.innerHTML =
				'<div class="notice notice-error"><p>Failed to initialize builder: ' +
				(err.message || err) +
				'</p></div>';
		});
})();
