import { EventEmitter } from 'events';
import { v4 as uuidv4 } from 'uuid';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';
import { eq, and, gte, lte, lt, desc } from 'drizzle-orm';
import { app } from 'electron';
import { getDatabase } from '../database';
import { media, Media, NewMedia, postMedia } from '../database/schema';
import { stemText, stemQuery, SupportedLanguage } from './stemmer';
import { CliNotifier, NoopNotifier } from './CliNotifier';

// Thumbnail sizes
const THUMBNAIL_SIZES = {
  small: { width: 150, height: 150 },
  medium: { width: 400, height: 400 },
  large: { width: 800, height: 800 },
} as const;

type ThumbnailSize = keyof typeof THUMBNAIL_SIZES;
import { taskManager, Task } from './TaskManager';

export interface MediaData {
  id: string;
  filename: string;
  originalName: string;
  mimeType: string;
  size: number;
  width?: number;
  height?: number;
  title?: string;
  alt?: string;
  caption?: string;
  author?: string;
  createdAt: Date;
  updatedAt: Date;
  tags: string[];
  linkedPostIds?: string[]; // Posts this media is linked to
}

export interface MediaMetadata {
  id: string;
  originalName: string;
  mimeType: string;
  size: number;
  width?: number;
  height?: number;
  title?: string;
  alt?: string;
  caption?: string;
  author?: string;
  createdAt: string;
  updatedAt: string;
  tags: string[];
  linkedPostIds?: string[]; // Posts this media is linked to (persisted in sidecar)
}

export interface MediaFilter {
  tags?: string[];
  startDate?: Date;
  endDate?: Date;
  year?: number;
  month?: number;
}

export interface MediaSearchResult {
  id: string;
  originalName: string;
  title?: string;
  mimeType: string;
  createdAt: Date;
}

export class MediaEngine extends EventEmitter {
  private currentProjectId: string = 'default';
  private dataDir: string | null = null;      // For media files (may be external)
  private internalDir: string | null = null;   // For thumbnails (always local)
  private searchLanguage: SupportedLanguage = 'english';
  private readonly notifier: CliNotifier;

  constructor(notifier: CliNotifier = new NoopNotifier()) {
    super();
    this.notifier = notifier;
  }

  /** No persistent cache — DB is the source of truth. No-op for watcher compat. */
  invalidate(_entityId?: string): void {}

  /**
   * Set the language used for full-text search stemming.
   */
  setSearchLanguage(language: SupportedLanguage): void {
    this.searchLanguage = language;
  }

  /**
   * Get the current search language.
   */
  getSearchLanguage(): SupportedLanguage {
    return this.searchLanguage;
  }

  /**
   * Update the FTS index for a media item.
   * Stores stemmed content from original_name, title, alt, caption, and tags.
   */
  private async updateFTSIndex(item: {
    id: string;
    projectId: string;
    originalName: string;
    title?: string;
    alt?: string;
    caption?: string;
    tags: string[];
  }): Promise<void> {
    const client = getDatabase().getLocalClient();
    if (!client) return;

    // Delete existing entry
    await client.execute({ sql: 'DELETE FROM media_fts WHERE id = ?', args: [item.id] });

    // Combine all searchable fields and stem them
    const allText = [
      item.originalName,
      item.title || '',
      item.alt || '',
      item.caption || '',
      item.tags.join(' '),
    ].join(' ');

    const stemmedContent = stemText(allText, this.searchLanguage);

    // Insert with id, project_id, and stemmed content
    await client.execute({
      sql: 'INSERT INTO media_fts (id, project_id, content) VALUES (?, ?, ?)',
      args: [item.id, item.projectId, stemmedContent],
    });
  }

  /**
   * Delete a media item from the FTS index.
   */
  private async deleteFTSIndex(id: string): Promise<void> {
    const client = getDatabase().getLocalClient();
    if (!client) return;
    await client.execute({ sql: 'DELETE FROM media_fts WHERE id = ?', args: [id] });
  }

  private getDefaultBaseDir(): string {
    const userDataPath = app.getPath('userData');
    return path.join(userDataPath, 'projects', this.currentProjectId);
  }

  private getDataDir(): string {
    return this.dataDir || this.getDefaultBaseDir();
  }

  private getInternalDir(): string {
    return this.internalDir || this.getDefaultBaseDir();
  }

  private getMediaBaseDir(): string {
    return path.join(this.getDataDir(), 'media');
  }

  private getMediaDir(): string {
    // Kept for backwards compatibility - returns base media directory
    return this.getMediaBaseDir();
  }

  /**
   * Get the date-based directory for media based on its creation date.
   * Format: media/YYYY/MM/
   */
  private getMediaDirForDate(date: Date): string {
    const baseDir = this.getMediaBaseDir();
    const year = date.getFullYear().toString();
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    return path.join(baseDir, year, month);
  }

  /**
   * Get the full path for a media file based on id, extension, and date.
   * Returns: media/YYYY/MM/{id}.{ext}
   */
  getMediaPathForDate(id: string, ext: string, date: Date): string {
    const dir = this.getMediaDirForDate(date);
    const extension = ext.startsWith('.') ? ext : `.${ext}`;
    return path.join(dir, `${id}${extension}`);
  }

  setProjectContext(projectId: string, dataDir?: string, internalDir?: string): void {
    const nextDataDir = dataDir || null;
    const nextInternalDir = internalDir || null;
    if (
      this.currentProjectId === projectId
      && this.dataDir === nextDataDir
      && this.internalDir === nextInternalDir
    ) {
      return;
    }

    this.currentProjectId = projectId;
    this.dataDir = nextDataDir;
    this.internalDir = nextInternalDir;
    console.log(`[MediaEngine] setProjectContext: projectId=${projectId}, dataDir=${this.dataDir}, internalDir=${this.internalDir}`);
  }

  getProjectContext(): string {
    return this.currentProjectId;
  }

  private calculateChecksum(buffer: Buffer): string {
    return crypto.createHash('md5').update(buffer).digest('hex');
  }

  /**
   * Get the thumbnails directory for the current project
   */
  private getThumbnailsDir(): string {
    return path.join(this.getInternalDir(), 'thumbnails');
  }

  /**
   * Get the thumbnail subdirectory for a media item.
   * Uses first 2 characters of the media ID for organization.
   * Returns: thumbnails/xx/ where xx is the first 2 chars of mediaId
   */
  private getThumbnailSubDir(mediaId: string): string {
    const prefix = mediaId.substring(0, 2).toLowerCase();
    return path.join(this.getThumbnailsDir(), prefix);
  }

  /**
   * Generate thumbnails for an image file
   */
  async generateThumbnails(mediaId: string, sourcePath: string): Promise<Record<ThumbnailSize, string>> {
    const thumbnailSubDir = this.getThumbnailSubDir(mediaId);
    await fs.mkdir(thumbnailSubDir, { recursive: true });

    const thumbnails: Record<ThumbnailSize, string> = {} as Record<ThumbnailSize, string>;

    try {
      // Dynamic import of sharp (it's a native module)
      const sharp = (await import('sharp')).default;

      for (const [size, dimensions] of Object.entries(THUMBNAIL_SIZES) as [ThumbnailSize, { width: number; height: number }][]) {
        const thumbnailPath = path.join(thumbnailSubDir, `${mediaId}-${size}.webp`);

        await sharp(sourcePath)
          .resize(dimensions.width, dimensions.height, {
            fit: 'inside',
            withoutEnlargement: true,
          })
          .webp({ quality: 80 })
          .toFile(thumbnailPath);

        thumbnails[size] = thumbnailPath;
      }

      this.emit('thumbnailsGenerated', { mediaId, thumbnails });
    } catch (error) {
      console.error('Failed to generate thumbnails:', error);
      // Return empty thumbnails on error - non-critical failure
    }

    return thumbnails;
  }

  /**
   * Get existing thumbnail paths for a media item
   */
  async getThumbnailPaths(mediaId: string): Promise<Record<ThumbnailSize, string | null>> {
    const thumbnailSubDir = this.getThumbnailSubDir(mediaId);
    const result: Record<ThumbnailSize, string | null> = {
      small: null,
      medium: null,
      large: null,
    };

    for (const size of Object.keys(THUMBNAIL_SIZES) as ThumbnailSize[]) {
      const thumbnailPath = path.join(thumbnailSubDir, `${mediaId}-${size}.webp`);
      try {
        await fs.access(thumbnailPath);
        result[size] = thumbnailPath;
      } catch {
        // Thumbnail doesn't exist
      }
    }

    return result;
  }

  /**
   * Get thumbnail as base64 data URL for renderer
   */
  async getThumbnailDataUrl(mediaId: string, size: ThumbnailSize = 'small'): Promise<string | null> {
    const thumbnailSubDir = this.getThumbnailSubDir(mediaId);
    const thumbnailPath = path.join(thumbnailSubDir, `${mediaId}-${size}.webp`);

    try {
      const data = await fs.readFile(thumbnailPath);
      return `data:image/webp;base64,${data.toString('base64')}`;
    } catch {
      return null;
    }
  }

  /**
   * Delete thumbnails for a media item
   */
  private async deleteThumbnails(mediaId: string): Promise<void> {
    const thumbnailSubDir = this.getThumbnailSubDir(mediaId);

    for (const size of Object.keys(THUMBNAIL_SIZES) as ThumbnailSize[]) {
      const thumbnailPath = path.join(thumbnailSubDir, `${mediaId}-${size}.webp`);
      try {
        await fs.unlink(thumbnailPath);
      } catch {
        // Thumbnail doesn't exist, ignore
      }
    }
  }

  private async writeSidecarFile(mediaData: MediaData, mediaPath: string): Promise<string> {
    const sidecarPath = `${mediaPath}.meta`;
    
    const metadata: MediaMetadata = {
      id: mediaData.id,
      originalName: mediaData.originalName,
      mimeType: mediaData.mimeType,
      size: mediaData.size,
      width: mediaData.width,
      height: mediaData.height,
      title: mediaData.title,
      alt: mediaData.alt,
      caption: mediaData.caption,
      author: mediaData.author,
      createdAt: mediaData.createdAt.toISOString(),
      updatedAt: mediaData.updatedAt.toISOString(),
      tags: mediaData.tags,
      linkedPostIds: mediaData.linkedPostIds,
    };

    // Write YAML-like format consistent with posts
    const lines = [
      '---',
      `id: ${metadata.id}`,
      `originalName: "${metadata.originalName}"`,
      `mimeType: ${metadata.mimeType}`,
      `size: ${metadata.size}`,
    ];

    if (metadata.width) lines.push(`width: ${metadata.width}`);
    if (metadata.height) lines.push(`height: ${metadata.height}`);
    if (metadata.title) lines.push(`title: "${metadata.title}"`);
    if (metadata.alt) lines.push(`alt: "${metadata.alt}"`);
    if (metadata.caption) lines.push(`caption: "${metadata.caption}"`);
    if (metadata.author) lines.push(`author: "${metadata.author}"`);
    
    lines.push(`createdAt: ${metadata.createdAt}`);
    lines.push(`updatedAt: ${metadata.updatedAt}`);
    lines.push(`tags: [${metadata.tags.map(t => `"${t}"`).join(', ')}]`);
    if (metadata.linkedPostIds && metadata.linkedPostIds.length > 0) {
      lines.push(`linkedPostIds: [${metadata.linkedPostIds.map(id => `"${id}"`).join(', ')}]`);
    }
    lines.push('---');

    await fs.writeFile(sidecarPath, lines.join('\n'), 'utf-8');
    return sidecarPath;
  }

  private async readSidecarFile(sidecarPath: string): Promise<MediaMetadata | null> {
    try {
      // Check if file exists first to avoid noisy errors
      try {
        await fs.access(sidecarPath);
      } catch {
        // File doesn't exist - this is expected when DB has stale paths
        return null;
      }

      const content = await fs.readFile(sidecarPath, 'utf-8');
      const lines = content.split('\n');
      
      const metadata: Partial<MediaMetadata> = {
        tags: [],
        linkedPostIds: [],
      };

      for (const line of lines) {
        if (line === '---') continue;
        
        const colonIndex = line.indexOf(':');
        if (colonIndex === -1) continue;
        
        const key = line.substring(0, colonIndex).trim();
        let value = line.substring(colonIndex + 1).trim();
        
        // Remove quotes
        if (value.startsWith('"') && value.endsWith('"')) {
          value = value.slice(1, -1);
        }

        switch (key) {
          case 'id':
            metadata.id = value;
            break;
          case 'originalName':
            metadata.originalName = value;
            break;
          case 'mimeType':
            metadata.mimeType = value;
            break;
          case 'size':
            metadata.size = parseInt(value, 10);
            break;
          case 'width':
            metadata.width = parseInt(value, 10);
            break;
          case 'height':
            metadata.height = parseInt(value, 10);
            break;
          case 'title':
            metadata.title = value;
            break;
          case 'alt':
            metadata.alt = value;
            break;
          case 'caption':
            metadata.caption = value;
            break;
          case 'author':
            metadata.author = value;
            break;
          case 'createdAt':
            metadata.createdAt = value;
            break;
          case 'updatedAt':
            metadata.updatedAt = value;
            break;
          case 'tags':
            // Parse array format: ["tag1", "tag2"]
            const tagsMatch = value.match(/\[(.*)\]/);
            if (tagsMatch) {
              metadata.tags = tagsMatch[1]
                .split(',')
                .map(t => t.trim().replace(/"/g, ''))
                .filter(t => t.length > 0);
            }
            break;
          case 'linkedPostIds':
            // Parse array format: ["postId1", "postId2"]
            const postIdsMatch = value.match(/\[(.*)\]/);
            if (postIdsMatch) {
              metadata.linkedPostIds = postIdsMatch[1]
                .split(',')
                .map(id => id.trim().replace(/"/g, ''))
                .filter(id => id.length > 0);
            }
            break;
        }
      }

      if (!metadata.id || !metadata.originalName || !metadata.mimeType) {
        return null;
      }

      return metadata as MediaMetadata;
    } catch (error) {
      console.error(`Failed to parse sidecar file: ${sidecarPath}`, error);
      return null;
    }
  }

  private getMimeType(filename: string): string {
    const ext = path.extname(filename).toLowerCase();
    const mimeTypes: Record<string, string> = {
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.png': 'image/png',
      '.gif': 'image/gif',
      '.webp': 'image/webp',
      '.svg': 'image/svg+xml',
      '.bmp': 'image/bmp',
      '.ico': 'image/x-icon',
    };
    return mimeTypes[ext] || 'application/octet-stream';
  }

  async importMedia(sourcePath: string, metadata?: Partial<MediaData>): Promise<MediaData> {
    const db = getDatabase().getLocal();
    const id = uuidv4();
    const now = new Date();
    
    // Use provided createdAt date or current date
    const createdAt = metadata?.createdAt ?? now;
    const updatedAt = metadata?.updatedAt ?? now;
    
    const sourceBuffer = await fs.readFile(sourcePath);
    const originalName = path.basename(sourcePath);
    const ext = path.extname(originalName);
    const filename = `${id}${ext}`;
    
    // Use date-based directory structure (media/YYYY/MM/) based on createdAt
    const mediaDir = this.getMediaDirForDate(createdAt);
    await fs.mkdir(mediaDir, { recursive: true });
    const destPath = path.join(mediaDir, filename);

    // Copy file to media directory
    await fs.copyFile(sourcePath, destPath);

    const mimeType = metadata?.mimeType || this.getMimeType(originalName);
    let width = metadata?.width;
    let height = metadata?.height;

    // Get image dimensions using sharp if it's an image
    if (mimeType.startsWith('image/') && !mimeType.includes('svg')) {
      try {
        const sharp = (await import('sharp')).default;
        const imageMetadata = await sharp(destPath).metadata();
        width = imageMetadata.width;
        height = imageMetadata.height;
      } catch (error) {
        console.error('Failed to get image dimensions:', error);
      }
    }

    const mediaData: MediaData = {
      id,
      filename,
      originalName,
      mimeType,
      size: sourceBuffer.length,
      width,
      height,
      title: metadata?.title,
      alt: metadata?.alt,
      caption: metadata?.caption,
      author: metadata?.author,
      createdAt,
      updatedAt,
      tags: metadata?.tags || [],
    };

    const sidecarPath = await this.writeSidecarFile(mediaData, destPath);
    const checksum = this.calculateChecksum(sourceBuffer);

    // Generate thumbnails for images (async, non-blocking)
    if (mimeType.startsWith('image/') && !mimeType.includes('svg')) {
      this.generateThumbnails(id, destPath).catch(err => {
        console.error('Failed to generate thumbnails:', err);
      });
    }

    const dbMedia: NewMedia = {
      id: mediaData.id,
      projectId: this.currentProjectId,
      filename: mediaData.filename,
      originalName: mediaData.originalName,
      mimeType: mediaData.mimeType,
      size: mediaData.size,
      width: mediaData.width,
      height: mediaData.height,
      title: mediaData.title,
      alt: mediaData.alt,
      caption: mediaData.caption,
      author: mediaData.author,
      filePath: destPath,
      sidecarPath,
      createdAt: mediaData.createdAt,
      updatedAt: mediaData.updatedAt,
      checksum,
      tags: JSON.stringify(mediaData.tags),
    };

    await db.insert(media).values(dbMedia);

    // Update FTS index
    await this.updateFTSIndex({
      id: mediaData.id,
      projectId: this.currentProjectId,
      originalName: mediaData.originalName,
      title: mediaData.title,
      alt: mediaData.alt,
      caption: mediaData.caption,
      tags: mediaData.tags,
    });

    this.emit('mediaImported', mediaData);
    await this.notifier.notify('media', mediaData.id, 'created');
    return mediaData;
  }

  async updateMedia(id: string, data: Partial<MediaData>): Promise<MediaData | null> {
    const db = getDatabase().getLocal();
    const existing = await this.getMedia(id);
    
    if (!existing) {
      return null;
    }

    const updated: MediaData = {
      ...existing,
      ...data,
      id, // Ensure ID doesn't change
      updatedAt: new Date(),
    };

    const dbMedia = await db.select().from(media).where(eq(media.id, id)).get();
    if (!dbMedia) return null;

    await this.writeSidecarFile(updated, dbMedia.filePath);

    await db.update(media)
      .set({
        title: updated.title,
        alt: updated.alt,
        caption: updated.caption,
        author: updated.author,
        updatedAt: updated.updatedAt,
        tags: JSON.stringify(updated.tags),
      })
      .where(eq(media.id, id));

    // Update FTS index
    await this.updateFTSIndex({
      id: updated.id,
      projectId: this.currentProjectId,
      originalName: updated.originalName,
      title: updated.title,
      alt: updated.alt,
      caption: updated.caption,
      tags: updated.tags,
    });

    this.emit('mediaUpdated', updated);
    await this.notifier.notify('media', id, 'updated');
    return updated;
  }

  /**
   * Replace the actual file content for an existing media item.
   * This will:
   * - Check if the new file has a different checksum
   * - Replace the file if checksum differs
   * - Update size, dimensions (for images), and checksum in database
   * - Regenerate thumbnails for images
   * 
   * @returns The updated MediaData if file was replaced, null if media not found or checksum unchanged
   */
  async replaceMediaFile(id: string, newSourcePath: string): Promise<MediaData | null> {
    const db = getDatabase().getLocal();
    const dbMedia = await db.select().from(media).where(eq(media.id, id)).get();
    
    if (!dbMedia) {
      return null;
    }

    // Read the new source file
    const newBuffer = await fs.readFile(newSourcePath);
    const newChecksum = this.calculateChecksum(newBuffer);

    // If checksum is the same, no need to replace
    if (dbMedia.checksum === newChecksum) {
      return null;
    }

    // Copy new file to existing location
    await fs.copyFile(newSourcePath, dbMedia.filePath);

    // Get new dimensions for images
    let width = dbMedia.width;
    let height = dbMedia.height;
    if (dbMedia.mimeType.startsWith('image/') && !dbMedia.mimeType.includes('svg')) {
      try {
        const sharp = (await import('sharp')).default;
        const imageMetadata = await sharp(dbMedia.filePath).metadata();
        width = imageMetadata.width ?? width;
        height = imageMetadata.height ?? height;
      } catch (error) {
        console.error('Failed to get image dimensions:', error);
      }
    }

    const now = new Date();

    // Update database
    await db.update(media)
      .set({
        size: newBuffer.length,
        width,
        height,
        checksum: newChecksum,
        updatedAt: now,
      })
      .where(eq(media.id, id));

    // Regenerate thumbnails for images
    if (dbMedia.mimeType.startsWith('image/') && !dbMedia.mimeType.includes('svg')) {
      // Await thumbnail generation to ensure it completes before returning
      await this.generateThumbnails(id, dbMedia.filePath);
    }

    // Get the updated media data
    const updated = await this.getMedia(id);
    if (updated) {
      this.emit('mediaFileReplaced', updated);
    }

    return updated;
  }

  async deleteMedia(id: string): Promise<boolean> {
    const db = getDatabase().getLocal();
    const existing = await db.select().from(media).where(eq(media.id, id)).get();

    if (!existing) {
      return false;
    }

    // Delete media file
    try {
      await fs.unlink(existing.filePath);
    } catch {
      // File might not exist
    }

    // Delete sidecar file
    try {
      await fs.unlink(existing.sidecarPath);
    } catch {
      // File might not exist
    }

    // Delete thumbnails
    await this.deleteThumbnails(id);

    // Delete post-media links (cascade cleanup)
    const { postMedia } = await import('../database/schema');
    await db.delete(postMedia).where(eq(postMedia.mediaId, id));

    await db.delete(media).where(eq(media.id, id));

    // Delete from FTS index
    await this.deleteFTSIndex(id);

    this.emit('mediaDeleted', id);
    await this.notifier.notify('media', id, 'deleted');
    return true;
  }

  async getMedia(id: string): Promise<MediaData | null> {
    const db = getDatabase().getLocal();
    const dbMedia = await db.select().from(media).where(eq(media.id, id)).get();
    
    if (!dbMedia) {
      return null;
    }

    return {
      id: dbMedia.id,
      filename: dbMedia.filename,
      originalName: dbMedia.originalName,
      mimeType: dbMedia.mimeType,
      size: dbMedia.size,
      width: dbMedia.width || undefined,
      height: dbMedia.height || undefined,
      title: dbMedia.title || undefined,
      alt: dbMedia.alt || undefined,
      caption: dbMedia.caption || undefined,
      author: dbMedia.author || undefined,
      createdAt: dbMedia.createdAt,
      updatedAt: dbMedia.updatedAt,
      tags: JSON.parse(dbMedia.tags || '[]'),
    };
  }

  async getAllMedia(): Promise<MediaData[]> {
    const db = getDatabase().getLocal();
    const dbMediaList = await db
      .select()
      .from(media)
      .where(eq(media.projectId, this.currentProjectId))
      .orderBy(desc(media.createdAt))
      .all();

    return dbMediaList.map(dbMedia => ({
      id: dbMedia.id,
      filename: dbMedia.filename,
      originalName: dbMedia.originalName,
      mimeType: dbMedia.mimeType,
      size: dbMedia.size,
      width: dbMedia.width || undefined,
      height: dbMedia.height || undefined,
      title: dbMedia.title || undefined,
      alt: dbMedia.alt || undefined,
      caption: dbMedia.caption || undefined,
      author: dbMedia.author || undefined,
      createdAt: dbMedia.createdAt,
      updatedAt: dbMedia.updatedAt,
      tags: JSON.parse(dbMedia.tags || '[]'),
    }));
  }

  async getMediaFiltered(filter: MediaFilter): Promise<MediaData[]> {
    const db = getDatabase().getLocal();
    const conditions = [eq(media.projectId, this.currentProjectId)];

    console.log(`[MediaEngine] getMediaFiltered called with filter:`, JSON.stringify(filter));

    if (filter.startDate) {
      conditions.push(gte(media.createdAt, filter.startDate));
    }

    if (filter.endDate) {
      conditions.push(lte(media.createdAt, filter.endDate));
    }

    if (filter.year !== undefined) {
      // Use UTC dates to avoid timezone issues
      const startOfYear = new Date(Date.UTC(filter.year, 0, 1));
      const endOfYear = new Date(Date.UTC(filter.year + 1, 0, 1));
      console.log(`[MediaEngine] Year filter: ${startOfYear.toISOString()} to ${endOfYear.toISOString()}`);
      conditions.push(gte(media.createdAt, startOfYear));
      conditions.push(lt(media.createdAt, endOfYear));
    }

    if (filter.month !== undefined && filter.year !== undefined) {
      // Use UTC dates to avoid timezone issues (filter.month is 1-indexed)
      const startOfMonth = new Date(Date.UTC(filter.year, filter.month - 1, 1));
      const endOfMonth = new Date(Date.UTC(filter.year, filter.month, 1));
      console.log(`[MediaEngine] Month filter: ${startOfMonth.toISOString()} to ${endOfMonth.toISOString()}`);
      conditions.push(gte(media.createdAt, startOfMonth));
      conditions.push(lt(media.createdAt, endOfMonth));
    }

    const dbMediaList = await db
      .select()
      .from(media)
      .where(and(...conditions))
      .orderBy(desc(media.createdAt))
      .all();

    console.log(`[MediaEngine] Query returned ${dbMediaList.length} media items`);

    let result: MediaData[] = [];

    for (const dbMedia of dbMediaList) {
      const mediaData: MediaData = {
        id: dbMedia.id,
        filename: dbMedia.filename,
        originalName: dbMedia.originalName,
        mimeType: dbMedia.mimeType,
        size: dbMedia.size,
        width: dbMedia.width || undefined,
        height: dbMedia.height || undefined,
        title: dbMedia.title || undefined,
        alt: dbMedia.alt || undefined,
        caption: dbMedia.caption || undefined,
        author: dbMedia.author || undefined,
        createdAt: dbMedia.createdAt,
        updatedAt: dbMedia.updatedAt,
        tags: JSON.parse(dbMedia.tags || '[]'),
      };

      // Client-side filtering for tags (JSON array)
      if (filter.tags && filter.tags.length > 0) {
        const hasAllTags = filter.tags.every(tag => mediaData.tags.includes(tag));
        if (!hasAllTags) continue;
      }

      result.push(mediaData);
    }

    return result;
  }

  async searchMedia(query: string): Promise<MediaSearchResult[]> {
    const client = getDatabase().getLocalClient();
    if (!client) return [];

    try {
      // Stem the query for multilingual matching
      const stemmedQuery = stemQuery(query, this.searchLanguage);
      
      // Search the stemmed content, filtered by project_id for project isolation
      const result = await client.execute({
        sql: `SELECT id FROM media_fts WHERE project_id = ? AND media_fts MATCH ? ORDER BY rank LIMIT 50`,
        args: [this.currentProjectId, stemmedQuery],
      });

      // Fetch actual media data for results
      const db = getDatabase().getLocal();
      const searchResults: MediaSearchResult[] = [];
      
      for (const row of result.rows) {
        const mediaId = row.id as string;
        const item = await db.select().from(media).where(eq(media.id, mediaId)).get();
        if (item) {
          searchResults.push({
            id: item.id,
            originalName: item.originalName,
            title: item.title || undefined,
            mimeType: item.mimeType,
            createdAt: item.createdAt,
          });
        }
      }

      return searchResults;
    } catch (error) {
      console.error('Media search failed:', error);
      return [];
    }
  }

  async getMediaByYearMonth(): Promise<{ year: number; month: number; count: number }[]> {
    const allMedia = await this.getAllMedia();
    const counts = new Map<string, { year: number; month: number; count: number }>();

    for (const item of allMedia) {
      const year = item.createdAt.getFullYear();
      const month = item.createdAt.getMonth() + 1; // 1-indexed
      const key = `${year}-${month}`;
      const current = counts.get(key) || { year, month, count: 0 };
      current.count++;
      counts.set(key, current);
    }

    return Array.from(counts.values()).sort((a, b) => {
      if (a.year !== b.year) return b.year - a.year;
      return b.month - a.month;
    });
  }

  async getAvailableTags(): Promise<string[]> {
    const allMedia = await this.getAllMedia();
    const tags = new Set<string>();
    for (const item of allMedia) {
      for (const tag of item.tags) {
        tags.add(tag);
      }
    }
    return Array.from(tags).sort();
  }

  async getTagsWithCounts(): Promise<{ tag: string; count: number }[]> {
    const db = getDatabase().getLocal();
    const dbMediaList = await db
      .select({ tags: media.tags })
      .from(media)
      .where(eq(media.projectId, this.currentProjectId))
      .all();

    const tagCounts = new Map<string, number>();
    for (const row of dbMediaList) {
      const parsed: string[] = JSON.parse(row.tags || '[]');
      for (const tag of parsed) {
        tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
      }
    }

    return Array.from(tagCounts.entries())
      .map(([tag, count]) => ({ tag, count }))
      .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
  }

  getMediaPath(id: string): string {
    return path.join(this.getMediaDir(), id);
  }

  /**
   * Get the relative path for a media item (e.g. media/2025/01/uuid.jpg).
   * This is the path format used in markdown content for image references.
   */
  async getRelativePath(id: string): Promise<string | null> {
    const db = getDatabase().getLocal();
    const dbMedia = await db.select().from(media).where(eq(media.id, id)).get();
    if (!dbMedia?.filePath) return null;
    const dataDir = this.getDataDir();
    const relativePath = path.relative(dataDir, dbMedia.filePath);
    return relativePath.replace(/\\/g, '/');
  }

  async rebuildDatabaseFromFiles(): Promise<void> {
    const mediaBaseDir = this.getMediaBaseDir();
    console.log(`[MediaEngine] rebuildDatabaseFromFiles: scanning mediaBaseDir=${mediaBaseDir}`);
    const task: Task<void> = {
      id: uuidv4(),
      name: 'Rebuild database from media files',
      execute: async (onProgress) => {
        const db = getDatabase().getLocal();

        onProgress(0, 'Deleting existing media for project...');

        // Notify UI that rebuild is starting so it can clear the list
        this.emit('rebuildStarted');

        // Delete all media for the current project - clean slate rebuild
        const existingMedia = await db.select({ id: media.id }).from(media).where(eq(media.projectId, this.currentProjectId)).all();
        if (existingMedia.length > 0) {
          await db.delete(media).where(eq(media.projectId, this.currentProjectId));
          console.log(`Deleted ${existingMedia.length} existing media record(s) for project ${this.currentProjectId}`);
        }

        // Also delete all post-media links for the current project
        await db.delete(postMedia).where(eq(postMedia.projectId, this.currentProjectId));
        console.log(`Deleted post-media links for project ${this.currentProjectId}`);

        // Delete all FTS entries for the current project
        const client = getDatabase().getLocalClient();
        if (client) {
          await client.execute({
            sql: 'DELETE FROM media_fts WHERE project_id = ?',
            args: [this.currentProjectId],
          });
          console.log(`Deleted media FTS entries for project ${this.currentProjectId}`);
        }

        onProgress(5, 'Scanning media directory...');

        // Recursively find all .meta files in the media directory tree
        const metaFiles: string[] = [];
        const scanDir = async (dir: string) => {
          try {
            const entries = await fs.readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
              const fullPath = path.join(dir, entry.name);
              if (entry.isDirectory()) {
                await scanDir(fullPath);
              } else if (entry.name.endsWith('.meta')) {
                metaFiles.push(fullPath);
              }
            }
          } catch {
            // Directory might not exist
          }
        };

        try {
          await fs.mkdir(mediaBaseDir, { recursive: true });
        } catch {
          // Already exists
        }
        await scanDir(mediaBaseDir);

        onProgress(10, `Found ${metaFiles.length} media sidecar files`);

        for (let i = 0; i < metaFiles.length; i++) {
          const sidecarPath = metaFiles[i];
          const mediaFilePath = sidecarPath.replace('.meta', '');
          const metaFileName = path.basename(sidecarPath);

          onProgress(10 + (80 * (i / metaFiles.length)), `Processing ${i + 1}/${metaFiles.length}: ${metaFileName}`);

          const metadata = await this.readSidecarFile(sidecarPath);

          if (metadata) {
            try {
              const stats = await fs.stat(mediaFilePath);
              const buffer = await fs.readFile(mediaFilePath);
              const checksum = this.calculateChecksum(buffer);
              const filename = path.basename(mediaFilePath);

              // Insert fresh - we deleted all records at the start
              await db.insert(media).values({
                id: metadata.id,
                projectId: this.currentProjectId,
                filename,
                originalName: metadata.originalName,
                mimeType: metadata.mimeType,
                size: stats.size,
                width: metadata.width,
                height: metadata.height,
                title: metadata.title,
                alt: metadata.alt,
                caption: metadata.caption,
                filePath: mediaFilePath,
                sidecarPath,
                createdAt: new Date(metadata.createdAt),
                updatedAt: new Date(metadata.updatedAt),
                checksum,
                tags: JSON.stringify(metadata.tags),
              });

              // Update FTS index
              await this.updateFTSIndex({
                id: metadata.id,
                projectId: this.currentProjectId,
                originalName: metadata.originalName,
                title: metadata.title,
                alt: metadata.alt,
                caption: metadata.caption,
                tags: metadata.tags,
              });

              // Insert post-media links based on linkedPostIds from sidecar
              const linkedPostIds = metadata.linkedPostIds || [];
              for (let j = 0; j < linkedPostIds.length; j++) {
                const postId = linkedPostIds[j];
                await db.insert(postMedia).values({
                  id: uuidv4(),
                  projectId: this.currentProjectId,
                  postId,
                  mediaId: metadata.id,
                  sortOrder: j,
                  createdAt: new Date(),
                });
              }
            } catch (error) {
              console.error(`Media file not found for sidecar: ${sidecarPath}`, error);
            }
          }

          // Yield to event loop periodically so the window stays responsive
          if (i % 10 === 0) {
            await new Promise(resolve => setImmediate(resolve));
          }
        }

        onProgress(100, 'Database rebuild complete');
        this.emit('databaseRebuilt');
      },
    };

    await taskManager.runTask(task);
  }

  /**
   * Regenerate missing thumbnails for all image media.
   * Useful for media imported externally without thumbnails.
   */
  async regenerateMissingThumbnails(): Promise<{ processed: number; generated: number; failed: number }> {
    const result = { processed: 0, generated: 0, failed: 0 };

    const task: Task<{ processed: number; generated: number; failed: number }> = {
      id: uuidv4(),
      name: 'Generate missing thumbnails',
      execute: async (onProgress) => {
        const db = getDatabase().getLocal();

        onProgress(0, 'Finding images without thumbnails...');

        // Get all image media for current project
        const allMedia = await db
          .select()
          .from(media)
          .where(eq(media.projectId, this.currentProjectId))
          .all();

        // Filter to images only (not SVG - they don't need thumbnails)
        const imageMedia = allMedia.filter(
          m => m.mimeType.startsWith('image/') && !m.mimeType.includes('svg')
        );

        if (imageMedia.length === 0) {
          onProgress(100, 'No images found');
          return result;
        }

        onProgress(5, `Checking ${imageMedia.length} images...`);

        // Find which ones are missing thumbnails
        const missingThumbnails: typeof imageMedia = [];
        for (const item of imageMedia) {
          const thumbnails = await this.getThumbnailPaths(item.id);
          // Consider missing if any size is missing
          if (!thumbnails.small || !thumbnails.medium || !thumbnails.large) {
            missingThumbnails.push(item);
          }
        }

        if (missingThumbnails.length === 0) {
          onProgress(100, 'All thumbnails exist');
          return result;
        }

        onProgress(10, `Generating thumbnails for ${missingThumbnails.length} images...`);

        for (let i = 0; i < missingThumbnails.length; i++) {
          const item = missingThumbnails[i];
          result.processed++;

          const progress = 10 + (90 * ((i + 1) / missingThumbnails.length));
          onProgress(progress, `Processing ${i + 1}/${missingThumbnails.length}: ${item.originalName}`);

          try {
            // Verify the source file exists
            await fs.access(item.filePath);
            
            const thumbnails = await this.generateThumbnails(item.id, item.filePath);
            
            // Check if thumbnails were actually generated
            if (Object.keys(thumbnails).length > 0) {
              result.generated++;
            } else {
              result.failed++;
            }
          } catch (error) {
            console.error(`Failed to generate thumbnails for ${item.id}:`, error);
            result.failed++;
          }

          // Yield to event loop periodically
          if (i % 5 === 0) {
            await new Promise(resolve => setImmediate(resolve));
          }
        }

        onProgress(100, `Generated ${result.generated} thumbnails, ${result.failed} failed`);
        this.emit('thumbnailsRegenerated', result);
        return result;
      },
    };

    return await taskManager.runTask(task);
  }

  /**
   * Reindex all text for full-text search.
   * Runs as a background task with progress updates.
   * Call this when search algorithms change or to fix search issues.
   */
  async reindexText(): Promise<void> {
    const task: Task<void> = {
      id: uuidv4(),
      name: 'Reindex media search text',
      execute: async (onProgress) => {
        const client = getDatabase().getLocalClient();
        if (!client) {
          throw new Error('Database client not available');
        }

        onProgress(0, 'Clearing existing media search index...');

        // Clear the FTS entries for current project
        await client.execute({
          sql: 'DELETE FROM media_fts WHERE project_id = ?',
          args: [this.currentProjectId],
        });

        onProgress(5, 'Loading media...');

        const allMedia = await this.getAllMedia();
        const total = allMedia.length;

        if (total === 0) {
          onProgress(100, 'No media to index');
          return;
        }

        onProgress(10, `Indexing ${total} media items...`);

        for (let i = 0; i < allMedia.length; i++) {
          const item = allMedia[i];
          await this.updateFTSIndex({
            id: item.id,
            projectId: this.currentProjectId,
            originalName: item.originalName,
            alt: item.alt,
            caption: item.caption,
            tags: item.tags,
          });

          // Update progress (10% to 100%)
          const progress = 10 + Math.round((i + 1) / total * 90);
          if (i % 10 === 0 || i === allMedia.length - 1) {
            onProgress(progress, `Indexed ${i + 1} of ${total} media items`);
          }

          // Yield to event loop periodically
          if (i % 20 === 0) {
            await new Promise(resolve => setImmediate(resolve));
          }
        }

        onProgress(100, `Reindexed ${total} media items`);
        console.log(`Reindexed search text for ${total} media items`);
      },
    };

    await taskManager.runTask(task);
  }
}


