/* ==========================================================================
   Niwa Chatbot -- Chat Client
   ---------------------------------------------------------------------------
   Handles message sending, AJAX communication with the backend,
   queue polling for rate-limited requests, and response handling.

   NAMESPACE: window.Niwa.ChatClient
   ========================================================================== */

window.Niwa = window.Niwa || {};

window.Niwa.ChatClient = (function () {
  'use strict';

  var _isSending = false;
  var _initialized = false;
  var _resumeTimer = null;
  var PENDING_REQUEST_MAX_AGE_MS = 30 * 60 * 1000;

  /* -- Lenient JSON parser ------------------------------------------------ */

  function parseJsonLenient(raw) {
    if (raw && typeof raw === 'object') return raw;
    if (typeof raw !== 'string') return null;

    try { return JSON.parse(raw); } catch (e) { /* continue */ }

    var cleaned = raw.replace(/\uFEFF/g, '').replace(/\x1B\[[0-9;]*m/g, '').trim();
    if (!cleaned) return null;

    try { return JSON.parse(cleaned); } catch (e) { /* continue */ }

    var lines = cleaned.split('\n');
    for (var i = 0; i < lines.length; i++) {
      var line = (lines[i] || '').trim();
      if (line.charAt(0) === '{' || line.charAt(0) === '[') {
        var candidate = lines.slice(i).join('\n').trim();
        try { return JSON.parse(candidate); } catch (e) { /* continue */ }
      }
    }

    var firstJson = cleaned.search(/[\{\[]/);
    if (firstJson >= 0) {
      try { return JSON.parse(cleaned.slice(firstJson).trim()); } catch (e) { /* ignore */ }
    }

    return null;
  }

  /* -- Helpers ------------------------------------------------------------- */

  function _renderer() {
    return window.Niwa && window.Niwa.MessageRenderer;
  }

  function _utils() {
    return window.Niwa && window.Niwa.Utils;
  }

  function _newRequestId(session) {
    var suffix = '';
    try {
      if (window.crypto && window.crypto.getRandomValues) {
        var bytes = new Uint32Array(2);
        window.crypto.getRandomValues(bytes);
        suffix = bytes[0].toString(36) + bytes[1].toString(36);
      }
    } catch (e) { /* ignore crypto errors */ }
    if (!suffix) {
      suffix = Math.random().toString(36).slice(2) + Date.now().toString(36);
    }
    var sessionPart = String(session || 'anon').replace(/[^a-zA-Z0-9_-]/g, '').slice(-18);
    return ['front', sessionPart || 'anon', Date.now().toString(36), suffix].join('_').slice(0, 96);
  }

  function _morph() {
    return window.Niwa && window.Niwa.MorphController;
  }


  function _looksLikeHtmlResponse(rawText) {
	    if (typeof rawText !== 'string') return false;
	    return /^\s*</.test(rawText) && /<html|<body|<title|<h1/i.test(rawText);
	  }

  function _looksLikeNonceFailure(httpStatus, rawText) {
    if (httpStatus === 403) return true;
    return typeof rawText === 'string' && rawText.trim() === '-1';
  }

  function _getTextarea() {
    var mc = _morph();
    return mc ? mc.getTextarea() : null;
  }

  function _getSendBtn() {
    var mc = _morph();
    return mc ? mc.getSendBtn() : null;
  }

  function _historyStorageKey() {
    return (_utils() && _utils().STORAGE_KEY) || 'niwa_ai_chat_history_v1';
  }

  function _pendingRequestStorageKey() {
    return _historyStorageKey() + '_pending_request_v1';
  }

  function _unreadStorageKey() {
    return _historyStorageKey() + '_unread_v1';
  }

  function _readPendingRequest() {
    try {
      var raw = localStorage.getItem(_pendingRequestStorageKey());
      if (!raw) return null;
      var pending = JSON.parse(raw);
      if (!pending || typeof pending !== 'object' || !pending.requestId || !pending.session || !pending.message) {
        localStorage.removeItem(_pendingRequestStorageKey());
        return null;
      }
      pending.startedAt = parseInt(pending.startedAt, 10) || Date.now();
      if (Date.now() - pending.startedAt > PENDING_REQUEST_MAX_AGE_MS) {
        localStorage.removeItem(_pendingRequestStorageKey());
        return null;
      }
      return pending;
    } catch (e) {
      return null;
    }
  }

  function _writePendingRequest(pending) {
    if (!pending || !pending.requestId) return;
    try {
      localStorage.setItem(_pendingRequestStorageKey(), JSON.stringify(pending));
    } catch (e) { /* ignore storage errors */ }
  }

  function _updatePendingRequest(requestId, updates) {
    var pending = _readPendingRequest();
    if (!pending || String(pending.requestId) !== String(requestId || '')) return;
    updates = updates || {};
    var keys = Object.keys(updates);
    for (var i = 0; i < keys.length; i++) {
      pending[keys[i]] = updates[keys[i]];
    }
    _writePendingRequest(pending);
  }

  function _clearPendingRequest(requestId) {
    try {
      var pending = _readPendingRequest();
      if (!pending || !requestId || String(pending.requestId) === String(requestId)) {
        localStorage.removeItem(_pendingRequestStorageKey());
      }
    } catch (e) { /* ignore storage errors */ }
  }

  function _readUnreadCount() {
    try {
      return Math.max(0, parseInt(localStorage.getItem(_unreadStorageKey()), 10) || 0);
    } catch (e) {
      return 0;
    }
  }

  function _writeUnreadCount(count) {
    try {
      if (count > 0) {
        localStorage.setItem(_unreadStorageKey(), String(count));
      } else {
        localStorage.removeItem(_unreadStorageKey());
      }
    } catch (e) { /* ignore storage errors */ }
  }

  function _restoreUnreadBadge() {
    var count = _readUnreadCount();
    var morph = _morph();
    if (!morph) return;
    if (count > 0 && !morph.isOpen() && typeof morph.showBadge === 'function') {
      morph.showBadge(count);
    } else if (count === 0 && typeof morph.clearBadge === 'function') {
      morph.clearBadge();
    }
  }

  function _clearUnreadBadge() {
    _writeUnreadCount(0);
    var morph = _morph();
    if (morph && typeof morph.clearBadge === 'function') {
      morph.clearBadge();
    }
  }

  function _markUnreadResponse() {
    var morph = _morph();
    if (morph && morph.isOpen()) return;
    var count = _readUnreadCount() + 1;
    _writeUnreadCount(count);
    if (morph && typeof morph.showBadge === 'function') {
      morph.showBadge(count);
    }
  }

  function _hasRenderedResponse(requestId) {
    var renderer = _renderer();
    if (!requestId || !renderer || typeof renderer.getHistory !== 'function') return false;
    var history = renderer.getHistory() || [];
    for (var i = 0; i < history.length; i++) {
      var entry = history[i];
      if (!entry || (entry.role || 'bot') !== 'bot') continue;
      if (String(entry.requestId || entry.request_id || '') === String(requestId)) return true;
    }
    return false;
  }

  function _buildChatFormData(pending, ajaxNonce) {
    var formData = new FormData();
    formData.append('action', (window.NiwaAI && window.NiwaAI.action) || 'niwa_ai_chat');
    formData.append('_ajax_nonce', ajaxNonce);
    formData.append('nonce', ajaxNonce);
    formData.append('message', pending.message);
    formData.append('session', pending.session);
    formData.append('request_id', pending.requestId);
    formData.append('page_url', pending.pageUrl || window.location.href);

    if (pending.openingMessage) {
      formData.append('opening_message', pending.openingMessage);
    }
    if (pending.pendingCart) {
      formData.append('pending_cart', JSON.stringify(pending.pendingCart));
    }
    if (pending.visitorEmail) {
      formData.append('visitor_email', pending.visitorEmail);
    }
    return formData;
  }

  function _schedulePendingResume(delay) {
    if (_resumeTimer) clearTimeout(_resumeTimer);
    _resumeTimer = setTimeout(function () {
      _resumeTimer = null;
      resumePendingRequest();
    }, Math.max(250, delay || 2500));
  }

  function _deferPendingRetry(requestId, delay, resetQueue) {
    var pending = _readPendingRequest();
    var retryCount = pending && String(pending.requestId) === String(requestId || '')
      ? Math.max(0, parseInt(pending.retryCount, 10) || 0) + 1
      : 1;
    var updates = { retryCount: retryCount };
    if (resetQueue) updates.queueToken = '';
    _updatePendingRequest(requestId, updates);
    _isSending = false;
    var renderer = _renderer();
    if (renderer) renderer.showTyping();
    var backoff = delay || Math.min(30000, 1500 * Math.pow(2, Math.min(retryCount - 1, 4)));
    _schedulePendingResume(backoff);
  }

  function resumePendingRequest() {
    if (_isSending) return false;
    var pending = _readPendingRequest();
    if (!pending) return false;

    var utils = _utils();
    var currentSession = utils && typeof utils.getSession === 'function' ? utils.getSession() : '';
    if (!currentSession || String(currentSession) !== String(pending.session)) {
      _clearPendingRequest(pending.requestId);
      return false;
    }
    if (_hasRenderedResponse(pending.requestId)) {
      _clearPendingRequest(pending.requestId);
      return false;
    }

    var ajaxUrl = window.NiwaAI && window.NiwaAI.ajax;
    var ajaxNonce = window.NiwaAI && window.NiwaAI.nonce;
    if (!ajaxUrl || !ajaxNonce) return false;

    _isSending = true;
    var renderer = _renderer();
    if (renderer) renderer.showTyping();

    if (pending.queueToken) {
      pollQueue(pending.queueToken, pending.startedAt, pending.requestId, pending.session);
      return true;
    }

    // Re-submit with the original idempotency key. If the first page already
    // reached PHP, the backend returns its durable processing/done state; if
    // navigation aborted before delivery, this safely starts the same turn.
    _sendChatFetch(ajaxUrl, _buildChatFormData(pending, ajaxNonce), false);
    return true;
  }

  function init() {
    if (_initialized) return;
    _initialized = true;
    if (window.addEventListener) {
      window.addEventListener('niwa:morph:open', _clearUnreadBadge);
      window.addEventListener('storage', function (event) {
        if (!event || event.key === _unreadStorageKey()) {
          _restoreUnreadBadge();
        }
      });
    }
    _restoreUnreadBadge();
    resumePendingRequest();
  }

  function _pendingCartStorageKey() {
    return _historyStorageKey() + '_pending_cart';
  }

  function _readPendingCartSelection() {
    try {
      var raw = sessionStorage.getItem(_pendingCartStorageKey());
      if (!raw) return null;
      var parsed = JSON.parse(raw);
      if (!parsed || typeof parsed !== 'object') return null;
      if (!parsed.product_id && !parsed.product_query && !parsed.product_name) return null;
      return parsed;
    } catch (e) {
      return null;
    }
  }

  function _writePendingCartSelection(cart) {
    try {
      if (!cart || typeof cart !== 'object') {
        sessionStorage.removeItem(_pendingCartStorageKey());
        return;
      }
      if (cart.success && cart.status === 'added') {
        sessionStorage.removeItem(_pendingCartStorageKey());
        return;
      }
      if (cart.status !== 'needs_selection') return;

      sessionStorage.setItem(_pendingCartStorageKey(), JSON.stringify({
        product_id: cart.product_id || 0,
        product_query: cart.product_query || '',
        product_name: cart.product_name || '',
        selected: cart.selected || {},
        required_options: cart.required_options || [],
        all_options: cart.all_options || cart.required_options || [],
        updated_at: Date.now()
      }));
    } catch (e) { /* ignore storage errors */ }
  }

  var AGENT_NAVIGATION_DELAY_MS = 1200;

  function _scheduleAgentNavigation(navigation) {
    if (!navigation || typeof navigation !== 'object') return false;
    var url = typeof navigation.url === 'string' ? navigation.url.trim() : '';
    if (!url) return false;

    // Same-origin guard: never leave the store domain via agent navigation.
    var probe = document.createElement('a');
    probe.href = url;
    if (probe.protocol !== 'http:' && probe.protocol !== 'https:') return false;
    if (probe.host !== window.location.host) return false;

    var utils = _utils();
    if (utils && typeof utils.trackFrontendEvent === 'function') {
      utils.trackFrontendEvent('agent_navigation', {
        source: 'niwa_agent',
        meta: {
          url: probe.href,
          target: navigation.target || ''
        }
      });
    }

    // Short delay so the visitor sees the agent's confirmation message first.
    setTimeout(function () {
      window.location.assign(probe.href);
    }, AGENT_NAVIGATION_DELAY_MS);
    return true;
  }

  function _readCbRuntimeState() {
    try {
      var raw = localStorage.getItem(_historyStorageKey() + '_cb_runtime');
      if (!raw) return {};
      var parsed = JSON.parse(raw);
      return parsed && typeof parsed === 'object' ? parsed : {};
    } catch (e) {
      return {};
    }
  }

  function _getBlockingCbState() {
    var frontend = window.NiwaAIFrontend || null;
    if (frontend && frontend.cb) {
      if (typeof frontend.cb.getBlockingFlowState === 'function') {
        return frontend.cb.getBlockingFlowState() || '';
      }
      if (typeof frontend.cb.hasBlockingFlow === 'function' && frontend.cb.hasBlockingFlow()) {
        return 'awaiting_confirm';
      }
    }

    try {
      var flowState = sessionStorage.getItem('niwa_ai_cb_state') || '';
      if (flowState === 'resolving_confirm') flowState = 'awaiting_confirm';
      if (flowState === 'awaiting_confirm' || flowState === 'awaiting_email' || flowState === 'creating_coupon') {
        return flowState;
      }
      if (sessionStorage.getItem('niwa_ai_cb_confirm_pending') === '1') {
        return 'awaiting_confirm';
      }
    } catch (e) { /* ignore */ }

    var runtime = _readCbRuntimeState();
    var runtimeState = runtime.state || runtime.flowState || '';
    if (runtimeState === 'resolving_confirm') runtimeState = 'awaiting_confirm';
    if (runtimeState === 'awaiting_confirm' || runtimeState === 'awaiting_email' || runtimeState === 'creating_coupon') {
      return runtimeState;
    }
    if (runtime.confirmPending) {
      return 'awaiting_confirm';
    }

    return '';
  }

  function _getOpeningContextMessage() {
    var renderer = _renderer();
    if (!renderer || typeof renderer.getHistory !== 'function') return '';

    var history = renderer.getHistory() || [];
    if (!Array.isArray(history) || !history.length) return '';

    for (var i = 0; i < history.length; i++) {
      var entry = history[i];
      if (!entry || entry.type === 'suggestions') continue;
      var role = entry.role || 'bot';
      var source = String(entry.source || '');
      if (role !== 'bot' || source !== 'greeting') continue;

      var content = entry.content || entry.text || '';
      content = String(content || '').trim();
      if (content) return content;
    }

    return '';
  }

  function _tryHandleConversionBusterReply(text) {
    var blockingState = _getBlockingCbState();
    var frontend = window.NiwaAIFrontend || null;
    if (!(frontend && frontend.cb && typeof frontend.cb.handleUserReply === 'function')) {
      return false;
    }

    if (blockingState) {
      try {
        sessionStorage.setItem('niwa_ai_cb_state', blockingState);
        if (blockingState === 'awaiting_confirm') {
          sessionStorage.setItem('niwa_ai_cb_confirm_pending', '1');
        }
      } catch (e) { /* ignore */ }
    }

    if (!frontend.cb.handleUserReply(text, { forcedFlowState: blockingState || '' })) {
      return false;
    }

    var textarea = _getTextarea();
    var sendBtn = _getSendBtn();
    if (textarea) {
      textarea.value = '';
      if (window.Niwa && window.Niwa.MorphController && window.Niwa.MorphController.syncTextareaLayout) {
        window.Niwa.MorphController.syncTextareaLayout();
      } else {
        textarea.style.height = 'auto';
      }
    }

    if (sendBtn) {
      var disabledCls = sendBtn.classList.contains('niwa-center__send')
        ? 'niwa-center__send--disabled'
        : 'niwa-morph__send--disabled';
      sendBtn.classList.add(disabledCls);
    }

    return true;
  }

  /* -- Send Message ------------------------------------------------------- */

  /**
   * Send a message to the backend.
   * @param {string} text - Message text to send
   */
  function sendMessage(text) {
    if (!text || !text.trim()) return;
    if (_isSending) return false;
    text = text.trim();

    if (_tryHandleConversionBusterReply(text)) return;

    var renderer = _renderer();
    var utils = _utils();
    if (!renderer) return;

    var session = utils ? utils.getSession() : '';
    var requestId = _newRequestId(session);
    _isSending = true;

    // 1. Add user message
    renderer.addMessage(text, 'user');
    if (typeof renderer.saveHistory === 'function') {
      // The normal history writer is debounced. Persist immediately so a
      // navigation right after Send cannot leave the pending turn without its
      // visible user message on the destination page.
      renderer.saveHistory();
    }

    // 2. Clear textarea and update send button state
    var textarea = _getTextarea();
    var sendBtn = _getSendBtn();
    if (textarea) {
      textarea.value = '';
      if (window.Niwa && window.Niwa.MorphController && window.Niwa.MorphController.syncTextareaLayout) {
        window.Niwa.MorphController.syncTextareaLayout();
      } else {
        textarea.style.height = 'auto';
      }
    }
    if (sendBtn) {
      // Detect the correct disabled class
      var disabledCls = sendBtn.classList.contains('niwa-center__send')
        ? 'niwa-center__send--disabled'
        : 'niwa-morph__send--disabled';
      sendBtn.classList.add(disabledCls);
    }

    // 3. Clear old suggestions
    renderer.clearSuggestions();

	    // 4. Show typing indicator
	    renderer.showTyping();

    // 5. POST to backend
    var ajaxUrl = window.NiwaAI && window.NiwaAI.ajax;
    var ajaxNonce = window.NiwaAI && window.NiwaAI.nonce;

    if (!ajaxUrl || !ajaxNonce) {
      renderer.hideTyping();
      renderer.addMessage('Assistant configuration is incomplete.', 'bot');
      _isSending = false;
      return;
    }

    if (utils && typeof utils.trackFrontendEvent === 'function') {
      utils.trackFrontendEvent('chat_message_sent', {
        source: 'chat_input',
        session: session,
        meta: {
          message_length: text.length
        }
      });
    }

    var openingContextMessage = _getOpeningContextMessage();
    var pendingCart = _readPendingCartSelection();
    var visitorEmail = '';

    // Forward the validated visitor email (if captured earlier in this session
    // by email capture / CBEH / lead form) so the backend can surface it to
    // the agent and reuse it for tools like submit_missing_product_email
    // without asking the visitor again.
    try {
      visitorEmail = sessionStorage.getItem('niwa_validated_email') || '';
    } catch (e) { /* ignore storage errors */ }

    var pendingRequest = {
      requestId: requestId,
      session: session,
      message: text,
      startedAt: Date.now(),
      pageUrl: window.location.href,
      openingMessage: openingContextMessage || '',
      pendingCart: pendingCart || null,
      visitorEmail: String(visitorEmail || ''),
      queueToken: ''
    };
    _writePendingRequest(pendingRequest);

    var formData = _buildChatFormData(pendingRequest, ajaxNonce);

    // Bez klijentskog "request took too long" timera — turn-ovi mogu da traju
    // i 20+ minuta na coding zadacima i taj timer je samo prikazivao gresku
    // bez stvarnog aborta fetch-a. Typing ostaje aktivan dok fetch ne resolve-uje
    // ili reject-uje; ako mreza/server stvarno padne, .catch() ce sakriti typing.
    _sendChatFetch(ajaxUrl, formData, /*retriedWithFreshNonce*/ false);
    return true;
  }

  /**
   * Sredisnji fetch + recovery flow za chat send.
   *
   * Kad server vrati HTML umesto JSON, osvezi nonce i ponovi isti idempotentni
   * zahtev. Prolazni transport/model kvarovi ostaju u durable pending stanju;
   * nikada ih ne pretvaraj u laznu bot poruku ili prinudni reload stranice.
   *
   * @param {string} ajaxUrl
   * @param {FormData} formData
   * @param {boolean} retriedWithFreshNonce
   */
  function _completeRequest(response, requestId) {
    var renderer = _renderer();
    if (renderer) renderer.hideTyping();
    _clearPendingRequest(requestId);
    _isSending = false;

    // A restored page can finish after another tab already persisted the same
    // response. Correlate by request id so history and unread count stay exact.
    if (_hasRenderedResponse(requestId)) return;
    _handleResponse(response, requestId);
    if (renderer && typeof renderer.saveHistory === 'function') {
      renderer.saveHistory();
    }
  }

  function _sendChatFetch(ajaxUrl, formData, retriedWithFreshNonce) {
    var renderer = _renderer();
    var requestId = '';
    try { requestId = formData.get('request_id') || ''; } catch (e) { /* ignore */ }
    fetch(ajaxUrl, {
      method: 'POST',
      body: formData,
      credentials: 'same-origin',
      keepalive: true
    })
      .then(function (response) {
        return response.text().then(function (rawText) {
          return {
            rawText: rawText,
            status: response.status
          };
        });
      })
      .then(function (result) {
        var rawText = result.rawText;
        if (_looksLikeNonceFailure(result.status, rawText) && !retriedWithFreshNonce) {
          _recoverWithFreshNonce(ajaxUrl, formData, false);
          return;
        }

        var response = parseJsonLenient(rawText);
        if (!response) {
          // HTML upstream (nonce expired / WAF / login redirect / PHP fatal).
          if (_looksLikeHtmlResponse(rawText)) {
            console.error('[Niwa Chat] Non-JSON upstream response received.', rawText.slice(0, 1200));
            _handleHtmlUpstream(ajaxUrl, formData, retriedWithFreshNonce);
            return;
          }

          console.error('[Niwa Chat] Invalid upstream response received.', rawText.slice(0, 1200));
          _deferPendingRetry(requestId);
          return;
        }

        if (!response.success && response.data && response.data.retryable) {
          _deferPendingRetry(requestId, response.data.retry_after_ms || 0, true);
          return;
        }

        // Check for queue response
        var payload = response.success && response.data ? response.data : null;
        if (payload && payload.queued && payload.queue_token) {
          requestId = payload.request_id || requestId;
          var session = '';
          try {
            requestId = requestId || formData.get('request_id') || '';
            session = formData.get('session') || '';
          } catch (e) { /* ignore FormData read errors */ }
          var pending = _readPendingRequest();
          var startedAt = pending && String(pending.requestId) === String(requestId) ? pending.startedAt : Date.now();
          var clientSession = (pending && pending.session) || session;
          _updatePendingRequest(requestId, { queueToken: payload.queue_token });
          pollQueue(payload.queue_token, startedAt, requestId, clientSession);
          return;
        }

        // Normal response
        _completeRequest(response, requestId);
      })
      .catch(function (err) {
        // Preserve the durable pending record and retry with the same
        // idempotency key. This is safe whether the request was dropped before
        // PHP or only its response was lost.
        _deferPendingRetry(requestId, 2500);
      });
  }

  /**
   * Recovery flow za HTML upstream odgovor.
   *
   * @param {string} ajaxUrl
   * @param {FormData} originalFormData
   * @param {boolean} alreadyRetried Sprecava beskonacnu petlju.
   */
  function _handleHtmlUpstream(ajaxUrl, originalFormData, alreadyRetried) {
    _recoverWithFreshNonce(ajaxUrl, originalFormData, alreadyRetried);
  }

  function _recoverWithFreshNonce(ajaxUrl, originalFormData, alreadyRetried) {
    if (alreadyRetried) {
      var requestId = '';
      try { requestId = originalFormData.get('request_id') || ''; } catch (e) { /* ignore */ }
      _deferPendingRetry(requestId, 2500);
      return;
    }

    // Prvo automatski refresh nonce-a, pa retry istog request-a.
    _refreshNonceFromServer(function (freshNonce) {
      if (!freshNonce) {
        var requestId = '';
        try { requestId = originalFormData.get('request_id') || ''; } catch (e) { /* ignore */ }
        _deferPendingRetry(requestId, 2500);
        return;
      }

      // Update global + replace nonce u formData i retry.
      if (window.NiwaAI) window.NiwaAI.nonce = freshNonce;
      try {
        originalFormData.set('_ajax_nonce', freshNonce);
        if (originalFormData.has('nonce')) {
          originalFormData.set('nonce', freshNonce);
        }
      } catch (e) {
        // FormData.set nije podrzan u nekim starim browserima — fall back na rebuild.
        try {
          originalFormData.delete('_ajax_nonce');
          originalFormData.append('_ajax_nonce', freshNonce);
          originalFormData.delete('nonce');
          originalFormData.append('nonce', freshNonce);
        } catch (e2) { /* ignore */ }
      }

      _sendChatFetch(ajaxUrl, originalFormData, /*retriedWithFreshNonce*/ true);
    });
  }

  /**
   * Pita backend za svez `niwa_ai_chat_nonce`. Koristi se i u reactive recovery
   * i u periodicnom heartbeat-u.
   *
   * @param {function(string|null)} cb
   */
  function _refreshNonceFromServer(cb) {
    var ajaxUrl = window.NiwaAI && window.NiwaAI.ajax;
    if (!ajaxUrl) { cb(null); return; }

    var actions = [];
    actions.push((window.NiwaAI && window.NiwaAI.nonce_refresh_action) || 'niwa_ai_refresh_chat_nonce');
    actions.push('niwa_ai_refresh_nonce');

    function tryAction(index) {
      if (index >= actions.length) {
        cb(null);
        return;
      }

      var fd = new FormData();
      fd.append('action', actions[index]);
      // Ne saljemo nonce — endpoint postoji da oporavi stale nonce iz kesiranog HTML-a.

      fetch(ajaxUrl, {
        method: 'POST',
        body: fd,
        credentials: 'same-origin'
      })
        .then(function (r) { return r.text(); })
        .then(function (raw) {
          var json = parseJsonLenient(raw);
          if (json && json.success && json.data && typeof json.data.nonce === 'string' && json.data.nonce.length >= 10) {
            cb(json.data.nonce);
          } else {
            tryAction(index + 1);
          }
        })
        .catch(function () { tryAction(index + 1); });
    }

    tryAction(0);
  }

  /* -- Poll Queue --------------------------------------------------------- */

  function pollQueue(queueToken, startedAt, requestId, session) {
    if (!queueToken && requestId) {
      // The durable request-status record is keyed by request id. A non-empty
      // placeholder lets the same status endpoint recover a direct request
      // which never had a real queue job.
      queueToken = 'request_' + requestId;
    }
    if (!queueToken) {
      var renderer = _renderer();
      if (renderer) {
        renderer.hideTyping();
        renderer.addMessage('Request missing queue token.', 'bot');
      }
      _isSending = false;
      return;
    }

    var ajaxUrl = window.NiwaAI && window.NiwaAI.ajax;
    var ajaxNonce = window.NiwaAI && window.NiwaAI.nonce;
    if (!ajaxUrl || !ajaxNonce) {
      var r = _renderer();
      if (r) {
        r.hideTyping();
        r.addMessage('Assistant configuration is incomplete.', 'bot');
      }
      _isSending = false;
      return;
    }

    var beganAt = startedAt || Date.now();
    var pollCount = 0;
    _updatePendingRequest(requestId, { queueToken: queueToken });

    function timedOut() {
      return Date.now() - beganAt > PENDING_REQUEST_MAX_AGE_MS;
    }

    function deferTimedOutPoll() {
      _deferPendingRetry(requestId, 2500, true);
    }

    function tick() {
      pollCount++;
      var formData = new FormData();
      formData.append('action', 'niwa_ai_chat_queue_status');
      formData.append('nonce', ajaxNonce);
      formData.append('queue_token', queueToken);
      if (requestId) {
        formData.append('request_id', requestId);
      }
      if (session) {
        formData.append('session', session);
      }

      fetch(ajaxUrl, { method: 'POST', body: formData })
		.then(function (resp) {
			return resp.text().then(function (rawText) {
				return { status: resp.status, rawText: rawText };
			});
		})
		.then(function (result) {
			var rawText = result.rawText;
          var renderer = _renderer();
			if (_looksLikeNonceFailure(result.status, rawText)) {
				_refreshNonceFromServer(function (freshNonce) {
					if (!freshNonce) {
						_deferPendingRetry(requestId, 2500, false);
						return;
					}
					ajaxNonce = freshNonce;
					if (window.NiwaAI) window.NiwaAI.nonce = freshNonce;
					setTimeout(tick, 250);
				});
				return;
			}
          var response = parseJsonLenient(rawText);

          if (!response) {
            if (timedOut()) {
              deferTimedOutPoll();
              return;
            }
            var delay = pollCount <= 3 ? 500 : (pollCount <= 6 ? 1000 : 2500);
            setTimeout(tick, delay);
            return;
          }

          var data = response.data || {};

          if (response.success && data.done && data.payload) {
            _completeRequest({ success: true, data: data.payload }, requestId);
            return;
          }

          if (response.success && data.queued) {
            if (data.queue_token && data.queue_token !== queueToken) {
              queueToken = data.queue_token;
              _updatePendingRequest(requestId, { queueToken: queueToken });
            }
            if (timedOut()) {
              deferTimedOutPoll();
              return;
            }
            var delayQueued = pollCount <= 3 ? 500 : (pollCount <= 6 ? 1000 : 2500);
            setTimeout(tick, delayQueued);
            return;
          }

          if (!response.success && data.retryable) {
            _deferPendingRetry(requestId, data.retry_after_ms || 0, true);
            return;
          }
          _completeRequest(response, requestId);
        })
        .catch(function () {
          if (timedOut()) {
            deferTimedOutPoll();
            return;
          }
          var delayCatch = pollCount <= 3 ? 500 : (pollCount <= 6 ? 1000 : 2500);
          setTimeout(tick, delayCatch);
        });
    }

    tick();
  }

  /* -- Handle Response ---------------------------------------------------- */

	  function _handleResponse(res, requestId) {
	    var renderer = _renderer();
	    var utils = _utils();
	    if (!renderer) return;
	    var chatWasClosed = !(_morph() && _morph().isOpen());

    // Normalize payload
    var payload = res;
    if (res && typeof res === 'object' && typeof res.success !== 'undefined') {
      if (res.success && res.data && typeof res.data === 'object') {
        payload = res.data;
      } else if (!res.success) {
        if (res.data && typeof res.data === 'object') {
          payload = res.data;
        } else if (typeof res.data === 'string' && res.data.trim()) {
          payload = { message: res.data };
        } else {
          payload = {};
        }
      }
    }

    // Extract error message fallback
    if (payload && !payload.message && utils) {
      var extracted = utils.extractAjaxErrorMessage(res, '');
      if (extracted) payload.message = extracted;
    }

    // Map new backend format
    if (payload) {
      if (payload.answer && !payload.message) payload.message = payload.answer;
      if (payload.cards && Array.isArray(payload.cards) && !payload.products) {
        payload.products = payload.cards.map(function (c) {
          return utils ? utils.normalizeProduct(c) : c;
        });
      }
      if (!payload.suggestions) payload.suggestions = null;
    }

    // Strip NO_SEARCH_NEEDED signal
    var suppressedSignal = false;
    if (payload && payload.message) {
      var cleaned = payload.message.replace(/\[?\bNO_SEARCH_NEEDED\b\]?/gi, '').trim();
      if (cleaned !== payload.message.trim()) {
        payload.message = cleaned || null;
        if (!payload.message) suppressedSignal = true;
      }
    }

    // Mirror text history before rendering frontend-only blocks so products/suggestions
    // can append themselves and persist in the same storage payload.
	    if (payload && Array.isArray(payload.history) && payload.history.length) {

	      _mirrorServerHistory(payload.history);
	    }

    var hasContent = false;
    var responseAnchorEl = null;

    // Show message
	    if (payload && payload.message) {
	      responseAnchorEl = renderer.addMessage(payload.message, 'bot', { requestId: requestId || '' });
	      hasContent = true;
	    } else if (payload && payload.reply) {
	      responseAnchorEl = renderer.addMessage(payload.reply, 'bot', { requestId: requestId || '' });
	      hasContent = true;
	    }

    // Show products
    if (payload && payload.products && payload.products.length) {
      if (window.Niwa && window.Niwa.ProductManager) {
        window.Niwa.ProductManager.renderProducts(payload.products, false, {
          anchorEl: responseAnchorEl
        });
      }
      hasContent = true;
    }

    // Show suggestions
    if (payload && payload.suggestions && !(payload.products && payload.products.length)) {
      renderer.renderSuggestions(payload.suggestions, payload.suggestions_codes, false, {
        anchorEl: responseAnchorEl
      });
      hasContent = true;
    }

    // Apply cart updates returned by the frontend agent cart tool.
    if (payload && payload.cart && payload.cart.success && payload.cart.status === 'added') {
      if (window.Niwa && window.Niwa.ProductManager && typeof window.Niwa.ProductManager.applyWooFragments === 'function') {
        var fragmentsApplied = window.Niwa.ProductManager.applyWooFragments(
          payload.cart.fragments || null,
          payload.cart.cart_hash || '',
          null,
          { emitAdded: false }
        );
        if (typeof window.Niwa.ProductManager.refreshWcFragments === 'function') {
          window.Niwa.ProductManager.refreshWcFragments(null, { silent: true });
        } else if (!fragmentsApplied && typeof jQuery !== 'undefined') {
          jQuery(document.body).trigger('wc_fragment_refresh');
        }
      }
      if (utils && typeof utils.trackFrontendEvent === 'function') {
        utils.trackFrontendEvent('product_add_to_cart', {
          source: 'niwa_agent',
          product_id: payload.cart.product_id || 0,
          product_name: payload.cart.product_name || '',
          quantity: payload.cart.quantity || 1,
          meta: {
            action: 'agent_cart',
            variation_id: payload.cart.variation_id || 0,
            variation: payload.cart.variation || {}
          }
        });
      }
      hasContent = true;
    }

    if (payload && payload.cart) {
      _writePendingCartSelection(payload.cart);
    }

    // Agent-driven navigation (navigate_to_page tool) — redirect the browser
    // to an internal page (checkout, cart, any site page) after the reply.
    if (payload && payload.navigation && payload.navigation.success && payload.navigation.url) {
      if (_scheduleAgentNavigation(payload.navigation)) {
        hasContent = true;
      }
    }

    // Fallback if no content
	    if (!hasContent && !suppressedSignal) {
	      var labels = (window.NiwaAI && window.NiwaAI.labels) || {};
	      renderer.addMessage(labels.ask_what_you_need || 'How can I help you?', 'bot', { requestId: requestId || '' });
	      hasContent = true;
	    }

	    // Agent asked to launch the lead form (via the `activate_lead_form`
	    // tool). Kick the matching mode adapter. `Niwa.LeadForm.activate()`
	    // resolves chat-vs-card based on NiwaLF.config.mode and is a no-op
	    // if the flow is already running / completed in this session.
	    if (payload && payload.lead_form_trigger) {
	      if (window.Niwa && window.Niwa.LeadForm && typeof window.Niwa.LeadForm.activate === 'function') {
	        try { window.Niwa.LeadForm.activate({ source: 'ai_tool', reason: payload.lead_form_trigger_reason || '' }); }
	        catch (e) { /* swallow — activation is best-effort */ }
	      }
	    }

	    // Agent asked to start appointment booking. Scheduler is deliberately
	    // independent from Lead Form Generator, so this also works when the form
	    // builder module is disabled.
	    if (payload && payload.scheduler_trigger) {
	      if (window.Niwa && window.Niwa.Scheduler && typeof window.Niwa.Scheduler.activate === 'function') {
	        try {
	          var schedulerStart = window.Niwa.Scheduler.activate({
	            source: 'ai_tool',
	            serviceId: payload.scheduler_service_id || '',
	            teamMemberId: payload.scheduler_team_member_id || '',
	            reason: payload.scheduler_trigger_reason || ''
	          });
	          if (schedulerStart && typeof schedulerStart.catch === 'function') schedulerStart.catch(function () {});
	        } catch (e) { /* activation is best-effort */ }
	      }
	    }

	    if (hasContent && chatWasClosed) {
	      _markUnreadResponse();
	    }
	  }

  /* -- Mirror server history ---------------------------------------------- */

  function _mirrorServerHistory(serverHistory) {
    if (!Array.isArray(serverHistory) || !serverHistory.length) return;
    try {
      var key = _historyStorageKey();
      var renderer = _renderer();
      var localHistory = [];
      var mapped = [];
      var seen = {};
      var combined = [];
      var i;

      function signature(entry) {
        if (!entry || typeof entry !== 'object') return '';
        return [
          entry.type || 'text',
          entry.role || '',
          entry.content || entry.text || '',
          entry.source || '',
          entry.couponCode || entry.coupon_code || '',
          entry.data ? JSON.stringify(entry.data) : ''
        ].join('|');
      }

      function isFrontendOnlyEntry(entry) {
        if (!entry || typeof entry !== 'object') return false;
        if (entry.type && entry.type !== 'text') return true;
        var source = String(entry.source || '');
        return source === 'ConversionBuster'
          || source === 'ConversionBusterUser'
          || source === 'coupon_restore';
      }

      if (renderer && typeof renderer.getHistory === 'function') {
        localHistory = renderer.getHistory() || [];
      } else {
        var raw = localStorage.getItem(key);
        if (raw) {
          var parsed = JSON.parse(raw);
          if (Array.isArray(parsed)) {
            localHistory = parsed;
          }
        }
      }

      for (i = 0; i < serverHistory.length; i++) {
        var item = serverHistory[i];
        if (!item || !item.text) continue;
        var timestamp = item.ts ? parseInt(item.ts, 10) * 1000 : Date.now();
        mapped.push({
          type: 'text',
          role: item.role === 'user' ? 'user' : 'bot',
          content: String(item.text),
          source: item.source || null,
          timestamp: timestamp
        });
        if (Array.isArray(item.cards) && item.cards.length) {
          mapped.push({
            type: 'products',
            data: item.cards,
            timestamp: timestamp + 1
          });
        }
      }
      if (!mapped.length) return;

      for (i = 0; i < mapped.length; i++) {
        seen[signature(mapped[i])] = true;
        combined.push(mapped[i]);
      }

      for (i = 0; i < localHistory.length; i++) {
        var entry = localHistory[i];
        var sig = signature(entry);
        if (!isFrontendOnlyEntry(entry) || !sig || seen[sig]) continue;
        seen[sig] = true;
        combined.push(entry);
      }

      combined = combined
        .map(function (entry, order) {
          return { entry: entry, order: order };
        })
        .sort(function (a, b) {
          var aTs = typeof a.entry.timestamp === 'number' ? a.entry.timestamp : 0;
          var bTs = typeof b.entry.timestamp === 'number' ? b.entry.timestamp : 0;
          if (aTs === bTs) return a.order - b.order;
          return aTs - bTs;
        })
        .map(function (item) {
          return item.entry;
        });

      localStorage.setItem(key, JSON.stringify(combined.slice(-150)));
      localStorage.setItem('niwa_ai_last_activity', String(Date.now()));
    } catch (e) { /* ignore */ }
  }

  /* -- Default Suggestions ------------------------------------------------ */

  function getDefaultSuggestions() {
    var cfg = window.NiwaAI && window.NiwaAI.initial_suggestions;
    if (!cfg) return [];
    return cfg.list || cfg;
  }

  function getDefaultSuggestionCodes() {
    var cfg = window.NiwaAI && window.NiwaAI.initial_suggestions;
    if (!cfg) return null;
    return cfg.codes || null;
  }

  /* -- Public API --------------------------------------------------------- */

  /**
   * Whether a chat POST is currently in flight (between user send and final
   * response/queue resolution). Exposed so external pollers can suppress
   * their own AJAX traffic during a long LLM turn — AJAX requests that
   * fire while the chat handler is holding a PHP worker cause WC session row
   * lock contention and make the page feel unresponsive on WooCommerce sites.
   *
   * @returns {boolean}
   */
  function isSending() {
    return !!_isSending;
  }

  return {
    init: init,
    sendMessage: sendMessage,
    pollQueue: pollQueue,
    resumePendingRequest: resumePendingRequest,
    isSending: isSending,
    getDefaultSuggestions: getDefaultSuggestions,
    getDefaultSuggestionCodes: getDefaultSuggestionCodes
  };
})();
