import { translate } from './i18nHelpers.js';
import { state, getCurrentPageState } from '../state/index.js';
import { getStorageItem, setStorageItem } from './storageHelpers.js';
import { NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js';
import { eventManager } from './EventManager.js';
import { bannerService } from '../managers/BannerService.js';
import { modalManager } from '../managers/ModalManager.js';
import { buildCivitaiUrl, normalizeCivitaiPageHost } from './civitaiUtils.js';
import { resolveSamplerScheduler, findMatchingWidgets } from './genParamsMapper.js';

const CIVITAI_HOST_INFO_BANNER_ID = 'civitai-host-preference';
const CIVITAI_HOST_INFO_BANNER_SEEN_KEY = 'civitai_host_info_banner_seen';

function getPreferredCivitaiHost() {
  return normalizeCivitaiPageHost(state?.global?.settings?.civitai_host);
}

function maybeRegisterCivitaiHostInfoBanner() {
  if (getStorageItem(CIVITAI_HOST_INFO_BANNER_SEEN_KEY, false)) {
    return;
  }

  setStorageItem(CIVITAI_HOST_INFO_BANNER_SEEN_KEY, true);

  bannerService.registerBanner(CIVITAI_HOST_INFO_BANNER_ID, {
    id: CIVITAI_HOST_INFO_BANNER_ID,
    title: translate(
      'settings.civitaiHostBanner.title',
      {},
      'Civitai host preference available'
    ),
    content: translate(
      'settings.civitaiHostBanner.content',
      {},
      'Civitai now uses civitai.com for SFW content and civitai.red for unrestricted content. You can change which site opens by default in Settings.'
    ),
    actions: [
      {
        text: translate('settings.civitaiHostBanner.openSettings', {}, 'Open Settings'),
        icon: 'fas fa-cog',
        action: 'open-settings-modal',
        type: 'primary',
      },
    ],
    dismissible: true,
    priority: 70,
    onRegister: (bannerElement) => {
      const button = bannerElement.querySelector('.banner-action[data-action="open-settings-modal"]');
      if (button) {
        button.addEventListener('click', (event) => {
          event.preventDefault();
          modalManager.showModal('settingsModal');
        });
      }
    },
  });
}

export function openCivitaiUrl(url) {
  if (!url) {
    return null;
  }

  maybeRegisterCivitaiHostInfoBanner();
  return window.open(url, '_blank', 'noopener,noreferrer');
}

async function copyExampleImagesValue(value, successKey, fallbackKey, paramsKey = 'path') {
  if (!value) {
    return false;
  }

  const params = { [paramsKey]: value };
  try {
    await navigator.clipboard.writeText(value);
    showToast(successKey, params, 'success');
    return true;
  } catch (clipboardErr) {
    console.warn('Clipboard API not available:', clipboardErr);
    showToast(fallbackKey, params, 'info');
    return false;
  }
}

function tryOpenExternalUri(uri) {
  try {
    const openedWindow = window.open(uri, '_blank', 'noopener,noreferrer');
    return openedWindow !== null;
  } catch (error) {
    console.warn('Failed to open external URI:', error);
    return false;
  }
}

/**
 * Utility function to copy text to clipboard with fallback for older browsers
 * @param {string} text - The text to copy to clipboard
 * @param {string} successMessage - Optional success message to show in toast
 * @returns {Promise<boolean>} - Promise that resolves to true if copy was successful
/**
 * Utility function to copy text to clipboard with fallback for older browsers
 * @param {string} text - The text to copy to clipboard
 * @param {string} successMessage - Optional success message to show in toast
 * @returns {Promise<boolean>} - Promise that resolves to true if copy was successful
 */
export async function copyToClipboard(text, successMessage = null) {
  const defaultSuccessMessage = successMessage || translate('uiHelpers.clipboard.copied', {}, 'Copied to clipboard');

  try {
    // Modern clipboard API
    if (navigator.clipboard && window.isSecureContext) {
      await navigator.clipboard.writeText(text);
    } else {
      // Fallback for older browsers
      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.style.position = 'absolute';
      textarea.style.left = '-99999px';
      document.body.appendChild(textarea);
      textarea.select();
      document.execCommand('copy');
      document.body.removeChild(textarea);
    }

    if (defaultSuccessMessage) {
      showToast('uiHelpers.clipboard.copied', {}, 'success');
    }
    return true;
  } catch (err) {
    console.error('Copy failed:', err);
    showToast('uiHelpers.clipboard.copyFailed', {}, 'error');
    return false;
  }
}

/**
 * Build a toast element (internal — not exported).
 * @param {string} message - Already-resolved message text
 * @param {string} type - Toast type (info/success/warning/error)
 * @returns {HTMLElement} The toast element (not yet attached to the DOM)
 */
function createToastElement(message, type) {
  const toast = document.createElement('div');
  toast.className = `toast toast-${type}`;
  toast.textContent = message;
  return toast;
}

/**
 * Attach a toast to the shared container, position it, and schedule its
 * dismissal (internal — not exported).
 * @param {HTMLElement} toast - The toast element to display
 * @param {number} durationMs - How long the toast stays visible
 * @param {Function} [onDismiss] - Optional callback fired once when dismissal begins
 * @returns {Function} Manual dismiss function (idempotent)
 */
function appendToast(toast, durationMs, onDismiss = null) {
  // Get or create toast container
  let toastContainer = document.querySelector('.toast-container');
  if (!toastContainer) {
    toastContainer = document.createElement('div');
    toastContainer.className = 'toast-container';
    document.body.append(toastContainer);
  }

  toastContainer.append(toast);

  // Calculate vertical position for stacked toasts
  const existingToasts = Array.from(toastContainer.querySelectorAll('.toast'));
  const toastIndex = existingToasts.indexOf(toast);
  const topOffset = 20; // Base offset from top
  const spacing = 10; // Space between toasts

  // Set position based on existing toasts
  toast.style.top = `${topOffset + (toastIndex * (toast.offsetHeight || 60 + spacing))}px`;

  let dismissed = false;
  const dismiss = () => {
    if (dismissed) return;
    dismissed = true;

    if (typeof onDismiss === 'function') {
      onDismiss();
    }

    toast.classList.remove('show');
    toast.addEventListener('transitionend', () => {
      toast.remove();

      // Reposition remaining toasts
      if (toastContainer) {
        const remainingToasts = Array.from(toastContainer.querySelectorAll('.toast'));
        remainingToasts.forEach((t, index) => {
          t.style.top = `${topOffset + (index * (t.offsetHeight || 60 + spacing))}px`;
        });

        // Remove container if empty
        if (remainingToasts.length === 0) {
          toastContainer.remove();
        }
      }
    });
  };

  requestAnimationFrame(() => {
    toast.classList.add('show');
    setTimeout(dismiss, durationMs);
  });

  return dismiss;
}

export function showToast(key, params = {}, type = 'info', fallback = null) {
  // Plain messages (contain spaces) are not i18n dot-notation keys — use verbatim
  // to avoid spurious "Translation key not found" warnings from i18next
  const isPlainMessage = typeof key === 'string' && /\s/.test(key);
  const message = isPlainMessage ? key : translate(key, params, fallback);
  const toast = createToastElement(message, type);

  // Set timeout based on type
  let duration = 2000; // Default (info)
  if (type === 'warning' || type === 'error') {
    duration = 5000;
  }

  appendToast(toast, duration);
}

/**
 * Show a toast with an action button (e.g. Undo) and an optional countdown.
 * The message accepts the same key/plain-string contract as showToast, so
 * callers may pass either an i18n key or an already-translated string.
 * @param {string} key - i18n key or plain message
 * @param {Object} [params] - i18n interpolation params
 * @param {string} [type] - Toast type (info/success/warning/error)
 * @param {Object} [options]
 * @param {string} [options.actionText] - Label for the action button (button omitted when empty)
 * @param {Function} [options.onAction] - Callback invoked at most once on button click
 * @param {number} [options.durationMs=20000] - How long the toast stays visible
 * @param {boolean} [options.countdown=true] - Show a ticking `(N)s` countdown
 */
export function showActionToast(key, params = {}, type = 'info', options = {}) {
  const { actionText, onAction, durationMs = 20000, countdown = true } = options;

  const isPlainMessage = typeof key === 'string' && /\s/.test(key);
  const message = isPlainMessage ? key : translate(key, params);
  const toast = createToastElement(message, type);

  let countdownInterval = null;
  const clearCountdown = () => {
    if (countdownInterval !== null) {
      clearInterval(countdownInterval);
      countdownInterval = null;
    }
  };

  // The interval must be cleared on EVERY dismiss path (timeout, countdown end,
  // manual button click) — the onDismiss hook covers the appendToast timeout path.
  const dismiss = appendToast(toast, durationMs, clearCountdown);

  let actionFired = false;
  if (actionText) {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'toast-action-btn';
    button.textContent = actionText;
    button.addEventListener('click', (event) => {
      event.preventDefault();
      // Guard against double-click firing the action twice
      if (actionFired) return;
      actionFired = true;

      clearCountdown();
      if (typeof onAction === 'function') {
        onAction();
      }
      dismiss();
    });
    toast.append(button);
  }

  if (countdown) {
    const countdownEl = document.createElement('span');
    countdownEl.className = 'toast-countdown';
    let remainingSeconds = Math.max(0, Math.ceil(durationMs / 1000));
    countdownEl.textContent = `(${remainingSeconds}s)`;
    toast.append(countdownEl);

    countdownInterval = setInterval(() => {
      remainingSeconds -= 1;
      countdownEl.textContent = `(${Math.max(remainingSeconds, 0)}s)`;
      if (remainingSeconds <= 0) {
        clearCountdown();
        dismiss();
      }
    }, 1000);
  }

  // Manual close button: hides the toast early without firing onAction. The
  // backend undo window keeps running and the batch is purged when it expires.
  const closeBtn = document.createElement('button');
  closeBtn.type = 'button';
  closeBtn.className = 'toast-close-btn';
  closeBtn.textContent = '×';
  closeBtn.setAttribute('aria-label', translate('common.actions.close'));
  closeBtn.addEventListener('click', (event) => {
    event.preventDefault();
    clearCountdown();
    dismiss();
  });
  toast.append(closeBtn);
}

export function restoreFolderFilter() {
  const activeFolder = getStorageItem('activeFolder');
  const folderTag = activeFolder && document.querySelector(`.tag[data-folder="${activeFolder}"]`);
  if (folderTag) {
    folderTag.classList.add('active');
    filterByFolder(activeFolder);
  }
}

const CYCLE_ORDER = ['auto', 'light', 'dark'];
const PRESET_NAMES = ['default', 'nord', 'midnight', 'monokai', 'dracula', 'solarized'];

export { CYCLE_ORDER, PRESET_NAMES };

export function initTheme() {
  const savedTheme = getStorageItem('theme') || 'auto';
  // Migrate deprecated presets
  let savedPreset = getStorageItem('theme_preset');
  if (savedPreset === 'gruvbox') {
    savedPreset = 'midnight';
    setStorageItem('theme_preset', 'midnight');
  }
  applyTheme(savedTheme);
  applyPreset(savedPreset || 'default');

  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
    const currentTheme = getStorageItem('theme') || 'auto';
    if (currentTheme === 'auto') {
      applyTheme('auto');
    }
  });
}

export function toggleTheme() {
  const currentTheme = getStorageItem('theme') || 'auto';
  const currentIndex = CYCLE_ORDER.indexOf(currentTheme);
  const nextIndex = (currentIndex + 1) % CYCLE_ORDER.length;
  const newTheme = CYCLE_ORDER[nextIndex];

  setStorageItem('theme', newTheme);
  applyTheme(newTheme);

  document.body.style.display = 'none';
  document.body.offsetHeight;
  document.body.style.display = '';

  return newTheme;
}

export function cyclePreset() {
  const currentPreset = getStorageItem('theme_preset') || 'default';
  const currentIndex = PRESET_NAMES.indexOf(currentPreset);
  const nextIndex = (currentIndex + 1) % PRESET_NAMES.length;
  const newPreset = PRESET_NAMES[nextIndex];

  setStorageItem('theme_preset', newPreset);
  applyPreset(newPreset);

  return newPreset;
}

export function setPreset(name) {
  if (!PRESET_NAMES.includes(name)) return;
  setStorageItem('theme_preset', name);
  applyPreset(name);
}

function applyTheme(theme) {
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const htmlElement = document.documentElement;

  htmlElement.removeAttribute('data-theme');

  if (theme === 'dark' || (theme === 'auto' && prefersDark)) {
    htmlElement.setAttribute('data-theme', 'dark');
    document.body.dataset.theme = 'dark';
  } else {
    htmlElement.setAttribute('data-theme', 'light');
    document.body.dataset.theme = 'light';
  }

  updateThemeToggleIcons(theme);
}

function applyPreset(preset) {
  document.documentElement.setAttribute('data-theme-preset', preset);
}

function updateThemeToggleIcons(theme) {
  const themeToggle = document.querySelector('.theme-toggle');
  if (!themeToggle) return;

  themeToggle.classList.remove('theme-light', 'theme-dark', 'theme-auto');
  themeToggle.classList.add(`theme-${theme}`);
}

function filterByFolder(folderPath) {
  document.querySelectorAll('.model-card').forEach(card => {
    card.style.display = card.dataset.folder === folderPath ? '' : 'none';
  });
}

export function openCivitaiByMetadata(civitaiId, versionId, modelName = null) {
  const url = buildCivitaiUrl({
    modelId: civitaiId,
    versionId,
    modelName,
    host: getPreferredCivitaiHost(),
  });

  if (url) {
    openCivitaiUrl(url);
  }
}

export function openCivitai(filePath) {
  const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
    ? window.CSS.escape(filePath)
    : filePath.replace(/["\\]/g, '\\$&');
  const loraCard = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
  if (!loraCard) return;

  const metaData = JSON.parse(loraCard.dataset.meta);
  const civitaiId = metaData.modelId;
  const versionId = metaData.id;
  const modelName = loraCard.dataset.name;

  openCivitaiByMetadata(civitaiId, versionId, modelName);
}

/**
 * Open a Hugging Face model page in a new tab
 * @param {string} hfUrl - The Hugging Face URL
 */
export function openHuggingFace(hfUrl) {
  if (!hfUrl) return;
  window.open(hfUrl, '_blank', 'noopener,noreferrer');
}

/**
 * Dynamically positions the search options panel and filter panel
 * based on the current layout and folder tags container height
 */
export function updatePanelPositions() {
  const searchOptionsPanel = document.getElementById('searchOptionsPanel');
  const filterPanel = document.getElementById('filterPanel');

  if (!searchOptionsPanel && !filterPanel) return;

  // Get the header element
  const header = document.querySelector('.app-header');
  if (!header) return;

  // Calculate the position based on the bottom of the header
  const headerRect = header.getBoundingClientRect();
  const topPosition = headerRect.bottom + 5; // Add 5px padding

  // Set the positions
  if (searchOptionsPanel) {
    searchOptionsPanel.style.top = `${topPosition}px`;
  }

  if (filterPanel) {
    filterPanel.style.top = `${topPosition}px`;
    // Clamp panel height to the viewport below the header
    filterPanel.style.maxHeight = `calc(100vh - ${topPosition + 10}px)`;
  }

  // Adjust panel horizontal position based on the search container
  const searchContainer = document.querySelector('.header-search');
  if (searchContainer) {
    const searchRect = searchContainer.getBoundingClientRect();

    // Position the search options panel aligned with the search container
    if (searchOptionsPanel) {
      searchOptionsPanel.style.right = `${window.innerWidth - searchRect.right}px`;
    }

    // Position the filter panel aligned with the filter button
    if (filterPanel) {
      const filterButton = document.getElementById('filterButton');
      if (filterButton) {
        const filterRect = filterButton.getBoundingClientRect();
        filterPanel.style.right = `${window.innerWidth - filterRect.right}px`;
      }
    }
  }
}

export function initBackToTop() {
  const button = document.getElementById('backToTopBtn');
  if (!button) return;

  // Get the scrollable container
  const scrollContainer = document.querySelector('.page-content');

  // Show/hide button based on scroll position
  const toggleBackToTop = () => {
    const scrollThreshold = window.innerHeight * 0.3;
    if (scrollContainer.scrollTop > scrollThreshold) {
      button.classList.add('visible');
    } else {
      button.classList.remove('visible');
    }
  };

  // Smooth scroll to top
  button.addEventListener('click', () => {
    scrollContainer.scrollTo({
      top: 0,
      behavior: 'smooth'
    });
  });

  // Listen for scroll events on the scrollable container
  scrollContainer.addEventListener('scroll', toggleBackToTop);

  // Initial check
  toggleBackToTop();
}

export function getNSFWLevelName(level) {
  if (level === 0) return 'Unknown';
  if (level >= 32) return 'Blocked';
  if (level >= 16) return 'XXX';
  if (level >= 8) return 'X';
  if (level >= 4) return 'R';
  if (level >= 2) return 'PG13';
  if (level >= 1) return 'PG';
  return 'Unknown';
}

function parseUsageTipNumber(value) {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value;
  }

  if (typeof value === 'string') {
    const parsed = parseFloat(value);
    if (Number.isFinite(parsed)) {
      return parsed;
    }
  }

  return null;
}

export function getLoraStrengthsFromUsageTips(usageTips = {}) {
  const parsedStrength = parseUsageTipNumber(usageTips.strength);
  const clipStrengthSource = usageTips.clip_strength ?? usageTips.clipStrength;
  const parsedClipStrength = parseUsageTipNumber(clipStrengthSource);

  return {
    strength: parsedStrength !== null ? parsedStrength : 1,
    hasStrength: parsedStrength !== null,
    clipStrength: parsedClipStrength,
    hasClipStrength: parsedClipStrength !== null,
  };
}

export function buildLoraSyntax(fileName, usageTips = {}) {
  const { strength, hasStrength, clipStrength, hasClipStrength } = getLoraStrengthsFromUsageTips(usageTips);

  const effectiveName = state.global.settings?.lora_syntax_format === 'legacy'
    ? fileName.split('/').pop()
    : fileName;

  if (hasClipStrength) {
    const modelStrength = hasStrength ? strength : 1;
    return `<lora:${effectiveName}:${modelStrength}:${clipStrength}>`;
  }

  return `<lora:${effectiveName}:${strength}>`;
}

export function copyLoraSyntax(card) {
  const usageTips = JSON.parse(card.dataset.usage_tips || "{}");
  const folder = card.dataset.folder || '';
  const loraName = folder ? `${folder}/${card.dataset.file_name}` : card.dataset.file_name;
  const baseSyntax = buildLoraSyntax(loraName, usageTips);

  // Check if trigger words should be included
  const includeTriggerWords = state.global.settings.include_trigger_words;

  if (!includeTriggerWords) {
    const message = translate('uiHelpers.lora.syntaxCopied', {}, 'LoRA syntax copied to clipboard');
    copyToClipboard(baseSyntax, message);
    return;
  }

  // Get trigger words from metadata
  const meta = card.dataset.meta ? JSON.parse(card.dataset.meta) : null;
  const trainedWords = meta?.trainedWords;

  if (
    !trainedWords ||
    !Array.isArray(trainedWords) ||
    trainedWords.length === 0
  ) {
    const message = translate('uiHelpers.lora.syntaxCopiedNoTriggerWords', {}, 'LoRA syntax copied to clipboard (no trigger words found)');
    copyToClipboard(baseSyntax, message);
    return;
  }

  let finalSyntax = baseSyntax;

  if (trainedWords.length === 1) {
    // Single group: append trigger words to the same line
    const triggers = trainedWords[0]
      .split(",")
      .map((word) => word.trim())
      .filter((word) => word);
    if (triggers.length > 0) {
      finalSyntax = `${baseSyntax}, ${triggers.join(", ")}`;
    }
    const message = translate('uiHelpers.lora.syntaxCopiedWithTriggerWords', {}, 'LoRA syntax with trigger words copied to clipboard');
    copyToClipboard(finalSyntax, message);
  } else {
    // Multiple groups: format with separators
    const groups = trainedWords
      .map((group) => {
        const triggers = group
          .split(",")
          .map((word) => word.trim())
          .filter((word) => word);
        return triggers.join(", ");
      })
      .filter((group) => group);

    if (groups.length > 0) {
      // Use separator between all groups except the first
      finalSyntax = baseSyntax + ", " + groups[0];
      for (let i = 1; i < groups.length; i++) {
        finalSyntax += `\n${"-".repeat(17)}\n${groups[i]}`;
      }
    }
    const message = translate('uiHelpers.lora.syntaxCopiedWithTriggerWordGroups', {}, 'LoRA syntax with trigger word groups copied to clipboard');
    copyToClipboard(finalSyntax, message);
  }
}

/**
 * Strip <lora:...> tags from prompt text and clean up residual punctuation/whitespace.
 * Handles both unescaped (<lora:...>) and HTML-escaped (&lt;lora:...&gt;) variants.
 * Cleans up artifacts like leading ", ", double commas, and extra whitespace.
 */
export function stripLoraTags(text) {
    return text
        .replace(/<lora:[^>]*>/gi, '')
        .replace(/&lt;lora:[^&]*&gt;/gi, '')
        .replace(/,(\s*,)+/g, ',')
        .replace(/^,\s*/, '')
        .replace(/,\s*$/, '')
        .replace(/\s{2,}/g, ' ')
        .trim();
}

async function fetchWorkflowRegistry() {
  try {
    const response = await fetch('/api/lm/get-registry');
    const registryData = await response.json();

    if (!registryData.success) {
      if (registryData.error === 'Standalone Mode Active') {
        showToast('toast.general.cannotInteractStandalone', {}, 'warning');
      } else if (registryData.error === 'Empty Registry') {
        showToast('uiHelpers.workflow.noSupportedNodes', {}, 'warning');
      } else {
        showToast('toast.general.failedWorkflowInfo', {}, 'error');
      }
      return null;
    }

    return registryData.data;
  } catch (error) {
    console.error('Failed to get registry:', error);
    showToast('uiHelpers.workflow.communicationFailed', {}, 'error');
    return null;
  }
}

function filterRegistryNodes(nodes = {}, predicate) {
  if (typeof nodes !== 'object' || nodes === null) {
    return {};
  }

  return Object.fromEntries(
    Object.entries(nodes).filter(([, node]) => {
      try {
        return predicate(node);
      } catch (error) {
        console.warn('Failed to evaluate registry node predicate', error);
        return false;
      }
    }),
  );
}

function getWidgetNames(node) {
  if (!node) {
    return [];
  }

  if (Array.isArray(node.widget_names)) {
    return node.widget_names;
  }

  if (node.capabilities && Array.isArray(node.capabilities.widget_names)) {
    return node.capabilities.widget_names;
  }

  return [];
}

function isNodeEnabled(node) {
  if (!node) {
    return false;
  }
  // ComfyUI node mode (LGraphEventMode): 0 = Always, 2 = Never, 4 = Bypass
  return node.mode === undefined || node.mode === 0;
}

function isAbsolutePath(path) {
  if (typeof path !== 'string') {
    return false;
  }

  return path.startsWith('/') || path.startsWith('\\') || /^[a-zA-Z]:[\\/]/.test(path);
}

async function ensureRelativeModelPath(modelPath, collectionType) {
  if (!modelPath || !isAbsolutePath(modelPath)) {
    return modelPath;
  }

  const fileName = modelPath.split(/[/\\]/).pop();
  if (!fileName) {
    return modelPath;
  }

  // Remove model file extension (.safetensors, .ckpt, .pt, .bin) for cleaner matching
  // Backend removes extensions from paths before matching, so search term should not include extension
  const searchTerm = fileName.replace(/\.(safetensors|ckpt|pt|bin)$/i, '');

  try {
    const response = await fetch(`/api/lm/${collectionType}/relative-paths?search=${encodeURIComponent(searchTerm)}&limit=10`);
    if (!response.ok) {
      return modelPath;
    }
    const data = await response.json();
    const relativePaths = Array.isArray(data?.relative_paths) ? data.relative_paths : [];
    if (relativePaths.length === 0) {
      return modelPath;
    }
    const exactMatch = relativePaths.find((path) => path.endsWith(fileName));
    return exactMatch || relativePaths[0] || modelPath;
  } catch (error) {
    console.warn('LoRA Manager: failed to resolve relative path for model', error);
    return modelPath;
  }
}

/**
 * Sends LoRA syntax to the active ComfyUI workflow
 * @param {string} loraSyntax - The LoRA syntax to send
 * @param {boolean} replaceMode - Whether to replace existing LoRAs (true) or append (false)
 * @param {string} syntaxType - The type of syntax ('lora' or 'recipe')
 * @returns {Promise<boolean>} - Whether the operation was successful
 */
export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntaxType = 'lora', onComplete = null) {
  const registry = await fetchWorkflowRegistry();
  if (!registry) {
    return false;
  }

  const loraNodes = filterRegistryNodes(registry.nodes, (node) => {
    if (!isNodeEnabled(node)) {
      return false;
    }
    if (node.capabilities && typeof node.capabilities === 'object') {
      if (node.capabilities.supports_lora === true) {
        return true;
      }
    }
    return typeof node.type === 'number' && node.type > 0;
  });

  const nodeKeys = Object.keys(loraNodes);
  if (nodeKeys.length === 0) {
    showToast('uiHelpers.workflow.noSupportedNodes', {}, 'warning');
    return false;
  }

  if (nodeKeys.length === 1) {
    const result = await sendLoraToNodes([nodeKeys[0]], loraNodes, loraSyntax, replaceMode, syntaxType);
    if (result && typeof onComplete === 'function') onComplete();
    return result;
  }

  const actionType =
    syntaxType === 'recipe'
      ? translate('uiHelpers.nodeSelector.recipe', {}, 'Recipe')
      : translate('uiHelpers.nodeSelector.lora', {}, 'LoRA');
  const actionMode = replaceMode
    ? translate('uiHelpers.nodeSelector.replace', {}, 'Replace')
    : translate('uiHelpers.nodeSelector.append', {}, 'Append');

  showNodeSelector(loraNodes, {
    actionType,
    actionMode,
    onSend: async (selectedNodeIds) => {
      const result = await sendLoraToNodes(selectedNodeIds, loraNodes, loraSyntax, replaceMode, syntaxType);
      if (result && typeof onComplete === 'function') onComplete();
      return result;
    },
  });
  return true;
}

export async function sendModelPathToWorkflow(modelPath, options) {
  const {
    widgetName,
    collectionType = 'checkpoints',
    actionTypeText = 'Checkpoint',
    successMessage = 'Updated workflow node',
    failureMessage = 'Failed to update workflow node',
    missingNodesMessage = 'No compatible nodes available in the current workflow',
    missingTargetMessage = 'No target node selected',
  } = options;

  if (!widgetName) {
    console.warn('LoRA Manager: widget name is required to send model to workflow');
    return false;
  }

  const relativePath = await ensureRelativeModelPath(modelPath, collectionType);

  const registry = await fetchWorkflowRegistry();
  if (!registry) {
    return false;
  }

  const targetNodes = filterRegistryNodes(registry.nodes, (node) => {
    if (!isNodeEnabled(node)) {
      return false;
    }
    const widgetNames = getWidgetNames(node);
    return widgetNames.includes(widgetName);
  });

  const nodeKeys = Object.keys(targetNodes);
  if (nodeKeys.length === 0) {
    showToast(missingNodesMessage, {}, 'warning');
    return false;
  }

  const actionType = actionTypeText;
  const actionMode = translate('uiHelpers.nodeSelector.replace', {}, 'Replace');

  const messages = {
    successMessage,
    failureMessage,
    missingTargetMessage,
  };

  const handleSend = (selectedNodeIds) =>
    sendWidgetValueToNodes(selectedNodeIds, targetNodes, widgetName, relativePath, messages);

  if (nodeKeys.length === 1) {
    return await handleSend([nodeKeys[0]]);
  }

  showNodeSelector(targetNodes, {
    actionType,
    actionMode,
    onSend: handleSend,
  });
  return true;
}

/**
 * Send LoRA to specific nodes
 * @param {Array|undefined} nodeIds - Array of node IDs or undefined for desktop mode
 * @param {string} loraSyntax - The LoRA syntax to send
 * @param {boolean} replaceMode - Whether to replace existing LoRAs
 * @param {string} syntaxType - The type of syntax ('lora' or 'recipe')
 */
function resolveNodeReference(nodeKey, nodesMap) {
  if (!nodeKey) {
    return null;
  }

  const directMatch = nodesMap?.[nodeKey];
  if (directMatch) {
    return {
      node_id: directMatch.id,
      graph_id: directMatch.graph_id ?? null,
    };
  }

  if (typeof nodeKey === 'string' && nodeKey.includes(':')) {
    const [graphId, ...rest] = nodeKey.split(':');
    const nodeIdPart = rest.join(':');
    const numericNodeId = Number(nodeIdPart);
    return {
      node_id: Number.isNaN(numericNodeId) ? nodeIdPart : numericNodeId,
      graph_id: graphId || null,
    };
  }

  const numericId = Number(nodeKey);
  return {
    node_id: Number.isNaN(numericId) ? nodeKey : numericId,
    graph_id: null,
  };
}

async function sendLoraToNodes(nodeIds, nodesMap, loraSyntax, replaceMode, syntaxType) {
  try {
    // Call the backend API to update the lora code
    const requestBody = {
      lora_code: loraSyntax,
      mode: replaceMode ? 'replace' : 'append'
    };

    if (Array.isArray(nodeIds)) {
      const references = nodeIds
        .map((nodeKey) => resolveNodeReference(nodeKey, nodesMap))
        .filter((reference) => reference && reference.node_id !== undefined);

      if (references.length > 0) {
        requestBody.node_ids = references;
      }
    } else if (nodeIds) {
      const reference = resolveNodeReference(nodeIds, nodesMap);
      if (reference) {
        requestBody.node_ids = [reference];
      }
    }

    const response = await fetch('/api/lm/update-lora-code', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(requestBody)
    });

    const result = await response.json();

    if (result.success) {
      // Use different toast messages based on syntax type
      if (syntaxType === 'recipe') {
        const messageKey = replaceMode ?
          'uiHelpers.workflow.recipeReplaced' :
          'uiHelpers.workflow.recipeAdded';
        showToast(messageKey, {}, 'success');
      } else {
        const messageKey = replaceMode ?
          'uiHelpers.workflow.loraReplaced' :
          'uiHelpers.workflow.loraAdded';
        showToast(messageKey, {}, 'success');
      }
      return true;
    } else {
      const messageKey = syntaxType === 'recipe' ?
        'uiHelpers.workflow.recipeFailedToSend' :
        'uiHelpers.workflow.loraFailedToSend';
      showToast(messageKey, {}, 'error');
      return false;
    }
  } catch (error) {
    console.error('Failed to send to workflow:', error);
    const messageKey = syntaxType === 'recipe' ?
      'uiHelpers.workflow.recipeFailedToSend' :
      'uiHelpers.workflow.loraFailedToSend';
    showToast(messageKey, {}, 'error');
    return false;
  }
}

async function sendWidgetValueToNodes(nodeIds, nodesMap, widgetName, value, messages = {}) {
  const {
    successMessage = 'Updated workflow node',
    failureMessage = 'Failed to update workflow node',
    missingTargetMessage = 'No target node selected',
    silent = false,
  } = messages;

  const targetIds = Array.isArray(nodeIds) ? nodeIds : [];
  if (targetIds.length === 0) {
    if (!silent) showToast(missingTargetMessage, {}, 'warning');
    return false;
  }

  const references = targetIds
    .map((nodeKey) => resolveNodeReference(nodeKey, nodesMap))
    .filter((reference) => reference && reference.node_id !== undefined);

  if (references.length === 0) {
    if (!silent) showToast(missingTargetMessage, {}, 'warning');
    return false;
  }

  try {
    const response = await fetch('/api/lm/update-node-widget', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        widget_name: widgetName,
        value,
        node_ids: references,
      }),
    });

    const result = await response.json();
    if (result.success) {
      if (!silent) showToast(successMessage, {}, 'success');
      return true;
    }

    const errorMessage = result?.error || failureMessage;
    if (!silent) showToast(errorMessage, {}, 'error');
    return false;
  } catch (error) {
    console.error('Failed to send widget value to workflow:', error);
    if (!silent) showToast(failureMessage, {}, 'error');
    return false;
  }
}

async function sendTextToNodes(nodeIds, nodesMap, text, mode, messages = {}) {
  const {
    successMessage = 'Updated workflow node',
    failureMessage = 'Failed to update workflow node',
    missingTargetMessage = 'No target node selected',
  } = messages;

  const targetIds = Array.isArray(nodeIds) ? nodeIds : [];
  if (targetIds.length === 0) {
    showToast(missingTargetMessage, {}, 'warning');
    return false;
  }

  const references = targetIds
    .map((nodeKey) => resolveNodeReference(nodeKey, nodesMap))
    .filter((reference) => reference && reference.node_id !== undefined);

  if (references.length === 0) {
    showToast(missingTargetMessage, {}, 'warning');
    return false;
  }

  try {
    const response = await fetch('/api/lm/update-node-widget', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        action: 'inject_text',
        value: text,
        mode: mode || 'append',
        node_ids: references,
      }),
    });

    const result = await response.json();
    if (result.success) {
      showToast(successMessage, {}, 'success');
      return true;
    }

    const errorMessage = result?.error || failureMessage;
    showToast(errorMessage, {}, 'error');
    return false;
  } catch (error) {
    console.error('Failed to send text to workflow:', error);
    showToast(failureMessage, {}, 'error');
    return false;
  }
}

export async function sendEmbeddingToWorkflow(embeddingCode, onComplete = null) {
  const registry = await fetchWorkflowRegistry();
  if (!registry) {
    return false;
  }

  const textNodes = filterRegistryNodes(registry.nodes, (node) => {
    if (!isNodeEnabled(node)) {
      return false;
    }
    if (node.capabilities?.text_widget_connected === true) {
      return false;
    }
    return (
      node.capabilities?.has_text_widget === true ||
      node.marker_role === "send_prompt_target"
    );
  });

  const nodeKeys = Object.keys(textNodes);
  if (nodeKeys.length === 0) {
    showToast(
      translate(
        'uiHelpers.workflow.noPromptTargets',
        {},
        'No compatible prompt targets in the workflow.\nRight-click a node in ComfyUI → Mark as → Send Prompt Target'
      ),
      {},
      'warning'
    );
    return false;
  }

  const messages = {
    successMessage: translate('uiHelpers.workflow.embeddingAdded', {}, 'Embedding added to workflow'),
    failureMessage: translate('uiHelpers.workflow.embeddingFailed', {}, 'Failed to add embedding'),
    missingTargetMessage: translate('uiHelpers.workflow.noTargetNodeSelected', {}, 'No target node selected'),
  };

  const handleSend = async (selectedNodeIds) => {
    const result = await sendTextToNodes(selectedNodeIds, textNodes, embeddingCode, 'append', messages);
    if (result && typeof onComplete === 'function') onComplete();
    return result;
  };

  if (nodeKeys.length === 1) {
    return await handleSend([nodeKeys[0]]);
  }

  const actionType = translate('uiHelpers.nodeSelector.embedding', {}, 'Embedding');

  showNodeSelector(textNodes, {
    actionType,
    actionMode: '',
    onSend: handleSend,
  });
  return true;
}

/**
 * Send prompt text to workflow text-capable nodes (replaces existing content).
 * Uses the same target node discovery as sendEmbeddingToWorkflow.
 * @param {string} promptText - The prompt/negative prompt text to send
 * @param {Object} [options] - Optional messages overrides
 * @param {string} [options.actionTypeText] - Label for the action type (default "Prompt")
 * @param {string} [options.successMessage] - Success toast message
 * @param {string} [options.failureMessage] - Failure toast message
 * @param {string} [options.missingNodesMessage] - No nodes warning message
 * @param {string} [options.missingTargetMessage] - No target selected warning message
 * @returns {Promise<boolean>} Whether the send succeeded
 */
export async function sendPromptToWorkflow(promptText, options = {}) {
  const registry = await fetchWorkflowRegistry();
  if (!registry) {
    return false;
  }

  const textNodes = filterRegistryNodes(registry.nodes, (node) => {
    if (!isNodeEnabled(node)) {
      return false;
    }
    // A node whose text widget is backed by a connected input cannot have its
    // text changed via the widget — execution reads the linked input.
    if (node.capabilities?.text_widget_connected === true) {
      return false;
    }
    return (
      node.capabilities?.has_text_widget === true ||
      node.marker_role === "send_prompt_target"
    );
  });

  const nodeKeys = Object.keys(textNodes);
  if (nodeKeys.length === 0) {
    const defaultHint = translate(
      'uiHelpers.workflow.noPromptTargets',
      {},
      'No compatible prompt targets in the workflow.\nRight-click a node in ComfyUI → Mark as → Send Prompt Target'
    );
    showToast(options.missingNodesMessage || defaultHint, {}, 'warning');
    return false;
  }

  const messages = {
    successMessage: options.successMessage || translate('uiHelpers.workflow.promptSent', {}, 'Prompt sent to workflow'),
    failureMessage: options.failureMessage || translate('uiHelpers.workflow.promptFailed', {}, 'Failed to send prompt'),
    missingTargetMessage: options.missingTargetMessage || translate('uiHelpers.workflow.noTargetNodeSelected', {}, 'No target node selected'),
  };

  const handleSend = (selectedNodeIds) =>
    sendTextToNodes(selectedNodeIds, textNodes, promptText, 'replace', messages);

  if (nodeKeys.length === 1) {
    return await handleSend([nodeKeys[0]]);
  }

  const actionType = options.actionTypeText || translate('uiHelpers.nodeSelector.prompt', {}, 'Prompt');

  showNodeSelector(textNodes, {
    actionType,
    actionMode: translate('uiHelpers.nodeSelector.replace', {}, 'Replace'),
    onSend: handleSend,
  });
  return true;
}

/**
 * Send generation parameters (seed, steps, cfg, sampler, scheduler) to
 * workflow nodes that have been marked with "Send Gen Params Target".
 *
 * @param {Object} genParams - Raw gen_params from recipe or showcase metadata
 * @returns {Promise<boolean>} Whether the send succeeded
 */
export async function sendGenParamsToWorkflow(genParams) {
  if (!genParams || typeof genParams !== 'object') {
    showToast('No generation parameters to send', {}, 'warning');
    return false;
  }

  // 1. Extract relevant params (skip prompt, negative_prompt, clip_skip, denoising_strength)
  const raw = {
    seed: genParams.seed,
    steps: genParams.steps,
    cfg: genParams.cfg_scale,
  };

  // 2. Resolve sampler/scheduler
  const resolved = resolveSamplerScheduler(genParams.sampler);
  if (resolved) {
    if (resolved.sampler) raw.sampler = resolved.sampler;
    if (resolved.scheduler) raw.scheduler = resolved.scheduler;
  }

  // Check if we have anything to send
  const hasAny = Object.values(raw).some(v => v !== undefined && v !== null && v !== '');
  if (!hasAny) {
    showToast('No sendable parameters found', {}, 'warning');
    return false;
  }

  // 3. Fetch workflow registry
  const registry = await fetchWorkflowRegistry();
  if (!registry) {
    return false;
  }

  // 4. Filter nodes by marker_role === "send_gen_params"
  const targetNodes = filterRegistryNodes(registry.nodes, (node) => {
    return node.marker_role === 'send_gen_params' && isNodeEnabled(node);
  });

  const nodeKeys = Object.keys(targetNodes);
  if (nodeKeys.length === 0) {
    showToast(
      'No node marked as Send Gen Params Target.\nRight-click a node in ComfyUI → Mark as → Send Gen Params Target',
      {},
      'warning'
    );
    return false;
  }

  // 5. For each candidate node, find matching widgets
  // Also collect widget_names from registry for matching
  const sendToNode = async (nodeIds) => {
    const targetIds = Array.isArray(nodeIds) ? nodeIds : [nodeIds];
    let allSuccess = true;
    let totalSent = 0;
    let totalFailed = 0;

    for (const nodeKey of targetIds) {
      const node = targetNodes[nodeKey];
      if (!node) continue;

      const widgetNames = getWidgetNames(node);
      const updates = findMatchingWidgets(widgetNames, raw, node.type_name);

      if (updates.length === 0) {
        showToast(`Node "${node.title || node.type}" has no matching widgets for these parameters`, {}, 'warning');
        allSuccess = false;
        continue;
      }

      // Send each widget value sequentially
      for (const update of updates) {
        const success = await sendWidgetValueToNodes(
          [nodeKey],
          targetNodes,
          update.widgetName,
          update.value,
          {
            silent: true,
          }
        );
        if (success) {
          totalSent++;
        } else {
          totalFailed++;
          allSuccess = false;
        }
      }
    }

    // Show single summary toast
    if (totalSent > 0 && totalFailed === 0) {
      showToast(`Sent ${totalSent} parameter${totalSent > 1 ? 's' : ''} to workflow`, {}, 'success');
    } else if (totalFailed > 0 && totalSent > 0) {
      showToast(`Partially updated (${totalSent} ok, ${totalFailed} failed)`, {}, 'warning');
    } else if (totalFailed > 0) {
      showToast('Failed to update parameters', {}, 'error');
    }
    return allSuccess;
  };

  // 6. If multiple nodes, show node selector; otherwise send directly
  if (nodeKeys.length === 1) {
    return await sendToNode([nodeKeys[0]]);
  }

  showNodeSelector(targetNodes, {
    actionType: 'Gen Params',
    actionMode: 'Update',
    onSend: sendToNode,
    enableSendAll: true,
  });
  return true;
}

// Global variable to track active node selector state
let nodeSelectorState = {
  isActive: false,
  clickHandler: null,
  selectorClickHandler: null,
  currentNodes: {},
  onSend: null,
  enableSendAll: true,
};

/**
 * Show node selector popup near mouse position
 * @param {Object} nodes - Registry nodes data
 * @param {Object} options - Configuration for display and actions
 * @param {string} options.actionType - Display label for the action type (e.g. LoRA)
 * @param {string} options.actionMode - Display label for the action mode (e.g. Replace)
 * @param {Function} options.onSend - Callback invoked with selected node ids
 * @param {boolean} [options.enableSendAll=true] - Whether to show the "send to all" option
 */
function showNodeSelector(nodes, options = {}) {
  const selector = document.getElementById('nodeSelector');
  if (!selector) return;

  // Clean up any existing state
  hideNodeSelector();

  const safeNodes = nodes || {};
  const onSend = typeof options.onSend === 'function' ? options.onSend : null;
  if (!onSend) {
    console.warn('LoRA Manager: node selector invoked without send handler');
    return;
  }

  nodeSelectorState.currentNodes = safeNodes;
  nodeSelectorState.onSend = onSend;
  nodeSelectorState.enableSendAll = options.enableSendAll !== false;

  // Generate node list HTML with icons and proper colors
  const nodeItems = Object.entries(safeNodes)
    .sort(([, a], [, b]) => a.type - b.type || a.id - b.id)
    .map(([nodeKey, node]) => {
    const iconClass = NODE_TYPE_ICONS[node.type] || 'fas fa-question-circle';
    const bgColor = node.bgcolor || DEFAULT_NODE_COLOR;
    const graphLabel = node.graph_name ? ` (${node.graph_name})` : '';

    return `
      <div class="node-item" data-node-id="${nodeKey}">
        <div class="node-icon-indicator" style="background-color: ${bgColor}">
          <i class="${iconClass}"></i>
        </div>
        <span>#${node.id}${graphLabel} ${node.title}</span>
      </div>
    `;
  }).join('');

  // Add header with action mode indicator
  const actionType = options.actionType ?? translate('uiHelpers.nodeSelector.lora', {}, 'LoRA');
  const actionMode = options.actionMode ?? translate('uiHelpers.nodeSelector.replace', {}, 'Replace');
  const selectTargetNodeText = translate('uiHelpers.nodeSelector.selectTargetNode', {}, 'Select target node');
  const sendToAllText = translate('uiHelpers.nodeSelector.sendToAll', {}, 'Send to All');

  const sendAllMarkup = nodeSelectorState.enableSendAll
    ? `
    <div class="node-item send-all-item" data-action="send-all">
      <div class="node-icon-indicator all-nodes">
        <i class="fas fa-broadcast-tower"></i>
      </div>
      <span>${sendToAllText}</span>
    </div>`
    : '';

  selector.innerHTML = `
    <div class="node-selector-header">
      <span class="selector-action-type">${actionMode} ${actionType}</span>
      <span class="selector-instruction">${selectTargetNodeText}</span>
    </div>
    ${nodeItems}
    ${sendAllMarkup}
  `;

  // Position near mouse
  positionNearMouse(selector);

  // Show selector
  selector.style.display = 'block';
  nodeSelectorState.isActive = true;

  // Update event manager state
  eventManager.setState('nodeSelectorActive', true);

  // Setup event listeners with proper cleanup through event manager
  setupNodeSelectorEvents(selector);
}

/**
 * Setup event listeners for node selector using event manager
 * @param {HTMLElement} selector - The selector element
 */
function setupNodeSelectorEvents(selector) {
  // Clean up any existing event listeners
  cleanupNodeSelectorEvents();

  // Register click outside handler with event manager
  eventManager.addHandler('click', 'nodeSelector-outside', (e) => {
    if (!selector.contains(e.target)) {
      hideNodeSelector();
      return true; // Stop propagation
    }
  }, {
    priority: 200, // High priority to handle before other click handlers
    onlyWhenNodeSelectorActive: true
  });

  // Register node selection handler with event manager  
  eventManager.addHandler('click', 'nodeSelector-selection', async (e) => {
    const nodeItem = e.target.closest('.node-item');
    if (!nodeItem) return false; // Continue with other handlers

    const onSend = nodeSelectorState.onSend;
    if (typeof onSend !== 'function') {
      hideNodeSelector();
      return true;
    }

    e.stopPropagation();

    const action = nodeItem.dataset.action;
    const nodeId = nodeItem.dataset.nodeId;
    const nodes = nodeSelectorState.currentNodes || {};

    try {
      if (action === 'send-all') {
        if (!nodeSelectorState.enableSendAll) {
          return true;
        }
        const allNodeIds = Object.keys(nodes);
        await onSend(allNodeIds);
      } else if (nodeId) {
        await onSend([nodeId]);
      }
    } finally {
      hideNodeSelector();
    }

    return true; // Stop propagation
  }, {
    priority: 150, // High priority but lower than outside click
    targetSelector: '#nodeSelector',
    onlyWhenNodeSelectorActive: true
  });
}

/**
 * Clean up node selector event listeners
 */
function cleanupNodeSelectorEvents() {
  // Remove event handlers from event manager
  eventManager.removeHandler('click', 'nodeSelector-outside');
  eventManager.removeHandler('click', 'nodeSelector-selection');

  // Clear legacy references
  nodeSelectorState.clickHandler = null;
  nodeSelectorState.selectorClickHandler = null;
}

/**
 * Hide node selector
 */
function hideNodeSelector() {
  const selector = document.getElementById('nodeSelector');
  if (selector) {
    selector.style.display = 'none';
    selector.innerHTML = ''; // Clear content to prevent memory leaks
  }

  // Clean up event listeners
  cleanupNodeSelectorEvents();
  nodeSelectorState.isActive = false;
  nodeSelectorState.currentNodes = {};
  nodeSelectorState.onSend = null;
  nodeSelectorState.enableSendAll = true;

  // Update event manager state
  eventManager.setState('nodeSelectorActive', false);
}

/**
 * Position element near mouse cursor
 * @param {HTMLElement} element - Element to position
 */
function positionNearMouse(element) {
  // Get current mouse position from last mouse event or use default
  const mouseX = window.lastMouseX || window.innerWidth / 2;
  const mouseY = window.lastMouseY || window.innerHeight / 2;

  // Show element temporarily to get dimensions
  element.style.visibility = 'hidden';
  element.style.display = 'block';

  const rect = element.getBoundingClientRect();
  const viewportWidth = document.documentElement.clientWidth;
  const viewportHeight = document.documentElement.clientHeight;

  // Calculate position with offset from mouse
  let x = mouseX + 10;
  let y = mouseY + 10;

  // Ensure element doesn't go offscreen
  if (x + rect.width > viewportWidth) {
    x = mouseX - rect.width - 10;
  }

  if (y + rect.height > viewportHeight) {
    y = mouseY - rect.height - 10;
  }

  // Apply position
  element.style.left = `${x}px`;
  element.style.top = `${y}px`;
  element.style.visibility = 'visible';
}

/**
 * Initialize mouse tracking for positioning elements
 */
export function initializeMouseTracking() {
  // Register mouse tracking with event manager
  eventManager.addHandler('mousemove', 'uiHelpers-mouseTracking', (e) => {
    window.lastMouseX = e.clientX;
    window.lastMouseY = e.clientY;
  }, {
    priority: 10 // Low priority since this is just tracking
  });
}

// Initialize mouse tracking when module loads
initializeMouseTracking();

/**
 * Opens the example images folder for a specific model
 * @param {string} modelHash - The SHA256 hash of the model
 */
export async function openExampleImagesFolder(modelHash) {
  try {
    const response = await fetch('/api/lm/open-example-images-folder', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model_hash: modelHash
      })
    });

    const result = await response.json();

    if (result.success) {
      if (result.mode === 'clipboard' && result.path) {
        await copyExampleImagesValue(
          result.path,
          'uiHelpers.exampleImages.copiedPath',
          'uiHelpers.exampleImages.clipboardFallback',
          'path'
        );
        return true;
      }

      if (result.mode === 'uri' && result.uri) {
        const opened = tryOpenExternalUri(result.uri);
        if (!opened) {
          await copyExampleImagesValue(
            result.uri,
            'uiHelpers.exampleImages.copiedUri',
            'uiHelpers.exampleImages.uriClipboardFallback',
            'uri'
          );
        } else {
          showToast('uiHelpers.exampleImages.opened', {}, 'success');
        }
        return true;
      }

      showToast('uiHelpers.exampleImages.opened', {}, 'success');
      return true;
    } else {
      showToast('uiHelpers.exampleImages.failedToOpen', {}, 'error');
      return false;
    }
  } catch (error) {
    console.error('Failed to open example images folder:', error);
    showToast('uiHelpers.exampleImages.failedToOpen', {}, 'error');
    return false;
  }
}

/**
 * Set up a paste handler on a textarea that automatically appends a newline
 * after pasted content that looks like a URL (http/https). This lets users
 * paste multiple URLs one after another without manually pressing Enter.
 * @param {string} textareaId - The id of the textarea element
 */
export function setupAutoNewlineOnPaste(textareaId) {
  const el = document.getElementById(textareaId);
  if (!el || el.tagName !== 'TEXTAREA') return;

  el.addEventListener('paste', (e) => {
    const pastedText = (e.clipboardData || window.clipboardData).getData('text');
    // Only apply to text that starts with http:// or https://
    if (/^https?:\/\//.test(pastedText) && !pastedText.endsWith('\n')) {
      e.preventDefault();

      const start = el.selectionStart;
      const end = el.selectionEnd;
      const text = el.value;
      const before = text.substring(0, start);
      const after = text.substring(end);

      // Append newline after the pasted URL
      const modifiedText = pastedText + '\n';
      el.value = before + modifiedText + after;

      // Move cursor to just after the inserted text
      const newCursorPos = start + modifiedText.length;
      el.selectionStart = el.selectionEnd = newCursorPos;

      // Trigger input event so any listeners stay in sync
      el.dispatchEvent(new Event('input', { bubbles: true }));
    }
    // Non-URL text or text already ending with \n — let default paste happen
  });
}
