import { IEventBus } from '../types/CalendarTypes';
import { IGridConfig } from '../core/IGridConfig';
import { CoreEvents } from '../constants/CoreEvents';
import { snapToGrid } from '../utils/PositionUtils';
import {
  IMousePosition,
  IDragStartPayload,
  IDragMovePayload,
  IDragEndPayload,
  IDragCancelPayload,
  IDragColumnChangePayload,
  IDragEnterHeaderPayload,
  IDragMoveHeaderPayload,
  IDragLeaveHeaderPayload
} from '../types/DragTypes';
import { SwpEvent } from '../types/SwpEvent';

interface DragState {
  eventId: string;
  element: HTMLElement;
  ghostElement: HTMLElement | null;  // Null for header items
  startY: number;
  mouseOffset: IMousePosition;
  columnElement: HTMLElement | null; // Null when starting from header
  currentColumn: HTMLElement | null; // Null when in header
  targetY: number;
  currentY: number;
  animationId: number;
  sourceColumnKey: string;      // Source column key (where drag started)
  dragSource: 'grid' | 'header'; // Where drag originated
}

/**
 * DragDropManager - Handles drag-drop for calendar events
 *
 * Strategy: Drag original element, leave ghost-clone in place
 * - mousedown: Store initial state, wait for movement
 * - mousemove (>5px): Create ghost, start dragging original
 * - mouseup: Snap to grid, remove ghost, emit drag:end
 * - cancel: Animate back to startY, remove ghost
 */
export class DragDropManager {
  private dragState: DragState | null = null;
  private mouseDownPosition: IMousePosition | null = null;
  private pendingElement: HTMLElement | null = null;
  private pendingMouseOffset: IMousePosition | null = null;
  private container: HTMLElement | null = null;
  private inHeader = false;

  private readonly DRAG_THRESHOLD = 5;
  private readonly INTERPOLATION_FACTOR = 0.3;

  constructor(
    private eventBus: IEventBus,
    private gridConfig: IGridConfig
  ) {
    this.setupScrollListener();
  }

  private setupScrollListener(): void {
    this.eventBus.on(CoreEvents.EDGE_SCROLL_TICK, (e) => {
      if (!this.dragState) return;
      const { scrollDelta } = (e as CustomEvent<{ scrollDelta: number }>).detail;

      // Element skal flytte med scroll for at forblive under musen
      // (elementets top er relativ til kolonnen, som scroller med viewport)
      this.dragState.targetY += scrollDelta;
      this.dragState.currentY += scrollDelta;
      this.dragState.element.style.top = `${this.dragState.currentY}px`;
    });
  }

  /**
   * Initialize drag-drop on a container element
   */
  init(container: HTMLElement): void {
    this.container = container;
    container.addEventListener('pointerdown', this.handlePointerDown);
    document.addEventListener('pointermove', this.handlePointerMove);
    document.addEventListener('pointerup', this.handlePointerUp);
  }

  private handlePointerDown = (e: PointerEvent): void => {
    const target = e.target as HTMLElement;

    // Ignore if clicking on resize handle
    if (target.closest('swp-resize-handle')) return;

    // Match both swp-event and swp-header-item
    const eventElement = target.closest('swp-event') as HTMLElement;
    const headerItem = target.closest('swp-header-item') as HTMLElement;
    const draggable = eventElement || headerItem;

    if (!draggable) return;

    // Store for potential drag
    this.mouseDownPosition = { x: e.clientX, y: e.clientY };
    this.pendingElement = draggable;

    // Calculate mouse offset within element
    const rect = draggable.getBoundingClientRect();
    this.pendingMouseOffset = {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    };

    // Capture pointer for reliable tracking
    draggable.setPointerCapture(e.pointerId);
  };

  private handlePointerMove = (e: PointerEvent): void => {
    // Not in potential drag state
    if (!this.mouseDownPosition || !this.pendingElement) {
      // Already dragging - update target
      if (this.dragState) {
        this.updateDragTarget(e);
      }
      return;
    }

    // Check threshold
    const deltaX = Math.abs(e.clientX - this.mouseDownPosition.x);
    const deltaY = Math.abs(e.clientY - this.mouseDownPosition.y);
    const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);

    if (distance < this.DRAG_THRESHOLD) return;

    // Start drag
    this.initializeDrag(this.pendingElement, this.pendingMouseOffset!, e);
    this.mouseDownPosition = null;
    this.pendingElement = null;
    this.pendingMouseOffset = null;
  };

  private handlePointerUp = (_e: PointerEvent): void => {
    // Clear pending state
    this.mouseDownPosition = null;
    this.pendingElement = null;
    this.pendingMouseOffset = null;

    if (!this.dragState) return;

    // Stop animation
    cancelAnimationFrame(this.dragState.animationId);

    // Handle based on drag source and target
    if (this.dragState.dragSource === 'header') {
      // Header item drag end
      this.handleHeaderItemDragEnd();
    } else {
      // Grid event drag end
      this.handleGridEventDragEnd();
    }

    // Cleanup
    this.dragState.element.classList.remove('dragging');
    this.dragState = null;
    this.inHeader = false;
  };

  /**
   * Handle drag end for header items
   */
  private handleHeaderItemDragEnd(): void {
    if (!this.dragState) return;

    // If dropped in grid (not in header), the swp-event was already created
    // by EventRenderer listening to EVENT_DRAG_LEAVE_HEADER
    // Just emit drag:end for persistence

    if (!this.inHeader && this.dragState.currentColumn) {
      // Dropped in grid - emit drag:end with the new swp-event element
      const gridEvent = this.dragState.currentColumn.querySelector(
        `swp-event[data-event-id="${this.dragState.eventId}"]`
      ) as HTMLElement;

      if (gridEvent) {
        const columnKey = this.dragState.currentColumn.dataset.columnKey || '';
        const date = this.dragState.currentColumn.dataset.date || '';
        const swpEvent = SwpEvent.fromElement(gridEvent, columnKey, date, this.gridConfig);

        const payload: IDragEndPayload = {
          swpEvent,
          sourceColumnKey: this.dragState.sourceColumnKey,
          target: 'grid'
        };

        this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
      }
    }
    // If still in header, no persistence needed (stayed in header)
  }

  /**
   * Handle drag end for grid events
   */
  private handleGridEventDragEnd(): void {
    if (!this.dragState || !this.dragState.columnElement) return;

    // Snap to grid
    const snappedY = snapToGrid(this.dragState.currentY, this.gridConfig);
    this.dragState.element.style.top = `${snappedY}px`;

    // Remove ghost
    this.dragState.ghostElement?.remove();

    // Get columnKey and date from target column
    const columnKey = this.dragState.columnElement.dataset.columnKey || '';
    const date = this.dragState.columnElement.dataset.date || '';

    // Create SwpEvent from element (reads top/height/eventId from element)
    const swpEvent = SwpEvent.fromElement(
      this.dragState.element,
      columnKey,
      date,
      this.gridConfig
    );

    // Emit drag:end
    const payload: IDragEndPayload = {
      swpEvent,
      sourceColumnKey: this.dragState.sourceColumnKey,
      target: this.inHeader ? 'header' : 'grid'
    };

    this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
  }

  private initializeDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent): void {
    const eventId = element.dataset.eventId || '';
    const isHeaderItem = element.tagName.toLowerCase() === 'swp-header-item';
    const columnElement = element.closest('swp-day-column') as HTMLElement;

    // For grid events, we need a column
    if (!isHeaderItem && !columnElement) return;

    if (isHeaderItem) {
      // Header item drag initialization
      this.initializeHeaderItemDrag(element, mouseOffset, eventId);
    } else {
      // Grid event drag initialization
      this.initializeGridEventDrag(element, mouseOffset, e, columnElement, eventId);
    }
  }

  /**
   * Initialize drag for a header item (allDay event)
   */
  private initializeHeaderItemDrag(element: HTMLElement, mouseOffset: IMousePosition, eventId: string): void {
    // Mark as dragging
    element.classList.add('dragging');

    // Initialize drag state for header item
    this.dragState = {
      eventId,
      element,
      ghostElement: null,  // No ghost for header items
      startY: 0,
      mouseOffset,
      columnElement: null,
      currentColumn: null,
      targetY: 0,
      currentY: 0,
      animationId: 0,
      sourceColumnKey: '',  // Will be set from header item data
      dragSource: 'header'
    };

    // Start in header mode
    this.inHeader = true;
  }

  /**
   * Initialize drag for a grid event
   */
  private initializeGridEventDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent, columnElement: HTMLElement, eventId: string): void {
    // Calculate absolute Y position using getBoundingClientRect
    const elementRect = element.getBoundingClientRect();
    const columnRect = columnElement.getBoundingClientRect();
    const startY = elementRect.top - columnRect.top;

    // If event is inside a group, move it to events-layer for correct positioning during drag
    const group = element.closest('swp-event-group');
    if (group) {
      const eventsLayer = columnElement.querySelector('swp-events-layer');
      if (eventsLayer) {
        eventsLayer.appendChild(element);
      }
    }

    // Set consistent positioning for drag (works for both grouped and stacked events)
    element.style.position = 'absolute';
    element.style.top = `${startY}px`;
    element.style.left = '2px';
    element.style.right = '2px';
    element.style.marginLeft = '0';  // Reset stacking margin

    // Create ghost clone
    const ghostElement = element.cloneNode(true) as HTMLElement;
    ghostElement.classList.add('drag-ghost');
    ghostElement.style.opacity = '0.3';
    ghostElement.style.pointerEvents = 'none';

    // Insert ghost before original
    element.parentNode?.insertBefore(ghostElement, element);

    // Setup element for dragging
    element.classList.add('dragging');

    // Calculate initial target from mouse position
    const targetY = e.clientY - columnRect.top - mouseOffset.y;

    // Initialize drag state
    this.dragState = {
      eventId,
      element,
      ghostElement,
      startY,
      mouseOffset,
      columnElement,
      currentColumn: columnElement,
      targetY: Math.max(0, targetY),
      currentY: startY,
      animationId: 0,
      sourceColumnKey: columnElement.dataset.columnKey || '',
      dragSource: 'grid'
    };

    // Emit drag:start
    const payload: IDragStartPayload = {
      eventId,
      element,
      ghostElement,
      startY,
      mouseOffset,
      columnElement
    };

    this.eventBus.emit(CoreEvents.EVENT_DRAG_START, payload);

    // Start animation loop
    this.animateDrag();
  }

  private updateDragTarget(e: PointerEvent): void {
    if (!this.dragState) return;

    // Check header zone first
    this.checkHeaderZone(e);

    // Skip normal grid handling if in header
    if (this.inHeader) return;

    // Check for column change
    const columnAtPoint = this.getColumnAtPoint(e.clientX);

    // For header items entering grid, set initial column
    if (this.dragState.dragSource === 'header' && columnAtPoint && !this.dragState.currentColumn) {
      this.dragState.currentColumn = columnAtPoint;
      this.dragState.columnElement = columnAtPoint;
    }

    if (columnAtPoint && columnAtPoint !== this.dragState.currentColumn && this.dragState.currentColumn) {
      const payload: IDragColumnChangePayload = {
        eventId: this.dragState.eventId,
        element: this.dragState.element,
        previousColumn: this.dragState.currentColumn,
        newColumn: columnAtPoint,
        currentY: this.dragState.currentY
      };

      this.eventBus.emit(CoreEvents.EVENT_DRAG_COLUMN_CHANGE, payload);
      this.dragState.currentColumn = columnAtPoint;
      this.dragState.columnElement = columnAtPoint;
    }

    // Skip grid position updates if no column yet
    if (!this.dragState.columnElement) return;

    const columnRect = this.dragState.columnElement.getBoundingClientRect();
    const targetY = e.clientY - columnRect.top - this.dragState.mouseOffset.y;

    this.dragState.targetY = Math.max(0, targetY);

    // Start animation if not running
    if (!this.dragState.animationId) {
      this.animateDrag();
    }
  }

  /**
   * Check if pointer is in header zone and emit appropriate events
   */
  private checkHeaderZone(e: PointerEvent): void {
    if (!this.dragState) return;

    const headerViewport = document.querySelector('swp-header-viewport');
    if (!headerViewport) return;

    const rect = headerViewport.getBoundingClientRect();
    const isInHeader = e.clientY < rect.bottom;

    if (isInHeader && !this.inHeader) {
      // Entered header (from grid)
      this.inHeader = true;

      if (this.dragState.dragSource === 'grid' && this.dragState.columnElement) {
        const payload: IDragEnterHeaderPayload = {
          eventId: this.dragState.eventId,
          element: this.dragState.element,
          sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement),
          sourceColumnKey: this.dragState.columnElement.dataset.columnKey || '',
          title: this.dragState.element.querySelector('swp-event-title')?.textContent || '',
          colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-')),
          itemType: 'event',
          duration: 1
        };

        this.eventBus.emit(CoreEvents.EVENT_DRAG_ENTER_HEADER, payload);
      }
      // For header source re-entering header, just update inHeader flag
    } else if (!isInHeader && this.inHeader) {
      // Left header (entering grid)
      this.inHeader = false;

      const targetColumn = this.getColumnAtPoint(e.clientX);

      if (this.dragState.dragSource === 'header') {
        // Header item leaving header → create swp-event in grid
        const payload: IDragLeaveHeaderPayload = {
          eventId: this.dragState.eventId,
          source: 'header',
          element: this.dragState.element,
          targetColumn: targetColumn || undefined,
          start: this.dragState.element.dataset.start ? new Date(this.dragState.element.dataset.start) : undefined,
          end: this.dragState.element.dataset.end ? new Date(this.dragState.element.dataset.end) : undefined,
          title: this.dragState.element.textContent || '',
          colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-'))
        };

        this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);

        // Re-attach to the new swp-event created by EventRenderer
        if (targetColumn) {
          const newElement = targetColumn.querySelector(
            `swp-event[data-event-id="${this.dragState.eventId}"]`
          ) as HTMLElement;

          if (newElement) {
            this.dragState.element = newElement;
            this.dragState.columnElement = targetColumn;
            this.dragState.currentColumn = targetColumn;

            // Start animation for the new element
            this.animateDrag();
          }
        }
      } else {
        // Grid event leaving header → restore to grid
        const payload: IDragLeaveHeaderPayload = {
          eventId: this.dragState.eventId,
          source: 'grid'
        };

        this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
      }
    } else if (isInHeader) {
      // Moving within header
      const column = this.getColumnAtX(e.clientX);
      if (column) {
        const payload: IDragMoveHeaderPayload = {
          eventId: this.dragState.eventId,
          columnIndex: this.getColumnIndex(column),
          columnKey: column.dataset.columnKey || ''
        };

        this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE_HEADER, payload);
      }
    }
  }

  /**
   * Get column index (0-based) for a column element
   */
  private getColumnIndex(column: HTMLElement | null): number {
    if (!this.container || !column) return 0;
    const columns = Array.from(this.container.querySelectorAll('swp-day-column'));
    return columns.indexOf(column);
  }

  /**
   * Get column at X coordinate (alias for getColumnAtPoint)
   */
  private getColumnAtX(clientX: number): HTMLElement | null {
    return this.getColumnAtPoint(clientX);
  }

  /**
   * Find column element at given X coordinate
   */
  private getColumnAtPoint(clientX: number): HTMLElement | null {
    if (!this.container) return null;

    const columns = this.container.querySelectorAll('swp-day-column');
    for (const col of columns) {
      const rect = col.getBoundingClientRect();
      if (clientX >= rect.left && clientX <= rect.right) {
        return col as HTMLElement;
      }
    }
    return null;
  }

  private animateDrag = (): void => {
    if (!this.dragState) return;

    const diff = this.dragState.targetY - this.dragState.currentY;

    // Stop animation when close enough to target
    if (Math.abs(diff) <= 0.5) {
      this.dragState.animationId = 0;
      return;
    }

    // Interpolate towards target
    this.dragState.currentY += diff * this.INTERPOLATION_FACTOR;

    // Update element position
    this.dragState.element.style.top = `${this.dragState.currentY}px`;

    // Emit drag:move (only if we have a column)
    if (this.dragState.columnElement) {
      const payload: IDragMovePayload = {
        eventId: this.dragState.eventId,
        element: this.dragState.element,
        currentY: this.dragState.currentY,
        columnElement: this.dragState.columnElement
      };

      this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE, payload);
    }

    // Continue animation
    this.dragState.animationId = requestAnimationFrame(this.animateDrag);
  };

  /**
   * Cancel drag and animate back to start position
   */
  cancelDrag(): void {
    if (!this.dragState) return;

    // Stop animation
    cancelAnimationFrame(this.dragState.animationId);

    const { element, ghostElement, startY, eventId } = this.dragState;

    // Animate back to start
    element.style.transition = 'top 200ms ease-out';
    element.style.top = `${startY}px`;

    // Remove ghost after animation (if exists)
    setTimeout(() => {
      ghostElement?.remove();
      element.style.transition = '';
      element.classList.remove('dragging');
    }, 200);

    // Emit drag:cancel
    const payload: IDragCancelPayload = {
      eventId,
      element,
      startY
    };

    this.eventBus.emit(CoreEvents.EVENT_DRAG_CANCEL, payload);

    this.dragState = null;
    this.inHeader = false;
  }
}
