import { showToast } from '../components/utils';

const $daftplugAdmin = jQuery('#daftplugAdmin');
const jsVars = window[`generatify_admin_js_vars`] || {};

export function initChatbot() {
  const $ai = $daftplugAdmin.find('#ai');
  const $aiNewChatButton = $ai.find('#aiNewChatButton');
  const $aiContainer = $ai.find('#aiContainer');
  const $aiResponseContainer = $aiContainer.find('#aiResponseContainer');
  const $aiScrollToBottomButton = $aiContainer.find('#aiScrollToBottomButton');
  const $aiFormContainer = $aiContainer.find('#aiFormContainer');
  const $aiPromptStarters = $aiFormContainer.find('#aiPromptStarters');
  const $aiForm = $aiFormContainer.find('#aiForm');
  const $aiContext = $aiForm.find('#aiContext');
  const $aiPrompt = $aiForm.find('#aiPrompt');
  const $aiModel = $aiForm.find('#aiModel');
  const $aiSendButton = $aiForm.find('#aiSendButton');

  let chatHistory = [];

  handleSelectedModelPersistence($aiModel);
  handleModelSelectColoring($aiModel);
  handleRestartChat($ai, $aiResponseContainer, $aiNewChatButton, chatHistory);
  handleScrollToBottomButton($ai, $aiResponseContainer, $aiScrollToBottomButton);
  handlePromptAutofocus($aiPrompt);
  handlePromptStarters($aiPromptStarters, $aiPrompt, $aiModel, $aiForm);
  handleSendButtonActivation($aiPrompt, $aiSendButton);
  handleCopyMessage($aiResponseContainer);
  handleFormSubmission($ai, $aiNewChatButton, $aiResponseContainer, $aiForm, $aiContext, $aiPrompt, $aiModel, chatHistory);
}

function handleSelectedModelPersistence($aiModel) {
  const savedModel = localStorage.getItem('generatify_selected_ai_model');
  if (savedModel) {
    // Check if the saved model exists in the current options
    const optionExists = $aiModel.find(`option[value="${savedModel}"]`).length > 0;
    if (optionExists) {
      $aiModel.val(savedModel).trigger('change');
    } else {
      // Remove invalid saved model from localStorage
      localStorage.removeItem('generatify_selected_ai_model');
    }
  }

  $aiModel.on('change', function () {
    const selectedModel = jQuery(this).val();
    if (selectedModel) {
      localStorage.setItem('generatify_selected_ai_model', selectedModel);
    } else {
      localStorage.removeItem('generatify_selected_ai_model');
    }
  });
}

function handleModelSelectColoring($aiModel) {
  $aiModel
    .on('change', function () {
      const modelValue = jQuery(this).val();
      const $truncateElement = jQuery(this).next('[data-elm-toggle]');

      if (modelValue && modelValue.startsWith('gpt')) {
        $truncateElement.css({
          fontWeight: '600',
          color: '#7d7a75',
          backgroundColor: '#f0efed',
        });
      } else if (modelValue && modelValue.startsWith('claude')) {
        $truncateElement.css({
          fontWeight: '600',
          color: '#d5803b',
          backgroundColor: '#fbebde',
        });
      } else if (modelValue && modelValue.startsWith('gemini')) {
        $truncateElement.css({
          fontWeight: '600',
          color: '#b577d6',
          backgroundColor: '#f3ebf9',
        });
      } else if (modelValue && modelValue.startsWith('kimi')) {
        $truncateElement.css({
          fontWeight: '600',
          color: '#1f2937',
          backgroundColor: '#e5e5e5',
        });
      } else {
        $truncateElement.css({
          fontWeight: '600',
          color: '#7d7a75',
          backgroundColor: '#f0efed',
        });
      }
    })
    .trigger('change');
}

function handleRestartChat($ai, $aiResponseContainer, $aiNewChatButton, chatHistory) {
  $aiNewChatButton.on('click', function () {
    if (confirm(wp.i18n.__("Are you sure you want to start a new chat? This will clear all messages in the current conversation and you won't be able to return to it.", 'generatify'))) {
      chatHistory.length = 0;
      $ai.removeAttr('data-conversation');
      $aiResponseContainer.empty();
      $ai.find('#aiContext').val('').trigger('change');
      $ai.find('#aiPrompt').focus().val('').prop('readonly', false).trigger('change');
    }
  });
}

function handlePromptAutofocus($aiPrompt) {
  $aiPrompt.focus();
}

function handleScrollToBottomButton($ai, $aiResponseContainer, $aiScrollToBottomButton) {
  const scrollThreshold = 80;
  let shouldStickToBottom = true;
  let updateFrame = null;

  const getDistanceFromBottom = () => {
    const aiElement = $ai[0];
    return aiElement.scrollHeight - aiElement.scrollTop - aiElement.clientHeight;
  };

  const isConversationActive = () => $ai.attr('data-conversation') !== undefined;
  const isNearBottom = () => getDistanceFromBottom() <= scrollThreshold;

  const setButtonVisible = (isVisible) => {
    if (isVisible) {
      $aiScrollToBottomButton.attr('data-visible', 'true');
    } else {
      $aiScrollToBottomButton.removeAttr('data-visible');
    }
  };

  const updateButtonState = () => {
    const hasMessages = $aiResponseContainer.children().length > 0;
    setButtonVisible(isConversationActive() && hasMessages && !isNearBottom());
  };

  const scrollToBottom = (behavior = 'smooth') => {
    $ai[0].scrollTo({
      top: $ai[0].scrollHeight,
      behavior,
    });
  };

  const queueUpdate = () => {
    if (updateFrame !== null) {
      cancelAnimationFrame(updateFrame);
    }

    updateFrame = requestAnimationFrame(() => {
      if (shouldStickToBottom) {
        scrollToBottom('auto');
      }
      updateButtonState();
      updateFrame = null;
    });
  };

  $ai.on('scroll', () => {
    shouldStickToBottom = isNearBottom();
    updateButtonState();
  });

  $aiScrollToBottomButton.on('click', () => {
    shouldStickToBottom = true;
    scrollToBottom('smooth');
    setTimeout(updateButtonState, 250);
  });

  if (window.MutationObserver) {
    new MutationObserver(() => {
      queueUpdate();
    }).observe($aiResponseContainer[0], {
      childList: true,
      subtree: true,
      characterData: true,
    });

    new MutationObserver(() => {
      if (!isConversationActive()) {
        shouldStickToBottom = true;
      }
      queueUpdate();
    }).observe($ai[0], {
      attributes: true,
      attributeFilter: ['data-conversation'],
    });
  }

  updateButtonState();
}

function handlePromptStarters($aiPromptStarters, $aiPrompt, $aiModel, $aiForm) {
  $aiPromptStarters.find('[data-prompt-starter]').on('click', function () {
    if (!$aiModel.val()) {
      showToast(wp.i18n.__('AI Model Required', 'generatify'), wp.i18n.__('Please select an AI model first.', 'generatify'), 'warning', 'top-right');
      return;
    }

    const self = jQuery(this);
    const starterText = self.data('prompt-starter');
    $aiPrompt.val(starterText).trigger('change').focus();
  });
}

function handleSendButtonActivation($aiPrompt, $aiSendButton) {
  $aiPrompt.on('input change paste', () => {
    $aiSendButton.prop('disabled', $aiPrompt.val().trim() === '');
  });
}

function handleCopyMessage($aiResponseContainer) {
  $aiResponseContainer.on('click', '[data-copy-user-message-button], [data-copy-ai-response-button]', async function () {
    const copyButton = jQuery(this);
    const isUserMessage = copyButton.is('[data-copy-user-message-button]');
    const defaultIcon = copyButton.find('.copy-default');
    const successIcon = copyButton.find('.copy-success');
    const messageContainer = isUserMessage ? copyButton.closest('[data-user-message]') : copyButton.closest('[data-ai-response]');
    const messageElement = isUserMessage ? messageContainer.find('[data-user-message-content]') : messageContainer.find('[data-ai-response-content]');

    try {
      const messageContentPlainText = messageElement.text().trim();
      const messageContentHtml = messageElement.html().trim();
      const messageContentMarkdown = htmlToMarkdown(messageContentHtml);

      if (navigator.clipboard && navigator.clipboard.writeText) {
        // Always copy markdown as plain text
        await navigator.clipboard.writeText(messageContentMarkdown);
      } else {
        // Very old browsers fallback
        const textarea = document.createElement('textarea');
        textarea.value = messageContentMarkdown;
        textarea.style.position = 'fixed';
        textarea.style.opacity = '0';
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
      }

      defaultIcon.addClass('hidden');
      successIcon.removeClass('hidden');

      setTimeout(() => {
        defaultIcon.removeClass('hidden');
        successIcon.addClass('hidden');
      }, 2000);
    } catch (err) {
      console.error('Failed to copy text: ', err);
    }
  });
}

function handleFormSubmission($ai, $aiNewChatButton, $aiResponseContainer, $aiForm, $aiContext, $aiPrompt, $aiModel, chatHistory) {
  // Submit on Enter key (without Shift)
  $aiPrompt.on('keydown', (event) => {
    if (event.key === 'Enter' && !event.shiftKey) {
      event.preventDefault();
      $aiForm.trigger('submit');
    }
  });

  // Handle form submission
  $aiForm.on('submit', async (event) => {
    event.preventDefault();

    const promptValue = escapeHtml($aiPrompt.val().trim());
    const contextValue = $aiContext.val();
    const modelValue = $aiModel.val();

    // Prevent multiple submissions
    if ($aiPrompt.prop('readonly')) {
      return;
    }

    if (!promptValue || !modelValue) {
      showToast(wp.i18n.__('Missing Input', 'generatify'), wp.i18n.__('Please make sure you provide a prompt and select a model.', 'generatify'), 'warning', 'top-right');
      return;
    }

    // Set conversation state
    if ($ai.attr('data-conversation') === undefined) {
      const conversationId = Date.now();
      $ai.attr('data-conversation', conversationId);
    }
    $aiContext.val('').trigger('change');
    $aiNewChatButton.prop('disabled', true);
    $aiPrompt.blur().val('').prop('readonly', true).trigger('change');

    // Define loading messages with durations
    let loadingMessages = [
      { text: wp.i18n.__('Thinking', 'generatify'), duration: getRandomDuration(2000, 4000) },
      { text: wp.i18n.__('Generating response', 'generatify'), duration: getRandomDuration(1000, 2000) },
    ];

    

    // Generate unique ID for this message pair
    const messageId = Date.now();

    // Build selected context chips (if any)
    const decodeHtmlEntities = (str) => {
      if (typeof str !== 'string') return '';
      const txt = document.createElement('textarea');
      txt.innerHTML = str;
      return txt.value;
    };

    const contextChips = Array.isArray(contextValue)
      ? contextValue
          .map((val) => {
            try {
              const decoded = decodeHtmlEntities(val);
              return JSON.parse(decoded);
            } catch (e) {
              return null;
            }
          })
          .filter(Boolean)
          .map((ctx) => {
            const iconHtml = typeof ctx.iconHtml === 'string' ? ctx.iconHtml.trim() : '';
            const iconClass = ctx.icon || '';
            const titleText = ctx.title || '';
            return `
              <span class="inline-flex items-center gap-x-1.5 py-1 px-2 rounded-full bg-gray-50">
                ${iconHtml || (iconClass ? `<span class="dashicons ${iconClass} text-gray-500 text-[15px] leading-none size-[15px] -mr-0.5"></span>` : '')}
                <span class="truncate max-w-52 text-xs font-medium text-gray-600">${titleText}</span>
              </span>
            `;
          })
          .join('\n')
      : '';

    // Append user message and response container with initial loading message
    $aiResponseContainer.append(`
      <div data-user-message="${messageId}" class="group/message pt-12 first:pt-0 pb-5 min-h-8 relative flex w-full flex-col items-end gap-2 text-start break-words whitespace-normal empty:hidden rtl:items-start">
        ${contextChips ? `<div class="w-full flex flex-wrap items-center gap-1.5 justify-end rtl:justify-start">${contextChips}</div>` : ''}
        <div class="relative text-start bg-gray-100 text-sm leading-normal text-gray-800 inline-block rounded-[18px] py-2.5 px-4 max-w-[70%]">
          <div data-user-message-content class="whitespace-pre-wrap [word-break:break-word]">${promptValue}</div>
        </div>
        <div class="flex items-center justify-end opacity-0 group-hover/message:opacity-100 group-focus/message:opacity-100 transition-opacity duration-300">
          <button data-copy-user-message-button type="button" class="relative flex justify-center items-center gap-x-3 size-8 text-sm text-gray-600 hover:bg-gray-100 rounded-full disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 before:absolute before:top-9 before:left-1/2 before:-translate-x-1/2 before:hidden before:content-[attr(data-tooltip)] before:w-max before:max-w-xs sm:before:max-w-lg before:z-[99999999999999] before:text-center before:py-1 before:px-2 before:bg-gray-900 before:text-[11px] before:leading-snug before:font-medium before:text-white before:rounded-lg before:shadow-sm hover:before:block" data-tooltip="${wp.i18n.__('Copy', 'generatify')}">
            <svg class="copy-default shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
              <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
            </svg>
            <svg class="copy-success hidden shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M20 6 9 17l-5-5" />
            </svg>
          </button>
        </div>
      </div>
      <div data-ai-response="${messageId}" class="group/message w-full last:min-h-[calc(100dvh-465px)]">
        <div class="w-max text-sm bg-[linear-gradient(90deg,rgb(75_85_99)_0%,rgb(75_85_99_/_0.7)_20%,rgb(75_85_99_/_0.1)_50%,rgb(75_85_99_/_0.7)_80%,rgb(75_85_99)_100%)] bg-[length:200%_100%] bg-clip-text text-transparent animate-[shine-text_2.5s_ease-in_infinite]">
          ${loadingMessages[0].text}
        </div>
      </div>
    `);

    // Scroll to the bottom of the AI container
    $ai.scrollTop($ai[0].scrollHeight);

    const $aiResponseMessage = $aiResponseContainer.find(`[data-ai-response="${messageId}"]`);

    // Reference to the loading element
    const loadingElement = $aiResponseMessage.find('div').first();

    // Helper: show loading messages sequentially and hold on the last one
    const startLoadingLoop = (messages, el) => {
      let idx = 0;
      let active = true;
      let timeoutId = null;

      const step = () => {
        if (!active) return;
        el.text(messages[idx].text);
        const isLast = idx === messages.length - 1;
        if (!isLast) {
          const duration = messages[idx].duration || 1500;
          idx += 1;
          timeoutId = setTimeout(step, duration);
        }
      };

      step();

      return () => {
        active = false;
        if (timeoutId) clearTimeout(timeoutId);
      };
    };

    // Set AI response as promotion for free version
    let aiResponse = getPromoAiResponse();

    

    // Free version: show each loading message for its duration, then render the simulated response
    let currentMessageIndex = 0;
    const advanceOnce = () => {
      currentMessageIndex++;
      if (currentMessageIndex < loadingMessages.length) {
        loadingElement.text(loadingMessages[currentMessageIndex].text);
        setTimeout(advanceOnce, loadingMessages[currentMessageIndex].duration || 1500);
      } else {
        streamAiResponse($aiNewChatButton, $aiResponseMessage, aiResponse, $aiPrompt);
      }
    };
    // Kick off the first progression after the first duration
    setTimeout(advanceOnce, loadingMessages[0].duration || 1500);
  });
}

/*! Utility Functions */
// Function to generate AI response HTML promoting Pro version
function getPromoAiResponse() {
  return `
    <div class="relative w-full">
      <p class="mb-4 text-sm leading-relaxed">
        ${wp.i18n.__(`👋 Hey! I’m <strong>${jsVars.siteName}</strong>’s built-in AI assistant. On the free plan I can only say hello, but if you want to unlock all the powerful AI features in the plugin, including my full abilities and have me handle the heavy lifting for you as your tireless copilot —`, 'generatify')}
        <button type="button" class="font-bold bg-[#155dfc] !text-white text-[11px] py-1.5 px-2.5 rounded-md cursor-pointer !no-underline" data-needs-active-pro>
          ${wp.i18n.__('Upgrade to Pro 🚀', 'generatify')}
        </button>
      </p>
      <h3 class="mb-2.5 font-medium text-base">
        ${wp.i18n.__('Features:', 'generatify')}
      </h3>
      <ul class="list-disc list-outside ps-7 space-y-2">
        <li class="ps-0.5">
          <strong>${wp.i18n.__('Context-Aware', 'generatify')}:</strong>
          ${wp.i18n.__('🧠 Knows your posts, pages, plugins, themes, and users.', 'generatify')}
        </li>
        <li class="ps-0.5">
          <strong>${wp.i18n.__('Content Writing', 'generatify')}:</strong>
          ${wp.i18n.__('📝 Drafts on-brand posts, pages, and product copy.', 'generatify')}
        </li>
        <li class="ps-0.5">
          <strong>${wp.i18n.__('Rewrites & Translations', 'generatify')}:</strong>
          ${wp.i18n.__('✍️ Improves, repurposes, and translates existing text.', 'generatify')}
        </li>
        <li class="ps-0.5">
          <strong>${wp.i18n.__('Ideas & Planning', 'generatify')}:</strong>
          ${wp.i18n.__('💡 Brainstorms headlines, FAQs, offers, and content outlines.', 'generatify')}
        </li>
        <li class="ps-0.5">
          <strong>${wp.i18n.__('Troubleshooting Help', 'generatify')}:</strong>
          ${wp.i18n.__('🛠️ Explains errors, suggests fixes and step-by-step guides.', 'generatify')}
        </li>
      </ul>
      <p class="mt-5 text-sm">
        ${wp.i18n.__('Alternatively, you can visit', 'generatify')}
        <a class="text-blue-600 hover:underline cursor-pointer" href="/wp-admin/admin.php?page=generatify-settings">${wp.i18n.__('⚙️ Plugin Settings', 'generatify')}</a>
        ${wp.i18n.__('to configure and enable AI features such as automations, help desk, smart suggestions, and more.', 'generatify')}
      </p>
    </div>
  `;
}

// Function to get a random duration between min and max (in milliseconds)
function getRandomDuration(min = 2000, max = 4000) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// Minimal delay helper
function delay(ms) {
  return new Promise((res) => setTimeout(res, ms));
}

// Basic HTML escaping for safe text rendering
function escapeHtml(str) {
  if (typeof str !== 'string') return '';
  return str.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;');
}

// Function that converts HTML to Markdown
function htmlToMarkdown(html) {
  const temp = document.createElement('div');
  temp.innerHTML = html;

  const collapseWs = (str) => str.replace(/\s+/g, ' ');

  function convert(node) {
    if (node.nodeType === Node.TEXT_NODE) {
      // collapse pretty-print whitespace
      return collapseWs(node.textContent || '');
    }

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

    const tag = node.tagName.toLowerCase();
    let content = Array.from(node.childNodes).map(convert).join('');
    content = content.trim();

    switch (tag) {
      case 'strong':
      case 'b':
        return content ? `**${content}**` : '';
      case 'em':
      case 'i':
        return content ? `_${content}_` : '';
      case 'h1':
        return content ? `\n\n# ${content}\n\n` : '';
      case 'h2':
        return content ? `\n\n## ${content}\n\n` : '';
      case 'h3':
        return content ? `\n\n### **${content}**\n\n` : '';
      case 'h4':
        return content ? `\n\n#### ${content}\n\n` : '';
      case 'h5':
        return content ? `\n\n##### ${content}\n\n` : '';
      case 'h6':
        return content ? `\n\n###### ${content}\n\n` : '';
      case 'p':
        return content ? `\n\n${content}\n\n` : '';
      case 'br':
        return '\n';
      case 'ul':
      case 'ol':
        return content ? `\n${content}\n` : '';
      case 'li':
        return content ? `- ${content}\n` : '';
      case 'code':
        return content ? `\`${content}\`` : '';
      case 'pre':
        return content ? `\n\n\`\`\`\n${content}\n\`\`\`\n\n` : '';
      case 'a': {
        const href = node.getAttribute('href') || '';
        return content ? `[${content}](${href})` : href;
      }
      case 'img': {
        const alt = node.getAttribute('alt') || '';
        const src = node.getAttribute('src') || '';
        return src ? `![${alt}](${src})` : '';
      }
      case 'blockquote':
        return content ? `\n\n> ${content}\n\n` : '';
      default:
        return content;
    }
  }

  let md = convert(temp);

  // Normalise: remove trailing spaces and collapse excessive blank lines
  md = md
    .replace(/[ \t]+\n/g, '\n') // no spaces at end-of-line
    .replace(/\n{3,}/g, '\n\n') // max 2 consecutive newlines
    .trim();

  return md;
}

// Copy attributes from a source Element to a target Element
function copyAttributes(srcEl, targetEl) {
  if (!srcEl || !srcEl.attributes) return;
  for (let i = 0; i < srcEl.attributes.length; i++) {
    const attr = srcEl.attributes[i];
    try {
      targetEl.setAttribute(attr.name, attr.value);
    } catch (_) {
      // Ignore invalid attributes for security or browser quirks
    }
  }
}

// Stream text content into the given parent element in chunks
function streamTextTo(parentEl, text, charDelay, chunkSize = 5) {
  return new Promise((resolve) => {
    if (!text) {
      parentEl.appendChild(document.createTextNode(''));
      return resolve();
    }
    if (charDelay <= 0 || chunkSize <= 0) {
      parentEl.appendChild(document.createTextNode(text || ''));
      return resolve();
    }

    const textNode = document.createTextNode('');
    parentEl.appendChild(textNode);

    // Use code points to avoid breaking surrogate pairs (emojis)
    const units = Array.from(text);
    let i = 0;
    const step = () => {
      if (i >= units.length) return resolve();
      const next = units.slice(i, i + chunkSize).join('');
      textNode.nodeValue += next;
      i += chunkSize;
      setTimeout(step, charDelay);
    };
    step();
  });
}

// Recursively stream a DOM node (and children) into a destination parent
async function streamNode(srcNode, destParent, options) {
  const { charDelay, paragraphPause, chunkSize } = options;

  if (srcNode.nodeType === Node.TEXT_NODE) {
    const txt = srcNode.nodeValue || '';
    await streamTextTo(destParent, txt, charDelay, chunkSize);
    return;
  }

  if (srcNode.nodeType === Node.ELEMENT_NODE) {
    // Create a shallow clone of the element and copy attributes
    const tagName = srcNode.tagName.toLowerCase();
    const el = document.createElement(tagName);
    copyAttributes(srcNode, el);
    destParent.appendChild(el);

    // For void elements (e.g., br, img, hr, input), there are no children to stream
    const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
    if (voidTags.has(tagName)) {
      return;
    }

    // // If this is a code-like block, append its full text immediately for speed and readability
    // const codeLike = new Set(['code', 'pre', 'kbd', 'samp']);
    // if (codeLike.has(tagName)) {
    //   // Preserve text exactly
    //   el.textContent = srcNode.textContent || '';
    //   return;
    // }

    // Otherwise, stream children serially
    for (let i = 0; i < srcNode.childNodes.length; i++) {
      const child = srcNode.childNodes[i];
      await streamNode(child, el, options);
    }

    // Small pause after completing certain block-level elements for a natural feel
    const pauseAfterTags = new Set(['p', 'li', 'ul', 'ol', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
    if (pauseAfterTags.has(tagName) && paragraphPause > 0) {
      await delay(paragraphPause);
    }
  }
}

// Prepare HTML: parse safely in a detached container and trim code blocks
function prepareHtmlForStreaming(aiResponse) {
  const tempDiv = document.createElement('div');
  tempDiv.innerHTML = aiResponse;
  const codeTags = tempDiv.querySelectorAll('code');
  codeTags.forEach((codeTag) => {
    codeTag.textContent = (codeTag.textContent || '').trim();
  });
  return tempDiv;
}

// Render AI response using a simple typewriter-like HTML streaming approach
async function streamAiResponse($aiNewChatButton, $aiResponseMessage, aiResponse, $aiPrompt, options) {
  try {
    const { charDelay = 10, paragraphPause = 150, chunkSize = 2 } = options || {};

    // Reset container and create wrappers: content first, then actions
    const host = $aiResponseMessage[0];
    host.innerHTML = '';

    const contentDiv = document.createElement('div');
    contentDiv.setAttribute('data-ai-response-content', '');
    contentDiv.className = 'relative mb-3 text-[15px] leading-relaxed text-gray-900';

    const actionsDiv = document.createElement('div');
    actionsDiv.setAttribute('data-ai-response-actions', '');

    actionsDiv.className = 'hidden data-[reveal]:flex flex-wrap items-center';
    actionsDiv.innerHTML = `
      <button data-copy-ai-response-button type="button" class="relative flex justify-center items-center gap-x-3 size-8 text-sm text-gray-600 hover:bg-gray-100 rounded-full disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 before:absolute before:top-9 before:left-1/2 before:-translate-x-1/2 before:hidden before:content-[attr(data-tooltip)] before:w-max before:max-w-xs sm:before:max-w-lg before:z-[99999999999999] before:text-center before:py-1 before:px-2 before:bg-gray-900 before:text-[11px] before:leading-snug before:font-medium before:text-white before:rounded-lg before:shadow-sm hover:before:block" data-tooltip="${wp.i18n.__('Copy', 'generatify')}">
        <svg class="copy-default shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
          <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
        </svg>
        <svg class="copy-success hidden shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M20 6 9 17l-5-5" />
        </svg>
      </button>
    `;

    host.appendChild(contentDiv);
    host.appendChild(actionsDiv);

    // Prepare and stream HTML
    const prepared = prepareHtmlForStreaming(aiResponse);
    for (let i = 0; i < prepared.childNodes.length; i++) {
      const node = prepared.childNodes[i];
      await streamNode(node, contentDiv, { charDelay, paragraphPause, chunkSize });
    }

    // Reveal actions and re-enable inputs once streaming is done
    actionsDiv.setAttribute('data-reveal', '');
    actionsDiv.classList.remove('hidden');
  } catch (e) {
    $aiResponseMessage.html(aiResponse);
    console.error('Streaming AI response failed, rendered full response at once.', e);
  } finally {
    $aiNewChatButton.prop('disabled', false);
    $aiPrompt.prop('readonly', false).focus().trigger('change');
  }
}
