import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import ReactFlow, {
  Background,
  ConnectionMode,
  Handle,
  MarkerType,
  Panel,
  Position,
  ReactFlowProvider,
  useEdgesState,
  useNodesState,
  useReactFlow,
  useStore,
  type Connection,
  type Edge,
  type Node,
  type NodeProps,
} from 'reactflow';
import 'reactflow/dist/style.css';

import { isSceneGraphEdgeRejected } from '../../../shared/graph/sceneGraphEdgeRules';
import {
  canSetSideStoryStart,
  isNodeInSideStoryline,
  isSideStoryEdge,
} from '../../../shared/graph/sceneGraphLineage';
import type { AssetId, GraphNodeId, SceneGraphEdge, SceneGraphNode, SceneId } from '../../../shared/types';
import { ContainedVideo } from '../../shared/ContainedVideo';
import { RotatedImage } from '../../shared/RotatedImage';
import { EllipsisText } from '../../shared/ui/EllipsisText';
import ellipsisStyles from '../../shared/ui/ellipsisText.module.css';
import { useAssetUrl } from '../../shared/useAssetImageUrl';

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

/** Поля сцены, нужные только для карточки узла графа (без описания и прочего). */
export type SceneGraphSceneAudioSummary = {
  assetId: AssetId;
  loop: boolean;
  autoplay: boolean;
};

export type SceneGraphSceneCard = {
  title: string;
  previewAssetId: AssetId | null;
  previewThumbAssetId: AssetId | null;
  previewAssetType: 'image' | 'video' | null;
  previewVideoAutostart: boolean;
  previewRotationDeg: 0 | 90 | 180 | 270;
  loopVideo: boolean;
  audios: readonly SceneGraphSceneAudioSummary[];
};

/** MIME для перетаскивания сцены из списка на граф (см. EditorApp). */
export const DND_SCENE_ID_MIME = 'application/x-dnd-scene-id';

/** Примерные размеры карточки узла — чтобы точка сброса совпадала с центром карточки. */
const SCENE_CARD_W = 220;
const SCENE_CARD_H = 248;

/** UI strings for the scene graph (passed from editor i18n). */
export type SceneGraphUiStrings = {
  badgeStart: string;
  badgeSideStory: string;
  untitled: string;
  videoBadge: string;
  audioBadge: string;
  loop: string;
  autoplay: string;
  previewAutostart: string;
  videoLoop: string;
  zoomBar: string;
  zoomIn: string;
  zoomOut: string;
  fitAll: string;
  closeMenu: string;
  startScene: string;
  unsetStartScene: string;
  sideStoryStartScene: string;
  unsetSideStoryStartScene: string;
  runFromScene: string;
  delete: string;
};

const DEFAULT_SCENE_GRAPH_UI: SceneGraphUiStrings = {
  badgeStart: 'НАЧАЛО',
  badgeSideStory: 'ПОБОЧНАЯ',
  untitled: 'Без названия',
  videoBadge: 'Видео',
  audioBadge: 'Аудио',
  loop: 'Цикл',
  autoplay: 'Автостарт',
  previewAutostart: 'Авто превью',
  videoLoop: 'Цикл видео',
  zoomBar: 'Масштаб графа',
  zoomIn: 'Увеличить',
  zoomOut: 'Уменьшить',
  fitAll: 'Показать всё',
  closeMenu: 'Закрыть меню',
  startScene: 'Начальная сцена',
  unsetStartScene: 'Снять метку «Начальная сцена»',
  sideStoryStartScene: 'Начальная сцена побочной линии',
  unsetSideStoryStartScene: 'Снять метку «Начальная сцена побочной линии»',
  runFromScene: 'Запустить с этой сцены',
  delete: 'Удалить',
};

const GraphUiContext = createContext<SceneGraphUiStrings>(DEFAULT_SCENE_GRAPH_UI);

export type SceneGraphProps = {
  sceneGraphNodes: SceneGraphNode[];
  sceneGraphEdges: SceneGraphEdge[];
  sceneCardById: Record<SceneId, SceneGraphSceneCard>;
  /** Выделенная карточка на графе (одна нода, не все копии сцены). */
  selectedGraphNodeId: GraphNodeId | null;
  graphUi?: SceneGraphUiStrings;
  onGraphNodeSelect: (graphNodeId: GraphNodeId, sceneId: SceneId) => void;
  onConnect: (sourceGraphNodeId: GraphNodeId, targetGraphNodeId: GraphNodeId) => void;
  onDisconnect: (edgeId: string) => void;
  onNodePositionCommit: (nodeId: GraphNodeId, x: number, y: number) => void;
  onRemoveGraphNodes: (nodeIds: GraphNodeId[]) => void;
  onRemoveGraphNode: (graphNodeId: GraphNodeId) => void;
  onSetGraphNodeStart: (graphNodeId: GraphNodeId | null) => void;
  onSetGraphNodeSideStoryStart: (graphNodeId: GraphNodeId) => void;
  onRunFromGraphNode?: (graphNodeId: GraphNodeId) => void;
  onDropSceneFromList: (sceneId: SceneId, x: number, y: number) => void;
};

type SceneCardData = {
  sceneId: SceneId;
  title: string;
  active: boolean;
  previewAssetId: AssetId | null;
  previewThumbAssetId: AssetId | null;
  previewAssetType: 'image' | 'video' | null;
  previewVideoAutostart: boolean;
  previewRotationDeg: 0 | 90 | 180 | 270;
  isStartScene: boolean;
  isSideStoryStart: boolean;
  isSideStoryNode: boolean;
  hasSceneAudio: boolean;
  previewIsVideo: boolean;
  hasAnyAudioLoop: boolean;
  hasAnyAudioAutoplay: boolean;
  showPreviewVideoAutostart: boolean;
  showPreviewVideoLoop: boolean;
};

function IconAudioBadge() {
  return (
    <svg className={styles.badgeGlyph} viewBox="0 0 24 24" width={14} height={14} aria-hidden>
      <path fill="currentColor" d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6zM6 15a2 2 0 1 0 4 0 2 2 0 0 0-4 0z" />
    </svg>
  );
}

function IconVideoBadge() {
  return (
    <svg className={styles.badgeGlyph} viewBox="0 0 24 24" width={14} height={14} aria-hidden>
      <path
        fill="currentColor"
        d="M4 6.5A2.5 2.5 0 0 1 6.5 4h7A2.5 2.5 0 0 1 16 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-7A2.5 2.5 0 0 1 4 17.5v-11zM19 8.2l-3 2.2v3.2l3 2.2V8.2z"
      />
    </svg>
  );
}

function IconLoopParam() {
  return (
    <svg className={styles.musicParamIcon} viewBox="0 0 24 24" width={11} height={11} aria-hidden>
      <path
        fill="currentColor"
        d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0 0 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 0 0 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"
      />
    </svg>
  );
}

function IconAutoplayParam() {
  return (
    <svg className={styles.musicParamIcon} viewBox="0 0 24 24" width={11} height={11} aria-hidden>
      <path fill="currentColor" d="M13 2 3 14h7v8l11-14h-8l2-8z" />
    </svg>
  );
}

/** Иконка для «Авто превью» (видео-превью). */
function IconVideoPreviewAutostart() {
  return (
    <svg className={styles.musicParamIcon} viewBox="0 0 24 24" width={11} height={11} aria-hidden>
      <path fill="currentColor" d="M8 5v14l11-7-11-7z" />
    </svg>
  );
}

function SceneCardNode({ data }: NodeProps<SceneCardData>) {
  const ui = useContext(GraphUiContext);
  const thumbUrl = useAssetUrl(data.previewThumbAssetId);
  const previewUrl = useAssetUrl(data.previewAssetId);
  const cardClass = [
    styles.card,
    data.active ? (data.isSideStoryNode ? styles.cardActiveSide : styles.cardActive) : '',
  ]
    .filter(Boolean)
    .join(' ');
  const handleClass = data.isSideStoryNode ? styles.handleSide : styles.handle;
  const showCornerVideo = data.previewIsVideo;
  const showCornerAudio = data.hasSceneAudio;
  return (
    <div className={styles.nodeWrap}>
      <Handle type="target" position={Position.Top} className={handleClass} />
      <div className={cardClass}>
        <div className={styles.previewShell}>
          {data.isStartScene ? <div className={styles.badgeStart}>{ui.badgeStart}</div> : null}
          {data.isSideStoryStart ? (
            <div className={styles.badgeSideStory}>{ui.badgeSideStory}</div>
          ) : null}
          {thumbUrl ? (
            <div className={styles.previewFill}>
              {data.previewRotationDeg === 0 ? (
                <img
                  src={thumbUrl}
                  alt=""
                  className={styles.imageCover}
                  draggable={false}
                  loading="lazy"
                  decoding="async"
                />
              ) : (
                <RotatedImage
                  url={thumbUrl}
                  rotationDeg={data.previewRotationDeg}
                  mode="cover"
                  loading="lazy"
                  decoding="async"
                  style={{ width: '100%', height: '100%' }}
                />
              )}
            </div>
          ) : previewUrl && data.previewAssetType === 'image' ? (
            <div className={styles.previewFill}>
              {data.previewRotationDeg === 0 ? (
                <img
                  src={previewUrl}
                  alt=""
                  className={styles.imageCover}
                  draggable={false}
                  loading="lazy"
                  decoding="async"
                />
              ) : (
                <RotatedImage
                  url={previewUrl}
                  rotationDeg={data.previewRotationDeg}
                  mode="cover"
                  loading="lazy"
                  decoding="async"
                  style={{ width: '100%', height: '100%' }}
                />
              )}
            </div>
          ) : previewUrl && data.previewAssetType === 'video' ? (
            <div className={styles.previewFill}>
              {data.previewRotationDeg === 0 ? (
                <video
                  src={previewUrl}
                  muted
                  playsInline
                  preload="metadata"
                  className={styles.videoCover}
                  onLoadedData={(e) => {
                    const v = e.currentTarget;
                    try {
                      v.currentTime = 0;
                      v.pause();
                    } catch {
                      // ignore
                    }
                  }}
                />
              ) : (
                <ContainedVideo
                  url={previewUrl}
                  rotationDeg={data.previewRotationDeg}
                  mode="cover"
                  muted
                  playsInline
                  preload="metadata"
                  style={{ width: '100%', height: '100%' }}
                  onLoadedData={(e) => {
                    const v = e.currentTarget;
                    try {
                      v.currentTime = 0;
                      v.pause();
                    } catch {
                      // ignore
                    }
                  }}
                />
              )}
            </div>
          ) : (
            <div className={styles.previewPlaceholder} aria-hidden />
          )}
          {showCornerVideo || showCornerAudio ? (
            <div className={styles.cornerBadges}>
              {showCornerVideo ? (
                <span className={styles.mediaBadge} title={ui.videoBadge}>
                  <IconVideoBadge />
                </span>
              ) : null}
              {showCornerAudio ? (
                <span className={styles.mediaBadge} title={ui.audioBadge}>
                  <IconAudioBadge />
                </span>
              ) : null}
            </div>
          ) : null}
        </div>
        <div className={styles.nodeBody}>
          <EllipsisText text={data.title || ui.untitled} className={[styles.title, ellipsisStyles.root].join(' ')} />
          {data.hasAnyAudioLoop || data.hasAnyAudioAutoplay ? (
            <div className={styles.musicParams}>
              {data.hasAnyAudioLoop ? (
                <div className={styles.musicParam}>
                  <IconLoopParam />
                  <span>{ui.loop}</span>
                </div>
              ) : null}
              {data.hasAnyAudioAutoplay ? (
                <div className={styles.musicParam}>
                  <IconAutoplayParam />
                  <span>{ui.autoplay}</span>
                </div>
              ) : null}
            </div>
          ) : null}
          {data.showPreviewVideoAutostart || data.showPreviewVideoLoop ? (
            <div className={styles.musicParams}>
              {data.showPreviewVideoAutostart ? (
                <div className={styles.musicParam}>
                  <IconVideoPreviewAutostart />
                  <span>{ui.previewAutostart}</span>
                </div>
              ) : null}
              {data.showPreviewVideoLoop ? (
                <div className={styles.musicParam}>
                  <IconLoopParam />
                  <span>{ui.videoLoop}</span>
                </div>
              ) : null}
            </div>
          ) : null}
        </div>
      </div>
      <Handle type="source" position={Position.Bottom} className={handleClass} />
    </div>
  );
}

const nodeTypes = { sceneCard: SceneCardNode };

function GraphZoomToolbar() {
  const ui = useContext(GraphUiContext);
  const { zoomIn, zoomOut, fitView } = useReactFlow();
  const zoom = useStore((s) => s.transform[2]);
  const pct = Math.max(1, Math.round(zoom * 100));

  return (
    <Panel position="bottom-center" className={styles.zoomPanel}>
      <div className={styles.zoomBar} role="toolbar" aria-label={ui.zoomBar}>
        <button type="button" className={styles.zoomBtn} onClick={() => zoomIn()} aria-label={ui.zoomIn}>
          +
        </button>
        <span className={styles.zoomPct}>{pct}%</span>
        <button type="button" className={styles.zoomBtn} onClick={() => zoomOut()} aria-label={ui.zoomOut}>
          −
        </button>
        <span className={styles.zoomDivider} aria-hidden />
        <button
          type="button"
          className={styles.zoomBtn}
          onClick={() => fitView({ padding: 0.25 })}
          aria-label={ui.fitAll}
          title={ui.fitAll}
        >
          <svg className={styles.zoomFitIcon} viewBox="0 0 24 24" width={18} height={18} aria-hidden>
            <path
              fill="none"
              stroke="currentColor"
              strokeWidth={2}
              strokeLinecap="round"
              d="M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5"
            />
          </svg>
        </button>
      </div>
    </Panel>
  );
}

function SceneGraphCanvas({
  sceneGraphNodes,
  sceneGraphEdges,
  sceneCardById,
  selectedGraphNodeId,
  graphUi,
  onGraphNodeSelect,
  onConnect,
  onDisconnect,
  onNodePositionCommit,
  onRemoveGraphNodes,
  onRemoveGraphNode,
  onSetGraphNodeStart,
  onSetGraphNodeSideStoryStart,
  onRunFromGraphNode,
  onDropSceneFromList,
}: SceneGraphProps) {
  const ui = graphUi ?? DEFAULT_SCENE_GRAPH_UI;
  const { screenToFlowPosition } = useReactFlow();
  const [menu, setMenu] = useState<{ x: number; y: number; graphNodeId: GraphNodeId } | null>(null);
  const [edgeMenu, setEdgeMenu] = useState<{ x: number; y: number; edgeId: string } | null>(null);

  useEffect(() => {
    if (!menu && !edgeMenu) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        setMenu(null);
        setEdgeMenu(null);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [edgeMenu, menu]);

  const menuNodeIsStart = useMemo(() => {
    if (!menu) return false;
    return sceneGraphNodes.some((n) => n.id === menu.graphNodeId && n.isStartScene);
  }, [menu, sceneGraphNodes]);

  const menuNodeIsSideStoryStart = useMemo(() => {
    if (!menu) return false;
    return sceneGraphNodes.some((n) => n.id === menu.graphNodeId && n.isSideStoryStart);
  }, [menu, sceneGraphNodes]);

  const menuCanSetSideStoryStart = useMemo(() => {
    if (!menu) return false;
    return canSetSideStoryStart(sceneGraphNodes, sceneGraphEdges, menu.graphNodeId);
  }, [menu, sceneGraphEdges, sceneGraphNodes]);

  const menuNodeIsSideBranch = useMemo(() => {
    if (!menu) return false;
    return isNodeInSideStoryline(sceneGraphNodes, sceneGraphEdges, menu.graphNodeId);
  }, [menu, sceneGraphEdges, sceneGraphNodes]);

  const sideStoryEdgeStroke = 'rgba(0,120,212,0.95)';
  const sideStoryEdgeStrokeDim = 'rgba(0,120,212,0.55)';
  const mainEdgeStroke = 'rgba(167,139,250,0.95)';
  const mainEdgeStrokeDim = 'rgba(167,139,250,0.55)';

  const desiredNodes = useMemo<Node<SceneCardData>[]>(() => {
    return sceneGraphNodes.map((gn) => {
      const c = sceneCardById[gn.sceneId];
      const active = selectedGraphNodeId === gn.id;
      const isSideStoryNode = isNodeInSideStoryline(sceneGraphNodes, sceneGraphEdges, gn.id);
      const audios = c?.audios ?? [];
      return {
        id: gn.id,
        type: 'sceneCard',
        position: { x: gn.x, y: gn.y },
        data: {
          sceneId: gn.sceneId,
          title: c?.title ?? '',
          active,
          previewAssetId: c?.previewAssetId ?? null,
          previewThumbAssetId: c?.previewThumbAssetId ?? null,
          previewAssetType: c?.previewAssetType ?? null,
          previewVideoAutostart: c?.previewVideoAutostart ?? false,
          previewRotationDeg: c?.previewRotationDeg ?? 0,
          isStartScene: gn.isStartScene,
          isSideStoryStart: gn.isSideStoryStart,
          isSideStoryNode,
          hasSceneAudio: audios.length >= 1,
          previewIsVideo: c?.previewAssetType === 'video',
          hasAnyAudioLoop: audios.some((a) => a.loop),
          hasAnyAudioAutoplay: audios.some((a) => a.autoplay),
          showPreviewVideoAutostart: c?.previewAssetType === 'video' ? c.previewVideoAutostart : false,
          showPreviewVideoLoop: c?.previewAssetType === 'video' ? c.loopVideo : false,
        },
        style: { padding: 0, background: 'transparent', border: 'none' },
      };
    });
  }, [sceneCardById, sceneGraphEdges, sceneGraphNodes, selectedGraphNodeId]);

  const desiredEdges = useMemo<Edge[]>(() => {
    const hasSelection = selectedGraphNodeId != null;
    return sceneGraphEdges.map((e) => {
      const isSide = isSideStoryEdge(sceneGraphNodes, sceneGraphEdges, e);
      const strokeActive = isSide ? sideStoryEdgeStroke : mainEdgeStroke;
      const strokeIdle = isSide ? sideStoryEdgeStrokeDim : mainEdgeStrokeDim;
      const strokeDim = 'rgba(255,255,255,0.10)';
      const markerDim = 'rgba(255,255,255,0.18)';
      const touchesSelection =
        selectedGraphNodeId != null &&
        (e.sourceGraphNodeId === selectedGraphNodeId || e.targetGraphNodeId === selectedGraphNodeId);
      return {
        ...(hasSelection
          ? {
              style: touchesSelection
                ? { stroke: strokeActive, strokeWidth: 3 }
                : { stroke: strokeDim, strokeWidth: 2 },
              markerEnd: touchesSelection
                ? { type: MarkerType.ArrowClosed, color: strokeActive, strokeWidth: 2 }
                : { type: MarkerType.ArrowClosed, color: markerDim, strokeWidth: 2 },
            }
          : {
              style: { stroke: strokeIdle, strokeWidth: 2 },
              markerEnd: {
                type: MarkerType.ArrowClosed,
                color: isSide ? 'rgba(0,120,212,0.85)' : 'rgba(167,139,250,0.85)',
                strokeWidth: 2,
              },
            }),
        id: e.id,
        source: e.sourceGraphNodeId,
        target: e.targetGraphNodeId,
        type: 'smoothstep',
        animated: false,
        selectable: false,
      };
    });
  }, [sceneGraphEdges, sceneGraphNodes, selectedGraphNodeId]);

  const [nodes, setNodes, onNodesChange] = useNodesState<Node<SceneCardData>>([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);

  useEffect(() => {
    setNodes(desiredNodes as unknown as Parameters<typeof setNodes>[0]);
    setEdges(desiredEdges);
  }, [desiredEdges, desiredNodes, setEdges, setNodes]);

  const isValidConnection = useCallback(
    (conn: Connection) => {
      const source = conn.source as GraphNodeId | null;
      const target = conn.target as GraphNodeId | null;
      if (!source || !target) return false;
      return !isSceneGraphEdgeRejected(sceneGraphNodes, sceneGraphEdges, source, target);
    },
    [sceneGraphEdges, sceneGraphNodes],
  );

  const onConnectInternal = (conn: Connection) => {
    const source = conn.source as GraphNodeId | null;
    const target = conn.target as GraphNodeId | null;
    if (!source || !target) return;
    if (!isValidConnection(conn)) return;
    onConnect(source, target);
  };

  const onDragOver = (e: React.DragEvent) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy';
  };

  const onDrop = (e: React.DragEvent) => {
    e.preventDefault();
    const id = e.dataTransfer.getData(DND_SCENE_ID_MIME);
    if (!id) return;
    const p = screenToFlowPosition({ x: e.clientX, y: e.clientY });
    onDropSceneFromList(id as SceneId, p.x - SCENE_CARD_W / 2, p.y - SCENE_CARD_H / 2);
  };

  const menuPosition = useMemo(() => {
    if (!menu) return null;
    const pad = 8;
    const mw = 220;
    const mh = 210;
    const x = Math.max(pad, Math.min(menu.x, window.innerWidth - mw - pad));
    const y = Math.max(pad, Math.min(menu.y, window.innerHeight - mh - pad));
    return { x, y };
  }, [menu]);

  const edgeMenuPosition = useMemo(() => {
    if (!edgeMenu) return null;
    const pad = 8;
    const mw = 200;
    const mh = 48;
    const x = Math.max(pad, Math.min(edgeMenu.x, window.innerWidth - mw - pad));
    const y = Math.max(pad, Math.min(edgeMenu.y, window.innerHeight - mh - pad));
    return { x, y };
  }, [edgeMenu]);

  return (
    <GraphUiContext.Provider value={ui}>
      <div className={styles.canvasWrap}>
        <ReactFlow
          nodes={nodes}
          edges={edges}
          nodeTypes={nodeTypes}
          onNodesChange={onNodesChange}
          onNodeDragStop={(_, node) => {
            onNodePositionCommit(node.id as GraphNodeId, node.position.x, node.position.y);
          }}
          onEdgesChange={onEdgesChange}
          isValidConnection={isValidConnection}
          connectionMode={ConnectionMode.Loose}
          onConnect={onConnectInternal}
          onEdgeContextMenu={(e, edge) => {
            e.preventDefault();
            setMenu(null);
            setEdgeMenu({ x: e.clientX, y: e.clientY, edgeId: edge.id });
          }}
          onNodesDelete={(nds) => {
            onRemoveGraphNodes(nds.map((n) => n.id as GraphNodeId));
          }}
          onNodeClick={(_, node) => {
            setMenu(null);
            setEdgeMenu(null);
            const d = node.data as SceneCardData;
            onGraphNodeSelect(node.id as GraphNodeId, d.sceneId);
          }}
          onNodeContextMenu={(e, node) => {
            e.preventDefault();
            setEdgeMenu(null);
            setMenu({ x: e.clientX, y: e.clientY, graphNodeId: node.id as GraphNodeId });
          }}
          onPaneClick={() => {
            setMenu(null);
            setEdgeMenu(null);
          }}
          onPaneContextMenu={(e) => {
            e.preventDefault();
            setMenu(null);
            setEdgeMenu(null);
          }}
          onInit={(instance) => {
            instance.fitView({ padding: 0.25 });
          }}
          onDragOver={onDragOver}
          onDrop={onDrop}
          panOnScroll
          selectionOnDrag={false}
          minZoom={0.1}
          deleteKeyCode={['Backspace', 'Delete']}
          proOptions={{ hideAttribution: true }}
        >
          <Background gap={18} size={1} color="rgba(255,255,255,0.06)" />
          <GraphZoomToolbar />
        </ReactFlow>
        {menu && menuPosition
          ? createPortal(
              <>
                <button
                  type="button"
                  aria-label={ui.closeMenu}
                  className={styles.menuBackdrop}
                  onClick={() => setMenu(null)}
                />
                <div
                  role="menu"
                  tabIndex={-1}
                  className={styles.ctxMenu}
                  style={{ left: menuPosition.x, top: menuPosition.y }}
                  onClick={(e) => e.stopPropagation()}
                  onKeyDown={(e) => {
                    if (e.key === 'Escape') setMenu(null);
                  }}
                >
                  <button
                    type="button"
                    role="menuitem"
                    className={styles.ctxItem}
                    onClick={() => {
                      if (menuNodeIsStart) {
                        onSetGraphNodeStart(null);
                      } else {
                        onSetGraphNodeStart(menu.graphNodeId);
                      }
                      setMenu(null);
                    }}
                  >
                    {menuNodeIsStart ? ui.unsetStartScene : ui.startScene}
                  </button>
                  {menuNodeIsSideStoryStart || menuCanSetSideStoryStart ? (
                    <button
                      type="button"
                      role="menuitem"
                      className={styles.ctxItem}
                      onClick={() => {
                        onSetGraphNodeSideStoryStart(menu.graphNodeId);
                        setMenu(null);
                      }}
                    >
                      {menuNodeIsSideStoryStart ? ui.unsetSideStoryStartScene : ui.sideStoryStartScene}
                    </button>
                  ) : null}
                  {!menuNodeIsSideBranch ? (
                    <button
                      type="button"
                      role="menuitem"
                      className={styles.ctxItem}
                      disabled={!onRunFromGraphNode}
                      onClick={() => {
                        onRunFromGraphNode?.(menu.graphNodeId);
                        setMenu(null);
                      }}
                    >
                      {ui.runFromScene}
                    </button>
                  ) : null}
                  <button
                    type="button"
                    role="menuitem"
                    className={styles.ctxItemDanger}
                    onClick={() => {
                      onRemoveGraphNode(menu.graphNodeId);
                      setMenu(null);
                    }}
                  >
                    {ui.delete}
                  </button>
                </div>
              </>,
              document.body,
            )
          : null}
        {edgeMenu && edgeMenuPosition
          ? createPortal(
              <>
                <button
                  type="button"
                  aria-label={ui.closeMenu}
                  className={styles.menuBackdrop}
                  onClick={() => setEdgeMenu(null)}
                />
                <div
                  role="menu"
                  tabIndex={-1}
                  className={styles.ctxMenu}
                  style={{ left: edgeMenuPosition.x, top: edgeMenuPosition.y }}
                  onClick={(e) => e.stopPropagation()}
                  onKeyDown={(e) => {
                    if (e.key === 'Escape') setEdgeMenu(null);
                  }}
                >
                  <button
                    type="button"
                    role="menuitem"
                    className={styles.ctxItemDanger}
                    onClick={() => {
                      onDisconnect(edgeMenu.edgeId);
                      setEdgeMenu(null);
                    }}
                  >
                    {ui.delete}
                  </button>
                </div>
              </>,
              document.body,
            )
          : null}
      </div>
    </GraphUiContext.Provider>
  );
}

export function SceneGraph(props: SceneGraphProps) {
  return (
    <ReactFlowProvider>
      <SceneGraphCanvas {...props} />
    </ReactFlowProvider>
  );
}
