import { z } from "zod";

import {
  createTRPCRouter,
  protectedProcedure,
  publicProcedure,
} from "@/server/api/trpc";
import { Article, articles } from "@/server/db/schema";
import {
  articleFilterSchema,
  articleSchema,
  createArticleSchema,
} from "@/lib/validation/zod/article";
import {
  and,
  asc,
  count,
  desc,
  eq,
  gte,
  ilike,
  like,
  lte,
  sql,
} from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import { generateSlug } from "@/lib/utils";
import SuperJSON from "superjson";

type ArticleCursor = Pick<Article, "slug" | "createdAt" | "title">;
const getArticleSorting = (sort: string, cursor?: ArticleCursor) => {
  // Default to newest
  const baseCase = {
    orderBy: [desc(articles.createdAt), asc(articles.slug)],
    cursor: cursor ? lte(articles.createdAt, cursor.createdAt) : undefined,
  };

  switch (sort) {
    case "newest":
      return baseCase;
    case "oldest":
      return {
        orderBy: [asc(articles.createdAt), asc(articles.slug)],
        cursor: cursor ? gte(articles.createdAt, cursor.createdAt) : undefined,
      };
    case "abc":
      return {
        orderBy: [asc(articles.title), asc(articles.slug)],
        cursor: cursor ? gte(articles.title, cursor.title) : undefined,
      };
    case "cba":
      return {
        orderBy: [desc(articles.title), asc(articles.slug)],
        cursor: cursor ? lte(articles.title, cursor.title) : undefined,
      };
    default:
      return baseCase;
  }
};

export const articleRouter = createTRPCRouter({
  // queries
  search: publicProcedure
    .input(z.object({ query: z.string() }))
    .query(async ({ ctx, input }) => {
      return (await ctx.db.query.articles.findMany({
        where: like(articles.title, "%" + input.query + "%"),
        with: { category: true },
      })) as Article[];
    }),
  get: publicProcedure
    .input(z.object({ slug: z.string() }))
    .query(async ({ ctx, input }) => {
      const user = ctx?.session?.user;
      const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
      const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
      return (await ctx.db.query.articles.findFirst({
        where: and(eq(articles.slug, input.slug), publishedArg),
        with: { category: true },
      })) as Article;
    }),
  getByCursor: publicProcedure
    .input(
      z.object({
        limit: z.number().optional(),
        cursor: z.string().optional(),
        filter: articleFilterSchema.optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const { cursor } = input!;
      const limit = input?.limit ?? 50;

      // Decode cursor if using it
      let cursorObj: ArticleCursor | undefined;
      if (cursor) {
        try {
          cursorObj = SuperJSON.parse(Buffer.from(cursor, "base64").toString());
        } catch (e) {
          // Handle invalid cursor
          cursorObj = undefined;
        }
      }

      const queryFilterArg = input?.filter?.query?.length
        ? ilike(articles.title, "%" + input.filter.query + "%")
        : undefined;

      const categoryArg = input?.filter?.category
        ? eq(articles.categoryId, input.filter.category)
        : undefined;

      const sortConfig = input?.filter?.sort ?? "newest";
      const { orderBy, cursor: cursorArg } = getArticleSorting(
        sortConfig,
        cursorObj,
      );
      const user = ctx?.session?.user;
      const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
      const publishedArg = !isEditor ? eq(articles.published, true) : undefined;

      const items = await ctx.db.query.articles.findMany({
        where: and(cursorArg, categoryArg, queryFilterArg, publishedArg),
        limit: limit + 1,
        orderBy,
        columns: {
          title: true,
          slug: true,
          createdAt: true,
          published: isEditor ? true : false,
        },
      });

      let nextCursor: string | undefined = undefined;
      if (items.length > limit) {
        const cursorItem = items.pop();
        // Create a cursor object with the relevant fields for sorting
        const cursorData: ArticleCursor = {
          slug: cursorItem!.slug,
          createdAt: cursorItem!.createdAt,
          title: cursorItem!.title,
        };
        // Encode the cursor as base64
        nextCursor = Buffer.from(SuperJSON.stringify(cursorData)).toString(
          "base64",
        );
      }

      return {
        items,
        nextCursor,
      };
    }),
  getMany: publicProcedure
    .input(
      z
        .object({
          categoryId: z.string().optional(),
          limit: z.number().optional(),
        })
        .optional(),
    )
    .query(async ({ ctx, input }) => {
      const limit = input?.limit ?? 50;
      return (await ctx.db.query.articles.findMany({
        where: input?.categoryId
          ? eq(articles.categoryId, input.categoryId)
          : undefined,
        limit: limit,
        columns: {
          title: true,
          slug: true,
          createdAt: true,
        },
      })) as Array<Article>;
    }),

  getCount: publicProcedure
    .input(z.object({ categoryId: z.string() }).optional())
    .query(async ({ ctx, input }) => {
      return (
        await ctx.db
          .select({ count: count() })
          .from(articles)
          .where(
            input?.categoryId
              ? eq(articles.categoryId, input.categoryId)
              : undefined,
          )
      )[0]?.count;
    }),
  // mutations
  create: protectedProcedure
    .input(z.object({ article: createArticleSchema }))
    .mutation(async ({ ctx, input }) => {
      const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR);
      if (!isEditor) {
        throw new Error("You are not allowed to create articles");
      }
      const slug = generateSlug(input.article.title);
      return await ctx.db
        .insert(articles)
        .values({ ...input.article, slug })
        .returning({
          slug: articles.slug,
        })
        .onConflictDoUpdate({
          target: articles.slug,
          set: {
            slug: sql`${slug} || '-' || (SELECT COUNT(*) FROM ${articles} WHERE slug LIKE ${slug + "-%"})`,
          },
        });
    }),
  update: protectedProcedure
    .input(z.object({ article: articleSchema, articleId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR);
      if (!isEditor) {
        throw new Error("You are not allowed to update articles");
      }

      return await ctx.db
        .update(articles)
        .set(input.article)
        .where(eq(articles.id, input.articleId))
        .returning({
          slug: articles.slug,
        });
    }),
  delete: protectedProcedure
    .input(z.object({ articleId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR);
      if (!isEditor) {
        throw new Error("You are not allowed to delete articles");
      }
      return await ctx.db
        .delete(articles)
        .where(eq(articles.id, input.articleId))
        .returning({
          id: articles.id,
        });
    }),
});
