/**
 * Basic Forms - AI Form Builder chat widget
 *
 * @package BasicForms
 */
(function ($) {
	'use strict';

	var STORAGE_TRANSFER = 'wppsBfAiChatTransfer';

	$(function () {
		if (typeof wppsBfAiData === 'undefined') {
			return;
		}

		var $chat = $('#wpps-bf-ai-chat');
		if (!$chat.length) {
			return;
		}

		var $panel = $('#wpps-bf-ai-chat-panel');
		var $launcher = $('#wpps-bf-ai-chat-launcher');
		var $messages = $('#wpps-bf-ai-chat-messages');
		var $prompt = $('#wpps-bf-ai-prompt');
		var $send = $('#wpps-bf-ai-send');
		var $loader = $send.find('.wpps-bf-ai-send__loader');
		var $sendLabel = $send.find('.wpps-bf-ai-send__label');
		var $improve = $('#wpps-bf-ai-improve');
		var $improveUndo = $('#wpps-bf-ai-improve-undo');
		var promptBeforeImprove = '';
		var isImproving = false;
		var strings = wppsBfAiData.strings || {};
		var isBusy = false;
		var isOpen = false;
		var isEditMode = wppsBfAiData.mode === 'edit';
		var messages = [];
		var lastUserPrompt = '';
		var shouldOpenOnLoad = false;
		// Preview/accept-reject state. Only one AI change is pending review at a time,
		// and the undo snapshot lets us roll back the most recently applied change.
		var pendingChange = null;
		var undoSnapshot = null;
		// Live text accumulated from a streaming response; null when not streaming,
		// '' before the first token arrives (shows the typing indicator).
		var streamingText = null;
		var revealTimer = null;

		function storageKey() {
			var formId = parseInt($('input[name="wpps_bf_form_id"]').val(), 10) || 0;
			return 'wppsBfAiChat_' + (wppsBfAiData.mode || 'create') + '_' + formId;
		}

		function escapeHtml(text) {
			return String(text || '')
				.replace(/&/g, '&amp;')
				.replace(/</g, '&lt;')
				.replace(/>/g, '&gt;')
				.replace(/"/g, '&quot;');
		}

		// Render a safe subset of markdown: everything is escaped first, then a few
		// inline/block patterns are re-introduced. No raw HTML from the model is trusted.
		function renderRichText(text) {
			var safe = escapeHtml(text);

			// Fenced/inline code first so their contents are not further transformed.
			safe = safe.replace(/```([\s\S]*?)```/g, function (m, code) {
				return '<pre class="wpps-bf-ai-code">' + code.replace(/^\n+|\n+$/g, '') + '</pre>';
			});
			safe = safe.replace(/`([^`]+)`/g, '<code>$1</code>');

			// Bold and italic.
			safe = safe.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
			safe = safe.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');

			// Bulleted lists: consecutive lines beginning with - or *.
			safe = safe.replace(/(?:^|\n)((?:[-*] .*(?:\n|$))+)/g, function (m, block) {
				var items = block.trim().split(/\n/).map(function (line) {
					return '<li>' + line.replace(/^[-*]\s+/, '') + '</li>';
				}).join('');
				return '<ul class="wpps-bf-ai-list">' + items + '</ul>';
			});

			// Remaining newlines become breaks (but not inside block elements we just made).
			safe = safe.replace(/\n/g, '<br>');
			safe = safe.replace(/<br>(\s*<(?:ul|pre))/g, '$1');
			safe = safe.replace(/(<\/(?:ul|pre)>)\s*<br>/g, '$1');

			return safe;
		}

		function welcomeMessage() {
			return isEditMode ? (strings.welcomeEdit || '') : (strings.welcomeCreate || '');
		}

		function defaultMessages() {
			var welcome = welcomeMessage();
			if (!welcome) {
				return [];
			}
			return [{ role: 'assistant', content: welcome }];
		}

		function loadMessages() {
			var transferred = sessionStorage.getItem(STORAGE_TRANSFER);
			if (transferred) {
				try {
					var parsedTransfer = JSON.parse(transferred);
					if (Array.isArray(parsedTransfer) && parsedTransfer.length) {
						sessionStorage.removeItem(STORAGE_TRANSFER);
						messages = parsedTransfer;
						shouldOpenOnLoad = true;
						saveMessages();
						return;
					}
				} catch (e) {
					sessionStorage.removeItem(STORAGE_TRANSFER);
				}
			}

			try {
				var stored = sessionStorage.getItem(storageKey());
				if (stored) {
					var parsed = JSON.parse(stored);
					if (Array.isArray(parsed) && parsed.length) {
						messages = parsed;
						return;
					}
				}
			} catch (err) {
				sessionStorage.removeItem(storageKey());
			}

			messages = defaultMessages();
		}

		function saveMessages() {
			try {
				sessionStorage.setItem(storageKey(), JSON.stringify(messages));
			} catch (err) {
				// Ignore quota errors.
			}
		}

		function historyForRequest() {
			var conversational = messages.filter(function (message) {
				return !message.error;
			});

			if (conversational.length <= 1) {
				return [];
			}

			var history = conversational.slice(0, -1).map(function (message) {
				return {
					role: message.role,
					content: message.content
				};
			});

			while (history.length && history[0].role === 'assistant') {
				history.shift();
			}

			return history;
		}

		function renderMessages() {
			var html = '';
			messages.forEach(function (message, index) {
				var roleClass = message.role === 'user' ? 'is-user' : 'is-assistant';
				if (message.error) {
					roleClass += ' is-error';
				} else if (message.notice) {
					roleClass += ' is-notice';
				}
				var roleLabel = message.role === 'user'
					? (strings.you || 'You')
					: (strings.assistant || 'Assistant');
				html += '<div class="wpps-bf-ai-chat-message ' + roleClass + '">';
				html += '<div class="wpps-bf-ai-chat-message__label">' + escapeHtml(roleLabel) + '</div>';
				html += '<div class="wpps-bf-ai-chat-message__bubble">' + renderRichText(message.content) + '</div>';
				var tools = '';
				if (message.role === 'assistant' && !message.error && !message.notice) {
					tools += '<button type="button" class="wpps-bf-ai-msg-copy" data-copy="' + escapeHtml(message.content) + '" title="' + escapeHtml(strings.copy || 'Copy') + '"><span class="dashicons dashicons-admin-page" aria-hidden="true"></span><span class="screen-reader-text">' + escapeHtml(strings.copy || 'Copy') + '</span></button>';
				}
				if (message.error && index === messages.length - 1 && lastUserPrompt) {
					tools += '<button type="button" class="button button-small wpps-bf-ai-chat-retry" data-prompt="' + escapeHtml(lastUserPrompt) + '">' + escapeHtml(strings.retry || 'Retry') + '</button>';
				}
				if (tools) {
					html += '<div class="wpps-bf-ai-chat-message__tools">' + tools + '</div>';
				}
				html += '</div>';
			});

			if (pendingChange) {
				html += previewCardHtml(pendingChange.diff);
			} else if (undoSnapshot) {
				html += undoBarHtml();
			}

			if (!pendingChange && !isBusy && streamingText === null && messages.length <= 1) {
				html += suggestionsHtml();
			}

			if (streamingText) {
				html += '<div class="wpps-bf-ai-chat-message is-assistant is-streaming">';
				html += '<div class="wpps-bf-ai-chat-message__label">' + escapeHtml(strings.assistant || 'Assistant') + '</div>';
				html += '<div class="wpps-bf-ai-chat-message__bubble">' + renderRichText(streamingText) + '<span class="wpps-bf-ai-chat-cursor" aria-hidden="true"></span></div>';
				html += '</div>';
			} else if (isBusy) {
				html += '<div class="wpps-bf-ai-chat-message is-assistant is-typing">';
				html += '<div class="wpps-bf-ai-chat-message__label">' + escapeHtml(strings.assistant || 'Assistant') + '</div>';
				html += '<div class="wpps-bf-ai-chat-message__bubble"><span class="wpps-bf-ai-chat-typing">' + escapeHtml(strings.thinking || 'Thinking…') + '</span></div>';
				html += '</div>';
			}

			$messages.html(html);
			scrollMessagesToBottom();
		}

		function addMessage(role, content, isError, isNotice) {
			content = (content || '').trim();
			if (!content) {
				return;
			}
			messages.push({
				role: role,
				content: content,
				error: !!isError,
				notice: !!isNotice
			});
			saveMessages();
			renderMessages();
		}

		function removeLastMessage() {
			if (!messages.length) {
				return;
			}
			messages.pop();
			saveMessages();
			renderMessages();
		}

		function scrollMessagesToBottom() {
			if ($messages.length && $messages[0]) {
				$messages.scrollTop($messages[0].scrollHeight);
			}
		}

		function openChat() {
			isOpen = true;
			$chat.addClass('is-open').attr('aria-hidden', 'false');
			$panel.prop('hidden', false);
			$launcher.attr('aria-expanded', 'true');
			// The panel was hidden while messages rendered, so scroll height was 0.
			// Jump to the latest message now that it is visible.
			scrollMessagesToBottom();
			if ($prompt.length && !$prompt.prop('disabled')) {
				$prompt.trigger('focus');
			}
		}

		function closeChat() {
			if (isBusy) {
				return;
			}
			isOpen = false;
			$chat.removeClass('is-open').attr('aria-hidden', 'true');
			$panel.prop('hidden', true);
			$launcher.attr('aria-expanded', 'false');
		}

		function toggleChat() {
			if (isOpen) {
				closeChat();
			} else {
				openChat();
			}
		}

		function setBusy(busy) {
			isBusy = busy;
			$messages.attr('aria-busy', busy ? 'true' : 'false');
			$send.prop('disabled', busy || isImproving || !wppsBfAiData.hasApiKey);
			$prompt.prop('disabled', busy || isImproving || !wppsBfAiData.hasApiKey);
			$improve.prop('disabled', busy || isImproving || !wppsBfAiData.hasApiKey || !($prompt.val() || '').trim());
			$loader.prop('hidden', !busy);
			$sendLabel.prop('hidden', busy);
			$chat.find('.wpps-bf-ai-example, .wpps-bf-ai-chat-clear, .wpps-bf-ai-chat-close').prop('disabled', busy);
			renderMessages();
			$chat.find('.wpps-bf-ai-apply, .wpps-bf-ai-discard, .wpps-bf-ai-undo__btn, .wpps-bf-ai-suggest').prop('disabled', busy);
		}

		function updatePromptCount() {
			var $count = $('#wpps-bf-ai-prompt-count');
			if ($count.length) {
				$count.text(($prompt.val() || '').length);
			}
			$improve.prop('disabled', isBusy || isImproving || !wppsBfAiData.hasApiKey || !($prompt.val() || '').trim());
		}

		function setImproving(busy) {
			isImproving = busy;
			$improve.toggleClass('is-busy', busy).prop('disabled', busy || isBusy || !wppsBfAiData.hasApiKey || !($prompt.val() || '').trim());
			$improve.find('.wpps-bf-ai-improve__spinner').prop('hidden', !busy);
			$send.prop('disabled', busy || isBusy || !wppsBfAiData.hasApiKey);
			$prompt.prop('disabled', busy || isBusy || !wppsBfAiData.hasApiKey);
		}

		function improveCurrentPrompt() {
			var text = ($prompt.val() || '').trim();
			if (!text || isBusy || isImproving || !wppsBfAiData.hasApiKey) {
				if (!text) {
					addMessage('assistant', strings.improveEmpty || 'Type a request first, then improve it.', false, true);
				}
				return;
			}

			setImproving(true);
			var payload = {
				action: 'wpps_basic_forms_ai_improve_prompt',
				nonce: wppsBfAiData.nonce,
				prompt: text,
				mode: wppsBfAiData.mode || 'create'
			};
			if (isEditMode) {
				payload.form_context = JSON.stringify(getFormContext());
			}

			$.post(wppsBfAiData.ajaxUrl, payload).done(function (response) {
				var improved = response && response.success && response.data ? (response.data.prompt || '') : '';
				improved = String(improved).trim();
				if (!improved) {
					var msg = response && response.data && response.data.message
						? response.data.message
						: (strings.improveFailed || 'Could not improve that prompt. Try again.');
					addMessage('assistant', msg, true);
					return;
				}
				promptBeforeImprove = text;
				$prompt.val(improved);
				updatePromptCount();
				$improveUndo.prop('hidden', false);
				$prompt.trigger('focus');
			}).fail(function () {
				addMessage('assistant', strings.networkError || 'Network error. Please try again.', true);
			}).always(function () {
				setImproving(false);
				updatePromptCount();
			});
		}

		function undoImprovedPrompt() {
			if (!promptBeforeImprove) {
				return;
			}
			$prompt.val(promptBeforeImprove);
			promptBeforeImprove = '';
			$improveUndo.prop('hidden', true);
			updatePromptCount();
			$prompt.trigger('focus');
		}

		function applySettings(settings) {
			if (!settings || typeof settings !== 'object') {
				return;
			}
			if (settings.subject) {
				$('#wpps-bf-subject').val(settings.subject);
			}
			if (settings.success_message) {
				$('#wpps-bf-success-message').val(settings.success_message);
			}
			if (settings.submit_button_text) {
				$('#wpps-bf-submit-text').val(settings.submit_button_text);
			}
			if (settings.redirect_url) {
				$('#wpps-bf-redirect').val(settings.redirect_url);
			}
			if (Object.prototype.hasOwnProperty.call(settings, 'enable_autoreply')) {
				var enabled = settings.enable_autoreply === '1' || settings.enable_autoreply === true || settings.enable_autoreply === 1;
				$('#wpps-bf-enable-autoreply').prop('checked', enabled);
				$('#wpps-bf-autoreply-settings').toggle(enabled);
			}
			if (settings.autoreply_subject) {
				$('#wpps-bf-autoreply-subject').val(settings.autoreply_subject);
			}
			if (settings.autoreply_message) {
				$('#wpps-bf-autoreply-message').val(settings.autoreply_message);
			}
		}

		function getFormContext() {
			if (typeof window.wppsBfGetFormSnapshot === 'function') {
				return window.wppsBfGetFormSnapshot();
			}
			return {
				title: $('#wpps-bf-form-title').val() || '',
				fields: [],
				settings: {}
			};
		}

		function applyDesign(design) {
			if (!design || typeof design !== 'object') {
				return $.Deferred().resolve().promise();
			}
			if (typeof window.wppsBfSyncDesignModalFromSaved === 'function') {
				window.wppsBfSyncDesignModalFromSaved(design);
			}
			var formId = parseInt($('input[name="wpps_bf_form_id"]').val(), 10) || 0;
			if (formId <= 0 || typeof window.wppsBfSaveDesignFromModal !== 'function') {
				return $.Deferred().resolve().promise();
			}
			return window.wppsBfSaveDesignFromModal();
		}

		function fieldKey(field) {
			if (!field || typeof field !== 'object') {
				return '';
			}
			if (field.field_id) {
				return 'fid:' + field.field_id;
			}
			return 'id:' + field.id;
		}

		function fieldLabel(field) {
			if (!field || typeof field !== 'object') {
				return '';
			}
			var label = (field.label || '').toString().trim();
			if (label) {
				return label;
			}
			return (field.type || 'field').toString();
		}

		function comparableField(field) {
			// Ignore the numeric id when comparing, since it can be reassigned on apply.
			var clone = $.extend(true, {}, field);
			delete clone.id;
			return JSON.stringify(clone);
		}

		function changedDesignKeys(before, after) {
			var keys = [];
			if (!after || typeof after !== 'object') {
				return keys;
			}
			before = before && typeof before === 'object' ? before : {};
			Object.keys(after).forEach(function (key) {
				if ((before[key] || '') !== (after[key] || '')) {
					keys.push(key);
				}
			});
			return keys;
		}

		var settingLabels = {
			subject: strings.setSubject || 'Email subject',
			success_message: strings.setSuccess || 'Success message',
			submit_button_text: strings.setSubmit || 'Submit button',
			redirect_url: strings.setRedirect || 'Redirect URL',
			enable_autoreply: strings.setAutoreply || 'Auto-reply',
			autoreply_subject: strings.setAutoreplySubject || 'Auto-reply subject',
			autoreply_message: strings.setAutoreplyMessage || 'Auto-reply message'
		};

		function computeDiff(data, intent, before) {
			var diff = { fieldsAdded: [], fieldsRemoved: [], fieldsModified: [], settings: [], design: [], titleFrom: null, titleTo: null };
			var changed = Array.isArray(data.changed) ? data.changed : null;

			function partChanged(part) {
				if (changed) {
					return changed.indexOf(part) !== -1;
				}
				return intent !== 'add_fields' && intent !== 'design';
			}

			if (partChanged('title') && data.title && data.title !== (before.title || '')) {
				diff.titleFrom = before.title || '';
				diff.titleTo = data.title;
			}

			if (Array.isArray(data.fields) && (partChanged('fields') || intent === 'add_fields')) {
				var after = data.fields;
				if (intent === 'add_fields') {
					after.forEach(function (f) { diff.fieldsAdded.push(fieldLabel(f)); });
				} else {
					var beforeFields = Array.isArray(before.fields) ? before.fields : [];
					var beforeMap = {};
					var afterMap = {};
					beforeFields.forEach(function (f) { beforeMap[fieldKey(f)] = f; });
					after.forEach(function (f) { afterMap[fieldKey(f)] = f; });
					after.forEach(function (f) {
						var k = fieldKey(f);
						if (!Object.prototype.hasOwnProperty.call(beforeMap, k)) {
							diff.fieldsAdded.push(fieldLabel(f));
						} else if (comparableField(beforeMap[k]) !== comparableField(f)) {
							diff.fieldsModified.push(fieldLabel(f));
						}
					});
					beforeFields.forEach(function (f) {
						if (!Object.prototype.hasOwnProperty.call(afterMap, fieldKey(f))) {
							diff.fieldsRemoved.push(fieldLabel(f));
						}
					});
				}
			}

			if (partChanged('settings')) {
				// Only diff the keys the editor tracks, so internal settings that the
				// backend always fills (recipient, captcha, store_ip, etc.) are not noise.
				var editableSettingKeys = ['subject', 'success_message', 'submit_button_text', 'redirect_url', 'enable_autoreply', 'autoreply_subject', 'autoreply_message'];
				var beforeSettings = before.settings || {};
				var afterSettings = data.settings || {};
				diff.settings = editableSettingKeys.filter(function (k) {
					return (beforeSettings[k] || '').toString() !== (afterSettings[k] || '').toString();
				}).map(function (k) {
					return settingLabels[k] || k;
				});
			}

			if (partChanged('design') && data.design) {
				diff.design = changedDesignKeys(before.design, data.design);
			}

			return diff;
		}

		function diffLine(labelText, items, extraClass) {
			if (!items || !items.length) {
				return '';
			}
			var shown = items.slice(0, 6).map(escapeHtml).join(', ');
			if (items.length > 6) {
				shown += ' +' + (items.length - 6);
			}
			return '<li class="wpps-bf-ai-diff-line ' + (extraClass || '') + '"><span class="wpps-bf-ai-diff-tag">' +
				escapeHtml(labelText) + '</span> <span class="wpps-bf-ai-diff-items">' + shown + '</span></li>';
		}

		function previewCardHtml(diff) {
			var body = '';
			if (diff.titleTo !== null) {
				body += diffLine(strings.diffTitle || 'Title', [diff.titleTo], 'is-modified');
			}
			body += diffLine(strings.diffAdded || 'Add fields', diff.fieldsAdded, 'is-added');
			body += diffLine(strings.diffModified || 'Update fields', diff.fieldsModified, 'is-modified');
			body += diffLine(strings.diffRemoved || 'Remove fields', diff.fieldsRemoved, 'is-removed');
			body += diffLine(strings.diffSettings || 'Settings', diff.settings, 'is-modified');
			if (diff.design.length) {
				body += diffLine(strings.diffDesign || 'Design', [
					(diff.design.length + ' ' + (strings.diffDesignKeys || 'style changes'))
				], 'is-modified');
			}
			if (!body) {
				body = '<li class="wpps-bf-ai-diff-line">' + escapeHtml(strings.diffNone || 'Minor changes.') + '</li>';
			}

			var html = '<div class="wpps-bf-ai-preview" role="group" aria-label="' + escapeHtml(strings.previewTitle || 'Proposed changes') + '">';
			html += '<div class="wpps-bf-ai-preview__head">' + escapeHtml(strings.previewTitle || 'Proposed changes') + '</div>';
			html += '<ul class="wpps-bf-ai-preview__list">' + body + '</ul>';
			html += '<div class="wpps-bf-ai-preview__actions">';
			html += '<button type="button" class="button button-primary button-small wpps-bf-ai-apply">' + escapeHtml(strings.apply || 'Apply changes') + '</button>';
			html += '<button type="button" class="button button-small wpps-bf-ai-discard">' + escapeHtml(strings.discard || 'Discard') + '</button>';
			html += '</div></div>';
			return html;
		}

		function suggestionsHtml() {
			var list = isEditMode
				? (wppsBfAiData.suggestionsEdit || [])
				: (wppsBfAiData.suggestionsCreate || []);
			if (!list.length || !wppsBfAiData.hasApiKey) {
				return '';
			}
			var chips = list.map(function (item) {
				return '<button type="button" class="wpps-bf-ai-suggest" data-prompt="' + escapeHtml(item) + '">' + escapeHtml(item) + '</button>';
			}).join('');
			return '<div class="wpps-bf-ai-suggestions">' +
				'<span class="wpps-bf-ai-suggestions__label">' + escapeHtml(strings.suggestions || 'Try asking') + '</span>' +
				'<div class="wpps-bf-ai-suggestions__list">' + chips + '</div></div>';
		}

		function undoBarHtml() {
			return '<div class="wpps-bf-ai-undo">' +
				'<span class="wpps-bf-ai-undo__label">' + escapeHtml(strings.applied || 'Changes applied.') + '</span>' +
				'<button type="button" class="button-link wpps-bf-ai-undo__btn">' + escapeHtml(strings.undo || 'Undo') + '</button>' +
				'</div>';
		}

		function payloadHasActionableChange(data) {
			if (!data || typeof data !== 'object') {
				return false;
			}
			if (Array.isArray(data.fields) && data.fields.length > 0) {
				return true;
			}
			if (data.design && typeof data.design === 'object' && Object.keys(data.design).length > 0) {
				return true;
			}
			if (Array.isArray(data.changed) && data.changed.length > 0) {
				return true;
			}
			return false;
		}

		function applyPayloadToForm(data, intent) {
			var changed = Array.isArray(data.changed) && data.changed.length ? data.changed : null;

			function didChange(part) {
				if (changed) {
					return changed.indexOf(part) !== -1;
				}
				if (part === 'fields') {
					return Array.isArray(data.fields) && data.fields.length > 0;
				}
				if (part === 'design') {
					return !!(data.design && typeof data.design === 'object' && Object.keys(data.design).length);
				}
				if (part === 'title') {
					return !!(data.title && String(data.title).trim());
				}
				if (part === 'settings') {
					return !!(data.settings && typeof data.settings === 'object');
				}
				return intent !== 'add_fields' && intent !== 'design';
			}

			if (data.title && didChange('title')) {
				$('#wpps-bf-form-title').val(data.title);
			}
			if (didChange('settings')) {
				applySettings(data.settings);
			}
			if (didChange('fields')) {
				if (typeof window.wppsBfApplyAiPayload !== 'function') {
					throw new Error('form-builder-unavailable');
				}
				var mode = data.fields_mode || '';
				var applyIntent = (mode === 'append' || intent === 'add_fields')
					? 'add_fields'
					: (mode === 'replace' || intent === 'replace')
						? 'replace'
						: 'modify';
				window.wppsBfApplyAiPayload(data, applyIntent);
			}
			if (didChange('design')) {
				applyDesign(data.design);
			}
		}

		function restoreSnapshot(snapshot) {
			// A snapshot is a full form context; replay it through the same apply paths.
			if (!snapshot) {
				return;
			}
			$('#wpps-bf-form-title').val(snapshot.title || '');
			applySettings(snapshot.settings || {});
			if (typeof window.wppsBfApplyAiPayload === 'function') {
				window.wppsBfApplyAiPayload({ fields: snapshot.fields || [] }, 'modify');
			}
			applyDesign(snapshot.design || {});
		}

		function applyPendingChange() {
			if (!pendingChange) {
				return;
			}
			// Snapshot the current form first so Undo can roll back to exactly this state.
			undoSnapshot = getFormContext();
			applyPayloadToForm(pendingChange.data, pendingChange.intent);
			pendingChange = null;
			renderMessages();
		}

		function discardPendingChange() {
			if (!pendingChange) {
				return;
			}
			pendingChange = null;
			addMessage('assistant', strings.discarded || 'Discarded those changes. Your form is unchanged.', false, true);
		}

		function undoLastChange() {
			if (!undoSnapshot) {
				return;
			}
			restoreSnapshot(undoSnapshot);
			undoSnapshot = null;
			addMessage('assistant', strings.undone || 'Reverted the last change.', false, true);
		}

		function processData(data, overrideMessage) {
			data = data || {};
			var assistantMessage = (overrideMessage && overrideMessage.trim())
				? overrideMessage.trim()
				: (data.assistant_message || strings.applied || 'Done.');

			if (isEditMode) {
				var intent = data.intent || 'modify';
				if (!payloadHasActionableChange(data)) {
					addMessage('assistant', assistantMessage, false, true);
					return;
				}

				// Apply immediately so the form updates without a hidden Apply step.
				// Undo is offered for the last change; Update Form still persists it.
				try {
					undoSnapshot = getFormContext();
					applyPayloadToForm(data, intent);
					pendingChange = null;
					addMessage('assistant', assistantMessage);
					renderMessages();
					return;
				} catch (err) {
					pendingChange = { data: data, intent: intent, diff: computeDiff(data, intent, getFormContext()) };
					undoSnapshot = null;
					addMessage('assistant', assistantMessage);
					renderMessages();
					return;
				}
			}

			addMessage('assistant', assistantMessage, false, false);

			if (data.redirect_url) {
				try {
					sessionStorage.setItem(STORAGE_TRANSFER, JSON.stringify(messages));
					// The new form's conversation moves to the editor, so the "create"
					// chat must reset — otherwise the next new form reuses this thread.
					sessionStorage.removeItem(storageKey());
				} catch (err) {
					// Ignore quota errors.
				}
				window.location.href = data.redirect_url;
			}
		}

		function handleSuccess(response) {
			processData(response.data || {}, null);
		}

		// When the model returns pure JSON with no natural-language preamble to stream,
		// reveal the friendly summary with a short typewriter effect so the assistant
		// still feels responsive, then finalize (message + preview) via processData.
		function revealThenProcess(data) {
			var text = (data.assistant_message || strings.applied || 'Done.').toString();
			if (!text) {
				processData(data, null);
				return;
			}
			var i = 0;
			var step = Math.max(1, Math.ceil(text.length / 32));
			streamingText = '';
			renderMessages();
			revealTimer = window.setInterval(function () {
				i += step;
				streamingText = text.slice(0, i);
				renderMessages();
				if (i >= text.length) {
					window.clearInterval(revealTimer);
					revealTimer = null;
					streamingText = null;
					processData(data, text);
				}
			}, 24);
		}

		function cancelReveal() {
			if (revealTimer) {
				window.clearInterval(revealTimer);
				revealTimer = null;
			}
		}

		function requestPayload(text, action) {
			var payload = {
				action: action,
				nonce: wppsBfAiData.nonce,
				prompt: text,
				mode: wppsBfAiData.mode || 'create',
				history: JSON.stringify(historyForRequest())
			};
			if (isEditMode) {
				payload.form_context = JSON.stringify(getFormContext());
			}
			return payload;
		}

		function postMessage(text) {
			$.post(wppsBfAiData.ajaxUrl, requestPayload(text, 'wpps_basic_forms_ai_generate')).done(function (response) {
				if (!response || !response.success) {
					var msg = response && response.data && response.data.message
						? response.data.message
						: strings.networkError;
					if (response && response.data && response.data.code === 'rate_limit') {
						msg = strings.rateLimited || msg;
					}
					addMessage('assistant', msg, true);
					return;
				}
				handleSuccess(response);
			}).fail(function () {
				addMessage('assistant', strings.networkError || 'Network error. Please try again.', true);
			}).always(function () {
				setBusy(false);
			});
		}

		function canUseStreaming() {
			return typeof window.fetch === 'function'
				&& typeof window.ReadableStream !== 'undefined'
				&& typeof window.TextDecoder !== 'undefined';
		}

		function parseSseChunk(chunk) {
			var lines = chunk.split('\n');
			var event = 'message';
			var dataStr = '';
			lines.forEach(function (line) {
				if (line.indexOf('event:') === 0) {
					event = line.slice(6).trim();
				} else if (line.indexOf('data:') === 0) {
					dataStr += line.slice(5).replace(/^\s/, '');
				}
			});
			return { event: event, data: dataStr };
		}

		function streamMessage(text, onHandled) {
			streamingText = '';
			renderMessages();

			var handledDone = false;

			function dispatch(chunk) {
				var parsed = parseSseChunk(chunk);
				if (!parsed.data) {
					return;
				}
				var data;
				try {
					data = JSON.parse(parsed.data);
				} catch (e) {
					return;
				}
				if (parsed.event === 'delta') {
					streamingText += (data.text || '');
					renderMessages();
				} else if (parsed.event === 'done') {
					if (isEditMode && !payloadHasActionableChange(data) && !data.redirect_url) {
						// Stream "succeeded" with a chat line but no form payload — fall
						// back to the buffered endpoint so fields are not silently dropped.
						throw new Error('stream-payload-incomplete');
					}
					handledDone = true;
					onHandled();
					var streamed = (streamingText && streamingText.trim()) ? streamingText : null;
					streamingText = null;
					processData(data, streamed);
				} else if (parsed.event === 'error') {
					handledDone = true;
					onHandled();
					streamingText = null;
					var msg = data.message || strings.networkError;
					if (data.code === 'rate_limit') {
						msg = strings.rateLimited || msg;
					}
					addMessage('assistant', msg, true);
				}
			}

			return fetch(wppsBfAiData.ajaxUrl, {
				method: 'POST',
				credentials: 'same-origin',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
					'Accept': 'text/event-stream'
				},
				body: $.param(requestPayload(text, 'wpps_basic_forms_ai_stream'))
			}).then(function (response) {
				if (!response.ok || !response.body) {
					throw new Error('stream-unavailable');
				}
				var contentType = response.headers.get('Content-Type') || '';
				if (contentType.indexOf('text/event-stream') === -1) {
					throw new Error('not-sse');
				}

				var reader = response.body.getReader();
				var decoder = new TextDecoder();
				var buffer = '';

				function pump() {
					return reader.read().then(function (result) {
						if (result.done) {
							return;
						}
						buffer += decoder.decode(result.value, { stream: true });
						var parts = buffer.split('\n\n');
						buffer = parts.pop();
						parts.forEach(dispatch);
						return pump();
					});
				}

				return pump().then(function () {
					if (buffer && buffer.trim()) {
						dispatch(buffer);
					}
					if (!handledDone) {
						throw new Error('stream-incomplete');
					}
				});
			});
		}

		function sendMessage(text, options) {
			options = options || {};
			text = (text || '').trim();
			if (!text || isBusy || !wppsBfAiData.hasApiKey) {
				return;
			}

			// Asking for something new supersedes any change still awaiting review.
			pendingChange = null;
			cancelReveal();
			streamingText = null;

			if (!options.skipUserMessage) {
				addMessage('user', text, false);
				$prompt.val('');
				promptBeforeImprove = '';
				$improveUndo.prop('hidden', true);
				updatePromptCount();
			}

			lastUserPrompt = text;
			setBusy(true);

			// Edit mode uses the buffered endpoint so the form payload cannot be
			// dropped by an incomplete SSE "done" event (the cause of "I added
			// fields" with no fields appearing). Create mode still streams.
			if (!canUseStreaming() || isEditMode) {
				postMessage(text);
				return;
			}

			var handled = false;
			streamMessage(text, function () { handled = true; }).then(function () {
				setBusy(false);
			}).catch(function () {
				// Streaming failed (unsupported, buffered, or interrupted) before a final
				// event — fall back to the buffered endpoint so the request still completes.
				streamingText = null;
				if (handled) {
					setBusy(false);
				} else {
					postMessage(text);
				}
			});
		}

		function clearChat() {
			if (isBusy) {
				return;
			}
			if (!window.confirm(strings.clearConfirm || 'Clear this conversation?')) {
				return;
			}
			cancelReveal();
			streamingText = null;
			messages = defaultMessages();
			lastUserPrompt = '';
			pendingChange = null;
			undoSnapshot = null;
			saveMessages();
			renderMessages();
		}

		loadMessages();
		renderMessages();
		updatePromptCount();

		if (shouldOpenOnLoad) {
			openChat();
		}

		$(document).on('click', '#wpps-bf-open-ai-builder, #wpps-bf-open-ai-builder-hero, #wpps-bf-open-ai-builder-promo, .wpps-bf-open-ai-builder', function (e) {
			e.preventDefault();
			openChat();
		});

		$launcher.on('click', function (e) {
			e.preventDefault();
			toggleChat();
		});

		$('#wpps-bf-ai-chat-close').on('click', function (e) {
			e.preventDefault();
			closeChat();
		});

		$('#wpps-bf-ai-chat-clear').on('click', function (e) {
			e.preventDefault();
			clearChat();
		});

		$(document).on('click', '.wpps-bf-ai-chat-retry', function () {
			var text = $(this).data('prompt') || lastUserPrompt;
			if (!text || isBusy) {
				return;
			}
			removeLastMessage();
			sendMessage(text, { skipUserMessage: true });
		});

		$chat.on('click', '.wpps-bf-ai-apply', function () {
			if (isBusy) {
				return;
			}
			applyPendingChange();
		});

		$chat.on('click', '.wpps-bf-ai-discard', function () {
			if (isBusy) {
				return;
			}
			discardPendingChange();
		});

		$chat.on('click', '.wpps-bf-ai-undo__btn', function () {
			if (isBusy) {
				return;
			}
			undoLastChange();
		});

		$chat.on('click', '.wpps-bf-ai-suggest', function () {
			if (isBusy) {
				return;
			}
			sendMessage($(this).data('prompt'));
		});

		$chat.on('click', '.wpps-bf-ai-msg-copy', function () {
			var text = $(this).data('copy');
			if (text === undefined || text === null) {
				return;
			}
			text = String(text);
			var $btn = $(this);
			function flash() {
				$btn.addClass('is-copied');
				window.setTimeout(function () { $btn.removeClass('is-copied'); }, 1200);
			}
			if (navigator.clipboard && navigator.clipboard.writeText) {
				navigator.clipboard.writeText(text).then(flash, function () {});
			} else {
				var $tmp = $('<textarea>').val(text).css({ position: 'fixed', left: '-9999px' }).appendTo('body');
				$tmp[0].select();
				try { document.execCommand('copy'); flash(); } catch (e) {}
				$tmp.remove();
			}
		});

		$improve.on('click', function (e) {
			e.preventDefault();
			improveCurrentPrompt();
		});

		$improveUndo.on('click', function (e) {
			e.preventDefault();
			undoImprovedPrompt();
		});

		$prompt.on('input', function () {
			if (promptBeforeImprove && ($prompt.val() || '') === promptBeforeImprove) {
				$improveUndo.prop('hidden', true);
				promptBeforeImprove = '';
			}
			updatePromptCount();
		});

		$prompt.on('keydown', function (e) {
			if (e.key === 'Enter' && !e.shiftKey) {
				e.preventDefault();
				sendMessage($prompt.val());
			}
		});

		$panel.on('keydown', function (e) {
			if (e.key === 'Escape' && !isBusy) {
				closeChat();
				$launcher.trigger('focus');
			}
		});

		$send.on('click', function () {
			sendMessage($prompt.val());
		});
	});
})(jQuery);
