import React, { Suspense, lazy, useCallback, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';

import { ipcChannels, type SessionState } from '../../shared/ipc/contracts';
import { buildNpcGroupForest, type NpcGroupTreeNode } from '../../shared/npcs/npcGroups';
import type {
  NpcGroupId,
  NpcId,
  NpcRelationId,
  ProjectNpc,
  ProjectNpcGroup,
  ProjectNpcRelation,
} from '../../shared/types';
import editorStyles from '../editor/EditorApp.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { getDndApi } from '../shared/dndApi';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import { PlayerTokenView } from '../shared/playerToken/PlayerTokenView';
import { Button, Input, Select } from '../shared/ui/controls';
import { useAssetUrl } from '../shared/useAssetImageUrl';
import { npcDispositionRingColor } from '../../shared/types/npcDisposition';

import { NpcDescriptionField } from './NpcDescriptionField';
import { NpcEditModal } from './NpcEditModal';
import type { GraphGroupFilter } from './NpcGraph';
import { NpcGroupModal } from './NpcGroupModal';
import {
  flattenGroupOptions,
  moveNpcToGroupEnd,
  reorderNpcIds,
  reorderSiblingGroups,
} from './npcListHelpers';
import { NpcRelationModal } from './NpcRelationModal';
import styles from './NpcsEditorApp.module.css';

const NpcGraph = lazy(async () => {
  const mod = await import('./NpcGraph');
  return { default: mod.NpcGraph };
});

const DND_NPC_ID_MIME = 'application/x-dnd-npc-id';
const DND_NPC_GROUP_ID_MIME = 'application/x-dnd-npc-group-id';

type GroupModalState =
  | { mode: 'create'; parentId: NpcGroupId | null }
  | { mode: 'edit'; group: ProjectNpcGroup };

function NpcTile({
  npc,
  selected,
  dragging,
  dropPlace,
  accentColor,
  onSelect,
  onMenu,
  onDragStart,
  onDragEnd,
  onDragOver,
  onDropReorder,
}: {
  npc: ProjectNpc;
  selected: boolean;
  dragging: boolean;
  dropPlace: 'before' | 'after' | null;
  accentColor?: string | null;
  onSelect: () => void;
  onMenu: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onDragStart: () => void;
  onDragEnd: () => void;
  onDragOver: (place: 'before' | 'after') => void;
  onDropReorder: () => void;
}) {
  const { t } = useEditorI18n();
  const url = useAssetUrl(npc.avatarAssetId);
  return (
    <div
      className={[
        styles.tile,
        selected ? styles.tileSelected : '',
        dragging ? styles.tileDragging : '',
        dropPlace === 'before' ? styles.tileDropBefore : '',
        dropPlace === 'after' ? styles.tileDropAfter : '',
      ]
        .filter(Boolean)
        .join(' ')}
      style={accentColor ? { borderLeftColor: accentColor, borderLeftWidth: 3 } : undefined}
      draggable
      onDragStart={(e) => {
        e.dataTransfer.setData(DND_NPC_ID_MIME, npc.id);
        e.dataTransfer.effectAllowed = 'move';
        onDragStart();
      }}
      onDragEnd={onDragEnd}
      onDragOver={(e) => {
        e.preventDefault();
        const rect = e.currentTarget.getBoundingClientRect();
        const mid = rect.top + rect.height / 2;
        onDragOver(e.clientY < mid ? 'before' : 'after');
      }}
      onDrop={(e) => {
        e.preventDefault();
        onDropReorder();
      }}
    >
      <button type="button" className={styles.tileBody} onClick={onSelect}>
        <div className={styles.tileAvatar}>
          {url ? <img className={styles.tileAvatarImg} src={url} alt="" draggable={false} /> : null}
        </div>
        <div className={styles.tileName}>{npc.name}</div>
      </button>
      <button
        type="button"
        className={styles.tileMenuBtn}
        data-npc-menu-root="1"
        aria-label={t('npcs.tileMenu')}
        onClick={onMenu}
      >
        ⋮
      </button>
    </div>
  );
}

function GroupSection({
  node,
  depth,
  isExpanded,
  onToggleExpanded,
  selectedId,
  dragId,
  dropPlace,
  dragGroupId,
  groupDropPlace,
  dropTargetGroup,
  groupColorById,
  onSelectNpc,
  onNpcMenu,
  onGroupMenu,
  onNpcDragStart,
  onNpcDragEnd,
  onNpcDragOver,
  onNpcDropReorder,
  onGroupDragStart,
  onGroupDragEnd,
  onGroupDragOver,
  onGroupDropReorder,
  onGroupBodyDragOver,
  onGroupBodyDrop,
}: {
  node: NpcGroupTreeNode;
  depth: number;
  isExpanded: (id: NpcGroupId) => boolean;
  onToggleExpanded: (id: NpcGroupId) => void;
  selectedId: NpcId | null;
  dragId: NpcId | null;
  dropPlace: { id: NpcId; place: 'before' | 'after' } | null;
  dragGroupId: NpcGroupId | null;
  groupDropPlace: { id: NpcGroupId; place: 'before' | 'after' } | null;
  dropTargetGroup: NpcGroupId | null;
  groupColorById: Map<NpcGroupId, string>;
  onSelectNpc: (id: NpcId) => void;
  onNpcMenu: (id: NpcId, e: React.MouseEvent<HTMLButtonElement>) => void;
  onGroupMenu: (id: NpcGroupId, e: React.MouseEvent<HTMLButtonElement>) => void;
  onNpcDragStart: (id: NpcId) => void;
  onNpcDragEnd: () => void;
  onNpcDragOver: (id: NpcId, place: 'before' | 'after') => void;
  onNpcDropReorder: (targetId: NpcId) => void;
  onGroupDragStart: (id: NpcGroupId) => void;
  onGroupDragEnd: () => void;
  onGroupDragOver: (id: NpcGroupId, place: 'before' | 'after') => void;
  onGroupDropReorder: (targetId: NpcGroupId) => void;
  onGroupBodyDragOver: (groupId: NpcGroupId) => void;
  onGroupBodyDrop: (groupId: NpcGroupId) => void;
}) {
  const { t } = useEditorI18n();
  const g = node.group;
  const expanded = isExpanded(g.id);
  const isGroupDragging = dragGroupId === g.id;
  const groupDropBefore = groupDropPlace?.id === g.id && groupDropPlace.place === 'before';
  const groupDropAfter = groupDropPlace?.id === g.id && groupDropPlace.place === 'after';
  const bodyHighlight = dropTargetGroup === g.id && dragId !== null;

  return (
    <div className={styles.groupBlock} style={{ paddingLeft: depth > 0 ? 12 : 0 }}>
      <div
        className={[
          styles.groupHeader,
          isGroupDragging ? styles.groupHeaderDragging : '',
          groupDropBefore ? styles.groupDropBefore : '',
          groupDropAfter ? styles.groupDropAfter : '',
        ]
          .filter(Boolean)
          .join(' ')}
        draggable
        onDragStart={(e) => {
          e.dataTransfer.setData(DND_NPC_GROUP_ID_MIME, g.id);
          e.dataTransfer.effectAllowed = 'move';
          onGroupDragStart(g.id);
        }}
        onDragEnd={onGroupDragEnd}
        onDragOver={(e) => {
          if (dragGroupId && dragGroupId !== g.id) {
            e.preventDefault();
            e.stopPropagation();
            const rect = e.currentTarget.getBoundingClientRect();
            const mid = rect.top + rect.height / 2;
            onGroupDragOver(g.id, e.clientY < mid ? 'before' : 'after');
            return;
          }
          if (dragId) {
            e.preventDefault();
            onGroupBodyDragOver(g.id);
          }
        }}
        onDrop={(e) => {
          e.preventDefault();
          e.stopPropagation();
          if (dragGroupId && dragGroupId !== g.id && groupDropPlace?.id === g.id) {
            onGroupDropReorder(g.id);
            return;
          }
          if (dragId) onGroupBodyDrop(g.id);
        }}
      >
        <button
          type="button"
          className={styles.groupToggle}
          onClick={() => onToggleExpanded(g.id)}
          aria-expanded={expanded}
        >
          {expanded ? '▾' : '▸'}
        </button>
        <span className={styles.groupColorDot} style={{ background: g.color }} aria-hidden />
        <span className={styles.groupTitle}>{g.name}</span>
        <button
          type="button"
          className={styles.groupMenuBtn}
          data-npc-menu-root="1"
          aria-label={t('npcs.tileMenu')}
          onClick={(e) => onGroupMenu(g.id, e)}
        >
          ⋮
        </button>
      </div>

      {expanded ? (
        <div
          className={[styles.groupBody, bodyHighlight ? styles.groupBodyDrop : ''].filter(Boolean).join(' ')}
          onDragOver={(e) => {
            if (!dragId) return;
            e.preventDefault();
            onGroupBodyDragOver(g.id);
          }}
          onDrop={(e) => {
            if (!dragId) return;
            e.preventDefault();
            onGroupBodyDrop(g.id);
          }}
        >
          {node.npcs.map((n) => (
            <NpcTile
              key={n.id}
              npc={n}
              selected={n.id === selectedId}
              dragging={dragId === n.id}
              dropPlace={dropPlace?.id === n.id ? dropPlace.place : null}
              accentColor={groupColorById.get(g.id) ?? g.color}
              onSelect={() => onSelectNpc(n.id)}
              onMenu={(e) => onNpcMenu(n.id, e)}
              onDragStart={() => onNpcDragStart(n.id)}
              onDragEnd={onNpcDragEnd}
              onDragOver={(place) => onNpcDragOver(n.id, place)}
              onDropReorder={() => onNpcDropReorder(n.id)}
            />
          ))}
          {node.children.map((child) => (
            <GroupSection
              key={child.group.id}
              node={child}
              depth={depth + 1}
              isExpanded={isExpanded}
              onToggleExpanded={onToggleExpanded}
              selectedId={selectedId}
              dragId={dragId}
              dropPlace={dropPlace}
              dragGroupId={dragGroupId}
              groupDropPlace={groupDropPlace}
              dropTargetGroup={dropTargetGroup}
              groupColorById={groupColorById}
              onSelectNpc={onSelectNpc}
              onNpcMenu={onNpcMenu}
              onGroupMenu={onGroupMenu}
              onNpcDragStart={onNpcDragStart}
              onNpcDragEnd={onNpcDragEnd}
              onNpcDragOver={onNpcDragOver}
              onNpcDropReorder={onNpcDropReorder}
              onGroupDragStart={onGroupDragStart}
              onGroupDragEnd={onGroupDragEnd}
              onGroupDragOver={onGroupDragOver}
              onGroupDropReorder={onGroupDropReorder}
              onGroupBodyDragOver={onGroupBodyDragOver}
              onGroupBodyDrop={onGroupBodyDrop}
            />
          ))}
        </div>
      ) : null}
    </div>
  );
}

export function NpcsEditorApp() {
  const { t } = useEditorI18n();
  const api = getDndApi();
  const [session, setSession] = useState<SessionState | null>(null);
  const [selectedId, setSelectedId] = useState<NpcId | null>(null);
  const [query, setQuery] = useState('');
  const [editOpen, setEditOpen] = useState(false);
  const [editInitial, setEditInitial] = useState<ProjectNpc | null>(null);
  const [pendingDelete, setPendingDelete] = useState<ProjectNpc | null>(null);
  const [menuFor, setMenuFor] = useState<NpcId | null>(null);
  const [groupMenuFor, setGroupMenuFor] = useState<NpcGroupId | null>(null);
  const [menuPos, setMenuPos] = useState<{ left: number; top: number } | null>(null);
  const [dragId, setDragId] = useState<NpcId | null>(null);
  const [dropPlace, setDropPlace] = useState<{ id: NpcId; place: 'before' | 'after' } | null>(null);
  const [dragGroupId, setDragGroupId] = useState<NpcGroupId | null>(null);
  const [groupDropPlace, setGroupDropPlace] = useState<{
    id: NpcGroupId;
    place: 'before' | 'after';
  } | null>(null);
  const [dropTargetGroup, setDropTargetGroup] = useState<NpcGroupId | 'ungrouped' | null>(null);
  const [expandedGroups, setExpandedGroups] = useState<Set<NpcGroupId>>(() => new Set());
  const [groupModal, setGroupModal] = useState<GroupModalState | null>(null);
  const [pendingDeleteGroup, setPendingDeleteGroup] = useState<ProjectNpcGroup | null>(null);
  const [graphFilter, setGraphFilter] = useState<GraphGroupFilter>('all');
  const [nameDraft, setNameDraft] = useState('');
  const [relationModal, setRelationModal] = useState<
    | { mode: 'create'; sourceNpcId: NpcId; targetNpcId: NpcId }
    | { mode: 'edit'; relationId: NpcRelationId; label: string }
    | null
  >(null);
  const [pendingDeleteRelation, setPendingDeleteRelation] = useState<ProjectNpcRelation | null>(null);
  const [avatarBusy, setAvatarBusy] = useState(false);

  useEffect(() => {
    void api.invoke(ipcChannels.project.get, {}).then(({ project }) => {
      setSession({ project, currentSceneId: project?.currentSceneId ?? null });
      const list = project?.npcs ?? [];
      setSelectedId(list[0]?.id ?? null);
      const groups = project?.npcGroups ?? [];
      setExpandedGroups(new Set(groups.map((g) => g.id)));
    });
    return api.on(ipcChannels.session.stateChanged, ({ state }) => {
      setSession(state);
    });
  }, [api]);

  const project = session?.project ?? null;
  const npcs = useMemo(() => project?.npcs ?? [], [project?.npcs]);
  const npcGroups = useMemo(() => project?.npcGroups ?? [], [project?.npcGroups]);
  const relations = useMemo(() => project?.npcRelations ?? [], [project?.npcRelations]);
  const selected = npcs.find((n) => n.id === selectedId) ?? null;

  useEffect(() => {
    setNameDraft(selected?.name ?? '');
  }, [selected?.id, selected?.name]);

  useEffect(() => {
    if (selectedId && npcs.some((n) => n.id === selectedId)) return;
    setSelectedId(npcs[0]?.id ?? null);
  }, [npcs, selectedId]);

  useEffect(() => {
    if (!menuFor && !groupMenuFor) return;
    const onDown = (e: MouseEvent) => {
      const tgt = e.target as HTMLElement | null;
      if (tgt?.closest('[data-npc-menu-root="1"]')) return;
      setMenuFor(null);
      setGroupMenuFor(null);
      setMenuPos(null);
    };
    window.addEventListener('mousedown', onDown);
    return () => window.removeEventListener('mousedown', onDown);
  }, [groupMenuFor, menuFor]);

  const filteredNpcs = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return npcs;
    return npcs.filter((n) => n.name.toLowerCase().includes(q));
  }, [npcs, query]);

  const { roots, ungrouped } = useMemo(
    () => buildNpcGroupForest(npcGroups, filteredNpcs),
    [filteredNpcs, npcGroups],
  );

  const groupColorById = useMemo(() => new Map(npcGroups.map((g) => [g.id, g.color])), [npcGroups]);

  const groupOptions = useMemo(() => flattenGroupOptions(roots), [roots]);

  const selectedUrl = useAssetUrl(selected?.avatarAssetId ?? null);

  const relationsForSelected = useMemo(() => {
    if (!selected) return [];
    return relations
      .filter((r) => r.sourceNpcId === selected.id)
      .map((r) => {
        const other = npcs.find((n) => n.id === r.targetNpcId);
        return { relation: r, otherName: other?.name ?? '—' };
      });
  }, [npcs, relations, selected]);

  const pickAvatar = useCallback(async () => {
    const res = await api.invoke(ipcChannels.project.pickNpcAvatar, {});
    if (res.canceled) return null;
    return { filePath: res.filePath, previewDataUrl: res.previewDataUrl };
  }, [api]);

  const commitNpcGroupChange = useCallback(
    async (npcId: NpcId, groupId: NpcGroupId | null, orderIds?: NpcId[]) => {
      const npc = npcs.find((n) => n.id === npcId);
      if (npc && npc.groupId !== groupId) {
        await api.invoke(ipcChannels.project.updateNpcFields, { npcId, groupId });
      }
      if (orderIds) {
        await api.invoke(ipcChannels.project.setNpcsOrder, { npcIds: orderIds });
      }
    },
    [api, npcs],
  );

  const handleNpcDropOnTile = useCallback(
    (targetId: NpcId) => {
      if (!dragId || dropPlace?.id !== targetId || dragId === targetId) return;
      const targetNpc = npcs.find((n) => n.id === targetId);
      if (!targetNpc) return;
      const dragNpc = npcs.find((n) => n.id === dragId);
      if (!dragNpc) return;

      const newGroupId = targetNpc.groupId;
      const orderIds = reorderNpcIds(
        dragNpc.groupId === newGroupId
          ? npcs
          : npcs.map((n) => (n.id === dragId ? { ...n, groupId: newGroupId } : n)),
        dragId,
        targetId,
        dropPlace.place,
      );

      setDragId(null);
      setDropPlace(null);
      setDropTargetGroup(null);
      void commitNpcGroupChange(dragId, newGroupId, orderIds);
    },
    [commitNpcGroupChange, dragId, dropPlace, npcs],
  );

  const handleNpcDropOnGroup = useCallback(
    (groupId: NpcGroupId | null) => {
      if (!dragId) return;
      const dragNpc = npcs.find((n) => n.id === dragId);
      if (!dragNpc) return;
      const orderIds = moveNpcToGroupEnd(
        dragNpc.groupId === groupId ? npcs : npcs.map((n) => (n.id === dragId ? { ...n, groupId } : n)),
        dragId,
        groupId,
      );
      setDragId(null);
      setDropPlace(null);
      setDropTargetGroup(null);
      void commitNpcGroupChange(dragId, groupId, orderIds);
    },
    [commitNpcGroupChange, dragId, npcs],
  );

  const handleGroupDropReorder = useCallback(
    (targetId: NpcGroupId) => {
      if (!dragGroupId || !groupDropPlace || dragGroupId === groupDropPlace.id) return;
      const order = reorderSiblingGroups(npcGroups, dragGroupId, targetId, groupDropPlace.place);
      setDragGroupId(null);
      setGroupDropPlace(null);
      void api.invoke(ipcChannels.project.setNpcGroupsOrder, { groupIds: order });
    },
    [api, dragGroupId, groupDropPlace, npcGroups],
  );

  const openMenuAt = (e: React.MouseEvent<HTMLButtonElement>) => {
    const r = e.currentTarget.getBoundingClientRect();
    const menuW = 180;
    const menuH = 120;
    const left = Math.max(8, Math.min(r.right - menuW, window.innerWidth - menuW - 8));
    const top = r.bottom + 8 + menuH > window.innerHeight - 8 ? Math.max(8, r.top - menuH - 8) : r.bottom + 8;
    setMenuPos({ left, top });
  };

  const graphUi = useMemo(
    () => ({
      zoomBar: t('npcs.graphZoomBar'),
      zoomIn: t('npcs.graphZoomIn'),
      zoomOut: t('npcs.graphZoomOut'),
      fitAll: t('npcs.graphFitAll'),
      editRelation: t('npcs.relationEdit'),
      deleteRelation: t('npcs.relationDelete'),
      untitled: t('npcs.untitled'),
      graphFilter: t('npcs.graphFilter'),
      graphFilterAll: t('npcs.graphFilterAll'),
      graphFilterUngrouped: t('npcs.graphFilterUngrouped'),
    }),
    [t],
  );

  const isExpanded = (id: NpcGroupId) => expandedGroups.has(id);
  const toggleExpanded = (id: NpcGroupId) => {
    setExpandedGroups((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  const siblingNamesForGroup = (parentId: NpcGroupId | null, exceptId?: NpcGroupId) =>
    npcGroups.filter((g) => g.parentId === parentId && g.id !== exceptId).map((g) => g.name);

  const hasListContent = npcGroups.length > 0 || ungrouped.length > 0;

  return (
    <div className={styles.page}>
      <div className={styles.topBar}>
        <div className={styles.topTitle}>{t('npcs.editorTitle')}</div>
        <Button
          onClick={() => {
            void api.invoke(ipcChannels.windows.closeNpcsEditor, {});
          }}
        >
          {t('common.close')}
        </Button>
      </div>

      <div className={styles.body}>
        <div className={[styles.col, styles.side].join(' ')}>
          <Input value={query} onChange={setQuery} placeholder={t('npcs.search')} />
          <div className={styles.sideActions}>
            <Button
              variant="primary"
              onClick={() => {
                setEditInitial(null);
                setEditOpen(true);
              }}
            >
              {t('npcs.add')}
            </Button>
            <Button
              onClick={() => {
                setGroupModal({ mode: 'create', parentId: null });
              }}
            >
              {t('npcs.addGroup')}
            </Button>
          </div>
          <div className={styles.list}>
            {roots.map((node) => (
              <GroupSection
                key={node.group.id}
                node={node}
                depth={0}
                isExpanded={isExpanded}
                onToggleExpanded={toggleExpanded}
                selectedId={selectedId}
                dragId={dragId}
                dropPlace={dropPlace}
                dragGroupId={dragGroupId}
                groupDropPlace={groupDropPlace}
                dropTargetGroup={dropTargetGroup === 'ungrouped' ? null : dropTargetGroup}
                groupColorById={groupColorById}
                onSelectNpc={setSelectedId}
                onNpcMenu={(id, e) => {
                  openMenuAt(e);
                  setGroupMenuFor(null);
                  setMenuFor((cur) => (cur === id ? null : id));
                }}
                onGroupMenu={(id, e) => {
                  openMenuAt(e);
                  setMenuFor(null);
                  setGroupMenuFor((cur) => (cur === id ? null : id));
                }}
                onNpcDragStart={setDragId}
                onNpcDragEnd={() => {
                  setDragId(null);
                  setDropPlace(null);
                  setDropTargetGroup(null);
                }}
                onNpcDragOver={(id, place) => {
                  if (!dragId || dragId === id) {
                    setDropPlace(null);
                    return;
                  }
                  setDropPlace({ id, place });
                }}
                onNpcDropReorder={handleNpcDropOnTile}
                onGroupDragStart={setDragGroupId}
                onGroupDragEnd={() => {
                  setDragGroupId(null);
                  setGroupDropPlace(null);
                }}
                onGroupDragOver={(id, place) => {
                  if (!dragGroupId || dragGroupId === id) {
                    setGroupDropPlace(null);
                    return;
                  }
                  setGroupDropPlace({ id, place });
                }}
                onGroupDropReorder={handleGroupDropReorder}
                onGroupBodyDragOver={setDropTargetGroup}
                onGroupBodyDrop={(groupId) => handleNpcDropOnGroup(groupId)}
              />
            ))}

            <div className={styles.ungroupedSection}>
              <div className={styles.ungroupedHeader}>{t('npcs.ungrouped')}</div>
              <div
                className={[
                  styles.groupBody,
                  dropTargetGroup === 'ungrouped' && dragId ? styles.groupBodyDrop : '',
                ]
                  .filter(Boolean)
                  .join(' ')}
                onDragOver={(e) => {
                  if (!dragId) return;
                  e.preventDefault();
                  setDropTargetGroup('ungrouped');
                }}
                onDrop={(e) => {
                  if (!dragId) return;
                  e.preventDefault();
                  handleNpcDropOnGroup(null);
                }}
              >
                {ungrouped.map((n) => (
                  <NpcTile
                    key={n.id}
                    npc={n}
                    selected={n.id === selectedId}
                    dragging={dragId === n.id}
                    dropPlace={dropPlace?.id === n.id ? dropPlace.place : null}
                    onSelect={() => setSelectedId(n.id)}
                    onMenu={(e) => {
                      openMenuAt(e);
                      setGroupMenuFor(null);
                      setMenuFor((cur) => (cur === n.id ? null : n.id));
                    }}
                    onDragStart={() => setDragId(n.id)}
                    onDragEnd={() => {
                      setDragId(null);
                      setDropPlace(null);
                      setDropTargetGroup(null);
                    }}
                    onDragOver={(place) => {
                      if (!dragId || dragId === n.id) {
                        setDropPlace(null);
                        return;
                      }
                      setDropPlace({ id: n.id, place });
                    }}
                    onDropReorder={() => handleNpcDropOnTile(n.id)}
                  />
                ))}
              </div>
            </div>

            {!hasListContent && npcs.length === 0 ? (
              <div className={styles.muted}>{t('npcs.empty')}</div>
            ) : null}
            {npcs.length > 0 && filteredNpcs.length === 0 ? (
              <div className={styles.muted}>{t('npcs.searchEmpty')}</div>
            ) : null}
          </div>
        </div>

        <div className={styles.col}>
          <Suspense fallback={<div className={styles.graphLoading}>{t('npcs.graphLoading')}</div>}>
            <NpcGraph
              npcs={npcs}
              relations={relations}
              npcGroups={npcGroups}
              selectedNpcId={selectedId}
              graphFilter={graphFilter}
              onGraphFilterChange={setGraphFilter}
              graphUi={graphUi}
              onSelect={setSelectedId}
              onConnectRequest={(sourceNpcId, targetNpcId) => {
                setRelationModal({ mode: 'create', sourceNpcId, targetNpcId });
              }}
              onNodePositionCommit={(npcId, x, y) => {
                void (async () => {
                  setSession((prev) => {
                    if (!prev?.project) return prev;
                    return {
                      ...prev,
                      project: {
                        ...prev.project,
                        npcs: prev.project.npcs.map((n) => (n.id === npcId ? { ...n, x, y } : n)),
                      },
                    };
                  });
                  try {
                    const res = await api.invoke(ipcChannels.project.updateNpcPosition, {
                      npcId,
                      x,
                      y,
                    });
                    setSession({
                      project: res.project,
                      currentSceneId: res.project?.currentSceneId ?? null,
                    });
                  } catch {
                    /* позиция уже оптимистично в UI; следующий session sync поправит при CRUD */
                  }
                })();
              }}
              onEditRelation={(relationId) => {
                const rel = relations.find((r) => r.id === relationId);
                if (!rel) return;
                setRelationModal({ mode: 'edit', relationId, label: rel.label });
              }}
              onDeleteRelation={(relationId) => {
                const rel = relations.find((r) => r.id === relationId);
                if (!rel) return;
                setPendingDeleteRelation(rel);
              }}
            />
          </Suspense>
        </div>

        <div className={[styles.col, styles.inspector].join(' ')}>
          <div className={styles.inspectorScroll}>
            {selected && project ? (
              <>
                <div>
                  <div className={styles.fieldLabel}>{t('npcs.avatar')}</div>
                  <div className={styles.avatarPick}>
                    {USERS_BRANCH_FEATURES_ENABLED ? (
                      <PlayerTokenView
                        name={selected.name}
                        imageUrl={selectedUrl}
                        ringColor={npcDispositionRingColor(selected.disposition ?? 'neutral')}
                        imageOffset={selected.imageOffset}
                        imageScale={selected.imageScale}
                        sizePx={140}
                        panEnabled
                        onImageOffsetChange={(imageOffset) => {
                          void api.invoke(ipcChannels.project.updateNpcFields, {
                            npcId: selected.id,
                            imageOffset,
                          });
                        }}
                        onImageScaleChange={(imageScale) => {
                          void api.invoke(ipcChannels.project.updateNpcFields, {
                            npcId: selected.id,
                            imageScale,
                          });
                        }}
                      />
                    ) : (
                      <div className={styles.avatarPreview}>
                        {selectedUrl ? (
                          <img className={styles.avatarPreviewImg} src={selectedUrl} alt="" />
                        ) : null}
                      </div>
                    )}
                    <Button
                      disabled={avatarBusy}
                      onClick={() => {
                        void (async () => {
                          setAvatarBusy(true);
                          try {
                            const picked = await pickAvatar();
                            if (!picked) return;
                            await api.invoke(ipcChannels.project.upsertNpc, {
                              npcId: selected.id,
                              name: selected.name,
                              filePath: picked.filePath,
                            });
                          } finally {
                            setAvatarBusy(false);
                          }
                        })();
                      }}
                    >
                      {t('npcs.chooseAvatar')}
                    </Button>
                  </div>
                </div>

                {USERS_BRANCH_FEATURES_ENABLED ? (
                <label>
                  <div className={styles.fieldLabel}>{t('npcs.disposition')}</div>
                  <Select
                    value={selected.disposition ?? 'neutral'}
                    ariaLabel={t('npcs.disposition')}
                    onChange={(next) => {
                      void api.invoke(ipcChannels.project.updateNpcFields, {
                        npcId: selected.id,
                        disposition: next as 'hostile' | 'neutral' | 'friendly',
                      });
                    }}
                    options={[
                      { value: 'hostile', label: t('npcs.disposition.hostile') },
                      { value: 'neutral', label: t('npcs.disposition.neutral') },
                      { value: 'friendly', label: t('npcs.disposition.friendly') },
                    ]}
                  />
                </label>
                ) : null}

                <div>
                  <div className={styles.fieldLabel}>{t('npcs.name')}</div>
                  <Input
                    value={nameDraft}
                    onChange={setNameDraft}
                    onBlur={() => {
                      const next = nameDraft.trim();
                      if (!next || next === selected.name) {
                        setNameDraft(selected.name);
                        return;
                      }
                      void api
                        .invoke(ipcChannels.project.updateNpcFields, {
                          npcId: selected.id,
                          name: next,
                        })
                        .catch(() => setNameDraft(selected.name));
                    }}
                    placeholder={t('npcs.namePlaceholder')}
                  />
                </div>

                <div>
                  <div className={styles.fieldLabel}>{t('npcs.group')}</div>
                  <Select
                    value={selected.groupId ?? ''}
                    ariaLabel={t('npcs.group')}
                    onChange={(next) => {
                      const groupId = (next || null) as NpcGroupId | null;
                      void api.invoke(ipcChannels.project.updateNpcFields, {
                        npcId: selected.id,
                        groupId,
                      });
                    }}
                    options={[
                      { value: '', label: t('npcs.ungrouped') },
                      ...groupOptions.map((g) => ({ value: g.id, label: g.label })),
                    ]}
                  />
                </div>

                <div>
                  <div className={styles.fieldLabel}>{t('npcs.description')}</div>
                  <NpcDescriptionField
                    key={selected.id}
                    html={selected.description}
                    onCommit={(html) => {
                      if (html === selected.description) return;
                      void api.invoke(ipcChannels.project.updateNpcFields, {
                        npcId: selected.id,
                        description: html,
                      });
                    }}
                  />
                </div>

                {relationsForSelected.length > 0 ? (
                  <div>
                    <div className={styles.relationsTitle}>{t('npcs.relations')}</div>
                    <div className={styles.relationsList}>
                      {relationsForSelected.map(({ relation, otherName }) => (
                        <div key={relation.id} className={styles.relationItem}>
                          {relation.label} {otherName}
                        </div>
                      ))}
                    </div>
                  </div>
                ) : null}
              </>
            ) : (
              <div className={styles.emptyInspector}>{t('npcs.selectPrompt')}</div>
            )}
          </div>
        </div>
      </div>

      <NpcEditModal
        open={editOpen}
        initial={editInitial}
        existingNames={npcs.map((n) => n.name)}
        npcGroups={npcGroups}
        onClose={() => setEditOpen(false)}
        onPickImage={pickAvatar}
        onSave={async (input) => {
          const res = await api.invoke(ipcChannels.project.upsertNpc, {
            ...(editInitial ? { npcId: editInitial.id } : {}),
            name: input.name,
            ...(input.filePath ? { filePath: input.filePath } : {}),
            ...(input.groupId !== undefined ? { groupId: input.groupId } : {}),
          });
          const created = res.project.npcs.find((n) => n.name === input.name.trim());
          if (created) setSelectedId(created.id);
        }}
      />

      <NpcGroupModal
        open={Boolean(groupModal)}
        initial={groupModal?.mode === 'edit' ? groupModal.group : null}
        siblingNames={
          groupModal?.mode === 'edit'
            ? siblingNamesForGroup(groupModal.group.parentId, groupModal.group.id)
            : groupModal?.mode === 'create'
              ? siblingNamesForGroup(groupModal.parentId)
              : []
        }
        onClose={() => setGroupModal(null)}
        onSave={async ({ name, color }) => {
          if (!groupModal) return;
          if (groupModal.mode === 'edit') {
            await api.invoke(ipcChannels.project.upsertNpcGroup, {
              groupId: groupModal.group.id,
              name,
              color,
            });
          } else {
            await api.invoke(ipcChannels.project.upsertNpcGroup, {
              name,
              color,
              parentId: groupModal.parentId,
            });
          }
        }}
      />

      <NpcRelationModal
        open={Boolean(relationModal)}
        initialLabel={relationModal?.mode === 'edit' ? relationModal.label : ''}
        onClose={() => setRelationModal(null)}
        onSave={async (label) => {
          if (!relationModal) return;
          if (relationModal.mode === 'create') {
            await api.invoke(ipcChannels.project.upsertNpcRelation, {
              sourceNpcId: relationModal.sourceNpcId,
              targetNpcId: relationModal.targetNpcId,
              label,
            });
          } else {
            const rel = relations.find((r) => r.id === relationModal.relationId);
            if (!rel) return;
            await api.invoke(ipcChannels.project.upsertNpcRelation, {
              relationId: rel.id,
              sourceNpcId: rel.sourceNpcId,
              targetNpcId: rel.targetNpcId,
              label,
            });
          }
        }}
      />

      {menuFor && menuPos
        ? createPortal(
            <div
              role="menu"
              className={styles.menu}
              style={{ left: menuPos.left, top: menuPos.top }}
              data-npc-menu-root="1"
            >
              <button
                type="button"
                role="menuitem"
                className={styles.menuItemDanger}
                onClick={() => {
                  const npc = npcs.find((n) => n.id === menuFor);
                  if (npc) setPendingDelete(npc);
                  setMenuFor(null);
                }}
              >
                {t('common.delete')}
              </button>
            </div>,
            document.body,
          )
        : null}

      {groupMenuFor && menuPos
        ? createPortal(
            <div
              role="menu"
              className={styles.menu}
              style={{ left: menuPos.left, top: menuPos.top }}
              data-npc-menu-root="1"
            >
              <button
                type="button"
                role="menuitem"
                className={styles.menuItem}
                onClick={() => {
                  const g = npcGroups.find((x) => x.id === groupMenuFor);
                  if (g) setGroupModal({ mode: 'edit', group: g });
                  setGroupMenuFor(null);
                }}
              >
                {t('npcs.editGroup')}
              </button>
              <button
                type="button"
                role="menuitem"
                className={styles.menuItem}
                onClick={() => {
                  setGroupModal({ mode: 'create', parentId: groupMenuFor });
                  setGroupMenuFor(null);
                }}
              >
                {t('npcs.addSubgroup')}
              </button>
              <button
                type="button"
                role="menuitem"
                className={styles.menuItemDanger}
                onClick={() => {
                  const g = npcGroups.find((x) => x.id === groupMenuFor);
                  if (g) setPendingDeleteGroup(g);
                  setGroupMenuFor(null);
                }}
              >
                {t('npcs.deleteGroup')}
              </button>
            </div>,
            document.body,
          )
        : null}

      {pendingDelete
        ? createPortal(
            <>
              <button
                type="button"
                aria-label={t('common.close')}
                className={editorStyles.modalBackdrop}
                onClick={() => setPendingDelete(null)}
              />
              <div role="dialog" aria-modal="true" className={editorStyles.modalDialog}>
                <div className={editorStyles.modalHeader}>
                  <div className={editorStyles.modalTitle}>{t('npcs.deleteTitle')}</div>
                  <button
                    type="button"
                    className={editorStyles.modalClose}
                    onClick={() => setPendingDelete(null)}
                  >
                    ×
                  </button>
                </div>
                <div>{t('npcs.deleteConfirm', { name: pendingDelete.name })}</div>
                <div className={editorStyles.modalFooter}>
                  <Button onClick={() => setPendingDelete(null)}>{t('common.cancel')}</Button>
                  <Button
                    variant="primary"
                    onClick={() => {
                      const id = pendingDelete.id;
                      setPendingDelete(null);
                      void api.invoke(ipcChannels.project.deleteNpc, { npcId: id });
                    }}
                  >
                    {t('common.delete')}
                  </Button>
                </div>
              </div>
            </>,
            document.body,
          )
        : null}

      {pendingDeleteGroup
        ? createPortal(
            <>
              <button
                type="button"
                aria-label={t('common.close')}
                className={editorStyles.modalBackdrop}
                onClick={() => setPendingDeleteGroup(null)}
              />
              <div role="dialog" aria-modal="true" className={editorStyles.modalDialog}>
                <div className={editorStyles.modalHeader}>
                  <div className={editorStyles.modalTitle}>{t('npcs.deleteGroupTitle')}</div>
                  <button
                    type="button"
                    className={editorStyles.modalClose}
                    onClick={() => setPendingDeleteGroup(null)}
                  >
                    ×
                  </button>
                </div>
                <div>{t('npcs.deleteGroupConfirm', { name: pendingDeleteGroup.name })}</div>
                <div className={editorStyles.modalFooter}>
                  <Button onClick={() => setPendingDeleteGroup(null)}>{t('common.cancel')}</Button>
                  <Button
                    variant="primary"
                    onClick={() => {
                      const id = pendingDeleteGroup.id;
                      setPendingDeleteGroup(null);
                      void api.invoke(ipcChannels.project.deleteNpcGroup, { groupId: id });
                    }}
                  >
                    {t('common.delete')}
                  </Button>
                </div>
              </div>
            </>,
            document.body,
          )
        : null}

      {pendingDeleteRelation
        ? createPortal(
            <>
              <button
                type="button"
                aria-label={t('common.close')}
                className={editorStyles.modalBackdrop}
                onClick={() => setPendingDeleteRelation(null)}
              />
              <div role="dialog" aria-modal="true" className={editorStyles.modalDialog}>
                <div className={editorStyles.modalHeader}>
                  <div className={editorStyles.modalTitle}>{t('npcs.relationDeleteTitle')}</div>
                  <button
                    type="button"
                    className={editorStyles.modalClose}
                    onClick={() => setPendingDeleteRelation(null)}
                  >
                    ×
                  </button>
                </div>
                <div>{t('npcs.relationDeleteConfirm', { name: pendingDeleteRelation.label })}</div>
                <div className={editorStyles.modalFooter}>
                  <Button onClick={() => setPendingDeleteRelation(null)}>{t('common.cancel')}</Button>
                  <Button
                    variant="primary"
                    onClick={() => {
                      const id = pendingDeleteRelation.id;
                      setPendingDeleteRelation(null);
                      void api.invoke(ipcChannels.project.deleteNpcRelation, { relationId: id });
                    }}
                  >
                    {t('common.delete')}
                  </Button>
                </div>
              </div>
            </>,
            document.body,
          )
        : null}
    </div>
  );
}
