Ext.namespace('Zarafa.plugins.ai.data');

/**
 * @class Zarafa.plugins.ai.data.AIResponseHandler
 * @extends Zarafa.core.data.AbstractResponseHandler
 *
 * Response handler for non-streaming AI module requests. Routes the server
 * 'success'/'error' feedback to the supplied callbacks.
 */
Zarafa.plugins.ai.data.AIResponseHandler = Ext.extend(Zarafa.core.data.AbstractResponseHandler, {

	/**
	 * @cfg {Function} successCallback Called with (response) on success.
	 */
	successCallback: undefined,

	/**
	 * @cfg {Function} errorCallback Called with (message, response) on error.
	 */
	errorCallback: undefined,

	/**
	 * @cfg {Object} scope Scope for the callbacks.
	 */
	scope: undefined,

	/**
	 * Handle a successful response.
	 * @param {Object} response The response data.
	 */
	doSuccess: function(response)
	{
		if (Ext.isFunction(this.successCallback)) {
			this.successCallback.call(this.scope || this, response || {});
		}
	},

	/**
	 * Handle an error response.
	 * @param {Object} response The response data.
	 */
	doError: function(response)
	{
		var message = _('An unknown error occurred.');
		if (response && response.info && response.info.display_message) {
			message = response.info.display_message;
		}
		if (Ext.isFunction(this.errorCallback)) {
			this.errorCallback.call(this.scope || this, message, response);
		}
	}
});
Ext.namespace('Zarafa.plugins.ai');

/**
 * @class Zarafa.plugins.ai.AIClient
 * @singleton
 *
 * Central helper for talking to the server-side AI module. Owns the settings
 * lookups (which features/actions are available), the UI-language mapping, and
 * the request payload construction. The API key lives only on the server, so
 * nothing secret passes through here.
 */
Zarafa.plugins.ai.AIClient = {

	/**
	 * @property {String} MODULE The server module name.
	 */
	MODULE: 'pluginaimodule',

	/**
	 * Read a plugin setting.
	 * @param {String} key Key below zarafa/v1/plugins/ai/.
	 * @param {Mixed} defaultValue Returned when unset.
	 * @return {Mixed}
	 */
	getSetting: function(key, defaultValue)
	{
		var value = container.getSettingsModel().get('zarafa/v1/plugins/ai/' + key, true);
		return Ext.isDefined(value) ? value : defaultValue;
	},

	/**
	 * The key-free server info injected at login (provider, model, features).
	 * @return {Object}
	 */
	getServerInfo: function()
	{
		return this.getSetting('server', {}) || {};
	},

	/**
	 * The configured model's display name.
	 * @return {String}
	 */
	getModelName: function()
	{
		return this.getServerInfo().model || '';
	},

	/**
	 * Whether the plugin is enabled for the user and configured on the server.
	 * @return {Boolean}
	 */
	isPluginEnabled: function()
	{
		// Opt-in: a missing 'enable' is treated as disabled. 'configured' reflects
		// the administrator's master switch (PLUGIN_AI_ENABLE) plus a usable
		// provider, so the assistant only appears once the admin has unlocked it.
		return this.getSetting('enable', false) === true && this.getServerInfo().configured === true;
	},

	/**
	 * Whether a feature is usable: enabled by the admin and not turned off by
	 * the user.
	 * @param {String} feature summarize | translate | compose | actions
	 * @return {Boolean}
	 */
	isFeatureEnabled: function(feature)
	{
		if (!this.isPluginEnabled()) {
			return false;
		}
		var serverFeatures = this.getServerInfo().features || {};
		if (serverFeatures[feature] === false) {
			return false;
		}
		return this.getSetting('feature_' + feature, true) !== false;
	},

	/**
	 * Whether a smart action is usable (admin enabled and user enabled).
	 * @param {String} action meeting | task | contact | reply
	 * @return {Boolean}
	 */
	isActionEnabled: function(action)
	{
		if (!this.isFeatureEnabled('actions')) {
			return false;
		}
		var serverActions = this.getServerInfo().actions || {};
		if (serverActions[action] === false) {
			return false;
		}
		return this.getSetting('action_' + action, true) !== false;
	},

	/**
	 * Whether any smart action at all is usable.
	 * @return {Boolean}
	 */
	anyActionEnabled: function()
	{
		return this.isActionEnabled('meeting') || this.isActionEnabled('task') ||
			this.isActionEnabled('contact') || this.isActionEnabled('reply');
	},

	/**
	 * Whether token streaming should be attempted.
	 * @return {Boolean}
	 */
	isStreamingEnabled: function()
	{
		if (this.getServerInfo().streaming === false) {
			return false;
		}
		return this.getSetting('streaming', true) !== false;
	},

	/**
	 * The English name of the user's UI language, or '' if unknown.
	 * @return {String}
	 */
	uiLanguageName: function()
	{
		var lang = container.getSettingsModel().get('zarafa/v1/main/language') || '';
		return this.codeToLanguage(lang);
	},

	/**
	 * The effective translation target: the user's configured target, or their
	 * UI language when set to 'auto'.
	 * @return {String}
	 */
	translateTarget: function()
	{
		var target = this.getSetting('translate_target', 'auto');
		if (!target || target === 'auto') {
			return this.uiLanguageName() || 'English';
		}
		return target;
	},

	/**
	 * Map a locale code (e.g. 'de_DE.UTF-8') to an English language name.
	 * @param {String} code
	 * @return {String}
	 */
	codeToLanguage: function(code)
	{
		if (!code) {
			return '';
		}
		var key = code.toLowerCase().split('.')[0].split('_')[0];
		var map = {
			en: 'English', de: 'German', fr: 'French', es: 'Spanish', it: 'Italian',
			nl: 'Dutch', pt: 'Portuguese', pl: 'Polish', ru: 'Russian', cs: 'Czech',
			sk: 'Slovak', hu: 'Hungarian', tr: 'Turkish', zh: 'Chinese', ja: 'Japanese',
			ko: 'Korean', ar: 'Arabic', uk: 'Ukrainian', sv: 'Swedish', da: 'Danish',
			fi: 'Finnish', nb: 'Norwegian', no: 'Norwegian', el: 'Greek', ro: 'Romanian',
			bg: 'Bulgarian', hr: 'Croatian', sl: 'Slovenian', sr: 'Serbian', et: 'Estonian',
			lv: 'Latvian', lt: 'Lithuanian', he: 'Hebrew', hi: 'Hindi', th: 'Thai',
			vi: 'Vietnamese', id: 'Indonesian', fa: 'Persian', ca: 'Catalan'
		};
		return map[key] || '';
	},

	/**
	 * Convert AI plain-text output into HTML suitable for the rich mail editor.
	 * Unlike HTMLParser.convertPlainToHTML (which wraps text in a <pre>, forcing
	 * a monospace font), this produces normal paragraphs that inherit the
	 * editor's default font. Blank lines become paragraph breaks; single line
	 * breaks become <br>.
	 * @param {String} text
	 * @return {String} HTML
	 */
	toEditorHtml: function(text)
	{
		// Decode any HTML entities the model emitted (e.g. &nbsp;) so they don't
		// show up literally, then escape for safety, then preserve every line
		// break as <br> (keeps the original spacing; no <pre>, so the editor's
		// default font is used).
		var decoded = this.decodeEntities(String(text || ''));
		var escaped = Ext.util.Format.htmlEncode(decoded);
		return escaped.replace(/\r\n?/g, '\n').replace(/\n/g, '<br />');
	},

	/**
	 * Decode HTML entities (named/numeric) in a string to their characters.
	 * Used so model output like "&nbsp;" or "&amp;" is shown as the real
	 * character instead of the literal entity. Safe: a detached <textarea>
	 * decodes entities without executing markup.
	 * @param {String} text
	 * @return {String}
	 */
	decodeEntities: function(text)
	{
		text = String(text || '');
		if (text.indexOf('&') === -1) {
			return text;
		}
		var textarea = document.createElement('textarea');
		textarea.innerHTML = text;
		return textarea.value;
	},

	/**
	 * Build the request payload identifying a message plus feature options.
	 * @param {Zarafa.core.data.IPMRecord} record
	 * @param {Object} opts Extra fields (scope, length, target, language).
	 * @return {Object}
	 */
	payloadFor: function(record, opts)
	{
		return Ext.apply({
			entryid: record.get('entryid'),
			parent_entryid: record.get('parent_entryid'),
			store_entryid: record.get('store_entryid')
		}, opts || {});
	},

	/**
	 * Run a feature against a message, delivering output into the given panel.
	 * @param {String} feature
	 * @param {Zarafa.core.data.IPMRecord} record
	 * @param {Object} opts Request options.
	 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
	 */
	run: function(feature, record, opts, panel)
	{
		var data = this.payloadFor(record, opts);
		if (this.isStreamingEnabled() && typeof window.fetch === 'function' && window.ReadableStream) {
			this.streamRequest(feature, data, panel);
		} else {
			this.moduleRequest(feature, data, panel);
		}
	},

	/**
	 * Run the compose feature on draft text (no stored message involved).
	 * @param {Object} opts operation, text, tone/target
	 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
	 */
	runCompose: function(opts, panel)
	{
		var data = Ext.apply({}, opts);
		if (this.isStreamingEnabled() && typeof window.fetch === 'function' && window.ReadableStream) {
			this.streamRequest('compose', data, panel);
		} else {
			this.moduleRequest('compose', data, panel);
		}
	},

	/**
	 * Request suggested smart actions for a message (always buffered — the
	 * response is structured JSON, not streamed). Renders chips into the window
	 * on success.
	 * @param {Zarafa.core.data.IPMRecord} record
	 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
	 * @param {Zarafa.plugins.ai.ui.AIAssistantWindow} win
	 */
	suggestActions: function(record, panel, win)
	{
		var self = this;
		var data = this.payloadFor(record, { language: this.uiLanguageName() });

		container.getRequest().singleRequest(
			this.MODULE,
			'suggest_actions',
			data,
			new Zarafa.plugins.ai.data.AIResponseHandler({
				successCallback: function(response) {
					var actions = (response && response.actions) || [];
					actions = actions.filter(function(action) {
						return self.isActionEnabled(action.type);
					});
					if (!actions.length) {
						panel.setResult(_('No suggested actions were found for this message.'));
						return;
					}
					panel.setResult(_('Suggested actions — each opens a prefilled dialog you review before saving:'));
					if (win && win.setChips) {
						win.setChips(actions, record);
					}
				},
				errorCallback: function(message) {
					panel.setError(message);
				},
				scope: self
			})
		);
	},

	/**
	 * The relative URL of the streaming endpoint.
	 * @return {String}
	 */
	streamUrl: function()
	{
		return 'plugins/ai/php/stream.php';
	},

	/**
	 * Attempt a streamed request via Server-Sent Events, falling back to the
	 * buffered module path on any transport failure (network error, non-200,
	 * or an empty stream).
	 * @param {String} feature
	 * @param {Object} data Request payload (without feature).
	 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
	 */
	streamRequest: function(feature, data, panel)
	{
		var self = this;
		var state = { gotEvent: false, finalized: false, fellBack: false };

		// Fall back to the buffered path only while nothing has streamed yet.
		var fallback = function() {
			if (state.fellBack || state.gotEvent) {
				return;
			}
			state.fellBack = true;
			self.moduleRequest(feature, data, panel);
		};

		// The stream ended without a 'done'/'error' event — a mid-stream drop.
		// Keep the text already shown, but mark it as possibly incomplete rather
		// than presenting a truncated answer as if it were final.
		var finalize = function() {
			if (!state.finalized) {
				state.finalized = true;
				panel.setResult(panel.getText());
				if (panel.getText()) {
					panel.markInterrupted();
				}
			}
		};

		window.fetch(this.streamUrl(), {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify(Ext.apply({ feature: feature }, data)),
			credentials: 'same-origin'
		}).then(function(response) {
			if (!response.ok || !response.body || !response.body.getReader) {
				fallback();
				return undefined;
			}

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

			var pump = function() {
				return reader.read().then(function(result) {
					if (result.done) {
						if (state.gotEvent) {
							finalize();
						} else {
							fallback();
						}
						return undefined;
					}

					buffer += decoder.decode(result.value, { stream: true });
					var index;
					while ((index = buffer.indexOf('\n\n')) !== -1) {
						self.handleStreamEvent(buffer.substring(0, index), panel, state);
						buffer = buffer.substring(index + 2);
					}
					return pump();
				});
			};

			return pump();
		}).catch(function() {
			// Transport failed: fall back if nothing streamed, else finalize.
			if (state.gotEvent) {
				finalize();
			} else {
				fallback();
			}
		});
	},

	/**
	 * Parse one raw SSE event block and apply it to the panel.
	 * @param {String} raw The text between event boundaries.
	 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
	 * @param {Object} state Streaming state flags.
	 * @private
	 */
	handleStreamEvent: function(raw, panel, state)
	{
		var lines = raw.split('\n');
		var event = 'message';
		var dataStr = '';
		for (var i = 0; i < lines.length; i++) {
			var line = lines[i];
			if (line.indexOf('event:') === 0) {
				event = line.substring(6).trim();
			} else if (line.indexOf('data:') === 0) {
				dataStr += line.substring(5).trim();
			}
		}

		var payload;
		try {
			payload = dataStr ? JSON.parse(dataStr) : {};
		} catch (e) {
			return;
		}

		state.gotEvent = true;
		if (event === 'delta') {
			panel.appendDelta(payload.text || '');
		} else if (event === 'done') {
			panel.setResult(payload.text ? payload.text : panel.getText());
			state.finalized = true;
		} else if (event === 'error') {
			panel.setError(payload.message || _('The AI request failed.'));
			state.finalized = true;
		}
	},

	/**
	 * Perform a buffered (non-streaming) module request.
	 * @param {String} feature
	 * @param {Object} data Request payload.
	 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
	 */
	moduleRequest: function(feature, data, panel)
	{
		container.getRequest().singleRequest(
			this.MODULE,
			feature,
			data,
			new Zarafa.plugins.ai.data.AIResponseHandler({
				successCallback: function(response) {
					panel.setResult(response.text || '');
				},
				errorCallback: function(message) {
					panel.setError(message);
				},
				scope: this
			})
		);
	}
};
Ext.namespace('Zarafa.plugins.ai.ui');

/**
 * @class Zarafa.plugins.ai.ui.AIAssistantPanel
 * @extends Ext.Panel
 * @xtype zarafa.aiassistantpanel
 *
 * The body of the AI Assistant window: a scrollable area that renders the
 * assistant's output. It supports incremental streaming (appendDelta) as well
 * as a single buffered result (setResult), plus loading and error states.
 */
Zarafa.plugins.ai.ui.AIAssistantPanel = Ext.extend(Ext.Panel, {

	/**
	 * @property {String} accumulated text of the current response.
	 * @private
	 */
	text: '',

	/**
	 * @property {String} pending HTML to apply once the panel is rendered.
	 * @private
	 */
	pendingHtml: null,

	constructor: function(config)
	{
		config = config || {};
		Ext.applyIf(config, {
			border: false,
			autoScroll: true,
			cls: 'k-ai-body',
			html: '<div class="k-ai-content"></div><div class="k-ai-chips" style="display:none"></div>'
		});

		this.addEvents(
			/**
			 * @event airesult Fired when a full result is available.
			 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
			 * @param {String} text
			 */
			'airesult',
			/**
			 * @event aierror Fired when the request failed.
			 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
			 * @param {String} message
			 */
			'aierror',
			/**
			 * @event chipclick Fired when a suggested-action chip is clicked.
			 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
			 * @param {Number} index The chip's index.
			 */
			'chipclick'
		);

		Zarafa.plugins.ai.ui.AIAssistantPanel.superclass.constructor.call(this, config);

		this.on('afterrender', this.onAfterRender, this);
	},

	/**
	 * Apply any HTML produced before the panel was rendered and wire chip clicks.
	 * @private
	 */
	onAfterRender: function()
	{
		if (this.pendingHtml !== null) {
			this.writeHtml(this.pendingHtml);
			this.pendingHtml = null;
		}
		this.body.on('click', this.onBodyClick, this);
	},

	/**
	 * Delegate clicks on chips to a 'chipclick' event carrying the chip index.
	 * @param {Ext.EventObject} event
	 * @private
	 */
	onBodyClick: function(event)
	{
		var chip = event.getTarget('.k-ai-chip');
		if (chip) {
			this.fireEvent('chipclick', this, parseInt(chip.getAttribute('data-idx'), 10));
		}
	},

	/**
	 * Render suggested-action chips below the content and reveal them.
	 * @param {String} html
	 */
	setChips: function(html)
	{
		var el = this.rendered && this.body ? this.body.child('.k-ai-chips', true) : null;
		if (el) {
			el.innerHTML = html;
			el.style.display = '';
		}
	},

	/**
	 * Clear and hide the chips area.
	 */
	clearChips: function()
	{
		var el = this.rendered && this.body ? this.body.child('.k-ai-chips', true) : null;
		if (el) {
			el.innerHTML = '';
			el.style.display = 'none';
		}
	},

	/**
	 * Enter the loading state.
	 * @param {String} label Optional status text.
	 */
	startLoading: function(label)
	{
		this.text = '';
		this.writeHtml('<div class="k-ai-status"><span class="k-ai-spinner"></span>' +
			Ext.util.Format.htmlEncode(label || _('Thinking…')) + '</div>');
	},

	/**
	 * Append a streamed text fragment and re-render with a caret.
	 * @param {String} delta
	 */
	appendDelta: function(delta)
	{
		if (!delta) {
			return;
		}
		this.text += delta;
		this.renderText(true);
	},

	/**
	 * Set the full result text and re-render without a caret.
	 * @param {String} text
	 */
	setResult: function(text)
	{
		this.text = text || '';
		this.renderText(false);
		this.fireEvent('airesult', this, this.text);
	},

	/**
	 * Show an error message.
	 * @param {String} message
	 */
	setError: function(message)
	{
		this.writeHtml('<div class="k-ai-error">' + Ext.util.Format.htmlEncode(message) + '</div>');
		this.fireEvent('aierror', this, message);
	},

	/**
	 * Append a notice that the streamed response was cut off and may be
	 * incomplete, without discarding the text already shown.
	 */
	markInterrupted: function()
	{
		var contentEl = this.rendered && this.body ? this.body.child('.k-ai-content', true) : null;
		if (contentEl) {
			contentEl.innerHTML += '<div class="k-ai-note">' +
				Ext.util.Format.htmlEncode(_('The response was interrupted and may be incomplete.')) + '</div>';
		}
	},

	/**
	 * @return {String} The current text.
	 */
	getText: function()
	{
		return this.text;
	},

	/**
	 * Render the accumulated text as HTML, optionally with a streaming caret,
	 * and keep the view scrolled to the bottom.
	 * @param {Boolean} withCaret
	 * @private
	 */
	renderText: function(withCaret)
	{
		var html = Zarafa.plugins.ai.ui.AIAssistantPanel.renderMarkdown(this.text);
		if (withCaret) {
			html += '<span class="k-ai-caret"></span>';
		}
		this.writeHtml(html);

		if (this.rendered && this.body) {
			var dom = this.body.dom;
			dom.scrollTop = dom.scrollHeight;
		}
	},

	/**
	 * Write HTML into the content element, deferring until rendered.
	 * @param {String} html
	 * @private
	 */
	writeHtml: function(html)
	{
		if (!this.rendered || !this.body) {
			this.pendingHtml = html;
			return;
		}
		var contentEl = this.body.child('.k-ai-content', true);
		if (contentEl) {
			contentEl.innerHTML = html;
		}
	}
});

Ext.reg('zarafa.aiassistantpanel', Zarafa.plugins.ai.ui.AIAssistantPanel);

/**
 * Render a safe subset of Markdown (paragraphs, '-'/'*' bullets, #/##/###
 * headings, **bold**, `code`) to HTML. Input is HTML-escaped first, so model
 * output can never inject markup.
 *
 * @param {String} text
 * @return {String} HTML
 * @static
 */
Zarafa.plugins.ai.ui.AIAssistantPanel.renderMarkdown = function(text)
{
	text = String(text || '');

	var inline = function(value) {
		// Decode entities the model emitted (e.g. &nbsp;) to real characters,
		// then re-escape so display is both correct and injection-safe.
		value = Ext.util.Format.htmlEncode(Zarafa.plugins.ai.AIClient.decodeEntities(value));
		value = value.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
		value = value.replace(/`([^`]+)`/g, '<code>$1</code>');
		return value;
	};

	var lines = text.replace(/\r\n?/g, '\n').split('\n');
	var html = [];
	var i = 0;

	while (i < lines.length) {
		var line = lines[i];

		if ((/^\s*$/).test(line)) {
			i++;
			continue;
		}

		var heading = line.match(/^(#{1,6})\s+(.*)$/);
		if (heading) {
			html.push('<h3>' + inline(heading[2]) + '</h3>');
			i++;
			continue;
		}

		if ((/^\s*[-*+]\s+/).test(line)) {
			html.push('<ul>');
			while (i < lines.length && (/^\s*[-*+]\s+/).test(lines[i])) {
				html.push('<li>' + inline(lines[i].replace(/^\s*[-*+]\s+/, '')) + '</li>');
				i++;
			}
			html.push('</ul>');
			continue;
		}

		if ((/^\s*\d+[.)]\s+/).test(line)) {
			html.push('<ol>');
			while (i < lines.length && (/^\s*\d+[.)]\s+/).test(lines[i])) {
				html.push('<li>' + inline(lines[i].replace(/^\s*\d+[.)]\s+/, '')) + '</li>');
				i++;
			}
			html.push('</ol>');
			continue;
		}

		var para = [];
		while (i < lines.length &&
			!(/^\s*$/).test(lines[i]) &&
			!(/^\s*[-*+]\s+/).test(lines[i]) &&
			!(/^\s*\d+[.)]\s+/).test(lines[i]) &&
			!(/^#{1,6}\s+/).test(lines[i])) {
			para.push(inline(lines[i]));
			i++;
		}
		html.push('<p>' + para.join('<br/>') + '</p>');
	}

	return html.join('');
};
Ext.namespace('Zarafa.plugins.ai.ui');

/**
 * @class Zarafa.plugins.ai.ui.AIAssistantWindow
 * @extends Ext.Window
 * @xtype zarafa.aiassistantwindow
 *
 * A floating, resizable card that hosts the {@link Zarafa.plugins.ai.ui.AIAssistantPanel}
 * and a footer toolbar. Reading-side features (summarize, translate, suggested
 * actions) run inside this window so the user keeps their place in the mail.
 *
 * A single instance is reused across invocations via {@link #showFeature}.
 */
Zarafa.plugins.ai.ui.AIAssistantWindow = Ext.extend(Ext.Window, {

	/**
	 * @property {Zarafa.plugins.ai.ui.AIAssistantPanel} the content panel.
	 */
	aiPanel: undefined,

	/**
	 * @property {Function} the current runner; re-invoked by Regenerate.
	 * @private
	 */
	runner: undefined,

	/**
	 * @property {Boolean} whether the window has been positioned once.
	 * @private
	 */
	positioned: false,

	constructor: function(config)
	{
		config = config || {};

		this.aiPanel = new Zarafa.plugins.ai.ui.AIAssistantPanel();

		Ext.applyIf(config, {
			title: _('AI Assistant'),
			cls: 'k-ai-window',
			width: 560,
			height: 460,
			minWidth: 420,
			minHeight: 260,
			closeAction: 'hide',
			constrainHeader: true,
			layout: 'fit',
			items: this.aiPanel,
			buttonAlign: 'left',
			buttons: this.createButtons()
		});

		Zarafa.plugins.ai.ui.AIAssistantWindow.superclass.constructor.call(this, config);

		this.aiPanel.on('chipclick', this.onChipClick, this);
	},

	/**
	 * Build the footer buttons. Insert/Replace are created hidden and shown
	 * only when a feature provides insert/replace handlers (compose).
	 * @return {Array}
	 * @private
	 */
	createButtons: function()
	{
		this.insertBtn = new Ext.Button({
			text: _('Insert at cursor'),
			hidden: true,
			scope: this,
			handler: this.onInsert
		});
		this.replaceBtn = new Ext.Button({
			text: _('Replace draft'),
			iconCls: 'icon_pencil',
			hidden: true,
			scope: this,
			handler: this.onReplace
		});
		this.copyBtn = new Ext.Button({
			text: _('Copy'),
			iconCls: 'icon_copy',
			scope: this,
			handler: this.onCopy
		});
		this.regenerateBtn = new Ext.Button({
			text: _('Regenerate'),
			scope: this,
			handler: this.onRegenerate
		});

		return [this.insertBtn, this.replaceBtn, this.copyBtn, '->', this.regenerateBtn, {
			text: _('Close'),
			scope: this,
			handler: function() {
				this.hide();
			}
		}];
	},

	/**
	 * Start a feature run: show the window, set its title, clear chips, store
	 * the runner for Regenerate, and execute it against the panel.
	 *
	 * @param {Object} options title {String}, model {String}, runner {Function}
	 */
	startFeature: function(options)
	{
		options = options || {};
		this.runner = options.runner;
		this.insertHandler = options.onInsert || null;
		this.replaceHandler = options.onReplace || null;

		if (this.insertBtn) {
			this.insertBtn.setVisible(!!this.insertHandler);
			this.insertBtn.setText(options.insertLabel || _('Insert at cursor'));
			// Optional icon per feature (e.g. a reply icon for "Open as reply").
			if (Ext.isFunction(this.insertBtn.setIconClass)) {
				this.insertBtn.setIconClass(options.insertIconCls || '');
			}
		}
		if (this.replaceBtn) {
			this.replaceBtn.setVisible(!!this.replaceHandler);
			this.replaceBtn.setText(options.replaceLabel || _('Replace draft'));
		}

		var title = options.title || _('AI Assistant');
		if (options.model) {
			title += ' · ' + options.model;
		}
		this.setTitle(Ext.util.Format.htmlEncode(title));

		this.clearChips();

		if (!this.isVisible()) {
			this.show();
		}
		if (!this.positioned) {
			this.center();
			this.positioned = true;
		}
		this.toFront();

		if (Ext.isFunction(this.runner)) {
			this.runner(this.aiPanel, this);
		}
	},

	/**
	 * Copy the current result to the clipboard, with brief button feedback.
	 * @param {Ext.Button} button
	 * @private
	 */
	onCopy: function(button)
	{
		var text = this.aiPanel.getText();
		if (!text) {
			return;
		}

		if (navigator.clipboard && navigator.clipboard.writeText) {
			navigator.clipboard.writeText(text);
		} else {
			var textarea = document.createElement('textarea');
			textarea.value = text;
			document.body.appendChild(textarea);
			textarea.select();
			try {
				document.execCommand('copy');
			} catch (e) {
				// Ignore: clipboard may be unavailable.
			}
			document.body.removeChild(textarea);
		}

		var original = button.text;
		button.setText(_('Copied'));
		(function() {
			if (button && !button.isDestroyed) {
				button.setText(original);
			}
		}).defer(1500);
	},

	/**
	 * Re-run the current feature.
	 * @private
	 */
	onRegenerate: function()
	{
		if (Ext.isFunction(this.runner)) {
			// Clear any chips from the previous run so they cannot be clicked while
			// the regenerate is loading or after it errors (startFeature does this
			// for a fresh run; Regenerate re-invokes the runner directly).
			this.clearChips();
			this.runner(this.aiPanel, this);
		}
	},

	/**
	 * Insert the result into the composer at the cursor and close.
	 * @private
	 */
	onInsert: function()
	{
		var text = this.aiPanel.getText();
		if (text && Ext.isFunction(this.insertHandler)) {
			// A handler may return false to signal it could not apply the text
			// (e.g. the target editor is gone); keep the window open in that case.
			if (this.insertHandler(text) !== false) {
				this.hide();
			}
		}
	},

	/**
	 * Replace the composer draft with the result and close.
	 * @private
	 */
	onReplace: function()
	{
		var text = this.aiPanel.getText();
		if (text && Ext.isFunction(this.replaceHandler)) {
			if (this.replaceHandler(text) !== false) {
				this.hide();
			}
		}
	},

	/**
	 * Render suggested-action chips inside the panel and reveal them.
	 * @param {Array} actions Sanitized action objects.
	 * @param {Zarafa.core.data.IPMRecord} record The source mail record.
	 */
	setChips: function(actions, record)
	{
		this.chipsActions = actions || [];
		this.chipsRecord = record;
		this.aiPanel.setChips(Zarafa.plugins.ai.ui.AIActionChips.renderHtml(this.chipsActions));
	},

	/**
	 * Execute the action behind a clicked chip (opens a confirm-first dialog).
	 * @param {Zarafa.plugins.ai.ui.AIAssistantPanel} panel
	 * @param {Number} index
	 * @private
	 */
	onChipClick: function(panel, index)
	{
		var action = this.chipsActions && this.chipsActions[index];
		if (action) {
			Zarafa.plugins.ai.SmartActions.execute(action, this.chipsRecord);
		}
	},

	/**
	 * Remove all suggested-action chips.
	 */
	clearChips: function()
	{
		this.chipsActions = [];
		this.aiPanel.clearChips();
	}
});

Ext.reg('zarafa.aiassistantwindow', Zarafa.plugins.ai.ui.AIAssistantWindow);

/**
 * Show the shared AI Assistant window and start a feature in it.
 *
 * @param {Object} options title {String}, model {String}, runner {Function}
 * @return {Zarafa.plugins.ai.ui.AIAssistantWindow}
 * @static
 */
Zarafa.plugins.ai.ui.AIAssistantWindow.showFeature = function(options)
{
	var instance = Zarafa.plugins.ai.ui.AIAssistantWindow.instance;
	if (!instance || instance.isDestroyed) {
		instance = new Zarafa.plugins.ai.ui.AIAssistantWindow();
		Zarafa.plugins.ai.ui.AIAssistantWindow.instance = instance;
	}
	instance.startFeature(options);
	return instance;
};
Ext.namespace('Zarafa.plugins.ai.ui');

/**
 * @class Zarafa.plugins.ai.ui.AIComposeMenu
 * @singleton
 *
 * Builds the "AI" split-button for the mail composer toolbar. Its menu offers
 * operations on the current draft text (improve, shorten, expand, change tone,
 * fix grammar, translate). The editor is resolved at click time via a holder
 * closed over by the menus, so submenu items work too.
 */
Zarafa.plugins.ai.ui.AIComposeMenu = {

	/**
	 * Build the composer split-button config.
	 * @param {Zarafa.plugins.ai.AIPlugin} plugin Scope for the click handler.
	 * @return {Object} Button config.
	 */
	createButton: function(plugin)
	{
		var holder = { button: null };
		var getEditor = function() {
			return holder.button ? Zarafa.plugins.ai.ui.AIComposeMenu.findEditorField(holder.button) : null;
		};

		return {
			xtype: 'splitbutton',
			// Icon-only, matching the composer toolbar's other buttons (Save,
			// Delete, attachment, ...) and avoiding any toolbar overflow.
			tooltip: _('AI writing assistant'),
			overflowText: _('AI writing assistant'),
			iconCls: 'icon_ai',
			cls: 'k-ai-toolbar-btn',
			plugins: ['zarafa.recordcomponentupdaterplugin'],
			menu: this.buildMenu(plugin, getEditor),
			handler: function() {
				this.showMenu();
			},
			listeners: {
				afterrender: function(button) {
					holder.button = button;
				}
			},
			update: function(record) {
				this.record = record;
			}
		};
	},

	/**
	 * Build the compose menu and submenus, tagging each with the editor getter.
	 * @param {Zarafa.plugins.ai.AIPlugin} plugin
	 * @param {Function} getEditor
	 * @return {Object} Menu config.
	 */
	buildMenu: function(plugin, getEditor)
	{
		var listeners = { click: plugin.onComposeMenuClick, scope: plugin };

		var tones = [
			['formal', _('Formal')],
			['friendly', _('Friendly')],
			['confident', _('Confident')],
			['concise', _('Concise')],
			['casual', _('Casual')]
		];
		var toneItems = tones.map(function(tone) {
			return { text: tone[1], aiOpts: { operation: 'tone', tone: tone[0] } };
		});

		var languages = ['English', 'German', 'French', 'Spanish', 'Italian', 'Dutch', 'Portuguese', 'Polish', 'Chinese', 'Japanese'];
		var translateItems = [{ text: _('My language'), aiOpts: { operation: 'translate', target: '' } }];
		languages.forEach(function(language) {
			translateItems.push({ text: language, aiOpts: { operation: 'translate', target: language } });
		});

		return {
			xtype: 'menu',
			getEditor: getEditor,
			listeners: listeners,
			items: [{
				text: _('Improve writing'),
				iconCls: 'icon_ai',
				aiOpts: { operation: 'improve' }
			}, {
				text: _('Shorten'),
				aiOpts: { operation: 'shorten' }
			}, {
				text: _('Expand'),
				aiOpts: { operation: 'expand' }
			}, {
				text: _('Change tone'),
				menu: { xtype: 'menu', getEditor: getEditor, listeners: listeners, items: toneItems }
			}, {
				text: _('Fix grammar & spelling'),
				aiOpts: { operation: 'grammar' }
			}, {
				text: _('Translate'),
				menu: { xtype: 'menu', getEditor: getEditor, listeners: listeners, items: translateItems }
			}]
		};
	},

	/**
	 * Walk up from a component to the composer's editor field.
	 * @param {Ext.Component} cmp
	 * @return {Zarafa.common.ui.EditorField|null}
	 */
	findEditorField: function(cmp)
	{
		while (cmp) {
			if (cmp.editorField) {
				return cmp.editorField;
			}
			if (cmp.mainPanel && cmp.mainPanel.editorField) {
				return cmp.mainPanel.editorField;
			}
			cmp = cmp.ownerCt;
		}
		return null;
	}
};
Ext.namespace('Zarafa.plugins.ai.ui');

/**
 * @class Zarafa.plugins.ai.ui.AIActionChips
 * @singleton
 *
 * Renders suggested smart actions as clickable chips. Pure HTML (with an
 * emoji marker per type) so it needs no extra iconset; the hosting window wires
 * a single delegated click handler that maps a chip's data-idx back to its
 * action.
 */
Zarafa.plugins.ai.ui.AIActionChips = {

	/**
	 * Build the chips HTML for a list of actions.
	 * @param {Array} actions
	 * @return {String}
	 */
	renderHtml: function(actions)
	{
		var html = [];
		for (var i = 0; i < actions.length; i++) {
			html.push('<span class="k-ai-chip" data-idx="' + i + '">' +
				Ext.util.Format.htmlEncode(this.label(actions[i])) + '</span>');
		}
		return html.join('');
	},

	/**
	 * A human label for an action chip.
	 * @param {Object} action
	 * @return {String}
	 */
	label: function(action)
	{
		switch (action.type) {
			case 'meeting':
				return '📅 ' + (action.title ? String.format(_('Meeting: {0}'), action.title) : _('Create meeting'));
			case 'task':
				return '✅ ' + (action.title ? String.format(_('Task: {0}'), action.title) : _('Create task'));
			case 'contact':
				return '👤 ' + (action.name ? String.format(_('Add contact: {0}'), action.name) : _('Add contact'));
			case 'reply':
				return '↩️ ' + _('Draft reply');
			default:
				return action.type;
		}
	}
};
Ext.namespace('Zarafa.plugins.ai');

/**
 * @class Zarafa.plugins.ai.SmartActions
 * @singleton
 *
 * Executes a suggested smart action by opening a PREFILLED dialog the user
 * reviews and confirms — nothing is committed without an explicit save. Uses
 * the same record-creation paths the application itself uses.
 */
Zarafa.plugins.ai.SmartActions = {

	/**
	 * Dispatch an action to its handler.
	 * @param {Object} action The sanitized action object from the server.
	 * @param {Zarafa.core.data.IPMRecord} record The source mail record.
	 */
	execute: function(action, record)
	{
		switch (action.type) {
			case 'meeting':
				this.createMeeting(action);
				break;
			case 'task':
				this.createTask(action);
				break;
			case 'contact':
				this.createContact(action);
				break;
			case 'reply':
				this.draftReply(action, record);
				break;
		}
	},

	/**
	 * The default folder record for a hierarchy key (calendar/task/contact).
	 * @param {String} key
	 * @return {Zarafa.hierarchy.data.MAPIFolderRecord|undefined}
	 * @private
	 */
	defaultFolder: function(key)
	{
		var hierarchy = container.getHierarchyStore();
		return hierarchy ? hierarchy.getDefaultFolder(key) : undefined;
	},

	/**
	 * Resolve a default folder, alerting the user instead of silently doing
	 * nothing when it is unavailable (e.g. a delegate-only mailbox).
	 * @param {String} key calendar | task | contact
	 * @param {String} label Human folder label for the message.
	 * @return {Zarafa.hierarchy.data.MAPIFolderRecord|undefined}
	 * @private
	 */
	requireFolder: function(key, label)
	{
		var folder = this.defaultFolder(key);
		if (!folder) {
			Ext.MessageBox.alert(_('AI Assistant'),
				String.format(_('No default {0} folder is available for this account.'), label));
		}
		return folder;
	},

	/**
	 * Open a prefilled meeting request with attendees resolved by name/email.
	 * @param {Object} action
	 */
	createMeeting: function(action)
	{
		var folder = this.requireFolder('calendar', _('calendar'));
		if (!folder) {
			return;
		}

		var times = this.resolveTimes(action);
		var record = Zarafa.core.data.RecordFactory.createRecordObjectByMessageClass('IPM.Appointment', {
			store_entryid: folder.get('store_entryid'),
			parent_entryid: folder.get('entryid'),
			startdate: times.start,
			duedate: times.end,
			commonstart: times.start,
			commonend: times.end,
			duration: times.minutes,
			busystatus: Zarafa.core.mapi.BusyStatus.BUSY
		});

		record.beginEdit();
		if (action.title) {
			record.set('subject', action.title);
		}
		if (action.location) {
			record.set('location', action.location);
		}
		if (action.notes) {
			record.set('body', action.notes);
		}
		record.endEdit();

		if (Ext.isFunction(record.convertToMeeting)) {
			record.convertToMeeting();
		}

		var recipientStore = record.getRecipientStore();
		var added = [];
		Ext.each(action.attendees || [], function(name) {
			if (!name) {
				return;
			}
			var recipient = recipientStore.parseRecipient(name, Zarafa.core.mapi.RecipientType.MAPI_TO);
			if (recipient) {
				recipientStore.add(recipient);
				added.push(recipient);
			}
		});
		if (added.length && Ext.isFunction(recipientStore.resolve)) {
			recipientStore.resolve(added);
		}

		Zarafa.core.data.UIFactory.openCreateRecord(record);
	},

	/**
	 * Open a prefilled task.
	 * @param {Object} action
	 */
	createTask: function(action)
	{
		var folder = this.requireFolder('task', _('tasks'));
		if (!folder) {
			return;
		}

		var data = {
			store_entryid: folder.get('store_entryid'),
			parent_entryid: folder.get('entryid')
		};
		if (action.title) {
			data.subject = action.title;
		}
		if (action.notes) {
			data.body = action.notes;
		}
		var due = this.parseDate(action.due);
		if (due) {
			data.duedate = due;
			data.startdate = due;
		}

		var record = Zarafa.core.data.RecordFactory.createRecordObjectByMessageClass('IPM.Task', data);
		Zarafa.core.data.UIFactory.openCreateRecord(record);
	},

	/**
	 * Open a prefilled contact.
	 * @param {Object} action
	 */
	createContact: function(action)
	{
		var folder = this.requireFolder('contact', _('contacts'));
		if (!folder) {
			return;
		}

		var data = {
			store_entryid: folder.get('store_entryid'),
			parent_entryid: folder.get('entryid')
		};
		if (action.name) {
			data.display_name = action.name;
			data.fileas = action.name;
		}
		if (action.email) {
			data.email_address_1 = action.email;
			// Without an explicit type the saved one-off entryid carries an empty
			// address type, which diverges from a normally-created contact.
			data.email_address_type_1 = 'SMTP';
		}

		var record = Zarafa.core.data.RecordFactory.createRecordObjectByMessageClass('IPM.Contact', data);
		this.applyContactName(record, action.name);

		Zarafa.core.data.UIFactory.openCreateRecord(record);
	},

	/**
	 * Fill a contact's structured name parts from a free-form name, using the
	 * framework's own parser (handles prefixes/suffixes/middle names) and the
	 * "Last, First" form. display_name/fileas already hold the full string, so
	 * the contact stays usable even when the parse is ambiguous.
	 * @param {Zarafa.core.data.IPMRecord} record
	 * @param {String} name
	 * @private
	 */
	applyContactName: function(record, name)
	{
		name = (name || '').trim();
		if (!name) {
			return;
		}
		// Normalize "Surname, Given" into "Given Surname" before parsing.
		var comma = name.indexOf(',');
		if (comma > -1) {
			name = name.substring(comma + 1).trim() + ' ' + name.substring(0, comma).trim();
			name = name.trim();
		}

		var parsed;
		try {
			parsed = new Zarafa.contact.data.ContactDetailsParser().parseInfo('name', name);
		} catch (e) {
			parsed = null;
		}

		record.beginEdit();
		if (parsed) {
			Ext.each(['display_name_prefix', 'given_name', 'middle_name', 'surname', 'generation'], function(field) {
				if (parsed[field]) {
					record.set(field, parsed[field]);
				}
			});
		} else {
			// Fallback: only split when it is unambiguous (exactly two tokens).
			var parts = name.split(/\s+/);
			if (parts.length === 2) {
				record.set('given_name', parts[0]);
				record.set('surname', parts[1]);
			} else {
				record.set('given_name', name);
			}
		}
		record.endEdit();
	},

	/**
	 * Generate a reply with AI, then offer to open it as a prefilled reply.
	 * @param {Object} action carries the reply intent
	 * @param {Zarafa.core.data.IPMRecord} record The message being replied to
	 */
	draftReply: function(action, record)
	{
		if (!record) {
			return;
		}
		var client = Zarafa.plugins.ai.AIClient;
		var self = this;

		Zarafa.plugins.ai.ui.AIAssistantWindow.showFeature({
			title: _('Draft reply'),
			model: client.getModelName(),
			insertLabel: _('Open as reply'),
			insertIconCls: 'icon_reply',
			onInsert: function(text) {
				self.openReplyWithText(record, text);
			},
			runner: function(panel) {
				panel.startLoading(_('Drafting reply…'));
				client.run('draft_reply', record, { intent: action.intent || '', language: client.uiLanguageName() }, panel);
			}
		});
	},

	/**
	 * Open a reply to the message with the generated text placed above the
	 * quoted original.
	 * @param {Zarafa.core.data.IPMRecord} record
	 * @param {String} text
	 */
	openReplyWithText: function(record, text)
	{
		var model = container.getContextByName('mail').getModel();

		var open = function() {
			var response = model.createResponseRecord(record, Zarafa.mail.data.ActionTypes.REPLY);
			if (response.get('isHTML')) {
				// Editor's default font (proportional), not monospace <pre>.
				var html = Zarafa.plugins.ai.AIClient.toEditorHtml(text);
				response.set('html_body', html + (response.get('html_body') || ''));
			} else {
				response.set('body', text + '\n\n' + (response.get('body') || ''));
			}
			Zarafa.core.data.UIFactory.openCreateRecord(response);
		};

		if (record.isOpened()) {
			open();
			return;
		}
		var store = record.getStore();
		if (!store) {
			open();
			return;
		}

		// Opening is async and can fail without ever firing 'open' (server error,
		// record removed). Bind cleanup to 'open' and 'exception', plus a timeout
		// safety net, so the listener is never leaked and the user gets feedback.
		var done = false;
		var timer, onOpen, onException, cleanup, failed;
		cleanup = function() {
			if (done) {
				return;
			}
			done = true;
			if (timer) {
				window.clearTimeout(timer);
			}
			store.un('open', onOpen, this);
			store.un('exception', onException, this);
		};
		failed = function() {
			cleanup.call(this);
			Ext.MessageBox.alert(_('Draft reply'), _('The message could not be opened to draft a reply.'));
		};
		onOpen = function(openedStore, openedRecord) {
			if (openedRecord !== record) {
				return;
			}
			cleanup.call(this);
			open();
		};
		onException = failed;
		timer = failed.defer(20000, this);

		store.on('open', onOpen, this);
		store.on('exception', onException, this);
		record.open();
	},

	/**
	 * Compute meeting start/end from an action's date/time/duration, defaulting
	 * to the next full hour for 30 minutes.
	 * @param {Object} action
	 * @return {Object} { start: Date, end: Date, minutes: Number }
	 * @private
	 */
	resolveTimes: function(action)
	{
		var minutes = action.duration_minutes > 0 ? action.duration_minutes : 30;
		var start = this.parseDate(action.date);
		if (start) {
			// Apply the time onto the valid date. A malformed time falls back to
			// 09:00 but must never throw the date away.
			var hours = 9, mins = 0;
			var match = (/^(\d{1,2}):(\d{2})$/).exec(String(action.time || '').trim());
			if (match) {
				var h = parseInt(match[1], 10), m = parseInt(match[2], 10);
				if (h >= 0 && h <= 23 && m >= 0 && m <= 59) {
					hours = h;
					mins = m;
				}
			}
			start.setHours(hours, mins, 0, 0);
		} else {
			start = this.nextHour();
		}
		return { start: start, end: new Date(start.getTime() + minutes * 60000), minutes: minutes };
	},

	/**
	 * Parse a YYYY-MM-DD string into a Date, or null.
	 * @param {String} value
	 * @return {Date|null}
	 * @private
	 */
	parseDate: function(value)
	{
		if (!value) {
			return null;
		}
		var date = new Date(value + 'T00:00');
		return isNaN(date.getTime()) ? null : date;
	},

	/**
	 * The next full hour from now.
	 * @return {Date}
	 * @private
	 */
	nextHour: function()
	{
		var date = new Date();
		date.setMinutes(0, 0, 0);
		date.setHours(date.getHours() + 1);
		return date;
	}
};
Ext.namespace('Zarafa.plugins.ai.settings');

/**
 * @class Zarafa.plugins.ai.settings.SettingsAICategory
 * @extends Zarafa.settings.ui.SettingsCategory
 * @xtype zarafa.settingsaicategory
 *
 * The settings category for the AI Assistant plugin.
 */
Zarafa.plugins.ai.settings.SettingsAICategory = Ext.extend(Zarafa.settings.ui.SettingsCategory, {

	constructor: function(config)
	{
		config = config || {};
		Ext.applyIf(config, {
			title: _('AI Assistant'),
			categoryIndex: 9950,
			iconCls: 'icon_ai',
			items: [{
				xtype: 'zarafa.settingsaiwidget',
				settingsContext: config.settingsContext
			}]
		});
		Zarafa.plugins.ai.settings.SettingsAICategory.superclass.constructor.call(this, config);
	}
});

Ext.reg('zarafa.settingsaicategory', Zarafa.plugins.ai.settings.SettingsAICategory);
Ext.namespace('Zarafa.plugins.ai.settings');

/**
 * @class Zarafa.plugins.ai.settings.SettingsAIWidget
 * @extends Zarafa.settings.ui.SettingsWidget
 * @xtype zarafa.settingsaiwidget
 *
 * User-facing settings for the AI Assistant. The provider, model and API key
 * are configured centrally by the administrator and are NOT editable here; this
 * widget shows the provider status (with a connection test) and the per-user
 * preferences: language, tone, summary length, streaming, and which enabled
 * features and smart actions to use.
 */
Zarafa.plugins.ai.settings.SettingsAIWidget = Ext.extend(Zarafa.settings.ui.SettingsWidget, {

	/**
	 * @property {Zarafa.settings.SettingsModel} the model being edited.
	 */
	model: undefined,

	constructor: function(config)
	{
		config = config || {};
		Ext.applyIf(config, {
			title: _('AI Assistant'),
			layout: { type: 'fit' },
			items: this.createPanelItems()
		});
		Zarafa.plugins.ai.settings.SettingsAIWidget.superclass.constructor.call(this, config);
	},

	/**
	 * Build the form items.
	 * @return {Array}
	 * @private
	 */
	createPanelItems: function()
	{
		var path = 'zarafa/v1/plugins/ai/';

		var lengthStore = new Ext.data.SimpleStore({
			fields: ['value', 'text'],
			data: [['brief', _('Brief')], ['standard', _('Standard')], ['detailed', _('Detailed')]]
		});

		var toneStore = new Ext.data.SimpleStore({
			fields: ['value', 'text'],
			data: [
				['neutral', _('Neutral')], ['formal', _('Formal')], ['friendly', _('Friendly')],
				['confident', _('Confident')], ['concise', _('Concise')], ['casual', _('Casual')]
			]
		});

		var languageData = [['auto', _('My language (automatic)')]];
		['English', 'German', 'French', 'Spanish', 'Italian', 'Dutch', 'Portuguese', 'Polish', 'Russian', 'Chinese', 'Japanese'].forEach(function(language) {
			languageData.push([language, language]);
		});
		var languageStore = new Ext.data.SimpleStore({ fields: ['value', 'text'], data: languageData });

		var comboDefaults = {
			xtype: 'combo',
			mode: 'local',
			triggerAction: 'all',
			editable: false,
			forceSelection: true,
			valueField: 'value',
			displayField: 'text',
			width: 220,
			listeners: { select: this.onComboSelect, scope: this }
		};

		return [{
			xtype: 'form',
			border: false,
			autoHeight: true,
			labelWidth: 220,
			items: [{
				xtype: 'displayfield',
				hideLabel: true,
				cls: 'k-ai-settings-section',
				value: _('Provider')
			}, {
				xtype: 'displayfield',
				ref: '../providerField',
				htmlEncode: false,
				fieldLabel: _('Configured by administrator')
			}, {
				xtype: 'displayfield',
				ref: '../statusField',
				htmlEncode: false,
				fieldLabel: _('Status'),
				value: '&#160;'
			}, {
				xtype: 'button',
				text: _('Test connection'),
				width: 150,
				hideLabel: true,
				ref: '../testBtn',
				handler: this.onTestConnection,
				scope: this,
				style: 'margin-bottom: 10px;'
			}, {
				xtype: 'displayfield',
				hideLabel: true,
				cls: 'k-ai-settings-section',
				value: _('Preferences')
			}, {
				// The plugin-level on/off lives in Settings > Plugins (the
				// framework's canonical control). Duplicating it here would make
				// this whole page vanish the moment it is switched off, so we only
				// point users to it.
				xtype: 'displayfield',
				hideLabel: true,
				cls: 'k-ai-settings-note',
				value: _('You can turn the AI assistant on or off under Settings → Plugins.')
			}, Ext.apply({
				fieldLabel: _('Summary length'),
				name: path + 'summary_length',
				ref: '../summaryLengthCombo',
				store: lengthStore
			}, comboDefaults), Ext.apply({
				fieldLabel: _('Translate into'),
				name: path + 'translate_target',
				ref: '../translateTargetCombo',
				store: languageStore
			}, comboDefaults), Ext.apply({
				fieldLabel: _('Writing tone'),
				name: path + 'compose_tone',
				ref: '../composeToneCombo',
				store: toneStore
			}, comboDefaults), {
				xtype: 'checkbox',
				boxLabel: _('Stream responses as they are generated'),
				name: path + 'streaming',
				hideLabel: true,
				ref: '../streamingCheck',
				listeners: { check: this.onCheck, scope: this }
			}, {
				xtype: 'displayfield',
				hideLabel: true,
				cls: 'k-ai-settings-section',
				value: _('Features')
			},
			this.featureCheck(_('Summarize emails and threads'), path + 'feature_summarize', 'featureSummarize'),
			this.featureCheck(_('Translate emails'), path + 'feature_translate', 'featureTranslate'),
			this.featureCheck(_('Writing assistant in the composer'), path + 'feature_compose', 'featureCompose'),
			this.featureCheck(_('Suggest smart actions'), path + 'feature_actions', 'featureActions'),
			{
				xtype: 'displayfield',
				hideLabel: true,
				cls: 'k-ai-settings-section',
				value: _('Smart actions')
			}, {
				xtype: 'displayfield',
				hideLabel: true,
				cls: 'k-ai-settings-note',
				value: _('Smart actions always open a prefilled dialog for you to review — nothing is saved or sent automatically.')
			},
			this.featureCheck(_('Create meetings and invite attendees'), path + 'action_meeting', 'actionMeeting'),
			this.featureCheck(_('Create tasks'), path + 'action_task', 'actionTask'),
			this.featureCheck(_('Add contacts'), path + 'action_contact', 'actionContact'),
			this.featureCheck(_('Draft replies'), path + 'action_reply', 'actionReply')]
		}];
	},

	/**
	 * Build a checkbox config for a feature/action toggle.
	 * @param {String} label
	 * @param {String} name Full settings path.
	 * @param {String} ref Reference name.
	 * @return {Object}
	 * @private
	 */
	featureCheck: function(label, name, ref)
	{
		return {
			xtype: 'checkbox',
			boxLabel: label,
			name: name,
			hideLabel: true,
			ref: '../' + ref,
			listeners: { check: this.onCheck, scope: this }
		};
	},

	/**
	 * Load the settings into the fields.
	 * @param {Zarafa.settings.SettingsModel} settingsModel
	 */
	update: function(settingsModel)
	{
		this.model = settingsModel;
		var client = Zarafa.plugins.ai.AIClient;
		var server = client.getServerInfo();

		// Provider status (read-only, no key).
		if (server.configured) {
			this.providerField.setValue(Ext.util.Format.htmlEncode(
				String.format(_('{0} · model {1}'), server.provider || '', server.model || '')));
		} else {
			this.providerField.setValue('<span class="k-ai-status-dot k-ai-bad"></span>' +
				Ext.util.Format.htmlEncode(server.reason || _('Not configured. Ask your administrator to set up a provider.')));
			this.testBtn.setDisabled(true);
		}

		this.streamingCheck.setValue(settingsModel.get(this.streamingCheck.name) !== false);
		this.summaryLengthCombo.setValue(settingsModel.get(this.summaryLengthCombo.name) || 'standard');
		this.translateTargetCombo.setValue(settingsModel.get(this.translateTargetCombo.name) || 'auto');
		this.composeToneCombo.setValue(settingsModel.get(this.composeToneCombo.name) || 'neutral');

		// Feature toggles, gated by the administrator's master switches.
		var features = server.features || {};
		this.applyToggle(this.featureSummarize, settingsModel, features.summarize);
		this.applyToggle(this.featureTranslate, settingsModel, features.translate);
		this.applyToggle(this.featureCompose, settingsModel, features.compose);
		this.applyToggle(this.featureActions, settingsModel, features.actions);

		// Action toggles, gated by the actions master switch and per-action switch.
		var actions = server.actions || {};
		var actionsAllowed = features.actions !== false;
		this.applyToggle(this.actionMeeting, settingsModel, actionsAllowed && actions.meeting !== false);
		this.applyToggle(this.actionTask, settingsModel, actionsAllowed && actions.task !== false);
		this.applyToggle(this.actionContact, settingsModel, actionsAllowed && actions.contact !== false);
		this.applyToggle(this.actionReply, settingsModel, actionsAllowed && actions.reply !== false);
	},

	/**
	 * Set a toggle's value from settings and disable it when the administrator
	 * has switched the capability off.
	 * @param {Ext.form.Checkbox} checkbox
	 * @param {Zarafa.settings.SettingsModel} settingsModel
	 * @param {Boolean} adminAllowed
	 * @private
	 */
	applyToggle: function(checkbox, settingsModel, adminAllowed)
	{
		var allowed = adminAllowed !== false;
		checkbox.setDisabled(!allowed);
		checkbox.setValue(allowed && settingsModel.get(checkbox.name) !== false);
		// Convey admin-disabled state via a tooltip on the rendered element.
		if (checkbox.rendered && checkbox.wrap) {
			checkbox.wrap.dom.title = allowed ? '' : _('Disabled by administrator');
		}
	},

	/**
	 * Persist field values that are not written immediately on change.
	 * @param {Zarafa.settings.SettingsModel} settingsModel
	 */
	updateSettings: function(settingsModel)
	{
		settingsModel.beginEdit();
		settingsModel.set(this.summaryLengthCombo.name, this.summaryLengthCombo.getValue());
		settingsModel.set(this.translateTargetCombo.name, this.translateTargetCombo.getValue());
		settingsModel.set(this.composeToneCombo.name, this.composeToneCombo.getValue());
		settingsModel.endEdit();
	},

	/**
	 * Apply a checkbox change to the model immediately (so the page is dirty).
	 * @param {Ext.form.Checkbox} checkbox
	 * @param {Boolean} checked
	 * @private
	 */
	onCheck: function(checkbox, checked)
	{
		if (this.model && this.model.get(checkbox.name) !== checked) {
			this.model.set(checkbox.name, checked);
		}
	},

	/**
	 * Apply a combo selection to the model immediately.
	 * @param {Ext.form.ComboBox} combo
	 * @param {Ext.data.Record} record
	 * @private
	 */
	onComboSelect: function(combo, record)
	{
		var value = record.get('value');
		if (this.model && this.model.get(combo.name) !== value) {
			this.model.set(combo.name, value);
		}
	},

	/**
	 * Run a connection test against the configured provider.
	 * @private
	 */
	onTestConnection: function()
	{
		this.statusField.setValue('<span class="k-ai-status-dot"></span>' + Ext.util.Format.htmlEncode(_('Testing…')));

		container.getRequest().singleRequest(
			'pluginaimodule',
			'test_connection',
			{},
			new Zarafa.plugins.ai.data.AIResponseHandler({
				successCallback: function(response) {
					this.statusField.setValue('<span class="k-ai-status-dot k-ai-ok"></span>' +
						Ext.util.Format.htmlEncode(String.format(
							_('Connected to {0} ({1}) — {2} ms'),
							response.provider || '', response.model || '', response.latency_ms || 0)));
				},
				errorCallback: function(message) {
					this.statusField.setValue('<span class="k-ai-status-dot k-ai-bad"></span>' +
						Ext.util.Format.htmlEncode(message));
				},
				scope: this
			})
		);
	}
});

Ext.reg('zarafa.settingsaiwidget', Zarafa.plugins.ai.settings.SettingsAIWidget);
Ext.namespace('Zarafa.plugins.ai');

/**
 * @class Zarafa.plugins.ai.AIPlugin
 * @extends Zarafa.core.Plugin
 *
 * The AI Assistant plugin. Adds AI entry points to the mail reading surfaces
 * (preview toolbar, open-mail toolbar, right-click menu) and the settings, and
 * routes each request into the shared {@link Zarafa.plugins.ai.ui.AIAssistantWindow}.
 */
Zarafa.plugins.ai.AIPlugin = Ext.extend(Zarafa.core.Plugin, {

	initPlugin: function()
	{
		Zarafa.plugins.ai.AIPlugin.superclass.initPlugin.apply(this, arguments);

		// Reading pane toolbar (current previewed message).
		this.registerInsertionPoint('previewpanel.toolbar.right', this.createPreviewButton, this);
		// Toolbar of a message opened in its own tab/window.
		this.registerInsertionPoint('context.mail.showmailcontentpanel.toolbar.actions.first', this.createShowMailButton, this);
		// Right-click menu on a message in the list.
		this.registerInsertionPoint('context.mail.contextmenu.actions', this.createContextMenuItems, this);
		// Composer toolbar (writing assistant).
		this.registerInsertionPoint('context.mail.mailcreatecontentpanel.toolbar.actions', this.createComposeButton, this);
		// Settings page.
		this.registerInsertionPoint('context.settings.categories', this.createSettingsCategory, this);
	},

	/**
	 * Build the AI writing-assistant split-button for the composer toolbar.
	 * @return {Object|undefined}
	 */
	createComposeButton: function()
	{
		if (!Zarafa.plugins.ai.AIClient.isFeatureEnabled('compose')) {
			return undefined;
		}
		return Zarafa.plugins.ai.ui.AIComposeMenu.createButton(this);
	},

	/**
	 * Handle a click on a composer AI menu (or submenu) item.
	 * @param {Ext.menu.Menu} menu
	 * @param {Ext.menu.Item} item
	 */
	onComposeMenuClick: function(menu, item)
	{
		if (!item || !item.aiOpts) {
			return;
		}
		var editor = menu.getEditor ? menu.getEditor() : null;
		if (!editor) {
			Ext.MessageBox.alert(_('AI Assistant'), _('The editor could not be found.'));
			return;
		}
		this.runCompose(item.aiOpts, editor);
	},

	/**
	 * Run a compose operation against the editor's current text and present the
	 * result with Insert/Replace actions.
	 * @param {Object} aiOpts operation, tone/target
	 * @param {Zarafa.common.ui.EditorField} editor
	 */
	runCompose: function(aiOpts, editor)
	{
		var client = Zarafa.plugins.ai.AIClient;
		var isHtml = Ext.isFunction(editor.isHtmlEditor) && editor.isHtmlEditor();
		var raw = editor.getValue() || '';
		var text = isHtml ? Zarafa.core.HTMLParser.convertHTMLToPlain(raw) : raw;

		if (!text.replace(/\s+/g, '')) {
			Ext.MessageBox.alert(_('AI Assistant'), _('Write some text in the message first.'));
			return;
		}

		var operation = aiOpts.operation || 'improve';
		var requestOpts = { operation: operation, text: text };
		if (operation === 'tone') {
			requestOpts.tone = aiOpts.tone || client.getSetting('compose_tone', 'neutral');
		} else if (operation === 'translate') {
			requestOpts.target = aiOpts.target ? aiOpts.target : client.translateTarget();
		}

		var titles = {
			improve: _('Improved draft'),
			shorten: _('Shortened draft'),
			expand: _('Expanded draft'),
			grammar: _('Grammar & spelling'),
			tone: _('Rewritten draft'),
			translate: _('Translated draft')
		};

		var toEditor = function(value) {
			// Use the editor's default (proportional) font, not the monospace
			// <pre> that HTMLParser.convertPlainToHTML would produce.
			return isHtml ? client.toEditorHtml(value) : value;
		};

		// The assistant window outlives the composer (it only hides on close), so
		// the captured editor may be gone by the time Insert/Replace is clicked.
		var editorAlive = function() {
			return editor && !editor.isDestroyed;
		};
		var editorGone = function() {
			Ext.MessageBox.alert(_('AI Assistant'), _('The message you were composing is no longer open.'));
		};

		Zarafa.plugins.ai.ui.AIAssistantWindow.showFeature({
			title: titles[operation] || _('AI Assistant'),
			model: client.getModelName(),
			onInsert: function(value) {
				if (!editorAlive()) {
					editorGone();
					return false;
				}
				editor.insertAtCursor(toEditor(value));
			},
			onReplace: function(value) {
				if (!editorAlive()) {
					editorGone();
					return false;
				}
				editor.setValue(toEditor(value));
			},
			runner: function(panel) {
				panel.startLoading();
				client.runCompose(requestOpts, panel);
			}
		});
	},

	/**
	 * Build the AI split-button for the preview toolbar. The current record is
	 * read live from the context model.
	 * @param {String} insertionPoint
	 * @param {Object} options Contains the {Zarafa.core.ContextModel} model.
	 * @return {Object|undefined}
	 */
	createPreviewButton: function(insertionPoint, options)
	{
		if (!Zarafa.plugins.ai.AIClient.isPluginEnabled()) {
			return undefined;
		}
		var model = (options && options.model instanceof Zarafa.core.ContextModel) ? options.model : null;
		return this.createAIButton(model ? function() {
			return model.getPreviewRecord();
		} : null);
	},

	/**
	 * Build the AI split-button for an opened message's toolbar. The record is
	 * delivered through the record-component-updater plugin.
	 * @return {Object|undefined}
	 */
	createShowMailButton: function()
	{
		if (!Zarafa.plugins.ai.AIClient.isPluginEnabled()) {
			return undefined;
		}
		return this.createAIButton(null);
	},

	/**
	 * Build a split-button whose menu offers the reading-side AI features.
	 *
	 * Record acquisition is unified through a holder closed over by the menu:
	 * preview supplies a live getter; the open-mail toolbar pushes the record
	 * via update(). Either source is consulted at click time.
	 *
	 * @param {Function|null} staticRecordFn Optional live record getter.
	 * @return {Object} Button config.
	 */
	createAIButton: function(staticRecordFn)
	{
		var holder = { fn: staticRecordFn || null, record: null };
		var getRecord = function() {
			if (holder.fn) {
				var record = holder.fn();
				if (record) {
					return record;
				}
			}
			return holder.record;
		};

		return {
			xtype: 'splitbutton',
			// Icon-only, matching the other reading-pane toolbar buttons (Delete,
			// More options, Pop-out). A text label here makes the toolbar wider
			// than the pane and can push a horizontal scrollbar onto the app.
			tooltip: _('AI Assistant'),
			overflowText: _('AI Assistant'),
			iconCls: 'icon_ai',
			cls: 'k-ai-toolbar-btn',
			plugins: ['zarafa.recordcomponentupdaterplugin'],
			menu: this.buildAIMenu(getRecord),
			handler: function() {
				this.showMenu();
			},
			update: function(record) {
				this.record = record;
				holder.record = record;
			}
		};
	},

	/**
	 * Build the AI menu (and translation submenu), tagging it with the record
	 * getter so click handlers can resolve the current message.
	 * @param {Function} getRecord
	 * @return {Object} Menu config.
	 */
	buildAIMenu: function(getRecord)
	{
		var client = Zarafa.plugins.ai.AIClient;
		var items = [];

		if (client.isFeatureEnabled('summarize')) {
			items.push({
				text: _('Summarize'),
				iconCls: 'icon_ai',
				feature: 'summarize',
				aiOpts: { scope: 'single' }
			}, {
				text: _('Summarize thread'),
				iconCls: 'icon_ai',
				feature: 'summarize',
				aiOpts: { scope: 'thread' }
			});
		}

		if (client.isFeatureEnabled('translate')) {
			items.push({
				text: _('Translate'),
				iconCls: 'icon_ai',
				menu: {
					xtype: 'menu',
					getRecord: getRecord,
					listeners: { click: this.onAIMenuClick, scope: this },
					items: this.buildTranslateItems()
				}
			});
		}

		if (client.isFeatureEnabled('actions') && client.anyActionEnabled()) {
			if (items.length > 0) {
				items.push({ xtype: 'menuseparator' });
			}
			items.push({
				text: _('Suggest actions'),
				iconCls: 'icon_ai',
				feature: 'suggest_actions'
			});
		}

		if (items.length === 0) {
			items.push({ text: _('No AI features enabled'), disabled: true });
		}

		return {
			xtype: 'menu',
			getRecord: getRecord,
			listeners: { click: this.onAIMenuClick, scope: this },
			items: items
		};
	},

	/**
	 * Build the target-language items for the translate submenu.
	 * @return {Array}
	 */
	buildTranslateItems: function()
	{
		var targets = [
			['', _('My language')],
			['English', _('English')],
			['German', _('German')],
			['French', _('French')],
			['Spanish', _('Spanish')],
			['Italian', _('Italian')],
			['Dutch', _('Dutch')],
			['Portuguese', _('Portuguese')],
			['Polish', _('Polish')],
			['Chinese', _('Chinese')],
			['Japanese', _('Japanese')]
		];

		return targets.map(function(target) {
			return {
				text: target[1],
				feature: 'translate',
				aiOpts: { target: target[0] }
			};
		});
	},

	/**
	 * Handle a click on an AI menu (or submenu) item.
	 * @param {Ext.menu.Menu} menu
	 * @param {Ext.menu.Item} item
	 */
	onAIMenuClick: function(menu, item)
	{
		if (!item || !item.feature) {
			return;
		}
		var record = menu.getRecord ? menu.getRecord() : null;
		if (!record) {
			Ext.MessageBox.alert(_('AI Assistant'), _('Please open or select a message first.'));
			return;
		}
		if (item.feature === 'suggest_actions') {
			this.runSuggestActions(record);
		} else {
			this.runFeature(item.feature, record, item.aiOpts || {});
		}
	},

	/**
	 * Request and present confirm-first smart actions for a message.
	 * @param {Zarafa.core.data.IPMRecord} record
	 */
	runSuggestActions: function(record)
	{
		var client = Zarafa.plugins.ai.AIClient;
		Zarafa.plugins.ai.ui.AIAssistantWindow.showFeature({
			title: _('Suggested actions'),
			model: client.getModelName(),
			runner: function(panel, win) {
				panel.startLoading(_('Looking for actions…'));
				client.suggestActions(record, panel, win);
			}
		});
	},

	/**
	 * Build the right-click context-menu items.
	 * @param {String} insertionPoint
	 * @param {Zarafa.mail.ui.MailGridContextMenu} contextMenu
	 * @return {Array|undefined}
	 */
	createContextMenuItems: function(insertionPoint, contextMenu)
	{
		var client = Zarafa.plugins.ai.AIClient;
		if (!client.isPluginEnabled()) {
			return undefined;
		}

		var getRecord = function() {
			var records = contextMenu ? contextMenu.records : null;
			return (records && records.length) ? records[0] : null;
		};

		var items = [];
		if (client.isFeatureEnabled('summarize')) {
			items.push({
				xtype: 'zarafa.conditionalitem',
				text: _('Summarize with AI'),
				iconCls: 'icon_ai',
				scope: this,
				handler: function() {
					var record = getRecord();
					if (record) {
						this.runFeature('summarize', record, { scope: 'single' });
					}
				}
			});
		}
		if (client.isFeatureEnabled('translate')) {
			items.push({
				xtype: 'zarafa.conditionalitem',
				text: _('Translate with AI'),
				iconCls: 'icon_ai',
				scope: this,
				handler: function() {
					var record = getRecord();
					if (record) {
						this.runFeature('translate', record, {});
					}
				}
			});
		}

		return items.length ? items : undefined;
	},

	/**
	 * Run a reading-side feature for a record in the shared assistant window.
	 * @param {String} feature
	 * @param {Zarafa.core.data.IPMRecord} record
	 * @param {Object} opts
	 */
	runFeature: function(feature, record, opts)
	{
		opts = opts || {};
		var client = Zarafa.plugins.ai.AIClient;
		var requestOpts = {};
		var title;

		if (feature === 'summarize') {
			requestOpts.scope = opts.scope === 'thread' ? 'thread' : 'single';
			requestOpts.length = client.getSetting('summary_length', 'standard');
			var language = client.uiLanguageName();
			if (language) {
				requestOpts.language = language;
			}
			title = requestOpts.scope === 'thread' ? _('Thread summary') : _('Summary');
		} else if (feature === 'translate') {
			requestOpts.target = opts.target ? opts.target : client.translateTarget();
			title = _('Translation') + ' · ' + requestOpts.target;
		} else {
			title = _('AI Assistant');
		}

		Zarafa.plugins.ai.ui.AIAssistantWindow.showFeature({
			title: title,
			model: client.getModelName(),
			runner: function(panel) {
				panel.startLoading();
				client.run(feature, record, requestOpts, panel);
			}
		});
	},

	/**
	 * Create the settings category for the plugin.
	 * @return {Object} Settings category config.
	 */
	createSettingsCategory: function()
	{
		return {
			xtype: 'zarafa.settingsaicategory',
			settingsContext: arguments[2]
		};
	}
});

Zarafa.onReady(function() {
	container.registerPlugin(new Zarafa.core.PluginMetaData({
		name: 'ai',
		displayName: _('AI Assistant'),
		pluginConstructor: Zarafa.plugins.ai.AIPlugin
	}));
});
