import { app, BrowserWindow, ipcMain, dialog, shell, clipboard } from 'electron';
import * as path from 'path';
import * as fsPromises from 'fs/promises';
import { eq } from 'drizzle-orm';
import { getPostEngine, PostData, PostFilter, PaginationOptions } from '../engine/PostEngine';
import { getMediaEngine, MediaData } from '../engine/MediaEngine';
import { getProjectEngine, ProjectData } from '../engine/ProjectEngine';
import { getMetaEngine } from '../engine/MetaEngine';
import { getMenuEngine, type MenuDocument } from '../engine/MenuEngine';
import { getTagEngine } from '../engine/TagEngine';
import { getPostMediaEngine } from '../engine/PostMediaEngine';
import { getScriptEngine, type CreateScriptInput, type UpdateScriptInput } from '../engine/ScriptEngine';
import { getGitEngine } from '../engine/GitEngine';
import { getGitApiAdapter } from '../engine/GitApiAdapter';
import { taskManager, TaskProgress } from '../engine/TaskManager';
import { getDatabase } from '../database';
import { media } from '../database/schema';
import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_WEB_CONTENTS_ACTIONS, type AppMenuAction } from '../shared/menuCommands';
import { generateBlogmarkBookmarkletSource } from '../shared/blogmark';
import { registerMetadataDiffHandlers } from './metadataDiffHandlers';
import { registerBlogHandlers } from './blogHandlers';
import { registerPublishHandlers } from './publishHandlers';

/**
 * Wrap an IPC handler so that "Database is closing" errors during shutdown
 * are silently swallowed instead of being logged as scary red error messages.
 */
function safeHandle(channel: string, handler: (...args: any[]) => Promise<any>): void {
  ipcMain.handle(channel, async (...args) => {
    try {
      return await handler(...args);
    } catch (error: any) {
      if (error?.message === 'Database is closing') {
        return null; // Silently ignore during shutdown
      }
      throw error; // Re-throw all other errors
    }
  });
}

function buildCanonicalPreviewPath(createdAt: Date, slug: string): string {
  const year = createdAt.getFullYear();
  const month = String(createdAt.getMonth() + 1).padStart(2, '0');
  const day = String(createdAt.getDate()).padStart(2, '0');
  return `/${year}/${month}/${day}/${slug}`;
}

function resolvePostCreatedAt(post: { createdAt: Date | string }): Date {
  if (post.createdAt instanceof Date) {
    return post.createdAt;
  }

  const parsed = new Date(post.createdAt);
  return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
}

function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean {
  if (!sender) {
    return false;
  }

  if (!APP_MENU_WEB_CONTENTS_ACTIONS.has(action)) {
    return false;
  }

  switch (action) {
    case 'undo':
      sender.undo?.();
      return true;
    case 'redo':
      sender.redo?.();
      return true;
    case 'cut':
      sender.cut?.();
      return true;
    case 'copy':
      sender.copy?.();
      return true;
    case 'paste':
      sender.paste?.();
      return true;
    case 'delete':
      sender.delete?.();
      return true;
    case 'selectAll':
      sender.selectAll?.();
      return true;
    case 'toggleDevTools':
      if (sender.isDevToolsOpened?.()) {
        sender.closeDevTools?.();
      } else {
        sender.openDevTools?.({ mode: 'detach' });
      }
      return true;
    case 'reload':
      sender.reload?.();
      return true;
    case 'forceReload':
      sender.reloadIgnoringCache?.();
      return true;
    case 'resetZoom':
      sender.setZoomLevel?.(0);
      return true;
    case 'zoomIn': {
      const currentZoomLevel = sender.getZoomLevel?.() ?? 0;
      sender.setZoomLevel?.(currentZoomLevel + 0.5);
      return true;
    }
    case 'zoomOut': {
      const currentZoomLevel = sender.getZoomLevel?.() ?? 0;
      sender.setZoomLevel?.(currentZoomLevel - 0.5);
      return true;
    }
    case 'toggleFullScreen': {
      const ownerWindow = BrowserWindow.fromWebContents(sender);
      if (!ownerWindow) {
        return false;
      }
      ownerWindow.setFullScreen(!ownerWindow.isFullScreen());
      return true;
    }
    default:
      return false;
  }
}

export function registerIpcHandlers(): void {
  // ============ Git Handlers ============

  safeHandle('git:checkAvailability', async () => {
    const engine = getGitEngine();
    return engine.checkAvailability();
  });

  safeHandle('git:getRepoState', async (_, projectPath: string) => {
    const engine = getGitEngine();
    return engine.getRepoState(projectPath);
  });

  safeHandle('git:status', async (_, projectPath: string) => {
    const engine = getGitEngine();
    return engine.getStatus(projectPath);
  });

  safeHandle('git:diff', async (_, projectPath: string, filePath: string) => {
    const engine = getGitEngine();
    return engine.getDiff(projectPath, filePath);
  });

  safeHandle('git:diffContent', async (_, projectPath: string, filePath: string) => {
    const engine = getGitEngine();
    return engine.getDiffContent(projectPath, filePath);
  });

  safeHandle('git:commitDiffContent', async (_, projectPath: string, commitHash: string) => {
    const engine = getGitEngine();
    return engine.getCommitDiffContent(projectPath, commitHash);
  });

  safeHandle('git:history', async (_, projectPath: string, limit?: number) => {
    const engine = getGitEngine();
    return engine.getHistory(projectPath, limit);
  });

  safeHandle('git:fileHistory', async (_, projectPath: string, filePath: string, limit?: number) => {
    const engine = getGitEngine();
    return engine.getFileHistory(projectPath, filePath, limit);
  });

  safeHandle('git:remoteState', async (_, projectPath: string) => {
    const engine = getGitEngine();
    return engine.getRemoteState(projectPath);
  });

  safeHandle('git:fetch', async (_, projectPath: string) => {
    const engine = getGitEngine();
    return engine.fetch(projectPath);
  });

  safeHandle('git:pull', async (_, projectPath: string) => {
    const engine = getGitEngine();
    const beforeHead = await engine.getHeadCommit(projectPath);
    const pullResult = await engine.pull(projectPath);

    if (!pullResult.success) {
      return pullResult;
    }

    const afterHead = await engine.getHeadCommit(projectPath);
    if (!beforeHead || !afterHead || beforeHead === afterHead) {
      return pullResult;
    }

    const [changedPostFiles, changedScriptFiles] = await Promise.all([
      engine.getChangedPostFilesBetween(projectPath, beforeHead, afterHead),
      engine.getChangedScriptFilesBetween(projectPath, beforeHead, afterHead),
    ]);
    if (changedPostFiles.length === 0 && changedScriptFiles.length === 0) {
      return pullResult;
    }

    try {
      const projectEngine = getProjectEngine();
      const project = await projectEngine.getActiveProject();
      const postEngine = getPostEngine();
      const scriptEngine = getScriptEngine();

      if (project) {
        const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
        postEngine.setProjectContext(project.id, dataDir);
        scriptEngine.setProjectContext(project.id, dataDir);
      }

      await Promise.all([
        changedPostFiles.length > 0
          ? postEngine.reconcilePublishedPostsFromGitChanges(projectPath, changedPostFiles)
          : Promise.resolve(),
        changedScriptFiles.length > 0
          ? scriptEngine.reconcileScriptsFromGitChanges(projectPath, changedScriptFiles)
          : Promise.resolve(),
      ]);
    } catch (error) {
      console.error('Failed to reconcile published posts/scripts after git pull:', error);
    }

    return pullResult;
  });

  safeHandle('git:push', async (_, projectPath: string) => {
    const engine = getGitEngine();
    return engine.push(projectPath);
  });

  safeHandle('git:commitAll', async (_, projectPath: string, message: string) => {
    const engine = getGitEngine();
    return engine.commitAll(projectPath, message);
  });

  safeHandle('git:init', async (event, projectPath: string, remoteUrl?: string) => {
    const engine = getGitEngine();
    return engine.initializeRepo(projectPath, remoteUrl, (progress) => {
      event.sender.send('git:initProgress', progress);
    });
  });

  safeHandle('git:ensureGitignore', async (_, projectPath: string) => {
    const engine = getGitEngine();
    return engine.ensureGitignore(projectPath);
  });

  safeHandle('git:pruneLfs', async (_, projectPath: string, options?: { dryRun?: boolean; verifyRemote?: boolean }) => {
    const engine = getGitEngine();
    return engine.pruneLfsCache(projectPath, options);
  });

  // ============ Project Handlers ============

  safeHandle('projects:create', async (_, data: { name: string; description?: string; slug?: string; dataPath?: string }) => {
    const engine = getProjectEngine();
    return engine.createProject(data);
  });

  safeHandle('projects:update', async (_, id: string, data: Partial<ProjectData>) => {
    const engine = getProjectEngine();
    return engine.updateProject(id, data);
  });

  safeHandle('projects:delete', async (_, id: string) => {
    const engine = getProjectEngine();
    return engine.deleteProject(id);
  });

  safeHandle('projects:deleteWithData', async (_, id: string) => {
    const engine = getProjectEngine();
    return engine.deleteProjectWithData(id);
  });

  safeHandle('app:getSystemLanguage', async () => {
    return app.getLocale();
  });

  safeHandle('projects:get', async (_, id: string) => {
    const engine = getProjectEngine();
    return engine.getProject(id);
  });

  safeHandle('projects:getAll', async () => {
    const engine = getProjectEngine();
    return engine.getAllProjects();
  });

  safeHandle('projects:getActive', async () => {
    const projectEngine = getProjectEngine();
    const project = await projectEngine.getActiveProject();

    // Ensure all engines have the correct project context
    if (project) {
      const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
      // For thumbnails and meta: use dataDir (whether custom or internal)
      // This ensures all project data lives in the same location for backup
      const postEngine = getPostEngine();
      const mediaEngine = getMediaEngine();
      const metaEngine = getMetaEngine();
      const menuEngine = getMenuEngine();
      const tagEngine = getTagEngine();
      const scriptEngine = getScriptEngine();
      postEngine.setProjectContext(project.id, dataDir);
      mediaEngine.setProjectContext(project.id, dataDir, dataDir);
      metaEngine.setProjectContext(project.id, dataDir);
      menuEngine.setProjectContext(project.id, dataDir);
      tagEngine.setProjectContext(project.id, dataDir);
      scriptEngine.setProjectContext(project.id, dataDir);
      const postMediaEngine = getPostMediaEngine();
      postMediaEngine.setProjectContext(project.id);

      // Sync meta on startup
      await metaEngine.syncOnStartup();

      // Set search language from project metadata
      const { isoToStemmerLanguage } = await import('../engine/stemmer');
      const metadata = await metaEngine.getProjectMetadata();
      if (metadata?.mainLanguage) {
        const stemmerLang = isoToStemmerLanguage(metadata.mainLanguage);
        postEngine.setSearchLanguage(stemmerLang);
        mediaEngine.setSearchLanguage(stemmerLang);
      }
    }

    return project;
  });

  safeHandle('projects:setActive', async (_, id: string) => {
    const projectEngine = getProjectEngine();
    const project = await projectEngine.setActiveProject(id);

    // Update all engines to use the new project context
    if (project) {
      const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
      // For thumbnails and meta: use dataDir (whether custom or internal)
      // This ensures all project data lives in the same location for backup
      const postEngine = getPostEngine();
      const mediaEngine = getMediaEngine();
      const metaEngine = getMetaEngine();
      const menuEngine = getMenuEngine();
      const tagEngine = getTagEngine();
      const scriptEngine = getScriptEngine();
      postEngine.setProjectContext(project.id, dataDir);
      mediaEngine.setProjectContext(project.id, dataDir, dataDir);
      metaEngine.setProjectContext(project.id, dataDir);
      menuEngine.setProjectContext(project.id, dataDir);
      tagEngine.setProjectContext(project.id, dataDir);
      scriptEngine.setProjectContext(project.id, dataDir);
      const postMediaEngine = getPostMediaEngine();
      postMediaEngine.setProjectContext(project.id);

      // Sync meta on project switch
      await metaEngine.syncOnStartup();

      // Set search language from project metadata
      const { isoToStemmerLanguage } = await import('../engine/stemmer');
      const metadata = await metaEngine.getProjectMetadata();
      if (metadata?.mainLanguage) {
        const stemmerLang = isoToStemmerLanguage(metadata.mainLanguage);
        postEngine.setSearchLanguage(stemmerLang);
        mediaEngine.setSearchLanguage(stemmerLang);
      }
    }

    return project;
  });

  // ============ Post Handlers ============
  
  safeHandle('posts:create', async (_, data: Partial<PostData>) => {
    const engine = getPostEngine();
    
    // If no author provided, use default author from project settings
    if (!data.author) {
      const metaEngine = getMetaEngine();
      const metadata = await metaEngine.getProjectMetadata();
      if (metadata?.defaultAuthor) {
        data.author = metadata.defaultAuthor;
      }
    }
    
    return engine.createPost(data);
  });

  safeHandle('posts:isSlugAvailable', async (_, slug: string, excludePostId?: string) => {
    const engine = getPostEngine();
    return engine.isSlugAvailable(slug, excludePostId);
  });

  safeHandle('posts:generateUniqueSlug', async (_, title: string, excludePostId?: string) => {
    const engine = getPostEngine();
    return engine.generateUniqueSlug(title, excludePostId);
  });

  safeHandle('posts:update', async (_, id: string, data: Partial<PostData>) => {
    const engine = getPostEngine();
    return engine.updatePost(id, data);
  });

  safeHandle('posts:delete', async (_, id: string) => {
    const engine = getPostEngine();
    return engine.deletePost(id);
  });

  safeHandle('posts:get', async (_, id: string) => {
    const engine = getPostEngine();
    return engine.getPost(id);
  });

  safeHandle('posts:getPreviewUrl', async (_, id: string, options?: { draft?: boolean }) => {
    const engine = getPostEngine();
    const post = await engine.getPost(id);

    if (!post) {
      return null;
    }

    const createdAt = resolvePostCreatedAt(post);
    const canonicalPath = buildCanonicalPreviewPath(createdAt, post.slug);
    if (options?.draft) {
      return `http://127.0.0.1:4123${canonicalPath}?draft=true&postId=${encodeURIComponent(id)}`;
    }

    return `http://127.0.0.1:4123${canonicalPath}`;
  });

  safeHandle('posts:getAll', async (_, options?: PaginationOptions) => {
    const engine = getPostEngine();
    return engine.getAllPosts(options);
  });

  safeHandle('posts:getByStatus', async (_, status: 'draft' | 'published' | 'archived') => {
    const engine = getPostEngine();
    return engine.getPostsByStatus(status);
  });

  safeHandle('posts:publish', async (_, id: string) => {
    const engine = getPostEngine();
    return engine.publishPost(id);
  });

  safeHandle('posts:discard', async (_, id: string) => {
    const engine = getPostEngine();
    return engine.discardChanges(id);
  });

  safeHandle('posts:hasPublishedVersion', async (_, id: string) => {
    const engine = getPostEngine();
    return engine.hasPublishedVersion(id);
  });

  safeHandle('posts:rebuildFromFiles', async () => {
    // Ensure project context is current before rebuilding
    const projectEngine = getProjectEngine();
    const project = await projectEngine.getActiveProject();
    const engine = getPostEngine();
    const metaEngine = getMetaEngine();
    if (project) {
      const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
      engine.setProjectContext(project.id, dataDir);
      metaEngine.setProjectContext(project.id, dataDir);
    }
    await engine.rebuildDatabaseFromFiles();
    await metaEngine.syncOnStartup();
    return true;
  });

  safeHandle('posts:search', async (_, query: string) => {
    const engine = getPostEngine();
    return engine.searchPosts(query);
  });

  safeHandle('posts:filter', async (_, filter: PostFilter) => {
    const engine = getPostEngine();
    return engine.getPostsFiltered(filter);
  });

  safeHandle('posts:getTags', async () => {
    const engine = getPostEngine();
    return engine.getAvailableTags();
  });

  safeHandle('posts:getCategories', async () => {
    const engine = getPostEngine();
    return engine.getAvailableCategories();
  });

  safeHandle('posts:getByYearMonth', async () => {
    const engine = getPostEngine();
    return engine.getPostsByYearMonth();
  });

  safeHandle('posts:getTagsWithCounts', async () => {
    const engine = getPostEngine();
    return engine.getTagsWithCounts();
  });

  safeHandle('posts:getCategoriesWithCounts', async () => {
    const engine = getPostEngine();
    return engine.getCategoriesWithCounts();
  });

  safeHandle('posts:getDashboardStats', async () => {
    const engine = getPostEngine();
    return engine.getDashboardStats();
  });

  safeHandle('posts:getLinksTo', async (_, id: string) => {
    const engine = getPostEngine();
    return engine.getLinksTo(id);
  });

  safeHandle('posts:getLinkedBy', async (_, id: string) => {
    const engine = getPostEngine();
    return engine.getLinkedBy(id);
  });

  safeHandle('posts:rebuildLinks', async () => {
    const engine = getPostEngine();
    return engine.rebuildAllPostLinks();
  });

  safeHandle('posts:reindexText', async () => {
    const projectEngine = getProjectEngine();
    const project = await projectEngine.getActiveProject();
    const engine = getPostEngine();
    if (project) {
      const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
      engine.setProjectContext(project.id, dataDir);
    }
    return engine.reindexText();
  });

  // ============ Media Handlers ============

  safeHandle('media:import', async (_, sourcePath: string, metadata?: Partial<MediaData>) => {
    const engine = getMediaEngine();
    
    // If no author provided, use default author from project settings
    if (!metadata?.author) {
      const metaEngine = getMetaEngine();
      const projectMetadata = await metaEngine.getProjectMetadata();
      if (projectMetadata?.defaultAuthor) {
        metadata = metadata || {};
        metadata.author = projectMetadata.defaultAuthor;
      }
    }
    
    return engine.importMedia(sourcePath, metadata);
  });

  safeHandle('media:importDialog', async () => {
    const result = await dialog.showOpenDialog({
      title: 'Import Media',
      filters: [
        { name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'] },
        { name: 'All Files', extensions: ['*'] },
      ],
      properties: ['openFile', 'multiSelections'],
    });

    if (result.canceled || result.filePaths.length === 0) {
      return [];
    }

    // Ensure project context is current before importing
    const projectEngine = getProjectEngine();
    const project = await projectEngine.getActiveProject();
    const engine = getMediaEngine();
    if (project) {
      const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
      // For thumbnails and meta: use dataDir (whether custom or internal)
      // This ensures all project data lives in the same location for backup
      engine.setProjectContext(project.id, dataDir, dataDir);
    }

    const imported: MediaData[] = [];

    // Get default author from project settings
    const metaEngine = getMetaEngine();
    const projectMetadata = await metaEngine.getProjectMetadata();
    const defaultAuthor = projectMetadata?.defaultAuthor;

    for (const filePath of result.filePaths) {
      try {
        const media = await engine.importMedia(filePath, defaultAuthor ? { author: defaultAuthor } : undefined);
        imported.push(media);
      } catch (error) {
        console.error(`Failed to import ${filePath}:`, error);
      }
    }

    return imported;
  });

  safeHandle('media:update', async (_, id: string, data: Partial<MediaData>) => {
    const engine = getMediaEngine();
    return engine.updateMedia(id, data);
  });

  safeHandle('media:replaceFile', async (_, id: string, newSourcePath: string) => {
    const engine = getMediaEngine();
    return engine.replaceMediaFile(id, newSourcePath);
  });

  safeHandle('media:replaceFileDialog', async (_, id: string) => {
    // Get the current media to determine file type filter
    const engine = getMediaEngine();
    const currentMedia = await engine.getMedia(id);
    if (!currentMedia) {
      return null;
    }

    // Build filter based on current media type
    let filters: { name: string; extensions: string[] }[] = [];
    if (currentMedia.mimeType.startsWith('image/')) {
      filters = [
        { name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'] },
        { name: 'All Files', extensions: ['*'] },
      ];
    } else {
      filters = [{ name: 'All Files', extensions: ['*'] }];
    }

    const result = await dialog.showOpenDialog({
      title: 'Replace Media File',
      filters,
      properties: ['openFile'],
    });

    if (result.canceled || result.filePaths.length === 0) {
      return null;
    }

    return engine.replaceMediaFile(id, result.filePaths[0]);
  });

  safeHandle('media:delete', async (_, id: string) => {
    const engine = getMediaEngine();
    return engine.deleteMedia(id);
  });

  safeHandle('media:get', async (_, id: string) => {
    const engine = getMediaEngine();
    return engine.getMedia(id);
  });

  safeHandle('media:getUrl', async (_, id: string) => {
    // Returns the relative path for a media item (e.g. media/2025/01/uuid.jpg)
    // and exposes it as an absolute preview path (e.g. /media/2025/01/uuid.jpg)
    // so inserted markdown uses root-absolute URLs.
    const engine = getMediaEngine();
    const relativePath = await engine.getRelativePath(id);
    const normalized = relativePath ?? `media/${id}`;
    return normalized.startsWith('/') ? normalized : `/${normalized}`;
  });

  safeHandle('media:getFilePath', async (_, id: string) => {
    // Returns the actual file path for a media item (for debugging/advanced use)
    const db = getDatabase().getLocal();
    const mediaItem = await db.select().from(media).where(eq(media.id, id)).get();
    return mediaItem?.filePath ?? null;
  });

  safeHandle('media:getAll', async () => {
    const engine = getMediaEngine();
    return engine.getAllMedia();
  });

  safeHandle('media:filter', async (_, filter: import('../engine/MediaEngine').MediaFilter) => {
    const engine = getMediaEngine();
    return engine.getMediaFiltered(filter);
  });

  safeHandle('media:search', async (_, query: string) => {
    const engine = getMediaEngine();
    return engine.searchMedia(query);
  });

  safeHandle('media:getByYearMonth', async () => {
    const engine = getMediaEngine();
    return engine.getMediaByYearMonth();
  });

  safeHandle('media:getTags', async () => {
    const engine = getMediaEngine();
    return engine.getAvailableTags();
  });

  safeHandle('media:getTagsWithCounts', async () => {
    const engine = getMediaEngine();
    return engine.getTagsWithCounts();
  });

  safeHandle('media:rebuildFromFiles', async () => {
    // Ensure project context is current before rebuilding
    const projectEngine = getProjectEngine();
    const project = await projectEngine.getActiveProject();
    const engine = getMediaEngine();
    if (project) {
      const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
      // For thumbnails and meta: use dataDir (whether custom or internal)
      // This ensures all project data lives in the same location for backup
      engine.setProjectContext(project.id, dataDir, dataDir);
    }
    return engine.rebuildDatabaseFromFiles();
  });

  safeHandle('media:reindexText', async () => {
    const engine = getMediaEngine();
    return engine.reindexText();
  });

  safeHandle('media:getThumbnail', async (_, id: string, size?: 'small' | 'medium' | 'large') => {
    const engine = getMediaEngine();
    return engine.getThumbnailDataUrl(id, size || 'small');
  });

  safeHandle('media:regenerateThumbnails', async (_, id: string) => {
    const engine = getMediaEngine();
    const mediaItem = await engine.getMedia(id);
    if (mediaItem && mediaItem.mimeType.startsWith('image/')) {
      const db = getDatabase().getLocal();
      const dbMedia = await db.select().from(media).where(eq(media.id, id)).get();
      if (dbMedia) {
        return engine.generateThumbnails(id, dbMedia.filePath);
      }
    }
    return null;
  });

  safeHandle('media:regenerateMissingThumbnails', async () => {
    const projectEngine = getProjectEngine();
    const project = await projectEngine.getActiveProject();
    const engine = getMediaEngine();
    if (project) {
      const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
      // For thumbnails and meta: use dataDir (whether custom or internal)
      // This ensures all project data lives in the same location for backup
      engine.setProjectContext(project.id, dataDir, dataDir);
    }
    return engine.regenerateMissingThumbnails();
  });

  // ============ Script Handlers ============

  safeHandle('scripts:create', async (_, data: CreateScriptInput) => {
    const engine = getScriptEngine();
    return engine.createScript(data);
  });

  safeHandle('scripts:update', async (_, id: string, data: UpdateScriptInput) => {
    const engine = getScriptEngine();
    return engine.updateScript(id, data);
  });

  safeHandle('scripts:delete', async (_, id: string) => {
    const engine = getScriptEngine();
    return engine.deleteScript(id);
  });

  safeHandle('scripts:get', async (_, id: string) => {
    const engine = getScriptEngine();
    return engine.getScript(id);
  });

  safeHandle('scripts:getAll', async () => {
    const engine = getScriptEngine();
    return engine.getAllScripts();
  });

  // Internal: used by the editor macro plugin to detect known Python macros.
  // Intentionally excluded from the Python API contract and API.md because
  // it is an internal renderer helper, not a user-facing scripting API.
  safeHandle('scripts:getEnabledMacroSlugs', async () => {
    const engine = getScriptEngine();
    const scripts = await engine.getEnabledMacroScripts();
    return scripts.map((s) => s.slug);
  });

  safeHandle('scripts:rebuildFromFiles', async () => {
    const projectEngine = getProjectEngine();
    const project = await projectEngine.getActiveProject();
    const engine = getScriptEngine();
    if (project) {
      const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
      engine.setProjectContext(project.id, dataDir);
    }
    await engine.rebuildDatabaseFromFiles();
    return true;
  });

  // ============ Task Handlers ============

  safeHandle('tasks:getAll', async () => {
    return taskManager.getAllTasks();
  });

  safeHandle('tasks:getRunning', async () => {
    return taskManager.getRunningTasks();
  });

  safeHandle('tasks:cancel', async (_, taskId: string) => {
    return taskManager.cancelTask(taskId);
  });

  safeHandle('tasks:clearCompleted', async () => {
    return taskManager.clearCompletedTasks();
  });

  // ============ Sync Handlers (git operations via GitApiAdapter) ============

  safeHandle('sync:checkAvailability', async () => {
    return getGitApiAdapter().checkAvailability();
  });

  safeHandle('sync:getRepoState', async () => {
    return getGitApiAdapter().getRepoState();
  });

  safeHandle('sync:getStatus', async () => {
    return getGitApiAdapter().getStatus();
  });

  safeHandle('sync:getHistory', async (_, limit?: number) => {
    return getGitApiAdapter().getHistory(limit);
  });

  safeHandle('sync:getRemoteState', async () => {
    return getGitApiAdapter().getRemoteState();
  });

  safeHandle('sync:fetch', async () => {
    return getGitApiAdapter().fetch();
  });

  safeHandle('sync:pull', async () => {
    return getGitApiAdapter().pull();
  });

  safeHandle('sync:push', async () => {
    return getGitApiAdapter().push();
  });

  safeHandle('sync:commitAll', async (_, message: string) => {
    return getGitApiAdapter().commitAll(message);
  });

  // ============ App Handlers ============

  safeHandle('app:getDataPaths', async () => {
    // Get paths for the active project
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    const projectId = activeProject?.id || 'default';
    const paths = projectEngine.getProjectPaths(projectId, activeProject?.dataPath);
    return {
      database: getDatabase().getDataPaths().database,
      posts: paths.posts,
      media: paths.media,
    };
  });

  safeHandle('app:openFolder', async (_, folderPath: string) => {
    return shell.openPath(folderPath);
  });

  safeHandle('app:selectFolder', async (_, title?: string) => {
    const result = await dialog.showOpenDialog({
      title: title || 'Select Folder',
      properties: ['openDirectory', 'createDirectory'],
    });
    if (result.canceled || result.filePaths.length === 0) {
      return null;
    }
    return result.filePaths[0];
  });

  safeHandle('app:getDefaultProjectPath', async (_, projectId: string) => {
    const projectEngine = getProjectEngine();
    return projectEngine.getDefaultProjectBaseDir(projectId);
  });

  safeHandle('app:getBlogmarkBookmarklet', async () => {
    return generateBlogmarkBookmarkletSource();
  });

  safeHandle('app:copyToClipboard', async (_, text: string) => {
    clipboard.writeText(String(text ?? ''));
    return true;
  });

  safeHandle('app:getTitleBarMetrics', async (event) => {
    const ownerWindow = BrowserWindow.fromWebContents(event.sender);
    const buttonPosition = ownerWindow?.getWindowButtonPosition?.();
    if (!buttonPosition) {
      return null;
    }

    const estimatedClusterWidth = Math.max(52, Math.round(buttonPosition.y * 4));
    const trailingPadding = Math.max(8, Math.round(buttonPosition.y * 0.6));
    const macosLeftInset = Math.max(0, Math.round(buttonPosition.x + estimatedClusterWidth + trailingPadding));

    return { macosLeftInset };
  });

  safeHandle('app:showItemInFolder', async (_, itemPath: string) => {
    return shell.showItemInFolder(itemPath);
  });

  safeHandle('app:readProjectMetadata', async (_, folderPath: string) => {
    const metaPath = path.join(folderPath, 'meta', 'project.json');
    try {
      const content = await fsPromises.readFile(metaPath, 'utf-8');
      const metadata = JSON.parse(content);
      // Return metadata but exclude dataPath (will be set to selected folder)
      return {
        name: metadata.name || undefined,
        description: metadata.description || undefined,
        publicUrl: metadata.publicUrl || undefined,
        mainLanguage: metadata.mainLanguage || undefined,
      };
    } catch {
      // File doesn't exist or is invalid - return null
      return null;
    }
  });

  safeHandle('app:triggerMenuAction', async (event, action: string) => {
    const typedAction = action as AppMenuAction;

    if (typedAction === 'quit') {
      app.quit();
      return;
    }

    if (typedAction === 'viewOnGitHub') {
      await shell.openExternal('https://github.com/rfc1437/bDS');
      return;
    }

    if (typedAction === 'openInBrowser') {
      await shell.openExternal('http://localhost:4123/');
      return;
    }

    if (typedAction === 'openDataFolder') {
      const paths = getDatabase().getDataPaths();
      await shell.openPath(path.dirname(paths.database));
      return;
    }

    if (typedAction === 'reportIssue') {
      await shell.openExternal('https://github.com/rfc1437/bDS/issues');
      return;
    }

    const handledByWebContents = runWebContentsMenuAction((event as any)?.sender, typedAction);
    if (handledByWebContents) {
      return;
    }

    const channel = APP_MENU_ACTION_EVENT_MAP[typedAction];
    if (!channel) {
      return;
    }
    event.sender.send(channel);
  });

  // ============ Meta Handlers ============

  const ensureMetaContext = async (engine: ReturnType<typeof getMetaEngine>) => {
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    if (!activeProject) {
      return;
    }

    const dataDir = projectEngine.getDataDir(activeProject.id, activeProject.dataPath);
    engine.setProjectContext(activeProject.id, dataDir);
  };

  const ensureMetaReady = async (engine: ReturnType<typeof getMetaEngine>) => {
    await ensureMetaContext(engine);
    if (!engine.isInitialized()) {
      await engine.syncOnStartup();
    }
  };

  safeHandle('menu:get', async () => {
    const projectEngine = getProjectEngine();
    const menuEngine = getMenuEngine();
    const project = await projectEngine.getActiveProject();

    if (!project) {
      throw new Error('No active project');
    }

    const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
    menuEngine.setProjectContext(project.id, dataDir);
    return menuEngine.getMenu();
  });

  safeHandle('menu:save', async (_, menu: MenuDocument) => {
    const projectEngine = getProjectEngine();
    const menuEngine = getMenuEngine();
    const project = await projectEngine.getActiveProject();

    if (!project) {
      throw new Error('No active project');
    }

    const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
    menuEngine.setProjectContext(project.id, dataDir);
    return menuEngine.saveMenu(menu);
  });

  safeHandle('meta:getTags', async () => {
    const engine = getMetaEngine();
    await ensureMetaReady(engine);
    return engine.getTags();
  });

  safeHandle('meta:getCategories', async () => {
    const engine = getMetaEngine();
    await ensureMetaReady(engine);
    return engine.getCategories();
  });

  safeHandle('meta:addTag', async (_, tag: string) => {
    const engine = getMetaEngine();
    await ensureMetaReady(engine);
    await engine.addTag(tag);
    return engine.getTags();
  });

  safeHandle('meta:removeTag', async (_, tag: string) => {
    const engine = getMetaEngine();
    await ensureMetaReady(engine);
    await engine.removeTag(tag);
    return engine.getTags();
  });

  safeHandle('meta:addCategory', async (_, category: string) => {
    const engine = getMetaEngine();
    await ensureMetaReady(engine);
    await engine.addCategory(category);
    return engine.getCategories();
  });

  safeHandle('meta:removeCategory', async (_, category: string) => {
    const engine = getMetaEngine();
    await ensureMetaReady(engine);
    await engine.removeCategory(category);
    return engine.getCategories();
  });

  safeHandle('meta:syncOnStartup', async () => {
    const engine = getMetaEngine();
    await ensureMetaContext(engine);
    await engine.syncOnStartup();
    return {
      tags: await engine.getTags(),
      categories: await engine.getCategories(),
      projectMetadata: await engine.getProjectMetadata(),
    };
  });

  safeHandle('meta:getProjectMetadata', async () => {
    const engine = getMetaEngine();
    await ensureMetaReady(engine);
    return engine.getProjectMetadata();
  });

  safeHandle('meta:setProjectMetadata', async (_, metadata: { name: string; description?: string }) => {
    const engine = getMetaEngine();
    await ensureMetaContext(engine);
    await engine.setProjectMetadata(metadata);
    return engine.getProjectMetadata();
  });

  safeHandle('meta:updateProjectMetadata', async (_, updates: { name?: string; description?: string; dataPath?: string; publicUrl?: string; mainLanguage?: string; defaultAuthor?: string; maxPostsPerPage?: number; blogmarkCategory?: string; pythonRuntimeMode?: 'webworker' | 'main-thread'; picoTheme?: import('../shared/picoThemes').PicoThemeName; categoryMetadata?: Record<string, { renderInLists: boolean; showTitle: boolean; title: string }>; categorySettings?: Record<string, { renderInLists: boolean; showTitle: boolean }> }) => {
    const engine = getMetaEngine();
    await ensureMetaContext(engine);
    await engine.updateProjectMetadata(updates);
    return engine.getProjectMetadata();
  });

  // ============ Tag Management Handlers ============

  safeHandle('tags:getAll', async () => {
    const engine = getTagEngine();
    return engine.getAllTags();
  });

  safeHandle('tags:getWithCounts', async () => {
    const engine = getTagEngine();
    return engine.getTagsWithCounts();
  });

  safeHandle('tags:get', async (_, id: string) => {
    const engine = getTagEngine();
    return engine.getTag(id);
  });

  safeHandle('tags:getByName', async (_, name: string) => {
    const engine = getTagEngine();
    return engine.getTagByName(name);
  });

  safeHandle('tags:create', async (_, data: { name: string; color?: string }) => {
    const engine = getTagEngine();
    return engine.createTag(data);
  });

  safeHandle('tags:update', async (_, id: string, data: { name?: string; color?: string | null }) => {
    const engine = getTagEngine();
    return engine.updateTag(id, data);
  });

  safeHandle('tags:delete', async (_, id: string) => {
    const engine = getTagEngine();
    return engine.deleteTag(id);
  });

  safeHandle('tags:merge', async (_, sourceTagIds: string[], targetTagId: string) => {
    const engine = getTagEngine();
    return engine.mergeTags(sourceTagIds, targetTagId);
  });

  safeHandle('tags:rename', async (_, id: string, newName: string) => {
    const engine = getTagEngine();
    return engine.renameTag(id, newName);
  });

  safeHandle('tags:getPostsWithTag', async (_, tagId: string) => {
    const engine = getTagEngine();
    return engine.getPostsWithTag(tagId);
  });

  safeHandle('tags:syncFromPosts', async () => {
    const engine = getTagEngine();
    return engine.syncTagsFromPosts();
  });

  // ============ Post-Media Link Handlers ============

  safeHandle('postMedia:link', async (_, postId: string, mediaId: string) => {
    const engine = getPostMediaEngine();
    return engine.linkMediaToPost(postId, mediaId);
  });

  safeHandle('postMedia:unlink', async (_, postId: string, mediaId: string) => {
    const engine = getPostMediaEngine();
    return engine.unlinkMediaFromPost(postId, mediaId);
  });

  safeHandle('postMedia:linkMany', async (_, postId: string, mediaIds: string[]) => {
    const engine = getPostMediaEngine();
    return engine.linkManyToPost(postId, mediaIds);
  });

  safeHandle('postMedia:unlinkMany', async (_, postId: string, mediaIds: string[]) => {
    const engine = getPostMediaEngine();
    return engine.unlinkManyFromPost(postId, mediaIds);
  });

  safeHandle('postMedia:getForPost', async (_, postId: string) => {
    const engine = getPostMediaEngine();
    return engine.getLinkedMediaForPost(postId);
  });

  safeHandle('postMedia:getForMedia', async (_, mediaId: string) => {
    const engine = getPostMediaEngine();
    return engine.getLinkedPostsForMedia(mediaId);
  });

  safeHandle('postMedia:getMediaDataForPost', async (_, postId: string) => {
    const engine = getPostMediaEngine();
    return engine.getLinkedMediaDataForPost(postId);
  });

  safeHandle('postMedia:reorder', async (_, postId: string, mediaIds: string[]) => {
    const engine = getPostMediaEngine();
    return engine.reorderMediaForPost(postId, mediaIds);
  });

  safeHandle('postMedia:isLinked', async (_, postId: string, mediaId: string) => {
    const engine = getPostMediaEngine();
    return engine.isMediaLinkedToPost(postId, mediaId);
  });

  safeHandle('postMedia:import', async (_, postId: string, filePath: string) => {
    const engine = getPostMediaEngine();
    return engine.importMediaForPost(postId, filePath);
  });

  safeHandle('postMedia:rebuild', async () => {
    const engine = getPostMediaEngine();
    return engine.rebuildFromSidecars();
  });

  // ============ Import Analysis Handlers ============

  // Helper to emit progress events
  const emitImportProgress = (step: string, detail?: string) => {
    ipcMain.emit('forward-to-renderer', 'import:progress', { step, detail });
  };

  safeHandle('import:selectAndAnalyze', async (_, uploadsFolder?: string) => {
    emitImportProgress('Selecting file...');
    
    const result = await dialog.showOpenDialog({
      title: 'Select WordPress Export File (WXR)',
      filters: [
        { name: 'WordPress Export', extensions: ['xml'] },
        { name: 'All Files', extensions: ['*'] },
      ],
      properties: ['openFile'],
    });

    if (result.canceled || result.filePaths.length === 0) {
      return null;
    }

    const filePath = result.filePaths[0];
    const fileName = filePath.split(/[/\\]/).pop() || filePath;
    
    emitImportProgress('Parsing WXR file...', fileName);
    
    const { WxrParser } = await import('../engine/WxrParser');
    const { ImportAnalysisEngine } = await import('../engine/ImportAnalysisEngine');

    const parser = new WxrParser();
    const wxrData = await parser.parseFile(filePath);

    emitImportProgress('Loading project data...', `Found ${wxrData.posts.length} posts, ${wxrData.media.length} media`);

    const analysisEngine = new ImportAnalysisEngine();
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    if (activeProject) {
      analysisEngine.setProjectContext(activeProject.id);
    }

    emitImportProgress('Analyzing posts...', `${wxrData.posts.length} posts`);
    
    // Add progress callback to engine
    analysisEngine.onProgress = (step: string, detail?: string) => {
      emitImportProgress(step, detail);
    };

    const report = await analysisEngine.analyzeWxr(wxrData, filePath, uploadsFolder || undefined);
    
    emitImportProgress('Analysis complete');
    
    return report;
  });

  safeHandle('import:analyzeFile', async (_, filePath: string, uploadsFolder?: string) => {
    const fileName = filePath.split(/[/\\]/).pop() || filePath;
    
    emitImportProgress('Parsing WXR file...', fileName);
    
    const { WxrParser } = await import('../engine/WxrParser');
    const { ImportAnalysisEngine } = await import('../engine/ImportAnalysisEngine');

    const parser = new WxrParser();
    const wxrData = await parser.parseFile(filePath);

    emitImportProgress('Loading project data...', `Found ${wxrData.posts.length} posts, ${wxrData.media.length} media`);

    const analysisEngine = new ImportAnalysisEngine();
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    if (activeProject) {
      analysisEngine.setProjectContext(activeProject.id);
    }

    emitImportProgress('Analyzing posts...');
    
    // Add progress callback to engine
    analysisEngine.onProgress = (step: string, detail?: string) => {
      emitImportProgress(step, detail);
    };

    const report = await analysisEngine.analyzeWxr(wxrData, filePath, uploadsFolder || undefined);
    
    emitImportProgress('Analysis complete');
    
    return report;
  });

  safeHandle('import:selectUploadsFolder', async () => {
    const result = await dialog.showOpenDialog({
      title: 'Select WordPress Uploads Folder',
      properties: ['openDirectory'],
    });

    if (result.canceled || result.filePaths.length === 0) {
      return null;
    }
    return result.filePaths[0];
  });

  // Helper to emit import execution progress events
  const emitImportExecutionProgress = (
    taskId: string,
    phase: string,
    current: number,
    total: number,
    detail?: string,
    eta?: number
  ) => {
    ipcMain.emit('forward-to-renderer', 'import:executionProgress', {
      taskId,
      phase,
      current,
      total,
      detail,
      eta,
    });
  };

  safeHandle('import:execute', async (_, reportJson: string, uploadsFolder?: string) => {
    const { ImportExecutionEngine } = await import('../engine/ImportExecutionEngine');
    
    // Parse the report
    const report = JSON.parse(reportJson) as import('../engine/ImportAnalysisEngine').ImportAnalysisReport;
    
    // Set up project context
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    
    // Calculate total items for ETA
    // Note: 'update' and 'content-duplicate' statuses are SKIPPED during import, only 'new' and resolved conflicts are imported
    const totalItems = 
      report.tags.filter(t => !t.existsInProject).length +
      report.categories.filter(c => !c.existsInProject).length +
      report.posts.items.filter(p => p.status === 'new' || (p.status === 'conflict' && p.conflictResolution && p.conflictResolution !== 'ignore')).length +
      report.media.items.filter(m => m.status === 'new').length +
      report.pages.items.filter(p => p.status === 'new' || (p.status === 'conflict' && p.conflictResolution && p.conflictResolution !== 'ignore')).length;

    // Create a task for the import
    const taskId = `import-${Date.now()}`;
    let processedItems = 0;
    let startTime = Date.now();

    const task = {
      id: taskId,
      name: `Import from ${report.site.title || 'WordPress'}`,
      execute: async (onProgress: (progress: number, message: string) => void) => {
        const executionEngine = new ImportExecutionEngine();
        
        if (activeProject) {
          executionEngine.setProjectContext(activeProject.id, activeProject.dataPath);
        }

        // Get default author from project settings
        const metaEngine = getMetaEngine();
        const projectMetadata = await metaEngine.getProjectMetadata();
        const defaultAuthor = projectMetadata?.defaultAuthor;

        const result = await executionEngine.executeImport(report, {
          uploadsFolder,
          defaultAuthor,
          onProgress: (phase, current, total, detail) => {
            // Update processed items count based on phase progress
            processedItems++;
            
            // Calculate ETA
            const elapsed = Date.now() - startTime;
            const itemsPerMs = processedItems / elapsed;
            const remainingItems = totalItems - processedItems;
            const etaMs = itemsPerMs > 0 ? remainingItems / itemsPerMs : 0;
            
            // Calculate overall progress percentage
            const overallProgress = totalItems > 0 
              ? Math.round((processedItems / totalItems) * 100) 
              : 0;
            
            // Report to TaskManager
            onProgress(overallProgress, `${phase}: ${detail || `${current}/${total}`}`);
            
            // Also emit detailed progress for UI
            emitImportExecutionProgress(taskId, phase, current, total, detail, etaMs);
          },
        });

        // Convert Map to plain object for serialization
        const serializedResult = {
          ...result,
          wpIdToPostId: Object.fromEntries(result.wpIdToPostId),
        };

        // Emit import:complete event to notify UI to refresh
        ipcMain.emit('forward-to-renderer', 'import:complete', {
          taskId,
          success: result.success,
          posts: result.posts,
          media: result.media,
          pages: result.pages,
          tags: result.tags,
        });

        return serializedResult;
      },
    };

    // Run the task - this returns immediately with a promise
    const resultPromise = taskManager.runTask(task);
    
    // Return task ID so UI can track it
    return { taskId, totalItems };
  });

  // ============ Import Definition CRUD Handlers ============

  safeHandle('importDefinitions:create', async (_, name?: string) => {
    const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
    const engine = new ImportDefinitionEngine();
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    if (activeProject) {
      engine.setProjectContext(activeProject.id);
    }
    return engine.createDefinition(name || undefined);
  });

  safeHandle('importDefinitions:get', async (_, id: string) => {
    const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
    const engine = new ImportDefinitionEngine();
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    if (activeProject) {
      engine.setProjectContext(activeProject.id);
    }
    return engine.getDefinition(id);
  });

  safeHandle('importDefinitions:getAll', async () => {
    const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
    const engine = new ImportDefinitionEngine();
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    if (activeProject) {
      engine.setProjectContext(activeProject.id);
    }
    return engine.getAllForProject();
  });

  safeHandle('importDefinitions:update', async (event, id: string, updates: any) => {
    const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
    const engine = new ImportDefinitionEngine();
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    if (activeProject) {
      engine.setProjectContext(activeProject.id);
    }
    const result = await engine.updateDefinition(id, updates);
    // Notify renderer of name changes for sidebar/tab updates
    if (result && updates.name !== undefined) {
      event.sender.send('importDefinition-name-updated', { definitionId: id, name: result.name });
    }
    return result;
  });

  safeHandle('importDefinitions:delete', async (_, id: string) => {
    const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
    const engine = new ImportDefinitionEngine();
    const projectEngine = getProjectEngine();
    const activeProject = await projectEngine.getActiveProject();
    if (activeProject) {
      engine.setProjectContext(activeProject.id);
    }
    return engine.deleteDefinition(id);
  });

  registerMetadataDiffHandlers(safeHandle);
  registerBlogHandlers(safeHandle);
  registerPublishHandlers(safeHandle);

  // ============ Event Forwarding ============
  
  // Forward engine events to renderer
  const postEngine = getPostEngine();
  const mediaEngine = getMediaEngine();
  const projectEngine = getProjectEngine();
  const metaEngine = getMetaEngine();
  const tagEngine = getTagEngine();
  const postMediaEngine = getPostMediaEngine();

  const forwardEvent = (eventName: string) => {
    return (...args: unknown[]) => {
      // Will be sent to renderer via webContents when window is available
      ipcMain.emit('forward-to-renderer', eventName, ...args);
    };
  };

  projectEngine.on('projectCreated', forwardEvent('project:created'));
  projectEngine.on('projectUpdated', forwardEvent('project:updated'));
  projectEngine.on('projectDeleted', forwardEvent('project:deleted'));
  projectEngine.on('activeProjectChanged', forwardEvent('project:activeChanged'));

  postEngine.on('postCreated', forwardEvent('post:created'));
  postEngine.on('postUpdated', forwardEvent('post:updated'));
  postEngine.on('postDeleted', forwardEvent('post:deleted'));
  postEngine.on('rebuildStarted', forwardEvent('posts:rebuildStarted'));
  postEngine.on('databaseRebuilt', forwardEvent('posts:databaseRebuilt'));

  mediaEngine.on('mediaImported', forwardEvent('media:imported'));
  mediaEngine.on('mediaUpdated', forwardEvent('media:updated'));
  mediaEngine.on('mediaDeleted', forwardEvent('media:deleted'));
  mediaEngine.on('mediaFileReplaced', forwardEvent('media:fileReplaced'));
  mediaEngine.on('rebuildStarted', forwardEvent('media:rebuildStarted'));
  mediaEngine.on('databaseRebuilt', forwardEvent('media:databaseRebuilt'));

  metaEngine.on('tagsChanged', forwardEvent('meta:tagsChanged'));
  metaEngine.on('categoriesChanged', forwardEvent('meta:categoriesChanged'));
  metaEngine.on('projectMetadataChanged', forwardEvent('meta:projectMetadataChanged'));

  tagEngine.on('tagCreated', forwardEvent('tag:created'));
  tagEngine.on('tagUpdated', forwardEvent('tag:updated'));
  tagEngine.on('tagDeleted', forwardEvent('tag:deleted'));
  tagEngine.on('tagRenamed', forwardEvent('tag:renamed'));
  tagEngine.on('tagsMerged', forwardEvent('tags:merged'));
  tagEngine.on('tagsSynced', forwardEvent('tags:synced'));

  postMediaEngine.on('mediaLinked', forwardEvent('postMedia:linked'));
  postMediaEngine.on('mediaUnlinked', forwardEvent('postMedia:unlinked'));
  postMediaEngine.on('mediaBatchLinked', forwardEvent('postMedia:batchLinked'));
  postMediaEngine.on('mediaBatchUnlinked', forwardEvent('postMedia:batchUnlinked'));
  postMediaEngine.on('mediaReordered', forwardEvent('postMedia:reordered'));
  postMediaEngine.on('rebuilt', forwardEvent('postMedia:rebuilt'));

  taskManager.on('taskCreated', forwardEvent('task:created'));
  taskManager.on('taskStarted', forwardEvent('task:started'));
  taskManager.on('taskProgress', forwardEvent('task:progress'));
  taskManager.on('taskCompleted', forwardEvent('task:completed'));
  taskManager.on('taskFailed', forwardEvent('task:failed'));

  const scriptEngine = getScriptEngine();
  scriptEngine.on('scriptCreated', forwardEvent('script:created'));
  scriptEngine.on('scriptUpdated', forwardEvent('script:updated'));
  scriptEngine.on('scriptDeleted', forwardEvent('script:deleted'));
  scriptEngine.on('scriptsRebuilt', forwardEvent('scripts:rebuilt'));
}
