// Rôle : fournit la couche applicative de réorganisation pour items, groupes,
// catégories, boundaries, parent/enfant et orientations verticale/horizontale.
// S'appuie sur usePointerReorder pour le suivi bas niveau du pointeur.
import { useMemo } from "react";
import { usePointerReorder } from "./usePointerReorder.js";

const TARGET_ITEM = "item";
const TARGET_GROUP = "group";
const TARGET_BOUNDARY = "boundary";
const TARGET_UNGROUPED = "ungrouped";
const DEFAULT_HIERARCHY = { enabled: false, stickyParents: false };
const DEFAULT_CAN_MOVE = () => true;

function cleanText(value) {
  return String(value || "");
}

function encodeTarget(target) {
  return JSON.stringify({
    kind: cleanText(target.kind),
    itemId: cleanText(target.itemId),
    groupId: cleanText(target.groupId),
    parentId: cleanText(target.parentId),
    placement: cleanText(target.placement)
  });
}

function parseTarget(value) {
  try {
    const target = JSON.parse(String(value || "{}"));
    return {
      kind: cleanText(target.kind),
      itemId: cleanText(target.itemId),
      groupId: cleanText(target.groupId),
      parentId: cleanText(target.parentId),
      placement: cleanText(target.placement)
    };
  } catch {
    return { kind: "", itemId: "", groupId: "", parentId: "", placement: "" };
  }
}

function getElementTargetId(target) {
  return encodeTarget({
    kind: target.dataset.reorderTarget,
    itemId: target.dataset.reorderItemId,
    groupId: target.dataset.reorderGroupId,
    parentId: target.dataset.reorderParentId,
    placement: target.dataset.reorderPlacement
  });
}

function getPointerPlacement(event, target, orientation = "vertical") {
  const placement = target.dataset.reorderPlacement;
  if (placement === "before" || placement === "after") return placement;
  const rect = target.getBoundingClientRect();
  if (orientation === "horizontal") return event.clientX > rect.left + rect.width / 2 ? "after" : "before";
  return event.clientY > rect.top + rect.height / 2 ? "after" : "before";
}

function getConfiguredPlacement(target) {
  const placement = target.dataset.reorderPlacement;
  return placement === "after" ? "after" : "before";
}

function targetId(target) {
  if (target.kind === TARGET_ITEM) return target.itemId;
  if (target.kind === TARGET_GROUP || target.kind === TARGET_BOUNDARY) return target.groupId;
  return "";
}

function buildOperation(kind, sourceId, targetIdValue, placement) {
  const source = parseTarget(sourceId);
  const target = parseTarget(targetIdValue);
  return {
    type: kind,
    sourceId: kind === TARGET_ITEM ? source.itemId : source.groupId,
    targetId: targetId(target),
    targetType: target.kind,
    placement: target.placement || placement,
    sourceGroup: source.groupId,
    targetGroup: target.kind === TARGET_UNGROUPED ? "" : target.groupId,
    sourceParentId: source.parentId,
    targetParentId: target.parentId
  };
}

function sameParent(target, parentId = "") {
  return target.parentId === cleanText(parentId);
}

function canMoveWithinHierarchy(operation, hierarchy) {
  if (!hierarchy?.enabled || !hierarchy?.stickyParents) return true;
  return operation.sourceParentId === operation.targetParentId;
}

function canMoveToDistinctTarget(operation) {
  if (operation.type === TARGET_ITEM && operation.targetType === TARGET_ITEM) return operation.sourceId !== operation.targetId;
  if (operation.type === TARGET_GROUP && (operation.targetType === TARGET_GROUP || operation.targetType === TARGET_BOUNDARY)) {
    return operation.sourceGroup && operation.sourceGroup !== operation.targetGroup;
  }
  return true;
}

const DEFAULT_REORDER_FEATURES = {
  item: {
    reorder: true,
    groupChange: false,
    boundaryDrop: false,
    ungroupedDrop: false,
    rootOnly: false
  },
  group: {
    reorder: false,
    boundaryDrop: false,
    ungroupedDrop: false,
    itemDrop: false,
    itemDropRequiresUngroupedTarget: true,
    rootOnly: false
  }
};

function getReorderFeatures(features = {}) {
  return {
    item: { ...DEFAULT_REORDER_FEATURES.item, ...(features.item || {}) },
    group: { ...DEFAULT_REORDER_FEATURES.group, ...(features.group || {}) }
  };
}

function isRootScoped(operation) {
  return !operation.sourceParentId && !operation.targetParentId;
}

function canMoveByFeatures(operation, features) {
  if (operation.type === TARGET_ITEM) {
    const item = features.item;
    const rootAllowed = !item.rootOnly || isRootScoped(operation);
    if (!rootAllowed) return false;
    if (operation.targetType === TARGET_ITEM) {
      return item.reorder && (operation.targetGroup === operation.sourceGroup || item.groupChange);
    }
    if (operation.targetType === TARGET_GROUP) {
      return item.groupChange && Boolean(operation.targetGroup) && operation.targetGroup !== operation.sourceGroup;
    }
    if (operation.targetType === TARGET_BOUNDARY) {
      return item.boundaryDrop && Boolean(operation.targetGroup);
    }
    if (operation.targetType === TARGET_UNGROUPED) {
      return item.ungroupedDrop && Boolean(operation.sourceGroup);
    }
    return false;
  }

  if (operation.type === TARGET_GROUP) {
    const group = features.group;
    const rootAllowed = !group.rootOnly || isRootScoped(operation);
    if (!rootAllowed || !operation.sourceGroup) return false;
    if (operation.targetType === TARGET_GROUP) {
      return group.reorder && Boolean(operation.targetGroup);
    }
    if (operation.targetType === TARGET_BOUNDARY) {
      return group.boundaryDrop && Boolean(operation.targetGroup);
    }
    if (operation.targetType === TARGET_UNGROUPED) {
      return group.ungroupedDrop;
    }
    if (operation.targetType === TARGET_ITEM) {
      return group.itemDrop && (!group.itemDropRequiresUngroupedTarget || !operation.targetGroup);
    }
    return false;
  }

  return false;
}

export function completeGroupOrder(groupOrder = [], groups = []) {
  return [
    ...groupOrder.filter((group) => groups.includes(group)),
    ...groups.filter((group) => !groupOrder.includes(group))
  ];
}

export function getGroupedEntries(items, { groupOrder = [], getItemGroup, groupItemKey = "items", groupIdKey = "id" } = {}) {
  const groups = [];
  const itemsByGroup = new Map();
  items.forEach((item) => {
    const group = cleanText(getItemGroup?.(item));
    if (!group) return;
    if (!itemsByGroup.has(group)) {
      groups.push(group);
      itemsByGroup.set(group, []);
    }
    itemsByGroup.get(group).push(item);
  });
  const orderedGroups = completeGroupOrder(groupOrder, groups);
  const groupEntries = new Map(orderedGroups.map((group) => [group, {
    [groupIdKey]: group,
    [groupItemKey]: itemsByGroup.get(group) || []
  }]));
  const renderedGroups = new Set();
  return items.map((item) => {
    const group = cleanText(getItemGroup?.(item));
    if (!group) return { type: "item", item };
    if (renderedGroups.has(group)) return null;
    renderedGroups.add(group);
    return { type: "group", group: groupEntries.get(group) };
  }).filter(Boolean);
}

export function moveItem(items, fromItemId, toItemId, placement = "before", getItemId = (item) => item.id) {
  const nextItems = [...items];
  const fromIndex = nextItems.findIndex((item) => getItemId(item) === fromItemId);
  const toIndex = nextItems.findIndex((item) => getItemId(item) === toItemId);
  if (fromIndex < 0 || toIndex < 0 || fromItemId === toItemId) return items;
  const [moved] = nextItems.splice(fromIndex, 1);
  const targetIndex = nextItems.findIndex((item) => getItemId(item) === toItemId);
  nextItems.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, moved);
  return nextItems;
}

export function moveItemGroup(items, movingItemIds, toItemId, placement = "before", getItemId = (item) => item.id) {
  const movingIds = new Set(movingItemIds);
  if (!movingIds.size || movingIds.has(toItemId)) return items;
  const movingItems = items.filter((item) => movingIds.has(getItemId(item)));
  const remainingItems = items.filter((item) => !movingIds.has(getItemId(item)));
  const targetIndex = remainingItems.findIndex((item) => getItemId(item) === toItemId);
  if (targetIndex < 0) return items;
  remainingItems.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, ...movingItems);
  return remainingItems;
}

export function getBoundaryItemId(items, itemIds, placement = "before", getItemId = (item) => item.id) {
  const ids = new Set(itemIds);
  const orderedItems = items.filter((item) => ids.has(getItemId(item)));
  return placement === "after" ? getItemId(orderedItems.at(-1) || {}) || "" : getItemId(orderedItems[0] || {}) || "";
}

export function moveGroupOrder(groupOrder, groups, fromGroup, toGroup, placement = "before") {
  const nextOrder = completeGroupOrder(groupOrder, groups);
  const index = nextOrder.indexOf(fromGroup);
  const targetIndex = nextOrder.indexOf(toGroup);
  if (index < 0 || targetIndex < 0 || fromGroup === toGroup) return nextOrder;
  const [moved] = nextOrder.splice(index, 1);
  const nextTargetIndex = nextOrder.indexOf(toGroup);
  nextOrder.splice(placement === "after" ? nextTargetIndex + 1 : nextTargetIndex, 0, moved);
  return nextOrder;
}

export function moveGroupOrderToStart(groupOrder, groups, fromGroup) {
  return [fromGroup, ...completeGroupOrder(groupOrder, groups).filter((group) => group !== fromGroup)];
}

export function moveGroupOrderToEnd(groupOrder, groups, fromGroup) {
  return [...completeGroupOrder(groupOrder, groups).filter((group) => group !== fromGroup), fromGroup];
}

function getGroupsFromItems(items, getItemGroup) {
  const groups = [];
  items.forEach((item) => {
    const group = cleanText(getItemGroup(item));
    if (group && !groups.includes(group)) groups.push(group);
  });
  return groups;
}

function getGroupItemIds(items, group, getItemId, getItemGroup) {
  return items.filter((item) => cleanText(getItemGroup(item)) === group).map(getItemId);
}

function setItemGroup(items, itemId, group, getItemId, setItemGroupValue) {
  return items.map((item) => (getItemId(item) === itemId ? setItemGroupValue(item, group) : item));
}

function getItemGroupById(items, itemId, getItemId, getItemGroup) {
  const item = items.find((entry) => getItemId(entry) === itemId);
  return item ? cleanText(getItemGroup(item)) : "";
}

function moveItemsToEdge(items, movingIds, edge, getItemId) {
  const movingSet = new Set(movingIds);
  const movingItems = items.filter((item) => movingSet.has(getItemId(item)));
  const remainingItems = items.filter((item) => !movingSet.has(getItemId(item)));
  return edge === "start" ? [...movingItems, ...remainingItems] : [...remainingItems, ...movingItems];
}

function compactGroupOrder(groupOrder, items, getItemGroup) {
  return completeGroupOrder(groupOrder, getGroupsFromItems(items, getItemGroup));
}

export function applyGroupedReorderOperation(data, {
  operation,
  itemsKey = "items",
  groupOrderKey = "groupOrder",
  collapsedGroupsKey = "",
  getItemId = (item) => item.id,
  getItemGroup,
  setItemGroup: setItemGroupValue
}) {
  const items = Array.isArray(data?.[itemsKey]) ? data[itemsKey] : [];
  const groupOrder = Array.isArray(data?.[groupOrderKey]) ? data[groupOrderKey] : [];
  const getGroup = getItemGroup || (() => "");
  const setGroup = setItemGroupValue || ((item) => item);
  const allGroups = () => getGroupsFromItems(items, getGroup);

  function withItems(nextItems, nextGroupOrder = compactGroupOrder(groupOrder, nextItems, getGroup), openedGroup = "") {
    const nextData = {
      ...data,
      [itemsKey]: nextItems,
      [groupOrderKey]: nextGroupOrder
    };
    if (collapsedGroupsKey && openedGroup && Array.isArray(data?.[collapsedGroupsKey])) {
      nextData[collapsedGroupsKey] = data[collapsedGroupsKey].filter((group) => group !== openedGroup);
    }
    return nextData;
  }

  function moveItemToEdge(group = "") {
    const groupedItems = setItemGroup(items, operation.sourceId, group, getItemId, setGroup);
    const movedItems = moveItemsToEdge(groupedItems, [operation.sourceId], operation.placement === "after" ? "end" : "start", getItemId);
    return withItems(movedItems, compactGroupOrder(groupOrder, movedItems, getGroup), group);
  }

  function moveItemAroundItem() {
    const targetGroup = getItemGroupById(items, operation.targetId, getItemId, getGroup);
    const groupedItems = setItemGroup(items, operation.sourceId, targetGroup, getItemId, setGroup);
    const movedItems = moveItemGroup(groupedItems, [operation.sourceId], operation.targetId, operation.placement, getItemId);
    return withItems(movedItems, compactGroupOrder(groupOrder, movedItems, getGroup), targetGroup);
  }

  function moveItemIntoGroup(group, placement) {
    if (!group) return moveItemToEdge("");
    const groupedItems = setItemGroup(items, operation.sourceId, group, getItemId, setGroup);
    const targetIds = getGroupItemIds(items, group, getItemId, getGroup).filter((id) => id !== operation.sourceId);
    const boundaryItemId = getBoundaryItemId(items, targetIds, placement, getItemId);
    const movedItems = boundaryItemId ? moveItemGroup(groupedItems, [operation.sourceId], boundaryItemId, placement, getItemId) : groupedItems;
    return withItems(movedItems, compactGroupOrder(groupOrder, movedItems, getGroup), group);
  }

  function moveItemOutsideGroup() {
    const groupedItems = setItemGroup(items, operation.sourceId, "", getItemId, setGroup);
    const targetIds = getGroupItemIds(items, operation.targetGroup, getItemId, getGroup).filter((id) => id !== operation.sourceId);
    const boundaryItemId = getBoundaryItemId(items, targetIds, operation.placement, getItemId);
    const movedItems = boundaryItemId ? moveItemGroup(groupedItems, [operation.sourceId], boundaryItemId, operation.placement, getItemId) : groupedItems;
    return withItems(movedItems, compactGroupOrder(groupOrder, movedItems, getGroup));
  }

  function moveGroupToEdge() {
    const movingIds = getGroupItemIds(items, operation.sourceGroup, getItemId, getGroup);
    if (!movingIds.length) return data;
    const movedItems = moveItemsToEdge(items, movingIds, operation.placement === "after" ? "end" : "start", getItemId);
    const nextGroupOrder = operation.placement === "after"
      ? moveGroupOrderToEnd(groupOrder, allGroups(), operation.sourceGroup)
      : moveGroupOrderToStart(groupOrder, allGroups(), operation.sourceGroup);
    return withItems(movedItems, nextGroupOrder);
  }

  function moveGroupAroundGroup(targetGroup) {
    if (!operation.sourceGroup || !targetGroup || operation.sourceGroup === targetGroup) return data;
    const movingIds = getGroupItemIds(items, operation.sourceGroup, getItemId, getGroup);
    const targetIds = getGroupItemIds(items, targetGroup, getItemId, getGroup);
    const boundaryItemId = getBoundaryItemId(items, targetIds, operation.placement, getItemId);
    if (!movingIds.length || !boundaryItemId) return data;
    return withItems(
      moveItemGroup(items, movingIds, boundaryItemId, operation.placement, getItemId),
      moveGroupOrder(groupOrder, allGroups(), operation.sourceGroup, targetGroup, operation.placement)
    );
  }

  function moveGroupAroundItem() {
    const movingIds = getGroupItemIds(items, operation.sourceGroup, getItemId, getGroup);
    if (!movingIds.length || !operation.targetId) return data;
    return withItems(moveItemGroup(items, movingIds, operation.targetId, operation.placement, getItemId));
  }

  if (operation.type === TARGET_ITEM) {
    if (operation.targetType === TARGET_ITEM) return moveItemAroundItem();
    if (operation.targetType === TARGET_GROUP) return moveItemIntoGroup(operation.targetGroup, operation.placement);
    if (operation.targetType === TARGET_BOUNDARY) return moveItemOutsideGroup();
    if (operation.targetType === TARGET_UNGROUPED) return moveItemToEdge("");
  }

  if (operation.type === TARGET_GROUP) {
    if (operation.targetType === TARGET_GROUP || operation.targetType === TARGET_BOUNDARY) return moveGroupAroundGroup(operation.targetGroup);
    if (operation.targetType === TARGET_ITEM) return moveGroupAroundItem();
    if (operation.targetType === TARGET_UNGROUPED) return moveGroupToEdge();
  }

  return data;
}

export function useGroupedReorder({
  namespace,
  items = [],
  getItemId = (item) => item.id,
  getItemGroup = () => "",
  getParentId = () => "",
  getEffectiveGroup = getItemGroup,
  reorderFeatures = {},
  canMoveItem = DEFAULT_CAN_MOVE,
  canMoveGroup = DEFAULT_CAN_MOVE,
  orientation = "vertical",
  getPlacement = (event, target) => getPointerPlacement(event, target, orientation),
  onItemMove,
  onGroupMove,
  hierarchy = DEFAULT_HIERARCHY
}) {
  const targetSelector = useMemo(() => `[data-reorder-namespace="${namespace}"]`, [namespace]);
  const features = useMemo(() => getReorderFeatures(reorderFeatures), [reorderFeatures]);
  const itemById = useMemo(() => new Map(items.map((item) => [getItemId(item), item])), [getItemId, items]);

  const itemReorder = usePointerReorder({
    targetSelector,
    getTargetId: getElementTargetId,
    getPlacement,
    canDropOn: (target, draggingId) => {
      const source = parseTarget(draggingId);
      const targetIdValue = getElementTargetId(target);
      const targetValue = parseTarget(targetIdValue);
      if (source.kind !== TARGET_ITEM) return false;
      const operation = buildOperation(TARGET_ITEM, draggingId, targetIdValue, getConfiguredPlacement(target));
      if (!canMoveToDistinctTarget(operation)) return false;
      if (!canMoveWithinHierarchy(operation, hierarchy)) return false;
      if (!canMoveByFeatures(operation, features)) return false;
      return canMoveItem(operation, { source, target: targetValue, features, hierarchy });
    },
    onMove: (sourceId, targetIdValue, placement) => {
      onItemMove?.(buildOperation(TARGET_ITEM, sourceId, targetIdValue, placement));
    }
  });

  const groupReorder = usePointerReorder({
    targetSelector,
    getTargetId: getElementTargetId,
    getPlacement,
    canDropOn: (target, draggingId) => {
      const source = parseTarget(draggingId);
      const targetIdValue = getElementTargetId(target);
      const targetValue = parseTarget(targetIdValue);
      if (source.kind !== TARGET_GROUP) return false;
      const operation = buildOperation(TARGET_GROUP, draggingId, targetIdValue, getConfiguredPlacement(target));
      if (!canMoveToDistinctTarget(operation)) return false;
      if (!canMoveByFeatures(operation, features)) return false;
      return canMoveGroup(operation, { source, target: targetValue, features, hierarchy });
    },
    onMove: (sourceId, targetIdValue, placement) => {
      onGroupMove?.(buildOperation(TARGET_GROUP, sourceId, targetIdValue, placement));
    }
  });

  function getItemProps({ itemId, groupId = "", parentId = "" }) {
    return {
      "data-reorder-namespace": namespace,
      "data-reorder-target": TARGET_ITEM,
      "data-reorder-item-id": itemId,
      "data-reorder-group-id": groupId,
      "data-reorder-parent-id": parentId,
      "data-reorder-orientation": orientation
    };
  }

  function getGroupProps({ groupId, parentId = "" }) {
    return {
      "data-reorder-namespace": namespace,
      "data-reorder-target": TARGET_GROUP,
      "data-reorder-group-id": groupId,
      "data-reorder-parent-id": parentId,
      "data-reorder-orientation": orientation
    };
  }

  function getGroupBoundaryProps({ groupId, parentId = "", placement }) {
    return {
      "data-reorder-namespace": namespace,
      "data-reorder-target": TARGET_BOUNDARY,
      "data-reorder-group-id": groupId,
      "data-reorder-parent-id": parentId,
      "data-reorder-placement": placement,
      "data-reorder-orientation": orientation
    };
  }

  function getUngroupedDropProps({ parentId = "" } = {}) {
    return {
      "data-reorder-namespace": namespace,
      "data-reorder-target": TARGET_UNGROUPED,
      "data-reorder-parent-id": parentId,
      "data-reorder-orientation": orientation
    };
  }

  function startItemDrag(event, itemId) {
    const item = itemById.get(itemId);
    itemReorder.startDrag(event, encodeTarget({
      kind: TARGET_ITEM,
      itemId,
      groupId: cleanText(item ? getEffectiveGroup(item) : ""),
      parentId: cleanText(item ? getParentId(item) : "")
    }));
  }

  function startGroupDrag(event, { groupId, parentId = "" }) {
    groupReorder.startDrag(event, encodeTarget({ kind: TARGET_GROUP, groupId, parentId }));
  }

  function isItemDragging(itemId) {
    const target = parseTarget(itemReorder.draggingId);
    return target.kind === TARGET_ITEM && target.itemId === itemId;
  }

  function isGroupDragging(groupId, parentId = "") {
    const target = parseTarget(groupReorder.draggingId);
    return target.kind === TARGET_GROUP && target.groupId === groupId && sameParent(target, parentId);
  }

  function isItemDropTarget(itemId) {
    return [itemReorder.dropTarget, groupReorder.dropTarget].some((dropTarget) => {
      const target = parseTarget(dropTarget.id);
      return target.kind === TARGET_ITEM && target.itemId === itemId;
    });
  }

  function isGroupDropTarget(groupId, parentId = "") {
    return [itemReorder.dropTarget, groupReorder.dropTarget].some((dropTarget) => {
      const target = parseTarget(dropTarget.id);
      return target.kind === TARGET_GROUP && target.groupId === groupId && sameParent(target, parentId);
    });
  }

  function isBoundaryDropTarget({ groupId, parentId = "", placement }) {
    return [itemReorder.dropTarget, groupReorder.dropTarget].some((dropTarget) => {
      const target = parseTarget(dropTarget.id);
      return target.kind === TARGET_BOUNDARY && target.groupId === groupId && target.placement === placement && sameParent(target, parentId);
    });
  }

  function isUngroupedDropTarget(parentId = "") {
    return [itemReorder.dropTarget, groupReorder.dropTarget].some((dropTarget) => {
      const target = parseTarget(dropTarget.id);
      return target.kind === TARGET_UNGROUPED && sameParent(target, parentId);
    });
  }

  function getDropPlacement(targetType, id, parentId = "") {
    const dropTarget = [itemReorder.dropTarget, groupReorder.dropTarget].find((currentTarget) => {
      const target = parseTarget(currentTarget.id);
      if (target.kind !== targetType) return false;
      if (targetType === TARGET_ITEM) return target.itemId === id;
      return target.groupId === id && sameParent(target, parentId);
    });
    return dropTarget?.placement || "";
  }

  function shouldShowUngroupedDropZone({ parentId = "" } = {}) {
    const draggingItem = parseTarget(itemReorder.draggingId);
    const draggingGroup = parseTarget(groupReorder.draggingId);
    return (
      draggingItem.kind === TARGET_ITEM && draggingItem.groupId && sameParent(draggingItem, parentId)
    ) || (
      draggingGroup.kind === TARGET_GROUP && draggingGroup.groupId && sameParent(draggingGroup, parentId)
    );
  }

  function shouldShowGroupBoundaries({ parentId = "" } = {}) {
    const draggingItem = parseTarget(itemReorder.draggingId);
    const draggingGroup = parseTarget(groupReorder.draggingId);
    return (
      draggingItem.kind === TARGET_ITEM && sameParent(draggingItem, parentId)
    ) || (
      draggingGroup.kind === TARGET_GROUP && sameParent(draggingGroup, parentId)
    );
  }

  return {
    itemReorder: { ...itemReorder, startDrag: startItemDrag },
    groupReorder: { ...groupReorder, startDrag: startGroupDrag },
    getItemProps,
    getGroupProps,
    getGroupBoundaryProps,
    getUngroupedDropProps,
    isItemDragging,
    isItemDropTarget,
    isGroupDragging,
    isGroupDropTarget,
    isBoundaryDropTarget,
    isUngroupedDropTarget,
    getDropPlacement,
    shouldShowUngroupedDropZone,
    shouldShowGroupBoundaries
  };
}
