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 matter from 'gray-matter';
import { eq, and, desc, gte, lte, like, inArray, ne, sql } from 'drizzle-orm';
import { app } from 'electron';
import { getDatabase } from '../database';
import { posts, Post, NewPost, postLinks } from '../database/schema';
import { taskManager, Task } from './TaskManager';
import { stemText, stemQuery, SupportedLanguage } from './stemmer';
import { readPostFile as readPostFileShared, type PostFileData } from './postFileUtils';
import { CliNotifier, NoopNotifier } from './CliNotifier';
import type { MediaEngine } from './MediaEngine';
import { slugify } from './slugify';

export interface PostData {
  id: string;
  projectId: string;
  title: string;
  slug: string;
  excerpt?: string;
  content: string;
  status: 'draft' | 'published' | 'archived';
  author?: string;
  createdAt: Date;
  updatedAt: Date;
  publishedAt?: Date;
  tags: string[];
  categories: string[];
}

export interface PostMetadata {
  id: string;
  projectId: string;
  title: string;
  slug: string;
  excerpt?: string;
  status: 'draft' | 'published' | 'archived';
  author?: string;
  createdAt: string;
  updatedAt: string;
  publishedAt?: string;
  tags: string[];
  categories: string[];
}

export interface SearchResult {
  id: string;
  title: string;
  slug: string;
  excerpt?: string;
}

export interface PostFilter {
  status?: 'draft' | 'published' | 'archived';
  tags?: string[];
  categories?: string[];
  excludeCategories?: string[];
  startDate?: Date;
  endDate?: Date;
  year?: number;
  month?: number;
}

export interface PaginatedResult<T> {
  items: T[];
  hasMore: boolean;
  total: number;
}

export interface PaginationOptions {
  limit?: number;
  offset?: number;
}

export type GitPostFileChangeStatus = 'added' | 'modified' | 'deleted' | 'renamed';

export interface GitPostFileChange {
  status: GitPostFileChangeStatus;
  path: string;
  previousPath?: string;
}

export interface PublishedPostReconcileResult {
  created: number;
  updated: number;
  deleted: number;
  processedFiles: number;
}

export class PostEngine extends EventEmitter {
  private currentProjectId: string = 'default';
  private searchLanguage: SupportedLanguage = 'english';
  private readonly notifier: CliNotifier;
  private readonly mediaEngine: MediaEngine | undefined;

  constructor(opts: { notifier?: CliNotifier; mediaEngine?: MediaEngine } = {}) {
    super();
    this.notifier = opts.notifier ?? new NoopNotifier();
    this.mediaEngine = opts.mediaEngine;
  }

  /** 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.
   * Affects both indexing and query processing.
   */
  setSearchLanguage(language: SupportedLanguage): void {
    this.searchLanguage = language;
  }

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

  /**
   * Update the FTS index for a post.
   * Updates the FTS index for a post.
   * Stores the stemmed content (combining title, excerpt, content, tags, categories).
   * Includes project_id for project-scoped search.
   * Only the post ID is returned from searches - actual post data comes from DB/files.
   * Public to allow ImportExecutionEngine to index imported posts directly.
   */
  async updateFTSIndex(post: {
    id: string;
    projectId: string;
    title: string;
    content: string;
    excerpt?: string;
    tags: string[];
    categories: string[];
  }): Promise<void> {
    const client = getDatabase().getLocalClient();
    if (!client) return;

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

    // Combine all searchable fields and stem them
    const allText = [
      post.title,
      post.excerpt || '',
      post.content,
      post.tags.join(' '),
      post.categories.join(' '),
    ].join(' ');

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

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

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

  private dataDir: string | null = null;

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

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

  private getPostsDir(): string {
    // Kept for backwards compatibility - returns base posts directory
    return this.getPostsBaseDir();
  }

  /**
   * Get the date-based directory for a post based on its creation date.
   * Format: posts/YYYY/MM/
   */
  private getPostsDirForDate(date: Date): string {
    const baseDir = this.getPostsBaseDir();
    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 post file based on slug and date.
   * Returns: posts/YYYY/MM/{slug}.md
   */
  getPostPath(slug: string, date: Date): string {
    const dir = this.getPostsDirForDate(date);
    return path.join(dir, `${slug}.md`);
  }

  setProjectContext(projectId: string, dataDir?: string): void {
    this.currentProjectId = projectId;
    this.dataDir = dataDir || null;
  }

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

  private generateSlug(title: string): string {
    return slugify(title);
  }

  /**
   * Check if a slug is available (not used by any existing post)
   * @param slug The slug to check
   * @param excludePostId Optional post ID to exclude (for updates)
   */
  async isSlugAvailable(slug: string, excludePostId?: string): Promise<boolean> {
    const db = getDatabase().getLocal();
    const existing = await db
      .select({ id: posts.id })
      .from(posts)
      .where(and(
        eq(posts.slug, slug),
        eq(posts.projectId, this.currentProjectId)
      ))
      .get();
    
    if (!existing) return true;
    if (excludePostId && existing.id === excludePostId) return true;
    return false;
  }

  /**
   * Generate a unique slug based on a title
   * If the slug already exists, appends -2, -3, etc.
   */
  async generateUniqueSlug(title: string, excludePostId?: string): Promise<string> {
    const baseSlug = this.generateSlug(title || 'untitled');
    
    if (await this.isSlugAvailable(baseSlug, excludePostId)) {
      return baseSlug;
    }

    // Find next available number
    let counter = 2;
    while (counter < 1000) {
      const candidateSlug = `${baseSlug}-${counter}`;
      if (await this.isSlugAvailable(candidateSlug, excludePostId)) {
        return candidateSlug;
      }
      counter++;
    }

    // Fallback: add timestamp
    return `${baseSlug}-${Date.now()}`;
  }

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

  private normalizePathForCompare(filePath: string): string {
    return path.resolve(filePath).replace(/\\/g, '/');
  }

  private isMarkdownPostPath(value: string): boolean {
    const normalized = value.replace(/\\/g, '/').replace(/^\.\//, '');
    if (!normalized.startsWith('posts/')) {
      return false;
    }

    const extension = path.extname(normalized).toLowerCase();
    return extension === '.md' || extension === '.markdown' || extension === '.mdx';
  }

  private async ensureUniquePostIdentity(id: string, slug: string): Promise<{ id: string; slug: string }> {
    const uniqueId = id.trim().length > 0 ? id.trim() : uuidv4();
    const safeSlug = slug.trim().length > 0 ? slug.trim() : await this.generateUniqueSlug('untitled');

    const db = getDatabase().getLocal();

    const existingById = await db
      .select({ id: posts.id })
      .from(posts)
      .where(eq(posts.id, uniqueId))
      .get();

    const finalId = existingById ? uuidv4() : uniqueId;

    const slugAvailable = await this.isSlugAvailable(safeSlug);
    const finalSlug = slugAvailable ? safeSlug : await this.generateUniqueSlug(safeSlug);

    return { id: finalId, slug: finalSlug };
  }

  private async writePostFile(post: PostData): Promise<string> {
    const metadata: Record<string, unknown> = {
      id: post.id,
      title: post.title,
      slug: post.slug,
      status: post.status,
      createdAt: post.createdAt.toISOString(),
      updatedAt: post.updatedAt.toISOString(),
      tags: post.tags,
      categories: post.categories,
    };

    // Only add optional fields if they have values (gray-matter can't serialize undefined)
    if (post.excerpt) metadata.excerpt = post.excerpt;
    if (post.author) metadata.author = post.author;
    if (post.publishedAt) metadata.publishedAt = post.publishedAt.toISOString();

    // Use date-based directory structure (posts/YYYY/MM/)
    const postsDir = this.getPostsDirForDate(post.createdAt);
    await fs.mkdir(postsDir, { recursive: true });
    
    const fileContent = matter.stringify(post.content, metadata);
    const filePath = path.join(postsDir, `${post.slug}.md`);
    
    await fs.writeFile(filePath, fileContent, 'utf-8');
    return filePath;
  }

  private async readPostFile(filePath: string): Promise<PostData | null> {
    const data = await readPostFileShared(filePath);
    if (!data) return null;

    const fileStem = path.parse(filePath).name;
    const normalizedTitle = typeof data.title === 'string' && data.title.trim().length > 0
      ? data.title.trim()
      : fileStem;
    const baseSlugSource = typeof data.slug === 'string' && data.slug.trim().length > 0
      ? data.slug.trim()
      : normalizedTitle;
    const normalizedSlug = this.generateSlug(baseSlugSource) || this.generateSlug(fileStem) || uuidv4();

    const createdAt = data.createdAt instanceof Date && !Number.isNaN(data.createdAt.getTime())
      ? data.createdAt
      : (data.updatedAt instanceof Date && !Number.isNaN(data.updatedAt.getTime()) ? data.updatedAt : new Date());
    const updatedAt = data.updatedAt instanceof Date && !Number.isNaN(data.updatedAt.getTime())
      ? data.updatedAt
      : createdAt;

    const normalizedTags = Array.isArray(data.tags)
      ? data.tags.filter((tag): tag is string => typeof tag === 'string')
      : [];
    const normalizedCategories = Array.isArray(data.categories)
      ? data.categories.filter((category): category is string => typeof category === 'string')
      : [];
    
    return {
      ...data,
      id: typeof data.id === 'string' && data.id.trim().length > 0 ? data.id : uuidv4(),
      projectId: this.currentProjectId,
      title: normalizedTitle,
      slug: normalizedSlug,
      createdAt,
      updatedAt,
      tags: normalizedTags,
      categories: normalizedCategories,
    };
  }

  async createPost(data: Partial<PostData>): Promise<PostData> {
    const db = getDatabase().getLocal();
    const client = getDatabase().getLocalClient();
    const now = new Date();
    const id = uuidv4();
    
    // Use provided slug or generate a unique one from title
    const slug = data.slug 
      ? (await this.isSlugAvailable(data.slug) ? data.slug : await this.generateUniqueSlug(data.title || 'untitled'))
      : await this.generateUniqueSlug(data.title || 'untitled');

    const post: PostData = {
      id,
      projectId: data.projectId || this.currentProjectId,
      title: data.title ?? '',
      slug,
      excerpt: data.excerpt,
      content: data.content || '',
      status: data.status || 'draft',
      author: data.author,
      createdAt: now,
      updatedAt: now,
      publishedAt: data.publishedAt,
      tags: data.tags || [],
      categories: data.categories || [],
    };

    const checksum = this.calculateChecksum(post.content);

    // Draft content lives in the database only — no file written
    const dbPost: NewPost = {
      id: post.id,
      projectId: post.projectId,
      title: post.title,
      slug: post.slug,
      excerpt: post.excerpt,
      content: post.content,
      status: post.status,
      author: post.author,
      createdAt: post.createdAt,
      updatedAt: post.updatedAt,
      publishedAt: post.publishedAt,
      filePath: '',
      checksum,
      tags: JSON.stringify(post.tags),
      categories: JSON.stringify(post.categories),
    };

    await db.insert(posts).values(dbPost);

    // Update FTS index
    await this.updateFTSIndex(post);

    this.emit('postCreated', post);
    await this.notifier.notify('post', post.id, 'created');
    return post;
  }

  async updatePost(id: string, data: Partial<PostData>): Promise<PostData | null> {
    const db = getDatabase().getLocal();
    const client = getDatabase().getLocalClient();
    const existing = await this.getPost(id);
    
    if (!existing) {
      return null;
    }

    // If post is currently published and content/metadata is changing,
    // automatically transition to draft status (content moves from file to DB)
    const isContentOrMetadataChange = data.content !== undefined ||
                                       data.title !== undefined ||
                                       data.tags !== undefined ||
                                       data.categories !== undefined ||
                                       data.excerpt !== undefined;

    let newStatus = data.status || existing.status;
    if (existing.status === 'published' && isContentOrMetadataChange && !data.status) {
      newStatus = 'draft';
    }

    // Auto-update slug when title changes, but only if post was never published
    let newSlug = data.slug ?? existing.slug;
    if (data.title !== undefined && data.title !== existing.title && !existing.publishedAt) {
      newSlug = await this.generateUniqueSlug(data.title || 'untitled', id);
    }

    const updated: PostData = {
      ...existing,
      ...data,
      id, // Ensure ID doesn't change
      projectId: existing.projectId, // Ensure projectId doesn't change
      slug: newSlug,
      status: newStatus as 'draft' | 'published' | 'archived',
      updatedAt: new Date(),
    };

    const checksum = this.calculateChecksum(updated.content);

    // All updates go to DB only — no file writes
    await db.update(posts)
      .set({
        title: updated.title,
        slug: updated.slug,
        excerpt: updated.excerpt,
        content: updated.content,
        status: updated.status,
        author: updated.author,
        updatedAt: updated.updatedAt,
        publishedAt: updated.publishedAt,
        checksum,
        tags: JSON.stringify(updated.tags),
        categories: JSON.stringify(updated.categories),
      })
      .where(eq(posts.id, id));

    // Update FTS index
    await this.updateFTSIndex(updated);

    // Update post links if content changed
    if (data.content) {
      await this.updatePostLinks(id, updated.content);
    }

    this.emit('postUpdated', updated);
    await this.notifier.notify('post', id, 'updated');
    return updated;
  }

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

    if (!existing) {
      return false;
    }

    // Only delete file if the post was published (has a file on disk)
    if (existing.filePath) {
      try {
        await fs.unlink(existing.filePath);
      } catch {
        // File might not exist
      }
    }

    // Delete post links
    await db.delete(postLinks).where(eq(postLinks.sourcePostId, id));
    await db.delete(postLinks).where(eq(postLinks.targetPostId, id));

    // Delete post-media links and update media sidecars
    const { postMedia } = await import('../database/schema');
    const linkedMediaResult = await db.select().from(postMedia).where(eq(postMedia.postId, id));
    const linkedMedia = Array.isArray(linkedMediaResult) ? linkedMediaResult : [];

    // Remove this post from each linked media's sidecar.
    // Requires mediaEngine to be injected at construction time.
    if (linkedMedia.length > 0 && this.mediaEngine) {
      for (const link of linkedMedia) {
        const media = await this.mediaEngine.getMedia(link.mediaId);
        if (media && media.linkedPostIds) {
          const updatedLinkedPostIds = media.linkedPostIds.filter(pid => pid !== id);
          await this.mediaEngine.updateMedia(link.mediaId, { linkedPostIds: updatedLinkedPostIds });
        }
      }
    } else if (linkedMedia.length > 0) {
      // Fallback: lazy-import (app singleton path, pre-DI callers)
      const { MediaEngine: ME } = await import('./MediaEngine');
      const fallbackEngine = new ME();
      for (const link of linkedMedia) {
        const media = await fallbackEngine.getMedia(link.mediaId);
        if (media && media.linkedPostIds) {
          const updatedLinkedPostIds = media.linkedPostIds.filter(pid => pid !== id);
          await fallbackEngine.updateMedia(link.mediaId, { linkedPostIds: updatedLinkedPostIds });
        }
      }
    }
    // Delete post-media junction entries
    await db.delete(postMedia).where(eq(postMedia.postId, id));

    // Delete from database
    await db.delete(posts).where(eq(posts.id, id));

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

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

  /**
   * Build a PostData object from a DB row, using the given body content.
   */
  private dbRowToPostData(dbPost: Post, body: string): PostData {
    return {
      id: dbPost.id,
      projectId: dbPost.projectId,
      title: dbPost.title,
      slug: dbPost.slug,
      excerpt: dbPost.excerpt || undefined,
      content: body,
      status: dbPost.status as 'draft' | 'published' | 'archived',
      author: dbPost.author || undefined,
      createdAt: dbPost.createdAt,
      updatedAt: dbPost.updatedAt,
      publishedAt: dbPost.publishedAt || undefined,
      tags: JSON.parse(dbPost.tags || '[]'),
      categories: JSON.parse(dbPost.categories || '[]'),
    };
  }

  async getPost(id: string): Promise<PostData | null> {
    const db = getDatabase().getLocal();
    const dbPost = await db.select().from(posts).where(eq(posts.id, id)).get();
    
    if (!dbPost) {
      return null;
    }

    // Draft content lives in the DB
    if (dbPost.content) {
      return this.dbRowToPostData(dbPost, dbPost.content);
    }

    // Published content lives in the filesystem
    if (dbPost.filePath) {
      const fileData = await this.readPostFile(dbPost.filePath);
      if (fileData) {
        return this.dbRowToPostData(dbPost, fileData.content);
      }
    }

    // Fallback: no content available
    return this.dbRowToPostData(dbPost, '');
  }

  /**
   * Sync a published post's file with current database metadata (e.g., tags).
   * This is needed when metadata changes outside of normal post editing flow,
   * such as tag merge or rename operations.
   *
   * @param postId - The post ID to sync
   * @returns true if file was updated, false if post is not published or doesn't exist
   */
  async syncPublishedPostFile(postId: string): Promise<boolean> {
    const db = getDatabase().getLocal();
    const dbPost = await db.select().from(posts).where(eq(posts.id, postId)).get();

    if (!dbPost || !dbPost.filePath) {
      // Not a published post or doesn't exist
      return false;
    }

    // Read content from the existing file
    const fileData = await this.readPostFile(dbPost.filePath);
    if (!fileData) {
      return false;
    }

    // Build the full post data with DB metadata (tags) and file content
    const postData = this.dbRowToPostData(dbPost, fileData.content);

    // Re-write the file with updated metadata
    await this.writePostFile(postData);

    return true;
  }

  async getAllPosts(options?: PaginationOptions): Promise<PaginatedResult<PostData>> {
    const db = getDatabase().getLocal();
    const limit = options?.limit ?? 500;
    const offset = options?.offset ?? 0;

    // Get total count for hasMore calculation
    const countResult = await db
      .select({ count: posts.id })
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId))
      .all();
    const total = countResult.length;

    // Drafts must ALWAYS be included regardless of pagination.
    // On the first page (offset=0), fetch all drafts and fill remaining slots with non-drafts.
    // On subsequent pages, only paginate non-draft posts (drafts were already returned).
    if (offset === 0) {
      // Fetch ALL drafts (typically few)
      const draftPosts = await db
        .select()
        .from(posts)
        .where(and(
          eq(posts.projectId, this.currentProjectId),
          eq(posts.status, 'draft')
        ))
        .orderBy(desc(posts.createdAt))
        .all();

      // Fill remaining slots with non-draft posts
      const remainingSlots = Math.max(0, limit - draftPosts.length);
      const nonDraftPosts = remainingSlots > 0 ? await db
        .select()
        .from(posts)
        .where(and(
          eq(posts.projectId, this.currentProjectId),
          ne(posts.status, 'draft')
        ))
        .orderBy(desc(posts.createdAt))
        .limit(remainingSlots)
        .all() : [];

      const allDbPosts = [...draftPosts, ...nonDraftPosts];
      const items: PostData[] = allDbPosts.map(dbPost =>
        this.dbRowToPostData(dbPost, dbPost.content || '')
      );

      return {
        items,
        hasMore: allDbPosts.length < total,
        total,
      };
    }

    // Subsequent pages: only paginate non-draft posts
    // Count drafts to calculate correct offset into non-draft posts
    const draftCount = await db
      .select({ count: posts.id })
      .from(posts)
      .where(and(
        eq(posts.projectId, this.currentProjectId),
        eq(posts.status, 'draft')
      ))
      .all();
    const numDrafts = draftCount.length;

    // Adjust offset: the first page returned numDrafts + (limit - numDrafts) non-draft posts
    // So for page 2+, offset into non-draft posts = offset - numDrafts
    const nonDraftOffset = offset - numDrafts;
    const dbPosts = await db
      .select()
      .from(posts)
      .where(and(
        eq(posts.projectId, this.currentProjectId),
        ne(posts.status, 'draft')
      ))
      .orderBy(desc(posts.createdAt))
      .limit(limit)
      .offset(nonDraftOffset)
      .all();

    const items: PostData[] = dbPosts.map(dbPost =>
      this.dbRowToPostData(dbPost, dbPost.content || '')
    );

    return {
      items,
      hasMore: offset + items.length < total,
      total,
    };
  }

  /**
   * Internal method to get all posts without pagination.
   * Used by methods that need to iterate over all posts (search, tags, categories, etc.)
   */
  private async getAllPostsUnpaginated(): Promise<PostData[]> {
    const db = getDatabase().getLocal();
    const dbPosts = await db
      .select()
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId))
      .orderBy(desc(posts.createdAt))
      .all();
    
    // Use DB content for drafts, empty string for published posts.
    // This avoids expensive filesystem reads.
    return dbPosts.map(dbPost => this.dbRowToPostData(dbPost, dbPost.content || ''));
  }

  async getPostsByStatus(status: 'draft' | 'published' | 'archived'): Promise<PostData[]> {
    const db = getDatabase().getLocal();
    const dbPosts = await db
      .select()
      .from(posts)
      .where(and(
        eq(posts.projectId, this.currentProjectId),
        eq(posts.status, status)
      ))
      .orderBy(desc(posts.createdAt))
      .all();
    
    // Use DB content for drafts, empty string for published posts.
    // This avoids expensive filesystem reads.
    return dbPosts.map(dbPost => this.dbRowToPostData(dbPost, dbPost.content || ''));
  }

  async getPostsFiltered(filter: PostFilter): Promise<PostData[]> {
    const db = getDatabase().getLocal();
    const conditions = [eq(posts.projectId, this.currentProjectId)];

    if (filter.status) {
      conditions.push(eq(posts.status, filter.status));
    }

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

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

    if (filter.year !== undefined) {
      const startOfYear = new Date(filter.year, 0, 1);
      const endOfYear = new Date(filter.year + 1, 0, 1);
      conditions.push(gte(posts.createdAt, startOfYear));
      conditions.push(lte(posts.createdAt, endOfYear));
    }

    if (filter.month !== undefined && filter.year !== undefined) {
      const startOfMonth = new Date(filter.year, filter.month - 1, 1);
      const endOfMonth = new Date(filter.year, filter.month, 1);
      conditions.push(gte(posts.createdAt, startOfMonth));
      conditions.push(lte(posts.createdAt, endOfMonth));
    }

    if (filter.categories && filter.categories.length > 0) {
      const includePredicates = filter.categories.map((category) =>
        sql`exists (
          select 1
          from json_each(${posts.categories}) as included_category
          where included_category.value = ${category}
        )`
      );
      conditions.push(sql`(${sql.join(includePredicates, sql` OR `)})`);
    }

    if (filter.excludeCategories && filter.excludeCategories.length > 0) {
      const excludePredicates = filter.excludeCategories.map((category) =>
        sql`exists (
          select 1
          from json_each(${posts.categories}) as excluded_category
          where excluded_category.value = ${category}
        )`
      );
      conditions.push(sql`NOT (${sql.join(excludePredicates, sql` OR `)})`);
    }

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

    let result: PostData[] = [];

    for (const dbPost of dbPosts) {
      // Use DB data directly instead of reading from filesystem
      const postData = this.dbRowToPostData(dbPost, dbPost.content || '');
      
      // Client-side filtering for tags only (category filtering is done in SQL)
      if (filter.tags && filter.tags.length > 0) {
        const hasAllTags = filter.tags.every(tag => postData.tags.includes(tag));
        if (!hasAllTags) continue;
      }

      result.push(postData);
    }

    return result;
  }

  async searchPosts(query: string): Promise<SearchResult[]> {
    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 posts_fts WHERE project_id = ? AND posts_fts MATCH ? ORDER BY rank LIMIT 500`,
        args: [this.currentProjectId, stemmedQuery],
      });

      // Fetch actual post data for results
      const db = getDatabase().getLocal();
      const searchResults: SearchResult[] = [];
      
      for (const row of result.rows) {
        const postId = row.id as string;
        const post = await db.select().from(posts).where(eq(posts.id, postId)).get();
        if (post) {
          searchResults.push({
            id: post.id,
            title: post.title,
            slug: post.slug,
            excerpt: post.excerpt ?? undefined,
          });
        }
      }

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

  /**
   * Combined FTS search + structural filters in a single SQL query.
   * Returns full PostData ordered by FTS rank, filtered by the given criteria.
   * Both MCP and internal AI tools use this for query+filter combinations.
   */
  async searchPostsFiltered(
    query: string,
    filter: PostFilter,
    pagination?: PaginationOptions,
  ): Promise<PostData[]> {
    if (!query.trim()) return [];

    const client = getDatabase().getLocalClient();
    if (!client) return [];

    try {
      const stemmedQuery = stemQuery(query, this.searchLanguage);

      // Build WHERE clauses and args for the joined query
      const conditions: string[] = [
        'posts_fts.project_id = ?',
        'posts_fts.MATCH ?',
      ];
      const args: (string | number | Date)[] = [this.currentProjectId, stemmedQuery];

      if (filter.status) {
        conditions.push('posts.status = ?');
        args.push(filter.status);
      }
      if (filter.year !== undefined) {
        const startOfYear = new Date(filter.year, 0, 1);
        const endOfYear = new Date(filter.year + 1, 0, 1);
        conditions.push('posts.created_at >= ?');
        args.push(startOfYear);
        conditions.push('posts.created_at <= ?');
        args.push(endOfYear);
      }
      if (filter.month !== undefined && filter.year !== undefined) {
        const startOfMonth = new Date(filter.year, filter.month - 1, 1);
        const endOfMonth = new Date(filter.year, filter.month, 1);
        conditions.push('posts.created_at >= ?');
        args.push(startOfMonth);
        conditions.push('posts.created_at <= ?');
        args.push(endOfMonth);
      }
      if (filter.categories && filter.categories.length > 0) {
        const catClauses = filter.categories.map(() =>
          `EXISTS (SELECT 1 FROM json_each(posts.categories) AS c WHERE c.value = ?)`
        );
        conditions.push(`(${catClauses.join(' OR ')})`);
        args.push(...filter.categories);
      }
      if (filter.excludeCategories && filter.excludeCategories.length > 0) {
        const exClauses = filter.excludeCategories.map(() =>
          `EXISTS (SELECT 1 FROM json_each(posts.categories) AS c WHERE c.value = ?)`
        );
        conditions.push(`NOT (${exClauses.join(' OR ')})`);
        args.push(...filter.excludeCategories);
      }
      if (filter.startDate) {
        conditions.push('posts.created_at >= ?');
        args.push(filter.startDate);
      }
      if (filter.endDate) {
        conditions.push('posts.created_at <= ?');
        args.push(filter.endDate);
      }

      const whereClause = conditions.join(' AND ');
      const sqlQuery = `
        SELECT posts.*
        FROM posts_fts
        JOIN posts ON posts_fts.id = posts.id
        WHERE ${whereClause}
        ORDER BY posts_fts.rank
        LIMIT 500
      `;

      const result = await client.execute({ sql: sqlQuery, args });

      let postDataList: PostData[] = result.rows.map((row) =>
        this.dbRowToPostData(row as unknown as Post, (row.content as string) || '')
      );

      // Tag filtering is done client-side (tags are stored as JSON arrays)
      if (filter.tags && filter.tags.length > 0) {
        postDataList = postDataList.filter((p) =>
          filter.tags!.every((tag) => p.tags.includes(tag))
        );
      }

      // Apply pagination
      const offset = pagination?.offset ?? 0;
      const limit = pagination?.limit ?? postDataList.length;
      return postDataList.slice(offset, offset + limit);
    } catch (error) {
      console.error('Search with filters failed:', error);
      return [];
    }
  }

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

  async getAvailableCategories(): Promise<string[]> {
    const allPosts = await this.getAllPostsUnpaginated();
    const categories = new Set<string>();
    for (const post of allPosts) {
      for (const cat of post.categories) {
        categories.add(cat);
      }
    }
    return Array.from(categories).sort();
  }

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

    const tagCounts = new Map<string, number>();
    for (const row of dbPosts) {
      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);
  }

  async getCategoriesWithCounts(): Promise<{ category: string; count: number }[]> {
    const db = getDatabase().getLocal();
    const dbPosts = await db
      .select({ categories: posts.categories })
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId))
      .all();

    const catCounts = new Map<string, number>();
    for (const row of dbPosts) {
      const parsed: string[] = JSON.parse(row.categories || '[]');
      for (const cat of parsed) {
        catCounts.set(cat, (catCounts.get(cat) || 0) + 1);
      }
    }

    return Array.from(catCounts.entries())
      .map(([category, count]) => ({ category, count }))
      .sort((a, b) => b.count - a.count);
  }

  async getDashboardStats(): Promise<{
    totalPosts: number;
    draftCount: number;
    publishedCount: number;
    archivedCount: number;
  }> {
    const db = getDatabase().getLocal();
    const dbPosts = await db
      .select({ status: posts.status })
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId))
      .all();

    let draftCount = 0;
    let publishedCount = 0;
    let archivedCount = 0;

    for (const row of dbPosts) {
      switch (row.status) {
        case 'draft': draftCount++; break;
        case 'published': publishedCount++; break;
        case 'archived': archivedCount++; break;
      }
    }

    return {
      totalPosts: dbPosts.length,
      draftCount,
      publishedCount,
      archivedCount,
    };
  }

  async getBlogStats(): Promise<{
    totalPosts: number;
    draftCount: number;
    publishedCount: number;
    archivedCount: number;
    oldestPostDate: Date | null;
    newestPostDate: Date | null;
    postsPerYear: Record<number, number>;
    tagCount: number;
    categoryCount: number;
  }> {
    const db = getDatabase().getLocal();
    const dbPosts = await db
      .select({ status: posts.status, createdAt: posts.createdAt, tags: posts.tags, categories: posts.categories })
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId))
      .all();

    let draftCount = 0;
    let publishedCount = 0;
    let archivedCount = 0;
    let oldestPostDate: Date | null = null;
    let newestPostDate: Date | null = null;
    const postsPerYear: Record<number, number> = {};
    const uniqueTags = new Set<string>();
    const uniqueCategories = new Set<string>();

    for (const row of dbPosts) {
      switch (row.status) {
        case 'draft': draftCount++; break;
        case 'published': publishedCount++; break;
        case 'archived': archivedCount++; break;
      }

      const created = row.createdAt;
      if (!oldestPostDate || created < oldestPostDate) oldestPostDate = created;
      if (!newestPostDate || created > newestPostDate) newestPostDate = created;

      const year = created.getFullYear();
      postsPerYear[year] = (postsPerYear[year] || 0) + 1;

      const parsedTags: string[] = JSON.parse(row.tags || '[]');
      for (const tag of parsedTags) uniqueTags.add(tag);

      const parsedCategories: string[] = JSON.parse(row.categories || '[]');
      for (const cat of parsedCategories) uniqueCategories.add(cat);
    }

    return {
      totalPosts: dbPosts.length,
      draftCount,
      publishedCount,
      archivedCount,
      oldestPostDate,
      newestPostDate,
      postsPerYear,
      tagCount: uniqueTags.size,
      categoryCount: uniqueCategories.size,
    };
  }

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

    for (const post of allPosts) {
      const year = post.createdAt.getFullYear();
      const month = post.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 publishPost(id: string): Promise<PostData | null> {
    const db = getDatabase().getLocal();
    const client = getDatabase().getLocalClient();
    const existing = await this.getPost(id);
    
    if (!existing) {
      return null;
    }

    const now = new Date();
    const publishedAt = existing.publishedAt || now;

    const published: PostData = {
      ...existing,
      status: 'published',
      publishedAt,
      updatedAt: now,
    };

    // Write content + metadata to the filesystem
    const newFilePath = await this.writePostFile(published);

    // If there was a previous file with a different path (slug changed), remove it
    const dbPost = await db.select().from(posts).where(eq(posts.id, id)).get();
    if (dbPost && dbPost.filePath && dbPost.filePath !== newFilePath && dbPost.filePath !== '') {
      try {
        await fs.unlink(dbPost.filePath);
      } catch {
        // Old file might not exist
      }
    }

    const checksum = this.calculateChecksum(published.content);

    // Update DB: clear draft content (it lives in the file now), set filePath
    await db.update(posts)
      .set({
        title: published.title,
        slug: published.slug,
        excerpt: published.excerpt,
        content: null,
        status: 'published',
        author: published.author,
        updatedAt: published.updatedAt,
        publishedAt: published.publishedAt,
        filePath: newFilePath,
        checksum,
        tags: JSON.stringify(published.tags),
        categories: JSON.stringify(published.categories),
      })
      .where(eq(posts.id, id));

    // Update FTS index
    await this.updateFTSIndex(published);

    // Update post links based on published content
    await this.updatePostLinks(id, published.content);

    this.emit('postUpdated', published);
    await this.notifier.notify('post', id, 'updated');
    return published;
  }

  async discardChanges(id: string): Promise<PostData | null> {
    const db = getDatabase().getLocal();
    const client = getDatabase().getLocalClient();
    const dbPost = await db.select().from(posts).where(eq(posts.id, id)).get();
    
    if (!dbPost) {
      return null;
    }

    // Can only discard if there's a published file to revert to
    if (!dbPost.filePath) {
      return null;
    }

    // Read the published version from the filesystem
    const publishedData = await this.readPostFile(dbPost.filePath);
    if (!publishedData) {
      return null;
    }

    const now = new Date();

    // Restore DB metadata from the published file, clear draft content
    await db.update(posts)
      .set({
        title: publishedData.title,
        slug: publishedData.slug,
        excerpt: publishedData.excerpt,
        content: null,
        status: 'published',
        author: publishedData.author,
        updatedAt: now,
        publishedAt: publishedData.publishedAt,
        tags: JSON.stringify(publishedData.tags),
        categories: JSON.stringify(publishedData.categories),
      })
      .where(eq(posts.id, id));

    const reverted: PostData = {
      id: dbPost.id,
      projectId: dbPost.projectId,
      title: publishedData.title,
      slug: publishedData.slug,
      excerpt: publishedData.excerpt,
      content: publishedData.content,
      status: 'published',
      author: publishedData.author,
      createdAt: dbPost.createdAt,
      updatedAt: now,
      publishedAt: publishedData.publishedAt,
      tags: publishedData.tags || [],
      categories: publishedData.categories || [],
    };

    // Update FTS index
    await this.updateFTSIndex(reverted);

    this.emit('postUpdated', reverted);
    return reverted;
  }

  async hasPublishedVersion(id: string): Promise<boolean> {
    const db = getDatabase().getLocal();
    const dbPost = await db.select().from(posts).where(eq(posts.id, id)).get();
    return !!(dbPost && dbPost.filePath && dbPost.filePath !== '');
  }

  async getPublishedVersion(id: string): Promise<PostData | null> {
    const db = getDatabase().getLocal();
    const dbPost = await db.select().from(posts).where(eq(posts.id, id)).get();

    if (!dbPost || !dbPost.filePath) {
      return null;
    }

    const fileData = await this.readPostFile(dbPost.filePath);
    if (!fileData) {
      return null;
    }

    return {
      id: dbPost.id,
      projectId: dbPost.projectId,
      title: fileData.title,
      slug: fileData.slug,
      excerpt: fileData.excerpt,
      content: fileData.content,
      status: 'published',
      author: fileData.author,
      createdAt: fileData.createdAt,
      updatedAt: fileData.updatedAt,
      publishedAt: fileData.publishedAt ?? dbPost.publishedAt ?? undefined,
      tags: fileData.tags,
      categories: fileData.categories,
    };
  }

  /**
   * Rebuild the FTS index for all posts in the current project.
   * Call this after changing the search language or after migration.
   */
  async rebuildFTSIndex(): Promise<void> {
    const client = getDatabase().getLocalClient();
    if (!client) return;

    const allPosts = await this.getAllPostsUnpaginated();
    
    for (const post of allPosts) {
      await this.updateFTSIndex(post);
    }
    
    console.log(`Rebuilt FTS index for ${allPosts.length} posts`);
  }

  async reconcilePublishedPostsFromGitChanges(
    projectPath: string,
    changes: GitPostFileChange[],
  ): Promise<PublishedPostReconcileResult> {
    const db = getDatabase().getLocal();
    const normalizedProjectPath = path.resolve(projectPath);

    const relevantChanges = changes.filter((change) => {
      if (!this.isMarkdownPostPath(change.path)) {
        return false;
      }
      if (change.status === 'renamed' && change.previousPath && !this.isMarkdownPostPath(change.previousPath) && !this.isMarkdownPostPath(change.path)) {
        return false;
      }
      return true;
    });

    if (relevantChanges.length === 0) {
      return { created: 0, updated: 0, deleted: 0, processedFiles: 0 };
    }

    const projectPosts = await db
      .select()
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId))
      .all();

    const publishedRows = projectPosts.filter((row) => row.status === 'published' && Boolean(row.filePath));
    const publishedByFilePath = new Map<string, Post>();
    for (const row of publishedRows) {
      if (!row.filePath) {
        continue;
      }
      publishedByFilePath.set(this.normalizePathForCompare(row.filePath), row);
    }

    let created = 0;
    let updated = 0;
    let deleted = 0;
    let processedFiles = 0;

    for (const change of relevantChanges) {
      const absolutePath = this.normalizePathForCompare(path.resolve(normalizedProjectPath, change.path));
      const previousAbsolutePath = change.previousPath
        ? this.normalizePathForCompare(path.resolve(normalizedProjectPath, change.previousPath))
        : null;

      if (change.status === 'deleted') {
        const existingPublished = publishedByFilePath.get(absolutePath);
        if (!existingPublished) {
          continue;
        }

        await db.delete(postLinks).where(eq(postLinks.sourcePostId, existingPublished.id));
        await db.delete(postLinks).where(eq(postLinks.targetPostId, existingPublished.id));
        await db.delete(posts).where(eq(posts.id, existingPublished.id));
        await this.deleteFTSIndex(existingPublished.id);
        this.emit('postDeleted', existingPublished.id);

        publishedByFilePath.delete(absolutePath);
        deleted += 1;
        processedFiles += 1;
        continue;
      }

      const existingPublished = previousAbsolutePath
        ? (publishedByFilePath.get(previousAbsolutePath) || publishedByFilePath.get(absolutePath))
        : publishedByFilePath.get(absolutePath);

      const fileData = await this.readPostFile(absolutePath);
      if (!fileData) {
        continue;
      }

      if (existingPublished) {
        const nextSlugCandidate = fileData.slug || existingPublished.slug;
        const nextSlug = await this.isSlugAvailable(nextSlugCandidate, existingPublished.id)
          ? nextSlugCandidate
          : await this.generateUniqueSlug(nextSlugCandidate, existingPublished.id);

        const checksum = this.calculateChecksum(fileData.content);
        const nextPublishedAt = fileData.publishedAt || existingPublished.publishedAt || fileData.updatedAt;

        await db.update(posts)
          .set({
            title: fileData.title,
            slug: nextSlug,
            excerpt: fileData.excerpt,
            content: null,
            status: 'published',
            author: fileData.author,
            createdAt: fileData.createdAt,
            updatedAt: fileData.updatedAt,
            publishedAt: nextPublishedAt,
            filePath: absolutePath,
            checksum,
            tags: JSON.stringify(fileData.tags),
            categories: JSON.stringify(fileData.categories),
          })
          .where(eq(posts.id, existingPublished.id));

        await this.updateFTSIndex({
          id: existingPublished.id,
          projectId: existingPublished.projectId,
          title: fileData.title,
          content: fileData.content,
          excerpt: fileData.excerpt,
          tags: fileData.tags,
          categories: fileData.categories,
        });

        const updatedPost: PostData = {
          id: existingPublished.id,
          projectId: existingPublished.projectId,
          title: fileData.title,
          slug: nextSlug,
          excerpt: fileData.excerpt || undefined,
          content: fileData.content,
          status: 'published',
          author: fileData.author || undefined,
          createdAt: fileData.createdAt,
          updatedAt: fileData.updatedAt,
          publishedAt: nextPublishedAt || undefined,
          tags: fileData.tags,
          categories: fileData.categories,
        };

        this.emit('postUpdated', updatedPost);

        if (previousAbsolutePath) {
          publishedByFilePath.delete(previousAbsolutePath);
        }

        publishedByFilePath.set(absolutePath, {
          ...existingPublished,
          title: updatedPost.title,
          slug: updatedPost.slug,
          excerpt: updatedPost.excerpt ?? null,
          content: null,
          status: 'published',
          author: updatedPost.author ?? null,
          createdAt: updatedPost.createdAt,
          updatedAt: updatedPost.updatedAt,
          publishedAt: updatedPost.publishedAt ?? null,
          filePath: absolutePath,
          checksum,
          tags: JSON.stringify(updatedPost.tags),
          categories: JSON.stringify(updatedPost.categories),
        });

        updated += 1;
        processedFiles += 1;
        continue;
      }

      if (change.status !== 'added') {
        continue;
      }

      const identity = await this.ensureUniquePostIdentity(fileData.id, fileData.slug);
      const checksum = this.calculateChecksum(fileData.content);
      const publishedAt = fileData.publishedAt || fileData.updatedAt;
      const newPostRow: NewPost = {
        id: identity.id,
        projectId: this.currentProjectId,
        title: fileData.title,
        slug: identity.slug,
        excerpt: fileData.excerpt,
        content: null,
        status: 'published',
        author: fileData.author,
        createdAt: fileData.createdAt,
        updatedAt: fileData.updatedAt,
        publishedAt,
        filePath: absolutePath,
        checksum,
        tags: JSON.stringify(fileData.tags),
        categories: JSON.stringify(fileData.categories),
      };

      await db.insert(posts).values(newPostRow);
      await this.updateFTSIndex({
        id: identity.id,
        projectId: this.currentProjectId,
        title: fileData.title,
        content: fileData.content,
        excerpt: fileData.excerpt,
        tags: fileData.tags,
        categories: fileData.categories,
      });

      const createdPost: PostData = {
        id: identity.id,
        projectId: this.currentProjectId,
        title: fileData.title,
        slug: identity.slug,
        excerpt: fileData.excerpt || undefined,
        content: fileData.content,
        status: 'published',
        author: fileData.author || undefined,
        createdAt: fileData.createdAt,
        updatedAt: fileData.updatedAt,
        publishedAt: publishedAt || undefined,
        tags: fileData.tags,
        categories: fileData.categories,
      };

      this.emit('postCreated', createdPost);
      publishedByFilePath.set(absolutePath, {
        ...newPostRow,
        excerpt: newPostRow.excerpt ?? null,
        content: null,
        author: newPostRow.author ?? null,
      } as Post);

      created += 1;
      processedFiles += 1;
    }

    return {
      created,
      updated,
      deleted,
      processedFiles,
    };
  }

  /**
   * 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 search text',
      execute: async (onProgress) => {
        const client = getDatabase().getLocalClient();
        if (!client) {
          throw new Error('Database client not available');
        }

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

        // Clear the entire FTS table
        await client.execute('DELETE FROM posts_fts');

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

        const allPosts = await this.getAllPostsUnpaginated();
        const total = allPosts.length;

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

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

        for (let i = 0; i < allPosts.length; i++) {
          const post = allPosts[i];
          await this.updateFTSIndex(post);

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

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

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

    await taskManager.runTask(task);
  }

  async rebuildDatabaseFromFiles(): Promise<void> {
    const postsBaseDir = this.getPostsBaseDir();
    const task: Task<void> = {
      id: uuidv4(),
      name: 'Rebuild database from post files',
      execute: async (onProgress) => {
        const db = getDatabase().getLocal();
        const client = getDatabase().getLocalClient();

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

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

        // Delete all posts for the current project - clean slate rebuild
        const existingPosts = await db.select({ id: posts.id }).from(posts).where(eq(posts.projectId, this.currentProjectId)).all();
        if (existingPosts.length > 0) {
          const postIds = existingPosts.map(p => p.id);
          // Delete FTS entries first
          for (const post of existingPosts) {
            await this.deleteFTSIndex(post.id);
          }
          // Delete post links where source or target is in the posts being deleted
          await db.delete(postLinks).where(inArray(postLinks.sourcePostId, postIds));
          await db.delete(postLinks).where(inArray(postLinks.targetPostId, postIds));
          // Delete posts
          await db.delete(posts).where(eq(posts.projectId, this.currentProjectId));
          console.log(`Deleted ${existingPosts.length} existing post(s) for project ${this.currentProjectId}`);
        }

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

        // Recursively find markdown files in the posts directory tree
        const markdownFiles: string[] = [];
        const markdownExtensions = new Set(['.md', '.markdown', '.mdx']);
        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 {
                const extension = path.extname(entry.name).toLowerCase();
                if (markdownExtensions.has(extension)) {
                  markdownFiles.push(fullPath);
                }
              }
            }
          } catch {
            // Directory might not exist
          }
        };

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

        onProgress(10, `Found ${markdownFiles.length} post files`);

        // Track slugs and ids to avoid collisions while still importing all files
        const insertedSlugs = new Set<string>(); // projectId:slug
        const insertedIds = new Set<string>();
        let importedCount = 0;
        let parseFailedCount = 0;
        let deduplicatedSlugCount = 0;
        let deduplicatedIdCount = 0;
        let insertFailedCount = 0;

        for (let i = 0; i < markdownFiles.length; i++) {
          const filePath = markdownFiles[i];
          const fileName = path.basename(filePath);

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

          const postData = await this.readPostFile(filePath);

          if (!postData) {
            parseFailedCount++;
            continue;
          }

          try {
            const projectId = this.currentProjectId;

            let postId = postData.id;
            while (insertedIds.has(postId)) {
              postId = uuidv4();
              deduplicatedIdCount++;
            }

            let slug = postData.slug;
            const baseSlug = slug;
            let slugAttempt = 2;
            while (insertedSlugs.has(`${projectId}:${slug}`)) {
              slug = `${baseSlug}-${slugAttempt}`;
              slugAttempt++;
              deduplicatedSlugCount++;
            }

            const checksum = this.calculateChecksum(postData.content);

            await db.insert(posts).values({
              id: postId,
              projectId,
              title: postData.title,
              slug,
              excerpt: postData.excerpt,
              content: null,
              status: 'published',
              author: postData.author,
              createdAt: postData.createdAt,
              updatedAt: postData.updatedAt,
              publishedAt: postData.publishedAt || postData.updatedAt,
              filePath,
              checksum,
              tags: JSON.stringify(postData.tags),
              categories: JSON.stringify(postData.categories),
            });

            insertedIds.add(postId);
            insertedSlugs.add(`${projectId}:${slug}`);
            importedCount++;

            await this.updateFTSIndex({
              id: postId,
              projectId,
              title: postData.title,
              content: postData.content,
              excerpt: postData.excerpt,
              tags: postData.tags,
              categories: postData.categories,
            });
          } catch (error: any) {
            insertFailedCount++;
            if (error?.code === 'SQLITE_CONSTRAINT_UNIQUE') {
              console.error(`Failed to insert post "${postData.title}" from ${filePath}: Unique constraint violation`);
            } else {
              console.error(`Failed to process post from ${filePath}:`, 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: imported ${importedCount}/${markdownFiles.length} files`);
        console.log(`[PostEngine] rebuildDatabaseFromFiles complete. scanned=${markdownFiles.length}, imported=${importedCount}, parseFailed=${parseFailedCount}, insertFailed=${insertFailedCount}, deduplicatedSlugs=${deduplicatedSlugCount}, deduplicatedIds=${deduplicatedIdCount}`);
        this.emit('databaseRebuilt');
      },
    };

    await taskManager.runTask(task);
  }

  /**
   * Extract internal post links from content (links to other posts in the blog)
   */
  extractInternalLinks(content: string): { slug: string; text: string }[] {
    const links: { slug: string; text: string }[] = [];
    
    // Match markdown links: [text](/posts/slug) or [text](/year/month/slug)
    const markdownLinkRegex = /\[([^\]]+)\]\(\/(?:posts\/)?(?:\d{4}\/\d{2}\/)?([a-z0-9-]+)(?:\.html?)?\)/gi;
    let match;
    while ((match = markdownLinkRegex.exec(content)) !== null) {
      links.push({ text: match[1], slug: match[2] });
    }
    
    // Match HTML links: <a href="/posts/slug">text</a>
    const htmlLinkRegex = /<a[^>]+href=["']\/(?:posts\/)?(?:\d{4}\/\d{2}\/)?([a-z0-9-]+)(?:\.html?)?["'][^>]*>([^<]+)<\/a>/gi;
    while ((match = htmlLinkRegex.exec(content)) !== null) {
      links.push({ text: match[2], slug: match[1] });
    }
    
    return links;
  }

  /**
   * Update post links in the database based on content analysis
   */
  async updatePostLinks(postId: string, content: string): Promise<void> {
    const db = getDatabase().getLocal();
    const extractedLinks = this.extractInternalLinks(content);
    
    // Delete existing links from this post
    await db.delete(postLinks).where(eq(postLinks.sourcePostId, postId));
    
    if (extractedLinks.length === 0) return;
    
    // Get all posts to resolve slugs to IDs
    const allPosts = await db.select({ id: posts.id, slug: posts.slug })
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId));
    
    const slugToId = new Map(allPosts.map(p => [p.slug, p.id]));
    
    // Insert new links
    for (const link of extractedLinks) {
      const targetId = slugToId.get(link.slug);
      if (targetId && targetId !== postId) {
        await db.insert(postLinks).values({
          id: uuidv4(),
          sourcePostId: postId,
          targetPostId: targetId,
          linkText: link.text,
          createdAt: new Date(),
        });
      }
    }
  }

  /**
   * Get posts that link TO the specified post ("linked by")
   */
  async getLinkedBy(postId: string): Promise<{ id: string; title: string; slug: string }[]> {
    const db = getDatabase().getLocal();
    
    const links = await db
      .select({
        sourcePostId: postLinks.sourcePostId,
        linkText: postLinks.linkText,
      })
      .from(postLinks)
      .where(eq(postLinks.targetPostId, postId));
    
    if (links.length === 0) return [];
    
    const sourceIds = links.map(l => l.sourcePostId);
    const sourcePosts = await db
      .select({ id: posts.id, title: posts.title, slug: posts.slug })
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId));
    
    return sourcePosts.filter(p => sourceIds.includes(p.id));
  }

  /**
   * Get posts that the specified post links TO ("links to")
   */
  async getLinksTo(postId: string): Promise<{ id: string; title: string; slug: string }[]> {
    const db = getDatabase().getLocal();
    
    const links = await db
      .select({
        targetPostId: postLinks.targetPostId,
        linkText: postLinks.linkText,
      })
      .from(postLinks)
      .where(eq(postLinks.sourcePostId, postId));
    
    if (links.length === 0) return [];
    
    const targetIds = links.map(l => l.targetPostId);
    const targetPosts = await db
      .select({ id: posts.id, title: posts.title, slug: posts.slug })
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId));
    
    return targetPosts.filter(p => targetIds.includes(p.id));
  }

  /**
   * Rebuild all post links from content analysis
   */
  async rebuildAllPostLinks(): Promise<void> {
    const db = getDatabase().getLocal();
    
    // Clear all existing links
    await db.delete(postLinks);
    
    // Get all posts with content source info
    const allPosts = await db
      .select({ id: posts.id, filePath: posts.filePath, content: posts.content })
      .from(posts)
      .where(eq(posts.projectId, this.currentProjectId));
    
    for (const post of allPosts) {
      try {
        let postContent: string;

        // Draft content is in DB, published content is in file
        if (post.content) {
          postContent = post.content;
        } else if (post.filePath) {
          const fileContent = await fs.readFile(post.filePath, 'utf-8');
          const { content } = matter(fileContent);
          postContent = content;
        } else {
          continue;
        }

        await this.updatePostLinks(post.id, postContent);
      } catch (error) {
        console.error(`Failed to update links for post ${post.id}:`, error);
      }
    }
    
    this.emit('postLinksRebuilt');
  }
}


