// MNote debug /tree shell fallback renderer 浏览器运行时模块。
// 该模块只承接 DOM fallback renderer，状态和命令语义仍由 tree-shell-runtime.js 注入。

import {
  TREE_SHELL_ICONS as ICONS,
  createTreeShellActionButton,
  createTreeShellKindBadge as createKindBadge,
} from "./tree-shell-icons-runtime.js";
import {
  getFileTreeRowAssetId,
  getFileTreeRowDocumentId,
  getFileTreeRowIconKind,
  getFileTreeRowMetaLabel,
  getFileTreeRowOwnerDocumentId,
} from "./tree-shell-filetree-runtime.js";

export function createTreeShellRenderer(context) {
  const {
    FILETREE_DRAG_MIME,
    PAGE_DRAG_MIME,
    allowRootPick,
    appElement,
    applyPageKeyboardAction,
    applyPickerFocusByItemKey,
    applyPickerStateAction,
    attachInlineRenameInput,
    beginInlineRename,
    canAcceptPageDrop,
    canExpandFileTreeRow,
    clearFileTreeDropFeedback,
    clearFileTreeSelection,
    clearPageDropFeedback,
    executeLocalFileTreeExternalDrop,
    executeLocalFileTreeInternalDrop,
    expanded,
    getActiveFileTreeDropPosition,
    getActiveFileTreeDropRowId,
    getActiveFileTreeRootDrop,
    getActivePageDropNodeId,
    getCurrentActiveDocumentId,
    getElementCenter,
    getFileTreeDropTargetFromEvent,
    getFileTreeRowById,
    focusRowElement,
    getFocusedNodeId,
    getInlineRenameState,
    getItemById,
    getRoots,
    getSelectedFileTreeRowIds,
    getSiblings,
    handleCreate,
    handleFileTreeKeyDown,
    handleMove,
    handleNavigate,
    handlePageDropMove,
    handleRowKeyDown,
    isBusy,
    mode,
    normalizeText,
    normalizeFileTreeSelectionForVisibleRows,
    openContextMenu,
    openFileTreeContextMenu,
    patchFileTreeCutDecoration,
    postPickerPickResultToHost,
    postToHost,
    pushVisibleFileTreeRowId,
    readFileTreeInternalDropPayload,
    readPageDragNodeId,
    resolveFileTreeDraggedRowIds,
    resolvePageDropTargetNodeId,
    resolvePickerRootFocused,
    runFileTreePreflight,
    selectFileTreeContextRow,
    selectFileTreeRow,
    setFileTreeDropFeedback,
    setDraggingFileTreeRowIds,
    setDraggingPageNodeId,
    setLastAction,
    setPageDropFeedback,
    setVisibleFileTreeRowIds,
    sourceKind,
    toggleExpand,
    validateFileTreeInternalDrop,
    workspaceId,
  } = context;

  const createActionButton = (icon, testId, title, onClick, disabled) =>
    createTreeShellActionButton(icon, testId, title, onClick, disabled, isBusy());

  const renderNode = (item) => {
    const hasChildren = item.childCount > 0;
    const row = document.createElement("div");
    row.className = "tree-row";
    row.dataset.active = String(item.nodeId === getCurrentActiveDocumentId());
    row.dataset.focused = String(item.nodeId === getFocusedNodeId());
    row.dataset.nodeId = item.nodeId;
    row.dataset.shellMode = mode;
    row.dataset.dropFeedback = String(getActivePageDropNodeId() === item.nodeId);
    row.tabIndex = item.nodeId === getFocusedNodeId() ? 0 : -1;
    row.setAttribute("role", "treeitem");
    row.setAttribute("aria-level", String(item.depth + 1));
    row.setAttribute("aria-expanded", hasChildren ? String(expanded.has(item.nodeId)) : "false");
    row.draggable = mode === "page";
    row.dataset.draggable = String(mode === "page");
    row.addEventListener("focus", () => {
      if (getFocusedNodeId() !== item.nodeId) {
        applyPageKeyboardAction({ kind: "focus", nodeId: item.nodeId }, item, row);
      }
    });
    row.addEventListener("keydown", (event) => handleRowKeyDown(event, item));
    row.addEventListener("contextmenu", (event) => {
      if (mode !== "page") return;
      event.preventDefault();
      openContextMenu(item.nodeId, event.clientX, event.clientY);
    });
    row.addEventListener("dragstart", (event) => {
      if (mode !== "page") return;
      setDraggingPageNodeId(item.nodeId);
      if (event.dataTransfer) {
        event.dataTransfer.effectAllowed = "move";
        event.dataTransfer.setData(PAGE_DRAG_MIME, item.nodeId);
        event.dataTransfer.setData("text/plain", item.nodeId);
      }
      setLastAction(`开始拖拽页面 ${item.title}`);
    });
    row.addEventListener("dragover", (event) => {
      if (mode !== "page") return;
      const sourceNodeId = readPageDragNodeId(event);
      const targetNodeId = resolvePageDropTargetNodeId(event.target);
      if (!canAcceptPageDrop(sourceNodeId, targetNodeId)) {
        clearPageDropFeedback();
        return;
      }
      event.preventDefault();
      if (event.dataTransfer) {
        event.dataTransfer.dropEffect = "move";
      }
      setPageDropFeedback(targetNodeId);
    });
    row.addEventListener("dragleave", (event) => {
      if (mode !== "page") return;
      const relatedTarget =
        event.relatedTarget instanceof Node ? event.relatedTarget : null;
      if (relatedTarget && row.contains(relatedTarget)) {
        return;
      }
      if (getActivePageDropNodeId() === item.nodeId) {
        clearPageDropFeedback();
      }
    });
    row.addEventListener("drop", (event) => {
      if (mode !== "page") return;
      const sourceNodeId = readPageDragNodeId(event);
      const targetNodeId = resolvePageDropTargetNodeId(event.target);
      clearPageDropFeedback();
      setDraggingPageNodeId("");
      if (!canAcceptPageDrop(sourceNodeId, targetNodeId)) {
        return;
      }
      event.preventDefault();
      void handlePageDropMove(sourceNodeId, targetNodeId);
    });
    row.addEventListener("dragend", () => {
      if (mode !== "page") return;
      setDraggingPageNodeId("");
      clearPageDropFeedback();
    });

    if (hasChildren) {
      const toggleButton = document.createElement("button");
      toggleButton.type = "button";
      toggleButton.className = "tree-toggle";
      toggleButton.setAttribute("data-testid", "tree-node-toggle");
      toggleButton.setAttribute(
        "aria-label",
        `${expanded.has(item.nodeId) ? "折叠" : "展开"} ${item.title}`,
      );
      toggleButton.textContent = expanded.has(item.nodeId) ? "▾" : "▸";
      toggleButton.addEventListener("click", (event) => {
        event.stopPropagation();
        applyPageKeyboardAction({ kind: "toggle", nodeId: item.nodeId }, item, event.currentTarget);
      });
      row.appendChild(toggleButton);
    } else {
      const spacer = document.createElement("div");
      spacer.className = "tree-spacer";
      row.appendChild(spacer);
    }

    row.appendChild(createKindBadge("page"));

    const linkButton = document.createElement("button");
    linkButton.type = "button";
    linkButton.className = "tree-link";
    linkButton.setAttribute("data-testid", "tree-node-open");
    linkButton.setAttribute("aria-label", `打开 ${item.title}`);
    linkButton.addEventListener("click", () => {
      if (mode === "picker") {
        applyPickerFocusByItemKey(item.nodeId, { focusDom: true });
        postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
        return;
      }
      handleNavigate(item.nodeId);
    });

    if (getInlineRenameState().mode === "page" && getInlineRenameState().id === item.nodeId) {
      const renameInput = document.createElement("input");
      renameInput.type = "text";
      renameInput.className = "tree-rename-input";
      renameInput.value = item.title;
      renameInput.setAttribute("aria-label", `重命名 ${item.title}`);
      renameInput.dataset.renameId = item.nodeId;
      attachInlineRenameInput(renameInput, item.title);
      linkButton.appendChild(renameInput);
    } else {
      const titleElement = document.createElement("span");
      titleElement.className = "tree-link-title";
      titleElement.textContent = item.title;
      linkButton.appendChild(titleElement);
    }

    const metaElement = document.createElement("span");
    metaElement.className = "tree-link-meta";
    metaElement.textContent = `${item.nodeId} · ${item.childCount} 个子页面`;
    linkButton.appendChild(metaElement);
    row.appendChild(linkButton);

    const actions = document.createElement("div");
    actions.className = "tree-actions";
    const siblingRows = getSiblings(item.parentNodeId);
    const siblingIndex = siblingRows.findIndex((entry) => entry.nodeId === item.nodeId);
    actions.appendChild(
      createActionButton(
        ICONS.add,
        "tree-action-create",
        `在 ${item.title} 下新建子页面`,
        () => handleCreate(item.nodeId),
        false,
      ),
    );
    if (mode === "page") {
      actions.appendChild(
        createActionButton(
          ICONS.edit,
          "tree-action-rename",
          `重命名 ${item.title}`,
          () => beginInlineRename("page", item.nodeId),
          false,
        ),
      );
      actions.appendChild(
        createActionButton(
          ICONS.up,
          "tree-action-move-up",
          `上移 ${item.title}`,
          () => void handleMove(item.nodeId, -1),
          siblingIndex <= 0,
        ),
      );
      actions.appendChild(
        createActionButton(
          ICONS.more,
          "tree-action-menu",
          `打开 ${item.title} 的更多操作`,
          (button) => {
            const center = getElementCenter(button);
            openContextMenu(
              item.nodeId,
              center.x,
              center.y,
            );
          },
          false,
        ),
      );
    }
    row.appendChild(actions);

    const nodeElement = document.createElement("li");
    nodeElement.className = "tree-node";
    nodeElement.dataset.nodeId = item.nodeId;
    nodeElement.appendChild(row);

    if (hasChildren && expanded.has(item.nodeId)) {
      const children = getSiblings(item.nodeId);
      if (children.length > 0) {
        const childrenList = document.createElement("ul");
        childrenList.className = "tree-children";
        children.forEach((child) => {
          childrenList.appendChild(renderNode(child));
        });
        nodeElement.appendChild(childrenList);
      }
    }

    return nodeElement;
  };

  const syncFileTreeSelectionDom = () => {
    if (mode !== "filetree") return;
    appElement.querySelectorAll('[data-rust-rendered-row="filetree"]').forEach((row) => {
      if (!(row instanceof HTMLElement)) return;
      const rowId = normalizeText(row.dataset.rowId);
      row.dataset.selected = String(Boolean(rowId && getSelectedFileTreeRowIds().has(rowId)));
    });
  };

  const openHydratedFileTreeItem = (item) => {
    const documentId = getFileTreeRowDocumentId(item);
    const ownerDocumentId = getFileTreeRowOwnerDocumentId(item);
    const assetId = getFileTreeRowAssetId(item);
    if (item.rowKind === "document" || item.rowKind === "index") {
      handleNavigate(documentId || item.nodeId);
      return;
    }
    setLastAction(`准备打开资源 ${assetId || item.rowId}`);
    postToHost("tree.asset.open", {
      documentId: ownerDocumentId || null,
      assetId: assetId || null,
      objectIdentity: item.resourceMeta?.objectIdentity || null,
      target: { documentId: ownerDocumentId || null },
      payload: {
        documentId: ownerDocumentId || null,
        assetId: assetId || null,
        rowId: item.rowId,
        rowKind: item.rowKind,
        objectIdentity: item.resourceMeta?.objectIdentity || null,
      },
    });
  };

  const postHydratedFileTreeDropToHost = (type, target, extra = {}) => {
    postToHost(type, {
      workspaceId,
      rowId: target.rowId,
      rowKind: target.rowKind,
      targetRowId: target.rowId,
      targetRowKind: target.rowKind,
      documentId: target.documentId,
      assetId: target.assetId,
      ...extra,
      payload: {
        workspaceId,
        rowId: target.rowId,
        rowKind: target.rowKind,
        targetRowId: target.rowId,
        targetRowKind: target.rowKind,
        documentId: target.documentId,
        assetId: target.assetId,
        ...extra,
      },
    });
  };

  const attachHydratedFileTreeDragSource = (row, item) => {
    row.draggable = true;
    row.addEventListener("dragstart", (event) => {
      const rowIds = resolveFileTreeDraggedRowIds(item.rowId);
      setDraggingFileTreeRowIds(rowIds);
      if (event.dataTransfer) {
        const payload = JSON.stringify({
          type: "mnote-file-tree-dnd",
          version: 1,
          rowIds,
        });
        event.dataTransfer.effectAllowed = "copyMove";
        event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
        event.dataTransfer.setData("application/x-mnote-file-tree", payload);
        event.dataTransfer.setData("text/plain", payload);
      }
      setLastAction(`开始拖拽 ${rowIds.length} 个文件树节点`);
    });
    row.addEventListener("dragend", () => {
      setDraggingFileTreeRowIds([]);
      clearFileTreeDropFeedback();
    });
  };

  const bindFileTreeRootEvents = (root) => {
    root.dataset.dropTarget = String(getActiveFileTreeRootDrop());
    root.addEventListener("mousedown", (event) => {
      if (event.target !== event.currentTarget) return;
      clearFileTreeSelection();
    });
    root.addEventListener("contextmenu", (event) => {
      if (event.target !== event.currentTarget) return;
      event.preventDefault();
      clearFileTreeSelection();
      openFileTreeContextMenu({
        documentId: null,
        assetId: null,
        rowId: null,
        rowKind: "root",
        clientX: event.clientX,
        clientY: event.clientY,
      });
    });
    root.addEventListener("dragover", (event) => {
      const internalRowIds = readFileTreeInternalDropPayload(event);
      const files = Array.from(event.dataTransfer?.files || []);
      if (!internalRowIds && files.length === 0) {
        return;
      }
      event.preventDefault();
      const target = getFileTreeDropTargetFromEvent(event);
      if (internalRowIds && !validateFileTreeInternalDrop(target, internalRowIds, event.altKey === true).ok) {
        clearFileTreeDropFeedback();
        return;
      }
      if (event.dataTransfer) {
        event.dataTransfer.dropEffect =
          files.length > 0 || event.altKey ? "copy" : "move";
      }
      setFileTreeDropFeedback(target);
    });
    root.addEventListener("dragleave", (event) => {
      const relatedTarget =
        event.relatedTarget instanceof Node ? event.relatedTarget : null;
      if (relatedTarget && root.contains(relatedTarget)) {
        return;
      }
      clearFileTreeDropFeedback();
    });
    root.addEventListener("drop", (event) => {
      const internalRowIds = readFileTreeInternalDropPayload(event);
      const files = Array.from(event.dataTransfer?.files || []);
      if (!internalRowIds && files.length === 0) {
        return;
      }
      event.preventDefault();
      const target = getFileTreeDropTargetFromEvent(event);
      if (internalRowIds && !validateFileTreeInternalDrop(target, internalRowIds, event.altKey === true).ok) {
        clearFileTreeDropFeedback();
        return;
      }
      if (files.length > 0) {
        if (sourceKind === "local_folder") {
          void executeLocalFileTreeExternalDrop(target, files);
        } else {
          void (async () => {
            const accepted = await runFileTreePreflight("dropFiles", { target, files });
            if (!accepted) return;
            setLastAction(`已发送 ${files.length} 个外部文件到宿主`);
            postHydratedFileTreeDropToHost("tree.filetree.external-drop", target, {
              files,
            });
          })();
        }
      } else {
        if (sourceKind === "local_folder") {
          void executeLocalFileTreeInternalDrop(target, internalRowIds, event.altKey === true);
        } else {
          void (async () => {
            const accepted = await runFileTreePreflight("move", {
              target,
              rowIds: internalRowIds,
              copy: event.altKey === true,
            });
            if (!accepted) return;
            setLastAction(
              event.altKey
                ? `已发送复制拖放到 ${target.rowId || "根目录"}`
                : `已发送移动拖放到 ${target.rowId || "根目录"}`,
            );
            postHydratedFileTreeDropToHost("tree.filetree.internal-drop", target, {
              rowIds: internalRowIds,
              copy: event.altKey === true,
            });
          })();
        }
      }
      setDraggingFileTreeRowIds([]);
      clearFileTreeDropFeedback();
    });
  };

  const bindFileTreeRowEvents = (row, item) => {
    if (!(row instanceof HTMLElement) || !item) return;
    const documentId = getFileTreeRowDocumentId(item) || null;
    const ownerDocumentId = getFileTreeRowOwnerDocumentId(item) || null;
    const assetId = getFileTreeRowAssetId(item) || null;
    row.dataset.active = String(
      item.rowKind === "document" && documentId === getCurrentActiveDocumentId()
    );
    row.dataset.nodeId = item.nodeId;
    row.dataset.rowId = item.rowId;
    row.dataset.rowKind = item.rowKind;
    row.dataset.documentId = documentId || "";
    row.dataset.ownerDocumentId = ownerDocumentId || "";
    row.dataset.assetId = assetId || "";
    row.dataset.objectIdentity = item.resourceMeta?.objectIdentity
      ? JSON.stringify(item.resourceMeta.objectIdentity)
      : "";
    row.dataset.objectKind = item.resourceMeta?.objectIdentity?.objectKind || "";
    row.dataset.shellMode = "filetree";
    row.dataset.selected = String(getSelectedFileTreeRowIds().has(item.rowId));
    row.dataset.dropTarget = String(getActiveFileTreeDropRowId() === item.rowId);
    if (getActiveFileTreeDropRowId() === item.rowId && getActiveFileTreeDropPosition()) {
      row.dataset.dropPosition = getActiveFileTreeDropPosition();
    }
    row.tabIndex = 0;
    row.setAttribute("role", "treeitem");
    row.setAttribute("aria-level", String(item.depth + 1));
    row.setAttribute(
      "aria-expanded",
      canExpandFileTreeRow(item) ? String(expanded.has(item.nodeId)) : "false",
    );
    row.addEventListener("click", (event) => {
      selectFileTreeRow(item.rowId, event);
      row.focus({ preventScroll: true });
      syncFileTreeSelectionDom();
    });
    row.addEventListener("dblclick", () => {
      openHydratedFileTreeItem(item);
    });
    row.addEventListener("keydown", (event) => handleFileTreeKeyDown(event, item));
    row.addEventListener("contextmenu", (event) => {
      event.preventDefault();
      selectFileTreeContextRow(item.rowId);
      syncFileTreeSelectionDom();
      openFileTreeContextMenu({
        documentId,
        assetId,
        rowId: item.rowId,
        rowKind: item.rowKind,
        clientX: event.clientX,
        clientY: event.clientY,
      });
    });
    attachHydratedFileTreeDragSource(row, item);
    patchFileTreeCutDecoration();
    row.querySelectorAll("[data-rust-action]").forEach((element) => {
      if (!(element instanceof HTMLElement)) return;
      element.addEventListener("click", (event) => {
        const action = normalizeText(element.dataset.rustAction);
        if (action === "open") {
          openHydratedFileTreeItem(item);
          return;
        }
        if (action === "menu") {
          event.preventDefault();
          event.stopPropagation();
          selectFileTreeContextRow(item.rowId);
          syncFileTreeSelectionDom();
          const center = getElementCenter(element);
          openFileTreeContextMenu({
            documentId,
            assetId,
            rowId: item.rowId,
            rowKind: item.rowKind,
            clientX: center.x,
            clientY: center.y,
          });
        }
      });
    });
  };

  const hydrateInitialFileTree = () => {
    if (mode !== "filetree") return false;
    const root = appElement.querySelector('[data-rust-filetree-renderer="initial_v1"]');
    if (!(root instanceof HTMLElement)) {
      return false;
    }
    setVisibleFileTreeRowIds([]);
    bindFileTreeRootEvents(root);
    root.querySelectorAll('[data-rust-rendered-row="filetree"]').forEach((row) => {
      if (!(row instanceof HTMLElement)) return;
      const rowId = normalizeText(row.dataset.rowId);
      const nodeId = normalizeText(row.dataset.nodeId);
      const item = getFileTreeRowById().get(rowId) || getItemById().get(nodeId);
      if (!item) return;
      pushVisibleFileTreeRowId(item.rowId);
      bindFileTreeRowEvents(row, item);
    });
    normalizeFileTreeSelectionForVisibleRows();
    syncFileTreeSelectionDom();
    return true;
  };


  const renderFileTree = () => {
    appElement.innerHTML = "";
    setVisibleFileTreeRowIds([]);
    clearFileTreeDropFeedback();

    const fileRoot = document.createElement("div");
    fileRoot.className = "tree-root";
    fileRoot.setAttribute("role", "tree");
    fileRoot.dataset.dropTarget = String(getActiveFileTreeRootDrop());
    fileRoot.addEventListener("mousedown", (event) => {
      if (event.target !== event.currentTarget) return;
      clearFileTreeSelection();
    });

    const openFileTreeItem = (item) => {
      const documentId = getFileTreeRowDocumentId(item);
      const ownerDocumentId = getFileTreeRowOwnerDocumentId(item);
      const assetId = getFileTreeRowAssetId(item);
      if (item.rowKind === "document" || item.rowKind === "index" || item.rowKind === "markdown") {
        handleNavigate(documentId || item.nodeId);
        return;
      }
      setLastAction(`准备打开资源 ${assetId || item.rowId}`);
      postToHost("tree.asset.open", {
        documentId: ownerDocumentId || null,
        assetId: assetId || null,
        objectIdentity: item.resourceMeta?.objectIdentity || null,
        target: { documentId: ownerDocumentId || null },
        payload: {
          documentId: ownerDocumentId || null,
          assetId: assetId || null,
          rowId: item.rowId,
          rowKind: item.rowKind,
          objectIdentity: item.resourceMeta?.objectIdentity || null,
        },
      });
    };

    const postFileTreeDropToHost = (type, target, extra = {}) => {
      postToHost(type, {
        workspaceId,
        rowId: target.rowId,
        rowKind: target.rowKind,
        targetRowId: target.rowId,
        targetRowKind: target.rowKind,
        documentId: target.documentId,
        assetId: target.assetId,
        ...extra,
        payload: {
          workspaceId,
          rowId: target.rowId,
          rowKind: target.rowKind,
          targetRowId: target.rowId,
          targetRowKind: target.rowKind,
          documentId: target.documentId,
          assetId: target.assetId,
          ...extra,
        },
      });
    };

    const attachFileTreeDragSource = (row, item) => {
      row.draggable = true;
      row.addEventListener("dragstart", (event) => {
        const rowIds = resolveFileTreeDraggedRowIds(item.rowId);
        setDraggingFileTreeRowIds(rowIds);
        if (event.dataTransfer) {
          const payload = JSON.stringify({
            type: "mnote-file-tree-dnd",
            version: 1,
            rowIds,
          });
          event.dataTransfer.effectAllowed = "copyMove";
          event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
          event.dataTransfer.setData("application/x-mnote-file-tree", payload);
          event.dataTransfer.setData("text/plain", payload);
        }
        setLastAction(`开始拖拽 ${rowIds.length} 个文件树节点`);
      });
      row.addEventListener("dragend", () => {
        setDraggingFileTreeRowIds([]);
        clearFileTreeDropFeedback();
      });
    };

    const appendFileTreeRow = (container, item) => {
      const children = getSiblings(item.nodeId);
      const hasBranches = canExpandFileTreeRow(item) && children.length > 0;
      const documentId = getFileTreeRowDocumentId(item) || null;
      const ownerDocumentId = getFileTreeRowOwnerDocumentId(item) || null;
      const assetId = getFileTreeRowAssetId(item) || null;

      const row = document.createElement("div");
      row.className = "tree-row";
      row.style.marginLeft = `${item.depth * 22}px`;
      row.dataset.active = String(
        item.rowKind === "document" && documentId === getCurrentActiveDocumentId()
      );
      row.dataset.nodeId = item.nodeId;
      row.dataset.rowId = item.rowId;
      row.dataset.rowKind = item.rowKind;
      row.dataset.documentId = documentId || "";
      row.dataset.ownerDocumentId = ownerDocumentId || "";
      row.dataset.assetId = assetId || "";
      row.dataset.objectIdentity = item.resourceMeta?.objectIdentity
        ? JSON.stringify(item.resourceMeta.objectIdentity)
        : "";
      row.dataset.objectKind = item.resourceMeta?.objectIdentity?.objectKind || "";
      row.dataset.shellMode = "filetree";
      row.dataset.selected = String(getSelectedFileTreeRowIds().has(item.rowId));
      row.dataset.dropTarget = String(getActiveFileTreeDropRowId() === item.rowId);
      if (getActiveFileTreeDropRowId() === item.rowId && getActiveFileTreeDropPosition()) {
        row.dataset.dropPosition = getActiveFileTreeDropPosition();
      }
      row.setAttribute(
        "data-testid",
        item.rowKind === "document"
          ? "filetree-doc-row"
          : item.rowKind === "index"
            ? "filetree-index-row"
            : "filetree-asset-row",
      );
      row.tabIndex = 0;
      row.setAttribute("role", "treeitem");
      row.setAttribute("aria-level", String(item.depth + 1));
      row.setAttribute("aria-expanded", hasBranches ? String(expanded.has(item.nodeId)) : "false");
      pushVisibleFileTreeRowId(item.rowId);

      row.addEventListener("click", (event) => {
        selectFileTreeRow(item.rowId, event);
        renderTree();
        const nextRow = appElement.querySelector(
          `.tree-row[data-shell-mode="filetree"][data-row-id="${CSS.escape(item.rowId)}"]`,
        );
        if (nextRow instanceof HTMLElement) {
          nextRow.focus({ preventScroll: true });
        }
      });
      row.addEventListener("dblclick", () => {
        openFileTreeItem(item);
      });
      row.addEventListener("keydown", (event) => handleFileTreeKeyDown(event, item));
      row.addEventListener("contextmenu", (event) => {
        event.preventDefault();
        selectFileTreeContextRow(item.rowId);
        renderTree();
        openFileTreeContextMenu({
          documentId,
          assetId,
          rowId: item.rowId,
          rowKind: item.rowKind,
          clientX: event.clientX,
          clientY: event.clientY,
        });
      });
      attachFileTreeDragSource(row, item);

      if (hasBranches) {
        const toggleButton = document.createElement("button");
        toggleButton.type = "button";
        toggleButton.className = "tree-toggle";
        toggleButton.setAttribute(
          "aria-label",
          `${expanded.has(item.nodeId) ? "折叠" : "展开"} ${item.title}`,
        );
        toggleButton.textContent = expanded.has(item.nodeId) ? "▾" : "▸";
        toggleButton.addEventListener("click", (event) => {
          event.stopPropagation();
          toggleExpand(item.nodeId);
        });
        row.appendChild(toggleButton);
      } else {
        const spacer = document.createElement("div");
        spacer.className = "tree-spacer";
        row.appendChild(spacer);
      }

      row.appendChild(createKindBadge(getFileTreeRowIconKind(item)));

      const linkButton = document.createElement("button");
      linkButton.type = "button";
      linkButton.className = "tree-link";
      if (item.rowKind === "document") {
        linkButton.setAttribute("data-testid", "filetree-doc-open");
      } else if (item.rowKind === "asset" || item.rowKind === "asset_folder") {
        linkButton.setAttribute("data-testid", "filetree-asset-open");
      }
      linkButton.addEventListener("click", () => openFileTreeItem(item));
      if (getInlineRenameState().mode === "filetree" && getInlineRenameState().id === item.rowId) {
        const renameInput = document.createElement("input");
        renameInput.type = "text";
        renameInput.className = "tree-rename-input";
        renameInput.value = item.title;
        renameInput.setAttribute("aria-label", `重命名 ${item.title}`);
        renameInput.dataset.renameId = item.rowId;
        attachInlineRenameInput(renameInput, item.title);
        linkButton.appendChild(renameInput);
      } else {
        const title = document.createElement("span");
        title.className = "tree-link-title";
        title.textContent = item.title;
        linkButton.appendChild(title);
      }
      const meta = document.createElement("span");
      meta.className = "tree-link-meta";
      meta.textContent = getFileTreeRowMetaLabel(item);
      linkButton.appendChild(meta);
      row.appendChild(linkButton);

      const actions = document.createElement("div");
      actions.className = "tree-actions";
      if (getFileTreeRowDocumentId(item)) {
        actions.appendChild(
          createActionButton(
            ICONS.edit,
            "filetree-action-rename",
            `重命名 ${item.title}`,
            () => beginInlineRename("filetree", item.rowId),
            false,
          ),
        );
      }
      actions.appendChild(
        createActionButton(
          ICONS.more,
          "filetree-action-menu",
          `打开 ${item.title} 的更多操作`,
          (button) => {
            const center = getElementCenter(button);
            openFileTreeContextMenu({
              documentId,
              assetId,
              rowId: item.rowId,
              rowKind: item.rowKind,
              clientX: center.x,
              clientY: center.y,
            });
          },
          false,
        ),
      );
      row.appendChild(actions);
      container.appendChild(row);

      if (!hasBranches || !expanded.has(item.nodeId)) return;
      children.forEach((child) => appendFileTreeRow(container, child));
    };

    fileRoot.addEventListener("dragover", (event) => {
      const internalRowIds = readFileTreeInternalDropPayload(event);
      const files = Array.from(event.dataTransfer?.files || []);
      if (!internalRowIds && files.length === 0) {
        return;
      }
      event.preventDefault();
      const target = getFileTreeDropTargetFromEvent(event);
      if (internalRowIds && !validateFileTreeInternalDrop(target, internalRowIds, event.altKey === true).ok) {
        clearFileTreeDropFeedback();
        return;
      }
      if (event.dataTransfer) {
        event.dataTransfer.dropEffect =
          files.length > 0 || event.altKey ? "copy" : "move";
      }
      setFileTreeDropFeedback(target);
    });
    fileRoot.addEventListener("dragleave", (event) => {
      const relatedTarget =
        event.relatedTarget instanceof Node ? event.relatedTarget : null;
      if (relatedTarget && fileRoot.contains(relatedTarget)) {
        return;
      }
      clearFileTreeDropFeedback();
    });
    fileRoot.addEventListener("contextmenu", (event) => {
      if (event.target !== event.currentTarget) return;
      event.preventDefault();
      clearFileTreeSelection();
      openFileTreeContextMenu({
        documentId: null,
        assetId: null,
        rowId: null,
        rowKind: "root",
        clientX: event.clientX,
        clientY: event.clientY,
      });
    });
    fileRoot.addEventListener("drop", (event) => {
      const internalRowIds = readFileTreeInternalDropPayload(event);
      const files = Array.from(event.dataTransfer?.files || []);
      if (!internalRowIds && files.length === 0) {
        return;
      }
      event.preventDefault();
      const target = getFileTreeDropTargetFromEvent(event);
      if (internalRowIds && !validateFileTreeInternalDrop(target, internalRowIds, event.altKey === true).ok) {
        clearFileTreeDropFeedback();
        return;
      }
      if (files.length > 0) {
        if (sourceKind === "local_folder") {
          void executeLocalFileTreeExternalDrop(target, files);
        } else {
          void (async () => {
            const accepted = await runFileTreePreflight("dropFiles", { target, files });
            if (!accepted) return;
            setLastAction(`已发送 ${files.length} 个外部文件到宿主`);
            postFileTreeDropToHost("tree.filetree.external-drop", target, {
              files,
            });
          })();
        }
      } else {
        if (sourceKind === "local_folder") {
          void executeLocalFileTreeInternalDrop(target, internalRowIds, event.altKey === true);
        } else {
          void (async () => {
            const accepted = await runFileTreePreflight("move", {
              target,
              rowIds: internalRowIds,
              copy: event.altKey === true,
            });
            if (!accepted) return;
            setLastAction(
              event.altKey
                ? `已发送复制拖放到 ${target.rowId || "根目录"}`
                : `已发送移动拖放到 ${target.rowId || "根目录"}`,
            );
            postFileTreeDropToHost("tree.filetree.internal-drop", target, {
              rowIds: internalRowIds,
              copy: event.altKey === true,
            });
          })();
        }
      }
      setDraggingFileTreeRowIds([]);
      clearFileTreeDropFeedback();
    });

    if (getRoots().length === 0) {
      const empty = document.createElement("div");
      empty.className = "tree-empty";
      empty.textContent = "当前 file tree 没有可渲染的页面。";
      fileRoot.appendChild(empty);
    } else {
      getRoots().forEach((item) => appendFileTreeRow(fileRoot, item));
    }
    normalizeFileTreeSelectionForVisibleRows();
    appElement.appendChild(fileRoot);
    patchFileTreeCutDecoration();
  };

  const renderTree = () => {
    if (mode === "filetree") {
      renderFileTree();
      return;
    }
    appElement.innerHTML = "";
    if (mode === "picker" && allowRootPick) {
      const rootButton = document.createElement("button");
      rootButton.type = "button";
      rootButton.className = "tree-row";
      rootButton.setAttribute("data-testid", "tree-picker-root");
      rootButton.dataset.focused = String(resolvePickerRootFocused());
      rootButton.tabIndex = resolvePickerRootFocused() ? 0 : -1;
      rootButton.addEventListener("click", () => {
        applyPickerFocusByItemKey("__root__", { focusDom: true });
        postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
      });

      const spacer = document.createElement("div");
      spacer.className = "tree-spacer";
      rootButton.appendChild(spacer);

      const label = document.createElement("div");
      label.className = "tree-link";
      const title = document.createElement("span");
      title.className = "tree-link-title";
      title.textContent = "根目录";
      const meta = document.createElement("span");
      meta.className = "tree-link-meta";
      meta.textContent = "选择工作空间根目录";
      label.appendChild(title);
      label.appendChild(meta);
      rootButton.appendChild(label);
      appElement.appendChild(rootButton);
    }

    if (getRoots().length === 0) {
      const empty = document.createElement("div");
      empty.className = "tree-empty";
      empty.textContent =
        mode === "picker"
          ? "当前 projection 没有可选择的页面。"
          : "当前 projection 没有可渲染的页面，点击上方按钮先创建一个根页面。";
      appElement.appendChild(empty);
      return;
    }
    const list = document.createElement("ul");
    list.className = "tree-root";
    list.setAttribute("role", "tree");
    getRoots().forEach((item) => {
      list.appendChild(renderNode(item));
    });
    appElement.appendChild(list);
    if (mode === "page" && getFocusedNodeId()) {
      focusRowElement(getFocusedNodeId());
    }
  };



  return {
    renderFileTree,
    renderNode,
    renderTree,
    syncFileTreeSelectionDom,
    openHydratedFileTreeItem,
    hydrateInitialFileTree,
  };
}
