import { db, Post, PostWorkingDraft, AuditLocation } from "@/lib/admin/db";
import { revalidatePath } from "next/cache";
import { calculateReadingStats } from "@/lib/reading-time";
import { getAuthorRoleLabel, toBlogAuthor } from "@/lib/admin/authors";
import { safeCleanupUnreferencedMedia } from "@/lib/media-storage";

/**
 * Gracefully revalidates Next.js cached routes without throwing outside request context.
 */
function safeRevalidatePath(path: string) {
  try {
    revalidatePath(path);
  } catch {
    // Graceful no-op when executing in background workers or CLI scripts
  }
}

/**
 * Parses <h2> and <h3> tags in HTML, ensures each has a unique id slug for in-page anchors,
 * and extracts a clean tableOfContents array.
 */
export function generateTocAndInjectIds(html: string): {
  html: string;
  tableOfContents: { id: string; title: string }[];
} {
  if (!html || typeof html !== "string") {
    return { html, tableOfContents: [] };
  }

  const tableOfContents: { id: string; title: string }[] = [];
  const usedSlugs = new Set<string>();

  const updatedHtml = html.replace(/<h([23])(?:\s+[^>]*)?>(.*?)<\/h\1>/gi, (fullMatch, level, innerText) => {
    const cleanTitle = innerText.replace(/<[^>]*>/g, "").trim();
    if (!cleanTitle) return fullMatch;

    let baseSlug = cleanTitle
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/(^-|-$)+/g, "");
    if (!baseSlug) baseSlug = `heading-${level}`;

    let uniqueSlug = baseSlug;
    let count = 1;
    while (usedSlugs.has(uniqueSlug)) {
      uniqueSlug = `${baseSlug}-${count}`;
      count++;
    }
    usedSlugs.add(uniqueSlug);

    tableOfContents.push({
      id: uniqueSlug,
      title: cleanTitle,
    });

    return `<h${level} id="${uniqueSlug}">${innerText}</h${level}>`;
  });

  return { html: updatedHtml, tableOfContents };
}

/**
 * Author / Actor context for blog management actions.
 */
export interface BlogActor {
  id: string;
  name: string;
  email: string;
  role: string;
  location?: AuditLocation | string | null;
}

function resolveBlogActorLocation(loc?: AuditLocation | string | null): AuditLocation | null {
  if (!loc) return null;
  if (typeof loc === "string") {
    return {
      status: "available",
      address: loc,
      city: loc,
      source: "Account Profile",
    };
  }
  return loc;
}

/**
 * High-performance Blog Service connecting MongoDB to the Public Website & Admin CMS.
 * Acts as the authoritative source of truth.
 */

export interface PreflightCheckResult {
  valid: boolean;
  errors: string[];
  warnings: string[];
}

export function validatePostPreflight(post: Partial<Post>): PreflightCheckResult {
  const errors: string[] = [];
  const warnings: string[] = [];

  if (!post.title || post.title.trim().length === 0) {
    errors.push("Title is required for publication.");
  }

  if (!post.slug || post.slug.trim().length === 0) {
    errors.push("URL slug is required for publication.");
  }

  const hasSections = post.sections && post.sections.length > 0;
  const hasContentHtml = post.contentHtml && post.contentHtml.trim().length > 0;
  if (!hasSections && !hasContentHtml) {
    errors.push("Article body content cannot be empty.");
  }

  if (!post.excerpt || post.excerpt.trim().length === 0) {
    warnings.push("Excerpt is empty; social cards and search listings will lack summary text.");
  }

  if (!post.seoTitle || post.seoTitle.trim().length === 0) {
    errors.push("SEO title is required before publishing.");
  }

  if (!post.metaDescription || post.metaDescription.trim().length === 0) {
    errors.push("Meta description is required before publishing.");
  }

  // Obvious placeholder detection
  const placeholderRegex = /\b(lorem ipsum|todo|placeholder|dummy|test content|asdf)\b/i;
  const combinedText = `${post.title || ""} ${post.excerpt || ""} ${post.contentHtml || ""}`;
  if (placeholderRegex.test(combinedText)) {
    warnings.push("Potential placeholder text detected (e.g., 'Lorem ipsum', 'TODO', 'Placeholder').");
  }

  return {
    valid: errors.length === 0,
    errors,
    warnings,
  };
}

/** Public, minimal blog card shape — safe to pass to client components. */
export interface PublicPostCard {
  slug: string;
  title: string;
  excerpt: string;
  category: string;
  readTime: string;
  publishedDate: string;
  featuredImage: string | null;
  featuredImageAlt: string | null;
  coverIllustration: string | null;
  authorName: string;
  authorAvatarText: string;
  authorAvatarColor: string;
}

function toPublicPostCard(p: Post): PublicPostCard {
  return {
    slug: p.slug,
    title: p.title,
    excerpt: p.excerpt,
    category: p.category,
    readTime: p.readTime,
    publishedDate: p.publishedDate,
    featuredImage: p.featuredImage || null,
    featuredImageAlt: p.featuredImageAlt || p.title,
    coverIllustration: p.coverIllustration || null,
    authorName: p.author?.name || "Quto AI",
    authorAvatarText: p.author?.avatarText || "Q",
    authorAvatarColor: p.author?.avatarColor || "from-neutral-700 to-neutral-900",
  };
}

export const blogService = {
  /**
   * Fetches published blog posts for public index and category listings.
   */
  async getPublishedPosts(options?: {
    category?: string;
    search?: string;
    limit?: number;
    skip?: number;
    /** Exclude heavy body fields (sections/contentHtml/contentJson) for listings. */
    lean?: boolean;
  }): Promise<Post[]> {
    // Check and trigger any due scheduled posts
    await this.processDueScheduledPosts();

    return db.getPosts({
      status: "PUBLISHED",
      category: options?.category,
      search: options?.search,
      limit: options?.limit,
      skip: options?.skip,
      sortBy: "publishedDate",
      sortOrder: -1,
      lean: options?.lean,
    });
  },

  /**
   * Fetches total published posts count.
   */
  async getPublishedPostsCount(options?: {
    category?: string;
    search?: string;
  }): Promise<number> {
    return db.getPostCount({
      status: "PUBLISHED",
      category: options?.category,
      search: options?.search,
    });
  },

  /**
   * Minimal PUBLIC card DTOs for the homepage carousel / listings. Server-only.
   * Returns ONLY publicly-safe fields, queried with a DB-side limit (never
   * fetch-all-then-slice in JS), sorted newest first, using the lean projection
   * (no sections/contentHtml/contentJson). Never runs scheduled-post processing
   * so the cached homepage stays cheap — the blog index/detail handle that.
   */
  async getLatestPublishedPostCards(limit = 6): Promise<PublicPostCard[]> {
    const safeLimit = Math.max(1, Math.min(12, limit));
    try {
      const posts = await db.getPosts({
        status: "PUBLISHED",
        limit: safeLimit,
        sortBy: "publishedDate",
        sortOrder: -1,
        lean: true,
      });
      return posts.map(toPublicPostCard);
    } catch (err) {
      // Never let a transient DB/auth outage crash the homepage prerender/ISR
      // build. The carousel simply renders nothing until the database is
      // reachable again (the homepage already hides it when the list is empty).
      console.warn("[blog-service] getLatestPublishedPostCards failed; returning []:", err);
      return [];
    }
  },

  /**
   * Resolves a single public post by its unique URL slug.
   */
  async getPostBySlug(slug: string): Promise<Post | null> {
    // Check and trigger any due scheduled posts
    await this.processDueScheduledPosts();

    const post = await db.getPostBySlug(slug);
    if (!post || post.status !== "PUBLISHED") {
      return null;
    }
    return post;
  },

  /**
   * Resolves a post for admin preview (allows viewing DRAFT, SCHEDULED, or PUBLISHED posts with working drafts).
   */
  async getPostForPreview(id: string): Promise<Post | null> {
    const post = await db.getPostById(id);
    if (!post) return null;
    if (post.workingDraft) {
      return {
        ...post,
        ...post.workingDraft,
        author: post.workingDraft.author || post.author,
        hasWorkingDraft: true,
      };
    }
    return post;
  },

  /**
   * Resolves related posts for an article.
   */
  async getRelatedPosts(post: Post, limit = 3): Promise<Post[]> {
    const allPublished = await this.getPublishedPosts({ limit: 20, lean: true });
    return allPublished
      .filter(
        (p) =>
          p.slug !== post.slug &&
          (post.relatedSlugs?.includes(p.slug) || p.category === post.category)
      )
      .slice(0, limit);
  },

  /**
   * Resolves posts authored by a specific individual.
   */
  async getPostsByAuthor(authorNameOrSlug: string): Promise<Post[]> {
    const all = await this.getPublishedPosts({ limit: 50, lean: true });
    const normalized = authorNameOrSlug.toLowerCase().replace(/-/g, " ");
    return all.filter(
      (p) =>
        p.author.name.toLowerCase() === normalized ||
        p.author.name.toLowerCase().replace(/\s+/g, "-") === authorNameOrSlug.toLowerCase() ||
        (p.author as { profileSlug?: string }).profileSlug === authorNameOrSlug.toLowerCase()
    );
  },

  /**
   * Returns all active categories with published articles.
   */
  async getActiveCategories(): Promise<string[]> {
    const dbCats = await db.getCategories();
    if (dbCats && dbCats.length > 0) {
      return dbCats.map((c) => c.name);
    }
    const posts = await this.getPublishedPosts({ limit: 100 });
    const cats = new Set<string>();
    for (const p of posts) {
      if (p.category) cats.add(p.category);
    }
    return Array.from(cats);
  },

  /**
   * Returns the featured post or the latest published post.
   */
  async getFeaturedPost(): Promise<Post | null> {
    const posts = await this.getPublishedPosts({ limit: 10 });
    return posts.find((p) => p.featured) || posts[0] || null;
  },

  // ==========================================
  // Editorial Management Methods
  // ==========================================

  /**
   * Creates a brand-new article draft in MongoDB.
   */
  async createDraft(
    initial: {
      title: string;
      category?: string;
      authorId?: string;
      authorName?: string;
    },
    creator: BlogActor
  ): Promise<Post> {
    const baseSlug = initial.title
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/(^-|-$)+/g, "") || `draft-${Date.now()}`;

    // Ensure unique slug
    let finalSlug = baseSlug;
    let counter = 1;
    while (await db.getPostBySlug(finalSlug)) {
      finalSlug = `${baseSlug}-${counter}`;
      counter++;
    }

    const now = new Date().toISOString();
    const id = crypto.randomUUID();

    // Resolve author from admin/sub-admin account
    const targetUserId = initial.authorId || creator.id;
    const authorUser = await db.getUserById(targetUserId);
    const authorId = authorUser ? authorUser.id : targetUserId;
    const authorObj = authorUser
      ? toBlogAuthor(authorUser)
      : {
          name: initial.authorName || creator.name,
          role: getAuthorRoleLabel(creator.role),
          avatarText: (initial.authorName || creator.name).slice(0, 2).toUpperCase(),
          avatarColor: "from-blue-600 to-indigo-600",
          bio: `${getAuthorRoleLabel(creator.role)} at Quto AI.`,
          profileSlug: (initial.authorName || creator.name).toLowerCase().replace(/[^a-z0-9]+/g, "-"),
        };

    const post: Post = {
      id,
      slug: finalSlug,
      title: initial.title,
      excerpt: "",
      category: initial.category || "Voice AI Guides",
      publishedDate: now,
      readTime: "4 min read",
      featured: false,
      status: "DRAFT",
      scheduledFor: null,
      authorId,
      author: authorObj,
      coverGradient: "from-blue-600/20 via-indigo-500/10 to-transparent",
      coverIllustration: "security",
      featuredImage: null,
      featuredImageAlt: null,
      sections: [
        {
          id: "section-1",
          title: "Introduction",
          content: "Begin drafting your analysis or guide here...",
        },
      ],
      contentHtml: "",
      executiveSummary: "",
      tableOfContents: [{ id: "section-1", title: "Introduction" }],
      relatedSlugs: [],
      seoTitle: `${initial.title} — Quto AI`,
      metaDescription: "",
      canonicalUrl: `https://qutoai.com/blog/${finalSlug}`,
      robots: "index, follow",
      ogTitle: initial.title,
      ogDescription: "",
      ogImage: null,
      version: 1,
      createdAt: now,
      updatedAt: now,
      createdBy: creator.email,
      updatedBy: creator.email,
    };

    const created = await db.createPost(post);

    // Initial revision snapshot
    await db.createRevision({
      id: crypto.randomUUID(),
      postId: created.id,
      version: 1,
      snapshot: { ...created },
      changeSummary: "Draft initialized",
      actorId: creator.id,
      actorName: creator.name,
      timestamp: now,
    });

    await db.logContentEvent({
      actorId: creator.id,
      actorName: creator.name,
      actorEmail: creator.email,
      actorRole: creator.role,
      location: resolveBlogActorLocation(creator.location),
      postId: created.id,
      postTitle: created.title,
      postSlug: created.slug,
      action: "POST_CREATED",
      details: { status: "DRAFT", slug: created.slug },
    });

    return created;
  },

  /**
   * Autosaves or updates post draft fields.
   * If the post is already PUBLISHED, updates are stored strictly in `post.workingDraft`.
   * The public live article remains 100% UNTOUCHED until "Update Live" is explicitly performed.
   */
  async updateDraft(
    id: string,
    updates: Partial<Post>,
    actor: BlogActor
  ): Promise<Post> {
    const existing = await db.getPostById(id);
    if (!existing) throw new Error("Article not found.");

    // Automatically calculate wordCount and readTime, and extract in-page Table of Contents
    let readStats;
    let processedHtml = updates.contentHtml;
    let autoToc;

    if (updates.contentHtml !== undefined) {
      const tocRes = generateTocAndInjectIds(updates.contentHtml);
      processedHtml = tocRes.html;
      if (tocRes.tableOfContents.length > 0) {
        autoToc = tocRes.tableOfContents;
      }
      readStats = calculateReadingStats(processedHtml);
    } else if (updates.sections !== undefined) {
      const content = updates.sections.map((s) => s.content).join(" ");
      readStats = calculateReadingStats(content);
    }

    const mergedUpdates: Partial<Post> = {
      ...updates,
      ...(processedHtml !== undefined ? { contentHtml: processedHtml } : {}),
      ...(autoToc ? { tableOfContents: autoToc } : {}),
      ...(readStats ? { readTime: readStats.readTime, wordCount: readStats.words } : {}),
      updatedBy: actor.email,
    };

    // ISOLATION: If the post is already PUBLISHED, save to workingDraft
    if (existing.status === "PUBLISHED") {
      const currentDraft = existing.workingDraft || {};
      const updatedDraft: PostWorkingDraft = {
        ...currentDraft,
        ...mergedUpdates,
        updatedAt: new Date().toISOString(),
        updatedBy: actor.email,
      };

      const updated = await db.updatePost(id, {
        workingDraft: updatedDraft,
        hasWorkingDraft: true,
        updatedAt: new Date().toISOString(),
        updatedBy: actor.email,
      });

      // Return synthetic view with working draft applied for the editor
      return {
        ...updated,
        ...updatedDraft,
        author: updatedDraft.author || updated.author,
        status: "PUBLISHED",
        hasWorkingDraft: true,
      };
    }

    // For DRAFT or SCHEDULED posts, updates apply directly to the draft document
    const updated = await db.updatePost(id, mergedUpdates);

    // Create periodic revision snapshot on significant edits
    if (updates.sections || updates.contentHtml || updates.title) {
      await db.createRevision({
        id: crypto.randomUUID(),
        postId: updated.id,
        version: updated.version,
        snapshot: { ...updated },
        changeSummary: "Content draft autosaved",
        actorId: actor.id,
        actorName: actor.name,
        timestamp: new Date().toISOString(),
      });
    }

    // Explicit audit for banner/featured image changes
    const bannerChanged =
      (updates.featuredImage !== undefined && updates.featuredImage !== existing.featuredImage) ||
      (updates.featuredImageId !== undefined && updates.featuredImageId !== existing.featuredImageId);

    if (bannerChanged) {
      await db.logContentEvent({
        actorId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actorRole: actor.role,
        location: resolveBlogActorLocation(actor.location),
        postId: existing.id,
        postTitle: existing.title,
        postSlug: existing.slug,
        action: "POST_BANNER_CHANGED",
        details: {
          previousBanner: existing.featuredImage || existing.featuredImageId || null,
          newBanner: updates.featuredImage || updates.featuredImageId || null,
          previousAlt: existing.featuredImageAlt || null,
          newAlt: updates.featuredImageAlt !== undefined ? updates.featuredImageAlt : existing.featuredImageAlt,
        },
      });
    }

    await db.logContentEvent({
      actorId: actor.id,
      actorName: actor.name,
      actorEmail: actor.email,
      actorRole: actor.role,
      location: resolveBlogActorLocation(actor.location),
      postId: updated.id,
      postTitle: updated.title,
      postSlug: updated.slug,
      action: "POST_EDITED",
      details: { version: updated.version },
    });

    return updated;
  },

  /**
   * Commits an uncommitted working draft on a PUBLISHED article to the live public website.
   * Performs preflight checks, registers redirects if the slug changed, creates a revision snapshot,
   * updates the live document, and revalidates public routes.
   */
  async updateLive(
    id: string,
    actor: BlogActor
  ): Promise<Post> {
    const existing = await db.getPostById(id);
    if (!existing) throw new Error("Article not found.");

    // Overlay workingDraft on top of live post to validate full article
    const working = existing.workingDraft;
    const postToValidate: Partial<Post> = working
      ? { ...existing, ...working }
      : { ...existing };

    const preflight = validatePostPreflight(postToValidate);
    if (!preflight.valid) {
      throw new Error(`Cannot update live article due to errors: ${preflight.errors.join("; ")}`);
    }

    const previousSlug = existing.slug;
    const newSlug = working?.slug || existing.slug;

    // If the slug changed on a published article, create a 301 permanent redirect
    if (newSlug !== previousSlug) {
      await db.createRedirect({
        id: crypto.randomUUID(),
        source: `/blog/${previousSlug}`,
        destination: `/blog/${newSlug}`,
        permanent: true,
        method: 301,
        createdAt: new Date().toISOString(),
        createdBy: actor.email,
      });

      await db.logContentEvent({
        actorId: actor.id,
        actorName: actor.name,
        actorEmail: actor.email,
        actorRole: actor.role,
        location: resolveBlogActorLocation(actor.location),
        postId: existing.id,
        postTitle: existing.title,
        postSlug: newSlug,
        action: "SLUG_CHANGED",
        details: {
          previousSlug,
          newSlug,
          redirectCreated: `/blog/${previousSlug} -> /blog/${newSlug}`,
        },
      });
    }

    const now = new Date().toISOString();
    const liveFieldsToCommit: Partial<Post> = {
      ...(working || {}),
      status: "PUBLISHED",
      workingDraft: null,
      hasWorkingDraft: false,
      version: (existing.version || 1) + 1,
      updatedAt: now,
      updatedBy: actor.email,
    };

    const updated = await db.updatePost(id, liveFieldsToCommit);

    // Create immutable revision snapshot
    await db.createRevision({
      id: crypto.randomUUID(),
      postId: updated.id,
      version: updated.version,
      snapshot: { ...updated },
      changeSummary: "Live update published to public website",
      actorId: actor.id,
      actorName: actor.name,
      timestamp: now,
    });

    await db.logContentEvent({
      actorId: actor.id,
      actorName: actor.name,
      actorEmail: actor.email,
      actorRole: actor.role,
      location: resolveBlogActorLocation(actor.location),
      postId: updated.id,
      postTitle: updated.title,
      postSlug: updated.slug,
      action: "POST_UPDATED_LIVE",
      details: { version: updated.version, slug: updated.slug },
    });

    // Safe media reference cleanup: If an old asset was replaced, check if safe to remove
    try {
      if (existing.featuredImageId && existing.featuredImageId !== updated.featuredImageId) {
        await safeCleanupUnreferencedMedia(existing.featuredImageId);
      } else if (existing.featuredImage && existing.featuredImage !== updated.featuredImage) {
        await safeCleanupUnreferencedMedia(existing.featuredImage);
      }
      if (existing.ogImageId && existing.ogImageId !== updated.ogImageId) {
        await safeCleanupUnreferencedMedia(existing.ogImageId);
      }
      if (existing.twitterImageId && existing.twitterImageId !== updated.twitterImageId) {
        await safeCleanupUnreferencedMedia(existing.twitterImageId);
      }
    } catch (cleanupErr) {
      console.warn("Non-fatal media cleanup notice:", cleanupErr);
    }

    // Revalidate public site
    safeRevalidatePath("/blog");
    safeRevalidatePath(`/blog/${updated.slug}`);
    if (previousSlug !== updated.slug) {
      safeRevalidatePath(`/blog/${previousSlug}`);
    }
    safeRevalidatePath("/sitemap.xml");

    return updated;
  },

  /**
   * Discards uncommitted working draft changes on a PUBLISHED article,
   * resetting back to the current live published version.
   */
  async discardWorkingDraft(
    id: string,
    actor: BlogActor
  ): Promise<Post> {
    const existing = await db.getPostById(id);
    if (!existing) throw new Error("Article not found.");

    const updated = await db.updatePost(id, {
      workingDraft: null,
      hasWorkingDraft: false,
      updatedAt: new Date().toISOString(),
      updatedBy: actor.email,
    });

    await db.logContentEvent({
      actorId: actor.id,
      actorName: actor.name,
      actorEmail: actor.email,
      actorRole: actor.role,
      location: resolveBlogActorLocation(actor.location),
      postId: updated.id,
      postTitle: updated.title,
      postSlug: updated.slug,
      action: "POST_EDITED",
      details: { change: "Working draft discarded; reverted to live published version" },
    });

    return updated;
  },

  /**
   * Publishes an article immediately with preflight validation.
   */
  async publish(
    id: string,
    actor: BlogActor
  ): Promise<Post> {
    const existing = await db.getPostById(id);
    if (!existing) throw new Error("Article not found.");

    const preflight = validatePostPreflight(existing);
    if (!preflight.valid) {
      throw new Error(`Cannot publish article due to preflight errors: ${preflight.errors.join("; ")}`);
    }

    const now = new Date().toISOString();
    const updated = await db.updatePost(id, {
      status: "PUBLISHED",
      publishedDate: now,
      scheduledFor: null,
      updatedBy: actor.email,
    });

    await db.createRevision({
      id: crypto.randomUUID(),
      postId: updated.id,
      version: updated.version,
      snapshot: { ...updated },
      changeSummary: "Published to public website",
      actorId: actor.id,
      actorName: actor.name,
      timestamp: now,
    });

    await db.logContentEvent({
      actorId: actor.id,
      actorName: actor.name,
      actorEmail: actor.email,
      actorRole: actor.role,
      location: resolveBlogActorLocation(actor.location),
      postId: updated.id,
      postTitle: updated.title,
      postSlug: updated.slug,
      action: "POST_PUBLISHED",
      details: { publishedAt: now, slug: updated.slug },
    });

    // Revalidate public site
    safeRevalidatePath("/blog");
    safeRevalidatePath(`/blog/${updated.slug}`);
    safeRevalidatePath("/sitemap.xml");

    return updated;
  },

  /**
   * Schedules an article for future automated publication.
   */
  async schedule(
    id: string,
    scheduledForIso: string,
    actor: BlogActor
  ): Promise<Post> {
    const existing = await db.getPostById(id);
    if (!existing) throw new Error("Article not found.");

    const targetDate = new Date(scheduledForIso);
    if (isNaN(targetDate.getTime()) || targetDate.getTime() <= Date.now()) {
      throw new Error("Scheduled publication date must be a valid future timestamp.");
    }

    const preflight = validatePostPreflight(existing);
    if (!preflight.valid) {
      throw new Error(`Preflight errors must be resolved before scheduling: ${preflight.errors.join("; ")}`);
    }

    const updated = await db.updatePost(id, {
      status: "SCHEDULED",
      scheduledFor: targetDate.toISOString(),
      updatedBy: actor.email,
    });

    await db.logContentEvent({
      actorId: actor.id,
      actorName: actor.name,
      actorEmail: actor.email,
      actorRole: actor.role,
      location: resolveBlogActorLocation(actor.location),
      postId: updated.id,
      postTitle: updated.title,
      postSlug: updated.slug,
      action: "POST_SCHEDULED",
      details: { scheduledFor: targetDate.toISOString() },
    });

    return updated;
  },

  /**
   * Archives a published article (unpublishes from public views).
   */
  async archive(
    id: string,
    actor: BlogActor
  ): Promise<Post> {
    const existing = await db.getPostById(id);
    if (!existing) throw new Error("Article not found.");

    const updated = await db.updatePost(id, {
      status: "ARCHIVED",
      updatedBy: actor.email,
    });

    await db.logContentEvent({
      actorId: actor.id,
      actorName: actor.name,
      actorEmail: actor.email,
      actorRole: actor.role,
      location: resolveBlogActorLocation(actor.location),
      postId: updated.id,
      postTitle: updated.title,
      postSlug: updated.slug,
      action: "POST_ARCHIVED",
      details: { previousStatus: existing.status },
    });

    safeRevalidatePath("/blog");
    safeRevalidatePath(`/blog/${updated.slug}`);
    safeRevalidatePath("/sitemap.xml");

    return updated;
  },

  /**
   * Duplicates an existing article into a new draft.
   */
  async duplicate(
    id: string,
    actor: BlogActor
  ): Promise<Post> {
    const existing = await db.getPostById(id);
    if (!existing) throw new Error("Article not found.");

    const baseSlug = `${existing.slug}-copy`;
    let finalSlug = baseSlug;
    let counter = 1;
    while (await db.getPostBySlug(finalSlug)) {
      finalSlug = `${baseSlug}-${counter}`;
      counter++;
    }

    const now = new Date().toISOString();
    const newId = crypto.randomUUID();

    const duplicatePost: Post = {
      ...existing,
      id: newId,
      slug: finalSlug,
      title: `${existing.title} (Copy)`,
      status: "DRAFT",
      scheduledFor: null,
      publishedDate: now,
      version: 1,
      createdAt: now,
      updatedAt: now,
      createdBy: actor.email,
      updatedBy: actor.email,
    };

    const created = await db.createPost(duplicatePost);

    await db.createRevision({
      id: crypto.randomUUID(),
      postId: created.id,
      version: 1,
      snapshot: { ...created },
      changeSummary: `Duplicated from ${existing.title}`,
      actorId: actor.id,
      actorName: actor.name,
      timestamp: now,
    });

    await db.logContentEvent({
      actorId: actor.id,
      actorName: actor.name,
      actorEmail: actor.email,
      actorRole: actor.role,
      location: resolveBlogActorLocation(actor.location),
      postId: created.id,
      postTitle: created.title,
      postSlug: created.slug,
      action: "POST_DUPLICATED",
      details: { originalPostId: existing.id },
    });

    return created;
  },

  /**
   * Restores an older revision as a brand-new revision without destroying history.
   */
  async restoreRevision(
    revisionId: string,
    actor: BlogActor
  ): Promise<Post> {
    const revision = await db.getRevisionById(revisionId);
    if (!revision) throw new Error("Revision snapshot not found.");

    const current = await db.getPostById(revision.postId);
    if (!current) throw new Error("Parent article not found.");

    const restoredSnapshot = revision.snapshot;

    const updated = await db.updatePost(current.id, {
      title: restoredSnapshot.title || current.title,
      excerpt: restoredSnapshot.excerpt || current.excerpt,
      sections: restoredSnapshot.sections || current.sections,
      contentHtml: restoredSnapshot.contentHtml ?? current.contentHtml,
      category: restoredSnapshot.category || current.category,
      coverIllustration: restoredSnapshot.coverIllustration || current.coverIllustration,
      coverGradient: restoredSnapshot.coverGradient || current.coverGradient,
      updatedBy: actor.email,
    });

    await db.createRevision({
      id: crypto.randomUUID(),
      postId: updated.id,
      version: updated.version,
      snapshot: { ...updated },
      changeSummary: `Restored from Version ${revision.version}`,
      actorId: actor.id,
      actorName: actor.name,
      timestamp: new Date().toISOString(),
    });

    await db.logContentEvent({
      actorId: actor.id,
      actorName: actor.name,
      actorEmail: actor.email,
      actorRole: actor.role,
      location: resolveBlogActorLocation(actor.location),
      postId: updated.id,
      postTitle: updated.title,
      postSlug: updated.slug,
      action: "REVISION_RESTORED",
      details: { restoredVersion: revision.version },
    });

    if (updated.status === "PUBLISHED") {
      safeRevalidatePath("/blog");
      safeRevalidatePath(`/blog/${updated.slug}`);
    }

    return updated;
  },

  /**
   * Server scheduler worker: Inspects all scheduled articles and automatically publishes due posts.
   */
  async processDueScheduledPosts(): Promise<number> {
    try {
      const scheduledPosts = await db.getPosts({ status: "SCHEDULED", limit: 50 });
      if (scheduledPosts.length === 0) return 0;

      const now = Date.now();
      let publishedCount = 0;

      for (const post of scheduledPosts) {
        if (post.scheduledFor && new Date(post.scheduledFor).getTime() <= now) {
          const publishedIso = new Date().toISOString();
          await db.updatePost(post.id, {
            status: "PUBLISHED",
            publishedDate: publishedIso,
            scheduledFor: null,
            updatedBy: "SYSTEM_SCHEDULER",
          });

          await db.logContentEvent({
            actorId: "SYSTEM_SCHEDULER",
            actorName: "Automated Scheduler",
            actorEmail: "scheduler@qutoai.com",
            actorRole: "MAIN_ADMIN",
            postId: post.id,
            postTitle: post.title,
            postSlug: post.slug,
            action: "POST_PUBLISHED",
            details: {
              note: "Automated release triggered by due schedule time",
              scheduledFor: post.scheduledFor,
              publishedAt: publishedIso,
            },
          });

          publishedCount++;
        }
      }

      if (publishedCount > 0) {
        safeRevalidatePath("/blog");
        safeRevalidatePath("/sitemap.xml");
      }

      return publishedCount;
    } catch (e) {
      console.error("[blogService.processDueScheduledPosts] Error:", e);
      return 0;
    }
  },
};
