import type { LicenseSnapshot } from '../license/licenseSnapshot';
import type {
  AssetId,
  EffectsEvent,
  EffectsState,
  GraphNodeId,
  MaterialId,
  MaterialLegend,
  MaterialsOverlayEvent,
  MaterialsOverlayState,
  MediaAsset,
  NpcId,
  NpcRelationId,
  NpcGroupId,
  NpcDisposition,
  NpcsOverlayEvent,
  NpcsOverlayState,
  Project,
  ProjectId,
  AppToken,
  AppPlayer,
  AppPlayerTeam,
  PlayerId,
  PlayerTeamId,
  PlayerImageOffset,
  PlayersUpsertProgressEvent,
  Scene,
  SceneDarknessEvent,
  SceneDarknessState,
  SceneGrid,
  SceneId,
  SceneNpcToken,
  NpcMapSpawnDragState,
  SceneNpcTokensSessionEvent,
  SceneNpcTokensSessionState,
  ScenePlayerTokensSessionEvent,
  ScenePlayerTokensSessionState,
  SceneToken,
  SceneTokensSessionEvent,
  SceneTokensSessionState,
  SceneTrap,
  SceneTrapsEvent,
  SceneTrapsState,
  SceneViewEvent,
  SceneViewState,
  TokenId,
  TokenPathSessionEvent,
  TokenPathSessionState,
  TokenPathTargetKind,
  VideoPlaybackEvent,
  VideoPlaybackState,
} from '../types';
import type {
  NpcImportResolution,
  SceneImportResolution,
  StorylineImportMergeReport,
  StorylineLabels,
  StorylineListItem,
  StorylineSelection,
} from '../graph/storylineExportImport';

export const ipcChannels = {
  app: {
    quit: 'app.quit',
    getVersion: 'app.getVersion',
  },
  updater: {
    check: 'updater.check',
    downloadAndRestart: 'updater.downloadAndRestart',
    progress: 'updater.progress',
  },
  project: {
    list: 'project.list',
    create: 'project.create',
    open: 'project.open',
    close: 'project.close',
    saveNow: 'project.saveNow',
    get: 'project.get',
    updateScene: 'project.updateScene',
    updateConnections: 'project.updateConnections',
    setCurrentScene: 'project.setCurrentScene',
    setCurrentGraphNode: 'project.setCurrentGraphNode',
    importMedia: 'project.importMedia',
    importCampaignAudio: 'project.importCampaignAudio',
    updateCampaignAudios: 'project.updateCampaignAudios',
    upsertMaterial: 'project.upsertMaterial',
    setMaterialRotation: 'project.setMaterialRotation',
    setMaterialLegend: 'project.setMaterialLegend',
    deleteMaterial: 'project.deleteMaterial',
    setMaterialsOrder: 'project.setMaterialsOrder',
    pickMaterialImage: 'project.pickMaterialImage',
    upsertNpc: 'project.upsertNpc',
    updateNpcFields: 'project.updateNpcFields',
    updateNpcPosition: 'project.updateNpcPosition',
    deleteNpc: 'project.deleteNpc',
    setNpcsOrder: 'project.setNpcsOrder',
    pickNpcAvatar: 'project.pickNpcAvatar',
    upsertNpcRelation: 'project.upsertNpcRelation',
    deleteNpcRelation: 'project.deleteNpcRelation',
    upsertNpcGroup: 'project.upsertNpcGroup',
    deleteNpcGroup: 'project.deleteNpcGroup',
    setNpcGroupsOrder: 'project.setNpcGroupsOrder',
    importScenePreview: 'project.importScenePreview',
    clearScenePreview: 'project.clearScenePreview',
    assetFileUrl: 'project.assetFileUrl',
    updateSceneGraphNodePosition: 'project.updateSceneGraphNodePosition',
    addSceneGraphNode: 'project.addSceneGraphNode',
    removeSceneGraphNode: 'project.removeSceneGraphNode',
    addSceneGraphEdge: 'project.addSceneGraphEdge',
    removeSceneGraphEdge: 'project.removeSceneGraphEdge',
    setSceneGraphNodeStart: 'project.setSceneGraphNodeStart',
    setSceneGraphNodeSideStoryStart: 'project.setSceneGraphNodeSideStoryStart',
    updateSideStoryLineTitle: 'project.updateSideStoryLineTitle',
    deleteScene: 'project.deleteScene',
    setSceneListOrder: 'project.setSceneListOrder',
    rename: 'project.rename',
    importZip: 'project.importZip',
    exportZip: 'project.exportZip',
    getProjectStorylines: 'project.getProjectStorylines',
    peekImportZip: 'project.peekImportZip',
    pickImportZipFile: 'project.pickImportZipFile',
    peekImportZipPath: 'project.peekImportZipPath',
    peekImportFromProject: 'project.peekImportFromProject',
    mergeImportZip: 'project.mergeImportZip',
    mergeImportFromProject: 'project.mergeImportFromProject',
    importZipFromPath: 'project.importZipFromPath',
    importFoundry: 'project.importFoundry',
    pickFoundrySource: 'project.pickFoundrySource',
    deleteProject: 'project.deleteProject',
    importZipProgress: 'project.importZipProgress',
    exportZipProgress: 'project.exportZipProgress',
    scenePreviewImportProgress: 'project.scenePreviewImportProgress',
    materialUpsertProgress: 'project.materialUpsertProgress',
    npcUpsertProgress: 'project.npcUpsertProgress',
  },
  windows: {
    openMultiWindow: 'windows.openMultiWindow',
    closeMultiWindow: 'windows.closeMultiWindow',
    togglePresentationFullscreen: 'windows.togglePresentationFullscreen',
    getMultiWindowState: 'windows.getMultiWindowState',
    multiWindowStateChanged: 'windows.multiWindowStateChanged',
    openSceneDescription: 'windows.openSceneDescription',
    closeSceneDescription: 'windows.closeSceneDescription',
    getSceneDescriptionContent: 'windows.getSceneDescriptionContent',
    sceneDescriptionContent: 'windows.sceneDescriptionContent',
    openMaterials: 'windows.openMaterials',
    closeMaterials: 'windows.closeMaterials',
    openNpcsEditor: 'windows.openNpcsEditor',
    closeNpcsEditor: 'windows.closeNpcsEditor',
    openNpcs: 'windows.openNpcs',
    closeNpcs: 'windows.closeNpcs',
    openSceneEditor: 'windows.openSceneEditor',
    closeSceneEditor: 'windows.closeSceneEditor',
    openTokenPathEditor: 'windows.openTokenPathEditor',
    closeTokenPathEditor: 'windows.closeTokenPathEditor',
    getTokenPathEditorTarget: 'windows.getTokenPathEditorTarget',
    tokenPathEditorTargetChanged: 'windows.tokenPathEditorTargetChanged',
    syncChromeTitles: 'windows.syncChromeTitles',
    getPresentationContentSize: 'windows.getPresentationContentSize',
    presentationContentSizeChanged: 'windows.presentationContentSizeChanged',
  },
  session: {
    stateChanged: 'session.stateChanged',
  },
  effects: {
    getState: 'effects.getState',
    dispatch: 'effects.dispatch',
    stateChanged: 'effects.stateChanged',
  },
  materialsOverlay: {
    getState: 'materialsOverlay.getState',
    dispatch: 'materialsOverlay.dispatch',
    stateChanged: 'materialsOverlay.stateChanged',
  },
  npcsOverlay: {
    getState: 'npcsOverlay.getState',
    dispatch: 'npcsOverlay.dispatch',
    stateChanged: 'npcsOverlay.stateChanged',
  },
  sceneDarkness: {
    getState: 'sceneDarkness.getState',
    dispatch: 'sceneDarkness.dispatch',
    stateChanged: 'sceneDarkness.stateChanged',
  },
  sceneTraps: {
    getState: 'sceneTraps.getState',
    dispatch: 'sceneTraps.dispatch',
    stateChanged: 'sceneTraps.stateChanged',
  },
  sceneView: {
    getState: 'sceneView.getState',
    dispatch: 'sceneView.dispatch',
    stateChanged: 'sceneView.stateChanged',
  },
  tokens: {
    list: 'tokens.list',
    upsert: 'tokens.upsert',
    delete: 'tokens.delete',
    pickImage: 'tokens.pickImage',
    imageUrl: 'tokens.imageUrl',
    stateChanged: 'tokens.stateChanged',
  },
  sceneTokensSession: {
    getState: 'sceneTokensSession.getState',
    dispatch: 'sceneTokensSession.dispatch',
    stateChanged: 'sceneTokensSession.stateChanged',
  },
  tokenPathSession: {
    getState: 'tokenPathSession.getState',
    dispatch: 'tokenPathSession.dispatch',
    stateChanged: 'tokenPathSession.stateChanged',
  },
  tokenGridSnap: {
    getState: 'tokenGridSnap.getState',
    setEnabled: 'tokenGridSnap.setEnabled',
    stateChanged: 'tokenGridSnap.stateChanged',
  },
  players: {
    list: 'players.list',
    upsert: 'players.upsert',
    delete: 'players.delete',
    setOrder: 'players.setOrder',
    upsertTeam: 'players.upsertTeam',
    deleteTeam: 'players.deleteTeam',
    setTeamsOrder: 'players.setTeamsOrder',
    assignTeam: 'players.assignTeam',
    pickImage: 'players.pickImage',
    imageUrl: 'players.imageUrl',
    upsertProgress: 'players.upsertProgress',
    stateChanged: 'players.stateChanged',
  },
  sceneNpcTokensSession: {
    getState: 'sceneNpcTokensSession.getState',
    dispatch: 'sceneNpcTokensSession.dispatch',
    stateChanged: 'sceneNpcTokensSession.stateChanged',
  },
  npcMapSpawn: {
    beginDrag: 'npcMapSpawn.beginDrag',
    setHover: 'npcMapSpawn.setHover',
    commit: 'npcMapSpawn.commit',
    cancel: 'npcMapSpawn.cancel',
    getState: 'npcMapSpawn.getState',
    stateChanged: 'npcMapSpawn.stateChanged',
  },
  scenePlayerTokensSession: {
    getState: 'scenePlayerTokensSession.getState',
    dispatch: 'scenePlayerTokensSession.dispatch',
    stateChanged: 'scenePlayerTokensSession.stateChanged',
  },
  video: {
    getState: 'video.getState',
    dispatch: 'video.dispatch',
    stateChanged: 'video.stateChanged',
  },
  license: {
    getStatus: 'license.getStatus',
    setToken: 'license.setToken',
    clearToken: 'license.clearToken',
    acceptEula: 'license.acceptEula',
    statusChanged: 'license.statusChanged',
  },
} as const;

export type ZipProgressEvent = {
  kind: 'import' | 'export';
  stage: 'copy' | 'unzip' | 'zip' | 'done';
  percent: number; // 0..100
  detail?: string;
};

export type ScenePreviewImportEvent = {
  sceneId: SceneId;
  assetId: AssetId;
  phase: 'queued' | 'optimizing' | 'thumbnail' | 'done' | 'error';
  project?: Project;
  message?: string;
};

export type MaterialUpsertProgressEvent = {
  percent: number;
  stage: string;
  detail?: string;
};

export type NpcUpsertProgressEvent = MaterialUpsertProgressEvent;

export type UpdaterCheckResponse =
  | { outcome: 'not_packaged' }
  | { outcome: 'no_license' }
  | { outcome: 'current'; currentVersion: string }
  | { outcome: 'available'; version: string }
  | { outcome: 'error'; message: string };

export type UpdaterDownloadResponse = { ok: true } | { ok: false; message: string };

export type UpdaterProgressPhase =
  | 'checking'
  | 'available'
  | 'not-available'
  | 'downloading'
  | 'installing'
  | 'error';

export type UpdaterProgressEvent = {
  phase: UpdaterProgressPhase;
  /** 0..100 при phase=downloading */
  percent?: number;
  version?: string;
  message?: string;
};

export type IpcEventMap = {
  [ipcChannels.session.stateChanged]: { state: SessionState };
  [ipcChannels.effects.stateChanged]: { state: EffectsState };
  [ipcChannels.materialsOverlay.stateChanged]: { state: MaterialsOverlayState };
  [ipcChannels.npcsOverlay.stateChanged]: { state: NpcsOverlayState };
  [ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState };
  [ipcChannels.sceneTraps.stateChanged]: { state: SceneTrapsState };
  [ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
  [ipcChannels.tokens.stateChanged]: { tokens: AppToken[] };
  [ipcChannels.sceneTokensSession.stateChanged]: { state: SceneTokensSessionState };
  [ipcChannels.tokenPathSession.stateChanged]: { state: TokenPathSessionState };
  [ipcChannels.windows.tokenPathEditorTargetChanged]: {
    kind: TokenPathTargetKind;
    placementId: string;
  } | null;
  [ipcChannels.tokenGridSnap.stateChanged]: { enabled: boolean };
  [ipcChannels.players.stateChanged]: { players: AppPlayer[]; teams: AppPlayerTeam[] };
  [ipcChannels.players.upsertProgress]: PlayersUpsertProgressEvent;
  [ipcChannels.sceneNpcTokensSession.stateChanged]: { state: SceneNpcTokensSessionState };
  [ipcChannels.npcMapSpawn.stateChanged]: { state: NpcMapSpawnDragState };
  [ipcChannels.scenePlayerTokensSession.stateChanged]: { state: ScenePlayerTokensSessionState };
  [ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
  [ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
  [ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
  [ipcChannels.windows.presentationContentSizeChanged]: { width: number; height: number };
  [ipcChannels.windows.sceneDescriptionContent]: { html: string };
  [ipcChannels.project.importZipProgress]: ZipProgressEvent;
  [ipcChannels.project.exportZipProgress]: ZipProgressEvent;
  [ipcChannels.project.scenePreviewImportProgress]: ScenePreviewImportEvent;
  [ipcChannels.project.materialUpsertProgress]: MaterialUpsertProgressEvent;
  [ipcChannels.project.npcUpsertProgress]: NpcUpsertProgressEvent;
  [ipcChannels.updater.progress]: UpdaterProgressEvent;
};

export type IpcInvokeMap = {
  [ipcChannels.app.quit]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.app.getVersion]: {
    req: Record<string, never>;
    res: { version: string; buildNumber: string | null; packaged: boolean };
  };
  [ipcChannels.updater.check]: {
    req: Record<string, never>;
    res: UpdaterCheckResponse;
  };
  [ipcChannels.updater.downloadAndRestart]: {
    req: { version: string };
    res: UpdaterDownloadResponse;
  };
  [ipcChannels.project.list]: {
    req: Record<string, never>;
    res: { projects: { id: ProjectId; name: string; updatedAt: string; fileName: string }[] };
  };
  [ipcChannels.project.create]: {
    req: { name: string };
    res: { project: Project };
  };
  [ipcChannels.project.open]: {
    req: { projectId: ProjectId };
    res: { project: Project };
  };
  [ipcChannels.project.close]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.project.get]: {
    req: Record<string, never>;
    res: { project: Project | null };
  };
  [ipcChannels.project.saveNow]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.project.updateScene]: {
    req: { sceneId: SceneId; patch: ScenePatch };
    res: { scene: Scene };
  };
  [ipcChannels.project.updateConnections]: {
    req: { sceneId: SceneId; connections: SceneId[] };
    res: { scene: Scene };
  };
  [ipcChannels.project.setCurrentScene]: {
    req: { sceneId: SceneId | null };
    res: { currentSceneId: SceneId | null };
  };
  [ipcChannels.project.setCurrentGraphNode]: {
    req: { graphNodeId: GraphNodeId | null };
    res: { currentGraphNodeId: GraphNodeId | null; currentSceneId: SceneId | null };
  };
  [ipcChannels.project.importMedia]: {
    req: { sceneId: SceneId; filePaths?: string[] };
    res: { project: Project; imported: MediaAsset[] };
  };
  [ipcChannels.project.importCampaignAudio]: {
    req: { filePaths?: string[] };
    res: { canceled: boolean; project: Project; imported: MediaAsset[] };
  };
  [ipcChannels.project.updateCampaignAudios]: {
    req: { audios: Project['campaignAudios'] };
    res: { project: Project };
  };
  [ipcChannels.project.upsertMaterial]: {
    req: { materialId?: MaterialId; name: string; filePath?: string };
    res: { project: Project };
  };
  [ipcChannels.project.setMaterialRotation]: {
    req: { materialId: MaterialId; rotationDeg: 0 | 90 | 180 | 270 };
    res: { project: Project };
  };
  [ipcChannels.project.setMaterialLegend]: {
    req: { materialId: MaterialId; legend: MaterialLegend | null };
    res: { project: Project };
  };
  [ipcChannels.project.deleteMaterial]: {
    req: { materialId: MaterialId };
    res: { project: Project };
  };
  [ipcChannels.project.setMaterialsOrder]: {
    req: { materialIds: MaterialId[] };
    res: { project: Project };
  };
  [ipcChannels.project.pickMaterialImage]: {
    req: Record<string, never>;
    res:
      | { canceled: true }
      | { canceled: false; filePath: string; previewDataUrl: string };
  };
  [ipcChannels.project.upsertNpc]: {
    req: {
      npcId?: NpcId;
      name: string;
      description?: string;
      filePath?: string;
      groupId?: NpcGroupId | null;
      ringColor?: string;
      disposition?: NpcDisposition;
      imageOffset?: PlayerImageOffset;
      imageScale?: number;
    };
    res: { project: Project };
  };
  [ipcChannels.project.updateNpcFields]: {
    req: {
      npcId: NpcId;
      name?: string;
      description?: string;
      groupId?: NpcGroupId | null;
      ringColor?: string;
      disposition?: NpcDisposition;
      imageOffset?: PlayerImageOffset;
      imageScale?: number;
    };
    res: { project: Project };
  };
  [ipcChannels.project.updateNpcPosition]: {
    req: { npcId: NpcId; x: number; y: number };
    res: { project: Project };
  };
  [ipcChannels.project.deleteNpc]: {
    req: { npcId: NpcId };
    res: { project: Project };
  };
  [ipcChannels.project.setNpcsOrder]: {
    req: { npcIds: NpcId[] };
    res: { project: Project };
  };
  [ipcChannels.project.pickNpcAvatar]: {
    req: Record<string, never>;
    res:
      | { canceled: true }
      | { canceled: false; filePath: string; previewDataUrl: string };
  };
  [ipcChannels.project.upsertNpcRelation]: {
    req: {
      relationId?: NpcRelationId;
      sourceNpcId: NpcId;
      targetNpcId: NpcId;
      label: string;
    };
    res: { project: Project };
  };
  [ipcChannels.project.deleteNpcRelation]: {
    req: { relationId: NpcRelationId };
    res: { project: Project };
  };
  [ipcChannels.project.upsertNpcGroup]: {
    req: {
      groupId?: NpcGroupId;
      name: string;
      color?: string;
      parentId?: NpcGroupId | null;
    };
    res: { project: Project };
  };
  [ipcChannels.project.deleteNpcGroup]: {
    req: { groupId: NpcGroupId };
    res: { project: Project };
  };
  [ipcChannels.project.setNpcGroupsOrder]: {
    req: { groupIds: NpcGroupId[] };
    res: { project: Project };
  };
  [ipcChannels.project.importScenePreview]: {
    req: { sceneId: SceneId; filePath?: string };
    res: { project: Project; assetId: AssetId | null; background: boolean };
  };
  [ipcChannels.project.clearScenePreview]: {
    req: { sceneId: SceneId };
    res: { project: Project };
  };
  [ipcChannels.project.assetFileUrl]: {
    req: { assetId: AssetId };
    res: { url: string | null };
  };
  [ipcChannels.project.updateSceneGraphNodePosition]: {
    req: { nodeId: GraphNodeId; x: number; y: number };
    res: { project: Project };
  };
  [ipcChannels.project.addSceneGraphNode]: {
    req: { sceneId: SceneId; x: number; y: number };
    res: { project: Project };
  };
  [ipcChannels.project.removeSceneGraphNode]: {
    req: { nodeId: GraphNodeId };
    res: { project: Project };
  };
  [ipcChannels.project.addSceneGraphEdge]: {
    req: { sourceGraphNodeId: GraphNodeId; targetGraphNodeId: GraphNodeId };
    res: { project: Project };
  };
  [ipcChannels.project.removeSceneGraphEdge]: {
    req: { edgeId: string };
    res: { project: Project };
  };
  [ipcChannels.project.setSceneGraphNodeStart]: {
    req: { graphNodeId: GraphNodeId | null };
    res: { project: Project };
  };
  [ipcChannels.project.setSceneGraphNodeSideStoryStart]: {
    req: { graphNodeId: GraphNodeId };
    res: { project: Project };
  };
  [ipcChannels.project.updateSideStoryLineTitle]: {
    req: { graphNodeId: GraphNodeId; title: string };
    res: { project: Project };
  };
  [ipcChannels.project.deleteScene]: {
    req: { sceneId: SceneId };
    res: { project: Project };
  };
  [ipcChannels.project.setSceneListOrder]: {
    req: { sceneListOrder: SceneId[] };
    res: { project: Project };
  };
  [ipcChannels.project.rename]: {
    req: { name: string; fileBaseName: string };
    res: { project: Project };
  };
  [ipcChannels.project.importZip]: {
    req: Record<string, never>;
    res: { canceled: true } | { canceled: false; project: Project };
  };
  [ipcChannels.project.getProjectStorylines]: {
    req: { projectId: ProjectId; labels: StorylineLabels };
    res: {
      storylines: StorylineListItem[];
      npcs: { id: string; name: string }[];
    };
  };
  [ipcChannels.project.peekImportZip]: {
    req: { labels: StorylineLabels; targetHasMainStart: boolean };
    res:
      | { canceled: true }
      | {
          canceled: false;
          filePath: string;
          projectName: string;
          storylines: StorylineListItem[];
          sourceProject: Project;
        };
  };
  [ipcChannels.project.pickImportZipFile]: {
    req: Record<string, never>;
    res: { canceled: true } | { canceled: false; filePath: string };
  };
  [ipcChannels.project.peekImportZipPath]: {
    req: { filePath: string; labels: StorylineLabels; targetHasMainStart: boolean };
    res: {
      filePath: string;
      projectName: string;
      storylines: StorylineListItem[];
      sourceProject: Project;
    };
  };
  [ipcChannels.project.peekImportFromProject]: {
    req: { sourceProjectId: ProjectId; labels: StorylineLabels; targetHasMainStart: boolean };
    res: {
      sourceProjectId: ProjectId;
      projectName: string;
      storylines: StorylineListItem[];
      sourceProject: Project;
    };
  };
  [ipcChannels.project.mergeImportZip]: {
    req: {
      filePath: string;
      storylineSelections: StorylineSelection[];
      sceneResolutions: SceneImportResolution[];
      npcResolutions?: NpcImportResolution[];
    };
    res: { project: Project; report: StorylineImportMergeReport };
  };
  [ipcChannels.project.mergeImportFromProject]: {
    req: {
      sourceProjectId: ProjectId;
      storylineSelections: StorylineSelection[];
      sceneResolutions: SceneImportResolution[];
      npcResolutions?: NpcImportResolution[];
    };
    res: { project: Project; report: StorylineImportMergeReport };
  };
  [ipcChannels.project.importZipFromPath]: {
    req: { filePath: string };
    res: { project: Project };
  };
  [ipcChannels.project.pickFoundrySource]: {
    req: { mode: 'folder' | 'archive' };
    res: { canceled: true } | { canceled: false; sourcePath: string };
  };
  [ipcChannels.project.importFoundry]: {
    req: { sourcePath: string };
    res: { project: Project };
  };
  [ipcChannels.project.exportZip]: {
    req: {
      projectId: ProjectId;
      storylineSelections: StorylineSelection[];
      npcIds: string[];
      labels: StorylineLabels;
    };
    res: { canceled: true } | { canceled: false };
  };
  [ipcChannels.project.deleteProject]: {
    req: { projectId: ProjectId };
    res: { ok: true };
  };
  [ipcChannels.windows.openMultiWindow]: {
    req: { playerIds?: string[] };
    res: { ok: true };
  };
  [ipcChannels.windows.closeMultiWindow]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.togglePresentationFullscreen]: {
    req: Record<string, never>;
    res: { ok: true; isFullScreen: boolean };
  };
  [ipcChannels.windows.getMultiWindowState]: {
    req: Record<string, never>;
    res: { open: boolean };
  };
  [ipcChannels.windows.getPresentationContentSize]: {
    req: Record<string, never>;
    res: { width: number; height: number } | { width: null; height: null };
  };
  [ipcChannels.windows.openSceneDescription]: {
    req: { html: string };
    res: { ok: true };
  };
  [ipcChannels.windows.closeSceneDescription]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.getSceneDescriptionContent]: {
    req: Record<string, never>;
    res: { html: string };
  };
  [ipcChannels.windows.openMaterials]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.closeMaterials]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.openNpcsEditor]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.closeNpcsEditor]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.openNpcs]: {
    req: { npcId?: NpcId | null };
    res: { ok: true };
  };
  [ipcChannels.windows.closeNpcs]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.openSceneEditor]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.closeSceneEditor]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.openTokenPathEditor]: {
    req: { kind: TokenPathTargetKind; placementId: string };
    res: { ok: true };
  };
  [ipcChannels.windows.closeTokenPathEditor]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.windows.getTokenPathEditorTarget]: {
    req: Record<string, never>;
    res: { kind: TokenPathTargetKind; placementId: string } | null;
  };
  [ipcChannels.windows.syncChromeTitles]: {
    req: { localeTag: string };
    res: { ok: true };
  };
  [ipcChannels.materialsOverlay.getState]: {
    req: Record<string, never>;
    res: { state: MaterialsOverlayState };
  };
  [ipcChannels.materialsOverlay.dispatch]: {
    req: { event: MaterialsOverlayEvent };
    res: { ok: true };
  };
  [ipcChannels.npcsOverlay.getState]: {
    req: Record<string, never>;
    res: { state: NpcsOverlayState };
  };
  [ipcChannels.npcsOverlay.dispatch]: {
    req: { event: NpcsOverlayEvent };
    res: { ok: true };
  };
  [ipcChannels.effects.getState]: {
    req: Record<string, never>;
    res: { state: EffectsState };
  };
  [ipcChannels.effects.dispatch]: {
    req: { event: EffectsEvent };
    res: { ok: true };
  };
  [ipcChannels.sceneDarkness.getState]: {
    req: Record<string, never>;
    res: { state: SceneDarknessState };
  };
  [ipcChannels.sceneDarkness.dispatch]: {
    req: { event: SceneDarknessEvent };
    res: { ok: true };
  };
  [ipcChannels.sceneTraps.getState]: {
    req: Record<string, never>;
    res: { state: SceneTrapsState };
  };
  [ipcChannels.sceneTraps.dispatch]: {
    req: { event: SceneTrapsEvent };
    res: { ok: true };
  };
  [ipcChannels.sceneView.getState]: {
    req: Record<string, never>;
    res: { state: SceneViewState };
  };
  [ipcChannels.sceneView.dispatch]: {
    req: { event: SceneViewEvent };
    res: { ok: true };
  };
  [ipcChannels.tokens.list]: {
    req: Record<string, never>;
    res: { tokens: AppToken[] };
  };
  [ipcChannels.tokens.upsert]: {
    req: { id?: TokenId | null; name: string; filePath?: string | null };
    res: { token: AppToken };
  };
  [ipcChannels.tokens.delete]: {
    req: { id: TokenId };
    res: { ok: true };
  };
  [ipcChannels.tokens.pickImage]: {
    req: Record<string, never>;
    res: { canceled: true } | { canceled: false; filePath: string; previewDataUrl: string };
  };
  [ipcChannels.tokens.imageUrl]: {
    req: { id: TokenId };
    res: { url: string | null };
  };
  [ipcChannels.sceneTokensSession.getState]: {
    req: Record<string, never>;
    res: { state: SceneTokensSessionState };
  };
  [ipcChannels.sceneTokensSession.dispatch]: {
    req: { event: SceneTokensSessionEvent };
    res: { ok: true };
  };
  [ipcChannels.tokenPathSession.getState]: {
    req: Record<string, never>;
    res: { state: TokenPathSessionState };
  };
  [ipcChannels.tokenPathSession.dispatch]: {
    req: { event: TokenPathSessionEvent };
    res: { ok: true };
  };
  [ipcChannels.tokenGridSnap.getState]: {
    req: Record<string, never>;
    res: { enabled: boolean };
  };
  [ipcChannels.tokenGridSnap.setEnabled]: {
    req: { enabled: boolean };
    res: { enabled: boolean };
  };
  [ipcChannels.players.list]: {
    req: Record<string, never>;
    res: { players: AppPlayer[]; teams: AppPlayerTeam[] };
  };
  [ipcChannels.players.upsert]: {
    req: {
      id?: PlayerId | null;
      name: string;
      filePath?: string | null;
      teamId?: PlayerTeamId | null;
      ringColor?: string;
      imageOffset?: PlayerImageOffset;
      imageScale?: number;
    };
    res: { player: AppPlayer };
  };
  [ipcChannels.players.delete]: {
    req: { id: PlayerId };
    res: { ok: true };
  };
  [ipcChannels.players.setOrder]: {
    req: { playerIds: PlayerId[] };
    res: { players: AppPlayer[] };
  };
  [ipcChannels.players.upsertTeam]: {
    req: { id?: PlayerTeamId | null; name: string; color?: string };
    res: { team: AppPlayerTeam };
  };
  [ipcChannels.players.deleteTeam]: {
    req: { id: PlayerTeamId };
    res: { ok: true };
  };
  [ipcChannels.players.setTeamsOrder]: {
    req: { teamIds: PlayerTeamId[] };
    res: { teams: AppPlayerTeam[] };
  };
  [ipcChannels.players.assignTeam]: {
    req: { playerId: PlayerId; teamId: PlayerTeamId | null };
    res: { player: AppPlayer | null };
  };
  [ipcChannels.players.pickImage]: {
    req: Record<string, never>;
    res: { canceled: true } | { canceled: false; filePath: string; previewDataUrl: string };
  };
  [ipcChannels.players.imageUrl]: {
    req: { id: PlayerId };
    res: { url: string | null };
  };
  [ipcChannels.sceneNpcTokensSession.getState]: {
    req: Record<string, never>;
    res: { state: SceneNpcTokensSessionState };
  };
  [ipcChannels.sceneNpcTokensSession.dispatch]: {
    req: { event: SceneNpcTokensSessionEvent };
    res: { ok: true };
  };
  [ipcChannels.npcMapSpawn.beginDrag]: {
    req: { npcId: NpcId };
    res: { ok: true };
  };
  [ipcChannels.npcMapSpawn.setHover]: {
    req: { nx: number | null; ny: number | null };
    res: { ok: true };
  };
  [ipcChannels.npcMapSpawn.commit]: {
    req: Record<string, never>;
    res: { spawned: boolean };
  };
  [ipcChannels.npcMapSpawn.cancel]: {
    req: Record<string, never>;
    res: { ok: true };
  };
  [ipcChannels.npcMapSpawn.getState]: {
    req: Record<string, never>;
    res: { state: NpcMapSpawnDragState };
  };
  [ipcChannels.scenePlayerTokensSession.getState]: {
    req: Record<string, never>;
    res: { state: ScenePlayerTokensSessionState };
  };
  [ipcChannels.scenePlayerTokensSession.dispatch]: {
    req: { event: ScenePlayerTokensSessionEvent };
    res: { ok: true };
  };
  [ipcChannels.video.getState]: {
    req: Record<string, never>;
    res: { state: VideoPlaybackState };
  };
  [ipcChannels.video.dispatch]: {
    req: { event: VideoPlaybackEvent };
    res: { ok: true };
  };
  [ipcChannels.license.getStatus]: {
    req: Record<string, never>;
    res: LicenseSnapshot;
  };
  [ipcChannels.license.setToken]: {
    req: { token: string };
    res: LicenseSnapshot;
  };
  [ipcChannels.license.clearToken]: {
    req: Record<string, never>;
    res: LicenseSnapshot;
  };
  [ipcChannels.license.acceptEula]: {
    req: { version: number };
    res: { ok: true };
  };
};

export type SessionState = {
  project: Project | null;
  currentSceneId: SceneId | null;
};

export type LegacyIpcEventMap = {
  [ipcChannels.session.stateChanged]: { state: SessionState };
  [ipcChannels.effects.stateChanged]: { state: EffectsState };
  [ipcChannels.materialsOverlay.stateChanged]: { state: MaterialsOverlayState };
  [ipcChannels.npcsOverlay.stateChanged]: { state: NpcsOverlayState };
  [ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState };
  [ipcChannels.sceneTraps.stateChanged]: { state: SceneTrapsState };
  [ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
  [ipcChannels.tokens.stateChanged]: { tokens: AppToken[] };
  [ipcChannels.sceneTokensSession.stateChanged]: { state: SceneTokensSessionState };
  [ipcChannels.tokenPathSession.stateChanged]: { state: TokenPathSessionState };
  [ipcChannels.tokenGridSnap.stateChanged]: { enabled: boolean };
  [ipcChannels.players.stateChanged]: { players: AppPlayer[]; teams: AppPlayerTeam[] };
  [ipcChannels.players.upsertProgress]: PlayersUpsertProgressEvent;
  [ipcChannels.sceneNpcTokensSession.stateChanged]: { state: SceneNpcTokensSessionState };
  [ipcChannels.npcMapSpawn.stateChanged]: { state: NpcMapSpawnDragState };
  [ipcChannels.scenePlayerTokensSession.stateChanged]: { state: ScenePlayerTokensSessionState };
  [ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
  [ipcChannels.license.statusChanged]: Record<string, never>;
};

export type ScenePatch = {
  title?: string;
  description?: string;
  previewAssetId?: AssetId | null;
  previewAssetType?: 'image' | 'video' | null;
  previewThumbAssetId?: AssetId | null;
  previewVideoAutostart?: boolean;
  previewRotationDeg?: 0 | 90 | 180 | 270;
  darkenScene?: boolean;
  traps?: SceneTrap[];
  tokens?: SceneToken[];
  npcTokens?: SceneNpcToken[];
  grid?: SceneGrid;
  settings?: Partial<Scene['settings']>;
  media?: Partial<Scene['media']>;
  layout?: Partial<Scene['layout']>;
};
