import { createRef, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';

import {
  type SceneLayout,
  SceneObjectBase,
  type SceneObjectState,
  type VizPanel,
  type SceneGridItemLike,
} from '@grafana/scenes';

import { isRepeatCloneOrChildOf } from '../../utils/clone';
import { getLayoutOrchestratorFor } from '../../utils/utils';
import { AUTO_GRID_ITEM_DROP_TARGET_ATTR } from '../types/DashboardDropTarget';

import { AutoGridItem } from './AutoGridItem';
import { AutoGridLayoutRenderer } from './AutoGridLayoutRenderer';
import { DRAGGED_ITEM_HEIGHT, DRAGGED_ITEM_LEFT, DRAGGED_ITEM_TOP, DRAGGED_ITEM_WIDTH } from './const';

export interface AutoGridLayoutState extends SceneObjectState, AutoGridLayoutOptions {
  children: AutoGridItem[];

  /**
   * True when the item should be rendered but not visible.
   * Useful for conditional display of layout items
   */
  isHidden?: boolean;

  /**
   * For media query for screens smaller than md breakpoint
   */
  md?: AutoGridLayoutOptions;

  /** True when the items should be draggable */
  isDraggable?: boolean;

  /** The key of the item being dragged */
  draggingKey?: string;
}

export interface AutoGridLayoutOptions {
  /**
   * Useful for setting a height on items without specifying how many rows there will be.
   * Defaults to 320px
   */
  autoRows?: CSSProperties['gridAutoRows'];
  /**
   * This overrides the autoRows with a specific row template.
   */
  templateRows?: CSSProperties['gridTemplateRows'];
  /**
   * Defaults to repeat(auto-fit, minmax(400px, 1fr)). This pattern us useful for equally sized items with a min width of 400px
   * and dynamic max width split equally among columns.
   */
  templateColumns: CSSProperties['gridTemplateColumns'];
  /** In Grafana design system grid units (8px)  */
  rowGap: number;
  /** In Grafana design system grid units (8px)  */
  columnGap: number;
  justifyItems?: CSSProperties['justifyItems'];
  alignItems?: CSSProperties['alignItems'];
  justifyContent?: CSSProperties['justifyContent'];
}

export class AutoGridLayout extends SceneObjectBase<AutoGridLayoutState> implements SceneLayout {
  public static Component = AutoGridLayoutRenderer;

  public containerRef = createRef<HTMLDivElement>();
  private _draggedGridItem: AutoGridItem | null = null;
  private _initialGridItemPosition: {
    pageX: number;
    pageY: number;
    top: number;
    left: number;
  } | null = null;
  /** Container's initial page position, used to compensate for layout shifts during drag */
  private _initialContainerRect: { top: number; left: number } | null = null;
  private _lastDropTargetGridItemKey: string | null = null;

  public constructor(state: Partial<AutoGridLayoutState>) {
    super({
      rowGap: 1,
      columnGap: 1,
      templateColumns: 'repeat(auto-fit, minmax(400px, 1fr))',
      autoRows: state.autoRows ?? `320px`,
      children: state.children ?? [],
      ...state,
    });

    this._onDragStart = this._onDragStart.bind(this);
    this._onDragEnd = this._onDragEnd.bind(this);
    this._onDrag = this._onDrag.bind(this);

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

  private _activationHandler() {
    return () => {
      this._resetPanelPositionAndSize();
      document.body.removeEventListener('pointermove', this._onDrag);
      document.body.removeEventListener('pointerup', this._onDragEnd);
      document.body.classList.remove('dashboard-draggable-transparent-selection');
    };
  }

  public isDraggable(): boolean {
    if (isRepeatCloneOrChildOf(this)) {
      return false;
    }

    return this.state.isDraggable ?? false;
  }

  public getDragClass(): string {
    return `grid-drag-handle-${this.state.key}`;
  }

  public getDragClassCancel(): string {
    return 'grid-drag-cancel';
  }

  public getDragHooks() {
    return {
      onDragStart: (evt: ReactPointerEvent, panel: VizPanel) => {
        const gridItem = panel.parent;
        if (gridItem instanceof AutoGridItem) {
          this._onDragStart(evt, gridItem);
        }
      },
    };
  }

  private _canDrag(evt: ReactPointerEvent): boolean {
    if (!this.isDraggable()) {
      return false;
    }

    if (!(evt.target instanceof Element)) {
      return false;
    }

    return !!evt.target.closest(`.${this.getDragClass()}`) && !evt.target.closest(`.${this.getDragClassCancel()}`);
  }

  // Start inside dragging
  private _onDragStart(evt: ReactPointerEvent, gridItem: SceneGridItemLike) {
    if (!this._canDrag(evt)) {
      return;
    }

    evt.preventDefault();
    evt.stopPropagation();

    if (!(gridItem instanceof AutoGridItem)) {
      throw new Error('Dragging wrong item');
    }

    this._draggedGridItem = gridItem;
    this._lastDropTargetGridItemKey = gridItem.state.key!;

    const { top, left, width, height } = this._draggedGridItem.getBoundingBox();
    this._initialGridItemPosition = { pageX: evt.pageX, pageY: evt.pageY, top, left: left };

    // Capture container's initial page position to compensate for layout shifts
    // (e.g., when a grid above expands due to placeholder insertion)
    const containerRect = this.containerRef.current?.getBoundingClientRect();
    this._initialContainerRect = containerRect ? { top: containerRect.top, left: containerRect.left } : null;

    this._updatePanelSize(width, height);
    this._updatePanelPosition(top, left);

    this.setState({ draggingKey: this._draggedGridItem.state.key });

    document.body.addEventListener('pointermove', this._onDrag);
    document.body.addEventListener('pointerup', this._onDragEnd);
    document.body.classList.add('dashboard-draggable-transparent-selection');

    getLayoutOrchestratorFor(this)?.startDraggingSync(evt, this._draggedGridItem);
  }

  // Stop inside dragging
  private _onDragEnd() {
    window.getSelection()?.removeAllRanges();

    this._draggedGridItem = null;
    this._initialGridItemPosition = null;
    this._initialContainerRect = null;
    this._lastDropTargetGridItemKey = null;

    // Only reset position/size and clear draggingKey if not dropping to a different layout.
    // For cross-grid drops, the orchestrator will call endExternalDrag() after the item is moved
    // to prevent flickering where the item would momentarily appear at wrong position
    // (CSS vars cleared but draggingKey still set = absolute positioning with no position).
    const orchestrator = getLayoutOrchestratorFor(this);
    if (!orchestrator?.isDroppedElsewhere()) {
      this._resetPanelPositionAndSize();
      this.setState({ draggingKey: undefined });
    }

    document.body.removeEventListener('pointermove', this._onDrag);
    document.body.removeEventListener('pointerup', this._onDragEnd);
    document.body.classList.remove('dashboard-draggable-transparent-selection');
  }

  /**
   * Called by the orchestrator after a cross-layout drag ends and the item has been moved.
   * Cleans up the drag state that was preserved during the cross-layout drop.
   */
  public endExternalDrag(): void {
    this._resetPanelPositionAndSize();
    this.setState({ draggingKey: undefined });
  }

  // Handle inside drag moves
  private _onDrag(evt: PointerEvent) {
    if (!this._draggedGridItem || !this._initialGridItemPosition) {
      this._onDragEnd();
      return;
    }

    // Calculate how much the container has shifted since drag started
    // This can happen when a grid above expands (e.g., placeholder causes row wrap)
    let containerShiftY = 0;
    let containerShiftX = 0;
    if (this._initialContainerRect && this.containerRef.current) {
      const currentRect = this.containerRef.current.getBoundingClientRect();
      containerShiftY = currentRect.top - this._initialContainerRect.top;
      containerShiftX = currentRect.left - this._initialContainerRect.left;
    }

    // Adjust position to compensate for container movement
    this._updatePanelPosition(
      this._initialGridItemPosition.top + (evt.pageY - this._initialGridItemPosition.pageY) - containerShiftY,
      this._initialGridItemPosition.left + (evt.pageX - this._initialGridItemPosition.pageX) - containerShiftX
    );

    const dropTargetGridItemKey = document
      .elementsFromPoint(evt.clientX, evt.clientY)
      ?.find((element) => {
        const key = element.getAttribute(AUTO_GRID_ITEM_DROP_TARGET_ATTR);

        return !!key && key !== this._draggedGridItem!.state.key;
      })
      ?.getAttribute(AUTO_GRID_ITEM_DROP_TARGET_ATTR);

    if (dropTargetGridItemKey && dropTargetGridItemKey !== this._lastDropTargetGridItemKey) {
      this._onDragOverItem(dropTargetGridItemKey);
    }
  }

  // Handle dragging an item from the same grid over another item from the same grid
  private _onDragOverItem(key: string) {
    const children = [...this.state.children];
    const draggedIdx = children.findIndex((child) => child === this._draggedGridItem);
    const draggedOverIdx = children.findIndex((child) => child.state.key === key);

    if (draggedIdx === -1 || draggedOverIdx === -1 || draggedIdx === draggedOverIdx) {
      this._lastDropTargetGridItemKey = key;
      return;
    }

    children.splice(draggedIdx, 1);
    children.splice(draggedOverIdx, 0, this._draggedGridItem!);
    this._lastDropTargetGridItemKey = this._draggedGridItem!.state.key!;

    this.setState({ children });
  }

  private _updatePanelPosition(top: number, left: number) {
    this._setContainerStyle(DRAGGED_ITEM_TOP, `${top}px`);
    this._setContainerStyle(DRAGGED_ITEM_LEFT, `${left}px`);
  }

  private _updatePanelSize(width: number, height: number) {
    this._setContainerStyle(DRAGGED_ITEM_WIDTH, `${Math.floor(width)}px`);
    this._setContainerStyle(DRAGGED_ITEM_HEIGHT, `${Math.floor(height)}px`);
  }

  private _resetPanelPositionAndSize() {
    this._removeContainerStyle(DRAGGED_ITEM_TOP);
    this._removeContainerStyle(DRAGGED_ITEM_LEFT);
    this._removeContainerStyle(DRAGGED_ITEM_WIDTH);
    this._removeContainerStyle(DRAGGED_ITEM_HEIGHT);
  }

  private _setContainerStyle(name: string, value: string) {
    this.containerRef.current?.style.setProperty(name, value);
  }

  private _removeContainerStyle(name: string) {
    this.containerRef.current?.style.removeProperty(name);
  }
}
