/**
 * PeepSo admin configuration search.
 *
 * Surfaces every PeepSo configuration option in the native WordPress Command Palette (⌘K / Ctrl+K).
 * The full option index is fetched once from GET /peepso/v1/config_search and filtered client-side;
 * matches are registered as palette commands whose callback navigates to the option's tab and
 * #field_ anchor.
 *
 * It also handles the landing experience on the config screen: after arriving at #field_x it scrolls
 * to and highlights that field — or, when the field is hidden because a parent switch is disabled,
 * highlights the parent instead and shows a brief note.
 *
 * No build step — this is a hand-maintained file loaded directly (see classes/admin.php). It depends
 * on wp-commands / wp-data / wp-element (WordPress 6.3+). On older WordPress those handles are absent,
 * WordPress skips this script, and the feature is simply not present.
 *
 * Globals: window.wp (data/element), window.peepsoConfigSearch ({ rest, nonce, i18n }).
 */
(function (wp, cfg) {
	'use strict';

	if (!wp || !wp.data || !wp.element || !cfg) {
		return;
	}

	var dispatch = wp.data.dispatch('core/commands');
	if (!dispatch || typeof dispatch.registerCommandLoader !== 'function') {
		return; // command palette unavailable on this WordPress
	}

	// Brand term(s) that every PeepSo option implicitly matches, so "peepso redirect" narrows within
	// PeepSo settings instead of filtering to only options that literally contain "peepso".
	var BRAND = ((cfg.brand && cfg.brand.length) ? cfg.brand : ['peepso']).map(function (s) {
		return String(s).toLowerCase();
	});

	/* ---------------------------------------------------------------------------------------------
	 * One-time index fetch (shared across every hook render)
	 * ------------------------------------------------------------------------------------------ */

	var INDEX = null;          // { options:[…], parents:{child:parent}, tabs:[…] }
	var fetchPromise = null;
	var lastFetchAt = 0;

	function loadIndex() {
		if (fetchPromise) {
			return fetchPromise;
		}
		// If we only have a partial index (server-side build still warming), throttle re-fetches.
		if (INDEX && INDEX.meta && INDEX.meta.complete === false && (Date.now() - lastFetchAt) < 1500) {
			return Promise.resolve(INDEX);
		}
		lastFetchAt = Date.now();
		fetchPromise = window.fetch(cfg.rest, {
			credentials: 'same-origin',
			headers: cfg.nonce ? { 'X-WP-Nonce': cfg.nonce } : {}
		})
			.then(function (r) { return r.json(); })
			.then(function (data) {
				INDEX = (data && data.options) ? data : { options: [], parents: {}, tabs: [] };
				// A partial index (cache still cold, built outside admin context) is upgraded to the
				// full one on a later fetch once the background build finishes — so allow re-fetching.
				if (INDEX.meta && INDEX.meta.complete === false) {
					fetchPromise = null;
				}
				return INDEX;
			})
			.catch(function () {
				INDEX = { options: [], parents: {}, tabs: [] };
				fetchPromise = null;
				return INDEX;
			});
		return fetchPromise;
	}

	/* ---------------------------------------------------------------------------------------------
	 * Client-side scoring / filtering
	 * ------------------------------------------------------------------------------------------ */

	// Score a single query token against one option's fields. 0 means the token is absent everywhere.
	function tokenScore(opt, token) {
		var label = opt.label.toLowerCase();
		// The field key rendered as words, e.g. always_link_to_peepso_profile -> "always link to peepso
		// profile", so an option is findable by its key too (a raw identifier — never translated).
		var key = opt.name.replace(/[_-]+/g, ' ').toLowerCase();

		if (label === token) { return 100; }
		if (label.indexOf(token) === 0) { return 90; }                        // label starts with the token
		if (label.indexOf(' ' + token) > -1) { return 80; }                   // token starts a word in the label
		if (label.indexOf(token) > -1) { return 65; }                         // token anywhere in the label
		if ((' ' + key + ' ').indexOf(' ' + token + ' ') > -1) { return 55; } // token is a whole word of the key
		if (opt.keywords) {
			for (var k = 0; k < opt.keywords.length; k++) {
				if (opt.keywords[k].toLowerCase().indexOf(token) > -1) { return 50; }
			}
		}
		if (key.indexOf(token) > -1) { return 45; }                           // token within the key
		if ((opt.tab_label || '').toLowerCase().indexOf(token) > -1) { return 42; }
		if ((opt.group || '').toLowerCase().indexOf(token) > -1) { return 38; }
		if ((opt.descript || '').toLowerCase().indexOf(token) > -1) { return 30; }
		for (var b = 0; b < BRAND.length; b++) {
			if (BRAND[b].indexOf(token) > -1) { return 8; } // brand term: matches every option, low weight
		}
		return 0;
	}

	// The command palette (cmdk) uses a command's value (searchLabel) as a `data-value` attribute AND
	// queries it back with document.querySelector('[data-value="…"]'). Quotes/backslashes in the value
	// produce an invalid selector and throw, so strip them here. The value is only used for matching —
	// never displayed — so this is lossless for the user (the visible `label` keeps its quotes).
	function safeValue(s) {
		return String(s).replace(/["'\\]/g, ' ').replace(/\s+/g, ' ').trim();
	}

	// Fuzzy, word-based matching: split the query into tokens and require EVERY token to appear
	// somewhere in the option (label / field name / description / tab / group), in any order — so
	// "post stream" finds "Post new purchases to stream". Ranked by the sum of the per-token scores.
	// (The command palette then applies its own gap-tolerant fuzzy ranking on top of these candidates.)
	function searchIndex(index, raw) {
		var tokens = (raw || '').toLowerCase().split(/\s+/).filter(Boolean);
		if (!tokens.length || !index || !index.options) {
			return [];
		}
		var scored = [];
		for (var i = 0; i < index.options.length; i++) {
			var opt = index.options[i];
			var total = 0;
			var matchedAll = true;
			for (var t = 0; t < tokens.length; t++) {
				var s = tokenScore(opt, tokens[t]);
				if (s === 0) { matchedAll = false; break; }
				total += s;
			}
			if (matchedAll) {
				scored.push({ s: total, o: opt });
			}
		}
		scored.sort(function (a, b) {
			return (b.s - a.s) || a.o.label.localeCompare(b.o.label);
		});
		return scored.slice(0, 50).map(function (x) { return x.o; });
	}

	/* ---------------------------------------------------------------------------------------------
	 * The command loader (a React hook, invoked by the palette on every keystroke)
	 * ------------------------------------------------------------------------------------------ */

	var createElement = wp.element.createElement;
	// The PeepSo logo mark, so results read as PeepSo settings at a glance. Falls back to a dashicon.
	var peepsoIcon = cfg.icon
		? createElement('img', {
			src: cfg.icon,
			alt: '',
			width: 20,
			height: 18,
			style: { display: 'block', width: '20px', height: '18px' }
		})
		: createElement('span', {
			className: 'dashicons dashicons-admin-generic',
			style: { fontSize: '20px', width: '20px', height: '20px' }
		});

	function useConfigCommands(props) {
		var searchTerm = props.search;
		var state = wp.element.useState(INDEX);
		var index = state[0];
		var setIndex = state[1];

		wp.element.useEffect(function () {
			if (!index || (index.meta && index.meta.complete === false)) {
				loadIndex().then(setIndex);
			}
		}, [searchTerm]);

		if (!index) {
			return { commands: [], isLoading: true };
		}

		var commands = searchIndex(index, searchTerm).map(function (opt) {
			var isGroup = !!opt.is_group;
			var kw = opt.keywords || [];
			var keyWords = opt.name.replace(/[_-]+/g, ' '); // key as words
			var base = [opt.label, keyWords, opt.descript || '', opt.tab_label, opt.group || '', kw.join(' ')].join(' ');
			return {
				name: (isGroup ? 'peepso-group-' : 'peepso-config-') + opt.tab + '-' + opt.name,
				// Groups ("boxes") read as "Tab › Group"; fields as "Tab: Field".
				label: opt.tab_label + (isGroup ? ' › ' : ': ') + opt.label,
				// Prepend the raw query so the palette (cmdk) always keeps what our loader already
				// matched — our token / synonym / brand / group logic is the source of truth for
				// filtering. cmdk otherwise re-filters loader results by a subsequence over this value.
				searchLabel: safeValue(searchTerm + ' ' + base),
				icon: peepsoIcon,
				// Unknown category => the palette shows no right-side label (CATEGORY_LABELS has no entry)
				// and keeps our icon (CATEGORY_ICONS has no entry). See wp-includes/js/dist/commands.js.
				category: 'peepso-config',
				callback: function (args) {
					if (args && args.close) {
						args.close();
					}
					window.location.href = opt.url;
				}
			};
		});

		return { commands: commands, isLoading: false };
	}

	dispatch.registerCommandLoader({
		name: 'peepso/config-options',
		hook: useConfigCommands
	});

	/* ---------------------------------------------------------------------------------------------
	 * Landing highlight on the config screen (admin.php?page=peepso_config#field_…)
	 * ------------------------------------------------------------------------------------------ */

	function isConfigPage() {
		return location.href.indexOf('page=peepso_config') > -1;
	}

	function onConfigLanding() {
		var hash = location.hash || '';

		// Group ("box") result: highlight the whole metabox that holds the group's first field.
		if (hash.indexOf('#psgroup_') === 0) {
			var gfield = document.getElementById('field_' + hash.slice('#psgroup_'.length));
			highlight((gfield && closest(gfield, '.postbox')) || gfield);
			return;
		}

		if (hash.indexOf('#field_') !== 0) {
			return;
		}

		var el = document.getElementById(hash.slice(1)); // <div id="field_xxx">
		if (!el) {
			return;
		}

		var name = hash.slice('#field_'.length);
		var target = el;
		var note = '';

		if (el.offsetParent === null) { // hidden — its parent switch is off
			var parent = resolveVisibleParent(el, name);
			if (parent) {
				target = parent;
			}
			note = (cfg.i18n && cfg.i18n.hidden) || 'This option is hidden — enable its parent setting to reveal it.';
		}

		highlight(target);
		if (note) {
			toast(note);
		}
	}

	/**
	 * Resolve a hidden field to the visible switch/checkbox that gates it:
	 *   1) a row declaring this field via data-children (the formal link, e.g. moderation);
	 *   2) otherwise the nearest PRECEDING visible row carrying a toggle, in the same metabox.
	 *
	 * PeepSo's per-tab config scripts hide a run of rows *after* a toggle — either the whole
	 * tail (nextAll) or up to the next header (nextUntil '[id$="_header"]') — so the closest
	 * visible toggle *above* the hidden field is the control that reveals it. It must be the
	 * nearest one above, not merely the last visible toggle in the box: a box can hold several
	 * independent toggle sections (e.g. Blog Posts' "Author Profiles" box has Two column /
	 * Featured images / Author box), and the last visible toggle may sit in a *later* section
	 * that has nothing to do with the hidden field. Walking upward also transparently skips an
	 * intermediate toggle that is itself hidden (nested gating) and lands on the real gate.
	 */
	function resolveVisibleParent(el, name) {
		var byChildren = document.querySelector('[data-children~="' + cssEscape(name) + '"]');
		if (byChildren && byChildren.offsetParent !== null) {
			return byChildren;
		}

		var box = closest(el, '.postbox') || document;
		var groups = box.querySelectorAll ? box.querySelectorAll('.form-group') : [];
		var idx = -1;
		for (var i = 0; i < groups.length; i++) {
			if (groups[i] === el) { idx = i; break; }
		}
		for (var j = idx - 1; j >= 0; j--) {
			var grp = groups[j];
			if (grp.offsetParent === null) { continue; } // hidden — not the visible gate
			if (grp.querySelector('input.ace-switch, input[type="checkbox"]')) {
				return grp;
			}
		}
		return null;
	}

	function highlight(el) {
		if (!el || !el.scrollIntoView) {
			return;
		}
		el.scrollIntoView({ behavior: 'smooth', block: 'center' });
		el.classList.add('peepso-config-hit');
		window.setTimeout(function () { el.classList.remove('peepso-config-hit'); }, 2600);
	}

	var toastEl = null;
	function toast(msg) {
		if (!toastEl) {
			toastEl = document.createElement('div');
			toastEl.className = 'peepso-config-toast';
			document.body.appendChild(toastEl);
		}
		toastEl.textContent = msg;
		// force reflow so the transition runs even if reused
		void toastEl.offsetWidth;
		toastEl.classList.add('is-visible');
		window.setTimeout(function () { toastEl.classList.remove('is-visible'); }, 5000);
	}

	/* --- tiny DOM helpers (avoid relying on jQuery / very old browsers) ------------------------- */

	function closest(el, selector) {
		if (el && el.closest) {
			return el.closest(selector);
		}
		while (el && el.nodeType === 1) {
			if (matches(el, selector)) { return el; }
			el = el.parentElement;
		}
		return null;
	}

	function matches(el, selector) {
		var fn = el.matches || el.msMatchesSelector || el.webkitMatchesSelector;
		return fn ? fn.call(el, selector) : false;
	}

	function cssEscape(s) {
		return String(s).replace(/["\\]/g, '\\$&');
	}

	/* ---------------------------------------------------------------------------------------------
	 * Styles (self-contained) + bootstrap
	 * ------------------------------------------------------------------------------------------ */

	var style = document.createElement('style');
	style.textContent =
		// Let long command-palette titles wrap to two lines instead of truncating to one (core forces
		// white-space:nowrap + ellipsis). Applies to the whole palette; a benign, more-readable default.
		'.commands-command-menu__item-label{white-space:normal!important;display:-webkit-box!important;' +
		'-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;line-height:1.35}' +
		'.peepso-config-hit{animation:peepsoConfigHit 2.4s ease-out;border-radius:4px}' +
		'@keyframes peepsoConfigHit{0%,25%{box-shadow:0 0 0 2px #00b9eb,0 0 0 6px rgba(0,185,235,.25);background:rgba(0,185,235,.08)}100%{box-shadow:none;background:transparent}}' +
		'.peepso-config-toast{position:fixed;left:50%;bottom:32px;transform:translateX(-50%) translateY(20px);z-index:100010;background:#1d2327;color:#fff;padding:11px 18px;border-radius:6px;font-size:13px;line-height:1.4;max-width:420px;box-shadow:0 6px 24px rgba(0,0,0,.3);opacity:0;transition:opacity .25s,transform .25s;pointer-events:none}' +
		'.peepso-config-toast.is-visible{opacity:1;transform:translateX(-50%) translateY(0)}';
	document.head.appendChild(style);

	if (isConfigPage() && (location.hash.indexOf('#field_') === 0 || location.hash.indexOf('#psgroup_') === 0)) {
		// Run after the config page's own show/hide JS has executed so offsetParent is accurate.
		if (document.readyState === 'complete') {
			window.setTimeout(onConfigLanding, 80);
		} else {
			window.addEventListener('load', function () { window.setTimeout(onConfigLanding, 80); });
		}
	}

})(window.wp, window.peepsoConfigSearch);
