/**
 * ThemeREX Auto Dealer — Frontend JavaScript
 *
 * Vanilla JS only, no jQuery. Handles compare, favorites, recently viewed,
 * gallery thumbnail swap, and sticky mobile contact bar.
 *
 * @package ThemeREX\AutoDealer
 * @version 1.0.0
 */

(function () {
	'use strict';

	/* ======================================================================
	   Constants
	   ====================================================================== */

	var STORAGE_KEYS = {
		favorites: 'trx_ad_favorites',
		compare: 'trx_ad_compare',
		recentlyViewed: 'trx_ad_recently_viewed'
	};

	var MAX_COMPARE = 4;
	var MAX_RECENT = 6;

	// Automotive acronyms that must stay fully uppercase when humanizing a
	// slug for display (otherwise "suv" -> "Suv", "cpo" -> "Cpo"). Keys are
	// lowercase; mirrors the server-side display map.
	var ACRONYMS = {
		suv: 1, cpo: 1, awd: 1, fwd: 1, rwd: 1, '4wd': 1, '4x4': 1,
		rv: 1, ev: 1, phev: 1, hev: 1, bmw: 1, gmc: 1, vin: 1, mpv: 1, ute: 1
	};

	// Title-case a slug while preserving known acronyms: "suv" -> "SUV",
	// "coming_soon" -> "Coming Soon", "long range" -> "Long Range".
	function humanizeSlug(value) {
		return String(value).replace(/[_-]+/g, ' ').replace(/\S+/g, function (word) {
			if (ACRONYMS[word.toLowerCase()]) { return word.toUpperCase(); }
			return word.charAt(0).toUpperCase() + word.slice(1);
		});
	}

	/* ======================================================================
	   Utility
	   ====================================================================== */

	/**
	 * Safely read from localStorage / sessionStorage.
	 *
	 * @param {Storage} store  localStorage or sessionStorage.
	 * @param {string}  key    Storage key.
	 * @returns {Array}        Parsed array or empty array.
	 */
	function readStore(store, key) {
		try {
			var raw = store.getItem(key);
			if (raw) {
				var parsed = JSON.parse(raw);
				if (!Array.isArray(parsed)) {
					return [];
				}
				// Normalise: numeric strings only, dedupe. Stops bad legacy
				// entries (empty strings, NaN, "undefined") from filling up
				// the cap or causing silent removal-on-add behaviour.
				var seen = {};
				var out = [];
				for (var i = 0; i < parsed.length; i++) {
					var v = parsed[i];
					if (v === null || typeof v === 'undefined') { continue; }
					var n = parseInt(v, 10);
					if (isNaN(n) || n <= 0) { continue; }
					var s = String(n);
					if (seen[s]) { continue; }
					seen[s] = 1;
					out.push(s);
				}
				return out;
			}
		} catch (e) {
			// Ignore parse errors.
		}
		return [];
	}

	/**
	 * Safely write to localStorage / sessionStorage.
	 *
	 * @param {Storage} store  localStorage or sessionStorage.
	 * @param {string}  key    Storage key.
	 * @param {Array}   value  Array to store.
	 */
	function writeStore(store, key, value) {
		try {
			store.setItem(key, JSON.stringify(value));
		} catch (e) {
			// Storage full or unavailable.
		}
	}

	/**
	 * Get localized string safely.
	 *
	 * @param {string} key     i18n key.
	 * @param {string} fallback Default string.
	 * @returns {string}
	 */
	function i18n(key, fallback) {
		if (typeof trxAdData !== 'undefined' && trxAdData.i18n && trxAdData.i18n[key]) {
			return trxAdData.i18n[key];
		}
		return fallback || '';
	}

	/* ======================================================================
	   Toast — transient confirmation with an SR-only live region. The i18n
	   strings (addedToFavorites etc.) shipped already; this is what surfaces
	   them (UAT #15: no "Added to favorites" feedback appeared on toggle).
	   ====================================================================== */

	var Toast = {
		el: null,
		region: null,
		timer: null,
		ensure: function () {
			if (!this.region) {
				this.region = document.createElement('div');
				this.region.className = 'trx-ad-sr-only';
				this.region.setAttribute('aria-live', 'polite');
				this.region.setAttribute('role', 'status');
				document.body.appendChild(this.region);
			}
			if (!this.el) {
				this.el = document.createElement('div');
				this.el.className = 'trx-ad-toast';
				document.body.appendChild(this.el);
			}
		},
		show: function (msg) {
			if (!msg) { return; }
			this.ensure();
			// Re-set the live region text so screen readers re-announce.
			this.region.textContent = '';
			this.region.textContent = msg;
			this.el.textContent = msg;
			this.el.classList.add('is-visible');
			var self = this;
			clearTimeout(this.timer);
			this.timer = setTimeout(function () {
				self.el.classList.remove('is-visible');
			}, 2200);
		}
	};

	/* ======================================================================
	   Favorites

	   Two backends, one public API:
	     - 'local' (default / anonymous): localStorage, per-device.
	     - 'rest'  (Pro + logged-in): user-meta via the /favorites REST routes,
	       so a user's favorites sync across devices and browsers. Pro flips the
	       backend by localizing trxAdData.favoritesBackend = 'rest' (UAT #36 —
	       favorites added in one browser didn't appear in another because the
	       client only ever touched localStorage; the Pro REST store was never
	       called).
	   ====================================================================== */

	var Favorites = {
		_backend: null,
		_remote: null,        // string[] of ids, authoritative in 'rest' mode
		_loadPromise: null,

		backend: function () {
			if (this._backend === null) {
				this._backend = (typeof trxAdData !== 'undefined'
					&& trxAdData.favoritesBackend === 'rest'
					&& trxAdData.restUrl) ? 'rest' : 'local';
			}
			return this._backend;
		},

		_endpoint: function () {
			return trxAdData.restUrl + 'favorites';
		},

		/* Load the authoritative list from user-meta (rest mode). Memoized: the
		   initial paint and the favorites-page hydration share one request. */
		loadRemote: function () {
			var self = this;
			if (this.backend() !== 'rest') { return Promise.resolve(this.getAll()); }
			if (this._loadPromise) { return this._loadPromise; }
			this._loadPromise = fetch(this._endpoint(), {
				headers: { 'X-WP-Nonce': trxAdData.restNonce, 'Accept': 'application/json' },
				credentials: 'same-origin'
			})
				.then(function (r) { return r.ok ? r.json() : null; })
				.then(function (data) {
					self._remote = (data && Array.isArray(data.favorites))
						? data.favorites.map(String)
						: (self._remote || []);
					return self._remote;
				})
				.catch(function () { self._remote = self._remote || []; return self._remote; });
			return this._loadPromise;
		},

		getAll: function () {
			if (this.backend() === 'rest') {
				return this._remote ? this._remote.slice() : [];
			}
			return readStore(localStorage, STORAGE_KEYS.favorites);
		},

		has: function (id) {
			return this.getAll().indexOf(String(id)) !== -1;
		},

		toggle: function (id) {
			if (this.backend() === 'rest') {
				return this._toggleRemote(id);
			}

			var list = this.getAll();
			var strId = String(id);
			var idx = list.indexOf(strId);

			if (idx !== -1) {
				list.splice(idx, 1);
				writeStore(localStorage, STORAGE_KEYS.favorites, list);
				this.updateUI(strId, false);
				Toast.show(i18n('removedFromFavorites', 'Removed from favorites'));
				return false;
			}

			list.push(strId);
			writeStore(localStorage, STORAGE_KEYS.favorites, list);
			this.updateUI(strId, true);
			Toast.show(i18n('addedToFavorites', 'Added to favorites'));
			return true;
		},

		/* Optimistic REST toggle: flip the UI immediately, reconcile with the
		   server response (the API returns the authoritative list). */
		_toggleRemote: function (id) {
			var self = this;
			var strId = String(id);
			var have = this.getAll().indexOf(strId) !== -1;
			var headers = { 'X-WP-Nonce': trxAdData.restNonce, 'Accept': 'application/json' };

			var reconcile = function (data) {
				if (data && Array.isArray(data.favorites)) {
					self._remote = data.favorites.map(String);
				}
				self.updateUI(strId, self.getAll().indexOf(strId) !== -1);
				self._emitChange();
			};

			self._remote = self.getAll();
			if (have) {
				self._remote = self._remote.filter(function (x) { return x !== strId; });
				self.updateUI(strId, false);
				Toast.show(i18n('removedFromFavorites', 'Removed from favorites'));
				self._emitChange();
				fetch(self._endpoint() + '/' + encodeURIComponent(parseInt(id, 10)), {
					method: 'DELETE', headers: headers, credentials: 'same-origin'
				}).then(function (r) { return r.ok ? r.json() : null; }).then(reconcile).catch(function () {});
				return false;
			}

			if (self._remote.indexOf(strId) === -1) { self._remote.push(strId); }
			self.updateUI(strId, true);
			Toast.show(i18n('addedToFavorites', 'Added to favorites'));
			self._emitChange();
			fetch(self._endpoint(), {
				method: 'POST',
				headers: { 'X-WP-Nonce': trxAdData.restNonce, 'Accept': 'application/json', 'Content-Type': 'application/json' },
				credentials: 'same-origin',
				body: JSON.stringify({ vehicle_id: parseInt(id, 10) })
			}).then(function (r) { return r.ok ? r.json() : null; }).then(reconcile).catch(function () {});
			return true;
		},

		/* Notify same-tab listeners (the favorites page) to re-hydrate. The
		   native `storage` event only fires for localStorage cross-tab writes,
		   so rest-mode changes need their own signal. */
		_emitChange: function () {
			try { document.dispatchEvent(new CustomEvent('trx-ad-favorites-changed')); } catch (e) { /* old browser */ }
		},

		updateUI: function (id, active) {
			var buttons = document.querySelectorAll('.trx-ad-card__action--favorite[data-vehicle-id="' + id + '"]');
			buttons.forEach(function (btn) {
				btn.classList.toggle('is-active', active);
				btn.setAttribute('aria-pressed', active ? 'true' : 'false');
				btn.setAttribute('aria-label', active ? i18n('removeFromFavorites', 'Remove from favorites') : i18n('addToFavorites', 'Add to favorites'));
				// Pro button carries a visible text label + localized strings in
				// data-label-on/off — keep it in sync with the toggle state.
				var span = btn.querySelector('span');
				var on = btn.getAttribute('data-label-on');
				var off = btn.getAttribute('data-label-off');
				if (span && (on || off)) {
					span.textContent = active ? (on || span.textContent) : (off || span.textContent);
				}
			});
		},

		initAll: function () {
			var self = this;
			if (this.backend() === 'rest') {
				this.loadRemote().then(function (ids) {
					ids.forEach(function (id) { self.updateUI(id, true); });
				});
				return;
			}
			this.getAll().forEach(function (id) {
				self.updateUI(id, true);
			});
		}
	};

	/* ======================================================================
	   Compare (localStorage — survives tab/window changes; capped at 4)
	   ====================================================================== */

	var Compare = {
		getAll: function () {
			return readStore(localStorage, STORAGE_KEYS.compare);
		},

		has: function (id) {
			return this.getAll().indexOf(String(id)) !== -1;
		},

		toggle: function (id) {
			var list = this.getAll();
			var strId = String(id);
			var idx = list.indexOf(strId);

			if (idx !== -1) {
				list.splice(idx, 1);
				writeStore(localStorage, STORAGE_KEYS.compare, list);
				this.updateUI(strId, false);
				this.updateCount();
				Toast.show(i18n('removedFromCompare', 'Removed from compare'));
				return false;
			}

			if (list.length >= MAX_COMPARE) {
				var msg = i18n('maxCompare', 'Maximum ' + MAX_COMPARE + ' vehicles can be compared');
				if (typeof trxAdData !== 'undefined') {
					alert(msg);
				}
				return null;
			}

			list.push(strId);
			writeStore(localStorage, STORAGE_KEYS.compare, list);
			this.updateUI(strId, true);
			this.updateCount();
			Toast.show(i18n('addedToCompare', 'Added to compare'));
			return true;
		},

		updateUI: function (id, active) {
			var buttons = document.querySelectorAll('[data-vehicle-id="' + id + '"].trx-ad-card__action--compare, .trx-ad-card__action--compare[data-vehicle-id="' + id + '"]');
			buttons.forEach(function (btn) {
				btn.classList.toggle('is-active', active);
				btn.setAttribute('aria-pressed', active ? 'true' : 'false');
				btn.setAttribute('aria-label', active ? i18n('removeFromCompare', 'Remove from compare') : i18n('addToCompare', 'Add to compare'));
			});
		},

		updateCount: function () {
			var count = this.getAll().length;
			var badges = document.querySelectorAll('.trx-ad-compare-count');
			badges.forEach(function (badge) {
				badge.textContent = String(count);
				badge.style.display = count > 0 ? '' : 'none';
			});
			// Header compare-link with `hide_if_empty=true` collapses entirely
			// when count is 0. CSS keys off [data-empty="1"] so we don't need
			// to touch inline display style here.
			var hideEmpty = document.querySelectorAll('[data-trx-ad-compare-link-hide-empty]');
			hideEmpty.forEach(function (link) {
				if (count > 0) {
					link.removeAttribute('data-empty');
				} else {
					link.setAttribute('data-empty', '1');
				}
			});
		},

		initAll: function () {
			var self = this;
			var list = this.getAll();
			list.forEach(function (id) {
				self.updateUI(id, true);
			});
			this.updateCount();

			// One-shot stale-ID prune: if a stored compare ID no longer exists
			// on the server (deleted listing), drop it so it doesn't burn a
			// slot in the 4-item cap. Falls back silently when REST is
			// unavailable.
			if (typeof trxAdData === 'undefined' || !trxAdData.restUrl) {
				return;
			}
			list.forEach(function (id) {
				fetch(trxAdData.restUrl + 'vehicles/' + id, {
					headers: { 'Accept': 'application/json' },
					credentials: 'same-origin'
				}).then(function (r) {
					if (r.status === 404) {
						var current = self.getAll();
						var idx = current.indexOf(String(id));
						if (idx !== -1) {
							current.splice(idx, 1);
							writeStore(localStorage, STORAGE_KEYS.compare, current);
							self.updateUI(String(id), false);
							self.updateCount();
						}
					}
				}).catch(function () { /* network errors: leave entry alone */ });
			});
		}
	};

	/* ======================================================================
	   Recently Viewed (localStorage)
	   ====================================================================== */

	var RecentlyViewed = {
		getAll: function () {
			return readStore(localStorage, STORAGE_KEYS.recentlyViewed);
		},

		add: function (id) {
			if (!id) {
				return;
			}
			var list = this.getAll();
			var strId = String(id);

			// Remove if already present to move to front.
			var idx = list.indexOf(strId);
			if (idx !== -1) {
				list.splice(idx, 1);
			}

			list.unshift(strId);

			// Keep only last MAX_RECENT entries.
			if (list.length > MAX_RECENT) {
				list = list.slice(0, MAX_RECENT);
			}

			writeStore(localStorage, STORAGE_KEYS.recentlyViewed, list);
		},

		track: function () {
			var singleEl = document.querySelector('.trx-ad-single[data-vehicle-id]');
			if (singleEl) {
				var vehicleId = singleEl.getAttribute('data-vehicle-id');
				this.add(vehicleId);
			}
		}
	};

	/* ======================================================================
	   Gallery — thumbnail swap
	   ====================================================================== */

	function initGallery() {
		var gallery = document.getElementById('trx-ad-gallery');
		if (!gallery) {
			return;
		}

		var mainImg = document.getElementById('trx-ad-gallery-main-img');
		if (!mainImg) {
			return;
		}

		var thumbs = gallery.querySelectorAll('.trx-ad-single__gallery-thumb');

		thumbs.forEach(function (thumb) {
			thumb.addEventListener('click', function () {
				var fullSrc = this.getAttribute('data-full-src');
				if (!fullSrc) {
					return;
				}

				mainImg.src = fullSrc;

				// Update active state.
				thumbs.forEach(function (t) {
					t.classList.remove('trx-ad-single__gallery-thumb--active');
				});
				this.classList.add('trx-ad-single__gallery-thumb--active');
			});
		});
	}

	/* ======================================================================
	   Sticky Mobile Contact Bar
	   ====================================================================== */

	function initMobileBar() {
		var bar = document.getElementById('trx-ad-mobile-bar');
		if (!bar) {
			return;
		}

		var lastScrollY = window.scrollY;
		var ticking = false;
		var isVisible = false;

		function onScroll() {
			var currentScrollY = window.scrollY;

			// Show bar after scrolling down past 300px.
			if (currentScrollY > 300 && currentScrollY > lastScrollY) {
				if (!isVisible) {
					bar.classList.add('is-visible');
					isVisible = true;
				}
			}

			// Hide bar when scrolling back to top.
			if (currentScrollY < 100) {
				if (isVisible) {
					bar.classList.remove('is-visible');
					isVisible = false;
				}
			}

			lastScrollY = currentScrollY;
			ticking = false;
		}

		window.addEventListener('scroll', function () {
			if (!ticking) {
				window.requestAnimationFrame(onScroll);
				ticking = true;
			}
		}, { passive: true });
	}

	/* ======================================================================
	   Event Delegation
	   ====================================================================== */

	function initEventDelegation() {
		document.addEventListener('click', function (e) {
			// Favorite buttons.
			var favBtn = e.target.closest('.trx-ad-card__action--favorite');
			if (favBtn) {
				e.preventDefault();
				e.stopPropagation();
				var vehicleId = favBtn.getAttribute('data-vehicle-id');
				if (vehicleId) {
					Favorites.toggle(vehicleId);
				}
				return;
			}

			// Compare buttons.
			var cmpBtn = e.target.closest('.trx-ad-card__action--compare');
			if (cmpBtn) {
				e.preventDefault();
				e.stopPropagation();
				var cmpId = cmpBtn.getAttribute('data-vehicle-id');
				if (cmpId) {
					Compare.toggle(cmpId);
				}
				return;
			}
		});
	}

	/* ======================================================================
	   Init
	   ====================================================================== */

	/* ======================================================================
	   Recently Viewed — hydration shell
	   ====================================================================== */

	/**
	 * Fetch /vehicles/{id} and prune the ID from `storageKey` if the listing
	 * is gone (404 = trashed/deleted). Without this, stale localStorage IDs
	 * spam the console with 404s every page load forever.
	 *
	 * @param {number} id
	 * @param {string} storageKey  STORAGE_KEYS.* — which list to prune from
	 * @returns {Promise<object|null>}
	 */
	function fetchVehiclePruneOn404(id, storageKey) {
		return fetch(trxAdData.restUrl + 'vehicles/' + id, {
			headers: { 'X-WP-Nonce': trxAdData.restNonce, 'Accept': 'application/json' },
			credentials: 'same-origin'
		})
			.then(function (r) {
				if (r.ok) { return r.json(); }
				if (r.status === 404) {
					var list = readStore(localStorage, storageKey);
					var strId = String(id);
					var idx = list.indexOf(strId);
					if (idx !== -1) {
						list.splice(idx, 1);
						writeStore(localStorage, storageKey, list);
					}
				}
				return null;
			})
			.catch(function () { return null; });
	}

	function hydrateRecentlyViewed() {
		var section = document.querySelector('[data-trx-ad-recently-viewed]');
		if (!section || typeof trxAdData === 'undefined' || !trxAdData.restUrl) {
			return;
		}

		var grid    = section.querySelector('[data-trx-ad-rv-grid]');
		var exclude = parseInt(section.getAttribute('data-exclude') || '0', 10);
		var limit   = parseInt(section.getAttribute('data-limit')   || '4', 10);
		if (!grid) { return; }

		var ids = RecentlyViewed.getAll()
			.map(function (id) { return parseInt(id, 10); })
			.filter(function (id) { return id > 0 && id !== exclude; })
			.slice(0, limit);

		if (ids.length === 0) { return; }

		Promise.all(ids.map(function (id) {
			return fetchVehiclePruneOn404(id, STORAGE_KEYS.recentlyViewed);
		})).then(function (cars) {
			cars.filter(Boolean).forEach(function (car) {
				grid.appendChild(buildSimpleCard(car));
			});
			if (grid.children.length > 0) {
				section.hidden = false;
			}
		});
	}

	/**
	 * Build a minimal card element from a /vehicles/{id} response.
	 *
	 * @param {object} car
	 * @returns {HTMLElement}
	 */
	function buildSimpleCard(car) {
		var a = document.createElement('a');
		a.className = 'trx-ad-card trx-ad-card--rv';
		a.href      = car.permalink || '#';
		a.setAttribute('data-vehicle-id', car.id);

		var img = document.createElement('div');
		img.className = 'trx-ad-card__image';
		if (car.featured_image_url) {
			var imgEl = document.createElement('img');
			imgEl.src = car.featured_image_url;
			imgEl.loading = 'lazy';
			imgEl.alt = car.title || '';
			img.appendChild(imgEl);
		}

		var body = document.createElement('div');
		body.className = 'trx-ad-card__body';

		var title = document.createElement('h3');
		title.className = 'trx-ad-card__title';
		title.textContent = car.title || '';
		body.appendChild(title);

		if (car.specs && (car.specs.price || car.specs.sale_price)) {
			var price = document.createElement('div');
			price.className = 'trx-ad-card__price';
			var n = car.specs.sale_price || car.specs.price;
			price.textContent = (trxAdData.currencySymbol || '$') + Number(n).toLocaleString();
			body.appendChild(price);
		}

		a.appendChild(img);
		a.appendChild(body);
		return a;
	}

	/* ======================================================================
	   Lightbox — image overlay viewer for vehicle-gallery
	   ====================================================================== */

	function initLightbox() {
		var overlay, imgEl, group = [], current = 0;

		// Resolve the full-size image URL for a trigger: an <a href> link, an
		// <img data-full-src>/<img src>, or a wrapper containing one.
		function srcOf(el) {
			if (!el) { return ''; }
			if (el.getAttribute('href')) { return el.getAttribute('href'); }
			if (el.getAttribute('data-full-src')) { return el.getAttribute('data-full-src'); }
			if (el.tagName === 'IMG') { return el.getAttribute('src') || ''; }
			var img = el.querySelector('img');
			return img ? (img.getAttribute('data-full-src') || img.getAttribute('src') || '') : '';
		}

		// Video slides/posters must keep their own behaviour, never the lightbox.
		function isVideo(el) {
			return !!el.closest('[data-slide-type="video-self"], [data-slide-type="video-external"]');
		}

		// Build the navigable image set from the gallery the trigger belongs to,
		// de-duplicated by src (Swiper clones loop slides).
		function buildGroup(trigger) {
			var container = trigger.closest('[data-trx-ad-gallery], .trx-ad-media-slider, .trx-ad-gallery') || document;
			group = [];
			container.querySelectorAll('[data-trx-ad-lightbox]').forEach(function (t) {
				if (isVideo(t)) { return; }
				var s = srcOf(t);
				if (s && group.indexOf(s) === -1) { group.push(s); }
			});
			if (!group.length) {
				var only = srcOf(trigger);
				group = only ? [only] : [];
			}
		}

		document.addEventListener('click', function (e) {
			var trigger = e.target.closest('[data-trx-ad-lightbox]');
			if (!trigger || isVideo(trigger)) { return; }
			var src = srcOf(trigger);
			if (!src) { return; }
			e.preventDefault();
			buildGroup(trigger);
			var idx = group.indexOf(src);
			openLightbox(idx === -1 ? 0 : idx);
		});

		function ensureOverlay() {
			if (overlay) { return; }
			overlay = document.createElement('div');
			overlay.className = 'trx-ad-lightbox';
			overlay.setAttribute('role', 'dialog');
			overlay.setAttribute('aria-modal', 'true');
			overlay.innerHTML = '<button type="button" class="trx-ad-lightbox__close" aria-label="' +
				(i18n('closeLightbox', 'Close') || 'Close') + '">&times;</button>' +
				'<button type="button" class="trx-ad-lightbox__nav trx-ad-lightbox__nav--prev" aria-label="' +
				(i18n('prevImage', 'Previous image') || 'Previous image') + '">&#8249;</button>' +
				'<img class="trx-ad-lightbox__img" alt="" />' +
				'<button type="button" class="trx-ad-lightbox__nav trx-ad-lightbox__nav--next" aria-label="' +
				(i18n('nextImage', 'Next image') || 'Next image') + '">&#8250;</button>';
			imgEl = overlay.querySelector('.trx-ad-lightbox__img');
			overlay.addEventListener('click', function (ev) {
				if (ev.target === overlay || ev.target.classList.contains('trx-ad-lightbox__close')) {
					closeLightbox();
				} else if (ev.target.classList.contains('trx-ad-lightbox__nav--prev')) {
					step(-1);
				} else if (ev.target.classList.contains('trx-ad-lightbox__nav--next')) {
					step(1);
				}
			});
			document.addEventListener('keydown', function (ev) {
				if (!overlay.classList.contains('is-open')) { return; }
				if (ev.key === 'Escape') { closeLightbox(); }
				else if (ev.key === 'ArrowLeft') { step(-1); }
				else if (ev.key === 'ArrowRight') { step(1); }
			});
			document.body.appendChild(overlay);
		}

		function render() {
			if (!group.length) { return; }
			imgEl.src = group[current];
			var multi = group.length > 1;
			overlay.querySelector('.trx-ad-lightbox__nav--prev').hidden = !multi;
			overlay.querySelector('.trx-ad-lightbox__nav--next').hidden = !multi;
		}

		function step(delta) {
			if (!group.length) { return; }
			current = (current + delta + group.length) % group.length;
			render();
		}

		function openLightbox(idx) {
			ensureOverlay();
			current = idx;
			render();
			overlay.classList.add('is-open');
		}

		function closeLightbox() {
			if (overlay) { overlay.classList.remove('is-open'); }
		}
	}

	/* ======================================================================
	   Web Share API — shareable listing button with copy-link fallback.
	   ====================================================================== */

	function initWebShare() {
		document.addEventListener('click', function (e) {
			var btn = e.target.closest('[data-trx-ad-share]');
			if (!btn) { return; }
			e.preventDefault();

			var url   = btn.getAttribute('data-share-url')   || window.location.href;
			var title = btn.getAttribute('data-share-title') || document.title;

			if (navigator.share) {
				navigator.share({ url: url, title: title }).catch(function () { /* user dismissed */ });
				return;
			}

			if (navigator.clipboard && navigator.clipboard.writeText) {
				navigator.clipboard.writeText(url).then(function () {
					var hint = btn.getAttribute('data-share-copied') || 'Link copied';
					var prev = btn.textContent;
					btn.textContent = hint;
					setTimeout(function () { btn.textContent = prev; }, 2000);
				});
			}
		});
	}

	/* ======================================================================
	   Compare bar — show when selection is non-empty, plus Clear button.
	   ====================================================================== */

	function initCompareBar() {
		var bar = document.querySelector('[data-trx-ad-compare-bar]');
		if (!bar) { return; }

		function refresh() {
			var n = Compare.getAll().length;
			bar.hidden = n === 0;
		}

		var clearBtn = bar.querySelector('[data-trx-ad-compare-clear]');
		if (clearBtn) {
			clearBtn.addEventListener('click', function () {
				writeStore(localStorage, STORAGE_KEYS.compare, []);
				document.querySelectorAll('.trx-ad-card__action--compare.is-active').forEach(function (b) {
					b.classList.remove('is-active');
				});
				Compare.updateCount();
				refresh();
			});
		}

		// Reflect initial state and listen for changes triggered elsewhere
		// (the Compare.toggle path calls updateCount, which fires on the same tick).
		refresh();
		document.addEventListener('click', refresh, true);
	}

	/* ======================================================================
	   Geolocation request — opt-in browser prompt + cookie write.
	   Cookie is consumed server-side by GeoLocation::getVisitorCoords().
	   ====================================================================== */

	function initGeoRequest() {
		document.addEventListener('click', function (e) {
			var btn = e.target.closest('[data-trx-ad-geo-request]');
			if (!btn || !navigator.geolocation) { return; }
			e.preventDefault();

			var prevText = btn.textContent;
			btn.disabled = true;
			btn.textContent = i18n('locating', 'Locating…') || 'Locating…';

			navigator.geolocation.getCurrentPosition(function (pos) {
				var lat = pos.coords.latitude.toFixed(5);
				var lng = pos.coords.longitude.toFixed(5);
				// 15-minute cookie, lax samesite, root path.
				document.cookie = 'trx_ad_geo=' + lat + ',' + lng + '; max-age=900; path=/; SameSite=Lax';
				btn.disabled = false;
				btn.textContent = i18n('locationSet', 'Location set') || 'Location set';
				// Reload so server-rendered distances pick up the new cookie.
				setTimeout(function () { window.location.reload(); }, 600);
			}, function () {
				btn.disabled = false;
				btn.textContent = prevText;
			}, { timeout: 8000, maximumAge: 60000 });
		});
	}

	/* ======================================================================
	   Compare page + Favorites page — hydrate from storage and fetch cards.
	   The server emits an empty <section> with a heading, an empty-state
	   <p>, and a <div data-trx-ad-{compare,favorites}-grid>. Each ID's
	   /vehicles/{id} is fetched in parallel; cards are appended in source
	   order. Storage changes (other tabs, button clicks elsewhere on the
	   same page) re-hydrate via the `storage` event.
	   ====================================================================== */

	function hydratePage(grid, emptyEl, ids, includeRemoveBtn, storageKey) {
		if (typeof trxAdData === 'undefined' || !trxAdData.restUrl) { return; }
		grid.innerHTML = '';
		ids = ids
			.map(function (id) { return parseInt(id, 10); })
			.filter(function (id) { return id > 0; });

		if (ids.length === 0) {
			if (emptyEl) { emptyEl.hidden = false; }
			return;
		}
		if (emptyEl) { emptyEl.hidden = true; }

		Promise.all(ids.map(function (id) {
			return fetchVehiclePruneOn404(id, storageKey);
		})).then(function (cars) {
			cars.filter(Boolean).forEach(function (car) {
				var card = buildSimpleCard(car);
				if (includeRemoveBtn) {
					var rm = document.createElement('button');
					rm.type = 'button';
					rm.className = 'trx-ad-card__remove';
					rm.setAttribute('data-vehicle-id', String(car.id));
					rm.setAttribute('aria-label', i18n('remove', 'Remove') || 'Remove');
					rm.innerHTML = '&times;';
					card.appendChild(rm);
				}
				grid.appendChild(card);
			});
			if (grid.children.length === 0 && emptyEl) {
				emptyEl.hidden = false;
			}
		});
	}

	function hydrateComparePage() {
		var section = document.querySelector('[data-trx-ad-compare-page]');
		if (!section) { return; }
		var specsEl = section.querySelector('[data-trx-ad-compare-specs]');
		var emptyEl = section.querySelector('[data-trx-ad-compare-empty]');
		if (!specsEl) { return; }

		var run = function () {
			if (typeof trxAdData === 'undefined' || !trxAdData.restUrl) { return; }
			var tbody = specsEl.querySelector('tbody');
			if (tbody) { tbody.innerHTML = ''; }

			var ids = Compare.getAll()
				.map(function (id) { return parseInt(id, 10); })
				.filter(function (id) { return id > 0; });

			if (ids.length === 0) {
				if (emptyEl) { emptyEl.hidden = false; }
				return;
			}
			if (emptyEl) { emptyEl.hidden = true; }

			Promise.all(ids.map(function (id) {
				return fetchVehiclePruneOn404(id, STORAGE_KEYS.compare);
			})).then(function (cars) {
				cars = cars.filter(Boolean);
				if (cars.length === 0 && emptyEl) {
					emptyEl.hidden = false;
					return;
				}
				renderCompareSpecsTable(specsEl, cars);
			});
		};
		run();

		// Re-hydrate on remove click (delegated within the section).
		section.addEventListener('click', function (e) {
			var rm = e.target.closest('.trx-ad-compare-page__remove');
			if (!rm) { return; }
			e.preventDefault();
			Compare.toggle(parseInt(rm.getAttribute('data-vehicle-id'), 10));
			run();
		});

		// Re-hydrate when storage changes (e.g. another tab toggles compare).
		window.addEventListener('storage', function (e) {
			if (e.key === STORAGE_KEYS.compare) { run(); }
		});
	}

	/**
	 * Build the side-by-side spec table from cars[] + the table's data-fields
	 * attribute. Each enabled field becomes one row. Header row is the
	 * vehicle titles (linked to the listing).
	 *
	 * @param {HTMLElement} table   The <table data-trx-ad-compare-specs> el.
	 * @param {Array}       cars    Vehicles array from /vehicles/{id} fetches.
	 */
	function renderCompareSpecsTable(table, cars) {
		var tbody = table.querySelector('tbody');
		if (!tbody) { return; }
		tbody.innerHTML = '';

		var raw = (table.getAttribute('data-fields') || '').trim();
		var fields = raw ? raw.split(',').map(function (s) { return s.trim(); }).filter(Boolean) : [];
		if (fields.length === 0 || cars.length === 0) {
			return;
		}

		var config  = (typeof trxAdData !== 'undefined' && trxAdData.compareFields) || {};
		var symbol  = (typeof trxAdData !== 'undefined' && trxAdData.currencySymbol) || '$';
		var na      = i18n('na', '—') || '—';
		var yesText = i18n('yes', 'Yes') || 'Yes';
		var noText  = i18n('no', 'No') || 'No';

		// Header row: blank corner + one cell per vehicle. Each cell stacks
		// featured image (linked) → title (linked) → price → remove (×).
		var head = document.createElement('tr');
		head.className = 'trx-ad-compare-page__head';
		head.appendChild(document.createElement('th'));
		cars.forEach(function (car) {
			var th = document.createElement('th');
			th.className = 'trx-ad-compare-page__head-cell';

			// Image: link to permalink. Falls back to a placeholder swatch
			// when the listing has no thumbnail so cell heights stay even.
			var imgLink = document.createElement('a');
			imgLink.href = car.permalink || '#';
			imgLink.className = 'trx-ad-compare-page__image';
			var imgUrl = car.featured_image_url || '';
			if (imgUrl) {
				var img = document.createElement('img');
				img.src = imgUrl;
				img.alt = car.title || '';
				img.loading = 'lazy';
				imgLink.appendChild(img);
			} else {
				imgLink.classList.add('trx-ad-compare-page__image--placeholder');
			}
			th.appendChild(imgLink);

			// Title (linked).
			var titleLink = document.createElement('a');
			titleLink.href = car.permalink || '#';
			titleLink.className = 'trx-ad-compare-page__title';
			titleLink.textContent = car.title || '';
			th.appendChild(titleLink);

			// Price (formatted).
			var priceEl = document.createElement('div');
			priceEl.className = 'trx-ad-compare-page__price';
			var priceVal = car.specs && (car.specs.sale_price || car.specs.price);
			if (priceVal) {
				priceEl.textContent = symbol + Number(priceVal).toLocaleString();
			} else {
				priceEl.textContent = na;
			}
			th.appendChild(priceEl);

			// Remove (×) — clears this car from the compare bag.
			var rm = document.createElement('button');
			rm.type = 'button';
			rm.className = 'trx-ad-compare-page__remove';
			rm.setAttribute('data-vehicle-id', String(car.id));
			rm.setAttribute('aria-label', i18n('remove', 'Remove') || 'Remove');
			rm.innerHTML = '&times;';
			th.appendChild(rm);

			head.appendChild(th);
		});
		tbody.appendChild(head);

		fields.forEach(function (key) {
			var cfg = config[key];
			if (!cfg) { return; }

			var tr = document.createElement('tr');
			var th = document.createElement('th');
			th.scope = 'row';
			th.textContent = cfg.label;
			tr.appendChild(th);

			cars.forEach(function (car) {
				var raw = (car.specs && key in car.specs) ? car.specs[key] : null;
				var td  = document.createElement('td');
				td.textContent = formatCompareValue(raw, cfg, { symbol: symbol, na: na, yes: yesText, no: noText });
				tr.appendChild(td);
			});
			tbody.appendChild(tr);
		});
	}

	function formatCompareValue(value, cfg, ctx) {
		if (value === null || value === undefined || value === '' || value === false && cfg.format !== 'bool') {
			// Allow `0` through (e.g. 0 doors would be weird, but 0 mileage is valid).
			if (value !== 0) { return ctx.na; }
		}
		switch (cfg.format) {
			case 'price':
				if (value === null || value === '' || value === undefined) { return ctx.na; }
				return ctx.symbol + Number(value).toLocaleString();
			case 'int':
				if (value === null || value === '' || value === undefined) { return ctx.na; }
				return String(parseInt(value, 10));
			case 'unit':
				if (value === null || value === '' || value === undefined) { return ctx.na; }
				return Number(value).toLocaleString() + (cfg.unit ? ' ' + cfg.unit : '');
			case 'bool':
				return value ? ctx.yes : ctx.no;
			case 'titlecase':
				return humanizeSlug(value);
			case 'upper':
				return String(value).toUpperCase();
			case 'string':
			default:
				return String(value);
		}
	}

	function hydrateFavoritesPage() {
		var section = document.querySelector('[data-trx-ad-favorites-page]');
		if (!section) { return; }
		var grid    = section.querySelector('[data-trx-ad-favorites-grid]');
		var emptyEl = section.querySelector('[data-trx-ad-favorites-empty]');
		if (!grid) { return; }

		var run = function () { hydratePage(grid, emptyEl, Favorites.getAll(), true, STORAGE_KEYS.favorites); };

		// In rest mode the list lives in user-meta — load it before first paint
		// (this is the cross-device sync path: a browser with an empty
		// localStorage still shows the account's favorites). UAT #36.
		if (Favorites.backend() === 'rest') {
			Favorites.loadRemote().then(run);
		} else {
			run();
		}

		section.addEventListener('click', function (e) {
			var rm = e.target.closest('.trx-ad-card__remove');
			if (!rm) { return; }
			e.preventDefault();
			Favorites.toggle(parseInt(rm.getAttribute('data-vehicle-id'), 10));
			// rest-mode toggle re-renders via the change event below; local mode
			// is synchronous so re-render now.
			if (Favorites.backend() !== 'rest') { run(); }
		});

		// localStorage cross-tab writes (local mode).
		window.addEventListener('storage', function (e) {
			if (e.key === STORAGE_KEYS.favorites) { run(); }
		});
		// rest-mode toggles dispatch this on the same tab.
		document.addEventListener('trx-ad-favorites-changed', run);
	}

	function init() {
		// Delegated handlers for the print button and confirm-before-follow
		// links. Markup carries data attributes instead of inline on* handlers
		// (inline handlers would be stripped by the kses escape-late pass and
		// are a wp.org review flag anyway).
		document.addEventListener('click', function (e) {
			if (e.target.closest('[data-trx-ad-print]')) {
				window.print();
				return;
			}
			var confirmEl = e.target.closest('[data-trx-ad-confirm]');
			if (confirmEl && !window.confirm(confirmEl.getAttribute('data-trx-ad-confirm'))) {
				e.preventDefault();
			}
		});

		// One-time migration: copy any compare list still in sessionStorage
		// (left over from before Compare moved to localStorage) into local
		// storage, then drop the session copy. Safe to run on every load —
		// it's a no-op once both stores agree.
		try {
			var sessRaw = sessionStorage.getItem(STORAGE_KEYS.compare);
			if (sessRaw) {
				var localRaw = localStorage.getItem(STORAGE_KEYS.compare);
				if (!localRaw || localRaw === '[]') {
					localStorage.setItem(STORAGE_KEYS.compare, sessRaw);
				}
				sessionStorage.removeItem(STORAGE_KEYS.compare);
			}
		} catch (e) { /* sessionStorage unavailable or quota error */ }

		Favorites.initAll();
		Compare.initAll();
		RecentlyViewed.track();
		hydrateRecentlyViewed();
		hydrateComparePage();
		hydrateFavoritesPage();
		initGallery();
		initLightbox();
		initMobileBar();
		initEventDelegation();
		initWebShare();
		initCompareBar();
		initGeoRequest();
		initArchiveAjax();
		initArchiveFilterDrawer();
	}

	/* ======================================================================
	   AJAX archive filter + pagination (no page reload)
	   ====================================================================== */

	/**
	 * Intercept the archive filter form, sort dropdown, and pagination links;
	 * fetch the same URL and swap in the response's grid + count + pagination.
	 * No new REST endpoint — we parse the existing rendered page server-side
	 * via DOMParser, which keeps the AJAX path and the no-JS path in lockstep.
	 */
	function initArchiveAjax() {
		var archive = document.querySelector('.trx-ad-archive');
		if (!archive) { return; }
		var form    = archive.querySelector('.trx-ad-filter-form');
		var grid    = archive.querySelector('.trx-ad-archive__grid, .trx-ad-archive__no-results');
		var count   = archive.querySelector('[data-trx-ad-result-count]');
		var results = archive.querySelector('.trx-ad-archive__results');
		if (!results) { return; }

		// Mark the archive root so loading state can show a spinner.
		archive.setAttribute('data-trx-ad-archive-ajax', '1');

		var inflight = null;

		function buildUrl(formEl, extraParams) {
			var url = new URL(formEl ? formEl.action : window.location.href, window.location.origin);
			if (formEl) {
				var fd = new FormData(formEl);
				// Keep args baked into the action URL (post_type=trx_ad_listing
				// on plain permalinks) unless the form owns a field of the same
				// name — mirrors the hidden inputs the template emits for the
				// native GET submit.
				var baked = new URLSearchParams(url.search);
				url.search = '';
				baked.forEach(function (value, key) {
					if (!fd.has(key)) {
						url.searchParams.append(key, value);
					}
				});
				fd.forEach(function (value, key) {
					if (value === '' || value === null || typeof value === 'undefined') { return; }
					url.searchParams.append(key, value);
				});
			}
			if (extraParams && typeof extraParams === 'object') {
				Object.keys(extraParams).forEach(function (k) {
					if (extraParams[k] === null || typeof extraParams[k] === 'undefined' || extraParams[k] === '') {
						url.searchParams.delete(k);
					} else {
						url.searchParams.set(k, extraParams[k]);
					}
				});
			}
			return url;
		}

		function setLoading(state) {
			archive.classList.toggle('trx-ad-archive--loading', !!state);
		}

		function load(url, pushState) {
			if (inflight) { inflight.abort(); }
			setLoading(true);
			var controller = new AbortController();
			inflight = controller;

			return fetch(url.toString(), {
				headers: { 'X-Requested-With': 'XMLHttpRequest' },
				credentials: 'same-origin',
				signal: controller.signal
			})
				.then(function (res) { return res.text(); })
				.then(function (html) {
					var doc = new DOMParser().parseFromString(html, 'text/html');
					var newResults = doc.querySelector('.trx-ad-archive__results');
					var newCount   = doc.querySelector('[data-trx-ad-result-count]');
					if (newResults) {
						results.innerHTML = newResults.innerHTML;
					}
					if (newCount && count) {
						count.innerHTML = newCount.innerHTML;
					}
					if (pushState) {
						window.history.pushState({ trxAdArchive: true }, '', url.toString());
					}
					// Re-init any per-card listeners that delegated handlers
					// don't already cover (event delegation on document does
					// most of the heavy lifting).
					archive.dispatchEvent(new CustomEvent('trxad:archive:updated', { bubbles: true }));
					setLoading(false);
					inflight = null;
				})
				.catch(function (err) {
					if (err && err.name === 'AbortError') { return; }
					// Fail-open: full reload preserves the no-JS path.
					window.location.href = url.toString();
				});
		}

		// Submit interception.
		if (form) {
			form.addEventListener('submit', function (e) {
				e.preventDefault();
				load(buildUrl(form), true);
			});

			// "Live" submit on filter changes — debounced for inputs that fire
			// per keystroke (search box).
			var liveTimer = null;
			form.addEventListener('change', function (e) {
				if (!e.target.matches('select, input[type=checkbox], input[type=radio]')) { return; }
				load(buildUrl(form), true);
			});
			form.addEventListener('input', function (e) {
				if (!e.target.matches('input[type=search], input[type=number], input[type=text]')) { return; }
				if (liveTimer) { clearTimeout(liveTimer); }
				liveTimer = setTimeout(function () { load(buildUrl(form), true); }, 450);
			});
		}

		// Pagination interception (delegated; pagination HTML is re-rendered
		// on every load so handlers can't be bound directly).
		archive.addEventListener('click', function (e) {
			var link = e.target.closest('.trx-ad-archive__pagination a');
			if (!link) { return; }
			e.preventDefault();
			var url = new URL(link.href, window.location.origin);
			load(url, true);
			results.scrollIntoView({ behavior: 'smooth', block: 'start' });
		});

		// Sort dropdown (outside the form so changes don't post the whole
		// filter set).
		var sortSel = archive.querySelector('[data-trx-ad-sort]');
		if (sortSel) {
			sortSel.addEventListener('change', function () {
				load(buildUrl(form, { orderby: sortSel.value }), true);
			});
		}

		// Browser back/forward.
		window.addEventListener('popstate', function () {
			load(new URL(window.location.href), false);
		});
	}

	/**
	 * Mobile-first filter drawer. On small screens (<768px) the sidebar is
	 * hidden behind a hamburger button; tapping the button slides it in from
	 * the side, the close icon dismisses it. Desktop layout is unchanged.
	 */
	function initArchiveFilterDrawer() {
		var archive = document.querySelector('.trx-ad-archive');
		if (!archive) { return; }
		var sidebar = archive.querySelector('.trx-ad-archive__sidebar');
		var toggle  = archive.querySelector('[data-trx-ad-filter-toggle]');
		var close   = archive.querySelector('[data-trx-ad-filter-close]');
		if (!sidebar || !toggle) { return; }

		// Dim backdrop. Built once, lives outside the archive so its
		// position:fixed isn't clipped by the archive's stacking context.
		var backdrop = document.createElement('div');
		backdrop.className = 'trx-ad-archive__sidebar-backdrop';
		backdrop.setAttribute('aria-hidden', 'true');
		document.body.appendChild(backdrop);

		function open() {
			sidebar.classList.add('trx-ad-archive__sidebar--open');
			backdrop.classList.add('trx-ad-archive__sidebar-backdrop--visible');
			document.body.classList.add('trx-ad-body--no-scroll');
			toggle.setAttribute('aria-expanded', 'true');
		}
		function dismiss() {
			sidebar.classList.remove('trx-ad-archive__sidebar--open');
			backdrop.classList.remove('trx-ad-archive__sidebar-backdrop--visible');
			document.body.classList.remove('trx-ad-body--no-scroll');
			toggle.setAttribute('aria-expanded', 'false');
		}
		toggle.addEventListener('click', function () {
			if (sidebar.classList.contains('trx-ad-archive__sidebar--open')) { dismiss(); } else { open(); }
		});
		if (close) { close.addEventListener('click', dismiss); }
		// Tap the backdrop to dismiss.
		backdrop.addEventListener('click', dismiss);
		// Escape to dismiss.
		document.addEventListener('keydown', function (e) {
			if (e.key === 'Escape') { dismiss(); }
		});

		// Auto-close the drawer when a filter is applied so the (AJAX-updated)
		// results are visible instead of staying hidden behind the open drawer
		// on mobile (UAT #44). Harmless on desktop / full-reload submits.
		var filterForm = sidebar.querySelector('form');
		if (filterForm) {
			filterForm.addEventListener('submit', function () {
				if (sidebar.classList.contains('trx-ad-archive__sidebar--open')) { dismiss(); }
			});
		}
	}

	// Run on DOMContentLoaded or immediately if already loaded.
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init);
	} else {
		init();
	}

})();
