import {
  useCallback,
  useEffect,
  useRef,
} from 'react';
import { getPointerPosition, preventAll, safeInvoke } from '../events';
import {
  isTagTransferEvent,
  parseTagTransferPayload,
  writeTagTransferData,
} from '../../documents/tagTransfer';

const TAG_REMOVE_DISTANCE = 160;
const DEBUG_DROP = false;

const createDragPreview = (node, clientX, clientY) => {
  if (!(node instanceof HTMLElement)) {
    return null;
  }
  const rect = node.getBoundingClientRect();
  const safeClientX = Number.isFinite(clientX) ? clientX : rect.left + rect.width / 2;
  const safeClientY = Number.isFinite(clientY) ? clientY : rect.top + rect.height / 2;
  const offsetX = Math.min(Math.max(safeClientX - rect.left, 0), rect.width);
  const offsetY = Math.min(Math.max(safeClientY - rect.top, 0), rect.height);
  const clone = node.cloneNode(true);
  clone.style.position = 'absolute';
  clone.style.top = '-9999px';
  clone.style.left = '-9999px';
  clone.style.pointerEvents = 'none';
  clone.style.opacity = '1';
  clone.style.transform = 'none';
  document.body.appendChild(clone);
  return { clone, offsetX, offsetY };
};

const cleanupPreview = (previewNode) => {
  if (previewNode && previewNode.parentNode) {
    previewNode.parentNode.removeChild(previewNode);
  }
};

export const useDeskTagInteractions = ({
  engine,
  onAssignTagToDocument,
  onRemoveTagFromDocument,
  requestCanvasFocus,
}) => {

  const draggingTagRef = useRef(null);
  const pendingDocTagDragRef = useRef(null);
  const removalCursorActiveRef = useRef(false);

  const updateRemovalCursor = useCallback((active) => {
    if (removalCursorActiveRef.current === active) {
      return;
    }
    const body = document.body;
    if (!body) {
      return;
    }
    removalCursorActiveRef.current = active;
    if (active) {
      body.classList.add('desk-cursor-remove');
    } else {
      body.classList.remove('desk-cursor-remove');
    }
  }, []);

  useEffect(
    () => () => {
      updateRemovalCursor(false);
    },
    [updateRemovalCursor],
  );

  const isTagTransfer = useCallback((event) => isTagTransferEvent(event), []);

  const handleTagDragEnd = useCallback(() => {
    updateRemovalCursor(false);
    engine.setTagDropTargetId(null);
  }, [engine, updateRemovalCursor]);

  const finalizeTagDrag = useCallback(
    (dropEffect = 'none') => {
      const state = draggingTagRef.current;
      if (!state) {
        updateRemovalCursor(false);
        return;
      }

      draggingTagRef.current = null;

      const node = state.element;
      const showNode = () => {
        if (node instanceof HTMLElement) {
          node.classList.remove('is-drag-hidden');
        }
      };
      const scheduleShowNode = () => {
        const raf = window.requestAnimationFrame;
        if (raf) {
          raf(showNode);
        } else {
          setTimeout(showNode, 0);
        }
      };

      cleanupPreview(state.previewClone);

      const shouldRemove =
        !state.dropHandled
        && dropEffect === 'none'
        && state.sourceDocId
        && (state.distance || 0) >= TAG_REMOVE_DISTANCE;

      if (!shouldRemove) {
        scheduleShowNode();
        updateRemovalCursor(false);
        return;
      }

      updateRemovalCursor(false);
      engine.setPendingRemovalTag({ docId: state.sourceDocId, tagId: state.tagId });
      const removePromise = safeInvoke(onRemoveTagFromDocument, state.sourceDocId, state.tagId);
      if (!removePromise || typeof removePromise.then !== 'function') {
        scheduleShowNode();
        engine.setPendingRemovalTag(null);
        updateRemovalCursor(false);
        return;
      }
      void (async () => {
        try {
          await removePromise;
          void DEBUG_DROP;
        } catch (error) {
          console.error('Failed to remove tag after drag', error);
          scheduleShowNode();
        } finally {
          engine.setPendingRemovalTag(null);
        }
      })();
    },
    [engine, onRemoveTagFromDocument, updateRemovalCursor],
  );

  const handleDocTagPointerDown = useCallback((event, doc, tag) => {
    if (!doc || !tag) {
      pendingDocTagDragRef.current = null;
      return;
    }
    const { x: startX, y: startY } = getPointerPosition(event);
    pendingDocTagDragRef.current = {
      docId: doc.id,
      tagId: tag.id,
      startX,
      startY,
    };
    updateRemovalCursor(false);
  }, [updateRemovalCursor]);

  const markActiveTagDropHandled = useCallback((tagId, sourceDocId = null) => {
    const state = draggingTagRef.current;
    if (!state) {
      return;
    }
    if (state.tagId !== tagId) {
      return;
    }
    if (sourceDocId && state.sourceDocId !== sourceDocId) {
      return;
    }
    state.dropHandled = true;
  }, []);

  const runTagHoverTransition = useCallback(
    (event, docId, { applyTarget = false, applyPending = false, updateCursor = true } = {}) => {
      if (!isTagTransfer(event)) {
        return;
      }
      preventAll(event);
      if (updateCursor) {
        updateRemovalCursor(false);
      }
      const stringId = docId != null ? String(docId) : null;
      if (applyTarget) {
        engine.setTagDropTargetId(stringId);
      }
      if (applyPending) {
        engine.setPendingTagDocId(stringId);
      }
    },
    [engine, isTagTransfer, updateRemovalCursor],
  );

  const handleTagDragEnterDoc = useCallback(
    (event, docId) => runTagHoverTransition(event, docId, { applyTarget: true }),
    [runTagHoverTransition],
  );

  const handleTagDragOverDoc = useCallback(
    (event, docId) => runTagHoverTransition(event, docId, { applyTarget: true, applyPending: true }),
    [runTagHoverTransition],
  );

  const handleTagDragLeaveDoc = useCallback(
    (event) => runTagHoverTransition(event, null, { applyTarget: true, applyPending: true }),
    [runTagHoverTransition],
  );

  const handleCanvasDragOver = useCallback(
    (event) => runTagHoverTransition(event, null),
    [runTagHoverTransition],
  );

  const handleCanvasDragLeave = useCallback(
    (event) => runTagHoverTransition(event, null, { applyTarget: true, applyPending: true }),
    [runTagHoverTransition],
  );

  const handleCanvasDrop = useCallback(
    (event) => runTagHoverTransition(event, null, { applyTarget: true, applyPending: true }),
    [runTagHoverTransition],
  );

  const handleTagDropOnDoc = useCallback(
    (event, doc) => {
      if (!doc) {
        return;
      }
      if (!isTagTransfer(event)) {
        return;
      }
      preventAll(event);
      engine.setTagDropTargetId(null);
      engine.setPendingTagDocId(null);

      const payload = parseTagTransferPayload(event);
      if (!payload || !payload.id) {
        return;
      }
      markActiveTagDropHandled(payload.id, payload.sourceDocId);

      if (payload.sourceDocId === doc.id) {
        return;
      }

      requestCanvasFocus?.();

      void safeInvoke(onAssignTagToDocument, doc.id, {
        id: payload.id,
        label: payload.label || '',
        sourceDocId: payload.sourceDocId ?? null,
      });
    },
    [engine, isTagTransfer, markActiveTagDropHandled, onAssignTagToDocument, requestCanvasFocus],
  );

  const handleDocTagDragStart = useCallback(
    (event, doc, tag) => {
      if (!event?.dataTransfer || !doc || !tag) {
        return;
      }
      event.dataTransfer.effectAllowed = 'move';
      writeTagTransferData(event.dataTransfer, tag, doc.id);

      const { x: pointerX, y: pointerY } = getPointerPosition(event, { fallbackToPage: false });
      const { clone, offsetX, offsetY } = createDragPreview(event.currentTarget, pointerX, pointerY) || {};
      if (clone && typeof event.dataTransfer.setDragImage === 'function') {
        event.dataTransfer.setDragImage(clone, offsetX || 0, offsetY || 0);
      }

      const element = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
      if (element) {
        element.classList.add('is-drag-hidden');
      }

      draggingTagRef.current = {
        element,
        previewClone: clone,
        sourceDocId: doc.id,
        tagId: tag.id,
        initialX: pointerX,
        initialY: pointerY,
        distance: 0,
        dropHandled: false,
      };

      updateRemovalCursor(false);
    },
    [updateRemovalCursor],
  );

  const handleDocTagDrag = useCallback((event) => {
    const state = draggingTagRef.current;
    if (!state) {
      return;
    }
    const { x, y } = getPointerPosition(event);
    const dx = x - (state.initialX || 0);
    const dy = y - (state.initialY || 0);
    state.distance = Math.sqrt(dx * dx + dy * dy);
    if (state.distance >= TAG_REMOVE_DISTANCE) {
      updateRemovalCursor(true);
    } else {
      updateRemovalCursor(false);
    }
  }, [updateRemovalCursor]);

  const handleDocTagDragEnd = useCallback(
    (event) => {
      finalizeTagDrag(event?.dataTransfer?.dropEffect || 'none');
      const state = draggingTagRef.current;
      if (!state) {
        return;
      }
      const element = state.element;
      if (element) {
        element.classList.remove('is-drag-hidden');
      }
      cleanupPreview(state.previewClone);
      draggingTagRef.current = null;
    },
    [finalizeTagDrag],
  );

  useEffect(() => {
    return () => {
      draggingTagRef.current = null;
      pendingDocTagDragRef.current = null;
    };
  }, []);

  return {
    handleTagDragEnterDoc,
    handleTagDragOverDoc,
    handleTagDragLeaveDoc,
    handleTagDropOnDoc,
    handleDocTagPointerDown,
    handleDocTagDragStart,
    handleDocTagDrag,
    handleDocTagDragEnd,
    handleTagDragEnd,
    markActiveTagDropHandled,
    handleCanvasDragOver,
    handleCanvasDragLeave,
    handleCanvasDrop,
  };
};

export default useDeskTagInteractions;
