/**
 * SiteStaffr Button Widget - UI Wrapper
 *
 * Thin UI wrapper for the inline button widget ([sitestaffr_button] shortcode).
 * All core voice logic (WebRTC, WebSocket, audio) is in sitestaffr-core.js
 * Text chat logic is in sitestaffr-text-chat.js
 *
 * @package    SiteStaffr
 * @since      0.11.93
 */

(function() {
	'use strict';

	// Track the single active panel across all button widget instances
	let activePanel = null; // { el, widget }

	// Listen for other widgets (including floating widget) opening panels
	document.addEventListener('sitestaffr:panel-opening', () => {
		if (activePanel) {
			activePanel.widget._forceClosePanel();
			activePanel = null;
		}
	});

	// Escape key dismisses any open panel
	document.addEventListener('keydown', (e) => {
		if (e.key === 'Escape' && activePanel) {
			activePanel.widget._forceClosePanel();
			activePanel = null;
		}
	});

	function showSiteStaffrGlobalNotice(message, timeoutMs = 6000) {
		if (!message) return;

		let notice = document.getElementById('sitestaffr-global-notice');
		if (!notice) {
			notice = document.createElement('div');
			notice.id = 'sitestaffr-global-notice';
			notice.setAttribute('role', 'status');
			notice.setAttribute('aria-live', 'polite');
			notice.style.position = 'fixed';
			notice.style.left = '50%';
			notice.style.bottom = '24px';
			notice.style.transform = 'translateX(-50%)';
			notice.style.background = 'rgba(185, 28, 28, 0.96)';
			notice.style.color = '#fff';
			notice.style.padding = '10px 14px';
			notice.style.borderRadius = '8px';
			notice.style.boxShadow = '0 10px 30px rgba(0,0,0,0.22)';
			notice.style.fontSize = '13px';
			notice.style.lineHeight = '1.4';
			notice.style.maxWidth = 'min(92vw, 560px)';
			notice.style.zIndex = '999999';
			notice.style.opacity = '0';
			notice.style.transition = 'opacity .18s ease';
			notice.style.pointerEvents = 'none';
			document.body.appendChild(notice);
		}

		notice.textContent = message;
		notice.style.opacity = '1';

		if (window.sitestaffrNoticeTimer) {
			clearTimeout(window.sitestaffrNoticeTimer);
		}
		window.sitestaffrNoticeTimer = setTimeout(() => {
			notice.style.opacity = '0';
		}, timeoutMs);
	}

	/**
	 * SiteStaffr Button Widget UI
	 */
	class SiteStaffrButtonWidget {
		constructor(container) {
			this.container = container;
			this.button = container.querySelector('.sitestaffr-button-widget');
			this.buttonText = container.querySelector('.sitestaffr-button-text');
			this.buttonIcon = container.querySelector('.sitestaffr-button-icon');

			// Mode configuration
			this.mode = this.container.dataset.mode || sitestaffr_button_data.mode || 'voice';
			this.soundsEnabled = sitestaffr_button_data.sounds_enabled !== false;

			// Chat state
			this.chatInstance = null;
			this.panelEl = null;
			this.landingCardEl = null;
			this.chatPanelOpen = false;
			this.landingCardOpen = false;
			this._streamingBubble = null;

			// Get configuration from data attributes
			const config = {
				siteId: this.container.dataset.siteId,
				ajaxUrl: sitestaffr_button_data.ajax_url || '',
				middlewareUrl: sitestaffr_button_data.middleware_url || '',
				pluginUrl: sitestaffr_button_data.plugin_url || '',
				channelSource: 'button',
				persona: this.container.dataset.persona || '',
			};

			this.config = config;

			// Get UI-specific settings from data attributes
			this.uiConfig = {
				listeningText: this.container.dataset.listeningText || 'Listening...',
				idleText: this.container.dataset.idleText || 'Talk to Us',
			};

			// Only require voice support for voice modes
			const needsVoice = this.mode === 'voice' || this.mode === 'both';
			if (needsVoice && !this.checkBrowserSupport()) {
				if (this.mode === 'both') {
					this.mode = 'text';
				} else {
					this.showStaticError(sitestaffr_button_data.strings.permission || 'Browser not supported');
					return;
				}
			}

			// Initialize voice core for voice-capable modes
			if (needsVoice && this.checkBrowserSupport()) {
				this.core = new SiteStaffrCore(config, {
					onStateChange: (state, oldState) => this.handleStateChange(state, oldState),
					onError: (message) => this.handleError(message),
					onConversationStart: () => this.handleConversationStart(),
					onConversationEnd: (reason, detail) => this.handleConversationEnd(reason, detail),
					onFieldUpdate: (field, value) => this.handleFieldUpdate(field, value),
					onSearchStart: (query) => this.showSearchIndicator(query),
					onSearchEnd: () => this.hideSearchIndicator(),
					onMicDenied: () => this.handleMicDenied(),
					onMinutesExhausted: () => this.handleMinutesExhausted(),
				});
			} else {
				this.core = null;
			}

			// Event listeners
			this.button.addEventListener('click', () => this.handleButtonClick());

			// Save chat on unload/hide — three complementary triggers so mobile bfcache and hard
			// closes don't silently drop the conversation. All three call the same saveViaBeacon()
			// which has an internal double-save guard (this.saved).
			const _saveChat = () => {
				if (this.chatInstance && !this.chatInstance.ended) {
					this.chatInstance.saveViaBeacon();
				}
			};
			window.addEventListener('beforeunload', _saveChat);
			window.addEventListener('pagehide', _saveChat);
			document.addEventListener('visibilitychange', () => {
				if (document.visibilityState === 'hidden') {
					_saveChat();
				}
			});

			// Reposition open panels on resize
			window.addEventListener('resize', () => {
				if (this._resizeTimer) clearTimeout(this._resizeTimer);
				this._resizeTimer = setTimeout(() => {
					const openPanel = this.landingCardEl && this.landingCardOpen ? this.landingCardEl
						: this.panelEl && this.chatPanelOpen ? this.panelEl
						: null;
					if (openPanel) {
						openPanel.style.transition = 'none';
						this._positionFloatingPanel(openPanel);
						void openPanel.offsetWidth;
						openPanel.style.transition = '';
					}
				}, 100);
			});

			// Current state for UI
			this.currentState = 'idle';
			this.inConversation = false;

			// Expose for page-level JS hooks (e.g., onboarding card)
			this.container._sitestaffrWidget = this;

			// Onboarding card elements (only present when persona="onboarding").
			this.onboardingCard = this.container.parentElement
				? this.container.parentElement.querySelector('.sitestaffr-onboarding-card')
				: null;

		}

		/**
		 * Check browser voice support
		 */
		checkBrowserSupport() {
			return !!(
				navigator.mediaDevices &&
				navigator.mediaDevices.getUserMedia &&
				window.MediaRecorder &&
				window.WebSocket
			);
		}

		/**
		 * Handle button click — mode-aware routing
		 */
		async handleButtonClick() {
			// Inactive plan — always show contact form
			if (sitestaffr_button_data.entitlement === 'inactive') {
				if (this.chatPanelOpen) {
					this.closeChatPanel();
				} else {
					this.openChatPanel();
				}
				return;
			}

			// GTM dataLayer: widget opened
			(window.dataLayer = window.dataLayer || []).push(Object.assign({
				event: 'sitestaffr_widget_open',
				sitestaffr_mode: this.mode,
				sitestaffr_widget_type: 'button',
			}, window._sitestaffrGTM.pageContext()));

			if (this.mode === 'voice') {
				await this.core.toggleConversation();
			} else if (this.mode === 'text') {
				this.toggleChatPanel();
			} else if (this.mode === 'both') {
				if (this.chatPanelOpen) {
					this.closeChatPanel();
				} else if (this.inConversation) {
					await this.core.toggleConversation();
				} else if (this.landingCardOpen) {
					this.closeLandingCard();
				} else {
					this.openLandingCard();
				}
			}
		}

		/* =============================================
		   LANDING CARD (both mode)
		   ============================================= */

		createLandingCard() {
			if (this.landingCardEl) return this.landingCardEl;

			const card = document.createElement('div');
			card.className = 'sitestaffr-landing-card';
			card.innerHTML = `
				<button type="button" class="sitestaffr-landing-card-close" aria-label="Close">
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
				</button>
				<div class="sitestaffr-landing-card-header">
					<h3>How would you like to connect?</h3>
					<p>Choose your preferred way to chat</p>
				</div>
				<div class="sitestaffr-landing-card-tiles">
					<div class="sitestaffr-landing-tile" data-choice="voice">
						<div class="sitestaffr-landing-tile-icon">
							<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
								<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
								<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
								<line x1="12" y1="19" x2="12" y2="23"></line>
								<line x1="8" y1="23" x2="16" y2="23"></line>
							</svg>
						</div>
						<span class="sitestaffr-landing-tile-label">Voice</span>
						<span class="sitestaffr-landing-tile-desc">Talk with our AI assistant</span>
					</div>
					<div class="sitestaffr-landing-tile" data-choice="text">
						<div class="sitestaffr-landing-tile-icon">
							<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
								<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
							</svg>
						</div>
						<span class="sitestaffr-landing-tile-label">Text</span>
						<span class="sitestaffr-landing-tile-desc">Type your questions</span>
					</div>
				</div>
			`;

			card.querySelector('[data-choice="voice"]').addEventListener('click', async () => {
				this.closeLandingCard();
				if (this.core) {
					await this.core.toggleConversation();
				}
			});

			card.querySelector('[data-choice="text"]').addEventListener('click', () => {
				this.closeLandingCard();
				this.openChatPanel();
			});

			card.querySelector('.sitestaffr-landing-card-close').addEventListener('click', () => {
				this.closeLandingCard();
			});

			this._propagateCssVars(card);

			document.body.appendChild(card);
			this.landingCardEl = card;
			return card;
		}

		openLandingCard() {
			const card = this.createLandingCard();
			this._positionFloatingPanel(card);
			document.dispatchEvent(new CustomEvent('sitestaffr:panel-opening'));
			activePanel = { el: card, widget: this };
			void card.offsetWidth;
			card.classList.add('is-open');
			this.landingCardOpen = true;
		}

		closeLandingCard() {
			if (this.landingCardEl) {
				this.landingCardEl.classList.remove('is-open');
			}
			this.landingCardOpen = false;
			if (activePanel && activePanel.widget === this) activePanel = null;
			document.dispatchEvent(new CustomEvent('sitestaffr:panel-closed'));
		}

		/**
		 * Force-close whichever panel this widget has open.
		 * Called by cross-widget coordination, not by user action.
		 * Note: closeChatPanel() already calls endConversation('visitor_close')
		 * fire-and-forget, satisfying the spec's requirement.
		 */
		_forceClosePanel() {
			if (this.chatPanelOpen) {
				this.closeChatPanel();
			} else if (this.landingCardOpen) {
				this.closeLandingCard();
			}
		}

		/* =============================================
		   CHAT PANEL
		   ============================================= */

		createChatPanel() {
			if (this.panelEl) return this.panelEl;

			const businessName = 'AI Assistant';
			const speakerOnSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path><path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>';
			const speakerOffSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line></svg>';

			const panel = document.createElement('div');
			panel.className = 'sitestaffr-chat-panel';
			panel.innerHTML = `
				<div class="sitestaffr-chat-header">
					<span class="sitestaffr-chat-header-title">${this._escapeHtml(businessName)}</span>
					<div class="sitestaffr-chat-header-actions">
						${this.soundsEnabled ? `<button type="button" class="sitestaffr-chat-header-btn sitestaffr-chat-mute-btn" aria-label="Toggle sounds">${speakerOnSvg}</button>` : ''}
						<button type="button" class="sitestaffr-chat-header-btn sitestaffr-chat-close-btn" aria-label="Close chat">
							<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
						</button>
					</div>
				</div>
				<div class="sitestaffr-chat-messages">
					<div class="sitestaffr-chat-typing">
						<span class="sitestaffr-chat-typing-dot"></span>
						<span class="sitestaffr-chat-typing-dot"></span>
						<span class="sitestaffr-chat-typing-dot"></span>
					</div>
				</div>
				<div class="sitestaffr-chat-char-count"></div>
				<div class="sitestaffr-chat-input-area">
					<div class="sitestaffr-chat-input-wrapper">
						<textarea class="sitestaffr-chat-textarea" placeholder="Type a message..." rows="1" maxlength="2000"></textarea>
					</div>
					<button type="button" class="sitestaffr-chat-send-btn" aria-label="Send message">
						<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
					</button>
				</div>
			`;

			const textarea = panel.querySelector('.sitestaffr-chat-textarea');
			const sendBtn = panel.querySelector('.sitestaffr-chat-send-btn');
			const closeBtn = panel.querySelector('.sitestaffr-chat-close-btn');
			const muteBtn = panel.querySelector('.sitestaffr-chat-mute-btn');

			sendBtn.addEventListener('click', () => this.handleSendClick());

			textarea.addEventListener('keydown', (e) => {
				if (e.key === 'Enter' && !e.shiftKey) {
					e.preventDefault();
					this.handleSendClick();
				}
			});

			textarea.addEventListener('input', () => {
				textarea.style.height = 'auto';
				textarea.style.height = Math.min(textarea.scrollHeight, 100) + 'px';
				this.updateCharCount();
			});

			closeBtn.addEventListener('click', () => this.closeChatPanel());

			if (muteBtn) {
				muteBtn.addEventListener('click', () => {
					if (this.chatInstance) {
						const muted = this.chatInstance.toggleMute();
						muteBtn.innerHTML = muted ? speakerOffSvg : speakerOnSvg;
						muteBtn.classList.toggle('is-muted', muted);
					}
				});
			}

			// Propagate widget CSS variables (panel is on body, not inside container)
			this._propagateCssVars(panel);

			document.body.appendChild(panel);
			this.panelEl = panel;
			this._bindChatLinkClicks(panel);
			return panel;
		}

		/**
		 * Links in AI messages navigate in the same tab, so following one ends
		 * the conversation. Record that as the end reason before the page
		 * unloads. Delegated from the panel so streamed bubbles are covered too.
		 */
		_bindChatLinkClicks(panel) {
			panel.addEventListener('click', (e) => {
				const target = e.target;
				if (!target || typeof target.closest !== 'function') return;
				if (!target.closest('a[data-sitestaffr-link]')) return;
				// Never preventDefault — the browser still follows the link.
				if (this.chatInstance) {
					this.chatInstance.endConversationViaLinkClick();
				}
			});
		}

		async openChatPanel() {
			// Inactive plan — always show contact form
			if (sitestaffr_button_data.entitlement === 'inactive') {
				this._entitlementInactive = true;
			}

			// Check if daily cap was already reached today (persists across page loads)
			if (!this._dailyCapReached && !this._entitlementInactive) {
				try {
					var capDate = sessionStorage.getItem('sitestaffr_daily_cap');
					if (capDate === new Date().toISOString().slice(0, 10)) {
						this._dailyCapReached = true;
					}
				} catch (e) { /* private browsing */ }
			}

			if (this._dailyCapReached || this._entitlementInactive) {
				const panel = this.createChatPanel();
				this._positionFloatingPanel(panel);
				document.dispatchEvent(new CustomEvent('sitestaffr:panel-opening'));
				activePanel = { el: panel, widget: this };
				void panel.offsetWidth;
				panel.classList.add('is-open');
				this.chatPanelOpen = true;
				this.showContactForm();
				return;
			}

			const panel = this.createChatPanel();
			this._positionFloatingPanel(panel);
			document.dispatchEvent(new CustomEvent('sitestaffr:panel-opening'));
			activePanel = { el: panel, widget: this };
			void panel.offsetWidth;
			panel.classList.add('is-open');
			this.chatPanelOpen = true;

			// Refresh nonces for page-cached environments before creating chat instance
			if (this.core) {
				await this.core._refreshNoncesOnce();
			}

			if (!this.chatInstance || this.chatInstance.ended) {
				this._clearMessages();

				this.chatInstance = new SiteStaffrTextChat({
					ajaxUrl: this.config.ajaxUrl,
					pluginUrl: this.config.pluginUrl,
					channelSource: 'button',
					persona: this.config.persona || 'default',
					saveNonce: sitestaffr_button_data.save_nonce || '',
					finalizeNonce: sitestaffr_button_data.finalize_nonce || '',
					chatNonce: sitestaffr_button_data.chat_nonce || '',
					soundsEnabled: this.soundsEnabled,
				}, {
					onMessage: (role, text) => this.appendMessage(role, text),
					onTextDelta: (delta) => this.appendStreamDelta(delta),
					onStreamStart: () => this.showTypingIndicator(),
					onStreamEnd: () => { this.hideTypingIndicator(); this.finalizeStreamMessage(); },
					onFieldUpdate: (field, value) => this.handleFieldUpdate(field, value),
					onEndSession: (reason) => {
						if (reason === 'inactivity') {
							this.appendNotice('This conversation has ended due to inactivity.');
							setTimeout(() => this.closeChatPanel(), 5000);
						}
					},
				onShowContactForm: () => this.showContactForm(),
				onDisableInput: () => {
					var textarea = this.panelEl.querySelector('.sitestaffr-chat-textarea');
					var sendBtn = this.panelEl.querySelector('.sitestaffr-chat-send-btn');
					if (textarea) { textarea.disabled = true; textarea.placeholder = ''; }
					if (sendBtn) { sendBtn.disabled = true; }
				},
				onError: (message) => this.appendNotice(message),
				});

				// Display greeting instantly (no API call needed)
				var greeting = sitestaffr_button_data.chat_greeting || 'Hi! How can I help you today?';
				if (this.config.persona === 'onboarding') {
					greeting = "Hi there, thanks for your interest in SiteStaffr! Let's get you set up \u2014 could I start with your name?";
				}
				this.appendMessage('assistant', greeting);
				this.chatInstance.start();
			}

			const textarea = panel.querySelector('.sitestaffr-chat-textarea');
			if (textarea) {
				setTimeout(() => textarea.focus(), 300);
			}
		}

		closeChatPanel() {
			if (this.chatInstance && !this.chatInstance.ended) {
				this.chatInstance.endConversation('visitor_close');
			}
			if (this.panelEl) {
				this.panelEl.classList.remove('is-open');
			}
			this.chatPanelOpen = false;
			if (activePanel && activePanel.widget === this) activePanel = null;
			document.dispatchEvent(new CustomEvent('sitestaffr:panel-closed'));
		}

		toggleChatPanel() {
			if (this.chatPanelOpen) {
				this.closeChatPanel();
			} else {
				this.openChatPanel();
			}
		}

		/* =============================================
		   CHAT INTERACTION
		   ============================================= */

		handleSendClick() {
			if (!this.panelEl || !this.chatInstance) {
				return;
			}

			const textarea = this.panelEl.querySelector('.sitestaffr-chat-textarea');
			const text = textarea.value.trim();
			if (!text) return;

			if (!this.chatInstance.sendMessage(text)) return;

			textarea.value = '';
			textarea.style.height = 'auto';
			this.updateCharCount();
		}

		appendMessage(role, text) {
			if (!this.panelEl) return;
			const messagesEl = this.panelEl.querySelector('.sitestaffr-chat-messages');
			const bubble = document.createElement('div');
			bubble.className = `sitestaffr-chat-message sitestaffr-chat-message--${role === 'user' ? 'visitor' : 'assistant'}`;
			if (role === 'user') {
				bubble.textContent = text;
			} else {
				bubble.innerHTML = this._renderMarkdown(text);
			}
			const typing = messagesEl.querySelector('.sitestaffr-chat-typing');
			messagesEl.insertBefore(bubble, typing);
			this.scrollToBottom();
		}

		appendStreamDelta(delta) {
			if (!this.panelEl) return;
			const messagesEl = this.panelEl.querySelector('.sitestaffr-chat-messages');

			if (!this._streamingBubble) {
				this._streamingBubble = document.createElement('div');
				this._streamingBubble.className = 'sitestaffr-chat-message sitestaffr-chat-message--assistant sitestaffr-chat-message--streaming';
				const typing = messagesEl.querySelector('.sitestaffr-chat-typing');
				messagesEl.insertBefore(this._streamingBubble, typing);
			}

			this._streamingBubble.textContent += delta;
			this.scrollToBottom();
		}

		finalizeStreamMessage() {
			if (this._streamingBubble) {
				this._streamingBubble.innerHTML = this._renderMarkdown(this._streamingBubble.textContent);
				this._streamingBubble.classList.remove('sitestaffr-chat-message--streaming');
				this._streamingBubble = null;
			}
		}

		/**
		 * Lightweight markdown to HTML for chat bubbles.
		 * Supports: **bold**, *italic*, `code`, unordered lists (- item), ordered lists (1. item).
		 * Escapes HTML to prevent XSS.
		 */
		_renderMarkdown(text) {
			// Escape HTML entities first
			const escaped = text
				.replace(/&/g, '&amp;')
				.replace(/</g, '&lt;')
				.replace(/>/g, '&gt;')
				.replace(/"/g, '&quot;');

			const lines = escaped.split('\n');
			const result = [];
			let inUl = false;
			let inOl = false;

			for (let i = 0; i < lines.length; i++) {
				let line = lines[i];

				// Headings: "## heading" or "### heading"
				const headingMatch = line.match(/^\s*(#{1,4})\s+(.+)/);
				if (headingMatch) {
					if (inUl) { result.push('</ul>'); inUl = false; }
					if (inOl) { result.push('</ol>'); inOl = false; }
					result.push('<p><strong>' + this._renderInline(headingMatch[2]) + '</strong></p>');
					continue;
				}

				// Unordered list: "- item" or "* item"
				const ulMatch = line.match(/^\s*[-*]\s+(.+)/);
				// Ordered list: "1. item"
				const olMatch = line.match(/^\s*\d+\.\s+(.+)/);

				if (ulMatch || olMatch) {
					const itemText = ulMatch ? ulMatch[1] : olMatch[1];
					// Lines ending with ":" are section headers, not list items
					if (itemText.trim().endsWith(':')) {
						if (inUl) { result.push('</ul>'); inUl = false; }
						if (inOl) { result.push('</ol>'); inOl = false; }
						result.push('<p><strong>' + this._renderInline(itemText) + '</strong></p>');
					} else {
						if (inOl) { result.push('</ol>'); inOl = false; }
						if (!inUl) { result.push('<ul>'); inUl = true; }
						result.push('<li>' + this._renderInline(itemText) + '</li>');
					}
				} else {
					if (inUl) { result.push('</ul>'); inUl = false; }
					if (inOl) { result.push('</ol>'); inOl = false; }
					if (line.trim() === '') {
						result.push('<br>');
					} else {
						result.push('<p>' + this._renderInline(line) + '</p>');
					}
				}
			}

			if (inUl) result.push('</ul>');
			if (inOl) result.push('</ol>');

			return result.join('');
		}

		_renderInline(text) {
			// Links first so URL characters can't be mangled by bold/italic passes.
			// http(s) or site-relative URLs only; text is already HTML-escaped.
			// Same tab, not _blank: following a link ends the conversation, and
			// the delegated handler stamps ended_by=link_clicked before unload.
			return text
				.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+|\/[^)\s]*)\)/g, '<a href="$2" class="sitestaffr-chat-link" data-sitestaffr-link="1">$1</a>')
				// Anything still in link syntax had an unusable href — typically a
				// "(#)" placeholder the model invented when it had no real URL.
				// Show the label so the visitor never sees raw markdown brackets.
				.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
				.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
				.replace(/\*(.+?)\*/g, '<em>$1</em>')
				.replace(/`(.+?)`/g, '<code>$1</code>');
		}

		showTypingIndicator() {
			if (!this.panelEl) return;
			const typing = this.panelEl.querySelector('.sitestaffr-chat-typing');
			if (typing) {
				typing.classList.add('is-visible');
				this.scrollToBottom();
			}
		}

		hideTypingIndicator() {
			if (!this.panelEl) return;
			const typing = this.panelEl.querySelector('.sitestaffr-chat-typing');
			if (typing) typing.classList.remove('is-visible');
		}

		_escapeHtml(text) {
			var div = document.createElement('div');
			div.textContent = text;
			return div.innerHTML;
		}

		appendNotice(text) {
			if (!this.panelEl) return;
			const messagesEl = this.panelEl.querySelector('.sitestaffr-chat-messages');
			const notice = document.createElement('div');
			notice.className = 'sitestaffr-chat-notice';
			notice.textContent = text;
			const typing = messagesEl.querySelector('.sitestaffr-chat-typing');
			messagesEl.insertBefore(notice, typing);
			this.scrollToBottom();
		}

		showContactForm() {
			const panel = this.panelEl;
			if (!panel) return;

			// Hide chat input area and messages (contact form replaces the chat UI)
			const inputArea = panel.querySelector('.sitestaffr-chat-input-area');
			const charCount = panel.querySelector('.sitestaffr-chat-char-count');
			const messagesEl = panel.querySelector('.sitestaffr-chat-messages');
			if (inputArea) inputArea.style.display = 'none';
			if (charCount) charCount.style.display = 'none';
			if (messagesEl) messagesEl.style.display = 'none';

			// Remove existing form if any
			const existing = panel.querySelector('.sitestaffr-contact-form');
			if (existing) return;

			const formHTML = `
				<div class="sitestaffr-contact-form">
					<p class="sitestaffr-contact-form-intro">Want us to reach out? Leave your details below.</p>
					<form class="sitestaffr-contact-form-fields">
						<input type="text" name="contact_name" placeholder="Your name" required class="sitestaffr-contact-input" />
						<input type="email" name="contact_email" placeholder="Your email" required class="sitestaffr-contact-input" />
						<textarea name="contact_message" placeholder="How can we help?" required rows="3" class="sitestaffr-contact-textarea"></textarea>
						<button type="submit" class="sitestaffr-contact-submit">Send Message</button>
					</form>
				</div>
			`;

			if (messagesEl) messagesEl.insertAdjacentHTML('afterend', formHTML);

			const form = panel.querySelector('.sitestaffr-contact-form-fields');
			const self = this;
			form.addEventListener('submit', function(e) {
				e.preventDefault();
				const submitBtn = form.querySelector('.sitestaffr-contact-submit');
				submitBtn.disabled = true;
				submitBtn.textContent = 'Sending...';

				const name = form.querySelector('[name="contact_name"]').value;
				const email = form.querySelector('[name="contact_email"]').value;
				const message = form.querySelector('[name="contact_message"]').value;

				const endReason = self._entitlementInactive ? 'entitlement_contact' : 'daily_cap_contact';

				if (self.chatInstance) {
					self.chatInstance.contactMemory.name = name;
					self.chatInstance.contactMemory.email = email;
					self.chatInstance.contactMemory.reason = message;
					self.chatInstance.submissionSource = 'contact_form';
					self.chatInstance.transcript.push({
						role: 'user',
						text: message,
						timestamp: new Date().toISOString()
					});
					self.chatInstance.endConversation(endReason);
				} else {
					// No chat instance (entitlement fallback) — create minimal instance to save
					self.chatInstance = new SiteStaffrTextChat({
						ajaxUrl: self.config.ajaxUrl,
						pluginUrl: self.config.pluginUrl,
						channelSource: 'button',
						persona: self.config.persona || 'default',
						saveNonce: sitestaffr_button_data.save_nonce || '',
						finalizeNonce: sitestaffr_button_data.finalize_nonce || '',
						chatNonce: sitestaffr_button_data.chat_nonce || '',
						soundsEnabled: false,
					}, {
						onMessage: function() {},
						onTextDelta: function() {},
						onStreamStart: function() {},
						onStreamEnd: function() {},
						onFieldUpdate: function() {},
						onEndSession: function() {},
						onShowContactForm: function() {},
						onDisableInput: function() {},
						onError: function() {},
					});
					self.chatInstance.startTime = Date.now();
					self.chatInstance.contactMemory.name = name;
					self.chatInstance.contactMemory.email = email;
					self.chatInstance.contactMemory.reason = message;
					self.chatInstance.submissionSource = 'contact_form';
					self.chatInstance.transcript.push({
						role: 'user',
						text: message,
						timestamp: new Date().toISOString()
					});
					self.chatInstance.endConversation(endReason);
				}

				const formContainer = panel.querySelector('.sitestaffr-contact-form');
				formContainer.innerHTML = '<p class="sitestaffr-contact-form-success">Thanks! We\'ll be in touch.</p>';
			});

			// Flag so reopening panel shows form
			this._dailyCapReached = true;
		}

		updateCharCount() {
			if (!this.panelEl) return;
			const textarea = this.panelEl.querySelector('.sitestaffr-chat-textarea');
			const charCountEl = this.panelEl.querySelector('.sitestaffr-chat-char-count');
			if (!textarea || !charCountEl) return;

			const len = textarea.value.length;
			if (len > 0) {
				charCountEl.textContent = len + ' / 2000';
				charCountEl.classList.add('is-visible');
				charCountEl.classList.toggle('near-limit', len > 1800);
			} else {
				charCountEl.classList.remove('is-visible');
			}
		}

		scrollToBottom() {
			if (!this.panelEl) return;
			const messagesEl = this.panelEl.querySelector('.sitestaffr-chat-messages');
			if (messagesEl) messagesEl.scrollTop = messagesEl.scrollHeight;
		}

		_clearMessages() {
			if (!this.panelEl) return;
			const messagesEl = this.panelEl.querySelector('.sitestaffr-chat-messages');
			if (!messagesEl) return;
			const typing = messagesEl.querySelector('.sitestaffr-chat-typing');
			messagesEl.innerHTML = '';
			if (typing) messagesEl.appendChild(typing);
			this._streamingBubble = null;
		}

		_escapeHtml(text) {
			const div = document.createElement('div');
			div.textContent = text;
			return div.innerHTML;
		}

		/**
		 * Copy widget CSS custom properties to a body-appended element.
		 */
		_propagateCssVars(el) {
			const style = getComputedStyle(this.container);
			['--widget-bg-color', '--widget-hover-bg-color', '--widget-icon-color'].forEach(prop => {
				const val = style.getPropertyValue(prop).trim();
				if (val) el.style.setProperty(prop, val);
			});
		}

		/**
		 * Position a floating panel relative to the button using quadrant detection.
		 * Uses fixed positioning so it's never clipped by ancestor overflow.
		 * Skips positioning on mobile (CSS handles fullscreen).
		 */
		_positionFloatingPanel(el) {
			// Mobile: CSS handles fullscreen, no JS positioning needed
			if (window.innerWidth <= 480) return;

			const rect = this.button.getBoundingClientRect();
			const panelWidth = parseInt(getComputedStyle(el).getPropertyValue('--chat-panel-width')) || 380;
			const panelHeight = el.offsetHeight || 520;
			const margin = 12;
			const gap = 8;

			el.style.position = 'fixed';
			el.style.zIndex = '999998';

			// Vertical: place on whichever side has more space, prefer below when equal
			const spaceBelow = window.innerHeight - rect.bottom - gap;
			const spaceAbove = rect.top - gap;

			if (spaceBelow >= panelHeight + margin || spaceBelow >= spaceAbove) {
				el.style.top = (rect.bottom + gap) + 'px';
				el.style.bottom = 'auto';
				// Clamp: don't let panel go off bottom
				const maxTop = window.innerHeight - panelHeight - margin;
				if (rect.bottom + gap > maxTop) {
					el.style.top = Math.max(margin, maxTop) + 'px';
				}
			} else {
				el.style.bottom = (window.innerHeight - rect.top + gap) + 'px';
				el.style.top = 'auto';
				// Clamp: don't let panel go off top
				const panelTop = rect.top - gap - panelHeight;
				if (panelTop < margin) {
					el.style.bottom = 'auto';
					el.style.top = margin + 'px';
				}
			}

			// Horizontal: align to button edge based on which half of viewport
			const buttonCenter = rect.left + rect.width / 2;
			const viewportCenter = window.innerWidth / 2;

			if (buttonCenter >= viewportCenter) {
				// Button in right half: align panel's right edge to button's right edge
				let right = window.innerWidth - rect.right;
				if (right < margin) right = margin;
				if (right + panelWidth > window.innerWidth - margin) {
					right = window.innerWidth - panelWidth - margin;
				}
				el.style.right = right + 'px';
				el.style.left = 'auto';
			} else {
				// Button in left half: align panel's left edge to button's left edge
				let left = rect.left;
				if (left < margin) left = margin;
				if (left + panelWidth > window.innerWidth - margin) {
					left = window.innerWidth - panelWidth - margin;
				}
				el.style.left = left + 'px';
				el.style.right = 'auto';
			}
		}

		/* =============================================
		   VOICE STATE HANDLERS (unchanged)
		   ============================================= */

		handleStateChange(state, oldState) {
			this.button.classList.remove(`state-${this.currentState}`);
			this.container.classList.remove(`state-${this.currentState}`);

			this.currentState = state;
			this.button.classList.add(`state-${this.currentState}`);
			this.container.classList.add(`state-${this.currentState}`);

			this.updateButtonUI();
		}

		handleError(message) {
			showSiteStaffrGlobalNotice(message, message && message.includes('Too many attempts') ? 10000 : 6000);

			if (this.buttonText) {
				this.buttonText.textContent = this.uiConfig.idleText || 'Talk to Us';
			}

			const isRateLimitError = message && message.includes('Too many attempts');
			const timeout = isRateLimitError ? 10000 : 3000;

			if (this.errorTimeout) {
				clearTimeout(this.errorTimeout);
			}

			this.errorTimeout = setTimeout(() => {
				if (this.currentState === 'error') {
					this.handleStateChange('idle', 'error');
				}
			}, timeout);
		}

		/**
		 * Handle microphone permission denied — fall back to text or show recovery.
		 */
		handleMicDenied() {
			if (this.mode === 'both') {
				this.mode = 'text';
				this.openChatPanel();
				setTimeout(() => {
					this._showRecoveryMessage('To use voice chat, allow microphone access in your browser\'s address bar settings, then refresh the page.');
				}, 300);
			} else if (this.mode === 'voice') {
				this._showMicDeniedRecovery();
			}
		}

		/**
		 * Show mic-denied recovery for voice-only widgets (no text fallback).
		 */
		_showMicDeniedRecovery() {
			const existing = document.getElementById('sitestaffr-mic-recovery');
			if (existing) existing.remove();

			const recovery = document.createElement('div');
			recovery.id = 'sitestaffr-mic-recovery';
			recovery.className = 'sitestaffr-mic-overlay is-visible';
			recovery.innerHTML =
				'<div class="sitestaffr-mic-overlay-text">' +
					'To use voice chat, allow microphone access in your browser\'s address bar settings, then refresh the page.' +
					'<br><br>' +
					'<button type="button" class="sitestaffr-mic-retry-btn">Try Again</button>' +
					'<br>' +
					'<button type="button" class="sitestaffr-mic-dismiss-btn">Dismiss</button>' +
				'</div>';

			recovery.querySelector('.sitestaffr-mic-retry-btn').addEventListener('click', async () => {
				recovery.remove();
				if (this.core) {
					await this.core.toggleConversation();
				}
			});

			recovery.querySelector('.sitestaffr-mic-dismiss-btn').addEventListener('click', () => {
				recovery.remove();
			});

			document.body.appendChild(recovery);
		}

		/**
		 * Handle minutes exhausted — silently fall back to text chat.
		 */
		handleMinutesExhausted() {
			this.mode = 'text';
			this.openChatPanel();
		}

		/**
		 * Show a system message in the chat panel.
		 */
		_showRecoveryMessage(text) {
			if (!this.panelEl) return;
			const messagesEl = this.panelEl.querySelector('.sitestaffr-chat-messages');
			if (!messagesEl) return;
			const bubble = document.createElement('div');
			bubble.className = 'sitestaffr-chat-bubble sitestaffr-chat-bubble--system';
			bubble.textContent = text;
			messagesEl.appendChild(bubble);
			messagesEl.scrollTop = messagesEl.scrollHeight;
		}

		handleConversationStart() {
			this.inConversation = true;
			this.updateButtonUI();

			// Show onboarding card.
			if (this.onboardingCard) {
				this.onboardingCard.hidden = false;
				var fields = this.onboardingCard.querySelectorAll('.sitestaffr-onboarding-field');
				for (var j = 0; j < fields.length; j++) {
					fields[j].classList.remove('is-filled', 'is-filling');
					fields[j].style.display = '';
					var val = fields[j].querySelector('.sitestaffr-onboarding-value');
					if (val) val.textContent = '';
				}
				var successEl = this.onboardingCard.querySelector('.sitestaffr-onboarding-success');
				if (successEl) successEl.hidden = true;
				var h3 = this.onboardingCard.querySelector('h3');
				if (h3) h3.style.display = '';
			}
		}

		handleConversationEnd(reason, detail) {
			this.inConversation = false;
			this.updateButtonUI();

			if (this.onboardingCard) {
				// Clean up any running typewriter timers.
				var fields = this.onboardingCard.querySelectorAll('.sitestaffr-onboarding-field');
				for (var j = 0; j < fields.length; j++) {
					if (fields[j]._typewriterTimer) {
						clearInterval(fields[j]._typewriterTimer);
						fields[j]._typewriterTimer = null;
					}
				}

				if (detail && detail.endSessionCalled) {
					// AI completed the flow — show success state.
					var h3 = this.onboardingCard.querySelector('h3');
					if (h3) h3.style.display = 'none';
					for (var k = 0; k < fields.length; k++) {
						fields[k].style.display = 'none';
					}
					var successEl = this.onboardingCard.querySelector('.sitestaffr-onboarding-success');
					if (successEl) successEl.hidden = false;
				} else {
					// Early hangup — hide the card so the button is reusable.
					this.onboardingCard.hidden = true;
				}
			}
		}

		showSearchIndicator(query) {
			var displayQuery = query && query.length > 40 ? query.slice(0, 40) + '...' : query;
			this.searchQuery = displayQuery || '';
			this.updateButtonText();
		}

		hideSearchIndicator() {
			this.searchQuery = null;
			this.updateButtonText();
		}

		/**
		 * Handle field update from onboarding — typewriter + glow animation.
		 */
		handleFieldUpdate(field, value) {
			if (!this.onboardingCard) return;

			var fieldEl = this.onboardingCard.querySelector('[data-field="' + field + '"]');
			if (!fieldEl) return;

			var valueEl = fieldEl.querySelector('.sitestaffr-onboarding-value');
			if (!valueEl) return;

			if (fieldEl._typewriterTimer) {
				clearInterval(fieldEl._typewriterTimer);
			}

			fieldEl.classList.remove('is-filled');
			fieldEl.classList.add('is-filling');

			var chars = value.split('');
			var i = 0;
			valueEl.textContent = '';

			fieldEl._typewriterTimer = setInterval(function() {
				if (i < chars.length) {
					valueEl.textContent += chars[i];
					i++;
				} else {
					clearInterval(fieldEl._typewriterTimer);
					fieldEl._typewriterTimer = null;
					fieldEl.classList.remove('is-filling');
					fieldEl.classList.add('is-filled');
				}
			}, 40);
		}

		updateButtonUI() {
			this.updateButtonText();
		}

		updateButtonText() {
			if (!this.buttonText) return;

			let text = '';

			if (this.inConversation || (this.core && this.core.isInConversation())) {
				switch (this.currentState) {
					case 'connecting':
						text = 'Connecting...';
						break;
					case 'listening':
						text = this.uiConfig.listeningText;
						break;
					case 'thinking':
						text = 'Processing...';
						break;
					case 'searching':
						text = this.searchQuery ? 'Searching ' + this.searchQuery + '...' : 'Searching...';
						break;
					case 'speaking':
						text = 'Speaking...';
						break;
					case 'error':
						return;
					default:
						text = 'Click to hang up';
				}
			} else {
				text = this.uiConfig.idleText;
			}

			this.buttonText.textContent = text;
		}

		showStaticError(message) {
			this.button.classList.add('state-error');
			this.container.classList.add('state-error');

			if (this.buttonText) {
				this.buttonText.textContent = message;
			}
		}
	}

	/**
	 * Initialize button widgets on DOM ready
	 */
	function initializeButtonWidgets() {
		const containers = document.querySelectorAll('.sitestaffr-button-container');

		// Determine if any button needs voice
		let anyNeedsVoice = false;
		containers.forEach(c => {
			const m = c.dataset.mode || (window.sitestaffr_button_data && sitestaffr_button_data.mode) || 'voice';
			if (m !== 'text') anyNeedsVoice = true;
		});


		if (anyNeedsVoice && typeof SiteStaffrCore === 'undefined') {
			console.error('SiteStaffr: Core not loaded. Make sure sitestaffr-core.js is enqueued first.');
			return;
		}

		containers.forEach(container => {
			try {
				new SiteStaffrButtonWidget(container);
			} catch (e) {
				console.error('SiteStaffr: Button widget initialization failed.', e);
			}
		});
	}

	// Initialize when DOM is ready
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', initializeButtonWidgets);
	} else {
		initializeButtonWidgets();
	}

})();
