import "server-only";

import type { Prisma } from "@/generated/prisma/client";
import { parseSnapshot, blogEditorInclude, toFormValues } from "@/features/blogs/service";
import { db } from "@/lib/db";
import { readingMinutes } from "@/lib/sanitize";
import { absoluteMediaUrl, absolutizeContentUrls, blogPath } from "@/lib/site";
import { slugify } from "@/lib/slug";

import type {
  ApiAuthor,
  ApiCategory,
  ApiPost,
  ApiPostListResponse,
  ApiPostSummary,
  ApiSitemapResponse,
} from "./types";

export const RELATED_LIMIT = 3;

/**
 * Articles readers can see: published, or scheduled with their time already
 * past (so they appear on the minute, even before the cron job flips them).
 */
export function liveWhere(now = new Date()): Prisma.BlogWhereInput {
  return {
    OR: [{ status: "PUBLISHED" }, { status: "SCHEDULED", scheduledAt: { lte: now } }],
  };
}

const summarySelect = {
  id: true,
  slug: true,
  title: true,
  excerpt: true,
  featuredImage: true,
  featuredImageAlt: true,
  featuredImageTitle: true,
  publishedAt: true,
  scheduledAt: true,
  contentUpdatedAt: true,
  createdAt: true,
  readingMinutes: true,
  category: { select: { name: true, slug: true, description: true } },
  author: { select: { name: true, slug: true, role: true, bio: true, profileImage: true } },
} satisfies Prisma.BlogSelect;

type SummaryRow = Prisma.BlogGetPayload<{ select: typeof summarySelect }>;

type ImageSizes = Map<string, { width: number | null; height: number | null }>;

/** Pixel sizes of library images by URL, so the website can reserve space for them. */
async function imageSizes(urls: (string | null | undefined)[]): Promise<ImageSizes> {
  const unique = [...new Set(urls.filter((url): url is string => Boolean(url)))];
  if (!unique.length) return new Map();
  const rows = await db.media.findMany({
    where: { url: { in: unique } },
    select: { url: true, width: true, height: true },
  });
  return new Map(rows.map((row) => [row.url, { width: row.width, height: row.height }]));
}

function toApiCategory(category: SummaryRow["category"]): ApiCategory | null {
  return category
    ? { name: category.name, slug: category.slug, description: category.description }
    : null;
}

function toApiAuthor(author: SummaryRow["author"]): ApiAuthor | null {
  return author
    ? {
        name: author.name,
        slug: author.slug,
        role: author.role,
        bio: author.bio,
        image: absoluteMediaUrl(author.profileImage),
      }
    : null;
}

type SummarySource = {
  id: string;
  slug: string;
  title: string;
  excerpt: string;
  featuredImage: string | null;
  featuredImageAlt: string | null;
  featuredImageTitle: string | null;
  publishedAt: Date | null;
  scheduledAt: Date | null;
  contentUpdatedAt: Date | null;
  createdAt: Date;
  readingMinutes: number;
  category: SummaryRow["category"];
  author: SummaryRow["author"];
};

function toSummary(row: SummarySource, sizes: ImageSizes): ApiPostSummary {
  const published = row.publishedAt ?? row.scheduledAt ?? row.createdAt;
  const size = row.featuredImage ? sizes.get(row.featuredImage) : undefined;
  return {
    id: row.id,
    slug: row.slug,
    path: blogPath(row.slug),
    title: row.title,
    excerpt: row.excerpt,
    featuredImage: row.featuredImage
      ? {
          url: absoluteMediaUrl(row.featuredImage)!,
          alt: row.featuredImageAlt ?? "",
          title: row.featuredImageTitle,
          width: size?.width ?? null,
          height: size?.height ?? null,
        }
      : null,
    category: toApiCategory(row.category),
    author: toApiAuthor(row.author),
    publishedAt: published.toISOString(),
    updatedAt: (row.contentUpdatedAt && row.contentUpdatedAt > published
      ? row.contentUpdatedAt
      : published
    ).toISOString(),
    readingMinutes: row.readingMinutes,
  };
}

// ---------------------------------------------------------------------------
// Listing
// ---------------------------------------------------------------------------

export async function listPublishedPosts({
  page,
  perPage,
  category,
}: {
  page: number;
  perPage: number;
  category?: string;
}): Promise<ApiPostListResponse> {
  const now = new Date();
  const where: Prisma.BlogWhereInput = {
    AND: [liveWhere(now), ...(category ? [{ category: { slug: category } }] : [])],
  };

  const [rows, total, categories] = await Promise.all([
    db.blog.findMany({
      where,
      select: summarySelect,
      orderBy: [{ publishedAt: "desc" }, { id: "asc" }],
      skip: (page - 1) * perPage,
      take: perPage,
    }),
    db.blog.count({ where }),
    db.category.findMany({
      where: { status: "ACTIVE", blogs: { some: liveWhere(now) } },
      select: { name: true, slug: true, description: true },
      orderBy: { name: "asc" },
    }),
  ]);

  const sizes = await imageSizes(rows.map((row) => row.featuredImage));
  return {
    posts: rows.map((row) => toSummary(row, sizes)),
    pagination: {
      page,
      perPage,
      total,
      totalPages: Math.max(1, Math.ceil(total / perPage)),
    },
    categories,
  };
}

export async function listSitemapPosts(): Promise<ApiSitemapResponse> {
  const rows = await db.blog.findMany({
    where: { AND: [liveWhere(), { robotsIndex: true }] },
    select: { slug: true, publishedAt: true, contentUpdatedAt: true, updatedAt: true },
    orderBy: { publishedAt: "desc" },
  });
  return {
    posts: rows.map((row) => ({
      path: blogPath(row.slug),
      updatedAt: (row.contentUpdatedAt ?? row.publishedAt ?? row.updatedAt).toISOString(),
    })),
  };
}

// ---------------------------------------------------------------------------
// Related articles
// ---------------------------------------------------------------------------

/**
 * Picks related articles: same category first, then shared tags, then the
 * newest articles, so the section is never empty on a young blog.
 */
export async function automaticRelated({
  excludeId,
  categoryId,
  tagSlugs,
  limit = RELATED_LIMIT,
}: {
  excludeId: string | null;
  categoryId: string | null;
  tagSlugs: string[];
  limit?: number;
}) {
  const now = new Date();
  const picked: SummaryRow[] = [];
  const exclude = () => [...(excludeId ? [excludeId] : []), ...picked.map((row) => row.id)];

  const take = async (extra: Prisma.BlogWhereInput) => {
    if (picked.length >= limit) return;
    const rows = await db.blog.findMany({
      where: { AND: [liveWhere(now), extra, { id: { notIn: exclude() } }] },
      select: summarySelect,
      orderBy: { publishedAt: "desc" },
      take: limit - picked.length,
    });
    picked.push(...rows);
  };

  if (categoryId) await take({ categoryId });
  if (tagSlugs.length) await take({ tags: { some: { tag: { slug: { in: tagSlugs } } } } });
  await take({});

  return picked;
}

// ---------------------------------------------------------------------------
// Single article
// ---------------------------------------------------------------------------

const postInclude = {
  ...blogEditorInclude,
  category: { select: { name: true, slug: true, description: true } },
  author: { select: { name: true, slug: true, role: true, bio: true, profileImage: true } },
} satisfies Prisma.BlogInclude;

type PostRow = Prisma.BlogGetPayload<{ include: typeof postInclude }>;

async function buildPost(row: PostRow): Promise<ApiPost> {
  const values = toFormValues(row);

  let relatedRows: SummaryRow[];
  if (values.relatedMode === "MANUAL" && values.relatedBlogIds.length) {
    const rows = await db.blog.findMany({
      where: { AND: [liveWhere(), { id: { in: values.relatedBlogIds } }] },
      select: summarySelect,
    });
    const byId = new Map(rows.map((related) => [related.id, related]));
    relatedRows = values.relatedBlogIds
      .map((id) => byId.get(id))
      .filter((related): related is SummaryRow => Boolean(related))
      .slice(0, RELATED_LIMIT);
  } else {
    relatedRows = await automaticRelated({
      excludeId: row.id,
      categoryId: row.categoryId,
      tagSlugs: values.tags.map((tag) => slugify(tag, 60)),
    });
  }

  const sizes = await imageSizes([row.featuredImage, ...relatedRows.map((related) => related.featuredImage)]);
  const summary = toSummary(row, sizes);
  const seoTitle = row.seoTitle || row.title;
  const seoDescription = row.metaDescription || row.excerpt;

  return {
    ...summary,
    contentHtml: absolutizeContentUrls(row.content),
    tags: row.tags.map(({ tag }) => ({ name: tag.name, slug: tag.slug })),
    faqs: row.faqs.map(({ id, question, answer }) => ({ id, question, answer })),
    faqSchema: row.faqSchemaEnabled,
    cta:
      row.ctaEnabled && row.ctaTitle && row.ctaButtonText && row.ctaButtonUrl
        ? {
            title: row.ctaTitle,
            description: row.ctaDescription,
            buttonText: row.ctaButtonText,
            buttonUrl: row.ctaButtonUrl,
            image: absoluteMediaUrl(row.ctaImage),
          }
        : null,
    related: relatedRows.map((related) => toSummary(related, sizes)),
    seo: {
      title: seoTitle,
      description: seoDescription,
      canonicalUrl: row.canonicalUrl,
      focusKeyword: row.focusKeyword,
      ogTitle: row.ogTitle || seoTitle,
      ogDescription: row.ogDescription || seoDescription,
      ogImage: absoluteMediaUrl(row.ogImage || row.featuredImage),
      robots: { index: row.robotsIndex, follow: row.robotsFollow },
    },
  };
}

export async function getPublishedPost(slug: string) {
  const row = await db.blog.findFirst({
    where: { AND: [{ slug }, liveWhere()] },
    include: postInclude,
  });
  return row ? buildPost(row) : null;
}

/** The redirect for an old article path, counting the hit. */
export async function findRedirect(path: string) {
  const redirect = await db.redirect.findUnique({ where: { fromPath: path } });
  if (!redirect) return null;
  await db.redirect.update({
    where: { id: redirect.id },
    data: { hits: { increment: 1 }, lastHitAt: new Date() },
  });
  return { destination: redirect.toPath, permanent: redirect.permanent };
}

/**
 * Any article by ID for previews, with autosaved edits applied so editors see
 * exactly what they're working on.
 */
export async function getPreviewPost(id: string) {
  const row = await db.blog.findUnique({ where: { id }, include: postInclude });
  if (!row) return null;

  const pending = row.autosave ? parseSnapshot(row.autosave, toFormValues(row)) : null;
  if (!pending) return { post: await buildPost(row), status: row.status };

  const [category, author, tags] = await Promise.all([
    pending.categoryId
      ? db.category.findUnique({
          where: { id: pending.categoryId },
          select: { name: true, slug: true, description: true },
        })
      : null,
    pending.authorId
      ? db.author.findUnique({
          where: { id: pending.authorId },
          select: { name: true, slug: true, role: true, bio: true, profileImage: true },
        })
      : null,
    Promise.resolve(pending.tags.map((name) => ({ tag: { name, slug: slugify(name, 60) } }))),
  ]);

  const merged = {
    ...row,
    title: pending.title,
    slug: pending.slug,
    excerpt: pending.excerpt,
    content: pending.content,
    readingMinutes: readingMinutes(pending.content),
    featuredImage: pending.featuredImage || null,
    featuredImageAlt: pending.featuredImageAlt || null,
    featuredImageTitle: pending.featuredImageTitle || null,
    categoryId: pending.categoryId,
    category,
    authorId: pending.authorId,
    author,
    seoTitle: pending.seoTitle || null,
    metaDescription: pending.metaDescription || null,
    canonicalUrl: pending.canonicalUrl || null,
    focusKeyword: pending.focusKeyword || null,
    ogTitle: pending.ogTitle || null,
    ogDescription: pending.ogDescription || null,
    ogImage: pending.ogImage || null,
    robotsIndex: pending.robotsIndex,
    robotsFollow: pending.robotsFollow,
    faqSchemaEnabled: pending.faqSchemaEnabled,
    ctaEnabled: pending.ctaEnabled,
    ctaTitle: pending.ctaTitle || null,
    ctaDescription: pending.ctaDescription || null,
    ctaButtonText: pending.ctaButtonText || null,
    ctaButtonUrl: pending.ctaButtonUrl || null,
    ctaImage: pending.ctaImage || null,
    relatedMode: pending.relatedMode,
    faqs: pending.faqs.map((faq, position) => ({
      ...faq,
      blogId: row.id,
      position,
      createdAt: row.createdAt,
      updatedAt: row.updatedAt,
    })),
    tags: tags.map(({ tag }) => ({
      blogId: row.id,
      tagId: tag.slug,
      createdAt: row.createdAt,
      tag: { id: tag.slug, name: tag.name, slug: tag.slug, createdAt: row.createdAt, updatedAt: row.updatedAt },
    })),
    relatedBlogs: pending.relatedBlogIds.map((relatedBlogId) => ({ relatedBlogId })),
  } satisfies PostRow;

  return { post: await buildPost(merged), status: row.status };
}
