/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { renderFolderedSelectorList } from "./folderedSelectorList";
import { normalizeEditorTags } from "./tagUtils";
import {
  buildPickerMenuItems,
  menuItem,
  menuLabel,
  menuSeparator,
  openContextMenuAtAnchor,
  openContextMenuAtPoint,
} from "./contextMenuSchema";

export function createNpcController(scope) {
  const documentScope = scope.documentScope || scope;
  const renderScope = scope.renderScope || scope;
  const historyScope = scope.historyScope || scope;
  const uiScope = scope.uiScope || scope;
  const sessionScope = scope.sessionScope || scope;
  const ENTITY_TYPE_META = {
    friendly: { label: "Friendly", noun: "friendly entity" },
    hostile: { label: "Hostile", noun: "hostile entity" },
    prop: { label: "Props", noun: "prop" },
  };

  function normalizeEntityType(value, fallback = "friendly") {
    const normalizedValue = String(value || "").trim().toLowerCase();
    if (normalizedValue === "friendly" || normalizedValue === "friend" || normalizedValue === "friendo" || normalizedValue === "npc") {
      return "friendly";
    }
    if (normalizedValue === "hostile" || normalizedValue === "enemy" || normalizedValue === "aggro" || normalizedValue === "monster") {
      return "hostile";
    }
    if (normalizedValue === "prop" || normalizedValue === "props" || normalizedValue === "thing" || normalizedValue === "things" || normalizedValue === "object") {
      return "prop";
    }
    return fallback;
  }

  function getEntityTypeLabel(value) {
    return ENTITY_TYPE_META[normalizeEntityType(value)]?.label || ENTITY_TYPE_META.friendly.label;
  }

  function getEntityTypeMeta(value) {
    return ENTITY_TYPE_META[normalizeEntityType(value)] || ENTITY_TYPE_META.friendly;
  }

  function getNpcEntityType(npc) {
    const record = npc && npc.record && typeof npc.record === "object" && !Array.isArray(npc.record)
      ? npc.record
      : {};
    return normalizeEntityType(
      record.entityType
        || record.entityCategory
        || record.kind
        || record.type
        || npc?.entityType,
      "friendly",
    );
  }

  function getVisibleNpcOverlays() {
    return documentScope.npcOverlays.filter((npc) => documentScope.isLayerRendered(Number(npc.layer) || 0));
  }

  function getNpcCatalogRecords() {
    const payload = documentScope.contentBundle.npc_templates;
    const entries = payload && Array.isArray(payload.npcTemplates) ? payload.npcTemplates : [];
    return entries
      .filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry))
      .map((entry) => {
        const spriteId = String(entry.spriteId || "").trim();
        const spriteEntry = documentScope.spriteCatalog[spriteId] || null;
        return {
          id: String(entry.id || "").trim(),
          name: String(entry.name || entry.id || "NPC").trim(),
          title: String(entry.title || "").trim(),
          entityType: normalizeEntityType(entry.entityType || entry.entityCategory || entry.kind || entry.type, "friendly"),
          spriteId,
          tags: normalizeEditorTags(entry.tags),
          dataUrl: spriteEntry ? spriteEntry.dataUrl : null,
          record: entry,
        };
      })
      .filter((entry) => entry.id);
  }

  function setNpcCatalogEntityType(templateId, nextType) {
    const normalizedTemplateId = String(templateId || "").trim();
    if (!normalizedTemplateId) {
      return false;
    }
    const normalizedType = normalizeEntityType(nextType, "friendly");
    const payload = documentScope.contentBundle.npc_templates;
    const entries = payload && Array.isArray(payload.npcTemplates) ? payload.npcTemplates : [];
    const targetEntry = entries.find((entry) => String(entry?.id || "").trim() === normalizedTemplateId);
    if (!targetEntry || typeof targetEntry !== "object" || Array.isArray(targetEntry)) {
      uiScope.setStatus("Entity reclassification failed: catalog entry not found.", true);
      return false;
    }
    const previousType = normalizeEntityType(targetEntry.entityType || targetEntry.entityCategory || targetEntry.kind || targetEntry.type, "friendly");
    if (previousType === normalizedType) {
      return false;
    }
    targetEntry.entityType = normalizedType;
    historyScope.registerHistory("Catalog entity reclassified", getEntityTypeLabel(previousType), getEntityTypeLabel(normalizedType), [
      "Catalog entity: " + String(targetEntry.name || targetEntry.id || normalizedTemplateId),
      "Type: " + getEntityTypeLabel(previousType) + " -> " + getEntityTypeLabel(normalizedType),
    ]);
    uiScope.renderInstancePalette();
    uiScope.renderNpcList();
    renderScope.draw();
    uiScope.setStatus("Reclassified catalog entity " + (targetEntry.name || targetEntry.id || normalizedTemplateId) + " to " + getEntityTypeLabel(normalizedType) + ".", false);
    return true;
  }

  function getDialogueCatalogRecords() {
    const payload = documentScope.contentBundle.dialogues;
    const entries = payload && Array.isArray(payload.dialogues) ? payload.dialogues : [];
    return entries
      .filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry))
      .map((entry) => ({
        id: String(entry.id || "").trim(),
        name: String(entry.name || entry.id || "Dialogue").trim(),
      }))
      .filter((entry) => entry.id);
  }

  function getFactionRecords() {
    const payload = documentScope.contentBundle.factions;
    const entries = payload && Array.isArray(payload.factions) ? payload.factions : [];
    return entries
      .filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry))
      .map((entry) => ({
        id: String(entry.id || "").trim(),
        name: String(entry.name || entry.id || "Faction").trim(),
      }))
      .filter((entry) => entry.id);
  }

  function getSpriteCatalogRecords() {
    const payload = documentScope.ensureDocumentContentPayload?.("sprites", { schemaVersion: 1, sprites: [] }) || { schemaVersion: 1, sprites: [] };
    const entries = payload && Array.isArray(payload.sprites) ? payload.sprites : [];
    return entries
      .filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry))
      .filter((entry) => String(entry.graphicRole || "sprite").trim().toLowerCase() !== "other")
      .map((entry) => {
        const id = String(entry.id || "").trim();
        const name = String(entry.name || id || "Sprite").trim();
        const sprite = documentScope.spriteCatalog[id] || null;
        return {
          id,
          name,
          dataUrl: sprite ? sprite.dataUrl : null,
        };
      })
      .filter((entry) => entry.id);
  }

  function openPlacedEntityContextMenu(npc, event, options = {}) {
    if (!npc || !event) {
      return false;
    }
    const entityType = getNpcEntityType(npc);
    const removeSource = String(options.removeSource || "instance-list");
    const tooltipId = String(options.tooltipId || ("entity-context:" + String(npc.id || "")));
    const buildItems = typeof options.buildItems === "function" ? options.buildItems : null;
    const menuItems = [
      menuLabel(npc.name || npc.id || "Entity"),
      menuItem("<span>Edit</span>", () => {
        selectNpc(npc);
        scope.openEntityEditorWindow?.(npc.id);
        uiScope.atTooltip.close();
      }),
    ];
      if (options.includeRemove !== false) {
        menuItems.push(menuItem("<span>Remove</span>", () => {
          removeNpcInstanceById(npc.id, removeSource);
          uiScope.atTooltip.close();
        }));
      }
    menuItems.push(...(buildItems?.({ npc, entityType }) || []), menuSeparator(), menuLabel("Entity Type"));
      ["friendly", "hostile", "prop"].forEach((type) => {
        menuItems.push(menuItem(
          "<span>" + uiScope.runtimeEscapeHtml(getEntityTypeLabel(type)) + "</span>",
          () => {
            applyNpcEditorChange(npc, (target) => {
              target.record.entityType = type;
            }, "Entity Type");
            scope.activeEntityCategory = type;
            uiScope.refreshEntityTypeTabs?.();
            uiScope.renderInstancePalette();
            uiScope.renderNpcList();
            uiScope.atTooltip.close();
          },
          entityType === type ? "active" : "",
        ));
      });
    openContextMenuAtPoint(uiScope.atTooltip, event.clientX, event.clientY, menuItems, tooltipId);
    if (options.status !== false) {
      uiScope.setStatus("Opened entity context menu for " + (npc.name || npc.id) + ".", false);
    }
    return true;
  }

  function isNpcPlaced(npc) {
    if (!npc) {
      return false;
    }
    const x = Number(npc.x);
    const y = Number(npc.y);
    return Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0;
  }

  function applyNpcEditorChange(npc, mutator, statusLabel) {
    const beforeIndex = documentScope.npcOverlays.findIndex((entry) => entry.id === npc.id);
    const beforeSnapshot = documentScope.cloneNpcOverlays([npc])[0];
    const beforeX = Math.floor(Number(npc?.x));
    const beforeY = Math.floor(Number(npc?.y));
    if (!npc.record || typeof npc.record !== "object" || Array.isArray(npc.record)) {
      npc.record = {};
    }
    mutator(npc);
    if (Number.isFinite(beforeX) && Number.isFinite(beforeY) && beforeX >= 0 && beforeY >= 0) {
      npc.isPlacementSlot = false;
    }
    documentScope.syncNpcOverlayFromRecord(npc);
    const afterX = Math.floor(Number(npc?.x));
    const afterY = Math.floor(Number(npc?.y));
    if (typeof scope.rebuildWorldChunksForLocalBounds === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      if (Number.isFinite(beforeX) && Number.isFinite(beforeY) && beforeX >= 0 && beforeY >= 0 && Number.isFinite(afterX) && Number.isFinite(afterY) && afterX >= 0 && afterY >= 0) {
        scope.rebuildWorldChunksForLocalBounds({
          minX: Math.min(beforeX, afterX),
          minY: Math.min(beforeY, afterY),
          maxX: Math.max(beforeX, afterX),
          maxY: Math.max(beforeY, afterY),
        });
      } else if (Number.isFinite(afterX) && Number.isFinite(afterY) && afterX >= 0 && afterY >= 0) {
        scope.rebuildWorldChunksForLocalBounds({ minX: afterX, minY: afterY, maxX: afterX, maxY: afterY });
      } else if (Number.isFinite(beforeX) && Number.isFinite(beforeY) && beforeX >= 0 && beforeY >= 0) {
        scope.rebuildWorldChunksForLocalBounds({ minX: beforeX, minY: beforeY, maxX: beforeX, maxY: beforeY });
      }
    }
    ensureNpcImageLoaded(npc);
    renderNpcList();
    renderScope.draw();
    const afterSnapshot = documentScope.cloneNpcOverlays([npc])[0];
    uiScope.setStatus(statusLabel, false);
    historyScope.registerHistory("NPC edited: " + (npc.name || npc.id), "record", "record", [
      "NPC id: " + npc.id,
      "Updated field: " + statusLabel,
    ], {
      operation: {
        type: "npc_entries",
        entries: [{
          before: beforeSnapshot,
          after: afterSnapshot,
          beforeIndex,
          afterIndex: beforeIndex,
        }],
      },
    });
  }

  function ensureNpcImageLoaded(npc) {
    if (!npc) {
      return;
    }
    if (!npc.dataUrl) {
      delete sessionScope.npcImages[npc.id];
      return;
    }
    const existing = sessionScope.npcImages[npc.id];
    if (existing && existing.src === npc.dataUrl) {
      return;
    }
    const img = new Image();
    img.src = npc.dataUrl;
    sessionScope.npcImages[npc.id] = img;
  }

  function getCachedImage(cacheKey, dataUrl) {
    const normalizedKey = String(cacheKey || "");
    if (!normalizedKey || !dataUrl) {
      return null;
    }
    const existing = sessionScope.npcImages[normalizedKey];
    if (existing && existing.src === dataUrl) {
      return existing;
    }
    const next = new Image();
    next.src = dataUrl;
    sessionScope.npcImages[normalizedKey] = next;
    return next;
  }

  function assignNpcToSlot(slotId, assignedTemplateId) {
    const slot = documentScope.npcOverlays.find((npc) => npc.id === slotId);
    if (!slot) {
      return;
    }
    const beforeIndex = documentScope.npcOverlays.findIndex((entry) => entry.id === slot.id);
    const beforeSnapshot = documentScope.cloneNpcOverlays([slot])[0];
    const catalogEntry = getNpcCatalogRecords().find((entry) => entry.id === assignedTemplateId);
    if (!catalogEntry) {
      uiScope.setStatus("Entity assignment failed: catalog entry not found.", true);
      return;
    }

    const nextRecord = JSON.parse(JSON.stringify(catalogEntry.record || {}));
    nextRecord.id = String(slot.id || documentScope.runtimeUniqueId());
    nextRecord.layer = Number(slot.layer) || 0;
    nextRecord.position = { x: slot.x, y: slot.y };
    nextRecord.name = String(nextRecord.name || catalogEntry.record?.name || "");
    nextRecord.templateId = String(assignedTemplateId || "").trim();
    nextRecord.entityType = normalizeEntityType(nextRecord.entityType || catalogEntry.entityType, "friendly");
    nextRecord.faction = String(nextRecord.faction || catalogEntry.record?.faction || "");
    nextRecord.spriteId = String(nextRecord.spriteId || catalogEntry.record?.spriteId || "");
    nextRecord.dialogueId = String(nextRecord.dialogueId || catalogEntry.record?.defaultDialogueId || catalogEntry.record?.dialogueId || "");
    nextRecord.description = String(nextRecord.description || catalogEntry.record?.description || "");
    nextRecord.enabled = typeof nextRecord.enabled === "boolean" ? nextRecord.enabled : true;

    slot.isPlacementSlot = false;
    slot.record = nextRecord;
    documentScope.syncNpcOverlayFromRecord(slot);
    if (typeof scope.rebuildWorldChunksForLocalBounds === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      const tileX = Math.floor(Number(slot?.x));
      const tileY = Math.floor(Number(slot?.y));
      if (Number.isFinite(tileX) && Number.isFinite(tileY) && tileX >= 0 && tileY >= 0) {
        scope.rebuildWorldChunksForLocalBounds({ minX: tileX, minY: tileY, maxX: tileX, maxY: tileY });
      }
    }
    ensureNpcImageLoaded(slot);
    sessionScope.selectedNpcId = slot.id;
    sessionScope.spritePickerOpenNpcId = "";
    renderNpcList();
    uiScope.renderInstancePalette();
    renderScope.draw();
    const afterSnapshot = documentScope.cloneNpcOverlays([slot])[0];
    historyScope.registerHistory("Catalog entity assigned: " + slot.name, "slot", assignedTemplateId, [
      "Assigned entity: " + slot.name,
      "Catalog source: " + assignedTemplateId,
      "Position: (" + slot.x + "," + slot.y + ")",
    ], {
      operation: {
        type: "npc_entries",
        entries: [{
          before: beforeSnapshot,
          after: afterSnapshot,
          beforeIndex,
          afterIndex: beforeIndex,
        }],
      },
    });
  }

  function renderNpcList() {
    if (uiScope.refreshInstanceSectionState) {
      uiScope.refreshInstanceSectionState();
    }
    const activeEntityType = normalizeEntityType(scope.activeEntityCategory, "friendly");
    const orderedNpcs = documentScope.npcOverlays
      .slice()
      .filter((npc) => getNpcEntityType(npc) === activeEntityType)
      .sort((left, right) => String(left.name || left.id || "").localeCompare(String(right.name || right.id || "")));
    const orderedNpcIds = orderedNpcs.map((npc) => String(npc.id || "").trim()).filter(Boolean);
    if (sessionScope.selectedNpcId && !orderedNpcIds.includes(String(sessionScope.selectedNpcId || "").trim())) {
      sessionScope.selectedNpcId = "";
      sessionScope.spritePickerOpenNpcId = "";
    }
    renderFolderedSelectorList({
      scope,
      container: uiScope.npcListEl,
      panelKey: "instances",
      items: orderedNpcs,
      getItemId: (npc) => npc.id,
      emptyMessage: "No " + getEntityTypeLabel(activeEntityType).toLowerCase() + " entities placed yet.",
      baseLabel: "Base Panel",
      onMove: (dragging, dropTarget) => {
        if (scope.movePanelNode("instances", orderedNpcIds, "Placed Entities", dragging, dropTarget)) {
          renderNpcList();
        }
      },
      onToggleFolder: (folderId) => {
        if (scope.togglePanelFolder("instances", orderedNpcIds, folderId, "Placed Entities")) {
          renderNpcList();
        }
      },
      onRenameFolder: (folderId) => {
        if (scope.renamePanelFolder("instances", orderedNpcIds, folderId, "Placed Entities")) {
          renderNpcList();
        }
      },
      onDeleteFolder: (folderId) => {
        if (scope.deletePanelFolder("instances", orderedNpcIds, folderId, "Placed Entities")) {
          renderNpcList();
        }
      },
      renderItemRow: (npc) => {
        const isSelected = npc.id === sessionScope.selectedNpcId;
        const entityType = getNpcEntityType(npc);
        const spriteMetaLabel = npc.spriteId ? npc.spriteId : "placeholder";
        const positionMetaLabel = isNpcPlaced(npc) ? "(" + npc.x + "," + npc.y + ")" : "unplaced";
        const showInlineEditor = false;
        const row = document.createElement("div");
        row.className = "history-row npc-row" + (isSelected ? " active" : "");
        const header = document.createElement("div");
        header.className = "npc-row-main";
        const summaryButton = document.createElement("button");
        summaryButton.type = "button";
        summaryButton.className = "npc-row-header";
        const thumb = npc.dataUrl
          ? '<img class="npc-thumb" alt="npc sprite" src="' + scope.runtimeEscapeHtml(npc.dataUrl) + '">'
          : '<span class="npc-thumb-fallback">NPC</span>';
        summaryButton.innerHTML =
          thumb +
          "<div><span>" + uiScope.runtimeEscapeHtml(npc.name) + "</span>" +
          '<span class="history-meta"><span class="entity-type-badge">' + uiScope.runtimeEscapeHtml(getEntityTypeLabel(entityType)) + "</span> | " + uiScope.runtimeEscapeHtml(npc.id) + " | layer: " + uiScope.runtimeEscapeHtml(String(npc.layer || 0)) + " | sprite: " + uiScope.runtimeEscapeHtml(spriteMetaLabel) + " | pos: " + uiScope.runtimeEscapeHtml(positionMetaLabel) + "</span></div>";
        summaryButton.addEventListener("click", () => {
          selectNpc(npc);
        });
        summaryButton.addEventListener("contextmenu", (event) => {
          event.preventDefault();
          if (sessionScope.activeInstanceBrushId) {
            sessionScope.activeInstanceBrushId = "";
            uiScope.renderInstancePalette();
          }
          sessionScope.selectedNpcId = npc.id;
          sessionScope.selectedTile = null;
          documentScope.setLayerVisibility(Number(npc.layer) || 0, true);
          uiScope.setSidebarTab("instances");
          renderNpcList();
          renderScope.draw();
          openPlacedEntityContextMenu(npc, event, {
            removeSource: "instance-list",
            tooltipId: "instance-list-context:" + String(npc.id || ""),
          });
        });
        header.appendChild(summaryButton);
        const editBtn = document.createElement("button");
        editBtn.type = "button";
        editBtn.className = "npc-row-edit-btn";
        editBtn.title = "Edit entity";
        editBtn.setAttribute("aria-label", "Edit entity " + String(npc.name || npc.id || "entity"));
        editBtn.appendChild(scope.uiIconEl("edit_note", "E", 14));
        editBtn.addEventListener("click", (event) => {
          event.preventDefault();
          event.stopPropagation();
          selectNpc(npc);
          scope.openEntityEditorWindow?.(npc.id);
        });
        header.appendChild(editBtn);
        row.appendChild(header);

        if (isSelected && showInlineEditor) {
          const editorPanel = document.createElement("div");
          editorPanel.className = "npc-editor-panel";

          const idRow = document.createElement("div");
          idRow.className = "npc-editor-row";
          idRow.innerHTML = "<label>Entity ID</label>";
          const idInput = document.createElement("input");
          idInput.type = "text";
          idInput.value = String(npc.id || "");
          idInput.readOnly = true;
          idInput.style.opacity = "0.6";
          idInput.style.cursor = "default";
          idRow.appendChild(idInput);
          editorPanel.appendChild(idRow);

          const topToolbar = document.createElement("div");
          topToolbar.className = "npc-top-toolbar";

          const templateBtnTag = "template:" + npc.id;
          const templateBtn = document.createElement("button");
          templateBtn.type = "button";
          templateBtn.className = "npc-icon-btn" + (scope.atTooltip.isOpenFor(templateBtnTag) ? " active" : "");
          templateBtn.title = "Assign Catalog Entity";
          templateBtn.appendChild(scope.uiIconEl("swap", "T", 16));
          templateBtn.addEventListener("click", () => {
            scope.spritePickerOpenNpcId = "";
            const menuItems = [
              menuItem("(Use no catalog source)", () => {
                applyNpcEditorChange(npc, (target) => {
                  target.record.name = String(target.record.name || "");
                  target.record.templateId = "";
                }, "Catalog Source");
                scope.atTooltip.close();
              }),
              menuSeparator(),
            ];
            menuItems.push(...buildPickerMenuItems(getNpcCatalogRecords(), {
              getInnerHtml: (entry) => {
                const thumbHtml = entry.dataUrl
                  ? '<img src="' + scope.runtimeEscapeHtml(entry.dataUrl) + '" alt="">'
                  : '<span class="npc-thumb-fallback" style="font-size:9px;width:22px;height:22px;">T</span>';
                return thumbHtml + "<span>" + scope.runtimeEscapeHtml(entry.name || entry.id) + "</span>";
              },
              onSelect: (entry) => {
                assignNpcToSlot(npc.id, entry.id);
                scope.atTooltip.close();
                renderNpcList();
              },
            }));
            openContextMenuAtAnchor(scope.atTooltip, templateBtn, menuItems, templateBtnTag);
            renderNpcList();
          });
          topToolbar.appendChild(templateBtn);

          const dialogueBtnTag = "dialogue:" + npc.id;
          const dialogueBtn = document.createElement("button");
          dialogueBtn.type = "button";
          dialogueBtn.className = "npc-icon-btn" + (scope.atTooltip.isOpenFor(dialogueBtnTag) ? " active" : "");
          dialogueBtn.title = "Assign Dialogue";
          dialogueBtn.appendChild(scope.uiIconEl("chat_bubble", "D", 16));
          dialogueBtn.addEventListener("click", () => {
            scope.spritePickerOpenNpcId = "";
            const menuItems = [
              menuItem("(Use no dialogue)", () => {
                applyNpcEditorChange(npc, (target) => { target.record.dialogueId = ""; }, "Dialogue");
                scope.atTooltip.close();
              }),
              menuSeparator(),
            ];
            menuItems.push(...buildPickerMenuItems(getDialogueCatalogRecords(), {
              getInnerHtml: (entry) => "<span>" + scope.runtimeEscapeHtml(entry.name || entry.id) + "</span>",
              onSelect: (entry) => {
                applyNpcEditorChange(npc, (target) => { target.record.dialogueId = entry.id; }, "Dialogue");
                scope.atTooltip.close();
              },
              getExtraClass: (entry) => (
                String(npc.record.dialogueId || "") === entry.id ? "active" : ""
              ),
            }));
            openContextMenuAtAnchor(scope.atTooltip, dialogueBtn, menuItems, dialogueBtnTag);
            renderNpcList();
          });
          topToolbar.appendChild(dialogueBtn);

          editorPanel.appendChild(topToolbar);

          const nameRow = document.createElement("div");
          nameRow.className = "npc-editor-row";
          nameRow.innerHTML = "<label>Name</label>";
          const nameInput = document.createElement("input");
          nameInput.type = "text";
          nameInput.value = String(npc.record.name || "");
          nameInput.placeholder = "Default: entity id";
          nameInput.addEventListener("change", () => {
            applyNpcEditorChange(npc, (target) => {
              target.record.name = String(nameInput.value || "");
            }, "Name");
          });
          nameRow.appendChild(nameInput);
          editorPanel.appendChild(nameRow);

          const typeRow = document.createElement("div");
          typeRow.className = "npc-editor-row";
          typeRow.innerHTML = "<label>Type</label>";
          const typeSelect = document.createElement("select");
          ["friendly", "hostile", "prop"].forEach((type) => {
            const option = document.createElement("option");
            option.value = type;
            option.textContent = getEntityTypeLabel(type);
            typeSelect.appendChild(option);
          });
          typeSelect.value = entityType;
          typeSelect.addEventListener("change", () => {
            const nextType = normalizeEntityType(typeSelect.value, entityType);
            applyNpcEditorChange(npc, (target) => {
              target.record.entityType = nextType;
            }, "Entity Type");
            scope.activeEntityCategory = nextType;
            uiScope.refreshEntityTypeTabs?.();
            uiScope.renderInstancePalette();
            uiScope.renderNpcList();
          });
          typeRow.appendChild(typeSelect);
          editorPanel.appendChild(typeRow);

          const factionRow = document.createElement("div");
          factionRow.className = "npc-editor-row";
          factionRow.innerHTML = "<label>Faction</label>";
          const factionSelect = document.createElement("select");
          const factionPlaceholder = document.createElement("option");
          factionPlaceholder.value = "";
          factionPlaceholder.textContent = "(None)";
          factionSelect.appendChild(factionPlaceholder);
          getFactionRecords().forEach((entry) => {
            const option = document.createElement("option");
            option.value = entry.id;
            option.textContent = entry.name !== entry.id ? entry.name + " (" + entry.id + ")" : entry.id;
            factionSelect.appendChild(option);
          });
          factionSelect.value = String(npc.record.faction || "");
          factionSelect.addEventListener("change", () => {
            applyNpcEditorChange(npc, (target) => {
              target.record.faction = String(factionSelect.value || "");
            }, "Faction");
          });
          factionRow.appendChild(factionSelect);
          editorPanel.appendChild(factionRow);

          const layerRow = document.createElement("div");
          layerRow.className = "npc-editor-row";
          layerRow.innerHTML = "<label>Layer</label>";
          const layerSelect = document.createElement("select");
          scope.roomLayers
            .slice()
            .sort((a, b) => a.layer - b.layer)
            .forEach((layerEntry) => {
              const option = document.createElement("option");
              option.value = String(layerEntry.layer);
              option.textContent = scope.getLayerDisplayName(layerEntry);
              layerSelect.appendChild(option);
            });
          layerSelect.value = String(Number(npc.layer || 0));
          layerSelect.addEventListener("change", () => {
            applyNpcEditorChange(npc, (target) => {
              const nextLayer = Number(layerSelect.value);
              target.layer = Number.isFinite(nextLayer) ? nextLayer : 0;
              target.record.layer = target.layer;
            }, "Layer");
          });
          layerRow.appendChild(layerSelect);
          editorPanel.appendChild(layerRow);

          const spriteRow = document.createElement("div");
          spriteRow.className = "npc-editor-row";
          spriteRow.innerHTML = "<label>Sprite</label>";
          const spriteWrap = document.createElement("div");
          spriteWrap.className = "sprite-dropdown-wrap";
          const spriteBtn = document.createElement("button");
          spriteBtn.type = "button";
          spriteBtn.className = "sprite-dropdown-btn";
          const currentSpriteId = String(npc.record.spriteId || "");
          const spriteOptions = getSpriteCatalogRecords();
          const currentSprite = spriteOptions.find((entry) => entry.id === currentSpriteId) || null;
          const currentThumb = currentSprite && currentSprite.dataUrl
            ? '<img class="npc-thumb" alt="sprite" src="' + scope.runtimeEscapeHtml(currentSprite.dataUrl) + '">'
            : '<span class="npc-thumb-fallback">Spr</span>';
          const currentLabel = currentSpriteId || "placeholder";
          spriteBtn.innerHTML =
            '<span class="sprite-dropdown-current">' +
            currentThumb +
            "<span>" + scope.runtimeEscapeHtml(currentLabel) + "</span></span>" +
            "<span>" + (scope.spritePickerOpenNpcId === npc.id ? "▲" : "▼") + "</span>";
          spriteBtn.addEventListener("click", () => {
            scope.spritePickerOpenNpcId = scope.spritePickerOpenNpcId === npc.id ? "" : npc.id;
            renderNpcList();
          });
          spriteWrap.appendChild(spriteBtn);
          if (scope.spritePickerOpenNpcId === npc.id) {
            const menu = document.createElement("div");
            menu.className = "sprite-dropdown-menu";
            const templateBtn2 = document.createElement("button");
            templateBtn2.type = "button";
            templateBtn2.className = "sprite-option-btn" + (!currentSpriteId ? " active" : "");
            templateBtn2.textContent = "placeholder";
            templateBtn2.addEventListener("click", () => {
              applyNpcEditorChange(npc, (target) => {
                target.record.spriteId = "";
                scope.spritePickerOpenNpcId = "";
              }, "Sprite");
            });
            menu.appendChild(templateBtn2);
            spriteOptions.forEach((entry) => {
              const optionBtn = document.createElement("button");
              optionBtn.type = "button";
              optionBtn.className = "sprite-option-btn" + (entry.id === currentSpriteId ? " active" : "");
              optionBtn.innerHTML =
                (entry.dataUrl
                  ? '<img class="npc-thumb" alt="sprite" src="' + scope.runtimeEscapeHtml(entry.dataUrl) + '">'
                  : '<span class="npc-thumb-fallback">Spr</span>') +
                "<span>" + scope.runtimeEscapeHtml(entry.id + " - " + entry.name) + "</span>";
              optionBtn.addEventListener("click", () => {
                applyNpcEditorChange(npc, (target) => {
                  target.record.spriteId = entry.id;
                  scope.spritePickerOpenNpcId = "";
                }, "Sprite");
              });
              menu.appendChild(optionBtn);
            });
            spriteWrap.appendChild(menu);
          }
          spriteRow.appendChild(spriteWrap);
          editorPanel.appendChild(spriteRow);

          const dialogueRow = document.createElement("div");
          dialogueRow.className = "npc-editor-row";
          dialogueRow.innerHTML = '<label>Dialogue</label><div class="muted">Use the D button above to change the dialogue.</div>';
          editorPanel.appendChild(dialogueRow);

          const descriptionRow = document.createElement("div");
          descriptionRow.className = "npc-editor-row";
          descriptionRow.innerHTML = "<label>Description</label>";
          const descriptionInput = document.createElement("textarea");
          descriptionInput.className = "npc-description-box";
          descriptionInput.value = String(npc.record.description || "");
          descriptionInput.placeholder = "Optional NPC notes or description";
          descriptionInput.addEventListener("input", () => {
            npc.record.description = String(descriptionInput.value || "");
            npc.description = npc.record.description;
          });
          descriptionInput.addEventListener("change", () => {
            applyNpcEditorChange(npc, (target) => {
              target.record.description = String(descriptionInput.value || "");
            }, "Description");
          });
          descriptionRow.appendChild(descriptionInput);
          editorPanel.appendChild(descriptionRow);

          row.appendChild(editorPanel);
        }

        return row;
      },
    });
  }

  function centerViewportOnNpc(npc) {
    if (!npc || !isNpcPlaced(npc)) {
      return;
    }
    const centerX = npc.x * scope.tileSize + scope.getScaledSize(npc.spriteWidth, scope.baseTileSize) / 2;
    const centerY = npc.y * scope.tileSize + scope.getScaledSize(npc.spriteHeight, scope.baseTileSize) / 2;
    const maxScrollLeft = Math.max(0, scope.viewport.scrollWidth - scope.viewport.clientWidth);
    const maxScrollTop = Math.max(0, scope.viewport.scrollHeight - scope.viewport.clientHeight);
    const nextLeft = Math.max(0, Math.min(maxScrollLeft, Math.floor(centerX - scope.viewport.clientWidth / 2)));
    const nextTop = Math.max(0, Math.min(maxScrollTop, Math.floor(centerY - scope.viewport.clientHeight / 2)));
    scope.viewport.scrollLeft = nextLeft;
    scope.viewport.scrollTop = nextTop;
  }

  function selectNpc(npc, options = {}) {
    const shouldCenterViewport = options.centerViewport !== false;
    scope.selectedNpcId = npc.id;
    scope.spritePickerOpenNpcId = "";
    if (scope.activeInstanceBrushId) {
      scope.activeInstanceBrushId = "";
      scope.renderInstancePalette();
    }
    scope.selectedTile = null;
    scope.setSidebarTab("instances");
    scope.setLayerVisibility(Number(npc.layer) || 0, true);
    renderNpcList();
    scope.draw();
    if (isNpcPlaced(npc)) {
      if (shouldCenterViewport) {
        centerViewportOnNpc(npc);
      }
      scope.setStatus("Selected entity " + (npc.name || npc.id) + ".", false);
    } else {
      scope.setStatus("Selected unplaced entity " + (npc.name || npc.id) + ". Click the canvas to place it.", false);
    }
  }

  function removeNpcInstanceById(instanceId, sourceLabel) {
    const index = scope.npcOverlays.findIndex((entry) => entry.id === instanceId);
    if (index < 0) {
      return false;
    }
    const beforeCount = scope.npcOverlays.length;
    const npc = scope.npcOverlays[index];
    const beforeSnapshot = scope.cloneNpcOverlays([npc])[0];
    const tileX = Math.floor(Number(npc?.x));
    const tileY = Math.floor(Number(npc?.y));
    scope.npcOverlays.splice(index, 1);
    if (typeof scope.rebuildWorldChunksForLocalBounds === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      if (Number.isFinite(tileX) && Number.isFinite(tileY) && tileX >= 0 && tileY >= 0) {
        scope.rebuildWorldChunksForLocalBounds({ minX: tileX, minY: tileY, maxX: tileX, maxY: tileY });
      }
    }
    delete scope.npcImages[instanceId];
    if (scope.selectedNpcId === instanceId) {
      scope.selectedNpcId = "";
    }
    renderNpcList();
    scope.renderInstancePalette();
    scope.draw();
    scope.registerHistory("Entity removed", "entities:" + beforeCount, "entities:" + scope.npcOverlays.length, [
      "Removed entity: " + (npc.name || npc.id),
      "Position: (" + npc.x + "," + npc.y + ")",
      "Source: " + sourceLabel,
    ], {
      operation: {
        type: "npc_entries",
        entries: [{
          before: beforeSnapshot,
          after: null,
          beforeIndex: index,
          afterIndex: -1,
        }],
      },
    });
    scope.setStatus("Removed entity " + (npc.name || npc.id) + ".", false);
    return true;
  }

  function findPlacedNpcByTemplateId(templateId) {
    const normalizedId = String(templateId || "").trim();
    if (!normalizedId) {
      return null;
    }
    return scope.npcOverlays.find((entry) => {
      const record = entry && entry.record && typeof entry.record === "object" && !Array.isArray(entry.record)
        ? entry.record
        : {};
      return String(record.templateId || "").trim() === normalizedId && Number(entry.x) >= 0 && Number(entry.y) >= 0;
    }) || null;
  }

  function findOpenNpcSpawnTile() {
    const occupied = new Set(scope.npcOverlays.map((npc) => npc.x + ":" + npc.y));
    const preferredTiles = [];
    if (scope.selectedTile && Number.isFinite(scope.selectedTile.x) && Number.isFinite(scope.selectedTile.y)) {
      preferredTiles.push({ x: Number(scope.selectedTile.x), y: Number(scope.selectedTile.y) });
    }
    if (Number.isFinite(scope.hoverTileX) && Number.isFinite(scope.hoverTileY) && scope.hoverTileX >= 0 && scope.hoverTileY >= 0) {
      preferredTiles.push({ x: Number(scope.hoverTileX), y: Number(scope.hoverTileY) });
    }

    for (const tile of preferredTiles) {
      if (tile.x >= 0 && tile.x < scope.width && tile.y >= 0 && tile.y < scope.height) {
        const key = tile.x + ":" + tile.y;
        if (!occupied.has(key)) {
          return { x: tile.x, y: tile.y };
        }
      }
    }

    const centerX = Math.max(0, Math.min(scope.width - 1, Math.floor(scope.width / 2)));
    const centerY = Math.max(0, Math.min(scope.height - 1, Math.floor(scope.height / 2)));
    if (!occupied.has(centerX + ":" + centerY)) {
      return { x: centerX, y: centerY };
    }
    for (let radius = 1; radius < Math.max(scope.width, scope.height); radius += 1) {
      for (let y = Math.max(0, centerY - radius); y <= Math.min(scope.height - 1, centerY + radius); y += 1) {
        for (let x = Math.max(0, centerX - radius); x <= Math.min(scope.width - 1, centerX + radius); x += 1) {
          const key = x + ":" + y;
          if (!occupied.has(key)) {
            return { x, y };
          }
        }
      }
    }
    return { x: centerX, y: centerY };
  }

  function createNewNpc() {
    scope.activeInstanceBrushId = "";
    scope.spritePickerOpenNpcId = "";
    const slotId = scope.runtimeUniqueId();
    const spawnLayer = scope.getEditableLayerNumber();
    const activeEntityType = normalizeEntityType(scope.activeEntityCategory, "friendly");
    scope.setLayerVisibility(spawnLayer, true);
    const overlay = {
      id: slotId,
      layer: spawnLayer,
      name: "Unassigned " + getEntityTypeMeta(activeEntityType).label + " Entity",
      spriteId: "",
      isPlacementSlot: true,
      x: -1,
      y: -1,
      dataUrl: null,
      spriteWidth: 28,
      spriteHeight: 28,
      record: {
        ...JSON.parse(JSON.stringify(scope.defaultNpcTemplate || {})),
        id: slotId,
        layer: spawnLayer,
        position: { x: -1, y: -1 },
        name: "",
        entityType: activeEntityType,
        faction: "",
        spriteId: "",
        dialogueId: "",
        description: "",
        enabled: true,
      },
    };
    scope.npcOverlays.push(overlay);
    scope.selectedNpcId = slotId;
    scope.selectedTile = null;
    scope.setSidebarTab("instances");
    scope.renderInstancePalette();
    renderNpcList();
    scope.draw();
    const afterSnapshot = scope.cloneNpcOverlays([overlay])[0];
    scope.registerHistory("Entity created", "none", "unplaced", [
      "Created unassigned unique entity.",
      "Entity id: " + slotId,
      "Type: " + getEntityTypeLabel(activeEntityType),
      "State: unplaced placeholder",
    ], {
      operation: {
        type: "npc_entries",
        entries: [{
          before: null,
          after: afterSnapshot,
          beforeIndex: -1,
          afterIndex: scope.npcOverlays.length - 1,
        }],
      },
    });
    scope.setStatus("Created new unplaced " + getEntityTypeMeta(activeEntityType).noun + " " + slotId + ". Click the canvas to place it.", false);
  }

  return {
    getVisibleNpcOverlays,
    getNpcCatalogRecords,
    getNpcEntityType,
    normalizeEntityType,
    getEntityTypeLabel,
    setNpcCatalogEntityType,
    getDialogueCatalogRecords,
    getFactionRecords,
    getSpriteCatalogRecords,
    applyNpcEditorChange,
    ensureNpcImageLoaded,
    getCachedImage,
    assignNpcToSlot,
    renderNpcList,
    openPlacedEntityContextMenu,
    centerViewportOnNpc,
    selectNpc,
    removeNpcInstanceById,
    findPlacedNpcByTemplateId,
    findOpenNpcSpawnTile,
    createNewNpc,
  };
}
