import React, { useCallback, useEffect, useMemo, useRef, 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, ProjectNpc } from '../../shared/types';
import matStyles from '../editor/MaterialsModals.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { sanitizeSceneDescriptionHtml } from '../editor/sceneDescriptionHtml';
import { getDndApi } from '../shared/dndApi';
import { useNpcsOverlayState } from '../shared/npcs/useNpcsOverlayState';
import { useNpcMapSpawnDrag } from '../shared/playerToken/useNpcMapSpawnDrag';
import { useSceneNpcTokensSession } from '../shared/playerToken/useSceneNpcTokensSession';
import { Button, Input } from '../shared/ui/controls';
import { useAssetUrl } from '../shared/useAssetImageUrl';
import {
  listVisibleSceneNpcTokens,
  nextSessionNpcSpawnPoint,
} from '../../shared/types/appPlayers';
import { normalizeNpcDisposition } from '../../shared/types/npcDisposition';

import styles from './NpcsApp.module.css';

function ZoomInIcon() {
  return (
    <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden>
      <circle cx="10.5" cy="10.5" r="6.25" fill="none" stroke="currentColor" strokeWidth="1.8" />
      <path d="M15.2 15.2 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
      <path
        d="M10.5 7.8v5.4M7.8 10.5h5.4"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.8"
        strokeLinecap="round"
      />
    </svg>
  );
}

function ZoomOutIcon() {
  return (
    <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden>
      <circle cx="10.5" cy="10.5" r="6.25" fill="none" stroke="currentColor" strokeWidth="1.8" />
      <path d="M15.2 15.2 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
      <path d="M7.8 10.5h5.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
    </svg>
  );
}

/** Убрать пустые ветки групп (удобно при поиске). */
function pruneEmptyGroupNodes(nodes: NpcGroupTreeNode[]): NpcGroupTreeNode[] {
  const out: NpcGroupTreeNode[] = [];
  for (const node of nodes) {
    const children = pruneEmptyGroupNodes(node.children);
    if (node.npcs.length === 0 && children.length === 0) continue;
    out.push({ ...node, children });
  }
  return out;
}

function RuntimeNpcTile({
  npc,
  selected,
  accentColor,
  mapDragEnabled,
  onActivate,
  onAddToMap,
  onMapDragBegin,
  onMapDragEnd,
}: {
  npc: ProjectNpc;
  selected: boolean;
  accentColor?: string | null;
  mapDragEnabled: boolean;
  onActivate: () => void;
  onAddToMap: () => void;
  onMapDragBegin: (npcId: NpcId) => void;
  onMapDragEnd: () => void;
}) {
  const { t } = useEditorI18n();
  const url = useAssetUrl(npc.avatarAssetId);
  const menuBtnRef = useRef<HTMLButtonElement | null>(null);
  const menuRef = useRef<HTMLDivElement | null>(null);
  const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
  const dragRef = useRef<{
    pointerId: number;
    x: number;
    y: number;
    started: boolean;
  } | null>(null);
  const suppressClickRef = useRef(false);

  useEffect(() => {
    if (!menu) return;
    const onPointerDown = (e: PointerEvent) => {
      const tgt = e.target;
      if (!(tgt instanceof Node)) return;
      if (menuBtnRef.current?.contains(tgt)) return;
      if (menuRef.current?.contains(tgt)) return;
      setMenu(null);
    };
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setMenu(null);
    };
    window.addEventListener('pointerdown', onPointerDown, true);
    window.addEventListener('keydown', onKey);
    return () => {
      window.removeEventListener('pointerdown', onPointerDown, true);
      window.removeEventListener('keydown', onKey);
    };
  }, [menu]);

  return (
    <div
      className={[styles.tile, selected ? styles.tileSelected : ''].filter(Boolean).join(' ')}
      style={accentColor ? { borderLeftColor: accentColor, borderLeftWidth: 3 } : undefined}
    >
      <button
        ref={menuBtnRef}
        type="button"
        className={styles.tileMenuBtn}
        aria-label={t('npcs.tileMenu')}
        aria-haspopup="menu"
        aria-expanded={menu !== null}
        title={t('npcs.tileMenu')}
        onPointerDown={(e) => e.stopPropagation()}
        onClick={(e) => {
          e.preventDefault();
          e.stopPropagation();
          const pad = 8;
          const menuW = 180;
          const menuH = 48;
          const rect = e.currentTarget.getBoundingClientRect();
          const x = Math.max(pad, Math.min(rect.right + 4, window.innerWidth - menuW - pad));
          const y = Math.max(pad, Math.min(rect.top, window.innerHeight - menuH - pad));
          setMenu((cur) => (cur ? null : { x, y }));
        }}
      >
        ⋮
      </button>
      <button
        type="button"
        className={styles.tileBody}
        onClick={(e) => {
          if (!suppressClickRef.current) return;
          suppressClickRef.current = false;
          e.preventDefault();
          e.stopPropagation();
        }}
        onPointerDown={(e) => {
          if (e.button !== 0 || !mapDragEnabled) return;
          dragRef.current = { pointerId: e.pointerId, x: e.clientX, y: e.clientY, started: false };
          e.currentTarget.setPointerCapture(e.pointerId);
        }}
        onPointerMove={(e) => {
          const d = dragRef.current;
          if (!d || d.pointerId !== e.pointerId || d.started) return;
          if (Math.hypot(e.clientX - d.x, e.clientY - d.y) < 7) return;
          d.started = true;
          onMapDragBegin(npc.id);
        }}
        onPointerUp={(e) => {
          const d = dragRef.current;
          if (!d || d.pointerId !== e.pointerId) return;
          dragRef.current = null;
          try {
            if (e.currentTarget.hasPointerCapture(e.pointerId)) {
              e.currentTarget.releasePointerCapture(e.pointerId);
            }
          } catch {
            /* ignore */
          }
          if (d.started) {
            suppressClickRef.current = true;
            e.preventDefault();
            onMapDragEnd();
            return;
          }
          onActivate();
        }}
        onPointerCancel={() => {
          dragRef.current = null;
        }}
      >
        <div className={styles.tileAvatar}>
          {url ? <img className={styles.tileAvatarImg} src={url} alt="" draggable={false} /> : null}
        </div>
        <div className={styles.tileName}>{npc.name}</div>
      </button>
      {menu
        ? createPortal(
            <div
              ref={menuRef}
              role="menu"
              className={styles.menu}
              style={{ left: menu.x, top: menu.y }}
              onPointerDown={(e) => e.stopPropagation()}
            >
              <button
                type="button"
                role="menuitem"
                className={styles.menuItem}
                disabled={!mapDragEnabled}
                title={mapDragEnabled ? t('npcs.addToMapHint') : t('npcs.addToMapNoScene')}
                onClick={() => {
                  setMenu(null);
                  if (!mapDragEnabled) return;
                  onAddToMap();
                }}
              >
                {t('npcs.addToMap')}
              </button>
            </div>,
            document.body,
          )
        : null}
    </div>
  );
}

function RuntimeGroupSection({
  node,
  depth,
  isExpanded,
  onToggleExpanded,
  selectedIds,
  mapDragEnabled,
  onActivate,
  onAddToMap,
  onMapDragBegin,
  onMapDragEnd,
}: {
  node: NpcGroupTreeNode;
  depth: number;
  isExpanded: (id: NpcGroupId) => boolean;
  onToggleExpanded: (id: NpcGroupId) => void;
  selectedIds: ReadonlySet<NpcId>;
  mapDragEnabled: boolean;
  onActivate: (id: NpcId) => void;
  onAddToMap: (id: NpcId) => void;
  onMapDragBegin: (npcId: NpcId) => void;
  onMapDragEnd: () => void;
}) {
  const g = node.group;
  const expanded = isExpanded(g.id);

  return (
    <div className={styles.groupBlock} style={{ paddingLeft: depth > 0 ? 12 : 0 }}>
      <div className={styles.groupHeader}>
        <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>
      </div>
      {expanded ? (
        <div className={styles.groupBody}>
          {node.npcs.map((n) => (
            <RuntimeNpcTile
              key={n.id}
              npc={n}
              selected={selectedIds.has(n.id)}
              accentColor={g.color}
              mapDragEnabled={mapDragEnabled}
              onActivate={() => onActivate(n.id)}
              onAddToMap={() => onAddToMap(n.id)}
              onMapDragBegin={onMapDragBegin}
              onMapDragEnd={onMapDragEnd}
            />
          ))}
          {node.children.map((child) => (
            <RuntimeGroupSection
              key={child.group.id}
              node={child}
              depth={depth + 1}
              isExpanded={isExpanded}
              onToggleExpanded={onToggleExpanded}
              selectedIds={selectedIds}
              mapDragEnabled={mapDragEnabled}
              onActivate={onActivate}
              onAddToMap={onAddToMap}
              onMapDragBegin={onMapDragBegin}
              onMapDragEnd={onMapDragEnd}
            />
          ))}
        </div>
      ) : null}
    </div>
  );
}

export function NpcsApp() {
  const { t } = useEditorI18n();
  const api = getDndApi();
  const [session, setSession] = useState<SessionState | null>(null);
  const [overlay, overlayApi] = useNpcsOverlayState();
  const [npcSession, npcSessionApi] = useSceneNpcTokensSession();
  const [mapDrag, mapDragApi] = useNpcMapSpawnDrag();
  const [query, setQuery] = useState('');
  const [collapsedGroups, setCollapsedGroups] = useState<Set<NpcGroupId>>(() => new Set());

  useEffect(() => {
    void api.invoke(ipcChannels.project.get, {}).then(({ project }) => {
      setSession({ project, currentSceneId: project?.currentSceneId ?? null });
    });
    return api.on(ipcChannels.session.stateChanged, ({ state }) => {
      setSession(state);
    });
  }, [api]);

  const activeIds = overlay?.activeNpcIds ?? [];
  const hasActive = activeIds.length > 0;

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        if (mapDrag.dragging) {
          void mapDragApi.cancel();
          return;
        }
        if (hasActive) {
          void overlayApi.dispatch({ kind: 'hide' });
          return;
        }
        if (overlay?.zoomTool) {
          void overlayApi.dispatch({ kind: 'zoomTool.set', tool: null });
          return;
        }
        window.close();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [hasActive, mapDrag.dragging, mapDragApi, overlay?.zoomTool, overlayApi]);

  const zoomTool = overlay?.zoomTool ?? null;

  const npcs = useMemo(() => session?.project?.npcs ?? [], [session?.project?.npcs]);
  const npcGroups = useMemo(() => session?.project?.npcGroups ?? [], [session?.project?.npcGroups]);
  const relations = useMemo(() => session?.project?.npcRelations ?? [], [session?.project?.npcRelations]);
  const selectedIds = useMemo(() => new Set(activeIds), [activeIds]);
  const selectedNpcs = useMemo(
    () => activeIds.map((id) => npcs.find((n) => n.id === id)).filter((n): n is ProjectNpc => Boolean(n)),
    [activeIds, npcs],
  );
  /** Описание: сфокусированный НПС (открытие информации / последний выбор), иначе все активные. */
  const detailNpcs = useMemo(() => {
    const focusId = overlay?.focusNpcId ?? null;
    if (focusId) {
      const focused = npcs.find((n) => n.id === focusId);
      if (focused) return [focused];
    }
    return selectedNpcs;
  }, [npcs, overlay?.focusNpcId, selectedNpcs]);

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

  const searching = query.trim().length > 0;

  const { roots, ungrouped } = useMemo(() => {
    const forest = buildNpcGroupForest(npcGroups, filteredNpcs);
    if (!searching) return forest;
    return {
      roots: pruneEmptyGroupNodes(forest.roots),
      ungrouped: forest.ungrouped,
    };
  }, [filteredNpcs, npcGroups, searching]);

  const isExpanded = useCallback(
    (id: NpcGroupId) => {
      if (searching) return true;
      return !collapsedGroups.has(id);
    },
    [collapsedGroups, searching],
  );

  const toggleExpanded = useCallback(
    (id: NpcGroupId) => {
      if (searching) return;
      setCollapsedGroups((prev) => {
        const next = new Set(prev);
        if (next.has(id)) next.delete(id);
        else next.add(id);
        return next;
      });
    },
    [searching],
  );

  const relationsByNpcId = useMemo(() => {
    const map = new Map<NpcId, { id: string; text: string }[]>();
    for (const npc of detailNpcs) {
      const list = relations
        .filter((r) => r.sourceNpcId === npc.id)
        .map((r) => {
          const other = npcs.find((n) => n.id === r.targetNpcId);
          return { id: r.id, text: `${r.label} ${other?.name ?? '—'}` };
        });
      map.set(npc.id, list);
    }
    return map;
  }, [detailNpcs, npcs, relations]);

  const onSelectTile = useCallback(
    (id: NpcId) => {
      void overlayApi.dispatch({ kind: 'toggle', npcId: id });
    },
    [overlayApi],
  );

  const sceneId = session?.currentSceneId ?? null;
  const currentScene = sceneId && session?.project ? session.project.scenes[sceneId] : undefined;
  const mapDragEnabled = Boolean(sceneId);

  const spawnNpcOnMap = useCallback(
    (npcId: NpcId) => {
      if (!sceneId) return;
      const npc = npcs.find((item) => item.id === npcId);
      if (!npc) return;
      const visible = listVisibleSceneNpcTokens(
        currentScene?.npcTokens,
        npcSession.spawned,
        sceneId,
      );
      const occupied = visible.map((tok) => {
        const override = npcSession.byPlacementId[String(tok.id)];
        return { nx: override?.nx ?? tok.nx, ny: override?.ny ?? tok.ny };
      });
      const point = nextSessionNpcSpawnPoint(occupied);
      npcSessionApi.dispatch({
        kind: 'spawn',
        npcId,
        sceneId,
        nx: point.nx,
        ny: point.ny,
        disposition: normalizeNpcDisposition(npc.disposition),
      });
    },
    [currentScene?.npcTokens, npcSession.byPlacementId, npcSession.spawned, npcSessionApi, npcs, sceneId],
  );

  const onMapDragBegin = useCallback(
    (id: NpcId) => {
      void mapDragApi.beginDrag(id);
    },
    [mapDragApi],
  );
  const onMapDragEnd = useCallback(() => {
    void mapDragApi.commit();
  }, [mapDragApi]);

  return (
    <div className={styles.page}>
      <div className={styles.toolbar}>
        <div className={matStyles.browserToolbarRow}>
          <Button
            variant={zoomTool === 'zoomIn' ? 'primary' : 'ghost'}
            iconOnly
            title={t('npcs.zoomIn')}
            ariaLabel={t('npcs.zoomIn')}
            tooltipPlacement="bottom"
            onClick={() => {
              void overlayApi.dispatch({
                kind: 'zoomTool.set',
                tool: zoomTool === 'zoomIn' ? null : 'zoomIn',
              });
            }}
          >
            <ZoomInIcon />
          </Button>
          <Button
            variant={zoomTool === 'zoomOut' ? 'primary' : 'ghost'}
            iconOnly
            title={t('npcs.zoomOut')}
            ariaLabel={t('npcs.zoomOut')}
            tooltipPlacement="bottom"
            onClick={() => {
              void overlayApi.dispatch({
                kind: 'zoomTool.set',
                tool: zoomTool === 'zoomOut' ? null : 'zoomOut',
              });
            }}
          >
            <ZoomOutIcon />
          </Button>
        </div>
        <div className={matStyles.browserToolbarHint}>
          {zoomTool === 'zoomIn'
            ? t('npcs.zoomInHint')
            : zoomTool === 'zoomOut'
              ? t('npcs.zoomOutHint')
              : t('npcs.zoomIdleHint')}
        </div>
        <div className={styles.toolbarHint}>
          {sceneId ? t('npcs.dragToMapHint') : t('npcs.addToMapNoScene')}
        </div>
        <div className={styles.toolbarRow}>
          <Button
            title={t('npcs.closeOverlay')}
            ariaLabel={t('npcs.closeOverlay')}
            tooltipPlacement="bottom"
            disabled={!hasActive}
            onClick={() => {
              void overlayApi.dispatch({ kind: 'hide' });
            }}
          >
            {t('npcs.closeOverlay')}
          </Button>
        </div>
      </div>

      <div className={styles.body}>
        <div className={styles.detail}>
          {detailNpcs.length > 0 ? (
            detailNpcs.map((npc) => {
              const safeHtml = sanitizeSceneDescriptionHtml(npc.description);
              const npcRelations = relationsByNpcId.get(npc.id) ?? [];
              return (
                <div key={npc.id} className={styles.detailCard}>
                  <div className={styles.detailName}>{npc.name}</div>
                  {safeHtml ? (
                    <div>
                      <div className={styles.detailSectionTitle}>{t('npcs.description')}</div>
                      <div className={styles.detailDesc} dangerouslySetInnerHTML={{ __html: safeHtml }} />
                    </div>
                  ) : (
                    <div className={styles.muted}>{t('npcs.descriptionEmpty')}</div>
                  )}
                  {npcRelations.length > 0 ? (
                    <div>
                      <div className={styles.detailSectionTitle}>{t('npcs.relations')}</div>
                      <div className={styles.relationsList}>
                        {npcRelations.map((r) => (
                          <div key={r.id} className={styles.relationItem}>
                            {r.text}
                          </div>
                        ))}
                      </div>
                    </div>
                  ) : null}
                </div>
              );
            })
          ) : (
            <div className={styles.detailCard}>
              <div className={styles.detailEmpty}>
                {npcs.length === 0 ? t('npcs.windowEmpty') : t('npcs.selectToShow')}
              </div>
            </div>
          )}
        </div>

        <div className={styles.listCol}>
          <Input value={query} onChange={setQuery} placeholder={t('npcs.search')} />
          <div className={styles.list}>
            {npcGroups.length === 0 ? (
              filteredNpcs.map((n) => (
                <RuntimeNpcTile
                  key={n.id}
                  npc={n}
                  selected={selectedIds.has(n.id)}
                  mapDragEnabled={mapDragEnabled}
                  onActivate={() => onSelectTile(n.id)}
                  onAddToMap={() => spawnNpcOnMap(n.id)}
                  onMapDragBegin={onMapDragBegin}
                  onMapDragEnd={onMapDragEnd}
                />
              ))
            ) : (
              <>
                {roots.map((node) => (
                  <RuntimeGroupSection
                    key={node.group.id}
                    node={node}
                    depth={0}
                    isExpanded={isExpanded}
                    onToggleExpanded={toggleExpanded}
                    selectedIds={selectedIds}
                    mapDragEnabled={mapDragEnabled}
                    onActivate={onSelectTile}
                    onAddToMap={spawnNpcOnMap}
                    onMapDragBegin={onMapDragBegin}
                    onMapDragEnd={onMapDragEnd}
                  />
                ))}
                {ungrouped.length > 0 || !searching ? (
                  <div className={styles.ungroupedSection}>
                    <div className={styles.ungroupedHeader}>{t('npcs.ungrouped')}</div>
                    <div className={styles.groupBody}>
                      {ungrouped.map((n) => (
                        <RuntimeNpcTile
                          key={n.id}
                          npc={n}
                          selected={selectedIds.has(n.id)}
                          mapDragEnabled={mapDragEnabled}
                          onActivate={() => onSelectTile(n.id)}
                          onAddToMap={() => spawnNpcOnMap(n.id)}
                          onMapDragBegin={onMapDragBegin}
                          onMapDragEnd={onMapDragEnd}
                        />
                      ))}
                    </div>
                  </div>
                ) : null}
              </>
            )}

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