import { css } from '@emotion/css';
import { type PointerEvent as ReactPointerEvent } from 'react';
import { createPortal } from 'react-dom';

import { type GrafanaTheme2 } from '@grafana/data';
import { logWarning } from '@grafana/runtime';
import {
  sceneGraph,
  type SceneComponentProps,
  SceneObjectBase,
  type SceneObjectRef,
  type SceneObjectState,
  VizPanel,
  type SceneGridItemLike,
} from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { getLayoutType } from 'app/features/dashboard/utils/tracking';

import { dashboardEditActions, DashboardStateChangedEvent, ObjectsReorderedOnCanvasEvent } from '../edit-pane/shared';
import { DashboardInteractions } from '../utils/interactions';
import { getDefaultVizPanel, getLayoutForObject } from '../utils/utils';

import { DashboardScene } from './DashboardScene';
import { AutoGridLayoutManager } from './layout-auto-grid/AutoGridLayoutManager';
import { type DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
import { type RowItem } from './layout-rows/RowItem';
import { RowsLayoutManager } from './layout-rows/RowsLayoutManager';
import { TabItem } from './layout-tabs/TabItem';
import { TabsLayoutManager } from './layout-tabs/TabsLayoutManager';
import {
  AUTO_GRID_ITEM_DROP_TARGET_ATTR,
  DASHBOARD_DROP_TARGET_KEY_ATTR,
  type DashboardDropTarget,
  isDashboardDropTarget,
} from './types/DashboardDropTarget';

const TAB_ACTIVATION_DELAY_MS = 600;

interface DashboardLayoutOrchestratorState extends SceneObjectState {
  /** Grid item currently being dragged */
  draggingGridItem?: SceneObjectRef<SceneGridItemLike>;
  /** Row currently being dragged */
  draggingRow?: SceneObjectRef<RowItem>;
  /** Key of the source tab where drag started */
  sourceTabKey?: string;
  /** Key of the tab currently being hovered during drag */
  hoverTabKey?: string;
  /** Preview state for cross-tab drag */
  dragPreview?: {
    x: number;
    y: number;
    width: number;
    height: number;
    /** Offset from cursor to top-left of preview (preserves click position) */
    offsetX: number;
    offsetY: number;
    label: string;
    type: 'panel' | 'row';
  };
}

export type TabDragState = {
  tab: TabItem;
  /** Width of the dragged tab header in pixels (for cross-manager placeholder sizing) */
  width: number;
  /** Height of the dragged tab header in pixels (for cross-manager placeholder sizing) */
  height: number;
  index: number;
};

export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayoutOrchestratorState> {
  public static Component = DragPreviewRenderer;

  private _sourceDropTarget: DashboardDropTarget | null = null;
  private _lastDropTarget: DashboardDropTarget | null = null;
  private _tabActivationTimer: ReturnType<typeof setTimeout> | null = null;
  private _lastHoveredTabKey: string | null = null;
  /** Track if item was detached from source during cross-tab drag */
  private _itemDetachedFromSource = false;
  /** Cached label for the preview */
  private _previewLabel = '';
  /** Cached type for the preview */
  private _previewType: 'panel' | 'row' = 'panel';
  /** Cached dimensions for the preview */
  private _previewWidth = 0;
  private _previewHeight = 0;
  /** Last known cursor position */
  private _lastCursorX = 0;
  private _lastCursorY = 0;
  /** Offset from cursor to item's top-left corner (captured on drag start) */
  private _dragOffsetX = 0;
  private _dragOffsetY = 0;
  /** Source layout manager for row drag (for removal before tab switch) */
  private _sourceRowsLayout: RowsLayoutManager | null = null;
  /** Flag to track if row drag offset has been captured */
  private _rowOffsetCaptured = false;
  /** Current drop position for AutoGrid (index where item will be inserted) */
  private _currentDropPosition: number | null = null;
  /** Last hovered AutoGrid item key (to prevent flickering) */
  private _lastHoveredAutoGridItemKey: string | null = null;
  /** Original child index of the dragged item before it was detached from its source layout */
  private _sourceOriginalIndex: number | null = null;
  private _tabDragState: TabDragState | undefined;
  /** Stored pointerup handler for new-panel drag so we can remove it */
  private _dropNewItemPointerUpHandler: ((evt: PointerEvent) => void) | null = null;

  public constructor() {
    super({});

    this._onPointerMove = this._onPointerMove.bind(this);
    this._stopDraggingSync = this._stopDraggingSync.bind(this);

    this._onTabDragPointerMove = this._onTabDragPointerMove.bind(this);
    this._onTabDragPointerUp = this._onTabDragPointerUp.bind(this);

    this.addActivationHandler(() => this._activationHandler());
  }

  private _activationHandler() {
    return () => {
      document.body.removeEventListener('pointermove', this._onPointerMove);
      document.body.removeEventListener('pointermove', this._onRowDragPointerMove);
      document.body.removeEventListener('pointermove', this._onTabDragPointerMove);
      document.body.removeEventListener('pointerup', this._stopDraggingSync, true);
      document.body.removeEventListener('pointerup', this._onRowDragPointerUp, true);
      document.body.removeEventListener('pointerup', this._onTabDragPointerUp, true);
      this._clearTabActivationTimer();
      this._clearDragPreview();
      this._cleanupDragState();
    };
  }

  /**
   * Returns true if the current drag operation will drop the item to a different layout
   * than where it started. Used by AutoGridLayout to know whether to clear draggingKey.
   */
  public isDroppedElsewhere(): boolean {
    return this._lastDropTarget !== null && this._lastDropTarget !== this._sourceDropTarget;
  }

  public startDraggingSync(evt: ReactPointerEvent, gridItem: SceneGridItemLike): void {
    const dropTarget = sceneGraph.findObject(gridItem, isDashboardDropTarget);

    if (!dropTarget || !isDashboardDropTarget(dropTarget)) {
      return;
    }

    this._sourceDropTarget = dropTarget;
    this._lastDropTarget = dropTarget;
    this._sourceOriginalIndex = this._getGridItemIndex(gridItem);

    // Capture the offset from cursor to item's top-left corner
    this._captureDragOffset(evt.clientX, evt.clientY, gridItem);

    document.body.addEventListener('pointermove', this._onPointerMove);
    // Use capture phase to ensure we receive the event even if something calls stopPropagation
    // (e.g., tab headers call stopPropagation on pointerup)
    document.body.addEventListener('pointerup', this._stopDraggingSync, true);

    const sourceTabKey = this._findParentTabKey(gridItem);
    this.setState({ draggingGridItem: gridItem.getRef(), sourceTabKey });
  }

  public startDraggingNewPanel(action: 'newPanel' | 'paste'): void {
    this._dropNewItemPointerUpHandler = (evt: PointerEvent) => this._dropNew(evt, action);
    document.body.addEventListener('pointermove', this._onNewPanelPointerMove, true);
    document.body.addEventListener('pointerup', this._dropNewItemPointerUpHandler, true);
  }

  private _stopDraggingSync(evt: PointerEvent) {
    const gridItem = this.state.draggingGridItem?.resolve();
    const wasDetached = this._itemDetachedFromSource;
    // Capture these before cleanup since setTimeout runs after cleanup
    const sourceDropTarget = this._sourceDropTarget;
    const lastDropTarget = this._lastDropTarget;
    const dropPosition = this._currentDropPosition;
    const sourceOriginalIndex = this._sourceOriginalIndex;

    // Fresh target under the pointer at drop time takes priority over
    // lastDropTarget which may be stale if pointermove events were coalesced.
    const validDropTargetUnderMouse = this._getDropTargetUnderMouse(evt);
    const effectiveDropTarget = validDropTargetUnderMouse ?? lastDropTarget;

    // When effectiveDropTarget differs from lastDropTarget (e.g. due to coalesced
    // pointermove), clear stale visual drop state on the previous target so it
    // doesn't keep showing a placeholder/highlight after the drop.
    if (effectiveDropTarget !== lastDropTarget && lastDropTarget) {
      lastDropTarget.setDropPosition?.(null);
      lastDropTarget.setIsDropTarget?.(false);
      this._currentDropPosition = null;
      this._lastHoveredAutoGridItemKey = null;
    }

    // TabsLayoutManager is not a valid panel drop target — it represents the
    // tab bar area between tab headers. Cancel the drop so the panel either
    // stays in place or returns to its source layout.
    if (effectiveDropTarget instanceof TabsLayoutManager) {
      this._clearDropPosition();
      this._lastDropTarget?.setIsDropTarget?.(false);

      if (wasDetached && gridItem) {
        setTimeout(() => {
          sourceDropTarget?.draggedGridItemInside?.(gridItem, sourceOriginalIndex ?? undefined);
          if (sourceDropTarget instanceof AutoGridLayoutManager) {
            sourceDropTarget.state.layout.endExternalDrag();
          }
        });
      }
    } else if (wasDetached && !validDropTargetUnderMouse && gridItem && effectiveDropTarget instanceof TabItem) {
      // If item was detached (cross-tab drag started) but there's no valid drop target under mouse,
      // drop into the current tab if lastDropTarget is a TabItem (e.g., dropped on tab header)
      setTimeout(() => {
        effectiveDropTarget.draggedGridItemInside?.(gridItem);
        if (sourceDropTarget instanceof AutoGridLayoutManager) {
          sourceDropTarget.state.layout.endExternalDrag();
        }
      });
    } else {
      const isCrossLayoutDrop = sourceDropTarget !== effectiveDropTarget || wasDetached;

      // Handle cross-layout or cross-tab drop
      if (isCrossLayoutDrop) {
        // Wrapped in setTimeout to ensure that any event handlers are called
        // Useful for allowing react-grid-layout to remove placeholders, etc.
        setTimeout(() => {
          if (gridItem) {
            // Only remove from source if not already detached during tab switch
            if (!wasDetached) {
              sourceDropTarget?.draggedGridItemOutside?.(gridItem);
            }
            // Pass drop position for precise placement (AutoGrid uses this)
            // Note: draggedGridItemInside also clears isDropTarget and dropPosition
            effectiveDropTarget?.draggedGridItemInside?.(gridItem, dropPosition ?? undefined);

            // Clean up source grid's drag state (CSS variables and draggingKey) after item is moved.
            // This is done here (after movement) to prevent flickering where the item
            // would momentarily appear at wrong position (CSS vars cleared but draggingKey set
            // = absolute positioning with no valid position values).
            if (sourceDropTarget instanceof AutoGridLayoutManager) {
              sourceDropTarget.state.layout.endExternalDrag();
            }
          } else {
            const warningMessage = 'No grid item to drag';
            console.warn(warningMessage);
            logWarning(warningMessage);
          }
        });
      } else {
        // For same-layout drops, clear drop position state synchronously
        this._clearDropPosition();
        this._lastDropTarget?.setIsDropTarget?.(false);
      }
    }

    document.body.removeEventListener('pointermove', this._onPointerMove);
    document.body.removeEventListener('pointerup', this._stopDraggingSync, true);

    this._clearTabActivationTimer();
    this._clearDragPreview();

    // Clear internal tracking state (but not the visual state on the target for cross-layout drops)
    this._currentDropPosition = null;
    this._lastHoveredAutoGridItemKey = null;
    this._lastDropTarget = null;
    this._sourceDropTarget = null;
    this._itemDetachedFromSource = false;
    this._sourceOriginalIndex = null;
    this.setState({ draggingGridItem: undefined, sourceTabKey: undefined, hoverTabKey: undefined });
  }

  /**
   * Called when a row drag starts (from RowsLayoutManagerRenderer)
   */
  public startRowDrag(row: RowItem): void {
    const sourceTabKey = this._findParentTabKey(row);

    // Store source layout info for removal before tab switch
    const parent = row.parent;
    if (parent instanceof RowsLayoutManager) {
      this._sourceRowsLayout = parent;
    }

    // Capture row dimensions
    this._captureRowDimensions(row);

    // Offset will be captured on first pointermove
    this._rowOffsetCaptured = false;

    this.setState({
      draggingRow: row.getRef(),
      sourceTabKey,
    });

    // Add pointer move listener for tab hover detection during row drag
    document.body.addEventListener('pointermove', this._onRowDragPointerMove);
    // Add pointerup listener to handle drop after cross-tab switch
    // Use capture phase to ensure we receive the event even if something calls stopPropagation
    document.body.addEventListener('pointerup', this._onRowDragPointerUp, true);
  }

  public startTabDrag(sourceTabsManagerId: string, draggedTabId: string): void {
    this._sourceDropTarget = this._findDropTargetByKey(sourceTabsManagerId);

    const draggedTab = sceneGraph.findByKeyAndType(this._getDashboard(), draggedTabId, TabItem);
    if (this._sourceDropTarget instanceof TabsLayoutManager) {
      this._sourceDropTarget.forceSelectTab(draggedTabId);
    }

    // Calculate dimensions of the dragged tab header and cache for cross-manager placeholder sizing
    const draggedHeaderEl = draggedTab?.containerRef?.current ?? undefined;
    if (draggedHeaderEl) {
      const rect = draggedHeaderEl.getBoundingClientRect();
      this._tabDragState = {
        tab: draggedTab,
        width: rect.width,
        height: rect.height,
        index: 0,
      };
    }

    document.body.addEventListener('pointerup', this._onTabDragPointerUp, true);
    document.body.addEventListener('pointermove', this._onTabDragPointerMove);
  }

  private _onTabDragPointerMove(evt: PointerEvent) {
    // Note this will never be the same as source - _getDropTargetUnderMouse prevents it.
    const dropTarget = this._getDropTargetUnderMouse(evt);

    if (!this._tabDragState) {
      return;
    }

    // Tabs can be dropped only to TabsLayoutManager
    if (dropTarget instanceof TabsLayoutManager) {
      this._lastDropTarget = dropTarget;
    } else {
      this.cleanUpTabDropTarget();
      return;
    }

    dropTarget.setIsDropTarget(true);
    const tabUnderMouse = this._getTabUnderMouse(evt.clientX, evt.clientY, this._tabDragState.tab.state.key);
    const targetTabIndex = dropTarget?.getTabsIncludingRepeats().findIndex((t) => t.state.key === tabUnderMouse);
    // move placeholder only when hovering over a tab, if not we may be outside of the drop area or over a placeholder
    if (targetTabIndex !== -1) {
      this._tabDragState.index = targetTabIndex === this._tabDragState.index ? targetTabIndex + 1 : targetTabIndex;
      dropTarget.setPlaceholder(this._tabDragState);
    }
  }

  private _onTabDragPointerUp() {
    document.body.removeEventListener('pointermove', this._onTabDragPointerMove);
    document.body.removeEventListener('pointerup', this._onTabDragPointerUp, true);
    // Note: do not handle dropping yet as pangea is still waiting for events to be fired from the dragged tab.
    // Finishing dropping in here would remove elements from dom causing pangea to think the element is still being
    // dragged and won't fire onDragEnd until container is re-rendered or removed (e.g. when row is collapsed).
  }

  /**
   * Single entry point for handling tab drag end. If the tab was dropped from a different manager we use values
   * calculated by layout orchestrator.
   */
  public stopTabDrag(targetIndex: number | undefined): void {
    if (this._sourceDropTarget === null || !(this._sourceDropTarget instanceof TabsLayoutManager)) {
      return;
    }

    if (!this._tabDragState?.tab) {
      return;
    }

    const sourceManager = this._sourceDropTarget;
    const sourceIndex = sourceManager.getTabsIncludingRepeats().findIndex((t) => t === this._tabDragState?.tab);

    // targetIndex !== undefined => dropped in the same manager, target index provided by hello-pangea
    if (targetIndex !== undefined) {
      if (sourceIndex !== targetIndex) {
        sourceManager.moveTab(sourceIndex, targetIndex);
      }
      this.cleanUpTabDrag();
      return;
    }

    // dropped in a different manager => handled by the orchestrator
    if (this._lastDropTarget instanceof TabsLayoutManager) {
      const destinationManager = this._lastDropTarget;
      targetIndex = this._tabDragState.index ?? destinationManager.getTabsIncludingRepeats().length;

      const realDestinationIndex = destinationManager.mapTabInsertIndex(targetIndex);
      // When moving a tab into a new tab group, make it the active tab.
      this._moveTabBetweenManagers(this._tabDragState.tab, sourceManager, destinationManager, realDestinationIndex);
      this.cleanUpTabDrag();
    }
  }

  private cleanUpTabDropTarget() {
    if (this._lastDropTarget && this._lastDropTarget instanceof TabsLayoutManager) {
      this._lastDropTarget.setIsDropTarget?.(false);
    }
    this._lastDropTarget = null;
  }

  private cleanUpTabDrag() {
    this.cleanUpTabDropTarget();
    this._tabDragState = undefined;
  }

  private _moveTabBetweenManagers(
    tab: TabItem,
    source: TabsLayoutManager,
    destination: TabsLayoutManager,
    destinationIndex: number
  ) {
    const prevSourceTabs = [...source.state.tabs];
    const prevSourceSlug = source.state.currentTabSlug;
    const prevDestinationTabs = [...destination.state.tabs];
    const prevDestinationSlug = destination.state.currentTabSlug;

    dashboardEditActions.moveElement({
      source,
      movedObject: tab,
      perform: () => {
        const sourceTabs = [...prevSourceTabs];
        const destTabs = [...prevDestinationTabs];

        const fromIndex = sourceTabs.findIndex((t) => t === tab);
        if (fromIndex < 0) {
          return;
        }

        sourceTabs.splice(fromIndex, 1);

        const clampedDestinationIndex = Math.max(0, Math.min(destinationIndex, destTabs.length));
        destTabs.splice(clampedDestinationIndex, 0, tab);

        // If the moved tab was active in source, pick a sensible new active tab.
        let nextSourceSlug = prevSourceSlug;
        if (prevSourceSlug === tab.getSlug()) {
          const newIndex = fromIndex > 0 ? fromIndex - 1 : 0;
          nextSourceSlug = sourceTabs[newIndex]?.getSlug();
        }

        // Important: avoid briefly parenting the same SceneObject in two places.
        // Remove from source first, then clear parent, then add to destination.
        source.setState({ tabs: sourceTabs, currentTabSlug: nextSourceSlug });
        tab.clearParent();
        destination.setState({
          tabs: destTabs,
          currentTabSlug: tab.getSlug(),
        });

        // Make sure outline is refreshed in DashboardEditPane
        source.publishEvent(new ObjectsReorderedOnCanvasEvent(source), true);
        destination.publishEvent(new ObjectsReorderedOnCanvasEvent(destination), true);

        // Make sure repeated rows are refreshed - triggers performRowRepeats in RowItemRepeater
        source.publishEvent(new DashboardStateChangedEvent({ source }), true);
        destination.publishEvent(new DashboardStateChangedEvent({ source: destination }), true);
      },
      undo: () => {
        // Reverse order for the same parenting reason as in perform().
        destination.setState({ tabs: prevDestinationTabs, currentTabSlug: prevDestinationSlug });
        tab.clearParent();
        source.setState({ tabs: prevSourceTabs, currentTabSlug: prevSourceSlug });

        // Make sure outline is refreshed in DashboardEditPane
        source.publishEvent(new ObjectsReorderedOnCanvasEvent(source), true);
        destination.publishEvent(new ObjectsReorderedOnCanvasEvent(destination), true);

        // Make sure repeated rows are refreshed - triggers performRowRepeats in RowItemRepeater
        source.publishEvent(new DashboardStateChangedEvent({ source }), true);
        destination.publishEvent(new DashboardStateChangedEvent({ source: destination }), true);
      },
    });
  }

  private _onRowDragPointerMove = (evt: PointerEvent): void => {
    // Capture row offset on first move (we don't have cursor position at drag start)
    if (!this._rowOffsetCaptured) {
      const row = this.state.draggingRow?.resolve();
      if (row) {
        this._captureRowDragOffset(evt.clientX, evt.clientY, row);
        this._rowOffsetCaptured = true;
      }
    }

    // Store cursor position early so it's available for immediate preview on tab switch
    this._lastCursorX = evt.clientX;
    this._lastCursorY = evt.clientY;

    this._checkTabHover(evt.clientX, evt.clientY);
    this._updateDragPreview(evt.clientX, evt.clientY);
  };

  private _onRowDragPointerUp = (_evt: PointerEvent): void => {
    // Always clear the tab activation timer on pointerup to prevent
    // the tab from switching after the user has released the mouse
    this._clearTabActivationTimer();

    // Handle drop after cross-tab row drag
    if (this._itemDetachedFromSource) {
      const row = this.state.draggingRow?.resolve();
      if (row) {
        // Find the drop target under cursor and add row to it
        const dropTarget = this._lastDropTarget ?? this._getDropTargetUnderMouse(_evt);
        if (dropTarget instanceof TabItem) {
          dropTarget.acceptDroppedRow?.(row);
        }
      }
      this._finalizeRowDrag();
    }
    // If not detached, stopRowDrag from hello-pangea/dnd will handle cleanup
  };

  private _cleanupDragState() {
    document.body.removeEventListener('pointermove', this._onNewPanelPointerMove, true);
    if (this._dropNewItemPointerUpHandler) {
      document.body.removeEventListener('pointerup', this._dropNewItemPointerUpHandler, true);
      this._dropNewItemPointerUpHandler = null;
    }
    this._lastDropTarget = null;
  }

  private _dropNew = (evt: PointerEvent, action: 'newPanel' | 'paste'): void => {
    const lastDropTarget = this._lastDropTarget;
    const elementsUnderPoint = document.elementsFromPoint(evt.clientX, evt.clientY);

    // if the cursor is in the sidebar, don't add panel
    const isInSidebar = elementsUnderPoint.some((el) => el.getAttribute('id') === 'sidebar-container');

    if (isInSidebar) {
      this._cleanupDragState();
      return;
    }
    // defer so the droppable doesn't rerender during dnd drag-end, resulting in a console warning
    requestAnimationFrame(() => {
      if (action === 'paste') {
        this._pastePanelToLayout(lastDropTarget);
      } else {
        this._addNewPanelToLayout(lastDropTarget);
      }
      this._cleanupDragState();
    });
    return;
  };

  private _getLayoutForDropTarget = (
    dropTarget: DashboardDropTarget | null
  ): DashboardScene | AutoGridLayoutManager | DefaultGridLayoutManager => {
    const dashboard = this._getDashboard();
    return getLayoutForObject(dropTarget ?? dashboard) ?? dashboard;
  };

  private _addNewPanelToLayout = (dropTarget: DashboardDropTarget | null) => {
    const panel = getDefaultVizPanel();
    this._getLayoutForDropTarget(dropTarget).addPanel(panel);
    DashboardInteractions.trackAddPanelClick('sidebar', dropTarget ? getLayoutType(dropTarget) : 'dashboard', 'drop');
  };

  private _pastePanelToLayout = (dropTarget: DashboardDropTarget | null) => {
    const layout = this._getLayoutForDropTarget(dropTarget);
    // shouldn't be undefined
    layout.pastePanel();
    DashboardInteractions.trackPastePanelClick('sidebar', dropTarget ? getLayoutType(dropTarget) : 'dashboard', 'drop');
  };

  /**
   * Called when a row drag ends (from RowsLayoutManagerRenderer)
   * This is called by hello-pangea/dnd when drag ends normally (within same layout)
   * For cross-tab drags, the row is already detached and _onRowDragPointerUp handles the drop
   */
  public stopRowDrag(): void {
    // If the row was detached (cross-tab drag), don't clean up yet
    // The pointerup handler will handle cleanup after drop
    if (this._itemDetachedFromSource) {
      return;
    }

    this._finalizeRowDrag();
  }

  private _finalizeRowDrag(): void {
    document.body.removeEventListener('pointermove', this._onRowDragPointerMove);
    document.body.removeEventListener('pointerup', this._onRowDragPointerUp, true);
    this._clearTabActivationTimer();
    this._clearDragPreview();
    this._lastDropTarget?.setIsDropTarget?.(false);
    this._lastDropTarget = null;
    this._sourceDropTarget = null;
    this._itemDetachedFromSource = false;
    this._sourceRowsLayout = null;
    this._rowOffsetCaptured = false;
    this.setState({
      draggingRow: undefined,
      sourceTabKey: undefined,
      hoverTabKey: undefined,
    });
  }

  private _clearTabActivationTimer(): void {
    if (this._tabActivationTimer) {
      clearTimeout(this._tabActivationTimer);
      this._tabActivationTimer = null;
    }
  }

  private _updateDragPreview(x: number, y: number): void {
    // Store cursor position for immediate preview on tab switch
    this._lastCursorX = x;
    this._lastCursorY = y;

    if (this._itemDetachedFromSource) {
      this.setState({
        dragPreview: {
          x,
          y,
          width: this._previewWidth,
          height: this._previewHeight,
          offsetX: this._dragOffsetX,
          offsetY: this._dragOffsetY,
          label: this._previewLabel,
          type: this._previewType,
        },
      });
    }
  }

  private _showDragPreview(): void {
    // Prevent text selection and set move cursor during cross-tab drag
    document.body.classList.add('dashboard-draggable-transparent-selection');
    document.body.classList.add('dragging-active');

    this.setState({
      dragPreview: {
        x: this._lastCursorX,
        y: this._lastCursorY,
        width: this._previewWidth,
        height: this._previewHeight,
        offsetX: this._dragOffsetX,
        offsetY: this._dragOffsetY,
        label: this._previewLabel,
        type: this._previewType,
      },
    });
  }

  private _captureDragOffset(cursorX: number, cursorY: number, gridItem: SceneGridItemLike): void {
    // Both DashboardGridItem and AutoGridItem have containerRef
    if ('containerRef' in gridItem) {
      const containerRef = gridItem.containerRef;
      if (
        containerRef &&
        typeof containerRef === 'object' &&
        'current' in containerRef &&
        containerRef.current instanceof HTMLElement
      ) {
        const rect = containerRef.current.getBoundingClientRect();
        // Offset is cursor position minus item's top-left
        this._dragOffsetX = cursorX - rect.left;
        this._dragOffsetY = cursorY - rect.top;
        return;
      }
    }

    // Fallback: center the preview on cursor
    this._dragOffsetX = 0;
    this._dragOffsetY = 0;
  }

  private _captureItemDimensions(gridItem: SceneGridItemLike): void {
    // Both DashboardGridItem and AutoGridItem have containerRef
    if ('containerRef' in gridItem) {
      const containerRef = gridItem.containerRef;
      if (
        containerRef &&
        typeof containerRef === 'object' &&
        'current' in containerRef &&
        containerRef.current instanceof HTMLElement
      ) {
        const rect = containerRef.current.getBoundingClientRect();
        this._previewWidth = rect.width;
        this._previewHeight = rect.height;
        return;
      }
    }

    // Fallback to reasonable default
    this._previewWidth = 400;
    this._previewHeight = 300;
  }

  private _captureRowDimensions(row: RowItem): void {
    // Try to find the DOM element for the row using DASHBOARD_DROP_TARGET_KEY_ATTR
    const element = document.querySelector(`[${DASHBOARD_DROP_TARGET_KEY_ATTR}="${row.state.key}"]`);
    if (element) {
      const rect = element.getBoundingClientRect();
      this._previewWidth = rect.width;
      this._previewHeight = rect.height;
      return;
    }

    // Fallback to reasonable default for rows
    this._previewWidth = 800;
    this._previewHeight = 48;
  }

  private _captureRowDragOffset(cursorX: number, cursorY: number, row: RowItem): void {
    // Try to find the DOM element for the row
    const element = document.querySelector(`[${DASHBOARD_DROP_TARGET_KEY_ATTR}="${row.state.key}"]`);
    if (element) {
      const rect = element.getBoundingClientRect();
      this._dragOffsetX = cursorX - rect.left;
      this._dragOffsetY = cursorY - rect.top;
      return;
    }

    // Fallback: use small offset
    this._dragOffsetX = 20;
    this._dragOffsetY = 20;
  }

  private _clearDragPreview(): void {
    // Re-enable text selection and reset cursor
    document.body.classList.remove('dashboard-draggable-transparent-selection');
    document.body.classList.remove('dragging-active');
    window.getSelection()?.removeAllRanges();

    if (this.state.dragPreview) {
      this.setState({ dragPreview: undefined });
    }
  }

  private _checkTabHover(clientX: number, clientY: number): void {
    const tabKey = this._getTabUnderMouse(clientX, clientY);

    if (tabKey !== this._lastHoveredTabKey) {
      // Cursor moved to a different tab or left all tabs
      this._clearTabActivationTimer();
      this._lastHoveredTabKey = tabKey;

      if (tabKey) {
        // Check if this tab is already active - no need to switch
        if (this._isTabAlreadyActive(tabKey)) {
          this.setState({ hoverTabKey: undefined });
          return;
        }

        // Start new timer for the new tab
        this._tabActivationTimer = setTimeout(() => {
          this._activateTab(tabKey);
        }, TAB_ACTIVATION_DELAY_MS);

        this.setState({ hoverTabKey: tabKey });
      } else {
        this.setState({ hoverTabKey: undefined });
      }
    }
  }

  private _isTabAlreadyActive(tabKey: string): boolean {
    const dashboard = this._getDashboard();
    const tabItem = sceneGraph.findByKey(dashboard, tabKey);

    if (tabItem instanceof TabItem) {
      const tabsManager = tabItem.getParentLayout();
      if (tabsManager instanceof TabsLayoutManager) {
        const currentTab = tabsManager.getCurrentTab();
        return currentTab === tabItem;
      }
    }
    return false;
  }

  private _activateTab(tabKey: string): void {
    const dashboard = this._getDashboard();
    const tabItem = sceneGraph.findByKey(dashboard, tabKey);

    if (tabItem instanceof TabItem) {
      const tabsManager = tabItem.getParentLayout();
      if (tabsManager instanceof TabsLayoutManager) {
        // For grid items: remove from source BEFORE switching tabs
        // This prevents the item from being unmounted with the source tab
        const gridItem = this.state.draggingGridItem?.resolve();
        if (gridItem && this._sourceDropTarget && !this._itemDetachedFromSource) {
          // Get label and dimensions for preview before detaching
          this._previewLabel = this._getItemLabel(gridItem);
          this._previewType = 'panel';
          this._captureItemDimensions(gridItem);

          this._sourceDropTarget.draggedGridItemOutside?.(gridItem);
          this._itemDetachedFromSource = true;

          // Show preview immediately using last known cursor position
          this._showDragPreview();
        }

        // For rows: remove from source layout and show preview
        const row = this.state.draggingRow?.resolve();
        if (row && !this._itemDetachedFromSource && this._sourceRowsLayout) {
          // Get label for preview (dimensions already captured in startRowDrag)
          this._previewLabel = row.state.title || 'Row';
          this._previewType = 'row';

          // Remove row from source layout (skip undo as this is part of drag operation)
          this._sourceRowsLayout.removeRow(row, true);
          this._itemDetachedFromSource = true;

          // Show preview immediately
          this._showDragPreview();
        }

        tabsManager.switchToTab(tabItem);

        // Update last drop target to the new tab
        // This ensures drop works even if user releases immediately after tab switch
        if (isDashboardDropTarget(tabItem)) {
          this._lastDropTarget = tabItem;
        }
      }
    }
  }

  private _getGridItemIndex(gridItem: SceneGridItemLike): number | null {
    if (this._sourceDropTarget instanceof AutoGridLayoutManager) {
      const children = this._sourceDropTarget.state.layout.state.children;
      const idx = children.findIndex((child) => child === gridItem);
      return idx >= 0 ? idx : null;
    }
    return null;
  }

  private _getItemLabel(gridItem: SceneGridItemLike): string {
    if ('state' in gridItem && 'body' in gridItem.state && gridItem.state.body instanceof VizPanel) {
      return gridItem.state.body.state.title || 'Panel';
    }
    return 'Panel';
  }

  private _getTabUnderMouse(clientX: number, clientY: number, excludeKey?: string): string | null {
    const elementsUnderPoint = document.elementsFromPoint(clientX, clientY);

    const tabKey = elementsUnderPoint
      ?.find(
        (element) =>
          element.getAttribute('data-tab-activation-key') &&
          (!excludeKey || element.getAttribute('data-tab-activation-key') !== excludeKey)
      )
      ?.getAttribute('data-tab-activation-key');

    return tabKey || null;
  }

  private _findParentTabKey(item: RowItem | SceneGridItemLike): string | undefined {
    let parent = item.parent;
    while (parent) {
      if (parent instanceof TabItem) {
        return parent.state.key;
      }
      parent = parent.parent;
    }
    return undefined;
  }

  private _onNewPanelPointerMove = (evt: PointerEvent): void => {
    const dropTarget = this._getDropTargetUnderMouse(evt) ?? this._sourceDropTarget;

    if (!dropTarget) {
      return;
    }

    if (dropTarget !== this._lastDropTarget) {
      this._lastDropTarget?.setIsDropTarget?.(false);
      this._lastDropTarget = dropTarget;

      if (dropTarget !== this._sourceDropTarget) {
        dropTarget.setIsDropTarget?.(true);
      }
    }
  };

  private _onPointerMove(evt: PointerEvent) {
    // Store cursor position early so it's available for immediate preview on tab switch
    this._lastCursorX = evt.clientX;
    this._lastCursorY = evt.clientY;

    // Check for tab hover to enable tab switching during drag
    this._checkTabHover(evt.clientX, evt.clientY);

    // Update drag preview position if item is detached
    this._updateDragPreview(evt.clientX, evt.clientY);

    const dropTarget = this._getDropTargetUnderMouse(evt) ?? this._sourceDropTarget;

    if (!dropTarget) {
      this._clearDropPosition();
      return;
    }

    if (dropTarget !== this._lastDropTarget) {
      // Clear drop position from previous target
      this._clearDropPosition();
      this._lastDropTarget?.setIsDropTarget?.(false);
      this._lastDropTarget = dropTarget;

      if (dropTarget !== this._sourceDropTarget) {
        dropTarget.setIsDropTarget?.(true);
      }
    }

    // Update drop position for AutoGrid targets
    this._updateDropPosition(evt.clientX, evt.clientY, dropTarget);
  }

  private _updateDropPosition(clientX: number, clientY: number, dropTarget: DashboardDropTarget): void {
    // Only update position for AutoGridLayoutManager targets
    if (!(dropTarget instanceof AutoGridLayoutManager)) {
      return;
    }

    // Don't show external placeholder when dragging within the same grid
    // (AutoGrid has its own internal drag placeholder)
    if (dropTarget === this._sourceDropTarget) {
      return;
    }

    // Find which AutoGridItem we're hovering over
    const elementsUnderPoint = document.elementsFromPoint(clientX, clientY);
    const targetElement = elementsUnderPoint?.find((el) => el.getAttribute(AUTO_GRID_ITEM_DROP_TARGET_ATTR));
    const targetKey = targetElement?.getAttribute(AUTO_GRID_ITEM_DROP_TARGET_ATTR);

    const children = dropTarget.state.layout.state.children;

    // If not hovering over any item
    if (!targetKey || !targetElement) {
      // Only set initial position when first entering the grid
      if (this._currentDropPosition === null) {
        this._currentDropPosition = children.length;
        dropTarget.setDropPosition?.(children.length);
      }
      // Otherwise keep the current position (prevents flickering when over placeholder)
      return;
    }

    // Determine if we should insert before or after the hovered item
    // by checking if cursor is in left half or right half
    const rect = targetElement.getBoundingClientRect();
    const isRightHalf = clientX > rect.left + rect.width / 2;

    // Create a composite key that includes both item key and side
    const compositeKey = `${targetKey}-${isRightHalf ? 'after' : 'before'}`;

    // Only update if we're hovering over a different position than before
    // This prevents flickering when the placeholder shifts items around
    if (compositeKey === this._lastHoveredAutoGridItemKey) {
      return;
    }

    this._lastHoveredAutoGridItemKey = compositeKey;

    // Find the index of the hovered item
    const hoveredIndex = children.findIndex((child) => child.state.key === targetKey);
    if (hoveredIndex < 0) {
      return;
    }

    // Insert after if in right half, before if in left half
    const newPosition = isRightHalf ? hoveredIndex + 1 : hoveredIndex;

    this._currentDropPosition = newPosition;
    dropTarget.setDropPosition?.(newPosition);
  }

  private _clearDropPosition(): void {
    if (this._currentDropPosition !== null && this._lastDropTarget) {
      this._lastDropTarget.setDropPosition?.(null);
      this._currentDropPosition = null;
    }
    this._lastHoveredAutoGridItemKey = null;
  }

  private _getDashboard(): DashboardScene {
    if (!(this.parent instanceof DashboardScene)) {
      throw new Error('Parent is not a DashboardScene');
    }

    return this.parent;
  }

  private _getDropTargetUnderMouse(evt: MouseEvent): DashboardDropTarget | null {
    const elementsUnderPoint = document.elementsFromPoint(evt.clientX, evt.clientY);
    const cursorIsInSourceTarget = elementsUnderPoint.some(
      (el) => el.getAttribute(DASHBOARD_DROP_TARGET_KEY_ATTR) === this._sourceDropTarget?.state.key
    );

    if (cursorIsInSourceTarget) {
      return null;
    }

    const key = elementsUnderPoint
      ?.find((element) => element.getAttribute(DASHBOARD_DROP_TARGET_KEY_ATTR))
      ?.getAttribute(DASHBOARD_DROP_TARGET_KEY_ATTR);

    if (!key) {
      return null;
    }

    return this._findDropTargetByKey(key);
  }

  private _findDropTargetByKey(key: string): DashboardDropTarget | null {
    const sceneObject = sceneGraph.findByKey(this._getDashboard(), key);

    if (!sceneObject || !isDashboardDropTarget(sceneObject)) {
      return null;
    }

    return sceneObject;
  }
}

/**
 * Renders a floating drag preview when an item is detached during cross-tab drag
 */
function DragPreviewRenderer({ model }: SceneComponentProps<DashboardLayoutOrchestrator>) {
  const { dragPreview } = model.useState();
  const styles = useStyles2(getPreviewStyles);

  if (!dragPreview) {
    return null;
  }

  // Position preview so cursor maintains same relative position as when drag started
  const previewLeft = dragPreview.x - dragPreview.offsetX;
  const previewTop = dragPreview.y - dragPreview.offsetY;

  const preview = (
    <div
      className={styles.preview}
      style={{
        left: previewLeft,
        top: previewTop,
        width: dragPreview.width,
        height: dragPreview.height,
      }}
    >
      <span className={styles.label}>{dragPreview.label}</span>
    </div>
  );

  return createPortal(preview, document.body);
}

const getPreviewStyles = (theme: GrafanaTheme2) => ({
  preview: css({
    position: 'fixed',
    background: theme.colors.background.primary,
    border: `1px dashed ${theme.colors.primary.main}`,
    borderRadius: theme.shape.radius.default,
    boxShadow: theme.shadows.z3,
    pointerEvents: 'none',
    zIndex: theme.zIndex.tooltip,
    overflow: 'hidden',
    opacity: 0.9,
  }),
  label: css({
    // Match panel header styling
    display: 'flex',
    alignItems: 'center',
    height: theme.spacing(theme.components.panel.headerHeight),
    padding: theme.spacing(0.5, 1, 0, 1.5),
    color: theme.colors.text.primary,
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
    ...theme.typography.h6,
  }),
});
