/**
 * CMS data layer — Prisma-backed.
 *
 * Drop-in replacement for `lib/wp.ts`. Mirrors the same public surface
 * (`listPosts`, `getPostBySlug`, `getCategoryBySlug`, `getTagBySlug`,
 * `getAllCategories`, `getAuthorBySlug`, `searchPosts`) and returns the same
 * `Post` / `WPTerm` shapes from `wp-types`, so existing pages and components
 * don't need to change beyond their import path.
 *
 * All public-facing reads are filtered to `status === "published"` and
 * `publishedAt <= now()` so future-dated drafts never leak.
 */

import type { Prisma } from "@prisma/client";
import { prisma } from "./db";
import type { Post, WPAuthor, WPTerm } from "./wp-types";
import { readTime } from "./utils";

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/**
 * Legacy WordPress numeric category IDs → DB slug. Lets pages that still pass
 * old WP IDs (e.g. the homepage CAT_IDS map) keep working without a refactor.
 */
const CAT_ID_TO_SLUG: Record<number, string> = {
  148: "tin-nong",
  36: "phan-tich",
  37: "crypto",
  252: "vang",
  40: "chung-khoan",
  39: "forex",
};

/** Filter applied to every public list / get query. */
function publishedWhere(): Prisma.ArticleWhereInput {
  return { status: "published", publishedAt: { lte: new Date() } };
}

// Looks like a Prisma cuid (starts with `c`, ~25 lowercase alphanumerics).
const CUID_RE = /^c[a-z0-9]{20,}$/i;

// ---------------------------------------------------------------------------
// Mapping helpers
// ---------------------------------------------------------------------------

const ARTICLE_SELECT = {
  id: true,
  slug: true,
  title: true,
  excerpt: true,
  content: true,
  coverImage: true,
  publishedAt: true,
  updatedAt: true,
  noIndex: true,
  category: { select: { id: true, slug: true, name: true } },
  author: { select: { id: true, name: true, email: true, image: true } },
  tags: {
    select: { tag: { select: { id: true, slug: true, name: true } } },
  },
} satisfies Prisma.ArticleSelect;

type DbArticle = Prisma.ArticleGetPayload<{ select: typeof ARTICLE_SELECT }>;

function authorSlugFromEmail(email: string | null | undefined): string {
  if (!email) return "finzone";
  const local = email.split("@")[0] ?? "finzone";
  return local.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
}

function mapAuthor(a: DbArticle["author"]): Post["author"] {
  return {
    id: a.id,
    name: a.name || "Finzone",
    slug: authorSlugFromEmail(a.email),
    avatar: a.image ?? undefined,
    bio: undefined,
  };
}

function mapPost(article: DbArticle): Post {
  const date = (article.publishedAt ?? article.updatedAt).toISOString();
  return {
    id: article.id,
    slug: article.slug,
    link: `/${article.slug}`,
    date,
    modified: article.updatedAt.toISOString(),
    title: article.title,
    excerpt: article.excerpt ?? "",
    content: article.content ?? undefined,
    readMinutes: readTime(article.content ?? article.excerpt ?? ""),
    noIndex: article.noIndex,
    author: mapAuthor(article.author),
    featured: article.coverImage
      ? { url: article.coverImage, alt: article.title }
      : undefined,
    categories: article.category
      ? [
          {
            id: article.category.id,
            slug: article.category.slug,
            name: article.category.name,
            taxonomy: "category",
          },
        ]
      : [],
    tags: article.tags.map(({ tag }) => ({
      id: tag.id,
      slug: tag.slug,
      name: tag.name,
      taxonomy: "post_tag",
    })),
  };
}

// ---------------------------------------------------------------------------
// ID/slug resolution for filter inputs
// ---------------------------------------------------------------------------

async function resolveCategorySlugs(
  input: ReadonlyArray<number | string> | undefined
): Promise<string[]> {
  if (!input || input.length === 0) return [];
  const slugs = new Set<string>();
  const cuids: string[] = [];

  for (const x of input) {
    if (typeof x === "number") {
      const s = CAT_ID_TO_SLUG[x];
      if (s) slugs.add(s);
      continue;
    }
    if (CUID_RE.test(x)) cuids.push(x);
    else slugs.add(x);
  }

  if (cuids.length) {
    const cats = await prisma.category.findMany({
      where: { id: { in: cuids } },
      select: { slug: true },
    });
    for (const c of cats) slugs.add(c.slug);
  }
  return [...slugs];
}

async function resolveTagSlugs(
  input: ReadonlyArray<number | string> | undefined
): Promise<string[]> {
  if (!input || input.length === 0) return [];
  const slugs = new Set<string>();
  const cuids: string[] = [];

  for (const x of input) {
    // Numeric WP tag IDs have no DB mapping; ignore them.
    if (typeof x === "number") continue;
    if (CUID_RE.test(x)) cuids.push(x);
    else slugs.add(x);
  }

  if (cuids.length) {
    const tags = await prisma.tag.findMany({
      where: { id: { in: cuids } },
      select: { slug: true },
    });
    for (const t of tags) slugs.add(t.slug);
  }
  return [...slugs];
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

export interface ListOpts {
  page?: number;
  perPage?: number;
  /** Accepts legacy WP numeric IDs (mapped via CAT_ID_TO_SLUG), Prisma cuids, or raw category slugs. */
  categories?: ReadonlyArray<number | string>;
  excludeCategories?: ReadonlyArray<number | string>;
  /** Tag filter: cuids or slugs. Legacy numeric WP tag IDs are ignored. */
  tags?: ReadonlyArray<number | string>;
  /** Author cuid (User.id). Legacy WP numeric IDs are ignored. */
  author?: number | string;
  search?: string;
  orderBy?: "date" | "modified" | "relevance" | "title";
  order?: "asc" | "desc";
  /** Promotion */
  featured?: boolean; // only featured articles
  pinnedFirst?: boolean; // sort pinned to top, then by orderBy (default true for category/tag listings)
  /** Kept for API parity with lib/wp.ts; no-op for Prisma reads. */
  revalidate?: number;
  tags_revalidate?: string[];
}

export async function listPosts(
  opts: ListOpts = {}
): Promise<{ posts: Post[]; total: number; totalPages: number }> {
  const page = Math.max(1, opts.page ?? 1);
  const perPage = Math.max(1, opts.perPage ?? 10);

  const [catSlugs, excludeCatSlugs, tagSlugs] = await Promise.all([
    resolveCategorySlugs(opts.categories),
    resolveCategorySlugs(opts.excludeCategories),
    resolveTagSlugs(opts.tags),
  ]);

  const where: Prisma.ArticleWhereInput = { ...publishedWhere() };

  if (catSlugs.length) {
    where.category = { slug: { in: catSlugs } };
  }
  if (excludeCatSlugs.length) {
    where.NOT = [{ category: { slug: { in: excludeCatSlugs } } }];
  }
  if (tagSlugs.length) {
    where.tags = { some: { tag: { slug: { in: tagSlugs } } } };
  }
  if (typeof opts.author === "string" && opts.author) {
    where.authorId = opts.author;
  }
  if (opts.search && opts.search.trim()) {
    const q = opts.search.trim();
    where.OR = [
      { title: { contains: q, mode: "insensitive" } },
      { excerpt: { contains: q, mode: "insensitive" } },
      { content: { contains: q, mode: "insensitive" } },
    ];
  }
  if (opts.featured) {
    where.featured = true;
  }

  const direction: Prisma.SortOrder = opts.order === "asc" ? "asc" : "desc";
  let primaryOrderBy: Prisma.ArticleOrderByWithRelationInput;
  switch (opts.orderBy) {
    case "title":
      primaryOrderBy = { title: direction };
      break;
    case "modified":
      primaryOrderBy = { updatedAt: direction };
      break;
    // "relevance" has no native Prisma equivalent; fall back to recency.
    case "relevance":
    case "date":
    default:
      primaryOrderBy = { publishedAt: direction };
      break;
  }

  // Default: pinned articles float to top for category/tag listings.
  // For homepage hero (featured=true) pinning doesn't matter.
  const pinnedFirst = opts.pinnedFirst ?? (catSlugs.length > 0 || tagSlugs.length > 0);
  const orderBy: Prisma.ArticleOrderByWithRelationInput[] = pinnedFirst
    ? [{ pinned: "desc" }, { pinOrder: "asc" }, primaryOrderBy]
    : [primaryOrderBy];

  const [total, articles] = await Promise.all([
    prisma.article.count({ where }),
    prisma.article.findMany({
      where,
      orderBy,
      skip: (page - 1) * perPage,
      take: perPage,
      select: ARTICLE_SELECT,
    }),
  ]);

  const totalPages = Math.max(1, Math.ceil(total / perPage));
  return { posts: articles.map(mapPost), total, totalPages };
}

export async function getPostBySlug(slug: string): Promise<Post | null> {
  const article = await prisma.article.findFirst({
    where: { slug, ...publishedWhere() },
    select: ARTICLE_SELECT,
  });
  return article ? mapPost(article) : null;
}

export async function getCategoryBySlug(slug: string): Promise<WPTerm | null> {
  const cat = await prisma.category.findUnique({
    where: { slug },
    select: { id: true, slug: true, name: true, description: true },
  });
  if (!cat) return null;
  const count = await prisma.article.count({
    where: { ...publishedWhere(), categoryId: cat.id },
  });
  return {
    id: cat.id,
    slug: cat.slug,
    name: cat.name,
    description: cat.description ?? undefined,
    count,
    taxonomy: "category",
  };
}

export async function getTagBySlug(slug: string): Promise<WPTerm | null> {
  const tag = await prisma.tag.findUnique({
    where: { slug },
    select: { id: true, slug: true, name: true },
  });
  if (!tag) return null;
  const count = await prisma.article.count({
    where: {
      ...publishedWhere(),
      tags: { some: { tagId: tag.id } },
    },
  });
  return {
    id: tag.id,
    slug: tag.slug,
    name: tag.name,
    count,
    taxonomy: "post_tag",
  };
}

export async function getAllCategories(): Promise<WPTerm[]> {
  const cats = await prisma.category.findMany({
    orderBy: [{ order: "asc" }, { name: "asc" }],
    select: { id: true, slug: true, name: true, description: true },
  });
  if (cats.length === 0) return [];

  // One count query keyed by categoryId is cheaper than N counts.
  const counts = await prisma.article.groupBy({
    by: ["categoryId"],
    where: { ...publishedWhere(), categoryId: { in: cats.map((c) => c.id) } },
    _count: { _all: true },
  });
  const countByCat = new Map<string, number>();
  for (const row of counts) {
    if (row.categoryId) countByCat.set(row.categoryId, row._count._all);
  }

  return cats.map((c) => ({
    id: c.id,
    slug: c.slug,
    name: c.name,
    description: c.description ?? undefined,
    count: countByCat.get(c.id) ?? 0,
    taxonomy: "category",
  }));
}

/**
 * "Author by slug" — slug here is the email local-part (everything before `@`).
 * Returns a WPAuthor-compatible record so legacy pages that read
 * `author.name`, `author.description`, `author.avatar_urls["96"]`, `author.id`
 * keep working.
 */
export async function getAuthorBySlug(slug: string): Promise<WPAuthor | null> {
  const lower = slug.toLowerCase();
  const user = await prisma.user.findFirst({
    where: { email: { startsWith: lower + "@", mode: "insensitive" } },
    select: { id: true, name: true, email: true, image: true },
  });
  if (!user) return null;
  return {
    id: user.id,
    name: user.name || lower,
    slug: authorSlugFromEmail(user.email),
    description: undefined,
    avatar_urls: user.image ? { "48": user.image, "96": user.image } : undefined,
  };
}

export async function searchPosts(q: string, page = 1, perPage = 10) {
  return listPosts({ search: q, page, perPage, orderBy: "date", order: "desc" });
}
