import { useState, useCallback, useMemo } from 'react';
import { arrayMove } from '@dnd-kit/sortable';
import { nestify } from 'nestify';
import { v4 as uuidv4 } from 'uuid';

export interface BaseNestableItem {
  id: number;
  uuid: string;
  parent: number | null;
  children?: BaseNestableItem[];
}

export interface FlattenedNestableItem extends BaseNestableItem {
  depth: number;
}

export interface Projection {
  depth: number;
  maxDepth: number;
  minDepth: number;
  parentId: number | null;
}

type DefineDefaultItem<T> = () => Omit<T, keyof BaseNestableItem>;

interface UseNestableEditReturn<T> {
  items: T[];
  setItems: React.Dispatch<React.SetStateAction<T[]>>;
  updateItem: <K extends keyof T>(id: number, key: K, value: T[K]) => void;
  addChild: (item: T) => void;
  removeItem: (id: number, confirmMessage: string) => void;
  nested: T[];
  reIndex: T[];
  flattenedItems: (T & { depth: number })[];
  collapsedIds: ReadonlySet<number>;
  toggleCollapse: (id: number) => void;
  moveTo: (activeId: number, overId: number, dragOffset: number, indentationWidth: number) => void;
}

/**
 * id の要素が持つ parent を辿ってルートまでの深さを求める。
 */
const getDepth = <T extends BaseNestableItem>(items: T[], id: number): number => {
  let depth = 0;
  let current = items.find((item) => item.id === id);
  while (current && current.parent !== null) {
    depth += 1;
    current = items.find((item) => item.id === current!.parent);
  }
  return depth;
};

/**
 * items 配列の並び順（深さ優先順である前提）に depth を付与する。
 * dnd-kit 公式の Sortable Tree 例（flattenTree）に相当する。
 * @see https://github.com/clauderic/dnd-kit/blob/master/stories/3%20-%20Examples/Tree/utilities.ts
 */
export const flattenWithDepth = <T extends BaseNestableItem>(items: T[]): (T & { depth: number })[] =>
  items.map((item) => ({ ...item, depth: getDepth(items, item.id) }));

/**
 * ドラッグ中の水平方向オフセット(dragOffset)から、ドロップ先の depth と parentId を導出する。
 * dnd-kit 公式 Sortable Tree の getProjection をそのまま移植したロジック。
 * @see https://github.com/clauderic/dnd-kit/blob/master/stories/3%20-%20Examples/Tree/utilities.ts
 */
export const getProjection = <T extends BaseNestableItem>(
  flattenedItems: (T & { depth: number })[],
  activeId: number,
  overId: number,
  dragOffset: number,
  indentationWidth: number
): Projection => {
  const overItemIndex = flattenedItems.findIndex((item) => item.id === overId);
  const activeItemIndex = flattenedItems.findIndex((item) => item.id === activeId);
  const activeItem = flattenedItems[activeItemIndex];
  const newItems = arrayMove(flattenedItems, activeItemIndex, overItemIndex);
  const previousItem = newItems[overItemIndex - 1];
  const nextItem = newItems[overItemIndex + 1];
  const dragDepth = Math.round(dragOffset / indentationWidth);
  const projectedDepth = activeItem.depth + dragDepth;
  const maxDepth = previousItem ? previousItem.depth + 1 : 0;
  const minDepth = nextItem ? nextItem.depth : 0;

  let depth = projectedDepth;
  if (projectedDepth >= maxDepth) {
    depth = maxDepth;
  } else if (projectedDepth < minDepth) {
    depth = minDepth;
  }

  const getParentId = (): number | null => {
    if (depth === 0 || !previousItem) {
      return null;
    }
    if (depth === previousItem.depth) {
      return previousItem.parent;
    }
    if (depth > previousItem.depth) {
      return previousItem.id;
    }
    const newParent = newItems
      .slice(0, overItemIndex)
      .reverse()
      .find((item) => item.depth === depth)?.parent;
    return newParent ?? null;
  };

  return { depth, maxDepth, minDepth, parentId: getParentId() };
};

/**
 * collapsedIds に含まれるアイテムの子孫を表示対象から除外する。
 * flattenedItems が深さ優先順（親の後に子が続く）であることを前提に、
 * 前から順に「親が除外済みなら自分も除外済みにする」ことで祖先を辿らずに孫以降も除外できる。
 * dnd-kit 公式 Sortable Tree の removeChildrenOf に相当する。
 */
export const filterVisibleItems = <T extends FlattenedNestableItem>(
  flattenedItems: T[],
  collapsedIds: ReadonlySet<number>
): T[] => {
  const excludedIds = new Set<number>();
  return flattenedItems.filter((item) => {
    if (item.parent !== null && (collapsedIds.has(item.parent) || excludedIds.has(item.parent))) {
      excludedIds.add(item.id);
      return false;
    }
    return true;
  });
};

/**
 * activeId の parent を newParentId に更新したうえで、配列内の位置を overId の位置へ移動する。
 */
export const reorderItems = <T extends BaseNestableItem>(
  items: T[],
  activeId: number,
  overId: number,
  newParentId: number | null
): T[] => {
  const activeIndex = items.findIndex((item) => item.id === activeId);
  const overIndex = items.findIndex((item) => item.id === overId);
  if (activeIndex === -1 || overIndex === -1) {
    return items;
  }
  const updated = items.map((item) => (item.id === activeId ? { ...item, parent: newParentId } : item));
  return arrayMove(updated, activeIndex, overIndex);
};

export const useNestableEdit = <T extends BaseNestableItem>(
  initialItems: T[],
  defineDefaultItem: DefineDefaultItem<T>
): UseNestableEditReturn<T> => {
  const setup = useCallback(
    (items: T[]): T[] => {
      const def = defineDefaultItem();
      if (items.length === 0) {
        items.push({ parent: null, ...def } as unknown as T);
      }
      return items.map((item, i) => ({
        ...item,
        id: i + 1,
        uuid: item.uuid ?? uuidv4(),
        parent: item.parent ?? null,
      }));
    },
    [defineDefaultItem]
  );

  const [items, setItems] = useState<T[]>(setup(initialItems || []));

  const nested = useMemo(() => {
    // itemsの中に以前のchildren情報が残っている可能性があるので、一旦childrenを削除してリセット
    const itemsWithoutChildren = items.map((item) => {
      const rest = { ...item };
      delete rest.children;
      return rest;
    });
    return nestify({ id: 'id', parentId: 'parent', children: 'children' }, itemsWithoutChildren) as T[];
  }, [items]);

  const reIndex = useMemo(() => {
    const newItems = items.map((item) => {
      const index = items.findIndex(({ id }) => id === item.parent); // 親のインデックスを取得
      return { ...item, parent: index >= 0 ? index + 1 : null }; // 親のインデックスを1から始まるように調整
    });
    return newItems.map((item, i) => ({ ...item, id: i + 1 })); // インデックスを1から始まるように調整
  }, [items]);

  const updateItem = useCallback(
    <K extends keyof T>(id: number, key: K, value: T[K]) => {
      const newItems = items.map((item) => (item.id === id ? { ...item, [key]: value } : item));
      setItems(newItems);
    },
    [items]
  );

  const removeItem = useCallback(
    (id: number, confirmMessage: string) => {
      if (window.confirm(confirmMessage)) {
        setItems((prev) => {
          const target = prev.find((item) => item.id === id);
          if (!target) return prev;

          // 削除対象の子要素を親要素に移動
          const updated = prev.map((item) => (item.parent === id ? { ...item, parent: target.parent } : item));
          return updated.filter((item) => item.id !== id);
        });
      }
    },
    [setItems]
  );

  const addChild = useCallback(
    (item: T) => {
      const def = defineDefaultItem();
      const newItem = {
        parent: item.parent,
        id: items.length + 1,
        uuid: uuidv4(),
        ...def,
      } as unknown as T;
      const index = items.findIndex((target) => target.id === item.id);
      const inserted = [...items.slice(0, index + 1), newItem, ...items.slice(index + 1)];
      setItems(inserted);
    },
    [items, defineDefaultItem]
  );

  const flattenedItems = useMemo(() => flattenWithDepth(items), [items]);

  const [collapsedIds, setCollapsedIds] = useState<Set<number>>(new Set());

  const toggleCollapse = useCallback((id: number) => {
    setCollapsedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  }, []);

  const moveTo = useCallback(
    (activeId: number, overId: number, dragOffset: number, indentationWidth: number) => {
      setItems((prev) => {
        const projected = getProjection(flattenWithDepth(prev), activeId, overId, dragOffset, indentationWidth);
        return reorderItems(prev, activeId, overId, projected.parentId);
      });
    },
    [setItems]
  );

  return {
    items,
    setItems,
    updateItem,
    addChild,
    removeItem,
    flattenedItems,
    collapsedIds,
    toggleCollapse,
    moveTo,
    nested,
    reIndex,
  };
};

export default useNestableEdit;
