/**
 * Shared calculator utilities and i18n fallbacks.
 */

const TEXT_DOMAIN = 'vareon-calculator-suite-lite';

/**
 * Translate a string when wp.i18n is available.
 */
export function __( text, domain = TEXT_DOMAIN ) {
  if ( typeof wp !== 'undefined' && wp.i18n && typeof wp.i18n.__ === 'function' ) {
    return wp.i18n.__( text, domain );
  }
  return text;
}

/**
 * Translate a string with context when wp.i18n is available.
 */
export function _x( text, context, domain = TEXT_DOMAIN ) {
  if ( typeof wp !== 'undefined' && wp.i18n && typeof wp.i18n._x === 'function' ) {
    return wp.i18n._x( text, context, domain );
  }
  return text;
}

/**
 * Translate singular and plural strings when wp.i18n is available.
 */
export function _n( single, plural, number, domain = TEXT_DOMAIN ) {
  if ( typeof wp !== 'undefined' && wp.i18n && typeof wp.i18n._n === 'function' ) {
    return wp.i18n._n( single, plural, number, domain );
  }
  return number === 1 ? single : plural;
}

/**
 * Translate contextual singular and plural strings when wp.i18n is available.
 */
export function _nx( single, plural, number, context, domain = TEXT_DOMAIN ) {
  if ( typeof wp !== 'undefined' && wp.i18n && typeof wp.i18n._nx === 'function' ) {
    return wp.i18n._nx( single, plural, number, context, domain );
  }
  return number === 1 ? single : plural;
}

/**
 * Format translated strings with or without wp.i18n.
 */
export function sprintf( format, ...args ) {
  if ( typeof wp !== 'undefined' && wp.i18n && typeof wp.i18n.sprintf === 'function' ) {
    return wp.i18n.sprintf( format, ...args );
  }
  // Basic fallback: handle both %s/%d and positional %1$s/%2$s formats
  let i = 0;
  return format
    .replace( /%(\d+)\$[sd]/g, ( match, pos ) => args[ parseInt( pos, 10 ) - 1 ] ?? '' )
    .replace( /%[sd]/g, () => args[i++] ?? '' );
}

export const ValidationHelpers = {
  getToastRoot(elementOrContainer) {
    if (!elementOrContainer) {
      return null;
    }

    if (elementOrContainer.classList?.contains('vareoncalc-toast')) {
      return elementOrContainer.closest('.vareoncalc-root') || elementOrContainer.parentElement;
    }

    if (elementOrContainer.classList?.contains('vareoncalc-root')) {
      return elementOrContainer;
    }

    if (typeof elementOrContainer.closest === 'function') {
      const root = elementOrContainer.closest('.vareoncalc-root');
      if (root) {
        return root;
      }
    }

    return elementOrContainer;
  },

  ensureToast(rootOrElement) {
    const root = this.getToastRoot(rootOrElement);
    if (!root || typeof root.querySelector !== 'function') {
      return null;
    }

    let toastEl = root.querySelector('.vareoncalc-toast');
    if (!toastEl) {
      toastEl = document.createElement('div');
      toastEl.className = 'vareoncalc-toast';
      toastEl.setAttribute('role', 'alert');
      toastEl.setAttribute('aria-live', 'polite');
      root.appendChild(toastEl);
    }

    return toastEl;
  },

  showToast(rootOrElement, message, options = {}) {
    const toastEl = this.ensureToast(rootOrElement);
    if (!toastEl) {
      return;
    }

    const type = options.type === 'success' || options.type === 'info' || options.type === 'warning' || options.type === 'error'
      ? options.type
      : 'error';
    toastEl.classList.remove('vareoncalc-toast--success', 'vareoncalc-toast--info', 'vareoncalc-toast--warning', 'vareoncalc-toast--error');
    toastEl.classList.add(`vareoncalc-toast--${type}`);

    const durationMs = Number.isFinite(options.durationMs) ? options.durationMs : 3000;

    if (toastEl._vareoncalcToastTimeoutId) {
      clearTimeout(toastEl._vareoncalcToastTimeoutId);
      toastEl._vareoncalcToastTimeoutId = null;
    }

    toastEl.textContent = message ?? '';
    toastEl.classList.add('vareoncalc-toast--visible');

    if (durationMs > 0) {
      toastEl._vareoncalcToastTimeoutId = setTimeout(() => {
        this.hideToast(toastEl);
      }, durationMs);
    }
  },

  hideToast(rootOrElement) {
    if (!rootOrElement) {
      return;
    }

    let toastEl = null;
    if (rootOrElement.classList?.contains('vareoncalc-toast')) {
      toastEl = rootOrElement;
    } else if (rootOrElement.querySelector) {
      toastEl = rootOrElement.querySelector('.vareoncalc-toast');
    } else if (typeof rootOrElement.closest === 'function') {
      const root = rootOrElement.closest('.vareoncalc-root');
      toastEl = root?.querySelector('.vareoncalc-toast') || null;
    }

    if (!toastEl) {
      return;
    }

    toastEl.classList.remove('vareoncalc-toast--visible');
    if (toastEl._vareoncalcToastTimeoutId) {
      clearTimeout(toastEl._vareoncalcToastTimeoutId);
      toastEl._vareoncalcToastTimeoutId = null;
    }
  },

  isInputValid(input, meta = {}) {
    if (!input) {
      return true;
    }

    const value = input.value;
    const isEmpty = value === '' || value === null || value === undefined;
    if (isEmpty && meta.allowEmpty) {
      return true;
    }
    if (isEmpty) {
      return false;
    }

    if (typeof meta.validator === 'function') {
      return !!meta.validator();
    }

    if (typeof input.checkValidity === 'function') {
      return input.checkValidity();
    }

    return true;
  },

  /**
   * Resolve the error colors for a given input by reading CSS custom properties
   * from the nearest .vareoncalc-root. Returns hardcoded fallbacks when the
   * variables are unavailable (e.g. if the stylesheet hasn't loaded yet).
   * @param {HTMLElement} input
   * @returns {{ border: string, bg: string }}
   */
  _resolveErrorColors(input) {
    const fallbackBorder = '#dc2626';
    const fallbackBg = '#fef2f2';
    try {
      const root = input.closest('.vareoncalc-root');
      if (root) {
        const cs = getComputedStyle(root);
        const border = cs.getPropertyValue('--sc-error-border').trim();
        const bg = cs.getPropertyValue('--sc-error-bg').trim();
        return {
          border: border || fallbackBorder,
          bg: bg || fallbackBg,
        };
      }
    } catch (_) { /* fall through */ }
    return { border: fallbackBorder, bg: fallbackBg };
  },

  /**
   * Apply inline error styles to an input. Uses !important so that editor /
   * admin CSS cannot override the visual error state.
   * @param {HTMLElement} input
   */
  _applyErrorStyles(input) {
    const { border, bg } = this._resolveErrorColors(input);
    input.style.setProperty('border-color', border, 'important');
    input.style.setProperty('background-color', bg, 'important');
    input.style.setProperty('box-shadow', `0 0 0 3px ${bg}, inset 0 0 0 1px ${border}`, 'important');
  },

  /**
   * Remove inline error styles previously applied by _applyErrorStyles.
   * @param {HTMLElement} input
   */
  _removeErrorStyles(input) {
    input.style.removeProperty('border-color');
    input.style.removeProperty('background-color');
    input.style.removeProperty('box-shadow');
  },

  markInvalid(input, options = {}) {
    if (!input) {
      return;
    }

    input.classList.add('vareoncalc-error');
    input.setAttribute('aria-invalid', 'true');
    this._applyErrorStyles(input);

    const nextMeta = {
      allowEmpty: !!options.allowEmpty,
      validator: typeof options.validator === 'function' ? options.validator : null,
      lastValue: input.value,
    };
    input._vareoncalcInvalidMeta = {
      ...(input._vareoncalcInvalidMeta || {}),
      ...nextMeta,
    };

    if (input._vareoncalcInvalidHandler) {
      return;
    }

    const handler = () => {
      const meta = input._vareoncalcInvalidMeta || {};
      const currentValue = input.value;
      if (!meta.validator && meta.lastValue !== undefined && currentValue === meta.lastValue) {
        return;
      }
      if (!this.isInputValid(input, meta)) {
        meta.lastValue = currentValue;
        input._vareoncalcInvalidMeta = meta;
        return;
      }
      this.clearInvalid(input);
      const errorEl = input.parentElement?.querySelector('.vareoncalc-error-msg');
      if (errorEl) {
        errorEl.remove();
      }

      const root = input.closest?.('.vareoncalc-root') || this.getToastRoot(input);
      if (root && typeof root.querySelector === 'function') {
        if (!root.querySelector('.vareoncalc-error')) {
          this.hideToast(root);
        }
      } else {
        this.hideToast(input);
      }

      input.removeEventListener('input', handler);
      input.removeEventListener('change', handler);
      input._vareoncalcInvalidHandler = null;
      input._vareoncalcInvalidMeta = null;
    };

    input._vareoncalcInvalidHandler = handler;
    input.addEventListener('input', handler);
    input.addEventListener('change', handler);
  },

  clearInvalid(input) {
    if (!input) {
      return;
    }

    input.classList.remove('vareoncalc-error');
    input.removeAttribute('aria-invalid');
    this._removeErrorStyles(input);

    if (input._vareoncalcInvalidHandler) {
      input.removeEventListener('input', input._vareoncalcInvalidHandler);
      input.removeEventListener('change', input._vareoncalcInvalidHandler);
      input._vareoncalcInvalidHandler = null;
    }
    if (input._vareoncalcInvalidMeta) {
      input._vareoncalcInvalidMeta = null;
    }
  },

  /**
   * Show error message in the result/output container (preferred for validation errors)
   * @param {HTMLElement} outputEl - The output/result container element
   * @param {string} message - Error message to display
   */
  showResultError(outputEl, message, options = {}) {
    if (!outputEl) return;
    outputEl.querySelectorAll('.vareoncalc-error-msg, .vareoncalc-result-error').forEach(el => el.remove());
    outputEl.innerHTML = '';
    const durationMs = Number.isFinite(options.durationMs) ? options.durationMs : 3000;
    this.showToast(outputEl, message, { durationMs, type: 'error' });
    return outputEl;
  },

  /**
   * Show inline error next to an input (for real-time field-level validation)
   * @deprecated Prefer showResultError for submit validation
   */
  showError(input, message, options = {}) {
    if (!input) return;
    this.markInvalid(input, options);

    const existingError = input.parentElement?.querySelector('.vareoncalc-error-msg');
    if (existingError) {
      existingError.remove();
    }
    const durationMs = Number.isFinite(options.durationMs) ? options.durationMs : 3000;
    this.showToast(input, message, { durationMs, type: 'error' });
  },

  clearError(input) {
    if (!input) return;
    this.clearInvalid(input);

    const errorEl = input.parentElement?.querySelector('.vareoncalc-error-msg');
    if (errorEl) {
      errorEl.remove();
    }
  },

  clearAllErrors(container) {
    if (!container) return;
    container.querySelectorAll('.vareoncalc-error').forEach(el => {
      el.classList.remove('vareoncalc-error');
      el.removeAttribute('aria-invalid');
      this._removeErrorStyles(el);
      if (el._vareoncalcInvalidHandler) {
        el.removeEventListener('input', el._vareoncalcInvalidHandler);
        el.removeEventListener('change', el._vareoncalcInvalidHandler);
        el._vareoncalcInvalidHandler = null;
      }
      if (el._vareoncalcInvalidMeta) {
        el._vareoncalcInvalidMeta = null;
      }
    });
    container.querySelectorAll('.vareoncalc-error-msg').forEach(el => el.remove());
    this.hideToast(container);
  },

  setButtonLoading(button, isLoading, loadingText = null) {
    if (!button) return;

    if (isLoading) {
      button.dataset.originalText = button.textContent;
      button.textContent = loadingText || __( 'Calculating...', 'vareon-calculator-suite-lite' );
      button.disabled = true;
      button.classList.add('vareoncalc-btn--loading');
    } else {
      button.textContent = button.dataset.originalText || button.textContent;
      button.disabled = false;
      button.classList.remove('vareoncalc-btn--loading');
    }
  },

  validateNumber(value, min = 0, max = Infinity, fieldName = null) {
    const name = fieldName || __( 'Value', 'vareon-calculator-suite-lite' );
    if (value === '' || value === null || value === undefined) {
      /* translators: %s: Field name */
      return { valid: false, message: sprintf( __( '%s is required.', 'vareon-calculator-suite-lite' ), name ) };
    }

    const num = Number(value);
    if (Number.isNaN(num)) {
      /* translators: %s: Field name */
      return { valid: false, message: sprintf( __( '%s must be a number.', 'vareon-calculator-suite-lite' ), name ) };
    }

    if (num < min) {
      /* translators: 1: Field name, 2: Minimum value */
      return { valid: false, message: sprintf( __( '%1$s must be at least %2$s.', 'vareon-calculator-suite-lite' ), name, min ) };
    }

    if (Number.isFinite(max) && num > max) {
      /* translators: 1: Field name, 2: Maximum value */
      return { valid: false, message: sprintf( __( '%1$s must be %2$s or less.', 'vareon-calculator-suite-lite' ), name, max ) };
    }

    return { valid: true, value: num };
  },

  validatePercentage(value, fieldName = null) {
    const name = fieldName || __( 'Value', 'vareon-calculator-suite-lite' );
    const result = this.validateNumber(value, 0, 100, name);
    if (!result.valid) {
      /* translators: %s: Field name */
      return { valid: false, message: sprintf( __( '%s must be between 0 and 100.', 'vareon-calculator-suite-lite' ), name ) };
    }
    return result;
  },

  validateRate(value, fieldName = null, min = 0, max = 100) {
    const name = fieldName || __( 'Rate', 'vareon-calculator-suite-lite' );
    const result = this.validateNumber(value, min, max, name);
    if (!result.valid) {
      /* translators: 1: Field name, 2: Minimum value, 3: Maximum value */
      return { valid: false, message: sprintf( __( '%1$s must be between %2$s and %3$s.', 'vareon-calculator-suite-lite' ), name, min, max ) };
    }
    return result;
  },

  validateRequired(value, fieldName = null) {
    const name = fieldName || __( 'Value', 'vareon-calculator-suite-lite' );
    if (value === '' || value === null || value === undefined) {
      return { valid: false, message: sprintf( __( '%s is required.', 'vareon-calculator-suite-lite' ), name ) };
    }
    return { valid: true, value };
  },

  formatNumber(num, decimals = 2) {
    const numberValue = Number(num);
    if (Number.isNaN(numberValue)) {
      return '0';
    }
    return numberValue.toLocaleString(undefined, {
      minimumFractionDigits: decimals,
      maximumFractionDigits: decimals
    });
  }
};

/**
 * Helpers for label tooltips.
 */
export const HelpTooltip = {
  escapeAttr(value) {
    return String(value).replace(/"/g, '&quot;');
  },

  escapeHtml(value) {
    return String(value)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  },

  /**
   * Create a text tooltip trigger.
   */
  create(text, label = '', options = {}) {
    const triggerLabel = String(label || __('More info', TEXT_DOMAIN));
    const tooltipText = String(text || '');
    const safeLabel = this.escapeHtml(triggerLabel);
    const safeTooltip = this.escapeAttr(tooltipText);
    const safeAria = this.escapeAttr(`${triggerLabel}: ${tooltipText}`);
    const safeAlign = options && options.align ? String(options.align) : '';
    const alignAttr = safeAlign ? ` data-tooltip-align="${this.escapeAttr(safeAlign)}"` : '';
    return `<span class="vareoncalc-tooltip" data-tooltip="${safeTooltip}" data-tooltip-trigger="text"${alignAttr} tabindex="0" role="note" aria-label="${safeAria}">${safeLabel}</span>`;
  },

  /**
   * Apply tooltip triggers to labels that declare help text.
   */
  apply(container) {
    if (!container) return;

    const labels = container.querySelectorAll('label[data-help]');
    labels.forEach(label => {
      if (label.querySelector('.vareoncalc-tooltip, .vareoncalc-help-icon')) return;

      const helpText = label.dataset.help || '';
      if (!helpText) return;

      const labelText = getNormalizedLabelText(label);
      const trigger = document.createElement('span');
      trigger.className = 'vareoncalc-tooltip';
      trigger.setAttribute('data-tooltip', helpText);
      trigger.setAttribute('data-tooltip-trigger', 'text');
      trigger.setAttribute('tabindex', '0');
      trigger.setAttribute('role', 'note');
      if (labelText) {
        trigger.setAttribute('aria-label', `${labelText}: ${helpText}`);
      }

      while (label.firstChild) {
        trigger.appendChild(label.firstChild);
      }
      label.appendChild(trigger);
    });
  }
};

/**
 * Helpers for input prefixes and suffixes.
 */
const SUFFIX_SYMBOL_CLASS = 'vareoncalc-input-adorned--suffix-symbol';
const SUFFIX_UNIT_CLASS = 'vareoncalc-input-adorned--suffix-unit';
const SUFFIX_CURRENCY_CLASS = 'vareoncalc-input-adorned--suffix-currency';
const SUFFIX_COMPOUND_CLASS = 'vareoncalc-input-adorned--suffix-compound';
const SUFFIX_SYMBOL_MAX_WIDTH = 44;
const SUFFIX_SYMBOL_WIDTH_BOOST = 2;
const SUFFIX_UNIT_WIDTH_BOOST = 4;
const MOBILE_LABEL_SHIFT_CLASS = 'vareoncalc-input-adorned--mobile-label-shift';
const MOBILE_LABEL_SUFFIX_CLASS = 'vareoncalc-label-mobile-suffix';
const MOBILE_LABEL_SUFFIX_ATTR = 'data-vareoncalc-mobile-suffix';
const MOBILE_GENERATED_LABEL_CLASS = 'vareoncalc-mobile-shift-label';
const MOBILE_GENERATED_LABEL_ATTR = 'data-vareoncalc-mobile-generated-label';
const MOBILE_LABEL_SHIFT_OPTOUT_ATTR = 'data-vareoncalc-mobile-label-shift';

export const InputAdornment = {
  suffixResizeWrappers: new Set(),
  suffixResizeBound: false,

  /**
   * Sync prefix spacing based on rendered width.
   * @param {HTMLElement} wrapper - The input wrapper element
   * @param {HTMLElement} prefixSpan - The prefix span element
   */
  syncPrefixMetrics(wrapper, prefixSpan) {
    if (!wrapper || !prefixSpan) return;

    wrapper.classList.add('vareoncalc-input-adorned--has-prefix');

    const updateMetrics = () => {
      const width = Math.ceil(prefixSpan.getBoundingClientRect().width);
      if (width) {
        wrapper.style.setProperty('--prefix-width', `${width}px`);
      }
    };

    if (typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(updateMetrics);
    } else {
      updateMetrics();
    }
  },

  /**
   * Clear prefix spacing metrics when prefix is removed.
   * @param {HTMLElement} wrapper - The input wrapper element
   */
  clearPrefixMetrics(wrapper) {
    if (!wrapper) return;
    wrapper.classList.remove('vareoncalc-input-adorned--has-prefix');
    wrapper.style.removeProperty('--prefix-width');
  },

  /**
   * Determine the suffix rail family from the rendered suffix text.
   * @param {string} suffixText - The suffix text content
   * @returns {'symbol'|'unit'|null} The suffix family
   */
  classifySuffixFamily(suffixText) {
    const text = String(suffixText || '').trim();
    if (!text) return null;
    if (/[A-Za-z0-9]/.test(text)) return 'unit';
    return Array.from(text).length <= 2 ? 'symbol' : 'unit';
  },

  /**
   * Normalize supported explicit suffix family metadata.
   * @param {string} suffixFamily - Explicit suffix family
   * @returns {'symbol'|'unit'|'currency'|'compound'|null} The normalized family
   */
  normalizeSuffixFamily(suffixFamily) {
    const normalized = String(suffixFamily || '').trim().toLowerCase();
    if (
      normalized === 'symbol'
      || normalized === 'unit'
      || normalized === 'currency'
      || normalized === 'compound'
    ) {
      return normalized;
    }
    return null;
  },

  /**
   * Resolve the suffix family from explicit metadata or fallback heuristics.
   * @param {HTMLInputElement|HTMLSelectElement|null} control - The control element
   * @param {string} suffixText - The suffix text content
   * @returns {'symbol'|'unit'|'currency'|'compound'|null} The suffix family
   */
  resolveSuffixFamily(control, suffixText) {
    const explicitFamily = InputAdornment.normalizeSuffixFamily(control?.dataset?.suffixFamily || '');
    return explicitFamily || InputAdornment.classifySuffixFamily(suffixText);
  },

  /**
   * Sync suffix modifier classes to match the rendered suffix.
   * @param {HTMLElement} wrapper - The input wrapper element
   * @param {string} suffixText - The suffix text content
   * @returns {'symbol'|'unit'|null} The suffix family
   */
  syncSuffixClasses(wrapper, suffixText, suffixFamily = null) {
    if (!wrapper) return null;

    const text = String(suffixText || '').trim();
    const family = InputAdornment.normalizeSuffixFamily(suffixFamily) || InputAdornment.classifySuffixFamily(text);

    wrapper.classList.toggle('vareoncalc-input-adorned--short', !!text && Array.from(text).length <= 2 && family !== 'compound');
    wrapper.classList.toggle(SUFFIX_SYMBOL_CLASS, family === 'symbol' || family === 'currency');
    wrapper.classList.toggle(SUFFIX_UNIT_CLASS, family === 'unit' || family === 'compound');
    wrapper.classList.toggle(SUFFIX_CURRENCY_CLASS, family === 'currency');
    wrapper.classList.toggle(SUFFIX_COMPOUND_CLASS, family === 'compound');
    if (family) {
      wrapper.setAttribute('data-suffix-family', family);
    } else {
      wrapper.removeAttribute('data-suffix-family');
    }

    return family;
  },

  /**
   * Read a pixel custom property value from the wrapper.
   * @param {HTMLElement} wrapper - The input wrapper element
   * @param {string} propertyName - The CSS custom property name
   * @param {number} fallback - Fallback pixel value
   * @returns {number} Parsed pixel value
   */
  readPixelProperty(wrapper, propertyName, fallback) {
    if (!wrapper || typeof getComputedStyle !== 'function') return fallback;
    const rawValue = getComputedStyle(wrapper).getPropertyValue(propertyName).trim();
    const match = rawValue.match(/(-?\d*\.?\d+)px/i);
    return match ? Number(match[1]) : fallback;
  },

  /**
   * Read a percentage custom property value from the wrapper.
   * @param {HTMLElement} wrapper - The input wrapper element
   * @param {string} propertyName - The CSS custom property name
   * @returns {number|null} Parsed percentage value
   */
  readPercentProperty(wrapper, propertyName) {
    if (!wrapper || typeof getComputedStyle !== 'function') return null;
    const rawValue = getComputedStyle(wrapper).getPropertyValue(propertyName).trim();
    const match = rawValue.match(/(-?\d*\.?\d+)%/);
    return match ? Number(match[1]) : null;
  },

  /**
   * Approximate rendered text width when layout metrics are unavailable.
   * @param {string} text - The suffix text content
   * @param {CSSStyleDeclaration} styles - The computed suffix styles
   * @returns {number} Estimated text width in pixels
   */
  estimateSuffixTextWidth(text, styles) {
    const fontSize = Number.parseFloat(styles?.fontSize || '11') || 11;
    const glyphCount = Array.from(String(text || '')).length;
    return Math.ceil(fontSize * Math.max(0.6, glyphCount * 0.58));
  },

  /**
   * Clear suffix width metrics when the suffix is removed.
   * @param {HTMLElement} wrapper - The input wrapper element
   */
  clearSuffixMetrics(wrapper) {
    if (!wrapper) return;
    wrapper.classList.remove(SUFFIX_SYMBOL_CLASS, SUFFIX_UNIT_CLASS, SUFFIX_CURRENCY_CLASS, SUFFIX_COMPOUND_CLASS);
    wrapper.removeAttribute('data-suffix-family');
    wrapper.style.removeProperty('--sc-suffix-rail-width');
    InputAdornment.clearMobileLabelSuffix(wrapper);
    InputAdornment.unregisterSuffixResizeSync(wrapper);
  },

  /**
   * Resolve the associated explicit label for a control.
   * @param {HTMLInputElement|HTMLSelectElement} control - The control element
   * @returns {HTMLLabelElement|null} Matching label element
   */
  resolveAssociatedLabel(control) {
    if (!control) return null;

    if (control.labels && control.labels.length) {
      for (const label of control.labels) {
        if (!label.hasAttribute?.(MOBILE_GENERATED_LABEL_ATTR)) {
          return label;
        }
      }
    }

    const controlId = (control.id || '').trim();
    if (!controlId) return null;

    const scope = control.closest?.('.vareoncalc-root') || control.ownerDocument || document;
    if (scope && typeof scope.querySelectorAll === 'function') {
      const labels = scope.querySelectorAll('label[for]');
      for (const label of labels) {
        if (label.hasAttribute?.(MOBILE_GENERATED_LABEL_ATTR)) continue;
        if ((label.getAttribute('for') || '').trim() === controlId) {
          return label;
        }
      }
    }

    return null;
  },

  /**
   * Find the nearest shared label that visually describes the current field group.
   * @param {HTMLInputElement|HTMLSelectElement} control - The control element
   * @returns {HTMLLabelElement|null} Nearby label element
   */
  findNearestSharedLabel(control) {
    if (!control) return null;

    let node = control.closest?.('.vareoncalc-field') || control.parentElement;
    while (node && node.nodeType === 1) {
      let sibling = node.previousElementSibling;
      while (sibling) {
        const directLabel = sibling.matches?.(`label:not([${MOBILE_GENERATED_LABEL_ATTR}])`) ? sibling : null;
        if (directLabel) return directLabel;

        const nestedLabel = sibling.querySelector?.(`label:not([${MOBILE_GENERATED_LABEL_ATTR}]):last-of-type`);
        if (nestedLabel) return nestedLabel;

        sibling = sibling.previousElementSibling;
      }

      if (node.classList?.contains('vareoncalc-root')) break;
      node = node.parentElement;
    }

    return null;
  },

  /**
   * Build fallback mobile label text for grouped fields without an explicit label.
   * @param {HTMLInputElement|HTMLSelectElement} control - The control element
   * @returns {string} Derived label text
   */
  deriveGeneratedMobileLabelText(control) {
    if (!control) return '';

    const ariaLabel = normalizeWhitespace(control.getAttribute?.('aria-label') || '');
    if (ariaLabel) return ariaLabel;

    const sharedLabel = InputAdornment.findNearestSharedLabel(control);
    const sharedLabelText = sharedLabel ? getNormalizedLabelText(sharedLabel) : '';
    const placeholder = normalizeWhitespace(control.getAttribute?.('placeholder') || '');
    const placeholderLabel = /^[\p{L}\s/&-]+$/u.test(placeholder) ? placeholder : '';

    if (sharedLabelText && placeholderLabel) {
      return sharedLabelText.toLowerCase().includes(placeholderLabel.toLowerCase())
        ? sharedLabelText
        : `${sharedLabelText} ${placeholderLabel}`;
    }

    if (sharedLabelText) return sharedLabelText;
    if (placeholderLabel) return placeholderLabel;
    return '';
  },

  /**
   * Resolve the label target used for mobile suffix shifting.
   * @param {HTMLElement} wrapper - The input wrapper element
   * @param {HTMLInputElement|HTMLSelectElement} control - The control element
   * @returns {{label: HTMLLabelElement, synthetic: boolean, baseText: string}|null} Label target metadata
   */
  resolveMobileLabelTarget(wrapper, control) {
    if (!wrapper || !control) return null;

    if ((control.getAttribute(MOBILE_LABEL_SHIFT_OPTOUT_ATTR) || '').trim().toLowerCase() === 'off') {
      return null;
    }

    const explicitLabel = InputAdornment.resolveAssociatedLabel(control);
    if (explicitLabel) {
      return { label: explicitLabel, synthetic: false, baseText: '' };
    }

    const controlId = String(control.id || '').trim();
    const baseText = InputAdornment.deriveGeneratedMobileLabelText(control);
    const field = control.closest?.('.vareoncalc-field') || wrapper.parentElement;
    if (!controlId || !baseText || !field) return null;

    let label = wrapper.__vareoncalcMobileLabel;
    if (!label || !label.isConnected || label.parentElement !== field) {
      label = document.createElement('label');
      label.className = MOBILE_GENERATED_LABEL_CLASS;
      label.setAttribute(MOBILE_GENERATED_LABEL_ATTR, '1');
      if (field.contains(wrapper)) {
        field.insertBefore(label, wrapper);
      } else {
        field.prepend(label);
      }
      wrapper.__vareoncalcMobileLabel = label;
    }

    label.setAttribute('for', controlId);
    return { label, synthetic: true, baseText };
  },

  /**
   * Remove the managed mobile label suffix from a wrapper, if present.
   * @param {HTMLElement} wrapper - The input wrapper element
   */
  clearMobileLabelSuffix(wrapper) {
    if (!wrapper) return;

    const mobileSuffix = wrapper.__vareoncalcMobileLabelSuffix;
    if (mobileSuffix && typeof mobileSuffix.remove === 'function') {
      mobileSuffix.remove();
    }

    const mobileLabel = wrapper.__vareoncalcMobileLabel;
    if (mobileLabel && typeof mobileLabel.remove === 'function') {
      mobileLabel.remove();
    }

    delete wrapper.__vareoncalcMobileLabelSuffix;
    delete wrapper.__vareoncalcMobileLabel;
    wrapper.classList.remove(MOBILE_LABEL_SHIFT_CLASS);
  },

  /**
   * Sync the managed mobile label suffix for a control.
   * @param {HTMLElement} wrapper - The input wrapper element
   * @param {HTMLInputElement|HTMLSelectElement|null} control - The control element
   * @param {string} suffixText - The current suffix text
   */
  syncMobileLabelSuffix(wrapper, control, suffixText, suffixFamily = null) {
    if (!wrapper || !control) {
      InputAdornment.clearMobileLabelSuffix(wrapper);
      return;
    }

    const text = String(suffixText || '').trim();
    if (!text) {
      InputAdornment.clearMobileLabelSuffix(wrapper);
      return;
    }

    if (suffixFamily === 'currency' || suffixFamily === 'compound') {
      InputAdornment.clearMobileLabelSuffix(wrapper);
      return;
    }

    const labelTarget = InputAdornment.resolveMobileLabelTarget(wrapper, control);
    if (!labelTarget?.label) {
      InputAdornment.clearMobileLabelSuffix(wrapper);
      return;
    }

    const { label, synthetic, baseText } = labelTarget;
    if (synthetic) {
      label.textContent = baseText;
    }

    let mobileSuffix = wrapper.__vareoncalcMobileLabelSuffix;
    if (!mobileSuffix || !mobileSuffix.isConnected || !label.contains(mobileSuffix)) {
      if (mobileSuffix && typeof mobileSuffix.remove === 'function') {
        mobileSuffix.remove();
      }
      mobileSuffix = document.createElement('span');
      mobileSuffix.className = MOBILE_LABEL_SUFFIX_CLASS;
      mobileSuffix.setAttribute(MOBILE_LABEL_SUFFIX_ATTR, '1');
      label.appendChild(mobileSuffix);
      wrapper.__vareoncalcMobileLabelSuffix = mobileSuffix;
    }

    mobileSuffix.textContent = ` (${text})`;
    wrapper.classList.add(MOBILE_LABEL_SHIFT_CLASS);
  },

  /**
   * Bind suffix metric refresh to wrapper resize events once.
   * @param {HTMLElement} wrapper - The input wrapper element
   * @param {HTMLElement} suffixSpan - The suffix span element
   */
  bindSuffixResizeSync(wrapper, suffixSpan) {
    if (!wrapper || !suffixSpan) return;

    if (!wrapper.__vareoncalcSuffixResizeHandler) {
      wrapper.__vareoncalcSuffixResizeHandler = () => {
        if (!wrapper.isConnected) {
          InputAdornment.unregisterSuffixResizeSync(wrapper);
          return;
        }
        const currentSuffix = wrapper.querySelector('.vareoncalc-input-suffix');
        if (!(currentSuffix instanceof HTMLSpanElement)) {
          InputAdornment.unregisterSuffixResizeSync(wrapper);
          return;
        }
        InputAdornment.syncSuffixMetrics(wrapper, currentSuffix);
      };
      InputAdornment.suffixResizeWrappers.add(wrapper);
    }

    if (!InputAdornment.suffixResizeBound && typeof window !== 'undefined') {
      InputAdornment.suffixResizeBound = true;
      window.addEventListener('resize', InputAdornment.handleSuffixWindowResize);
    }
  },

  /**
   * Refresh registered suffix rails on window resize.
   */
  handleSuffixWindowResize() {
    Array.from(InputAdornment.suffixResizeWrappers).forEach((wrapper) => {
      if (!(wrapper instanceof HTMLDivElement) || !wrapper.isConnected) {
        InputAdornment.unregisterSuffixResizeSync(wrapper);
        return;
      }
      if (typeof wrapper.__vareoncalcSuffixResizeHandler === 'function') {
        wrapper.__vareoncalcSuffixResizeHandler();
      }
    });
  },

  /**
   * Remove shared suffix resize syncing for a wrapper.
   * @param {HTMLElement} wrapper - The input wrapper element
   */
  unregisterSuffixResizeSync(wrapper) {
    if (!wrapper) return;
    if (wrapper.__vareoncalcSuffixResizeHandler) {
      delete wrapper.__vareoncalcSuffixResizeHandler;
    }
    InputAdornment.suffixResizeWrappers.delete(wrapper);
  },

  /**
   * Sync suffix width metrics based on rendered text width and wrapper size.
   * @param {HTMLElement} wrapper - The input wrapper element
   * @param {HTMLElement} suffixSpan - The suffix span element
   */
  syncSuffixMetrics(wrapper, suffixSpan) {
    if (!wrapper || !suffixSpan) return;

    const suffixText = String(suffixSpan.textContent || '').trim();
    const control = wrapper.querySelector('input, select');
    const family = InputAdornment.syncSuffixClasses(
      wrapper,
      suffixText,
      InputAdornment.resolveSuffixFamily(control, suffixText)
    );

    if (!family || suffixSpan.style.display === 'none') {
      InputAdornment.clearSuffixMetrics(wrapper);
      return;
    }

    InputAdornment.syncMobileLabelSuffix(wrapper, control, suffixText, family);

    const updateMetrics = () => {
      if (!wrapper.isConnected || !document.body) return;

      const computedStyles = getComputedStyle(suffixSpan);
      const measurementNode = document.createElement('span');
      measurementNode.textContent = suffixText;
      measurementNode.setAttribute('aria-hidden', 'true');
      measurementNode.style.position = 'absolute';
      measurementNode.style.visibility = 'hidden';
      measurementNode.style.pointerEvents = 'none';
      measurementNode.style.whiteSpace = 'nowrap';
      measurementNode.style.top = '0';
      measurementNode.style.left = '-9999px';
      measurementNode.style.font = computedStyles.font;
      measurementNode.style.fontSize = computedStyles.fontSize;
      measurementNode.style.fontWeight = computedStyles.fontWeight;
      measurementNode.style.fontFamily = computedStyles.fontFamily;
      measurementNode.style.letterSpacing = computedStyles.letterSpacing;
      measurementNode.style.textTransform = computedStyles.textTransform;
      measurementNode.style.lineHeight = computedStyles.lineHeight;

      document.body.appendChild(measurementNode);
      let textWidth = Math.ceil(measurementNode.getBoundingClientRect().width);
      measurementNode.remove();

      if (!textWidth) {
        textWidth = InputAdornment.estimateSuffixTextWidth(suffixText, computedStyles);
      }

      const minWidth = InputAdornment.readPixelProperty(wrapper, '--sc-suffix-rail-min-width', 24);
      const cssMaxWidth = InputAdornment.readPixelProperty(wrapper, '--sc-suffix-rail-max-width', 54);
      const fluidPercent = InputAdornment.readPercentProperty(wrapper, '--sc-suffix-rail-fluid-width');
      const wrapperWidth = Math.ceil(wrapper.getBoundingClientRect().width);
      const paddingWidth =
        (Number.parseFloat(computedStyles.paddingLeft) || 0) +
        (Number.parseFloat(computedStyles.paddingRight) || 0);
      const borderWidth =
        (Number.parseFloat(computedStyles.borderLeftWidth) || 0) +
        (Number.parseFloat(computedStyles.borderRightWidth) || 0);

      let maxWidth = family === 'symbol' ? Math.min(cssMaxWidth, SUFFIX_SYMBOL_MAX_WIDTH) : cssMaxWidth;
      if (fluidPercent !== null && wrapperWidth > 0) {
        maxWidth = Math.max(minWidth, Math.min(maxWidth, Math.floor((wrapperWidth * fluidPercent) / 100)));
      }

      const familyWidthBoost = (family === 'symbol' || family === 'currency')
        ? SUFFIX_SYMBOL_WIDTH_BOOST
        : SUFFIX_UNIT_WIDTH_BOOST;
      const measuredWidth = Math.ceil(textWidth + paddingWidth + borderWidth + familyWidthBoost);
      const finalWidth = Math.max(minWidth, Math.min(measuredWidth, maxWidth));
      const nextWidth = `${finalWidth}px`;
      if (wrapper.style.getPropertyValue('--sc-suffix-rail-width') !== nextWidth) {
        wrapper.style.setProperty('--sc-suffix-rail-width', nextWidth);
      }
    };

    if (typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(updateMetrics);
    } else {
      updateMetrics();
    }

    InputAdornment.bindSuffixResizeSync(wrapper, suffixSpan);
  },

  /**
   * Apply prefix and suffix adornments to all inputs with data-prefix or data-suffix attribute within a container.
   * Wraps each input in a .vareoncalc-input-adorned div and adds prefix/suffix spans.
   * @param {HTMLElement} container - The container to search for inputs
   */
  apply(container) {
    if (!container) return;

    const inputs = container.querySelectorAll('input[data-suffix], select[data-suffix], input[data-prefix], select[data-prefix]');
    inputs.forEach(input => {
      if (input.parentElement?.classList.contains('vareoncalc-input-adorned')) return;

      const suffix = input.dataset.suffix || '';
      const prefix = input.dataset.prefix || '';
      if (!suffix && !prefix) return;

      const wrapper = document.createElement('div');
      wrapper.className = 'vareoncalc-input-adorned';

      if (suffix && suffix.length <= 2) {
        wrapper.classList.add('vareoncalc-input-adorned--short');
      }

      input.parentElement.insertBefore(wrapper, input);

      if (prefix) {
        const prefixSpan = document.createElement('span');
        prefixSpan.className = 'vareoncalc-input-prefix';
        prefixSpan.textContent = prefix;
        prefixSpan.setAttribute('aria-hidden', 'true');
        wrapper.appendChild(prefixSpan);
        InputAdornment.syncPrefixMetrics(wrapper, prefixSpan);
      }

      wrapper.appendChild(input);

      if (suffix) {
        const suffixSpan = document.createElement('span');
        suffixSpan.className = 'vareoncalc-input-suffix';
        suffixSpan.textContent = suffix;
        suffixSpan.setAttribute('aria-hidden', 'true');
        wrapper.appendChild(suffixSpan);
        InputAdornment.syncSuffixMetrics(wrapper, suffixSpan);
      }
    });

    const existingPrefixes = container.querySelectorAll('.vareoncalc-input-adorned .vareoncalc-input-prefix');
    existingPrefixes.forEach(prefixSpan => {
      const wrapper = prefixSpan.closest('.vareoncalc-input-adorned');
      InputAdornment.syncPrefixMetrics(wrapper, prefixSpan);
    });

    const existingSuffixes = container.querySelectorAll('.vareoncalc-input-adorned .vareoncalc-input-suffix');
    existingSuffixes.forEach(suffixSpan => {
      const wrapper = suffixSpan.closest('.vareoncalc-input-adorned');
      InputAdornment.syncSuffixMetrics(wrapper, suffixSpan);
    });
  },

  /**
   * Update the suffix text for a specific input element.
   * If the input hasn't been wrapped yet, it will be wrapped first.
   * @param {HTMLInputElement} input - The input element
   * @param {string} newSuffix - The new suffix text (empty string hides suffix)
   */
  setSuffix(input, newSuffix) {
    if (!input) return;

    if (newSuffix) {
      input.dataset.suffix = newSuffix;
    } else {
      delete input.dataset.suffix;
    }

    if (arguments.length >= 3) {
      const nextFamily = InputAdornment.normalizeSuffixFamily(arguments[2]);
      if (nextFamily) {
        input.dataset.suffixFamily = nextFamily;
      } else {
        delete input.dataset.suffixFamily;
      }
    } else {
      delete input.dataset.suffixFamily;
    }

    const wrapper = input.parentElement;
    if (wrapper?.classList.contains('vareoncalc-input-adorned')) {
      let suffixSpan = wrapper.querySelector('.vareoncalc-input-suffix');
      if (suffixSpan) {
        suffixSpan.textContent = newSuffix || '';
        suffixSpan.style.display = newSuffix ? '' : 'none';
      } else if (newSuffix) {
        suffixSpan = document.createElement('span');
        suffixSpan.className = 'vareoncalc-input-suffix';
        suffixSpan.setAttribute('aria-hidden', 'true');
        suffixSpan.textContent = newSuffix;
        wrapper.appendChild(suffixSpan);
      }
      if (suffixSpan) {
        InputAdornment.syncSuffixMetrics(wrapper, suffixSpan);
      } else {
        InputAdornment.clearSuffixMetrics(wrapper);
        wrapper.classList.remove('vareoncalc-input-adorned--short');
      }
    } else if (newSuffix) {
      const newWrapper = document.createElement('div');
      newWrapper.className = 'vareoncalc-input-adorned';
      if (newSuffix.length <= 2) {
        newWrapper.classList.add('vareoncalc-input-adorned--short');
      }
      input.parentElement.insertBefore(newWrapper, input);
      newWrapper.appendChild(input);

      const suffixSpan = document.createElement('span');
      suffixSpan.className = 'vareoncalc-input-suffix';
      suffixSpan.textContent = newSuffix;
      suffixSpan.setAttribute('aria-hidden', 'true');
      newWrapper.appendChild(suffixSpan);
      InputAdornment.syncSuffixMetrics(newWrapper, suffixSpan);
    }
  },

  /**
   * Update the prefix text for a specific input element.
   * If the input hasn't been wrapped yet, it will be wrapped first.
   * @param {HTMLInputElement} input - The input element
   * @param {string} newPrefix - The new prefix text (empty string hides prefix)
   */
  setPrefix(input, newPrefix) {
    if (!input) return;

    if (newPrefix) {
      input.dataset.prefix = newPrefix;
    } else {
      delete input.dataset.prefix;
    }

    const wrapper = input.parentElement;
    if (wrapper?.classList.contains('vareoncalc-input-adorned')) {
      const prefixSpan = wrapper.querySelector('.vareoncalc-input-prefix');
      if (prefixSpan) {
        prefixSpan.textContent = newPrefix || '';
        prefixSpan.style.display = newPrefix ? '' : 'none';
        if (newPrefix) {
          InputAdornment.syncPrefixMetrics(wrapper, prefixSpan);
        } else {
          InputAdornment.clearPrefixMetrics(wrapper);
        }
      } else if (newPrefix) {
        const newPrefixSpan = document.createElement('span');
        newPrefixSpan.className = 'vareoncalc-input-prefix';
        newPrefixSpan.textContent = newPrefix;
        newPrefixSpan.setAttribute('aria-hidden', 'true');
        wrapper.insertBefore(newPrefixSpan, wrapper.firstChild);
        InputAdornment.syncPrefixMetrics(wrapper, newPrefixSpan);
      } else {
        InputAdornment.clearPrefixMetrics(wrapper);
      }
    } else if (newPrefix) {
      const newWrapper = document.createElement('div');
      newWrapper.className = 'vareoncalc-input-adorned';
      input.parentElement.insertBefore(newWrapper, input);
      
      const prefixSpan = document.createElement('span');
      prefixSpan.className = 'vareoncalc-input-prefix';
      prefixSpan.textContent = newPrefix;
      prefixSpan.setAttribute('aria-hidden', 'true');
      newWrapper.appendChild(prefixSpan);
      InputAdornment.syncPrefixMetrics(newWrapper, prefixSpan);
      
      newWrapper.appendChild(input);
    }
  },

  /**
   * Get the current suffix for an input.
   * @param {HTMLInputElement} input - The input element
   * @returns {string} The current suffix text
   */
  getSuffix(input) {
    if (!input) return '';
    const wrapper = input.parentElement;
    if (wrapper?.classList.contains('vareoncalc-input-adorned')) {
      const suffixSpan = wrapper.querySelector('.vareoncalc-input-suffix');
      return suffixSpan?.textContent || '';
    }
    return input.dataset.suffix || '';
  },

  /**
   * Get the current prefix for an input.
   * @param {HTMLInputElement} input - The input element
   * @returns {string} The current prefix text
   */
  getPrefix(input) {
    if (!input) return '';
    const wrapper = input.parentElement;
    if (wrapper?.classList.contains('vareoncalc-input-adorned')) {
      const prefixSpan = wrapper.querySelector('.vareoncalc-input-prefix');
      return prefixSpan?.textContent || '';
    }
    return input.dataset.prefix || '';
  }
};

const TRUNCATION_LABEL_CLASS = 'vareoncalc-label--truncate';
const MANAGED_TITLE_ATTR = 'data-vareoncalc-managed-title';
const NON_TRUNCATABLE_INPUT_TYPES = new Set([
  'checkbox',
  'radio',
  'range',
  'hidden',
  'button',
  'submit',
  'reset',
  'file',
  'image',
  'color',
]);

function escapeIdForSelector(rawId) {
  if (!rawId) {
    return '';
  }

  if (typeof CSS !== 'undefined' && CSS && typeof CSS.escape === 'function') {
    return CSS.escape(rawId);
  }

  return rawId.replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
}

function resolveLabelControl(scope, label, controlId) {
  if (label && label.control) {
    return label.control;
  }

  const escapedId = escapeIdForSelector(controlId);
  if (escapedId && scope && typeof scope.querySelector === 'function') {
    const scopedControl = scope.querySelector(`#${escapedId}`);
    if (scopedControl) {
      return scopedControl;
    }
  }

  if (typeof document !== 'undefined' && typeof document.getElementById === 'function') {
    return document.getElementById(controlId);
  }

  return null;
}

function shouldTruncateLabelForControl(control) {
  if (!control || !control.tagName) {
    return false;
  }

  const tagName = control.tagName.toLowerCase();
  if (tagName === 'select' || tagName === 'textarea') {
    return true;
  }

  if (tagName !== 'input') {
    return false;
  }

  const inputType = (control.getAttribute('type') || 'text').toLowerCase();
  return !NON_TRUNCATABLE_INPUT_TYPES.has(inputType);
}

function getNormalizedLabelText(label) {
  if (!label) {
    return '';
  }

  if (!label.querySelector?.(`[${MOBILE_LABEL_SUFFIX_ATTR}="1"]`)) {
    return normalizeWhitespace(label.textContent || '');
  }

  const clonedLabel = label.cloneNode(true);
  clonedLabel.querySelectorAll(`[${MOBILE_LABEL_SUFFIX_ATTR}="1"]`).forEach(node => node.remove());
  return normalizeWhitespace(clonedLabel.textContent || '');
}

function normalizeWhitespace(value) {
  return String(value || '').replace(/\s+/g, ' ').trim();
}

/**
 * Apply universal single-line truncation behavior to labels that target
 * non-toggle form controls, while preserving native/explicit label titles.
 *
 * @param {HTMLElement} container - Mounted calculator container
 */
export function applyInputLabelTruncation(container) {
  if (!container || typeof container.querySelectorAll !== 'function') {
    return;
  }

  const scope = container.closest?.('.vareoncalc-root') || container;
  const labels = scope.querySelectorAll('label[for]');

  labels.forEach(label => {
    if (label.hasAttribute?.(MOBILE_GENERATED_LABEL_ATTR)) {
      label.classList.remove(TRUNCATION_LABEL_CLASS);
      if (label.getAttribute(MANAGED_TITLE_ATTR) === '1') {
        label.removeAttribute('title');
        label.removeAttribute(MANAGED_TITLE_ATTR);
      }
      return;
    }

    const controlId = (label.getAttribute('for') || '').trim();
    if (!controlId) {
      return;
    }

    const control = resolveLabelControl(scope, label, controlId);
    const shouldTruncate = shouldTruncateLabelForControl(control);

    if (!shouldTruncate) {
      label.classList.remove(TRUNCATION_LABEL_CLASS);
      if (label.getAttribute(MANAGED_TITLE_ATTR) === '1') {
        label.removeAttribute('title');
        label.removeAttribute(MANAGED_TITLE_ATTR);
      }
      return;
    }

    label.classList.add(TRUNCATION_LABEL_CLASS);

    const labelText = getNormalizedLabelText(label);
    if (!labelText) {
      if (label.getAttribute(MANAGED_TITLE_ATTR) === '1') {
        label.removeAttribute('title');
        label.removeAttribute(MANAGED_TITLE_ATTR);
      }
      return;
    }

    if (label.getAttribute(MANAGED_TITLE_ATTR) === '1') {
      label.setAttribute('title', labelText);
      return;
    }

    if (!label.hasAttribute('title')) {
      label.setAttribute('title', labelText);
      label.setAttribute(MANAGED_TITLE_ATTR, '1');
    }
  });
}

/**
 * Frontend tooltip portal that renders tooltip bubbles at <body> level.
 * Prevents clipping when triggers live inside overflow-hidden/scroll containers.
 */
export const TooltipPortal = {
  _initialized: false,
  _listenersBound: false,
  _portalEl: null,
  _activeTrigger: null,
  _rafId: 0,

  init() {
    if (this._initialized || typeof document === 'undefined') {
      return;
    }

    if (!document.body) {
      document.addEventListener('DOMContentLoaded', () => this.init(), { once: true });
      return;
    }

    this._initialized = true;
    document.body.classList.add('vareoncalc-tooltip-portal-active');
    this._bindListeners();
  },

  ensureElement() {
    if (this._portalEl && this._portalEl.isConnected) {
      return this._portalEl;
    }

    let el = document.getElementById('vareoncalc-tooltip-portal');
    if (!el) {
      el = document.createElement('div');
      el.id = 'vareoncalc-tooltip-portal';
      el.className = 'vareoncalc-tooltip-portal';
      el.setAttribute('role', 'tooltip');
      el.setAttribute('aria-hidden', 'true');
      document.body.appendChild(el);
    }

    this._portalEl = el;
    return el;
  },

  _resolveTrigger(node) {
    if (typeof Element === 'undefined' || !(node instanceof Element)) {
      return null;
    }

    const trigger = node.closest('.vareoncalc-tooltip[data-tooltip], .vareoncalc-help-icon[data-tooltip]');
    if (!trigger) {
      return null;
    }

    // Scope delegation to actual calculator roots only.
    if (!trigger.closest('.vareoncalc-root')) {
      return null;
    }

    return trigger;
  },

  _bindListeners() {
    if (this._listenersBound || typeof document === 'undefined') {
      return;
    }

    if (!this._onMouseOver) {
      this._onMouseOver = (event) => {
        const trigger = this._resolveTrigger(event.target);
        if (!trigger) {
          return;
        }
        if (event.relatedTarget instanceof Node && trigger.contains(event.relatedTarget)) {
          return;
        }
        this.show(trigger);
      };
    }

    if (!this._onMouseOut) {
      this._onMouseOut = (event) => {
        const trigger = this._resolveTrigger(event.target);
        if (!trigger || this._activeTrigger !== trigger) {
          return;
        }
        if (event.relatedTarget instanceof Node && trigger.contains(event.relatedTarget)) {
          return;
        }
        this.hide();
      };
    }

    if (!this._onFocusIn) {
      this._onFocusIn = (event) => {
        const trigger = this._resolveTrigger(event.target);
        if (!trigger) {
          return;
        }
        this.show(trigger);
      };
    }

    if (!this._onFocusOut) {
      this._onFocusOut = (event) => {
        const trigger = this._resolveTrigger(event.target);
        if (!trigger || this._activeTrigger !== trigger) {
          return;
        }
        if (event.relatedTarget instanceof Node && trigger.contains(event.relatedTarget)) {
          return;
        }
        this.hide();
      };
    }

    if (!this._onViewportChange) {
      this._onViewportChange = () => this.requestPosition();
    }

    if (!this._onKeyDown) {
      this._onKeyDown = (event) => {
        if (event.key === 'Escape') {
          this.hide();
        }
      };
    }

    document.addEventListener('mouseover', this._onMouseOver, true);
    document.addEventListener('mouseout', this._onMouseOut, true);
    document.addEventListener('focusin', this._onFocusIn, true);
    document.addEventListener('focusout', this._onFocusOut, true);
    document.addEventListener('scroll', this._onViewportChange, true);
    document.addEventListener('keydown', this._onKeyDown, true);

    if (typeof window !== 'undefined') {
      window.addEventListener('resize', this._onViewportChange);
    }

    this._listenersBound = true;
  },

  show(trigger) {
    if (!trigger) {
      return;
    }

    const text = (trigger.getAttribute('data-tooltip') || '').trim();
    if (!text) {
      this.hide();
      return;
    }

    this._activeTrigger = trigger;
    const el = this.ensureElement();
    this._syncThemeVars(trigger, el);
    el.textContent = text;
    el.classList.add('is-visible');
    el.setAttribute('aria-hidden', 'false');

    const describedBy = trigger.getAttribute('aria-describedby');
    if (!describedBy || describedBy !== el.id) {
      trigger.setAttribute('aria-describedby', el.id);
    }

    this.position();
  },

  _syncThemeVars(trigger, el) {
    if (!trigger || !el || typeof getComputedStyle !== 'function') {
      return;
    }

    const root = trigger.closest('.vareoncalc-root');
    if (!root) {
      return;
    }

    const styles = getComputedStyle(root);
    const bg = styles.getPropertyValue('--sc-card-text').trim();
    const fg = styles.getPropertyValue('--sc-card-bg').trim();

    if (bg) {
      el.style.setProperty('--vc-tooltip-bg', bg);
    } else {
      el.style.removeProperty('--vc-tooltip-bg');
    }

    if (fg) {
      el.style.setProperty('--vc-tooltip-text', fg);
    } else {
      el.style.removeProperty('--vc-tooltip-text');
    }
  },

  hide() {
    if (this._rafId && typeof cancelAnimationFrame === 'function') {
      cancelAnimationFrame(this._rafId);
      this._rafId = 0;
    }

    const previousTrigger = this._activeTrigger;
    this._activeTrigger = null;

    if (previousTrigger && previousTrigger.getAttribute('aria-describedby') === 'vareoncalc-tooltip-portal') {
      previousTrigger.removeAttribute('aria-describedby');
    }

    if (!this._portalEl) {
      return;
    }

    this._portalEl.classList.remove('is-visible');
    this._portalEl.setAttribute('aria-hidden', 'true');
  },

  requestPosition() {
    if (!this._activeTrigger) {
      return;
    }

    if (this._rafId) {
      return;
    }

    if (typeof requestAnimationFrame !== 'function') {
      this.position();
      return;
    }

    this._rafId = requestAnimationFrame(() => {
      this._rafId = 0;
      this.position();
    });
  },

  position() {
    const trigger = this._activeTrigger;
    if (!trigger || !trigger.isConnected) {
      this.hide();
      return;
    }

    const el = this.ensureElement();

    // Move off-screen while measuring to avoid visual jumps.
    el.style.left = '-9999px';
    el.style.top = '-9999px';

    const rect = trigger.getBoundingClientRect();
    const ttRect = el.getBoundingClientRect();
    const viewportW = window.innerWidth || document.documentElement.clientWidth;
    const viewportH = window.innerHeight || document.documentElement.clientHeight;
    const margin = 8;
    const gap = 10;

    const placeTop = rect.top >= ttRect.height + gap + margin;
    const placement = placeTop ? 'top' : 'bottom';
    let top = placeTop ? rect.top - ttRect.height - gap : rect.bottom + gap;

    const align = (trigger.getAttribute('data-tooltip-align') || '').toLowerCase();
    let left;
    if (align === 'left') {
      left = rect.left;
    } else if (align === 'right') {
      left = rect.right - ttRect.width;
    } else {
      left = rect.left + (rect.width / 2) - (ttRect.width / 2);
    }

    const maxLeft = viewportW - ttRect.width - margin;
    left = Math.max(margin, Math.min(left, maxLeft));

    const maxTop = viewportH - ttRect.height - margin;
    top = Math.max(margin, Math.min(top, maxTop));

    const anchorX = rect.left + (rect.width / 2);
    const arrowLeft = Math.max(10, Math.min(anchorX - left, ttRect.width - 10));

    el.dataset.placement = placement;
    el.style.left = `${Math.round(left)}px`;
    el.style.top = `${Math.round(top)}px`;
    el.style.setProperty('--vareoncalc-tooltip-arrow-left', `${Math.round(arrowLeft)}px`);
  },

  resetForTests() {
    if (typeof document === 'undefined') {
      return;
    }

    this.hide();

    if (this._listenersBound) {
      document.removeEventListener('mouseover', this._onMouseOver, true);
      document.removeEventListener('mouseout', this._onMouseOut, true);
      document.removeEventListener('focusin', this._onFocusIn, true);
      document.removeEventListener('focusout', this._onFocusOut, true);
      document.removeEventListener('scroll', this._onViewportChange, true);
      document.removeEventListener('keydown', this._onKeyDown, true);
      if (typeof window !== 'undefined') {
        window.removeEventListener('resize', this._onViewportChange);
      }
      this._listenersBound = false;
    }

    const el = document.getElementById('vareoncalc-tooltip-portal');
    if (el) {
      el.remove();
    }
    this._portalEl = null;
    this._activeTrigger = null;
    this._initialized = false;

    if (document.body) {
      document.body.classList.remove('vareoncalc-tooltip-portal-active');
    }
  },
};
