import "server-only";

import { cache } from "react";

import type { AuthorOption } from "@/features/authors/queries";
import { getAuthorOptions } from "@/features/authors/queries";
import type { CategoryOption } from "@/features/categories/queries";
import { getCategoryOptions } from "@/features/categories/queries";
import { getBlogDefaults } from "@/features/settings/queries";
import {
  CmsError,
  cmsData,
  cmsPaths,
  dateOf,
  field,
  idOf,
  isMissingRoute,
  list,
  num,
  paginated,
  relation,
  str,
  strOrNull,
  withFallback,
} from "@/lib/cms/api";
import type { ContentStatus } from "@/lib/cms/types";

import type { BlogEditorMeta, BlogFormValues } from "./schemas";
import {
  emptyFormValues,
  pendingFromRow,
  readBlogPayload,
  rowRelations,
  rowToListFields,
  statusOf,
  type RevisionReason,
} from "./service";

/** Sort options for the list; the key is sent to the CMS as `sort`. */
export const BLOG_SORTS = {
  updated: { label: "Last updated" },
  newest: { label: "Newest first" },
  oldest: { label: "Oldest first" },
  published: { label: "Publish date" },
  title: { label: "Title A–Z" },
} as const;

export type BlogSort = keyof typeof BLOG_SORTS;

export type BlogListFilters = {
  query?: string;
  status?: ContentStatus;
  categoryId?: string;
  authorId?: string;
  sort?: BlogSort;
  page?: number;
  pageSize?: number;
};

/** Names for rows that carry only `category_id` / `author_id`. */
async function taxonomyNames() {
  const [categories, authors] = await Promise.all([
    getCategoryOptions(),
    getAuthorOptions(),
  ]);
  return {
    categories: new Map(
      categories.map((category) => [category.id, category.name]),
    ),
    authors: new Map(
      authors.map((author) => [
        author.id,
        { name: author.name, profileImage: author.profileImage },
      ]),
    ),
  };
}

type Names = Awaited<ReturnType<typeof taxonomyNames>>;

const toListItem = (names: Names) => (row: unknown) => ({
  ...rowToListFields(row),
  ...rowRelations(row, names),
});

export type BlogListItem = ReturnType<ReturnType<typeof toListItem>>;

/** `GET admin/blogs`: Laravel's paginator of `blog_posts` rows. */
function fetchBlogPage(filters: BlogListFilters) {
  return cmsData<unknown>(cmsPaths.blogs, {
    query: {
      q: filters.query?.trim(),
      status: filters.status,
      categoryId: filters.categoryId,
      authorId: filters.authorId,
      sort: filters.sort ?? "updated",
      page: filters.page ?? 1,
      pageSize: filters.pageSize ?? 15,
    },
  });
}

export async function listBlogs(
  filters: BlogListFilters,
): Promise<{ items: BlogListItem[]; total: number }> {
  const [body, names] = await Promise.all([
    fetchBlogPage(filters),
    taxonomyNames(),
  ]);
  return paginated(body, toListItem(names));
}

/**
 * Article counts per status. Asks the CMS for one row of each status; if it
 * ignores the filter (the row's status doesn't match), counts every row instead.
 */
export async function getBlogStatusCounts(): Promise<
  Record<ContentStatus, number> & { total: number }
> {
  const statuses: ContentStatus[] = ["PUBLISHED", "DRAFT", "SCHEDULED"];
  const [all, ...byStatus] = await Promise.all([
    fetchBlogPage({ pageSize: 1 }),
    ...statuses.map((status) => fetchBlogPage({ status, pageSize: 1 })),
  ]);
  const total = num(all, "total", list(all, "data").length);

  const filterWorks = byStatus.every((body, index) => {
    const first = list(body, "data")[0];
    return !first || statusOf(first) === statuses[index];
  });
  if (filterWorks) {
    const counts = Object.fromEntries(
      statuses.map((status, index) => [status, num(byStatus[index], "total")]),
    ) as Record<ContentStatus, number>;
    return { ...counts, total };
  }

  const counts: Record<ContentStatus, number> = {
    PUBLISHED: 0,
    DRAFT: 0,
    SCHEDULED: 0,
  };
  for (let page = 1, last = 1; page <= Math.min(last, 20); page += 1) {
    const body = await fetchBlogPage({ page, pageSize: 100 });
    last = num(body, "last_page", 1);
    for (const row of list(body, "data")) counts[statusOf(row)] += 1;
  }
  return { ...counts, total };
}

export type BlogEditorData = {
  meta: BlogEditorMeta | null;
  /** What's saved (or live). The editor's baseline for unsaved-change tracking. */
  saved: BlogFormValues;
  /** Autosaved edits to a live article, loaded into the form on open. */
  pending: BlogFormValues | null;
};

/** `GET admin/blogs/{id}`, or null when the article doesn't exist. */
export const getBlogEditorData = cache(
  async (id: string): Promise<BlogEditorData | null> => {
    let data: unknown;
    try {
      data = await cmsData<unknown>(cmsPaths.blog(id));
    } catch (error) {
      if (
        error instanceof CmsError &&
        error.status === 404 &&
        !isMissingRoute(error)
      )
        return null;
      throw error;
    }
    if (!data || !idOf(field(data, "meta") ?? field(data, "blog") ?? data))
      return null;

    const { blog, values, row } = readBlogPayload(data);
    const pendingSource =
      field(data, "pending") !== undefined
        ? { autosave: field(data, "pending") }
        : row;
    return {
      meta: blog,
      saved: values,
      pending: pendingFromRow(pendingSource, values),
    };
  },
);

/** `GET admin/blogs/new` when the CMS has it; otherwise blank values from the Blog defaults. */
export async function getNewBlogEditorData(): Promise<BlogEditorData> {
  return {
    meta: null,
    saved: emptyFormValues(await getBlogDefaults()),
    pending: null,
  };
}

export type DashboardBlog = {
  id: string;
  title: string;
  slug: string;
  status: ContentStatus;
  featuredImage: string | null;
  createdAt: Date;
  updatedAt: Date;
  publishedAt: Date | null;
  scheduledAt: Date | null;
  author: { name: string } | null;
  category: { name: string } | null;
  updatedBy: { name: string } | null;
};

const toDashboardBlog =
  (names: Names) =>
  (row: unknown): DashboardBlog => {
    const item = toListItem(names)(row);
    return {
      ...item,
      author: item.author ? { name: item.author.name } : null,
      category: item.category ? { name: item.category.name } : null,
    };
  };

/**
 * `GET admin/dashboard` when the CMS has it; otherwise built from the article
 * list, which staging serves.
 */
export async function getDashboardData() {
  const names = await taxonomyNames();
  const map = toDashboardBlog(names);

  const fromEndpoint = await withFallback(
    cmsData<unknown>(cmsPaths.dashboard),
    null,
  );
  if (fromEndpoint) {
    const stats = relation(fromEndpoint, "stats") ?? {};
    return {
      stats: {
        total: num(stats, "total"),
        published: num(stats, "published"),
        drafts: num(stats, "drafts"),
        scheduled: num(stats, "scheduled"),
        categories: num(stats, "categories", names.categories.size),
        authors: num(stats, "authors", names.authors.size),
      },
      recent: list(fromEndpoint, "recent").map(map),
      updated: list(fromEndpoint, "updated").map(map),
      scheduled: list(fromEndpoint, "scheduled").map(map),
    };
  }

  const [counts, recent, updated, scheduled] = await Promise.all([
    getBlogStatusCounts(),
    fetchBlogPage({ sort: "newest", pageSize: 5 }),
    fetchBlogPage({ sort: "updated", pageSize: 5 }),
    fetchBlogPage({ status: "SCHEDULED", pageSize: 5 }),
  ]);
  return {
    stats: {
      total: counts.total,
      published: counts.PUBLISHED,
      drafts: counts.DRAFT,
      scheduled: counts.SCHEDULED,
      categories: names.categories.size,
      authors: names.authors.size,
    },
    recent: list(recent, "data").map(map),
    updated: list(updated, "data").map(map),
    scheduled: list(scheduled, "data")
      .filter((row) => statusOf(row) === "SCHEDULED")
      .map(map),
  };
}

export type RevisionListItem = {
  id: string;
  title: string;
  slug: string;
  reason: RevisionReason | string;
  createdAt: Date;
  editedBy: { name: string; avatarUrl: string | null } | null;
};

const editor = (row: unknown) => {
  const user = relation(row, "edited_by") ?? relation(row, "editor");
  return user
    ? { name: str(user, "name"), avatarUrl: strOrNull(user, "avatar_url") }
    : null;
};

/** `GET admin/blogs/{id}/revisions`, newest first. */
export async function listRevisions(
  blogId: string,
): Promise<RevisionListItem[]> {
  const body = await cmsData<unknown>(cmsPaths.revisions(blogId));
  return paginated(body, (row) => ({
    id: idOf(row),
    title: str(row, "title"),
    slug: str(row, "slug"),
    reason: str(row, "reason"),
    createdAt: dateOf(row, "created_at"),
    editedBy: editor(row),
  })).items;
}

export type RevisionDetail = RevisionListItem & {
  excerpt: string;
  content: string;
  snapshot: unknown;
};

/** `GET admin/blogs/{id}/revisions/{revisionId}`, or null when it doesn't exist. */
export async function getRevision(
  blogId: string,
  revisionId: string,
): Promise<RevisionDetail | null> {
  let row: unknown;
  try {
    row = await cmsData<unknown>(cmsPaths.revision(blogId, revisionId));
  } catch (error) {
    if (
      error instanceof CmsError &&
      error.status === 404 &&
      !isMissingRoute(error)
    )
      return null;
    throw error;
  }
  row = relation(row, "revision") ?? row;
  if (!idOf(row)) return null;
  let snapshot = field(row, "snapshot");
  if (typeof snapshot === "string") {
    try {
      snapshot = JSON.parse(snapshot);
    } catch {
      snapshot = null;
    }
  }
  return {
    id: idOf(row),
    title: str(row, "title"),
    slug: str(row, "slug"),
    excerpt: str(row, "excerpt"),
    content: str(row, "content"),
    reason: str(row, "reason"),
    createdAt: dateOf(row, "created_at"),
    editedBy: editor(row),
    snapshot,
  };
}

/** Existing tag names for the editor's tag input (`GET admin/tags/suggestions`). */
export async function getTagSuggestions(): Promise<string[]> {
  const body = await withFallback(
    cmsData<unknown>(cmsPaths.tagSuggestions),
    [],
  );
  const rows = Array.isArray(body) ? body : list(body, "tags");
  return rows
    .map((tag) => (typeof tag === "string" ? tag : str(tag, "name")))
    .filter(Boolean);
}

export type TaxonomyOptions = {
  categories: CategoryOption[];
  authors: AuthorOption[];
};
