/**
 * SiteStaffr Text Chat Module
 *
 * Handles streaming text chat via AJAX proxy to middleware.
 * Mirrors the voice conversation save pattern from SiteStaffrCore.
 *
 * @package    SiteStaffr
 * @since      1.5.0
 */

/* eslint-disable no-console */

(function () {
	'use strict';

	var MAX_MESSAGE_LENGTH = 2000;
	var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
	// Watchdog: if no NDJSON activity arrives within this window, treat the turn
	// as failed and tell the visitor, instead of leaving them staring at a
	// typing indicator forever (Loria Medical 2026-06-19: a transient upstream
	// error left a visitor waiting ~2 minutes with no response). Re-armed on each
	// chunk, so a slow-but-alive stream is never cut off.
	var RESPONSE_TIMEOUT_MS = 30 * 1000;

	// Temporary diagnostic: enable with window.SITESTAFFR_DEBUG = true in DevTools
	// before opening the chat. Logs every NDJSON event and function call received.
	function log() {
		if (window.SITESTAFFR_DEBUG) {
			console.info.apply(console, ['[SiteStaffr TextChat]'].concat(Array.prototype.slice.call(arguments)));
		}
	}

	/**
	 * @param {Object} config
	 * @param {string} config.ajaxUrl
	 * @param {string} config.pluginUrl
	 * @param {string} config.channelSource  - 'widget' or 'button'
	 * @param {string} config.persona        - 'default' or 'onboarding'
	 * @param {string} config.saveNonce
	 * @param {string} config.finalizeNonce
	 * @param {string} config.chatNonce
	 * @param {boolean} config.soundsEnabled
	 * @param {Object} callbacks
	 * @param {Function} callbacks.onTextDelta      - (delta: string)
	 * @param {Function} callbacks.onStreamStart    - ()
	 * @param {Function} callbacks.onStreamEnd      - ()
	 * @param {Function} callbacks.onMessage        - (role: string, text: string)
	 * @param {Function} callbacks.onFieldUpdate    - (field: string, value: string)
	 * @param {Function} callbacks.onEndSession     - (reason: string)
	 * @param {Function} callbacks.onError          - (message: string)
	 */
	function SiteStaffrTextChat(config, callbacks) {
		this.config = config;
		this.callbacks = callbacks;
		this.callbacks.onDisableInput = callbacks.onDisableInput || null;
		this.callbacks.onShowContactForm = callbacks.onShowContactForm || null;

		this.sessionId = this._generateSessionId();
		this.previousResponseId = null;
		this.transcript = [];
		this.contactMemory = { name: '', phone: '', email: '', reason: '' };
		this.submissionSource = '';
		this.usageTotals = {
			input_tokens: 0,
			output_tokens: 0,
			response_count: 0,
			model: 'text-chat'
		};
		this.startTime = null;
		this.ended = false;
		this._endedBy = null;
		this.sending = false;
		this.saved = false;
		this.muted = false;
		this._qualifiedLeadFired = false;

		this._sendSound = null;
		this._receiveSound = null;
		this._inactivityTimer = null;

		if (config.soundsEnabled) {
			this._sendSound = new Audio(config.pluginUrl + 'assets/audio/visitor-send.mp3');
			this._receiveSound = new Audio(config.pluginUrl + 'assets/audio/ai-send.mp3');
			this._sendSound.volume = 0.3;
			this._receiveSound.volume = 0.3;
		}
	}

	SiteStaffrTextChat.prototype.MAX_MESSAGE_LENGTH = MAX_MESSAGE_LENGTH;

	/**
	 * Start a new chat session. Greeting is displayed client-side;
	 * first API call happens when the visitor sends their first message.
	 */
	SiteStaffrTextChat.prototype.start = function () {
		// Duration timer starts on the FIRST message the visitor sends (see
		// sendMessage), not when the panel opens — so it reflects real conversation
		// length, not time spent reading the greeting or typing the first message.
		this._resetInactivityTimer();

		if (window._sitestaffrTestMode) {
			window._sitestaffrTestMode.flag(this.sessionId, this.config.ajaxUrl);
		}

		// GTM dataLayer: text conversation started
		(window.dataLayer = window.dataLayer || []).push(Object.assign({
			event: 'sitestaffr_conversation_start',
			sitestaffr_mode: 'text',
			sitestaffr_session_id: this.sessionId,
		}, window._sitestaffrGTM.pageContext()));
	};

	/**
	 * Send a message to the AI.
	 * @param {string} message
	 */
	SiteStaffrTextChat.prototype.sendMessage = function (message) {
		if (this.sending) {
			return false;
		}

		// If the session ended (farewell), allow continuing — reset flags so
		// transcript updates are saved and the middleware gets a fresh session.
		if (this.ended) {
			this.ended = false;
			this.saved = false;
		}

		if (message.length > MAX_MESSAGE_LENGTH) {
			message = message.substring(0, MAX_MESSAGE_LENGTH);
		}

		this.sending = true;
		this._resetInactivityTimer();

		// Add visitor message to transcript (skip empty greeting trigger)
		if (message) {
			// Start the duration timer on the first real message sent.
			if (this.startTime === null) {
				this.startTime = Date.now();
			}
			this.transcript.push({
				role: 'user',
				text: message,
				timestamp: new Date().toISOString()
			});
			this.callbacks.onMessage('user', message);
			this._playSound(this._sendSound);
		}

		this.callbacks.onStreamStart();

		var formData = new FormData();
		formData.append('action', 'sitestaffr_chat_message');
		formData.append('nonce', this.config.chatNonce);
		formData.append('message', message);
		formData.append('previous_response_id', this.previousResponseId || '');
		formData.append('session_id', this.sessionId);
		formData.append('persona', this.config.persona || 'default');
		formData.append('contact_fields', JSON.stringify(this.contactMemory));
		formData.append('fields_to_collect', JSON.stringify(this.config.fieldsToCollect || ['name','phone','email','reason']));
		if (window.sitestaffr_widget_data && window.sitestaffr_widget_data.current_post_id) {
			formData.append('current_post_id', window.sitestaffr_widget_data.current_post_id);
		}

		var self = this;
		var controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
		this._abortController = controller;
		this._failureHandled = false;
		this._armResponseWatchdog();

		fetch(this.config.ajaxUrl, {
			method: 'POST',
			body: formData,
			signal: controller ? controller.signal : undefined,
		})
			.then(function (response) {
				if (!response.ok) {
					throw new Error('HTTP ' + response.status);
				}
				return self._readNDJSON(response);
			})
			.catch(function (err) {
				self._clearResponseWatchdog();
				// If our own watchdog already aborted and reported the failure,
				// the abort rejects this promise with an AbortError — don't
				// report a second error on top of the timeout message.
				if (self._failureHandled && err && err.name === 'AbortError') {
					return;
				}
				self.sending = false;
				self.callbacks.onStreamEnd();
				self.callbacks.onError('Something went wrong. Please try again in a moment.');
				console.error('SiteStaffrTextChat: fetch error', err);
			});

		return true;
	};

	/**
	 * Arm (or re-arm) the response watchdog. Called when a turn starts and again
	 * on every NDJSON chunk, so an actively-streaming response is never cut off
	 * but a stalled/hung connection is caught.
	 */
	SiteStaffrTextChat.prototype._armResponseWatchdog = function () {
		var self = this;
		var ms = (this.config && this.config.responseTimeoutMs) || RESPONSE_TIMEOUT_MS;
		this._clearResponseWatchdog();
		this._responseTimer = setTimeout(function () {
			self._onResponseTimeout();
		}, ms);
	};

	/**
	 * Clear the response watchdog timer.
	 */
	SiteStaffrTextChat.prototype._clearResponseWatchdog = function () {
		if (this._responseTimer) {
			clearTimeout(this._responseTimer);
			this._responseTimer = null;
		}
	};

	/**
	 * Watchdog fired: no NDJSON activity within the timeout. Abort the request,
	 * reset the UI, and surface a clear, friendly message so the visitor is
	 * never left waiting on a silent typing indicator.
	 */
	SiteStaffrTextChat.prototype._onResponseTimeout = function () {
		if (!this.sending) {
			return;
		}
		this._failureHandled = true;
		this._clearResponseWatchdog();
		if (this._abortController) {
			try { this._abortController.abort(); } catch (e) { /* already settled */ }
		}
		this.sending = false;
		this.callbacks.onStreamEnd();
		this.callbacks.onError('That took longer than expected. Please try again in a moment.');
		console.error('SiteStaffrTextChat: response watchdog timeout');
	};

	/**
	 * Read NDJSON stream from response.
	 * @param {Response} response
	 */
	SiteStaffrTextChat.prototype._readNDJSON = function (response) {
		var reader = response.body.getReader();
		var decoder = new TextDecoder();
		var buffer = '';
		var accumulatedText = '';
		var self = this;

		function processChunk(result) {
			if (result.done) {
				self._clearResponseWatchdog();
				// Process any remaining buffer
				if (buffer.trim()) {
					self._processLine(buffer.trim(), accumulatedText);
				}
				self._onStreamComplete(accumulatedText);
				return;
			}

			// Activity received — the stream is alive, so push the watchdog out.
			self._armResponseWatchdog();

			buffer += decoder.decode(result.value, { stream: true });
			var lines = buffer.split('\n');
			buffer = lines.pop(); // Keep incomplete last line

			for (var i = 0; i < lines.length; i++) {
				var line = lines[i].trim();
				if (!line) {
					continue;
				}
				accumulatedText = self._processLine(line, accumulatedText);
			}

			return reader.read().then(processChunk);
		}

		return reader.read().then(processChunk);
	};

	/**
	 * Process a single NDJSON line.
	 * @param {string} line
	 * @param {string} accumulatedText
	 * @returns {string} Updated accumulated text
	 */
	SiteStaffrTextChat.prototype._processLine = function (line, accumulatedText) {
		try {
			var chunk = JSON.parse(line);
		} catch (e) {
			log('invalid JSON line', line);
			return accumulatedText;
		}

		log('stream event', chunk.type, chunk);

		if (chunk.type === 'text_delta') {
			accumulatedText += chunk.delta;
			this.callbacks.onTextDelta(chunk.delta);
		} else if (chunk.type === 'function_call') {
			this._handleFunctionCall(chunk.name, chunk.arguments);
		} else if (chunk.type === 'done') {
			this.previousResponseId = chunk.response_id || this.previousResponseId;
			this._pendingSourceChunks = Array.isArray(chunk.source_chunks) ? chunk.source_chunks : null;
			if (chunk.usage) {
				this.usageTotals.input_tokens += chunk.usage.input_tokens || 0;
				this.usageTotals.output_tokens += chunk.usage.output_tokens || 0;
				this.usageTotals.response_count++;
			}
		} else if (chunk.type === 'error') {
			if (chunk.code === 'SESSION_EXPIRED') {
				this.previousResponseId = null;
				this.callbacks.onError(chunk.message || 'Session expired. Starting fresh.');
			} else if (chunk.code === 'CIRCUIT_BREAKER_OPEN' || chunk.code === 'CIRCUIT_BREAKER_LOCKOUT') {
				this.callbacks.onError(chunk.message || 'Our text chat is temporarily unavailable. You can try our voice assistant instead, or check back shortly. Sorry for the inconvenience!');
				if (this.callbacks.onDisableInput) { this.callbacks.onDisableInput(); }
			} else {
				this.callbacks.onError(chunk.message || 'An error occurred.');
			}
		} else if (chunk.type === 'session_cap') {
			this.callbacks.onError(chunk.message || 'Thanks for chatting! If you need more help, feel free to start a new conversation or try our voice assistant.');
			if (this.callbacks.onDisableInput) { this.callbacks.onDisableInput(); }
		} else if (chunk.type === 'daily_cap') {
			try { sessionStorage.setItem('sitestaffr_daily_cap', new Date().toISOString().slice(0, 10)); } catch (e) { /* private browsing */ }
			if (this.callbacks.onShowContactForm) { this.callbacks.onShowContactForm(); }
		}

		return accumulatedText;
	};

	/**
	 * Called when the full stream is complete.
	 * @param {string} fullText
	 */
	SiteStaffrTextChat.prototype._onStreamComplete = function (fullText) {
		this._clearResponseWatchdog();
		this.sending = false;
		this.callbacks.onStreamEnd();

		if (fullText) {
			this.transcript.push({
				role: 'assistant',
				text: fullText,
				timestamp: new Date().toISOString(),
				source_chunks: this._pendingSourceChunks || null
			});
			this._pendingSourceChunks = null;
			this._playSound(this._receiveSound);
		}

		this._resetInactivityTimer();
	};

	/**
	 * Handle function calls from the AI.
	 * @param {string} name
	 * @param {string} argsJson
	 */
	SiteStaffrTextChat.prototype._handleFunctionCall = function (name, argsJson) {
		try {
			var args = JSON.parse(argsJson);
		} catch (e) {
			log('function_call parse error', name, argsJson);
			return;
		}

		log('function_call', name, args);

		if (name === 'update_contact_field' && args.field && args.value) {
			var field = String(args.field);
			var value = String(args.value);
			if (this.contactMemory.hasOwnProperty(field)) {
				this.contactMemory[field] = value;
				this.callbacks.onFieldUpdate(field, value);

				// GTM dataLayer: lead field captured
				(window.dataLayer = window.dataLayer || []).push({
					event: 'sitestaffr_lead_captured',
					sitestaffr_field: field,
					sitestaffr_mode: 'text',
					sitestaffr_session_id: this.sessionId,
				});

				// GTM dataLayer: qualified lead (name + phone or name + email)
				if (!this._qualifiedLeadFired && this.contactMemory.name) {
					if (this.contactMemory.phone || this.contactMemory.email) {
						this._qualifiedLeadFired = true;
						var cm = this.contactMemory;
						var qualifiedFields = Object.keys(cm).filter(function(k) { return !!cm[k]; });
						(window.dataLayer = window.dataLayer || []).push({
							event: 'sitestaffr_qualified_lead',
							sitestaffr_mode: 'text',
							sitestaffr_session_id: this.sessionId,
							sitestaffr_fields_captured: qualifiedFields,
							sitestaffr_service_interest: this.contactMemory.reason || '',
						});
					}
				}
			}
		} else if (name === 'begin_contact_collection' || name === 'begin_answering' || name === 'begin_farewell') {
			// Lifecycle transitions handled server-side — no client action needed.
		}
		// end_session is voice-only. Text chat stays open until the visitor closes
		// the widget, reloads the page, or the inactivity timer fires.
	};

	/**
	 * End the conversation and save.
	 * @param {string} reason
	 */
	SiteStaffrTextChat.prototype.endConversation = function (reason) {
		if (this.ended) {
			return;
		}
		this.ended = true;
		// ended_by contract: ambiguous ends (cap/entitlement contact forms, unknown)
		// stay null deliberately — never guess. end_session is voice-only.
		if (reason === 'inactivity') {
			this._endedBy = 'timed_out';
		} else if (reason === 'visitor_close') {
			this._endedBy = 'visitor_left';
		}
		this._clearInactivityTimer();
		this._save();
	};

	/**
	 * The visitor followed a link in an AI message.
	 *
	 * Chat links navigate in the same tab, so this IS the end of the
	 * conversation — the page is about to unload. Flush with sendBeacon, which
	 * survives unload where the regular fetch() save would be cancelled.
	 *
	 * Forces a re-save even when one already fired: tab-switch beacon saves are
	 * routine mid-conversation, and without this the click would be dropped and
	 * the record left saying "visitor left". The server updates on re-save
	 * (keyed by session_id) and never re-fires the completed hook, so ended_by
	 * is corrected without a second notification email.
	 */
	SiteStaffrTextChat.prototype.endConversationViaLinkClick = function () {
		if (this._endedBy === 'link_clicked') {
			return;
		}
		this._endedBy = 'link_clicked';
		this._clearInactivityTimer();
		this.saved = false;
		this.ended = false;
		this.saveViaBeacon();
	};

	/**
	 * Save conversation via AJAX (async).
	 */
	SiteStaffrTextChat.prototype._save = function () {
		if (this.saved || !this.transcript.length) {
			return;
		}
		this.saved = true;

		var duration = this.startTime ? Math.round((Date.now() - this.startTime) / 1000) : 0;

		// GTM dataLayer: text conversation ended
		var self = this;
		var contactFields = Object.keys(this.contactMemory).filter(function(k) { return !!self.contactMemory[k]; });
		(window.dataLayer = window.dataLayer || []).push({
			event: 'sitestaffr_conversation_end',
			sitestaffr_mode: 'text',
			sitestaffr_session_id: this.sessionId,
			sitestaffr_duration: duration,
			sitestaffr_has_contact_info: contactFields.length > 0,
			sitestaffr_fields_captured: contactFields,
			sitestaffr_message_count: this.transcript.length,
		});
		var saveAction = this.config.channelSource === 'button'
			? 'sitestaffr_button_save_conversation'
			: 'sitestaffr_save_conversation';

		var formData = new FormData();
		formData.append('action', saveAction);
		formData.append('nonce', this.config.saveNonce);
		formData.append('session_id', this.sessionId);
		formData.append('duration', String(duration));
		formData.append('transcript', JSON.stringify(this.transcript));
		formData.append('realtime_usage', JSON.stringify(this.usageTotals));
		formData.append('channel_source', this.config.channelSource || 'widget');
		formData.append('conversation_mode', 'text');
		formData.append('persona', this.config.persona || 'default');
		formData.append('contact_fields', JSON.stringify(this.contactMemory));
		formData.append('page_url', window.location.href);
		formData.append('page_title', document.title || '');
		if (this._endedBy) {
			formData.append('ended_by', this._endedBy);
		}
		if (this.submissionSource) {
			formData.append('submission_source', this.submissionSource);
		}

		fetch(this.config.ajaxUrl, {
			method: 'POST',
			body: formData,
		}).catch(function (err) {
			console.error('SiteStaffrTextChat: save error', err);
		});
	};

	/**
	 * Save via sendBeacon (tab close).
	 */
	SiteStaffrTextChat.prototype.saveViaBeacon = function () {
		if (this.saved || !this.transcript.length) {
			return;
		}
		this.saved = true;
		this.ended = true;

		var duration = this.startTime ? Math.round((Date.now() - this.startTime) / 1000) : 0;
		var saveAction = this.config.channelSource === 'button'
			? 'sitestaffr_button_save_conversation'
			: 'sitestaffr_save_conversation';

		var params = new URLSearchParams();
		params.append('action', saveAction);
		params.append('nonce', this.config.saveNonce);
		params.append('session_id', this.sessionId);
		params.append('duration', String(duration));
		params.append('transcript', JSON.stringify(this.transcript));
		params.append('realtime_usage', JSON.stringify(this.usageTotals));
		params.append('channel_source', this.config.channelSource || 'widget');
		params.append('conversation_mode', 'text');
		params.append('persona', this.config.persona || 'default');
		params.append('contact_fields', JSON.stringify(this.contactMemory));
		params.append('page_url', window.location.href);
		params.append('page_title', document.title || '');
		// Beacon fires when the visitor leaves the page mid-chat. A reason
		// already set is more specific than "left" — e.g. they followed a link,
		// which unloads the page through this same path.
		params.append('ended_by', this._endedBy || 'visitor_left');
		if (this.submissionSource) {
			params.append('submission_source', this.submissionSource);
		}

		navigator.sendBeacon(this.config.ajaxUrl, params);
	};

	/**
	 * Toggle mute state.
	 * @returns {boolean} New muted state
	 */
	SiteStaffrTextChat.prototype.toggleMute = function () {
		this.muted = !this.muted;
		return this.muted;
	};

	/**
	 * Play a sound if not muted.
	 * @param {HTMLAudioElement|null} sound
	 */
	SiteStaffrTextChat.prototype._playSound = function (sound) {
		if (!sound || this.muted) {
			return;
		}
		sound.currentTime = 0;
		sound.play().catch(function () {
			// Autoplay blocked — ignore
		});
	};

	/**
	 * Reset the inactivity timer.
	 */
	SiteStaffrTextChat.prototype._resetInactivityTimer = function () {
		this._clearInactivityTimer();
		var self = this;
		this._inactivityTimer = setTimeout(function () {
			if (!self.ended) {
				self.endConversation('inactivity');
				self.callbacks.onEndSession('inactivity');
			}
		}, INACTIVITY_TIMEOUT_MS);
	};

	/**
	 * Clear the inactivity timer.
	 */
	SiteStaffrTextChat.prototype._clearInactivityTimer = function () {
		if (this._inactivityTimer) {
			clearTimeout(this._inactivityTimer);
			this._inactivityTimer = null;
		}
	};

	/**
	 * Generate a unique session ID.
	 * @returns {string}
	 */
	SiteStaffrTextChat.prototype._generateSessionId = function () {
		return 'tc_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9);
	};

	// Export
	window.SiteStaffrTextChat = SiteStaffrTextChat;
})();
