/**
 * Vareon Calculator Suite Lite - Split View Preview (V2)
 */
(function ($, win, doc) {
  'use strict';

  var boot = win.VAREONCALC_LITE_PREVIEW_V2 || null;
  if (!boot || !boot.frameUrl) {
    return;
  }

  var i18n = win.wp && win.wp.i18n ? win.wp.i18n : null;
  var __ = i18n && typeof i18n.__ === 'function'
    ? i18n.__
    : function (text) { return text; };
  var sprintf = i18n && typeof i18n.sprintf === 'function'
    ? i18n.sprintf
    : null;

  var stateKey = String(boot.stateKey || 'vareoncalc_lite_preview_v2_state');
  var MIN_PREVIEW_ZOOM = 30;
  var MAX_PREVIEW_ZOOM = 200;
  var MIN_PREVIEW_SCALE = MIN_PREVIEW_ZOOM / 100;
  var MAX_PREVIEW_SCALE = MAX_PREVIEW_ZOOM / 100;
  var MIN_PREVIEW_PAN = -12000;
  var MAX_PREVIEW_PAN = 12000;
  var WIDTH_MODE_FIXED = 'fixed';
  var defaultState = {
    width: 645,
    height: 220,
    widthMode: WIDTH_MODE_FIXED,
    zoom: 'fit',
    type: (boot.defaultConfig && boot.defaultConfig.type) || 'loan',
    panX: 0,
    panY: 0
  };
  var state = {
    width: defaultState.width,
    height: defaultState.height,
    widthMode: defaultState.widthMode,
    zoom: defaultState.zoom,
    type: defaultState.type,
    panX: defaultState.panX,
    panY: defaultState.panY,
    frameHeight: 220
  };

  var bridge = null;
  var updateTimer = null;
  var viewportUpdateTimer = null;
  var resizeTimer = null;
  var dragSession = null;
  var lastConfigFingerprint = '';
  var lastViewportFingerprint = '';

  var $split = null;
  var $form = null;
  var $canvas = null;
  var $stage = null;
  var $stageDock = null;
  var $stageDockHotzone = null;
  var $stageDockToggle = null;
  var $stageDockPanel = null;
  var $viewport = null;
  var $frameGuide = null;
  var $frameShell = null;
  var $calcType = null;
  var $retry = null;
  var $reset = null;
  var $zoomValue = null;
  var $statusWidthValue = null;
  var $statusZoomValue = null;
  var $zoomFit = null;
  var $zoomIn = null;
  var $zoomOut = null;
  var $widthButtons = null;
  var $widthCustomToggle = null;
  var $widthCustomPanel = null;
  var $widthCustomInput = null;
  var $widthCustomApply = null;
  var $helpToggle = null;
  var $helpPopover = null;
  var $loading = null;
  var frameHost = null;
  var stageDockCloseTimer = null;
  var loadingTimer = null;
  var isLoading = false;
  var panFromFrameStart = null;
  var panFromFrameMove = null;
  var panFromFrameEnd = null;

  function setPreviewAvailability(isAvailable) {
    if (!$split || !$split.length) {
      return;
    }
    $split.toggleClass('is-preview-unavailable', !isAvailable);
  }

  function clampNumber(value, min, max, fallback) {
    var parsed = parseFloat(value);
    if (!Number.isFinite(parsed)) {
      return fallback;
    }
    return Math.min(max, Math.max(min, parsed));
  }

  function sanitizeType(value) {
    if (typeof value !== 'string') {
      return defaultState.type;
    }
    var normalized = value.toLowerCase().trim().replace(/[^a-z0-9\-]/g, '');
    return normalized || defaultState.type;
  }

  function normalizeZoom(value) {
    var text = String(value || '').trim().toLowerCase();
    if (text === 'fit') {
      return 'fit';
    }
    return Math.round(clampNumber(text, MIN_PREVIEW_ZOOM, MAX_PREVIEW_ZOOM, 100));
  }

  function normalizeWidth(value) {
    return Math.round(clampNumber(value, 300, 2000, defaultState.width));
  }

  function normalizeHeight(value) {
    return Math.round(clampNumber(value, 220, 2000, defaultState.height));
  }

  function normalizeWidthMode(value) {
    void value;
    return WIDTH_MODE_FIXED;
  }

  function normalizePan(value) {
    return Math.round(clampNumber(value, MIN_PREVIEW_PAN, MAX_PREVIEW_PAN, 0));
  }

  function toNumber(value, fallback) {
    var parsed = parseInt(value, 10);
    return Number.isFinite(parsed) ? parsed : fallback;
  }

  function zoomValueToScale(value) {
    return clampNumber(value, MIN_PREVIEW_ZOOM, MAX_PREVIEW_ZOOM, 100) / 100;
  }

  function scaleToZoomValue(scale) {
    return Math.max(MIN_PREVIEW_ZOOM, Math.min(MAX_PREVIEW_ZOOM, Math.floor(scale * 100)));
  }

  function recenterFrameState() {
    state.panX = 0;
    state.panY = 0;
  }

  function getGuideWidthPx() {
    if ($frameGuide && $frameGuide.length) {
      var guideRect = $frameGuide[0].getBoundingClientRect();
      if (guideRect && Number.isFinite(guideRect.width) && guideRect.width > 0) {
        return Math.round(guideRect.width);
      }
    }
    if ($viewport && $viewport.length) {
      return Math.max(300, Math.floor($viewport[0].clientWidth - 24));
    }
    if ($stage && $stage.length) {
      return Math.max(300, Math.floor($stage[0].clientWidth - 24));
    }
    if ($canvas && $canvas.length) {
      return Math.max(300, Math.floor($canvas[0].clientWidth - 52));
    }
    return defaultState.width;
  }

  function getPreviewSurfaceWidth() {
    var visualWidth = normalizeWidth(state.width);
    return normalizeWidth(Math.max(getGuideWidthPx(), 1024, visualWidth));
  }

  function resolveWidthPresetToPx(rawPreset) {
    var preset = String(rawPreset || '').trim().toLowerCase();
    if (!preset) {
      return normalizeWidth(defaultState.width);
    }

    var percentMatch = preset.match(/^(\d+(?:\.\d+)?)%$/);
    if (percentMatch) {
      var percent = clampNumber(percentMatch[1], 1, 100, 100);
      return normalizeWidth(Math.round((getGuideWidthPx() * percent) / 100));
    }

    var pxMatch = preset.match(/^(\d+(?:\.\d+)?)px$/);
    if (pxMatch) {
      return normalizeWidth(pxMatch[1]);
    }

    return normalizeWidth(preset);
  }

  function loadState() {
    try {
      var raw = win.localStorage.getItem(stateKey);
      if (!raw) {
        return;
      }
      var parsed = JSON.parse(raw);
      if (!parsed || typeof parsed !== 'object') {
        return;
      }
      var storedWidthMode = typeof parsed.widthMode === 'string'
        ? parsed.widthMode.toLowerCase().trim()
        : '';
      state.width = normalizeWidth(parsed.width);
      state.widthMode = normalizeWidthMode(parsed.widthMode);
      if (storedWidthMode === 'auto') {
        state.width = defaultState.width;
        state.widthMode = defaultState.widthMode;
      }
      if (typeof parsed.type === 'string' && parsed.type) {
        state.type = sanitizeType(parsed.type);
      }
    } catch {
      // Ignore localStorage read failures.
    }
  }

  function saveState() {
    try {
      win.localStorage.setItem(
        stateKey,
        JSON.stringify({
          width: state.width,
          widthMode: state.widthMode,
          type: state.type
        })
      );
    } catch {
      // Ignore localStorage write failures.
    }
  }

  function isCustomWidthPopoverOpen() {
    if (!$widthCustomPanel || !$widthCustomPanel.length) {
      return false;
    }
    return !$widthCustomPanel.hasClass('vareoncalc-is-hidden') && !$widthCustomPanel.prop('hidden');
  }

  function isHelpPopoverOpen() {
    if (!$helpPopover || !$helpPopover.length) {
      return false;
    }
    return !$helpPopover.prop('hidden');
  }

  function setHelpPopoverOpen(open) {
    var shouldOpen = !!open;
    if ($helpPopover && $helpPopover.length) {
      $helpPopover
        .toggleClass('vareoncalc-is-hidden', !shouldOpen)
        .prop('hidden', !shouldOpen)
        .attr('aria-hidden', shouldOpen ? 'false' : 'true');
    }
    if ($helpToggle && $helpToggle.length) {
      $helpToggle.attr('aria-expanded', shouldOpen ? 'true' : 'false');
    }
  }

  function setCustomWidthPopoverOpen(open, focusInput) {
    var shouldOpen = !!open;
    if ($widthCustomPanel && $widthCustomPanel.length) {
      if (!shouldOpen) {
        var activeElement = doc.activeElement;
        if (activeElement && $widthCustomPanel[0].contains(activeElement) && typeof activeElement.blur === 'function') {
          activeElement.blur();
        }
      }
      $widthCustomPanel
        .toggleClass('vareoncalc-is-hidden', !shouldOpen)
        .prop('hidden', !shouldOpen)
        .attr('aria-hidden', shouldOpen ? 'false' : 'true');
    }
    if ($widthCustomToggle && $widthCustomToggle.length) {
      $widthCustomToggle.attr('aria-expanded', shouldOpen ? 'true' : 'false');
    }
    if (shouldOpen && focusInput && $widthCustomInput && $widthCustomInput.length) {
      var inputEl = $widthCustomInput[0];
      $widthCustomInput.val(String(state.width));
      win.setTimeout(function () {
        inputEl.focus();
        if (typeof inputEl.select === 'function') {
          inputEl.select();
        }
      }, 0);
    }
  }

  function isStageDockExpanded() {
    if (!$stageDock || !$stageDock.length) {
      return false;
    }
    return $stageDock.hasClass('is-open');
  }

  function setStageDockExpanded(expanded) {
    var shouldOpen = !!expanded;
    if ($stageDock && $stageDock.length) {
      $stageDock.toggleClass('is-open', shouldOpen);
    }
    if ($stageDockToggle && $stageDockToggle.length) {
      $stageDockToggle.attr('aria-expanded', shouldOpen ? 'true' : 'false');
    }
    if ($stageDockPanel && $stageDockPanel.length) {
      $stageDockPanel.attr('aria-hidden', shouldOpen ? 'false' : 'true');
    }
  }

  function clearStageDockCloseTimer() {
    if (stageDockCloseTimer) {
      clearTimeout(stageDockCloseTimer);
      stageDockCloseTimer = null;
    }
  }

  function scheduleStageDockClose() {
    clearStageDockCloseTimer();
    stageDockCloseTimer = win.setTimeout(function () {
      var activeElement = doc.activeElement;
      if ($stageDock && $stageDock.length && activeElement && $stageDock[0].contains(activeElement)) {
        return;
      }
      setStageDockExpanded(false);
    }, 120);
  }

  function getViewportSize() {
    var viewportWidth = defaultState.width;
    var viewportHeight = Math.max(220, toNumber(state.frameHeight, defaultState.height));

    if ($stage && $stage.length) {
      viewportWidth = Math.max(300, Math.floor($stage[0].clientWidth));
      viewportHeight = Math.max(220, Math.floor($stage[0].clientHeight));
    } else if ($canvas && $canvas.length) {
      viewportWidth = Math.max(300, Math.floor($canvas[0].clientWidth));
      viewportHeight = Math.max(220, Math.floor($canvas[0].clientHeight));
    }

    return {
      width: viewportWidth,
      height: viewportHeight
    };
  }

  function getFitScale(width, height, bounds) {
    var availableWidth = bounds && Number.isFinite(bounds.width) ? Math.floor(bounds.width) : 0;
    var availableHeight = bounds && Number.isFinite(bounds.height) ? Math.floor(bounds.height) : 0;

    if ((availableWidth <= 0 || availableHeight <= 0) && $canvas && $canvas.length) {
      availableWidth = Math.max(220, Math.floor($canvas[0].clientWidth));
      availableHeight = Math.max(220, Math.floor($canvas[0].clientHeight));
    }

    availableWidth = Math.max(220, availableWidth);
    availableHeight = Math.max(220, availableHeight);

    var frameSize = getFrameOuterSize(width, height, 1);
    var widthScale = availableWidth / Math.max(1, frameSize.scaledWidth);
    var heightScale = availableHeight / Math.max(1, frameSize.scaledHeight);

    return Math.min(1, Math.max(MIN_PREVIEW_SCALE, widthScale));
  }

  function getCurrentFitScale(width, height) {
    var viewportSize = getViewportSize();
    var guideBounds = getGuideBounds(viewportSize.width, viewportSize.height);
    var frameOuterSize = getFrameOuterDimensions(width, height);
    return getFitScale(frameOuterSize.width, frameOuterSize.height, guideBounds);
  }

  function resolveAppliedScale(width, height, bounds) {
    var frameOuterSize = getFrameOuterDimensions(width, height);
    var fitScale = getFitScale(frameOuterSize.width, frameOuterSize.height, bounds);
    if (state.zoom === 'fit') {
      return {
        fitScale: fitScale,
        appliedScale: fitScale,
        didMutateState: false,
        didCapZoom: false
      };
    }

    var requestedScale = zoomValueToScale(parseInt(state.zoom, 10));
    return {
      fitScale: fitScale,
      appliedScale: requestedScale,
      didMutateState: false,
      didCapZoom: false
    };
  }

  function clampPan(frameSize, bounds) {
    void frameSize;
    void bounds;

    state.panX = normalizePan(state.panX);
    state.panY = normalizePan(state.panY);
  }

  function getGuideBounds(viewportWidth, viewportHeight) {
    var fallbackInset = 12;
    var fallback = {
      offsetX: fallbackInset,
      offsetY: fallbackInset,
      width: Math.max(220, viewportWidth - (fallbackInset * 2)),
      height: Math.max(220, viewportHeight - (fallbackInset * 2))
    };

    if (!$viewport || !$viewport.length || !$frameGuide || !$frameGuide.length) {
      return fallback;
    }

    var viewportRect = $viewport[0].getBoundingClientRect();
    var guideRect = $frameGuide[0].getBoundingClientRect();
    if (!viewportRect.width || !viewportRect.height || !guideRect.width || !guideRect.height) {
      return fallback;
    }

    return {
      offsetX: Math.round(guideRect.left - viewportRect.left),
      offsetY: Math.round(guideRect.top - viewportRect.top),
      width: Math.max(220, Math.round(guideRect.width)),
      height: Math.max(220, Math.round(guideRect.height))
    };
  }

  function getZoomDisplay(scale) {
    return String(Math.round(scale * 100)) + '%';
  }

  function syncWidthButtons() {
    if (!$widthButtons || !$widthButtons.length) {
      return;
    }
    $widthButtons.removeClass('is-active');
    if ($widthCustomToggle && $widthCustomToggle.length) {
      $widthCustomToggle.removeClass('is-active');
    }
    if ($widthCustomInput && $widthCustomInput.length) {
      $widthCustomInput.val(String(state.width));
    }
    var defaultCustomLabel = __('Custom width', 'vareon-calculator-suite-lite');
    if ($widthCustomToggle && $widthCustomToggle.length) {
      $widthCustomToggle
        .attr('title', defaultCustomLabel)
        .attr('aria-label', defaultCustomLabel);
    }
    var $active = $();
    $widthButtons.each(function () {
      var $btn = $(this);
      var preset = $btn.attr('data-width-preset') || $btn.attr('data-width') || '';
      if (!preset) {
        return;
      }
      if (resolveWidthPresetToPx(preset) === state.width) {
        $active = $btn;
      }
    });
    if ($active.length) {
      $active.addClass('is-active');
      setCustomWidthPopoverOpen(false);
      return;
    }
    if ($widthCustomToggle && $widthCustomToggle.length) {
      var customLabel = sprintf
        ? sprintf(__('Custom width: %spx', 'vareon-calculator-suite-lite'), state.width)
        : defaultCustomLabel + ': ' + String(state.width) + 'px';
      $widthCustomToggle
        .addClass('is-active')
        .attr('title', customLabel)
        .attr('aria-label', customLabel);
    }
  }

  function syncZoomButtons(scale) {
    if ($zoomValue && $zoomValue.length) {
      $zoomValue.text(getZoomDisplay(scale));
    }
  }

  function syncTopbarStatus(width, scale) {
    if ($statusWidthValue && $statusWidthValue.length) {
      $statusWidthValue.text(String(width) + 'px');
    }
    if ($statusZoomValue && $statusZoomValue.length) {
      $statusZoomValue.text(getZoomDisplay(scale));
    }
  }

  function getDeviceClass(width) {
    if (width <= 480) {
      return 'phone';
    }
    if (width <= 900) {
      return 'tablet';
    }
    return 'desktop';
  }

  function toPx(value) {
    var parsed = parseFloat(value);
    return Number.isFinite(parsed) ? parsed : 0;
  }

  function getFrameShellChromeSize() {
    if (!$frameShell || !$frameShell.length || !$frameShell[0] || typeof win.getComputedStyle !== 'function') {
      return {
        width: 0,
        height: 0
      };
    }

    var styles = win.getComputedStyle($frameShell[0]);
    return {
      width:
        toPx(styles.paddingLeft) +
        toPx(styles.paddingRight) +
        toPx(styles.borderLeftWidth) +
        toPx(styles.borderRightWidth),
      height:
        toPx(styles.paddingTop) +
        toPx(styles.paddingBottom) +
        toPx(styles.borderTopWidth) +
        toPx(styles.borderBottomWidth)
    };
  }

  function getFrameOuterDimensions(contentWidth, contentHeight) {
    var rawWidth = Math.max(1, contentWidth);
    var rawHeight = Math.max(1, contentHeight);
    var frameChrome = getFrameShellChromeSize();

    return {
      width: rawWidth + frameChrome.width,
      height: rawHeight + frameChrome.height
    };
  }

  function getFrameOuterSize(contentWidth, contentHeight, scale) {
    var frameOuterSize = getFrameOuterDimensions(contentWidth, contentHeight);

    return {
      scaledWidth: Math.max(1, frameOuterSize.width * scale),
      scaledHeight: Math.max(1, frameOuterSize.height * scale)
    };
  }

  function setLoading(active) {
    if (!$loading || !$loading.length) {
      return;
    }
    isLoading = !!active;
    $loading
      .toggleClass('vareoncalc-is-hidden', !isLoading)
      .prop('hidden', !isLoading)
      .attr('aria-busy', isLoading ? 'true' : 'false');
    if (loadingTimer) {
      clearTimeout(loadingTimer);
      loadingTimer = null;
    }
    if (isLoading) {
      loadingTimer = setTimeout(function () {
        setLoading(false);
      }, 5000);
    }
  }

  function syncFrameViewportMask() {
    if (!frameHost) {
      return;
    }

    frameHost.style.position = 'relative';
    frameHost.style.overflow = 'hidden';

    if (!bridge || !bridge.frame) {
      return;
    }

    var frame = bridge.frame;
    var visualWidth = normalizeWidth(state.width);
    var surfaceWidth = getPreviewSurfaceWidth();
    var visibleWidth = Math.max(1, frameHost.clientWidth || visualWidth);

    frame.style.display = 'block';
    frame.style.height = '100%';
    frame.style.maxWidth = 'none';
    frame.style.margin = '0';
    frame.style.border = '0';
    frame.style.position = 'relative';
    frame.style.top = '0';

    if (surfaceWidth > visibleWidth) {
      frame.style.width = surfaceWidth + 'px';
      frame.style.left = (-(surfaceWidth - visibleWidth) / 2) + 'px';
      return;
    }

    frame.style.width = '100%';
    frame.style.left = '0px';
  }

  function applyViewportLayout() {
    if (!$viewport || !$viewport.length || !$frameShell || !$frameShell.length || !$canvas || !$canvas.length) {
      return {
        didMutateState: false,
        scale: 1
      };
    }

    var visualWidth = normalizeWidth(state.width);
    var baseHeight = Math.max(220, toNumber(state.frameHeight, 220));
    var viewportSize = getViewportSize();
    var viewportWidth = viewportSize.width;
    var viewportHeight = viewportSize.height;
    var didMutateState = false;
    var panBeforeLayout = {
      x: state.panX,
      y: state.panY
    };

    $viewport.css({
      width: viewportWidth + 'px',
      height: viewportHeight + 'px'
    });

    var guideBounds = getGuideBounds(viewportWidth, viewportHeight);
    var deviceClass = getDeviceClass(visualWidth);
    var frameChrome = getFrameShellChromeSize();

    if (state.width !== visualWidth) {
      didMutateState = true;
    }
    state.width = visualWidth;
    state.height = defaultState.height;

    $frameShell.attr('data-device', deviceClass);
    var scaleResolution = resolveAppliedScale(visualWidth, baseHeight, guideBounds);
    var scale = scaleResolution.appliedScale;
    if (scaleResolution.didMutateState) {
      didMutateState = true;
    }
    var frameSize = getFrameOuterSize(visualWidth, baseHeight, scale);

    var centeringOffsetY = 0;

    clampPan(frameSize, guideBounds);
    if (panBeforeLayout.x !== state.panX || panBeforeLayout.y !== state.panY) {
      didMutateState = true;
    }

    var left = guideBounds.offsetX + ((guideBounds.width - frameSize.scaledWidth) / 2) + state.panX;
    var top = guideBounds.offsetY + centeringOffsetY + state.panY;

    $frameShell.css({
      width: (visualWidth + frameChrome.width) + 'px',
      height: (baseHeight + frameChrome.height) + 'px',
      left: left + 'px',
      top: top + 'px',
      transform: 'scale(' + scale + ')',
      transformOrigin: 'top left'
    });
    $frameShell.attr('data-device', deviceClass);
    syncFrameViewportMask();

    syncWidthButtons();
    syncZoomButtons(scale);
    syncTopbarStatus(visualWidth, scale);

    return {
      didMutateState: didMutateState,
      scale: scale
    };
  }

  function buildConfigFromForm() {
    var baseConfig = $.extend(true, {}, boot.defaultConfig || {});
    var theme = $('#vareoncalc_theme_preset').val() || $("select[name='vareoncalc_lite_options[theme_preset]']").val() || baseConfig.theme || 'light';
    var layout = $('#calculator_layout').val() || $("select[name='vareoncalc_lite_options[calculator_layout]']").val() || baseConfig.layout || 'compact';
    var currencyCode = $('#currency_code').val() || (baseConfig.formatting && baseConfig.formatting.currencyCode) || 'USD';
    var currencySymbol = $('#currency_symbol').val() || (baseConfig.formatting && baseConfig.formatting.currencySymbol) || '$';
    var currencyPosition = $("select[name='vareoncalc_lite_options[currency_position]']").val() || 'before';
    var thousandSeparator = $("select[name='vareoncalc_lite_options[thousand_separator]']").val() || 'comma';
    var decimalSeparator = $("select[name='vareoncalc_lite_options[decimal_separator]']").val() || 'dot';
    var measurementSystem = $("select[name='vareoncalc_lite_options[measurement_system]']").val() || 'mixed';
    var brandingEnabled = $("input[name='vareoncalc_lite_options[branding]']").is(':checked');

    baseConfig.type = sanitizeType(state.type);
    baseConfig.theme = String(theme);
    baseConfig.layout = String(layout);
    baseConfig.branding = !!brandingEnabled;
    baseConfig.design = $.extend({}, baseConfig.design || {}, {
      preview_surface_width: normalizeWidth(state.width)
    });
    baseConfig.formatting = $.extend({}, baseConfig.formatting || {}, {
      currencyCode: String(currencyCode),
      currencySymbol: String(currencySymbol),
      currencyPosition: String(currencyPosition),
      thousandSeparator: String(thousandSeparator),
      decimalSeparator: String(decimalSeparator)
    });
    baseConfig.measurement = $.extend({}, baseConfig.measurement || {}, {
      system: String(measurementSystem)
    });
    baseConfig.viewport = {
      width: getPreviewSurfaceWidth(),
      height: defaultState.height,
      zoom: normalizeZoom(state.zoom),
      panX: normalizePan(state.panX),
      panY: normalizePan(state.panY)
    };

    return baseConfig;
  }

  function buildViewportPayload() {
    return {
      width: getPreviewSurfaceWidth(),
      height: defaultState.height,
      zoom: normalizeZoom(state.zoom),
      panX: normalizePan(state.panX),
      panY: normalizePan(state.panY)
    };
  }

  function pushUpdate(debounceMs) {
    if (!bridge) {
      return;
    }
    var nextConfig = buildConfigFromForm();
    var fingerprint = '';
    try {
      fingerprint = JSON.stringify(nextConfig);
    } catch {
      fingerprint = '';
    }
    if (fingerprint && fingerprint === lastConfigFingerprint) {
      return;
    }
    lastConfigFingerprint = fingerprint;
    var viewportFingerprint = '';
    try {
      viewportFingerprint = JSON.stringify(nextConfig.viewport || {});
    } catch {
      viewportFingerprint = '';
    }
    if (viewportFingerprint) {
      lastViewportFingerprint = viewportFingerprint;
    }
    bridge.updateConfig(nextConfig, debounceMs);
  }

  function pushViewportUpdate(debounceMs) {
    if (!bridge || typeof bridge.setViewport !== 'function') {
      return;
    }
    var viewport = buildViewportPayload();
    var fingerprint = '';
    try {
      fingerprint = JSON.stringify(viewport);
    } catch {
      fingerprint = '';
    }
    if (fingerprint && fingerprint === lastViewportFingerprint) {
      return;
    }
    lastViewportFingerprint = fingerprint;
    bridge.setViewport(viewport, debounceMs);
  }

  function scheduleViewportUpdate() {
    if (viewportUpdateTimer) {
      clearTimeout(viewportUpdateTimer);
    }
    viewportUpdateTimer = setTimeout(function () {
      pushViewportUpdate(0);
    }, 80);
  }

  function scheduleUpdate() {
    if (updateTimer) {
      clearTimeout(updateTimer);
    }
    updateTimer = setTimeout(function () {
      pushUpdate(0);
    }, 120);
  }

  function setWidth(width) {
    var nextWidth = normalizeWidth(width);
    state.widthMode = WIDTH_MODE_FIXED;
    state.width = nextWidth;
    applyViewportLayout();
    saveState();
    scheduleUpdate();
  }

  function setCustomDimensions(width) {
    var nextWidth = normalizeWidth(width);
    state.widthMode = WIDTH_MODE_FIXED;
    state.width = nextWidth;
    applyViewportLayout();
    saveState();
    scheduleUpdate();
  }

  function setZoom(zoom) {
    state.zoom = normalizeZoom(zoom);
    if (state.zoom === 'fit') {
      recenterFrameState();
    }
    applyViewportLayout();
  }

  function resetView() {
    state.zoom = 'fit';
    recenterFrameState();
    state.frameHeight = defaultState.height;
    applyViewportLayout();
  }

  function adjustZoom(step) {
    var currentZoom = state.zoom === 'fit'
      ? Math.round(getCurrentFitScale(state.width, Math.max(220, toNumber(state.frameHeight, 220))) * 100)
      : parseInt(state.zoom, 10);
    var nextZoom = Math.round(clampNumber(currentZoom + step, MIN_PREVIEW_ZOOM, MAX_PREVIEW_ZOOM, 100));
    state.zoom = nextZoom;
    applyViewportLayout();
  }

  function bindDragToPan() {
    if (!$canvas || !$canvas.length) {
      return;
    }

    var suppressNextContextMenu = false;
    var framePanActive = false;

    var applyPanAt = function (clientX, clientY) {
      if (!dragSession) {
        return;
      }
      if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) {
        return;
      }
      var deltaX = clientX - dragSession.startX;
      var deltaY = clientY - dragSession.startY;
      state.panX = normalizePan(dragSession.originX + deltaX);
      state.panY = normalizePan(dragSession.originY + deltaY);
      applyViewportLayout();
    };

    var endPan = function (button) {
      if (!dragSession) {
        return;
      }
      var endedButton = Number.isFinite(button) ? button : dragSession.button;
      if (endedButton === 2 || dragSession.button === 2) {
        suppressNextContextMenu = true;
      }
      dragSession = null;
      $canvas.removeClass('is-panning');
      win.removeEventListener('mousemove', onMouseMove, true);
      win.removeEventListener('mouseup', onMouseUp, true);
    };

    var startPan = function (clientX, clientY, button) {
      if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) {
        return;
      }
      if (dragSession) {
        endPan(button);
      }
      dragSession = {
        startX: clientX,
        startY: clientY,
        originX: state.panX,
        originY: state.panY,
        button: Number.isFinite(button) ? button : 0
      };
      $canvas.addClass('is-panning');
      win.addEventListener('mousemove', onMouseMove, true);
      win.addEventListener('mouseup', onMouseUp, true);
    };

    var onMouseMove = function (event) {
      applyPanAt(event.clientX, event.clientY);
    };

    var onMouseUp = function (event) {
      endPan(event && Number.isFinite(event.button) ? event.button : null);
    };

    $canvas.on('mousedown', function (event) {
      var $target = $(event.target);
      if ($target.closest('.vareoncalc-preview-v2__toolbar-top, .vareoncalc-preview-v2__stage-dock').length) {
        return;
      }
      if (event.button !== 0 && event.button !== 2) {
        return;
      }
      if (event.button !== 2 && $target.closest('input,button,select,textarea,a,label').length) {
        return;
      }

      event.preventDefault();
      startPan(event.clientX, event.clientY, event.button);
    });

    $canvas.on('contextmenu', function (event) {
      var $target = $(event.target);
      if ($target.closest('.vareoncalc-preview-v2__toolbar-top, .vareoncalc-preview-v2__stage-dock').length) {
        return;
      }
      if (dragSession || framePanActive || suppressNextContextMenu) {
        event.preventDefault();
        suppressNextContextMenu = false;
      }
    });

    panFromFrameStart = function () {
      if (dragSession) {
        endPan(2);
      }
      framePanActive = true;
      $canvas.addClass('is-panning');
    };
    panFromFrameMove = function (dx, dy) {
      if (!framePanActive) {
        return;
      }
      var deltaX = Number.isFinite(dx) ? dx : 0;
      var deltaY = Number.isFinite(dy) ? dy : 0;
      if (!deltaX && !deltaY) {
        return;
      }
      state.panX = normalizePan(state.panX + deltaX);
      state.panY = normalizePan(state.panY + deltaY);
      applyViewportLayout();
    };
    panFromFrameEnd = function () {
      if (!framePanActive) {
        return;
      }
      framePanActive = false;
      $canvas.removeClass('is-panning');
    };
  }

  function bindWheelZoom() {
    if (!$canvas || !$canvas.length) {
      return;
    }

    $canvas.on('wheel', function (event) {
      var nativeEvent = event.originalEvent || event;
      if (!nativeEvent.ctrlKey && !nativeEvent.metaKey) {
        return;
      }
      event.preventDefault();
      if (nativeEvent.deltaY < 0) {
        adjustZoom(10);
      } else {
        adjustZoom(-10);
      }
    });
  }

  function bindToolbar() {
    if ($calcType && $calcType.length) {
      $calcType.on('change', function () {
        state.type = sanitizeType($(this).val());
        state.zoom = 'fit';
        recenterFrameState();
        state.frameHeight = defaultState.height;
        setLoading(true);
        applyViewportLayout();
        saveState();
        pushUpdate(0);
      });
    }

    if ($widthButtons && $widthButtons.length) {
      $widthButtons.on('click', function () {
        var preset = $(this).attr('data-width-preset') || $(this).attr('data-width');
        if (!preset) {
          return;
        }
        setWidth(resolveWidthPresetToPx(preset));
        setCustomWidthPopoverOpen(false);
      });
    }

    if ($widthCustomToggle && $widthCustomToggle.length && $widthCustomPanel && $widthCustomPanel.length) {
      $widthCustomToggle.on('click', function (event) {
        event.preventDefault();
        if (isCustomWidthPopoverOpen()) {
          setCustomWidthPopoverOpen(false);
          return;
        }
        setCustomWidthPopoverOpen(true, true);
      });
    }

    if ($widthCustomApply && $widthCustomApply.length && $widthCustomInput && $widthCustomInput.length) {
      $widthCustomApply.on('click', function () {
        var parsedWidth = parseInt($widthCustomInput.val(), 10);
        if (!Number.isFinite(parsedWidth)) {
          return;
        }
        setCustomDimensions(parsedWidth);
        setCustomWidthPopoverOpen(false);
      });
    }

    if ($widthCustomInput && $widthCustomInput.length) {
      $widthCustomInput.on('keydown', function (event) {
        if (event.key === 'Enter') {
          event.preventDefault();
          if ($widthCustomApply && $widthCustomApply.length) {
            $widthCustomApply.trigger('click');
          }
        }
      });
    }

    if ($helpToggle && $helpToggle.length) {
      $helpToggle.on('click', function (event) {
        event.preventDefault();
        setHelpPopoverOpen(!isHelpPopoverOpen());
      });
    }

    if ($zoomIn && $zoomIn.length) {
      $zoomIn.on('click', function () {
        adjustZoom(10);
      });
    }

    if ($zoomOut && $zoomOut.length) {
      $zoomOut.on('click', function () {
        adjustZoom(-10);
      });
    }

    if ($zoomFit && $zoomFit.length) {
      $zoomFit.on('click', function () {
        state.zoom = 'fit';
        recenterFrameState();
        state.frameHeight = defaultState.height;
        applyViewportLayout();
      });
    }

    if ($reset && $reset.length) {
      $reset.on('click', function () {
        resetView();
      });
    }

    if ($retry && $retry.length) {
      $retry.on('click', function (event) {
        event.preventDefault();
        if (bridge) {
          setLoading(true);
          state.zoom = 'fit';
          recenterFrameState();
          state.frameHeight = defaultState.height;
          applyViewportLayout();
          bridge.retry();
          syncFrameViewportMask();
        }
      });
    }

    function bindStageDockHoverTarget($target) {
      if (!$target || !$target.length) {
        return;
      }

      $target.on('mouseenter', function () {
        clearStageDockCloseTimer();
        setStageDockExpanded(true);
      });

      $target.on('mouseleave', function () {
        scheduleStageDockClose();
      });
    }

    bindStageDockHoverTarget($stageDock);
    bindStageDockHoverTarget($stageDockHotzone);
    bindStageDockHoverTarget($stageDockToggle);
    bindStageDockHoverTarget($stageDockPanel);

    if ($stageDock && $stageDock.length) {
      $stageDock.on('focusin', function () {
        clearStageDockCloseTimer();
        setStageDockExpanded(true);
      });

      $stageDock.on('focusout', function () {
        win.setTimeout(function () {
          var activeElement = doc.activeElement;
          if ($stageDock[0].contains(activeElement)) {
            return;
          }
          setStageDockExpanded(false);
        }, 0);
      });
    }

    if ($stageDockToggle && $stageDockToggle.length) {
      $stageDockToggle.on('click', function (event) {
        event.preventDefault();
        event.stopPropagation();
        clearStageDockCloseTimer();
        if (!isStageDockExpanded()) {
          setStageDockExpanded(true);
        }
      });
    }

    $(doc).on('mousedown.vareoncalcPreviewV2', function (event) {
      var $target = $(event.target);
      if (isCustomWidthPopoverOpen() && !$target.closest('#vareoncalc-preview-v2-panel-width').length) {
        setCustomWidthPopoverOpen(false);
      }
      if (isHelpPopoverOpen() && !$target.closest('#vareoncalc-preview-v2-help').length) {
        setHelpPopoverOpen(false);
      }
      if (isStageDockExpanded() && !$target.closest('#vareoncalc-preview-v2-stage-dock').length) {
        setStageDockExpanded(false);
      }
    });

    $(doc).on('keydown.vareoncalcPreviewV2', function (event) {
      if (event.key !== 'Escape') {
        return;
      }
      if (isCustomWidthPopoverOpen()) {
        event.preventDefault();
        setCustomWidthPopoverOpen(false);
        if ($widthCustomToggle && $widthCustomToggle.length) {
          $widthCustomToggle.trigger('focus');
        }
        return;
      }
      if (isHelpPopoverOpen()) {
        event.preventDefault();
        setHelpPopoverOpen(false);
        if ($helpToggle && $helpToggle.length) {
          $helpToggle.trigger('focus');
        }
        return;
      }
      if (isStageDockExpanded() && $stageDock && $stageDock.length) {
        var activeElement = doc.activeElement;
        if (!activeElement || !$stageDock[0].contains(activeElement)) {
          return;
        }
        event.preventDefault();
        setStageDockExpanded(false);
        if ($stageDockToggle && $stageDockToggle.length) {
          $stageDockToggle.trigger('focus');
        }
      }
    });
  }

  function initBridge() {
    if (!frameHost) {
      setPreviewAvailability(false);
      return;
    }
    if (!win.VAREONCALC_PREVIEW_V2 || typeof win.VAREONCALC_PREVIEW_V2.create !== 'function') {
      setPreviewAvailability(false);
      return;
    }

    setPreviewAvailability(true);
    bridge = win.VAREONCALC_PREVIEW_V2.create({
      container: frameHost,
      frameUrl: boot.frameUrl,
      initialConfig: buildConfigFromForm(),
      nonce: boot.nonce,
      context: boot.context || 'settings',
      expectedOrigin: boot.expectedOrigin || '',
      timeoutMs: 12000,
      minHeight: 220,
      onReady: function () {
        setPreviewAvailability(true);
        syncFrameViewportMask();
        setLoading(false);
        pushUpdate(0);
      },
      onResize: function (height) {
        var nextHeight = normalizeHeight(height);
        state.frameHeight = nextHeight;
        setPreviewAvailability(true);
        applyViewportLayout();
        setLoading(false);
      },
      onWheelIntent: function (payload) {
        var deltaY = payload && Number.isFinite(payload.deltaY) ? payload.deltaY : 0;
        if (deltaY < 0) {
          adjustZoom(10);
          return;
        }
        if (deltaY > 0) {
          adjustZoom(-10);
        }
      },
      onPanIntent: function (payload) {
        if (!payload || typeof payload.phase !== 'string') {
          return;
        }
        if (payload.phase === 'start' && typeof panFromFrameStart === 'function') {
          panFromFrameStart();
          return;
        }
        if (payload.phase === 'move' && typeof panFromFrameMove === 'function') {
          panFromFrameMove(payload.dx, payload.dy);
          return;
        }
        if (payload.phase === 'end' && typeof panFromFrameEnd === 'function') {
          panFromFrameEnd();
        }
      },
      onError: function (message) {
        void message;
        setPreviewAvailability(false);
        setLoading(false);
      }
    });
    setLoading(true);
    bridge.mount();
    syncFrameViewportMask();
  }

  function bindFormStreaming() {
    if (!$form || !$form.length) {
      return;
    }
    $form.on('change input', 'input,select,textarea', function (event) {
      if ($(event.target).closest('.vareoncalc-preview-v2').length) {
        return;
      }
      scheduleUpdate();
    });
  }

  function initElements() {
    $split = $('#vareoncalc-settings-split');
    $form = $('#vareoncalc-settings-form');
    $canvas = $('#vareoncalc-preview-v2-canvas');
    $stage = $canvas.find('.vareoncalc-preview-v2__stage');
    $stageDock = $('#vareoncalc-preview-v2-stage-dock');
    $stageDockHotzone = $('#vareoncalc-preview-v2-stage-dock-hotzone');
    $stageDockToggle = $('#vareoncalc-preview-v2-stage-dock-toggle');
    $stageDockPanel = $('#vareoncalc-preview-v2-stage-dock-panel');
    $viewport = $('#vareoncalc-preview-v2-viewport');
    $frameGuide = $viewport.find('.vareoncalc-preview-v2__frame-guide');
    $frameShell = $('#vareoncalc-preview-v2-frame-shell');
    $calcType = $('#vareoncalc-preview-v2-type');
    $retry = $('#vareoncalc-preview-v2-retry');
    $reset = $('#vareoncalc-preview-v2-reset');
    $zoomValue = $('#vareoncalc-preview-v2-zoom-value');
    $statusWidthValue = $('#vareoncalc-preview-v2-status-width');
    $statusZoomValue = $('#vareoncalc-preview-v2-status-zoom');
    $zoomFit = $('#vareoncalc-preview-v2-zoom-fit');
    $zoomIn = $('#vareoncalc-preview-v2-zoom-in');
    $zoomOut = $('#vareoncalc-preview-v2-zoom-out');
    $widthButtons = $('.vareoncalc-preview-v2__width-btn[data-width-preset]');
    $widthCustomToggle = $('#vareoncalc-preview-v2-width-custom-toggle');
    $widthCustomPanel = $('#vareoncalc-preview-v2-width-custom-panel');
    $widthCustomInput = $('#vareoncalc-preview-v2-width-custom-input');
    $widthCustomApply = $('#vareoncalc-preview-v2-width-custom-apply');
    $helpToggle = $('#vareoncalc-preview-v2-help-toggle');
    $helpPopover = $('#vareoncalc-preview-v2-help-popover');
    $loading = $('#vareoncalc-preview-v2-loading');
    frameHost = doc.getElementById('vareoncalc-preview-v2-frame-host');
  }

  function init() {
    initElements();
    if (!$form.length || !$split.length) {
      return;
    }

    if (!$canvas.length || !frameHost) {
      setPreviewAvailability(false);
      return;
    }

    loadState();
    setStageDockExpanded(false);
    setCustomWidthPopoverOpen(false);
    setHelpPopoverOpen(false);

    if ($calcType.length) {
      $calcType.val(state.type);
      if (!$calcType.val()) {
        state.type = sanitizeType(defaultState.type);
        $calcType.val(state.type);
      }
    }

    if ($widthCustomInput.length) {
      $widthCustomInput.val(String(state.width));
    }

    applyViewportLayout();
    bindToolbar();
    bindDragToPan();
    bindWheelZoom();
    bindFormStreaming();
    initBridge();

    var scheduleLayoutRefresh = function () {
      win.setTimeout(function () {
        applyViewportLayout();
        pushUpdate(0);
      }, 120);
    };

    $(win).on('resize', function () {
      if (resizeTimer) {
        clearTimeout(resizeTimer);
      }
      resizeTimer = setTimeout(function () {
        applyViewportLayout();
        scheduleViewportUpdate();
      }, 120);
    });

    $(doc).on('click', '[data-tab="tab-general"]', function () {
      scheduleLayoutRefresh();
    });

    if (typeof win.MutationObserver !== 'undefined') {
      var settingsRoot = doc.querySelector('#tab-general');
      var observer = new win.MutationObserver(function () {
        scheduleLayoutRefresh();
      });

      if (settingsRoot) {
        observer.observe(settingsRoot, {
          attributes: true,
          attributeFilter: ['class']
        });
      }
    }
  }

  $(doc).ready(init);
})(jQuery, window, document);
