import { z } from "zod";
import {
  createTRPCRouter,
  protectedProcedure,
  publicProcedure,
} from "@/server/api/trpc";
import { categories, Category } from "@/server/db/schema";

import {
  and,
  asc,
  count,
  desc,
  eq,
  gte,
  ilike,
  like,
  lte,
  sql,
} from "drizzle-orm";
import { hasPermission, Role } from "@/lib/validation/permissions";
import {
  categoryFilterSchema,
  categorySchema,
} from "@/lib/validation/zod/category";
import { generateSlug } from "@/lib/utils";
import SuperJSON from "superjson";

type CategoryCursor = Pick<Category, "slug" | "createdAt" | "name">;
const getCategorySorting = (sort: string, cursor?: CategoryCursor) => {
  // Default to newest
  const baseCase = {
    orderBy: [desc(categories.createdAt), asc(categories.slug)],
    cursor: cursor ? lte(categories.createdAt, cursor.createdAt) : undefined,
  };

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

export const categoryRouter = createTRPCRouter({
  get: publicProcedure
    .input(z.object({ slug: z.string(), with: z.any() }))
    .query(async ({ ctx, input }) => {
      return (await ctx.db.query.categories.findFirst({
        where: eq(categories.slug, input.slug),
        with: input?.with,
      })) as Category;
    }),

  getMany: publicProcedure
    .input(
      z
        .object({
          limit: z.number().optional(),
        })
        .optional(),
    )
    .query(async ({ ctx, input }) => {
      const limit = input?.limit ?? 50;
      return (await ctx.db.query.categories.findMany({
        limit: limit,
        columns: {
          name: true,
          slug: true,
          createdAt: true,
        },
      })) as Category[];
    }),

  getByCursor: publicProcedure
    .input(
      z.object({
        limit: z.number().optional(),
        cursor: z.string().optional(),
        filter: categoryFilterSchema.optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const { cursor } = input!;
      const limit = input?.limit ?? 50;

      // Decode cursor if using it
      let cursorObj: CategoryCursor | 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(categories.name, "%" + input.filter.query + "%")
        : undefined;

      const sortConfig = input?.filter?.sort ?? "newest";
      const { orderBy, cursor: cursorArg } = getCategorySorting(
        sortConfig,
        cursorObj,
      );

      const items = (await ctx.db.query.categories.findMany({
        where: and(cursorArg, queryFilterArg),
        limit: limit + 1,
        orderBy,
        columns: {
          name: true,
          slug: true,
          createdAt: true,
        },
      })) as Category[];

      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: CategoryCursor = {
          slug: cursorItem!.slug,
          createdAt: cursorItem!.createdAt,
          name: cursorItem!.name,
        };
        // Encode the cursor as base64
        nextCursor = Buffer.from(SuperJSON.stringify(cursorData)).toString(
          "base64",
        );
      }

      return {
        items,
        nextCursor,
      };
    }),

  getCount: publicProcedure.query(async ({ ctx }) => {
    return (await ctx.db.select({ count: count() }).from(categories))[0]?.count;
  }),

  create: protectedProcedure
    .input(z.object({ category: categorySchema }))
    .mutation(async ({ ctx, input }) => {
      const isEditor = hasPermission(ctx.session.user.role, Role.EDITOR);
      if (!isEditor) {
        throw new Error("You are not allowed to create categories");
      }
      const slug = generateSlug(input.category.name);
      return await ctx.db
        .insert(categories)
        .values({ ...input.category, slug })
        .returning({
          slug: categories.slug,
        })
        .onConflictDoUpdate({
          target: categories.slug,
          set: {
            slug: sql`${slug} || '-' || (SELECT COUNT(*) FROM ${categories} WHERE slug LIKE ${slug + "-%"})`,
          },
        });
    }),
  update: protectedProcedure
    .input(z.object({ category: categorySchema, categoryId: 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 categories");
      }
      return await ctx.db
        .update(categories)
        .set(input.category)
        .where(eq(categories.id, input.categoryId))
        .returning({
          id: categories.id,
        });
    }),
  delete: protectedProcedure
    .input(z.object({ categoryId: 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(categories)
        .where(eq(categories.id, input.categoryId))
        .returning({
          id: categories.id,
        });
    }),
});
