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

import { ipcChannels } from '../../shared/ipc/contracts';
import { EULA_CURRENT_VERSION } from '../../shared/license/eulaVersion';
import type { LicenseSnapshot } from '../../shared/license/licenseSnapshot';
import type { AssetId, MediaAsset, Project, ProjectId, SceneAudioRef, SceneId } from '../../shared/types';
import { AppLogo } from '../shared/branding/AppLogo';
import { getDndApi } from '../shared/dndApi';
import { RotatedImage } from '../shared/RotatedImage';
import { Button, Input } from '../shared/ui/controls';
import { LayoutShell } from '../shared/ui/LayoutShell';
import { useAssetUrl } from '../shared/useAssetImageUrl';

import styles from './EditorApp.module.css';
import { buildNextSceneCardById } from './graph/sceneCardById';
import { DND_SCENE_ID_MIME, SceneGraph, type SceneGraphSceneCard } from './graph/SceneGraph';
import { EulaModal, LicenseAboutModal, LicenseTokenModal } from './license/EditorLicenseModals';
import { useProjectState } from './state/projectState';

type SceneCard = {
  id: SceneId;
  title: string;
  active: boolean;
  previewAssetId: AssetId | null;
  previewAssetType: 'image' | 'video' | null;
  previewVideoAutostart: boolean;
  previewRotationDeg: 0 | 90 | 180 | 270;
};

/** Лёгкая карта сцен для графа: стабильные ссылки на объекты, пока не меняются поля карточки. */
function useStableSceneCardById(project: Project | null): Record<SceneId, SceneGraphSceneCard> {
  const recordRef = useRef<Record<SceneId, SceneGraphSceneCard>>({});
  const projectIdRef = useRef<ProjectId | null>(null);

  /* Ref cache: avoid new Record / per-scene object identities when only irrelevant Scene fields change
   * (e.g. description). react-hooks/refs disallows ref access during render; this is intentional. */
  /* eslint-disable react-hooks/refs -- stable graph input identity */
  return useMemo(() => {
    if (!project) {
      recordRef.current = {};
      projectIdRef.current = null;
      return {};
    }
    if (projectIdRef.current !== project.id) {
      recordRef.current = {};
      projectIdRef.current = project.id;
    }

    const prevRecord = recordRef.current;
    const nextMap = buildNextSceneCardById(prevRecord, project);
    recordRef.current = nextMap;
    return nextMap;
  }, [project]);
  /* eslint-enable react-hooks/refs */
}

export function EditorApp() {
  const [appVersionText, setAppVersionText] = useState<string | null>(null);
  const [query, setQuery] = useState('');
  const [fileMenuOpen, setFileMenuOpen] = useState(false);
  const [projectMenuOpen, setProjectMenuOpen] = useState(false);
  const [settingsMenuOpen, setSettingsMenuOpen] = useState(false);
  const [renameOpen, setRenameOpen] = useState(false);
  const [exportModalOpen, setExportModalOpen] = useState(false);
  const [licenseSnap, setLicenseSnap] = useState<LicenseSnapshot | null>(null);
  const [licenseKeyModalOpen, setLicenseKeyModalOpen] = useState(false);
  const [eulaModalOpen, setEulaModalOpen] = useState(false);
  const [aboutLicenseOpen, setAboutLicenseOpen] = useState(false);
  const [openKeyAfterEula, setOpenKeyAfterEula] = useState(false);
  const licenseActive = licenseSnap?.active === true;
  const [state, actions] = useProjectState(licenseActive);
  const sceneCardById = useStableSceneCardById(state.project);
  const fileMenuBtnRef = useRef<HTMLButtonElement | null>(null);
  const projectMenuBtnRef = useRef<HTMLButtonElement | null>(null);
  const settingsMenuBtnRef = useRef<HTMLButtonElement | null>(null);
  const [fileMenuPos, setFileMenuPos] = useState<{ left: number; top: number } | null>(null);
  const [projectMenuPos, setProjectMenuPos] = useState<{ left: number; top: number } | null>(null);
  const [settingsMenuPos, setSettingsMenuPos] = useState<{ left: number; top: number } | null>(null);
  const scenes = useMemo<SceneCard[]>(() => {
    const p = state.project;
    if (!p) return [];
    const createdAtSortKey = (sceneId: string): number => {
      // sceneId создаётся как `${prefix}_${rand}_${Date.now().toString(16)}`
      const last = sceneId.split('_').at(-1) ?? '';
      const n = Number.parseInt(last, 16);
      return Number.isFinite(n) ? n : 0;
    };
    return Object.values(p.scenes)
      .map((s) => ({
        id: s.id,
        title: s.title,
        active: s.id === state.selectedSceneId,
        previewAssetId: s.previewAssetId,
        previewAssetType: s.previewAssetType,
        previewVideoAutostart: s.previewVideoAutostart,
        previewRotationDeg: s.previewRotationDeg,
      }))
      .sort((a, b) => createdAtSortKey(b.id) - createdAtSortKey(a.id));
  }, [state.project, state.selectedSceneId]);

  const filtered = useMemo(
    () => scenes.filter((s) => s.title.toLowerCase().includes(query.trim().toLowerCase())),
    [query, scenes],
  );

  const sceneMediaAssets = useMemo<MediaAsset[]>(() => {
    const p = state.project;
    const sid = state.selectedSceneId;
    if (!p || !sid) return [];
    const scene = p.scenes[sid];
    if (!scene) return [];
    const ids = [...scene.media.videos, ...scene.media.audios.map((a) => a.assetId)];
    return ids.map((id) => p.assets[id]).filter((a): a is MediaAsset => Boolean(a));
  }, [state.project, state.selectedSceneId]);

  const sceneAudioRefs = useMemo<SceneAudioRef[]>(() => {
    const p = state.project;
    const sid = state.selectedSceneId;
    if (!p || !sid) return [];
    const scene = p.scenes[sid];
    if (!scene) return [];
    return scene.media.audios;
  }, [state.project, state.selectedSceneId]);

  const campaignAudioRefs = useMemo<SceneAudioRef[]>(() => {
    return state.project?.campaignAudios ?? [];
  }, [state.project]);

  const campaignAudioAssets = useMemo<MediaAsset[]>(() => {
    const p = state.project;
    if (!p) return [];
    return campaignAudioRefs.map((r) => p.assets[r.assetId]).filter((a): a is MediaAsset => Boolean(a));
  }, [campaignAudioRefs, state.project]);

  const graphStartSceneId = useMemo(() => {
    const p = state.project;
    if (!p) return null;
    const gn = p.sceneGraphNodes.find((n) => n.isStartScene);
    return gn?.sceneId ?? null;
  }, [state.project]);
  const graphStartGraphNodeId = useMemo(() => {
    const p = state.project;
    if (!p) return null;
    const gn = p.sceneGraphNodes.find((n) => n.isStartScene);
    return gn?.id ?? null;
  }, [state.project]);

  const currentProjectName = state.project?.meta.name ?? '';
  const currentFileBaseName = state.project?.meta.fileBaseName ?? '';
  const existingProjectNames = useMemo(() => state.projects.map((p) => p.name), [state.projects]);
  const existingFileBaseNames = useMemo(() => {
    return state.projects.map((p) => p.fileName.replace(/\.dnd\.zip$/iu, ''));
  }, [state.projects]);

  useEffect(() => {
    if (!fileMenuOpen) return;
    const r = fileMenuBtnRef.current?.getBoundingClientRect() ?? null;
    queueMicrotask(() => {
      if (r) {
        setFileMenuPos({ left: r.left, top: r.bottom + 10 });
      } else {
        setFileMenuPos(null);
      }
    });
    const onDown = (e: MouseEvent) => {
      const t = e.target as HTMLElement | null;
      if (!t) return;
      if (t.closest('[data-filemenu-root="1"]')) return;
      setFileMenuOpen(false);
    };
    window.addEventListener('mousedown', onDown);
    return () => window.removeEventListener('mousedown', onDown);
  }, [fileMenuOpen]);

  useEffect(() => {
    if (!projectMenuOpen) return;
    const r = projectMenuBtnRef.current?.getBoundingClientRect() ?? null;
    queueMicrotask(() => {
      if (r) {
        setProjectMenuPos({ left: r.left, top: r.bottom + 10 });
      } else {
        setProjectMenuPos(null);
      }
    });
    const onDown = (e: MouseEvent) => {
      const t = e.target as HTMLElement | null;
      if (!t) return;
      if (t.closest('[data-projectmenu-root="1"]')) return;
      setProjectMenuOpen(false);
    };
    window.addEventListener('mousedown', onDown);
    return () => window.removeEventListener('mousedown', onDown);
  }, [projectMenuOpen]);

  useEffect(() => {
    if (!settingsMenuOpen) return;
    const r = settingsMenuBtnRef.current?.getBoundingClientRect() ?? null;
    queueMicrotask(() => {
      if (r) {
        setSettingsMenuPos({ left: r.left, top: r.bottom + 10 });
      } else {
        setSettingsMenuPos(null);
      }
    });
    const onDown = (e: MouseEvent) => {
      const t = e.target as HTMLElement | null;
      if (!t) return;
      if (t.closest('[data-settingsmenu-root="1"]')) return;
      setSettingsMenuOpen(false);
    };
    window.addEventListener('mousedown', onDown);
    return () => window.removeEventListener('mousedown', onDown);
  }, [settingsMenuOpen]);

  const reloadLicense = useCallback(() => {
    void (async () => {
      try {
        const s = await getDndApi().invoke(ipcChannels.license.getStatus, {});
        setLicenseSnap(s);
      } catch {
        setLicenseSnap(null);
      }
    })();
  }, []);

  useEffect(() => {
    reloadLicense();
    const unsub = getDndApi().on(ipcChannels.license.statusChanged, () => {
      reloadLicense();
    });
    return unsub;
  }, [reloadLicense]);

  useEffect(() => {
    void (async () => {
      try {
        const r = await getDndApi().invoke(ipcChannels.app.getVersion, {});
        const label = r.buildNumber ? `v${r.version} · ${r.buildNumber}` : `v${r.version}`;
        setAppVersionText(label);
      } catch {
        setAppVersionText(null);
      }
    })();
  }, []);

  const exportModalInitialProjectId = state.project?.id ?? state.projects[0]?.id ?? null;

  const bodyOverlay =
    licenseSnap === null ? (
      <div>
        <div className={styles.licenseBlockTitle}>Проверка лицензии…</div>
        <div className={styles.muted}>Подождите.</div>
      </div>
    ) : !licenseSnap.active ? (
      <div>
        <div className={styles.licenseBlockTitle}>Требуется лицензия</div>
        <div className={styles.muted}>
          Укажите ключ в меню «Настройки» → «Указать ключ». До активации доступно только меню «Настройки».
        </div>
      </div>
    ) : undefined;

  return (
    <>
      <LayoutShell
        bodyOverlay={bodyOverlay}
        topBar={
          <div className={styles.topBarRow}>
            <button
              type="button"
              className={styles.brandButton}
              onClick={() => {
                void actions.closeProject();
              }}
              title="К списку проектов"
            >
              <AppLogo className={styles.brandLogo} size={26} />
              <div className={styles.brandTitle}>DNDGamePlayer</div>
            </button>
            <div className={styles.fileToolbar}>
              <button
                ref={settingsMenuBtnRef}
                type="button"
                data-settingsmenu-root="1"
                className={styles.fileMenuTrigger}
                onClick={() => {
                  setFileMenuOpen(false);
                  setProjectMenuOpen(false);
                  setSettingsMenuOpen((v) => !v);
                }}
              >
                Настройки
              </button>
              <button
                ref={projectMenuBtnRef}
                type="button"
                data-projectmenu-root="1"
                className={styles.fileMenuTrigger}
                disabled={!licenseActive}
                title={!licenseActive ? 'Доступно после активации лицензии' : undefined}
                onClick={() => {
                  if (!licenseActive) return;
                  setFileMenuOpen(false);
                  setSettingsMenuOpen(false);
                  setProjectMenuOpen((v) => !v);
                }}
              >
                Проект
              </button>
              {state.project ? (
                <button
                  ref={fileMenuBtnRef}
                  type="button"
                  data-filemenu-root="1"
                  className={styles.fileMenuTrigger}
                  disabled={!licenseActive}
                  title={!licenseActive ? 'Доступно после активации лицензии' : undefined}
                  onClick={() => {
                    if (!licenseActive) return;
                    setProjectMenuOpen(false);
                    setSettingsMenuOpen(false);
                    setFileMenuOpen((v) => !v);
                  }}
                >
                  Файл
                </button>
              ) : null}
            </div>
            <div className={styles.flex1} />
            {appVersionText ? (
              <div className={styles.appVersion} title="Версия приложения">
                {appVersionText}
              </div>
            ) : null}
            <div className={styles.headerActions}>
              {state.project ? (
                <Button
                  variant="primary"
                  disabled={!licenseActive || !graphStartGraphNodeId}
                  title={
                    !licenseActive
                      ? 'Доступно после активации лицензии'
                      : graphStartSceneId
                        ? undefined
                        : 'Назначьте начальную сцену на графе (ПКМ по узлу)'
                  }
                  onClick={() => {
                    if (!licenseActive || !graphStartGraphNodeId) return;
                    void (async () => {
                      await getDndApi().invoke(ipcChannels.project.setCurrentGraphNode, {
                        graphNodeId: graphStartGraphNodeId,
                      });
                      await getDndApi().invoke(ipcChannels.windows.openMultiWindow, {});
                    })();
                  }}
                >
                  Запустить
                </Button>
              ) : null}
            </div>
          </div>
        }
        left={
          <div className={styles.editorSidebar}>
            {state.project ? (
              <>
                <div className={styles.gridTools}>
                  <Input value={query} onChange={setQuery} placeholder="Поиск сцен…" />
                  <Button variant="primary" onClick={() => void actions.createScene()}>
                    + Новая сцена
                  </Button>
                </div>
                <div className={styles.spacer14} />
                <div className={styles.sidebarScroll}>
                  <div className={styles.sceneListGrid}>
                    {filtered.map((s) => (
                      <SceneListCard
                        key={s.id}
                        scene={s}
                        onSelect={() => void actions.selectScene(s.id)}
                        onDeleteScene={(id) => void actions.deleteScene(id)}
                      />
                    ))}
                  </div>
                </div>
              </>
            ) : (
              <ProjectPicker
                projects={state.projects}
                onCreate={actions.createProject}
                onOpen={actions.openProject}
                onDelete={actions.deleteProject}
              />
            )}
          </div>
        }
        center={
          <div className={styles.editorGraphHost}>
            {state.project ? (
              <SceneGraph
                sceneGraphNodes={state.project.sceneGraphNodes}
                sceneGraphEdges={state.project.sceneGraphEdges}
                sceneCardById={sceneCardById}
                currentSceneId={state.selectedSceneId}
                onCurrentSceneChange={(id) => void actions.selectScene(id)}
                onConnect={(sourceGn, targetGn) => void actions.addSceneGraphEdge(sourceGn, targetGn)}
                onDisconnect={(edgeId) => void actions.removeSceneGraphEdge(edgeId)}
                onNodePositionCommit={(nodeId, x, y) =>
                  void actions.updateSceneGraphNodePosition(nodeId, x, y)
                }
                onRemoveGraphNodes={(ids) => {
                  void Promise.all(ids.map((id) => actions.removeSceneGraphNode(id)));
                }}
                onRemoveGraphNode={(id) => void actions.removeSceneGraphNode(id)}
                onSetGraphNodeStart={(graphNodeId) => void actions.setSceneGraphNodeStart(graphNodeId)}
                onDropSceneFromList={(sceneId, x, y) => void actions.addSceneGraphNode(sceneId, x, y)}
              />
            ) : (
              <div className={styles.centerEmpty} />
            )}
          </div>
        }
        right={
          <div className={styles.editorInspector}>
            <div className={styles.inspectorScroll}>
              {state.project ? (
                <>
                  <div className={styles.inspectorTitle}>Свойства игры</div>
                  <CampaignInspector
                    audioRefs={campaignAudioRefs}
                    mediaAssets={campaignAudioAssets}
                    onAudioRefsChange={(next) => void actions.updateCampaignAudios(next)}
                    onUploadAudio={() => {
                      void (async () => {
                        try {
                          await actions.importCampaignAudio();
                        } catch (e) {
                          window.alert(e instanceof Error ? e.message : String(e));
                        }
                      })();
                    }}
                  />
                  <div className={styles.spacer18} />
                  <div className={styles.inspectorTitle}>Свойства сцены</div>
                  {state.selectedSceneId ? (
                    (() => {
                      const proj = state.project;
                      const sid = state.selectedSceneId;
                      const sc = proj.scenes[sid];
                      return (
                        <SceneInspector
                          title={sc?.title ?? ''}
                          description={sc?.description ?? ''}
                          previewAssetId={sc?.previewAssetId ?? null}
                          previewAssetType={sc?.previewAssetType ?? null}
                          previewVideoAutostart={sc?.previewVideoAutostart ?? false}
                          previewRotationDeg={sc?.previewRotationDeg ?? 0}
                          mediaAssets={sceneMediaAssets}
                          audioRefs={sceneAudioRefs}
                          onAudioRefsChange={(next) =>
                            void actions.updateScene(sid, { media: { audios: next } })
                          }
                          onPreviewVideoAutostartChange={(next) =>
                            void actions.updateScene(sid, { previewVideoAutostart: next })
                          }
                          onTitleChange={(title) => void actions.updateScene(sid, { title })}
                          onDescriptionChange={(description) =>
                            void actions.updateScene(sid, { description })
                          }
                          onImportPreview={() => void actions.importScenePreview(sid)}
                          onClearPreview={() => void actions.clearScenePreview(sid)}
                          onRotatePreview={(previewRotationDeg) =>
                            void actions.updateScene(sid, { previewRotationDeg })
                          }
                          onUploadMedia={() => void actions.importMediaToScene(sid)}
                        />
                      );
                    })()
                  ) : (
                    <div className={styles.muted}>Выберите сцену слева, чтобы редактировать её свойства.</div>
                  )}
                </>
              ) : (
                <div className={styles.muted}>Откройте проект, чтобы редактировать кампанию и сцены.</div>
              )}
            </div>
          </div>
        }
      />
      {settingsMenuOpen && settingsMenuPos
        ? createPortal(
            <div
              role="menu"
              data-settingsmenu-root="1"
              className={styles.fileMenu}
              style={{ left: settingsMenuPos.left, top: settingsMenuPos.top }}
            >
              <button
                type="button"
                role="menuitem"
                className={styles.fileMenuItem}
                onClick={() => {
                  setSettingsMenuOpen(false);
                  if ((licenseSnap?.eulaAcceptedVersion ?? null) === EULA_CURRENT_VERSION) {
                    setLicenseKeyModalOpen(true);
                  } else {
                    setOpenKeyAfterEula(true);
                    setEulaModalOpen(true);
                  }
                }}
              >
                Указать ключ
              </button>
              <button
                type="button"
                role="menuitem"
                className={styles.fileMenuItem}
                onClick={() => {
                  setSettingsMenuOpen(false);
                  setAboutLicenseOpen(true);
                }}
              >
                О лицензии
              </button>
            </div>,
            document.body,
          )
        : null}
      <LicenseTokenModal
        open={licenseKeyModalOpen}
        onClose={() => setLicenseKeyModalOpen(false)}
        onSaved={() => {
          reloadLicense();
        }}
      />
      <EulaModal
        open={eulaModalOpen}
        onClose={() => {
          setEulaModalOpen(false);
          setOpenKeyAfterEula(false);
        }}
        onAccepted={() => {
          if (openKeyAfterEula) {
            setLicenseKeyModalOpen(true);
          }
          setOpenKeyAfterEula(false);
        }}
      />
      <LicenseAboutModal
        open={aboutLicenseOpen}
        onClose={() => setAboutLicenseOpen(false)}
        snapshot={licenseSnap}
      />
      {projectMenuOpen && projectMenuPos
        ? createPortal(
            <div
              role="menu"
              data-projectmenu-root="1"
              className={styles.fileMenu}
              style={{ left: projectMenuPos.left, top: projectMenuPos.top }}
            >
              <button
                type="button"
                role="menuitem"
                className={styles.fileMenuItem}
                onClick={() => {
                  setProjectMenuOpen(false);
                  void actions.closeProject();
                }}
              >
                Начальный экран
              </button>
              <button
                type="button"
                role="menuitem"
                className={styles.fileMenuItem}
                onClick={() => {
                  setProjectMenuOpen(false);
                  void actions.importProject();
                }}
              >
                Импорт
              </button>
              <button
                type="button"
                role="menuitem"
                className={styles.fileMenuItem}
                disabled={state.projects.length === 0}
                title={state.projects.length === 0 ? 'Нет сохранённых проектов' : undefined}
                onClick={() => {
                  setProjectMenuOpen(false);
                  setExportModalOpen(true);
                }}
              >
                Экспорт
              </button>
            </div>,
            document.body,
          )
        : null}
      {fileMenuOpen && fileMenuPos && state.project
        ? createPortal(
            <div
              role="menu"
              data-filemenu-root="1"
              className={styles.fileMenu}
              style={{ left: fileMenuPos.left, top: fileMenuPos.top }}
            >
              <button
                type="button"
                role="menuitem"
                className={styles.fileMenuItem}
                onClick={() => {
                  setFileMenuOpen(false);
                  setRenameOpen(true);
                }}
              >
                Переименовать проект
              </button>
            </div>,
            document.body,
          )
        : null}
      {state.project ? (
        <RenameProjectModal
          open={renameOpen}
          projectNameInitial={currentProjectName}
          fileBaseNameInitial={currentFileBaseName}
          existingProjectNames={existingProjectNames}
          existingFileBaseNames={existingFileBaseNames}
          onClose={() => setRenameOpen(false)}
          onSave={async (name, fileBaseName) => {
            await actions.renameProject(name, fileBaseName);
          }}
        />
      ) : null}
      <ExportProjectModal
        open={exportModalOpen}
        projects={state.projects}
        initialProjectId={exportModalInitialProjectId}
        onClose={() => setExportModalOpen(false)}
        onExport={async (projectId) => {
          await actions.exportProject(projectId);
        }}
      />
    </>
  );
}

type ExportProjectModalProps = {
  open: boolean;
  projects: { id: ProjectId; name: string; fileName: string }[];
  initialProjectId: ProjectId | null;
  onClose: () => void;
  onExport: (projectId: ProjectId) => Promise<void>;
};

function ExportProjectModal({
  open,
  projects,
  initialProjectId,
  onClose,
  onExport,
}: ExportProjectModalProps) {
  const [projectId, setProjectId] = useState<ProjectId | null>(initialProjectId);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!open) return;
    setProjectId(initialProjectId);
    setSaving(false);
    setError(null);
  }, [initialProjectId, open]);

  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose, open]);

  if (!open) return null;

  const canExport = projectId !== null && projects.some((p) => p.id === projectId);

  return createPortal(
    <>
      <button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalBackdrop} />
      <div role="dialog" aria-modal="true" className={styles.modalDialog}>
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>Экспорт проекта</div>
          <button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalClose}>
            ×
          </button>
        </div>

        <div className={styles.fieldGrid}>
          <div className={styles.fieldLabel}>ПРОЕКТ</div>
          <select
            className={styles.selectInput}
            value={projectId ?? ''}
            onChange={(e) => setProjectId((e.target.value as ProjectId) || null)}
            disabled={projects.length === 0}
          >
            {projects.map((p) => (
              <option key={p.id} value={p.id}>
                {p.name} ({p.fileName})
              </option>
            ))}
          </select>
          <div className={styles.muted}>
            Далее откроется окно сохранения: укажите имя и папку для файла .dnd.zip — будет создана копия
            архива проекта.
          </div>
        </div>

        {error ? <div className={styles.fieldError}>{error}</div> : null}

        <div className={styles.modalFooter}>
          <Button onClick={onClose} disabled={saving} title={saving ? 'Экспорт…' : undefined}>
            Отмена
          </Button>
          <Button
            variant="primary"
            disabled={!canExport || saving}
            onClick={() => {
              if (!projectId || !canExport) return;
              void (async () => {
                setSaving(true);
                setError(null);
                try {
                  await onExport(projectId);
                  onClose();
                } catch (e) {
                  setError(e instanceof Error ? e.message : String(e));
                } finally {
                  setSaving(false);
                }
              })();
            }}
          >
            Сохранить как…
          </Button>
        </div>
      </div>
    </>,
    document.body,
  );
}

function isValidFileBaseName(input: string): boolean {
  const trimmed = input.trim();
  if (trimmed.length < 3) return false;
  return !/[<>:"/\\|?*]/gu.test(trimmed);
}

function normalizeName(input: string): string {
  return input.trim().toLowerCase();
}

type RenameProjectModalProps = {
  open: boolean;
  projectNameInitial: string;
  fileBaseNameInitial: string;
  existingProjectNames: string[];
  existingFileBaseNames: string[];
  onClose: () => void;
  onSave: (projectName: string, fileBaseName: string) => Promise<void>;
};

function RenameProjectModal({
  open,
  projectNameInitial,
  fileBaseNameInitial,
  existingProjectNames,
  existingFileBaseNames,
  onClose,
  onSave,
}: RenameProjectModalProps) {
  const [projectName, setProjectName] = useState(projectNameInitial);
  const [fileBaseName, setFileBaseName] = useState(fileBaseNameInitial);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!open) return;
    setProjectName(projectNameInitial);
    setFileBaseName(fileBaseNameInitial);
    setSaving(false);
    setError(null);
  }, [fileBaseNameInitial, open, projectNameInitial]);

  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose, open]);

  const trimmedProjectName = projectName.trim();
  const trimmedFileBase = fileBaseName.trim();
  const projectNameOk = trimmedProjectName.length >= 3;
  const fileNameOk = isValidFileBaseName(trimmedFileBase);

  const projectNameDup =
    normalizeName(trimmedProjectName) !== normalizeName(projectNameInitial) &&
    existingProjectNames.some((n) => normalizeName(n) === normalizeName(trimmedProjectName));
  const fileNameDup =
    normalizeName(trimmedFileBase) !== normalizeName(fileBaseNameInitial) &&
    existingFileBaseNames.some((n) => normalizeName(n) === normalizeName(trimmedFileBase));

  const canSave = projectNameOk && fileNameOk && !projectNameDup && !fileNameDup && !saving;

  if (!open) return null;

  return createPortal(
    <>
      <button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalBackdrop} />
      <div role="dialog" aria-modal="true" className={styles.modalDialog}>
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>Переименовать проект</div>
          <button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalClose}>
            ×
          </button>
        </div>

        <div className={styles.fieldGrid}>
          <div className={styles.fieldLabel}>НАЗВАНИЕ ПРОЕКТА</div>
          <Input value={projectName} onChange={setProjectName} placeholder="Название проекта…" />
          {!projectNameOk ? <div className={styles.fieldError}>Минимум 3 символа.</div> : null}
          {projectNameDup ? (
            <div className={styles.fieldError}>Проект с таким названием уже существует.</div>
          ) : null}
        </div>

        <div className={styles.fieldGrid}>
          <div className={styles.fieldLabel}>НАЗВАНИЕ ФАЙЛА ПРОЕКТА</div>
          <div className={styles.rowFlex}>
            <div className={styles.flex1}>
              <Input value={fileBaseName} onChange={setFileBaseName} placeholder="my_campaign" />
            </div>
            <div className={styles.fileSuffix}>.dnd.zip</div>
          </div>
          {!fileNameOk ? (
            <div className={styles.fieldError}>Минимум 3 символа, без символов {'<>:"/\\|?*'}</div>
          ) : null}
          {fileNameDup ? (
            <div className={styles.fieldError}>Файл проекта с таким названием уже существует.</div>
          ) : null}
        </div>

        {error ? <div className={styles.fieldError}>{error}</div> : null}

        <div className={styles.modalFooter}>
          <Button onClick={onClose} disabled={saving} title={saving ? 'Сохранение…' : undefined}>
            Отмена
          </Button>
          <Button
            variant="primary"
            disabled={!canSave}
            onClick={() => {
              if (!canSave) return;
              void (async () => {
                setSaving(true);
                setError(null);
                try {
                  await onSave(trimmedProjectName, trimmedFileBase);
                  onClose();
                } catch (e) {
                  setError(e instanceof Error ? e.message : String(e));
                } finally {
                  setSaving(false);
                }
              })();
            }}
          >
            Сохранить
          </Button>
        </div>
      </div>
    </>,
    document.body,
  );
}

type ProjectPickerProps = {
  projects: { id: ProjectId; name: string; updatedAt: string }[];
  onCreate: (name: string) => Promise<void>;
  onOpen: (id: ProjectId) => Promise<void>;
  onDelete: (id: ProjectId) => Promise<void>;
};

function ProjectPicker({ projects, onCreate, onOpen, onDelete }: ProjectPickerProps) {
  const [name, setName] = useState('Моя кампания');
  const [rowMenuFor, setRowMenuFor] = useState<ProjectId | null>(null);
  const [rowMenuPos, setRowMenuPos] = useState<{ left: number; top: number } | null>(null);

  useEffect(() => {
    if (!rowMenuFor) return;
    const onDown = (e: MouseEvent) => {
      const t = e.target as HTMLElement | null;
      if (!t) return;
      if (t.closest('[data-project-row-menu-root="1"]')) return;
      setRowMenuFor(null);
      setRowMenuPos(null);
    };
    window.addEventListener('mousedown', onDown);
    return () => window.removeEventListener('mousedown', onDown);
  }, [rowMenuFor]);

  return (
    <div className={styles.projectPicker}>
      <div className={styles.projectPickerTitle}>Проекты</div>
      <div className={styles.projectPickerForm}>
        <Input value={name} onChange={setName} placeholder="Название нового проекта…" />
        <Button variant="primary" onClick={() => void onCreate(name)}>
          Создать проект
        </Button>
      </div>
      <div className={styles.spacer6} />
      <div className={styles.sectionLabel}>СУЩЕСТВУЮЩИЕ</div>
      <div className={styles.projectListScroll}>
        <div className={styles.projectList}>
          {projects.map((p) => (
            <div key={p.id} className={styles.projectCard}>
              <div
                className={styles.projectCardBody}
                onClick={() => void onOpen(p.id)}
                role="button"
                tabIndex={0}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' || e.key === ' ') void onOpen(p.id);
                }}
              >
                <div className={styles.projectCardName}>{p.name}</div>
                <div className={styles.projectCardMeta}>{new Date(p.updatedAt).toLocaleString('ru-RU')}</div>
              </div>
              <button
                type="button"
                className={styles.projectCardMenuBtn}
                data-project-row-menu-root="1"
                aria-label="Меню проекта"
                aria-haspopup="menu"
                aria-expanded={rowMenuFor === p.id}
                onClick={(e) => {
                  e.stopPropagation();
                  const r = e.currentTarget.getBoundingClientRect();
                  const menuW = 220;
                  const left = Math.max(8, Math.min(r.right - menuW, window.innerWidth - menuW - 8));
                  setRowMenuPos({ left, top: r.bottom + 8 });
                  setRowMenuFor((cur) => (cur === p.id ? null : p.id));
                }}
              >
                ⋮
              </button>
            </div>
          ))}
          {projects.length === 0 ? <div className={styles.muted}>Пока нет проектов.</div> : null}
        </div>
      </div>
      {rowMenuFor && rowMenuPos
        ? createPortal(
            <div
              role="menu"
              data-project-row-menu-root="1"
              className={styles.fileMenu}
              style={{ left: rowMenuPos.left, top: rowMenuPos.top }}
            >
              <button
                type="button"
                role="menuitem"
                className={styles.fileMenuItem}
                onClick={() => {
                  const id = rowMenuFor;
                  const proj = projects.find((x) => x.id === id);
                  setRowMenuFor(null);
                  setRowMenuPos(null);
                  if (
                    !id ||
                    !proj ||
                    !window.confirm(
                      `Удалить проект «${proj.name}» безвозвратно? Файл и кэш будут стёрты с диска.`,
                    )
                  ) {
                    return;
                  }
                  void (async () => {
                    try {
                      await onDelete(id);
                    } catch (e) {
                      window.alert(e instanceof Error ? e.message : String(e));
                    }
                  })();
                }}
              >
                Удалить
              </button>
            </div>,
            document.body,
          )
        : null}
    </div>
  );
}

type SceneInspectorProps = {
  title: string;
  description: string;
  previewAssetId: AssetId | null;
  previewAssetType: 'image' | 'video' | null;
  previewVideoAutostart: boolean;
  previewRotationDeg: 0 | 90 | 180 | 270;
  mediaAssets: MediaAsset[];
  audioRefs: SceneAudioRef[];
  onAudioRefsChange: (next: SceneAudioRef[]) => void;
  onPreviewVideoAutostartChange: (next: boolean) => void;
  onTitleChange: (v: string) => void;
  onDescriptionChange: (v: string) => void;
  onImportPreview: () => void;
  onClearPreview: () => void;
  onRotatePreview: (deg: 0 | 90 | 180 | 270) => void;
  onUploadMedia: () => void;
};

type CampaignInspectorProps = {
  mediaAssets: MediaAsset[];
  audioRefs: SceneAudioRef[];
  onAudioRefsChange: (next: SceneAudioRef[]) => void;
  onUploadAudio: () => void;
};

function CampaignInspector({
  mediaAssets,
  audioRefs,
  onAudioRefsChange,
  onUploadAudio,
}: CampaignInspectorProps) {
  const audioById = useMemo(() => new Map(audioRefs.map((a) => [a.assetId, a])), [audioRefs]);
  return (
    <div className={styles.sceneInspector}>
      <div className={styles.labelSm}>АУДИО ИГРЫ</div>
      <div className={styles.audioDrop}>
        {mediaAssets.filter((a) => a.type === 'audio').length === 0 ? (
          <div className={[styles.muted, styles.spanSm].join(' ')}>Файлов пока нет. Добавьте аудио.</div>
        ) : (
          <div className={styles.audioList}>
            {mediaAssets
              .filter((a) => a.type === 'audio')
              .map((a) => (
                <div key={a.id} className={styles.audioRow}>
                  <span className={styles.audioName}>{a.originalName}</span>
                  <span className={styles.audioControls}>
                    <label className={styles.checkboxLabelSm}>
                      <input
                        type="checkbox"
                        checked={audioById.get(a.id)?.autoplay ?? false}
                        onChange={(e) => {
                          const next = audioRefs.map((x) =>
                            x.assetId === a.id ? { ...x, autoplay: e.target.checked } : x,
                          );
                          onAudioRefsChange(next);
                        }}
                      />
                      <span className={styles.spanXs}>Авто</span>
                    </label>
                    <label className={styles.checkboxLabelSm}>
                      <input
                        type="checkbox"
                        checked={audioById.get(a.id)?.loop ?? false}
                        onChange={(e) => {
                          const next = audioRefs.map((x) =>
                            x.assetId === a.id ? { ...x, loop: e.target.checked } : x,
                          );
                          onAudioRefsChange(next);
                        }}
                      />
                      <span className={styles.spanXs}>Цикл</span>
                    </label>
                    <button
                      type="button"
                      title="Убрать из кампании"
                      className={styles.audioRemove}
                      onClick={() => {
                        onAudioRefsChange(audioRefs.filter((x) => x.assetId !== a.id));
                      }}
                    >
                      <svg
                        className={styles.audioRemoveIcon}
                        viewBox="0 0 24 24"
                        width={16}
                        height={16}
                        aria-hidden
                      >
                        <path
                          fill="currentColor"
                          d="M9 3h6a1 1 0 0 1 1 1v1h4v2H4V5h4V4a1 1 0 0 1 1-1zm1 5h2v9h-2V8zm4 0h2v9h-2V8zM7 8h2v9H7V8zm9-3H8v1h8V5zM6 21a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8H6v13z"
                        />
                      </svg>
                    </button>
                  </span>
                </div>
              ))}
          </div>
        )}
        <Button onClick={onUploadAudio}>Загрузить</Button>
      </div>
    </div>
  );
}

function SceneInspector({
  title,
  description,
  previewAssetId,
  previewAssetType,
  previewVideoAutostart,
  previewRotationDeg,
  mediaAssets,
  audioRefs,
  onAudioRefsChange,
  onPreviewVideoAutostartChange,
  onTitleChange,
  onDescriptionChange,
  onImportPreview,
  onClearPreview,
  onRotatePreview,
  onUploadMedia,
}: SceneInspectorProps) {
  const previewUrl = useAssetUrl(previewAssetId);
  const audioById = useMemo(() => new Map(audioRefs.map((a) => [a.assetId, a])), [audioRefs]);
  return (
    <div className={styles.sceneInspector}>
      <div className={styles.labelSm}>НАЗВАНИЕ СЦЕНЫ</div>
      <Input value={title} onChange={onTitleChange} />
      <div className={styles.spacer8} />
      <div className={styles.labelSm}>ОПИСАНИЕ</div>
      <textarea
        className={styles.textarea}
        value={description}
        onChange={(e) => onDescriptionChange(e.target.value)}
      />
      <div className={styles.spacer6} />
      <div className={styles.labelSm}>ПРЕВЬЮ СЦЕНЫ</div>
      <div className={styles.hint}>Файл изображения (PNG, JPG, WebP, GIF и т.д.).</div>
      <div className={styles.previewBox}>
        {previewUrl && previewAssetType === 'image' ? (
          <RotatedImage url={previewUrl} rotationDeg={previewRotationDeg} mode="cover" />
        ) : previewUrl && previewAssetType === 'video' ? (
          <video
            src={previewUrl}
            muted
            playsInline
            autoPlay={previewVideoAutostart}
            loop
            preload="metadata"
            className={styles.videoCover}
          />
        ) : (
          <div className={styles.previewEmpty}>Превью не задано</div>
        )}
      </div>
      <div className={styles.actionsRow}>
        <Button variant="primary" onClick={onImportPreview}>
          {previewAssetId ? 'Изменить' : 'Загрузить'}
        </Button>
        {previewAssetId ? <Button onClick={onClearPreview}>Очистить</Button> : null}
        {previewAssetId && previewAssetType === 'video' ? (
          <label className={styles.checkboxLabel}>
            <input
              type="checkbox"
              checked={previewVideoAutostart}
              onChange={(e) => onPreviewVideoAutostartChange(e.target.checked)}
            />
            <span className={styles.spanSm}>Автостарт</span>
          </label>
        ) : null}
        {previewAssetId && previewAssetType === 'image' ? (
          <Button
            onClick={() => {
              const next = ((previewRotationDeg + 90) % 360) as 0 | 90 | 180 | 270;
              onRotatePreview(next);
            }}
          >
            Повернуть
          </Button>
        ) : null}
      </div>
      <div className={styles.spacer6} />
      <div className={styles.labelSm}>АУДИО СЦЕНЫ</div>
      <div className={styles.audioDrop}>
        {mediaAssets.filter((a) => a.type === 'audio').length === 0 ? (
          <div className={[styles.muted, styles.spanSm].join(' ')}>Файлов пока нет. Добавьте аудио.</div>
        ) : (
          <div className={styles.audioList}>
            {mediaAssets
              .filter((a) => a.type === 'audio')
              .map((a) => (
                <div key={a.id} className={styles.audioRow}>
                  <span className={styles.audioName}>{a.originalName}</span>
                  <span className={styles.audioControls}>
                    <label className={styles.checkboxLabelSm}>
                      <input
                        type="checkbox"
                        checked={audioById.get(a.id)?.autoplay ?? false}
                        onChange={(e) => {
                          const next = audioRefs.map((x) =>
                            x.assetId === a.id ? { ...x, autoplay: e.target.checked } : x,
                          );
                          onAudioRefsChange(next);
                        }}
                      />
                      <span className={styles.spanXs}>Авто</span>
                    </label>
                    <label className={styles.checkboxLabelSm}>
                      <input
                        type="checkbox"
                        checked={audioById.get(a.id)?.loop ?? false}
                        onChange={(e) => {
                          const next = audioRefs.map((x) =>
                            x.assetId === a.id ? { ...x, loop: e.target.checked } : x,
                          );
                          onAudioRefsChange(next);
                        }}
                      />
                      <span className={styles.spanXs}>Цикл</span>
                    </label>
                    <button
                      type="button"
                      title="Убрать из сцены"
                      className={styles.audioRemove}
                      onClick={() => {
                        onAudioRefsChange(audioRefs.filter((x) => x.assetId !== a.id));
                      }}
                    >
                      <svg
                        className={styles.audioRemoveIcon}
                        viewBox="0 0 24 24"
                        width={16}
                        height={16}
                        aria-hidden
                      >
                        <path
                          fill="currentColor"
                          d="M9 3h6a1 1 0 0 1 1 1v1h4v2H4V5h4V4a1 1 0 0 1 1-1zm1 5h2v9h-2V8zm4 0h2v9h-2V8zM7 8h2v9H7V8zm9-3H8v1h8V5zM6 21a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8H6v13z"
                        />
                      </svg>
                    </button>
                  </span>
                </div>
              ))}
          </div>
        )}
        <Button onClick={onUploadMedia}>Загрузить</Button>
      </div>
      <div className={styles.spacer6} />
      <div className={styles.labelSm}>ВЕТВЛЕНИЯ</div>
      <div className={styles.hintBlock}>
        Перетащите сцену из списка на граф. С одной карточки можно задать несколько вариантов — по одной связи
        на каждую целевую сцену. Повторно к той же сцене (включая вторую карточку той же сцены на графе)
        подключить нельзя.
      </div>
    </div>
  );
}

type SceneListCardProps = {
  scene: SceneCard;
  onSelect: () => void;
  onDeleteScene: (sceneId: SceneId) => void;
};

function SceneListCard({ scene, onSelect, onDeleteScene }: SceneListCardProps) {
  const url = useAssetUrl(scene.previewAssetId);
  const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);

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

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

  const cardClass = [styles.sceneCard, scene.active ? styles.sceneCardActive : ''].filter(Boolean).join(' ');
  return (
    <div
      draggable
      className={cardClass}
      onDragStart={(e) => {
        e.dataTransfer.setData(DND_SCENE_ID_MIME, scene.id);
        e.dataTransfer.effectAllowed = 'copy';
      }}
      onClick={onSelect}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') onSelect();
      }}
    >
      <div className={url ? styles.sceneThumb : styles.sceneThumbEmpty}>
        {url && scene.previewAssetType === 'image' ? (
          <div className={styles.sceneThumbInner}>
            <RotatedImage
              url={url}
              rotationDeg={scene.previewRotationDeg}
              mode="cover"
              loading="lazy"
              decoding="async"
            />
          </div>
        ) : url && scene.previewAssetType === 'video' ? (
          <div className={styles.sceneThumbInner}>
            <video
              src={url}
              muted
              playsInline
              preload="metadata"
              className={styles.sceneThumbVideo}
              onLoadedData={(e) => {
                const v = e.currentTarget;
                try {
                  v.currentTime = 0;
                  v.pause();
                } catch {
                  // ignore
                }
              }}
            />
          </div>
        ) : (
          <div className={styles.sceneThumbEmptyInner} aria-hidden />
        )}
      </div>
      <div className={styles.sceneCardBody}>
        <div className={styles.sceneCardHeader}>
          {scene.active ? <div className={styles.badgeCurrent}>ТЕКУЩАЯ</div> : null}
          <button
            type="button"
            aria-label="Меню сцены"
            className={styles.sceneMenuBtn}
            onMouseDown={(e) => e.stopPropagation()}
            onClick={(e) => {
              e.stopPropagation();
              setMenu({ x: e.clientX, y: e.clientY });
            }}
          >
            ⋮
          </button>
        </div>
        <div className={styles.sceneCardTitle}>{scene.title}</div>
      </div>
      {menu && menuPos
        ? createPortal(
            <>
              <button
                type="button"
                aria-label="Закрыть меню"
                className={styles.menuBackdrop}
                onClick={() => setMenu(null)}
              />
              <div
                role="menu"
                tabIndex={-1}
                className={styles.sceneCtxMenu}
                style={{ left: menuPos.x, top: menuPos.y }}
                onClick={(e) => e.stopPropagation()}
                onKeyDown={(e) => {
                  if (e.key === 'Escape') setMenu(null);
                }}
              >
                <button
                  type="button"
                  role="menuitem"
                  className={styles.sceneCtxDanger}
                  onClick={() => {
                    onDeleteScene(scene.id);
                    setMenu(null);
                  }}
                >
                  Удалить
                </button>
              </div>
            </>,
            document.body,
          )
        : null}
    </div>
  );
}
