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

import {
  collectSceneIdsForSelections,
  findNpcNameConflicts,
  findSceneTitleConflicts,
  listExportedNpcsFromBundle,
  storylineSelectionKey,
  type NpcImportResolution,
  type NpcNameConflict,
  type SceneImportResolution,
  type SceneTitleConflict,
  type StorylineImportMergeReport,
  type StorylineLabels,
  type StorylineListItem,
  type StorylineSelection,
} from '../../shared/graph/storylineExportImport';
import type { Project, ProjectId, SceneId } from '../../shared/types';
import { Button, Select } from '../shared/ui/controls';

import styles from './EditorApp.module.css';
import { useEditorI18n } from './i18n/EditorI18nContext';

export type ExportNpcOption = { id: string; name: string };

type ExportProjectModalProps = {
  open: boolean;
  projects: { id: ProjectId; name: string; fileName: string }[];
  initialProjectId: ProjectId | null;
  storylineLabels: StorylineLabels;
  loadStorylines: (
    projectId: ProjectId,
  ) => Promise<{ storylines: StorylineListItem[]; npcs: ExportNpcOption[] }>;
  onClose: () => void;
  onExport: (
    projectId: ProjectId,
    selections: StorylineSelection[],
    npcIds: string[],
  ) => Promise<void>;
};

export function ExportProjectModal({
  open,
  projects,
  initialProjectId,
  storylineLabels: _storylineLabels,
  loadStorylines,
  onClose,
  onExport,
}: ExportProjectModalProps) {
  const { t } = useEditorI18n();
  const [step, setStep] = useState<'storylines' | 'npcs'>('storylines');
  const [projectId, setProjectId] = useState<ProjectId | null>(initialProjectId);
  const [storylines, setStorylines] = useState<StorylineListItem[]>([]);
  const [npcs, setNpcs] = useState<ExportNpcOption[]>([]);
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
  const [selectedNpcIds, setSelectedNpcIds] = useState<Set<string>>(new Set());
  const [loadingStorylines, setLoadingStorylines] = useState(false);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!open) return;
    setProjectId(initialProjectId);
    setSaving(false);
    setError(null);
    setSelectedKeys(new Set());
    setSelectedNpcIds(new Set());
    setStep('storylines');
    setNpcs([]);
  }, [initialProjectId, open]);

  useEffect(() => {
    if (!open || !projectId) {
      setStorylines([]);
      setNpcs([]);
      return;
    }
    let cancelled = false;
    setLoadingStorylines(true);
    void (async () => {
      try {
        const res = await loadStorylines(projectId);
        if (cancelled) return;
        const list = Array.isArray(res?.storylines) ? res.storylines : [];
        const npcList = Array.isArray(res?.npcs) ? res.npcs : [];
        setStorylines(list);
        setNpcs(npcList);
        setSelectedKeys(new Set(list.map((item) => storylineSelectionKey(item.selection))));
        setSelectedNpcIds(new Set(npcList.map((n) => n.id)));
        setStep('storylines');
      } catch (e) {
        if (!cancelled) {
          setStorylines([]);
          setNpcs([]);
          setSelectedKeys(new Set());
          setSelectedNpcIds(new Set());
          setError(e instanceof Error ? e.message : String(e));
        }
      } finally {
        if (!cancelled) setLoadingStorylines(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [loadStorylines, open, projectId]);

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

  if (!open) return null;

  const canContinueStorylines =
    projectId !== null &&
    projects.some((p) => p.id === projectId) &&
    selectedKeys.size > 0 &&
    !loadingStorylines;

  const toggleKey = (key: string, disabled?: boolean) => {
    if (disabled) return;
    setSelectedKeys((prev) => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  };

  const toggleNpc = (id: string) => {
    setSelectedNpcIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  const selectedSelections = storylines
    .filter((item) => selectedKeys.has(storylineSelectionKey(item.selection)))
    .map((item) => item.selection);

  const runExport = (npcIds: string[]) => {
    if (!projectId || !canContinueStorylines) return;
    void (async () => {
      setSaving(true);
      setError(null);
      try {
        await onExport(projectId, selectedSelections, npcIds);
        onClose();
      } catch (e) {
        setError(e instanceof Error ? e.message : String(e));
      } finally {
        setSaving(false);
      }
    })();
  };

  const goNextFromStorylines = () => {
    if (!canContinueStorylines) return;
    if (npcs.length === 0) {
      runExport([]);
      return;
    }
    setStep('npcs');
  };

  return createPortal(
    <>
      <button
        type="button"
        aria-label={t('common.close')}
        onClick={onClose}
        className={styles.modalBackdrop}
      />
      <div role="dialog" aria-modal="true" className={styles.modalDialog}>
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>
            {step === 'storylines' ? t('export.title') : t('export.npcsTitle')}
          </div>
          <button
            type="button"
            aria-label={t('common.close')}
            onClick={onClose}
            className={styles.modalClose}
          >
            ×
          </button>
        </div>

        {step === 'storylines' ? (
          <div className={styles.fieldGrid}>
            <div className={styles.fieldLabel}>{t('export.project')}</div>
            <Select
              value={projectId ?? ''}
              onChange={(next) => setProjectId((next as ProjectId) || null)}
              disabled={projects.length === 0 || saving}
              ariaLabel={t('export.project')}
              options={projects.map((p) => ({
                value: p.id,
                label: `${p.name} (${p.fileName})`,
              }))}
            />

            <div className={styles.fieldLabel}>{t('storyline.section')}</div>
            {loadingStorylines ? (
              <div className={styles.muted}>{t('storyline.loading')}</div>
            ) : storylines.length === 0 ? (
              <div className={styles.muted}>{t('storyline.empty')}</div>
            ) : (
              <div className={styles.storylineChecklist}>
                {storylines.map((item) => {
                  const key = storylineSelectionKey(item.selection);
                  const checked = selectedKeys.has(key);
                  const disabled = item.disabled === true;
                  return (
                    <label
                      key={key}
                      className={disabled ? styles.storylineCheckDisabled : styles.storylineCheck}
                    >
                      <input
                        type="checkbox"
                        checked={checked}
                        disabled={disabled || saving}
                        onChange={() => toggleKey(key, disabled)}
                      />
                      <span>{item.label}</span>
                      {disabled && item.disabledReason === 'main_exists' ? (
                        <span className={styles.muted}> — {t('storyline.mainExistsHint')}</span>
                      ) : null}
                    </label>
                  );
                })}
              </div>
            )}

            <div className={styles.muted}>{t('export.hint')}</div>
          </div>
        ) : (
          <div className={styles.fieldGrid}>
            <div className={styles.muted}>{t('export.npcsHint')}</div>
            <div className={styles.storylineChecklist}>
              {npcs.map((n) => (
                <label key={n.id} className={styles.storylineCheck}>
                  <input
                    type="checkbox"
                    checked={selectedNpcIds.has(n.id)}
                    disabled={saving}
                    onChange={() => toggleNpc(n.id)}
                  />
                  <span>{n.name}</span>
                </label>
              ))}
            </div>
            <Button
              disabled={saving || npcs.length === 0}
              onClick={() => setSelectedNpcIds(new Set(npcs.map((n) => n.id)))}
            >
              {t('export.selectAllNpcs')}
            </Button>
          </div>
        )}

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

        <div className={styles.modalFooter}>
          {step === 'npcs' ? (
            <Button onClick={() => setStep('storylines')} disabled={saving}>
              {t('export.back')}
            </Button>
          ) : (
            <Button onClick={onClose} disabled={saving} title={saving ? t('export.exporting') : undefined}>
              {t('common.cancel')}
            </Button>
          )}
          {step === 'storylines' ? (
            <Button
              variant="primary"
              disabled={!canContinueStorylines || saving}
              onClick={goNextFromStorylines}
            >
              {npcs.length > 0 ? t('export.next') : t('export.saveAs')}
            </Button>
          ) : (
            <Button
              variant="primary"
              disabled={saving}
              onClick={() => runExport([...selectedNpcIds])}
            >
              {t('export.saveAs')}
            </Button>
          )}
        </div>
      </div>
    </>,
    document.body,
  );
}

export type ImportPeekResult = {
  kind: 'file' | 'project';
  filePath?: string;
  sourceProjectId?: ProjectId;
  projectName: string;
  storylines: StorylineListItem[];
  sourceProject: Project;
};

export type ImportSourceSelection =
  | { kind: 'file'; filePath: string; fileName: string }
  | { kind: 'project'; sourceProjectId: ProjectId };

type ImportSourceModalProps = {
  open: boolean;
  /** false — только импорт из файла (полный импорт проекта). */
  canImportFromProject: boolean;
  projects: { id: ProjectId; name: string; fileName: string }[];
  currentProjectId: ProjectId | null;
  pickFile: () => Promise<{ canceled: true } | { canceled: false; filePath: string }>;
  onClose: () => void;
  onContinue: (selection: ImportSourceSelection) => Promise<void>;
};

export function ImportSourceModal({
  open,
  canImportFromProject,
  projects,
  currentProjectId,
  pickFile,
  onClose,
  onContinue,
}: ImportSourceModalProps) {
  const { t } = useEditorI18n();
  const [importKind, setImportKind] = useState<'project' | 'file'>('file');
  const [sourceProjectId, setSourceProjectId] = useState<ProjectId | null>(null);
  const [pickedFile, setPickedFile] = useState<{ path: string; name: string } | null>(null);
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const availableProjects = useMemo(
    () => projects.filter((p) => p.id !== currentProjectId),
    [currentProjectId, projects],
  );

  useEffect(() => {
    if (!open) return;
    setImportKind(canImportFromProject && availableProjects.length > 0 ? 'project' : 'file');
    setSourceProjectId(availableProjects[0]?.id ?? null);
    setPickedFile(null);
    setSubmitting(false);
    setError(null);
  }, [availableProjects, canImportFromProject, 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 canContinue =
    !submitting &&
    (importKind === 'project' ? canImportFromProject && sourceProjectId !== null : pickedFile !== null);

  return createPortal(
    <>
      <button
        type="button"
        aria-label={t('common.close')}
        onClick={onClose}
        className={styles.modalBackdrop}
      />
      <div role="dialog" aria-modal="true" className={styles.modalDialog}>
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>{t('importSource.title')}</div>
          <button
            type="button"
            aria-label={t('common.close')}
            onClick={onClose}
            className={styles.modalClose}
          >
            ×
          </button>
        </div>

        <div className={styles.fieldGrid}>
          {canImportFromProject ? (
            <div className={styles.fieldGroup}>
              <div className={styles.fieldLabel}>{t('importSource.type')}</div>
              <Select
                value={importKind}
                onChange={(next) => setImportKind(next as 'project' | 'file')}
                disabled={submitting}
                ariaLabel={t('importSource.type')}
                options={[
                  { value: 'project', label: t('importSource.fromProject') },
                  { value: 'file', label: t('importSource.fromFile') },
                ]}
              />
            </div>
          ) : (
            <div className={styles.muted}>{t('importSource.fileOnlyHint')}</div>
          )}

          {importKind === 'project' && canImportFromProject ? (
            <div className={[styles.fieldGroup, canImportFromProject ? styles.fieldGroupSpaced : ''].filter(Boolean).join(' ')}>
              <div className={styles.fieldLabel}>{t('importSource.project')}</div>
              <Select
                value={sourceProjectId ?? ''}
                onChange={(next) => setSourceProjectId((next as ProjectId) || null)}
                disabled={availableProjects.length === 0 || submitting}
                ariaLabel={t('importSource.project')}
                options={availableProjects.map((p) => ({
                  value: p.id,
                  label: `${p.name} (${p.fileName})`,
                }))}
              />
              {availableProjects.length === 0 ? (
                <div className={styles.muted}>{t('importSource.noOtherProjects')}</div>
              ) : null}
            </div>
          ) : (
            <div
              className={[styles.fieldGroup, canImportFromProject ? styles.fieldGroupSpaced : '']
                .filter(Boolean)
                .join(' ')}
            >
              <div className={styles.fieldLabel}>{t('importSource.file')}</div>
              <div className={styles.importFileRow}>
                <Button
                  disabled={submitting}
                  onClick={() => {
                    void (async () => {
                      setError(null);
                      const res = await pickFile();
                      if (res.canceled) return;
                      const name = res.filePath.split(/[/\\]/).pop() ?? res.filePath;
                      setPickedFile({ path: res.filePath, name });
                    })();
                  }}
                >
                  {t('importSource.chooseFile')}
                </Button>
                <span className={styles.muted}>
                  {pickedFile ? pickedFile.name : t('importSource.noFileSelected')}
                </span>
              </div>
            </div>
          )}
        </div>

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

        <div className={styles.modalFooter}>
          <Button onClick={onClose} disabled={submitting}>
            {t('common.cancel')}
          </Button>
          <Button
            variant="primary"
            disabled={!canContinue}
            onClick={() => {
              void (async () => {
                setSubmitting(true);
                setError(null);
                try {
                  if (importKind === 'project' && canImportFromProject && sourceProjectId) {
                    await onContinue({ kind: 'project', sourceProjectId });
                  } else if (pickedFile) {
                    await onContinue({ kind: 'file', filePath: pickedFile.path, fileName: pickedFile.name });
                  }
                } catch (e) {
                  setError(e instanceof Error ? e.message : String(e));
                } finally {
                  setSubmitting(false);
                }
              })();
            }}
          >
            {t('importStoryline.continue')}
          </Button>
        </div>
      </div>
    </>,
    document.body,
  );
}

type ImportStorylinesModalProps = {
  open: boolean;
  sourceName: string;
  storylines: StorylineListItem[];
  onClose: () => void;
  onContinue: (selections: StorylineSelection[]) => void;
};

export function ImportStorylinesModal({
  open,
  sourceName,
  storylines,
  onClose,
  onContinue,
}: ImportStorylinesModalProps) {
  const { t } = useEditorI18n();
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());

  useEffect(() => {
    if (!open) return;
    setSelectedKeys(new Set());
  }, [open, sourceName]);

  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 toggleKey = (key: string, disabled?: boolean) => {
    if (disabled) return;
    setSelectedKeys((prev) => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  };

  const selectedSelections = storylines
    .filter((item) => selectedKeys.has(storylineSelectionKey(item.selection)))
    .map((item) => item.selection);

  const canContinue = selectedKeys.size > 0;

  return createPortal(
    <>
      <button
        type="button"
        aria-label={t('common.close')}
        onClick={onClose}
        className={styles.modalBackdrop}
      />
      <div role="dialog" aria-modal="true" className={styles.modalDialog}>
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>{t('importStoryline.title')}</div>
          <button
            type="button"
            aria-label={t('common.close')}
            onClick={onClose}
            className={styles.modalClose}
          >
            ×
          </button>
        </div>

        <div className={styles.fieldGrid}>
          <div className={styles.fieldLabel}>{t('importStoryline.source')}</div>
          <div>{sourceName}</div>

          <div className={styles.fieldLabel}>{t('storyline.section')}</div>
          {storylines.length === 0 ? (
            <div className={styles.muted}>{t('storyline.empty')}</div>
          ) : (
            <div className={styles.storylineChecklist}>
              {storylines.map((item) => {
                const key = storylineSelectionKey(item.selection);
                const disabled = item.disabled === true;
                return (
                  <label
                    key={key}
                    className={disabled ? styles.storylineCheckDisabled : styles.storylineCheck}
                  >
                    <input
                      type="checkbox"
                      checked={selectedKeys.has(key)}
                      disabled={disabled}
                      onChange={() => toggleKey(key, disabled)}
                    />
                    <span>{item.label}</span>
                    {disabled && item.disabledReason === 'main_exists' ? (
                      <span className={styles.muted}> — {t('storyline.mainExistsHint')}</span>
                    ) : null}
                  </label>
                );
              })}
            </div>
          )}
        </div>

        <div className={styles.modalFooter}>
          <Button onClick={onClose}>{t('common.cancel')}</Button>
          <Button variant="primary" disabled={!canContinue} onClick={() => onContinue(selectedSelections)}>
            {t('importStoryline.continue')}
          </Button>
        </div>
      </div>
    </>,
    document.body,
  );
}

type SceneConflictModalProps = {
  open: boolean;
  conflicts: SceneTitleConflict[];
  onClose: () => void;
  onConfirm: (resolutions: SceneImportResolution[]) => void;
};

export function SceneConflictModal({ open, conflicts, onClose, onConfirm }: SceneConflictModalProps) {
  const { t } = useEditorI18n();
  const [choices, setChoices] = useState<Record<string, 'create' | SceneId>>({});

  useEffect(() => {
    if (!open) return;
    const init: Record<string, 'create' | SceneId> = {};
    for (const c of conflicts) {
      init[c.sourceSceneId] = c.matches[0]?.sceneId ?? 'create';
    }
    setChoices(init);
  }, [conflicts, open]);

  if (!open) return null;

  return createPortal(
    <>
      <button
        type="button"
        aria-label={t('common.close')}
        onClick={onClose}
        className={styles.modalBackdrop}
      />
      <div role="dialog" aria-modal="true" className={`${styles.modalDialog} ${styles.modalDialogWide}`}>
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>{t('importStoryline.conflictsTitle')}</div>
          <button
            type="button"
            aria-label={t('common.close')}
            onClick={onClose}
            className={styles.modalClose}
          >
            ×
          </button>
        </div>

        <p className={styles.muted}>{t('importStoryline.conflictsHint')}</p>

        <div className={styles.conflictList}>
          {conflicts.map((c) => (
            <div key={c.sourceSceneId} className={styles.conflictRow}>
              <div className={styles.conflictTitle}>{c.sourceTitle}</div>
              <Select
                value={choices[c.sourceSceneId] ?? 'create'}
                onChange={(v) => {
                  setChoices((prev) => ({
                    ...prev,
                    [c.sourceSceneId]: v === 'create' ? 'create' : (v as SceneId),
                  }));
                }}
                ariaLabel={c.sourceTitle}
                options={[
                  { value: 'create', label: t('importStoryline.createNewScene') },
                  ...c.matches.map((m) => ({
                    value: m.sceneId,
                    label: t('importStoryline.useExistingScene', { title: m.title }),
                  })),
                ]}
              />
            </div>
          ))}
        </div>

        <div className={styles.modalFooter}>
          <Button onClick={onClose}>{t('common.cancel')}</Button>
          <Button
            variant="primary"
            onClick={() => {
              const resolutions: SceneImportResolution[] = conflicts.map((c) => {
                const choice = choices[c.sourceSceneId] ?? 'create';
                if (choice === 'create') return { sourceSceneId: c.sourceSceneId, mode: 'create' };
                return { sourceSceneId: c.sourceSceneId, mode: 'use', targetSceneId: choice };
              });
              onConfirm(resolutions);
            }}
          >
            {t('importStoryline.import')}
          </Button>
        </div>
      </div>
    </>,
    document.body,
  );
}

type NpcConflictModalProps = {
  open: boolean;
  conflicts: NpcNameConflict[];
  onClose: () => void;
  onConfirm: (resolutions: NpcImportResolution[]) => void;
};

function defaultNpcConflictChoices(conflicts: NpcNameConflict[]): Record<string, 'create' | string> {
  const init: Record<string, 'create' | string> = {};
  for (const c of conflicts) {
    init[c.sourceNpcId] = c.matches[0]?.npcId ?? 'create';
  }
  return init;
}

export function NpcConflictModal({ open, conflicts, onClose, onConfirm }: NpcConflictModalProps) {
  if (!open) return null;
  const remountKey = conflicts.map((c) => c.sourceNpcId).join('|');
  return (
    <NpcConflictModalBody
      key={remountKey}
      conflicts={conflicts}
      onClose={onClose}
      onConfirm={onConfirm}
    />
  );
}

function NpcConflictModalBody({
  conflicts,
  onClose,
  onConfirm,
}: {
  conflicts: NpcNameConflict[];
  onClose: () => void;
  onConfirm: (resolutions: NpcImportResolution[]) => void;
}) {
  const { t } = useEditorI18n();
  const [choices, setChoices] = useState(() => defaultNpcConflictChoices(conflicts));

  return createPortal(
    <>
      <button
        type="button"
        aria-label={t('common.close')}
        onClick={onClose}
        className={styles.modalBackdrop}
      />
      <div
        role="dialog"
        aria-modal="true"
        className={[styles.modalDialog, styles.modalDialogWide].filter(Boolean).join(' ')}
      >
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>{t('importStoryline.npcConflictsTitle')}</div>
          <button
            type="button"
            aria-label={t('common.close')}
            onClick={onClose}
            className={styles.modalClose}
          >
            ×
          </button>
        </div>

        <p className={styles.muted}>{t('importStoryline.npcConflictsHint')}</p>

        <div className={styles.conflictList}>
          {conflicts.map((c) => (
            <div key={c.sourceNpcId} className={styles.conflictRow}>
              <div className={styles.conflictTitle}>{c.sourceName}</div>
              <Select
                value={choices[c.sourceNpcId] ?? 'create'}
                onChange={(v) => {
                  setChoices((prev) => ({
                    ...prev,
                    [c.sourceNpcId]: v === 'create' ? 'create' : v,
                  }));
                }}
                ariaLabel={c.sourceName}
                options={[
                  { value: 'create', label: t('importStoryline.createNewNpc') },
                  ...c.matches.map((m) => ({
                    value: m.npcId,
                    label: t('importStoryline.useExistingNpc', { name: m.name }),
                  })),
                ]}
              />
            </div>
          ))}
        </div>

        <div className={styles.modalFooter}>
          <Button onClick={onClose}>{t('common.cancel')}</Button>
          <Button
            variant="primary"
            onClick={() => {
              const resolutions: NpcImportResolution[] = conflicts.map((c) => {
                const choice = choices[c.sourceNpcId] ?? 'create';
                if (choice === 'create') return { sourceNpcId: c.sourceNpcId, mode: 'create' };
                return { sourceNpcId: c.sourceNpcId, mode: 'use', targetNpcId: choice };
              });
              onConfirm(resolutions);
            }}
          >
            {t('importStoryline.import')}
          </Button>
        </div>
      </div>
    </>,
    document.body,
  );
}

type ImportReportModalProps = {
  open: boolean;
  report: StorylineImportMergeReport | null;
  onClose: () => void;
};

export function ImportReportModal({ open, report, onClose }: ImportReportModalProps) {
  const { t } = useEditorI18n();

  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 || !report) return null;

  return createPortal(
    <>
      <button
        type="button"
        aria-label={t('common.close')}
        onClick={onClose}
        className={styles.modalBackdrop}
      />
      <div role="dialog" aria-modal="true" className={styles.modalDialog}>
        <div className={styles.modalHeader}>
          <div className={styles.modalTitle}>{t('importStoryline.reportTitle')}</div>
          <button
            type="button"
            aria-label={t('common.close')}
            onClick={onClose}
            className={styles.modalClose}
          >
            ×
          </button>
        </div>

        <ul className={styles.reportList}>
          <li>{t('importStoryline.reportLines', { count: report.storylinesImported })}</li>
          <li>{t('importStoryline.reportScenesCreated', { count: report.scenesCreated })}</li>
          <li>{t('importStoryline.reportScenesReused', { count: report.scenesReused })}</li>
          <li>{t('importStoryline.reportNpcsCreated', { count: report.npcsCreated })}</li>
          <li>{t('importStoryline.reportNpcsReused', { count: report.npcsReused })}</li>
          <li>{t('importStoryline.reportNodes', { count: report.graphNodesAdded })}</li>
          <li>{t('importStoryline.reportEdges', { count: report.edgesAdded })}</li>
          <li>{t('importStoryline.reportAssetsCopied', { count: report.assetsCopied })}</li>
          <li>{t('importStoryline.reportAssetsReused', { count: report.assetsReused })}</li>
          {report.renamedSideTitles.length > 0 ? (
            <li>{t('importStoryline.reportRenamedSides', { names: report.renamedSideTitles.join(', ') })}</li>
          ) : null}
        </ul>

        <div className={styles.modalFooter}>
          <Button variant="primary" onClick={onClose}>
            {t('common.close')}
          </Button>
        </div>
      </div>
    </>,
    document.body,
  );
}

export function buildSceneResolutionsForImport(
  targetProject: Project,
  sourceProject: Project,
  selections: StorylineSelection[],
  conflicts: SceneTitleConflict[],
  userResolutions: SceneImportResolution[],
): SceneImportResolution[] {
  const sceneIds = collectSceneIdsForSelections(sourceProject, selections);
  const conflictIds = new Set(conflicts.map((c) => c.sourceSceneId));
  const bySource = new Map(userResolutions.map((r) => [r.sourceSceneId, r]));
  const out: SceneImportResolution[] = [];
  for (const sid of sceneIds) {
    if (conflictIds.has(sid)) {
      const r = bySource.get(sid);
      if (r) out.push(r);
      continue;
    }
    out.push({ sourceSceneId: sid, mode: 'create' });
  }
  return out;
}

export function computeImportConflicts(
  targetProject: Project,
  sourceProject: Project,
  selections: StorylineSelection[],
): SceneTitleConflict[] {
  const sceneIds = collectSceneIdsForSelections(sourceProject, selections);
  return findSceneTitleConflicts(targetProject, sourceProject, sceneIds);
}

export function buildNpcResolutionsForImport(
  _targetProject: Project,
  sourceProject: Project,
  conflicts: NpcNameConflict[],
  userResolutions: NpcImportResolution[],
): NpcImportResolution[] {
  const exported = listExportedNpcsFromBundle(sourceProject);
  const conflictIds = new Set(conflicts.map((c) => c.sourceNpcId));
  const bySource = new Map(userResolutions.map((r) => [r.sourceNpcId, r]));
  const out: NpcImportResolution[] = [];
  for (const n of exported) {
    if (conflictIds.has(n.id)) {
      const r = bySource.get(n.id);
      if (r) out.push(r);
      continue;
    }
    out.push({ sourceNpcId: n.id, mode: 'create' });
  }
  return out;
}

export function computeNpcImportConflicts(
  targetProject: Project,
  sourceProject: Project,
): NpcNameConflict[] {
  const exported = listExportedNpcsFromBundle(sourceProject);
  return findNpcNameConflicts(
    targetProject,
    sourceProject,
    exported.map((n) => n.id),
  );
}

export function useStorylineLabels(): StorylineLabels {
  const { t } = useEditorI18n();
  return useMemo(
    () => ({
      main: t('storyline.main'),
      untitled: t('graph.untitled'),
    }),
    [t],
  );
}
