/* ==========================================================================
   Niwa Chatbot -- Utility Module
   ---------------------------------------------------------------------------
   Shared utility functions for the Niwa AI WordPress plugin frontend.
   No jQuery dependency.

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

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

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

  var STORAGE_KEY = (window.NiwaAI && window.NiwaAI.storage_key)
    ? window.NiwaAI.storage_key
    : 'niwa_ai_chat_history_v1';

  var volatileSessionId = null;
  var lastSessionCookieValue = '';

  /* -- DOM helpers -------------------------------------------------------- */

  function qs(sel, parent) {
    return (parent || document).querySelector(sel);
  }

  function qsa(sel, parent) {
    return Array.prototype.slice.call((parent || document).querySelectorAll(sel));
  }

  function on(el, event, handler, opts) {
    if (el) el.addEventListener(event, handler, opts || false);
  }

  function off(el, event, handler, opts) {
    if (el) el.removeEventListener(event, handler, opts || false);
  }

  /* -- HTML sanitization -------------------------------------------------- */

  var ESCAPE_MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };

  function escapeHtml(str) {
    return String(str || '').replace(/[&<>"']/g, function (ch) {
      return ESCAPE_MAP[ch];
    });
  }

  /**
   * Tag allowlist for bot messages.
   * Keys are uppercase tag names; values are arrays of safe attribute names.
   */
  var TAG_ALLOWLIST = {
    A:          ['href', 'target', 'rel'],
    STRONG:     [],
    B:          [],
    EM:         [],
    I:          [],
    CODE:       [],
    BR:         [],
    IMG:        ['src', 'alt', 'width', 'height'],
    P:          [],
    UL:         [],
    OL:         [],
    LI:         [],
    SPAN:       ['class'],
    DIV:        ['class'],
    H1:         [], H2: [], H3: [], H4: [], H5: [], H6: [],
    PRE:        [],
    BLOCKQUOTE: [],
    TABLE:      [],
    THEAD:      [],
    TBODY:      [],
    TR:         [],
    TH:         [],
    TD:         [],
    DEL:        [],
    INS:        [],
    BDI:        [],
    SMALL:      []
  };

  /**
   * Sanitize HTML string, keeping only allowlisted tags and safe attributes.
   * Validates href values (must start with http://, https://, or /).
   * Validates target values (must be _blank, _self, _parent, _top).
   */
  function sanitizeHtml(html) {
    if (!html || typeof html !== 'string') return '';

    var tmp = document.createElement('div');
    tmp.innerHTML = html;

    function processNode(node) {
      if (node.nodeType === Node.TEXT_NODE) {
        return escapeHtml(node.textContent);
      }

      if (node.nodeType !== Node.ELEMENT_NODE) return '';

      var tag = node.tagName;
      var allowed = TAG_ALLOWLIST[tag];

      if (tag === 'BR') return '<br>';

      if (typeof allowed !== 'undefined') {
        var tagLower = tag.toLowerCase();
        var attrs = '';

        if (allowed.length) {
          for (var i = 0; i < allowed.length; i++) {
            var attrName = allowed[i];
            var attrVal = node.getAttribute(attrName);
            if (attrVal === null) continue;

            // Validate href
            if (attrName === 'href') {
              if (!(attrVal.indexOf('http://') === 0 || attrVal.indexOf('https://') === 0 || attrVal.indexOf('/') === 0)) {
                continue;
              }
            }

            // Validate target
            if (attrName === 'target') {
              if (attrVal !== '_blank' && attrVal !== '_self' && attrVal !== '_parent' && attrVal !== '_top') {
                continue;
              }
            }

            // Validate rel
            if (attrName === 'rel') {
              if (!/^[a-z\s-]+$/i.test(attrVal)) continue;
            }

            attrs += ' ' + attrName + '="' + escapeHtml(attrVal) + '"';
          }
        }

        var inner = '';
        for (var ci = 0; ci < node.childNodes.length; ci++) {
          inner += processNode(node.childNodes[ci]);
        }

        return '<' + tagLower + attrs + '>' + inner + '</' + tagLower + '>';
      }

      // Not in allowlist -- just process children (strip the tag itself)
      var text = '';
      for (var ti = 0; ti < node.childNodes.length; ti++) {
        text += processNode(node.childNodes[ti]);
      }
      return text;
    }

    var result = '';
    for (var ri = 0; ri < tmp.childNodes.length; ri++) {
      result += processNode(tmp.childNodes[ri]);
    }
    return result;
  }

  /**
   * Sanitize WooCommerce price HTML -- allows only safe formatting tags.
   */
  function sanitizePriceHtml(priceHtml) {
    if (!priceHtml || typeof priceHtml !== 'string') return '';
    var tmp = document.createElement('div');
    tmp.innerHTML = priceHtml;
    var priceAllowed = { DEL: 1, INS: 1, SPAN: 1, BDI: 1, STRONG: 1, SMALL: 1 };

    function walk(node) {
      var out = '';
      for (var i = 0; i < node.childNodes.length; i++) {
        var child = node.childNodes[i];
        if (child.nodeType === 3) {
          out += escapeHtml(child.textContent);
        } else if (child.nodeType === 1 && priceAllowed[child.tagName]) {
          var t = child.tagName.toLowerCase();
          var cls = child.className ? ' class="' + escapeHtml(child.className) + '"' : '';
          out += '<' + t + cls + '>' + walk(child) + '</' + t + '>';
        } else if (child.nodeType === 1) {
          out += walk(child);
        }
      }
      return out;
    }
    return walk(tmp);
  }

  /**
   * Parse a localized WooCommerce price string or HTML snippet into a number.
   */
  function parsePriceAmount(value) {
    if (typeof value === 'number' && isFinite(value)) return value;
    if (!value && value !== 0) return NaN;

    var raw = String(value);
    if (!raw) return NaN;

    if (raw.indexOf('<') !== -1) {
      var tmp = document.createElement('div');
      tmp.innerHTML = raw;
      raw = tmp.textContent || tmp.innerText || raw;
    }

    raw = raw.replace(/\u00a0/g, ' ').replace(/[^\d,.\-]/g, '');
    if (!raw) return NaN;

    var lastComma = raw.lastIndexOf(',');
    var lastDot = raw.lastIndexOf('.');

    if (lastComma > -1 && lastDot > -1) {
      if (lastComma > lastDot) {
        raw = raw.replace(/\./g, '').replace(',', '.');
      } else {
        raw = raw.replace(/,/g, '');
      }
    } else if (lastComma > -1) {
      if (/,\d{1,2}$/.test(raw)) {
        raw = raw.replace(',', '.');
      } else {
        raw = raw.replace(/,/g, '');
      }
    } else if (lastDot > -1) {
      if (!/\.\d{1,2}$/.test(raw)) {
        raw = raw.replace(/\./g, '');
      }
    }

    var num = parseFloat(raw);
    return isFinite(num) ? num : NaN;
  }

  /**
   * Format a discount amount for the "amount off" banner state.
   */
  function formatAmountOff(amount, locale, suffix) {
    var parsed = typeof amount === 'number' ? amount : parsePriceAmount(amount);
    if (!isFinite(parsed) || parsed <= 0) return '';

    var formatted;
    try {
      formatted = new Intl.NumberFormat(locale || undefined, {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2
      }).format(parsed);
    } catch (e) {
      formatted = parsed.toFixed(2);
    }

    return '-' + formatted + ' ' + (suffix || 'OFF');
  }

  /* -- Markdown-lite formatting for bot messages -------------------------- */

  function formatBotRichText(text) {
    var raw = typeof text === 'string' ? text : String(text || '');
    var html = escapeHtml(raw);
    var blockPlaceholders = [];
    var inlinePlaceholders = [];

    html = html.replace(/```(?:([^\r\n`]*)\r?\n)?([\s\S]*?)```/g, function (_match, _lang, code) {
      var normalizedCode = String(code || '').replace(/\r/g, '');
      var token = '%%NIWA_CODE_BLOCK_' + blockPlaceholders.length + '%%';
      blockPlaceholders.push('<pre><code>' + normalizedCode + '</code></pre>');
      return token;
    });

    html = html.replace(/`([^`\r\n]+)`/g, function (_match, code) {
      var token = '%%NIWA_INLINE_CODE_' + inlinePlaceholders.length + '%%';
      inlinePlaceholders.push('<code>' + code + '</code>');
      return token;
    });

    // Markdown links [label](url)
    html = html.replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^\s\)]+)\)/g,
      '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
    // Bold **text**
    html = html.replace(/\*\*([\s\S]+?)\*\*/g, '<strong>$1</strong>');
    // Clean up stray **
    html = html.replace(/\*\*/g, '');
    // Newlines to <br>
    html = html.replace(/\r\n|\r|\n/g, '<br>');
    html = html.replace(/%%NIWA_INLINE_CODE_(\d+)%%/g, function (_match, index) {
      return inlinePlaceholders[parseInt(index, 10)] || '';
    });
    html = html.replace(/%%NIWA_CODE_BLOCK_(\d+)%%/g, function (_match, index) {
      return blockPlaceholders[parseInt(index, 10)] || '';
    });
    return html;
  }

  /* -- Product normalization ---------------------------------------------- */

  function normalizeProduct(p) {
    if (!p) return p;
    var priceHtml = p.price || p.price_html || '';
    var oldPriceHtml = p.oldPrice || p.old_price_html || '';
    var priceAmount = parsePriceAmount(priceHtml);
    var oldPriceAmount = parsePriceAmount(oldPriceHtml);
    var suppliedDiscountAmount = parseFloat(
      p.amount_off || p.discount_amount || 0
    ) || 0;
    var suppliedDiscountPercent = parseInt(
      p.sale_percent || p.discount_percent || 0,
      10
    ) || 0;
    var discountAmount = isFinite(oldPriceAmount) && isFinite(priceAmount) && oldPriceAmount > priceAmount
      ? oldPriceAmount - priceAmount
      : suppliedDiscountAmount;
    var discountPercent = isFinite(oldPriceAmount) && oldPriceAmount > 0 && discountAmount > 0
      ? Math.round((discountAmount / oldPriceAmount) * 100)
      : suppliedDiscountPercent;

    return {
      id: p.id,
      name: p.name || '',
      price: priceHtml,
      oldPrice: oldPriceHtml,
      image: p.image || '',
      image_width: parseInt(p.image_width || 0, 10) || 0,
      image_height: parseInt(p.image_height || 0, 10) || 0,
      preview_image: p.preview_image || '',
      preview_image_width: parseInt(p.preview_image_width || 0, 10) || 0,
      preview_image_height: parseInt(p.preview_image_height || 0, 10) || 0,
      preview_variation_id: parseInt(p.preview_variation_id || 0, 10) || 0,
      preview_attributes: p.preview_attributes || {},
      url: p.url || p.permalink || '#',
      type: p.type || '',
      badge: p.badge || '',
      hasOptions: !!(p.hasOptions || p.type === 'variable'),
      in_stock: p.in_stock !== false,
      purchasable: p.purchasable !== false,
      variation_options: Array.isArray(p.variation_options) ? p.variation_options : [],
      short_desc: p.short_desc || '',
      meta: p.meta || '',
      rating: parseFloat(p.rating_display || p.rating || p.average_rating || 0) || 0,
      rating_count: parseInt(p.rating_count || p.review_count || 0, 10) || 0,
      price_amount: isFinite(priceAmount) ? priceAmount : 0,
      old_price_amount: isFinite(oldPriceAmount) ? oldPriceAmount : 0,
      discount_amount: discountAmount,
      discount_percent: discountPercent,
      sale_percent_label: p.sale_percent_label || '',
      amount_off_label: p.amount_off_label || p.discount_amount_label || '',
      match_class: p.match_class || '',
      disclosure_required: !!p.disclosure_required,
      matched_constraints: Array.isArray(p.matched_constraints) ? p.matched_constraints : [],
      missing_constraints: Array.isArray(p.missing_constraints) ? p.missing_constraints : [],
      conflicting_constraints: Array.isArray(p.conflicting_constraints) ? p.conflicting_constraints : []
    };
  }

  function buildRatingHtml(rating, count, options) {
    var opts = options || {};
    var hasRating = isFinite(rating) && rating > 0;
    var score = hasRating ? rating : (opts.fallbackStars ? 5 : 0);
    if (!score || score <= 0) return '';

    var full = Math.floor(score);
    var half = (score - full) >= 0.3 ? 1 : 0;
    var empty = 5 - full - half;
    var stars = '';
    for (var i = 0; i < full; i++) stars += '\u2605';
    if (half) stars += '\u00BD';
    for (var j = 0; j < empty; j++) stars += '\u2606';

    var countValue = parseInt(count, 10) || 0;
    var showCount = opts.showCount !== false;
    var showZeroCount = !!opts.showZeroCount;
    var countLabel = opts.countLabel ? String(opts.countLabel) : '';
    var countBody = countValue + (countLabel ? ' ' + escapeHtml(countLabel) : '');
    var countText = showCount && (countValue > 0 || showZeroCount)
      ? '<span class="niwa-rating-count">' + (opts.wrapCount ? '(' + countBody + ')' : countBody) + '</span>'
      : '';

    return '<div class="niwa-card-rating">'
      + (opts.showValue ? '<span class="niwa-rating-value">' + score.toFixed(1) + '</span>' : '')
      + '<span class="niwa-stars">' + stars + '</span>'
      + countText
      + '</div>';
  }

  /* -- Session management ------------------------------------------------- */

  function persistSessionCookie(sessionId) {
    if (!sessionId || typeof document === 'undefined') return;
    if (lastSessionCookieValue === sessionId) return;
    var maxAge = 60 * 60 * 24 * 30; // 30 days
    var secure = (window.location && window.location.protocol === 'https:') ? '; Secure' : '';
    document.cookie = 'niwa_ai_session=' + encodeURIComponent(sessionId)
      + '; path=/; max-age=' + maxAge + '; SameSite=Lax' + secure;
    lastSessionCookieValue = sessionId;
  }

  function getSession() {
    var key = 'niwa_ai_session_id';
    try {
      var sid = localStorage.getItem(key);
      if (!sid) {
        sid = 'niwa_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
        localStorage.setItem(key, sid);
      }
      persistSessionCookie(sid);
      return sid;
    } catch (e) {
      if (!volatileSessionId) {
        volatileSessionId = 'niwa_ephemeral_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
      }
      persistSessionCookie(volatileSessionId);
      return volatileSessionId;
    }
  }

  /* -- localStorage JSON wrappers ----------------------------------------- */

  function getStorage(key) {
    try {
      var raw = localStorage.getItem(key);
      return raw ? JSON.parse(raw) : null;
    } catch (e) {
      return null;
    }
  }

  function setStorage(key, val) {
    try {
      localStorage.setItem(key, JSON.stringify(val));
    } catch (e) { /* ignore */ }
  }

  /* -- Time formatting ---------------------------------------------------- */

  function formatTime(date) {
    var d = date instanceof Date ? date : new Date(date);
    var h = d.getHours();
    var m = d.getMinutes();
    var ampm = h >= 12 ? 'PM' : 'AM';
    h = h % 12 || 12;
    return h + ':' + (m < 10 ? '0' : '') + m + ' ' + ampm;
  }

  function formatDate(date) {
    var d = date instanceof Date ? date : new Date(date);
    var today = new Date();
    var yesterday = new Date(today);
    yesterday.setDate(yesterday.getDate() - 1);
	var locale = (window.NiwaAI && (window.NiwaAI.language || window.NiwaAI.locale))
	  || (typeof document !== 'undefined' && document.documentElement ? document.documentElement.lang : '')
	  || (typeof navigator !== 'undefined' ? navigator.language : '')
	  || 'en';
	var relativeDay = function (offset, fallback) {
	  try {
		return new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(offset, 'day');
	  } catch (e) {
		return fallback;
	  }
	};

    if (d.toDateString() === today.toDateString()) return relativeDay(0, 'Today');
    if (d.toDateString() === yesterday.toDateString()) return relativeDay(-1, 'Yesterday');

    return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
  }

  function now() {
    return Date.now();
  }

  function generateId() {
    return 'msg_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
  }

  /* -- AJAX error extraction ---------------------------------------------- */

  function extractAjaxErrorMessage(response, fallback) {
    if (!response) return fallback || '';
    if (typeof response === 'string') return response;
    if (response.message) return response.message;
    if (response.error) return response.error;
    if (response.data) {
      if (typeof response.data === 'string') return response.data;
      if (response.data.message) return response.data.message;
      if (response.data.error) return response.data.error;
    }
    return fallback || '';
  }

  function clearLeaveStoperTrackingState() {
    try {
      sessionStorage.removeItem('niwa_ai_leave_stoper_active');
      sessionStorage.removeItem('niwa_ai_leave_stoper_trigger_type');
      sessionStorage.removeItem('niwa_ai_leave_stoper_triggered_at');
    } catch (e) { /* ignore */ }
  }

  function getLeaveStoperTrackingMeta() {
    var out = {};

    try {
      var leaveActive = sessionStorage.getItem('niwa_ai_leave_stoper_active') === '1';
      var leaveTriggerType = sessionStorage.getItem('niwa_ai_leave_stoper_trigger_type') || '';
      var leaveTriggeredAt = sessionStorage.getItem('niwa_ai_leave_stoper_triggered_at') || '';

      if (!leaveActive && !leaveTriggerType && !leaveTriggeredAt) {
        return out;
      }

      var triggeredAtMs = parseInt(leaveTriggeredAt, 10);
      var nowMs = Date.now();
      var leaveConfig = (window.NiwaAI && window.NiwaAI.leave_stoper) ? window.NiwaAI.leave_stoper : {};
      var windowMinutes = parseInt(leaveConfig.cooldown_minutes, 10);
      if (!windowMinutes || windowMinutes < 1) windowMinutes = 30;

      if (triggeredAtMs && nowMs > triggeredAtMs && (nowMs - triggeredAtMs) > (windowMinutes * 60 * 1000)) {
        clearLeaveStoperTrackingState();
        return out;
      }

      if (leaveActive) {
        out.leave_stoper_active = true;
      }
      if (leaveTriggerType) {
        out.leave_stoper_trigger_type = leaveTriggerType;
      }
      if (leaveTriggeredAt) {
        out.leave_stoper_triggered_at = leaveTriggeredAt;
      }
      if (triggeredAtMs && nowMs >= triggeredAtMs) {
        out.leave_stoper_elapsed_ms = nowMs - triggeredAtMs;
      }
    } catch (e) { /* ignore */ }

    return out;
  }

  function trackFrontendEvent(eventType, payload, options) {
    var cfg = window.NiwaAI && window.NiwaAI.analytics;
    if (!cfg || !cfg.ajax_url || !cfg.nonce || !eventType) return null;

    var data = payload || {};
    var meta = data.meta;
    if (!meta || typeof meta !== 'object') meta = {};

    try {
      var leaveMeta = getLeaveStoperTrackingMeta();
      for (var leaveKey in leaveMeta) {
        if (Object.prototype.hasOwnProperty.call(leaveMeta, leaveKey)) {
          meta[leaveKey] = leaveMeta[leaveKey];
        }
      }
    } catch (e) { /* ignore */ }

    var fd = new FormData();
    fd.append('action', cfg.track_action || 'niwa_ai_track_frontend_event');
    fd.append('nonce', cfg.nonce);
    fd.append('event_type', eventType);
    fd.append('page_url', data.page_url || window.location.href);
    fd.append('source', data.source || '');
    fd.append('session', data.session || getSession());
    if (data.product_id) fd.append('product_id', String(data.product_id));
    if (data.product_name) fd.append('product_name', String(data.product_name));
    if (data.coupon_code) fd.append('coupon_code', String(data.coupon_code));
    if (data.order_id) fd.append('order_id', String(data.order_id));
    if (data.quantity !== undefined && data.quantity !== null) fd.append('quantity', String(data.quantity));
    if (data.revenue !== undefined && data.revenue !== null) fd.append('revenue', String(data.revenue));
    if (data.order_total !== undefined && data.order_total !== null) fd.append('order_total', String(data.order_total));
    if (meta !== undefined) {
      try {
        fd.append('meta', typeof meta === 'string' ? meta : JSON.stringify(meta));
      } catch (e) { /* ignore */ }
    }

    if (options && options.transport === 'beacon' && navigator.sendBeacon) {
      try {
        return navigator.sendBeacon(cfg.ajax_url, fd);
      } catch (e) { /* ignore */ }
    }

    return fetch(cfg.ajax_url, {
      method: 'POST',
      body: fd,
      keepalive: !(options && options.keepalive === false)
    }).catch(function () {});
  }

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

  // Initialize session on load
  getSession();

  return {
    STORAGE_KEY: STORAGE_KEY,
    qs: qs,
    qsa: qsa,
    on: on,
    off: off,
    escapeHtml: escapeHtml,
    sanitizeHtml: sanitizeHtml,
    sanitizePriceHtml: sanitizePriceHtml,
    parsePriceAmount: parsePriceAmount,
    formatAmountOff: formatAmountOff,
    formatBotRichText: formatBotRichText,
    normalizeProduct: normalizeProduct,
    buildRatingHtml: buildRatingHtml,
    getSession: getSession,
    persistSessionCookie: persistSessionCookie,
    getStorage: getStorage,
    setStorage: setStorage,
    formatTime: formatTime,
    formatDate: formatDate,
    now: now,
    generateId: generateId,
    extractAjaxErrorMessage: extractAjaxErrorMessage,
    trackFrontendEvent: trackFrontendEvent
  };
})();
