/**
 * Cache module — Critical CSS browser-side extractor
 *
 * Designed to run inside an iframe whose parent (cache-admin.js) calls:
 *
 *   iframe.contentWindow.postMessage({ command: 'extract', viewport: 'mobile'|'desktop' }, '*');
 *
 * On completion it posts back:
 *
 *   parent.postMessage({ type: 'be-critical-result', viewport, css, used }, '*');
 *
 * Two outputs:
 *   - `css`  : rules whose selector matches an element in the viewport ABOVE THE FOLD
 *   - `used` : rules whose selector matches an element ANYWHERE in the document (for RUCSS)
 *
 * Limitations:
 *   - CSS that is loaded from a foreign origin without CORS is unreadable; we
 *     skip those stylesheets and append a flag to the result so the user
 *     knows the extraction was partial.
 *   - We don't intercept @import inside foreign-origin CSS (browser does, but
 *     we can't enumerate the rules).
 *
 * @package BlaminhorEssentials
 */
(function () {
	'use strict';

	// Pseudo-classes and pseudo-elements that querySelectorAll either throws on
	// (older browsers, unsupported `:has`) or that do not correspond to a real
	// DOM state at extraction time (`:hover`, `:focus`, `:visited`...). Strip
	// them before re-testing so we can ask "does the BASE selector match any
	// element above the fold?" and keep the rule on a real positive instead of
	// falling back to a permissive catch-all.
	var PSEUDO_STRIP_RE = /::[\w-]+|:[\w-]+(\([^)]*\))?/g;

	function safeQueryFirst(selector) {
		try {
			return document.querySelectorAll(selector);
		} catch (e) {
			var stripped = String(selector).replace(PSEUDO_STRIP_RE, '').trim();
			if (!stripped) { return null; }
			try {
				return document.querySelectorAll(stripped);
			} catch (e2) {
				return null;
			}
		}
	}

	function ruleMatchesVisible(selector) {
		var nodes = safeQueryFirst(selector);
		if (!nodes) { return false; }
		for (var i = 0; i < nodes.length; i++) {
			var rect = nodes[i].getBoundingClientRect();
			if (rect.top < window.innerHeight && rect.bottom > 0 && rect.width > 0 && rect.height > 0) {
				return true;
			}
		}
		return false;
	}

	function ruleMatchesAny(selector) {
		var nodes = safeQueryFirst(selector);
		return !!(nodes && nodes.length > 0);
	}

	function selectorPart(selector) {
		// Split compound selector on TOP-LEVEL commas only. A naive split(',')
		// fragments modern selectors that legitimately contain commas inside
		// parens (`:is(a,b)`, `:where(a,b)`, `:not(a,b)`, `:has(a,b)`),
		// brackets (`[name="a,b"]`) or strings — producing garbage like
		// `img:is([sizes=auto i]`, `[sizes^="auto`, `" i])` from a single
		// valid rule, which then gets re-emitted as a malformed selector
		// and pollutes the critical CSS with a leading `" i])` fragment.
		var parts = [];
		var depth = 0;
		var quote = '';
		var current = '';
		for (var i = 0; i < selector.length; i++) {
			var ch = selector.charAt(i);
			if (quote) {
				current += ch;
				if (ch === quote && selector.charAt(i - 1) !== '\\') { quote = ''; }
				continue;
			}
			if (ch === '"' || ch === "'") { current += ch; quote = ch; continue; }
			if (ch === '(' || ch === '[') { current += ch; depth++; continue; }
			if (ch === ')' || ch === ']') { current += ch; if (depth > 0) { depth--; } continue; }
			if (ch === ',' && depth === 0) {
				if (current.trim() !== '') { parts.push(current.trim()); }
				current = '';
				continue;
			}
			current += ch;
		}
		if (current.trim() !== '') { parts.push(current.trim()); }
		return parts;
	}

	function processSheet(sheet, criticalParts, usedParts, partial) {
		var rules;
		try {
			rules = sheet.cssRules;
		} catch (e) {
			partial.value = true;
			return;
		}
		if (!rules) { return; }

		for (var i = 0; i < rules.length; i++) {
			var rule = rules[i];
			switch (rule.type) {
				case 1: // CSSStyleRule
					if (!rule.selectorText) { break; }
					var parts = selectorPart(rule.selectorText);
					var visibleParts = parts.filter(ruleMatchesVisible);
					var usedSubParts = parts.filter(ruleMatchesAny);
					if (visibleParts.length > 0) {
						criticalParts.push(rebuildRule(rule, visibleParts));
					}
					if (usedSubParts.length > 0) {
						usedParts.push(rebuildRule(rule, usedSubParts));
					}
					break;
				case 4: // CSSMediaRule
					if (window.matchMedia(rule.media.mediaText).matches) {
						var innerCritical = [];
						var innerUsed = [];
						processSheet({ cssRules: rule.cssRules }, innerCritical, innerUsed, partial);
						if (innerCritical.length > 0) {
							criticalParts.push('@media ' + rule.media.mediaText + '{' + innerCritical.join('') + '}');
						}
						if (innerUsed.length > 0) {
							usedParts.push('@media ' + rule.media.mediaText + '{' + innerUsed.join('') + '}');
						}
					}
					break;
				case 5: // CSSFontFaceRule
					criticalParts.push(rule.cssText);
					usedParts.push(rule.cssText);
					break;
				case 6: // CSSPageRule
					break;
				case 7: // CSSKeyframesRule
					criticalParts.push(rule.cssText);
					usedParts.push(rule.cssText);
					break;
				case 11: // CSSCounterStyleRule
				case 12: // CSSSupportsRule
					if (rule.cssRules) {
						processSheet(rule, criticalParts, usedParts, partial);
					}
					break;
				default:
					break;
			}
		}
	}

	function rebuildRule(rule, selectorParts) {
		// Reconstruct: `selector1, selector2 { body }` from filtered parts.
		var body = rule.cssText.replace(/^[^{]*\{/, '{');
		return selectorParts.join(',') + body;
	}

	function extract(viewport) {
		var criticalParts = [];
		var usedParts = [];
		var partial = { value: false };

		var sheets = Array.prototype.slice.call(document.styleSheets);
		for (var i = 0; i < sheets.length; i++) {
			processSheet(sheets[i], criticalParts, usedParts, partial);
		}

		return {
			type: 'be-critical-result',
			viewport: viewport,
			css: criticalParts.join(''),
			used: usedParts.join(''),
			partial: partial.value,
			// Counters to help diagnose oversized payloads: if `sheets_count`
			// is high but `partial` is true, much of the CSS came from
			// origins without CORS and the result is incomplete; if both are
			// non-trivial, the page just genuinely uses a lot of CSS.
			sheets_count: sheets.length
		};
	}

	window.addEventListener('message', function (e) {
		if (!e.data || e.data.command !== 'extract') { return; }
		// Wait for next paint so getBoundingClientRect is accurate.
		requestAnimationFrame(function () {
			requestAnimationFrame(function () {
				try {
					var result = extract(e.data.viewport || 'desktop');
					(e.source || window.parent).postMessage(result, '*');
				} catch (err) {
					(e.source || window.parent).postMessage({
						type: 'be-critical-result',
						error: String(err && err.message ? err.message : err)
					}, '*');
				}
			});
		});
	}, false);
})();
