import { defineBackendPlugin, createEndpoint } from '@btst/stack/plugins/api';
import { z } from 'zod';
import { blogSchema } from '../db.mjs';
import { slugify } from '../utils.mjs';
import { createPostSchema, updatePostSchema } from '../schemas.mjs';
import { getAllTags, getPostBySlug, getAllPosts } from './getters.mjs';
import { BLOG_QUERY_KEYS } from './query-key-defs.mjs';
import { serializePost, serializeTag } from './serializers.mjs';
import { runHookWithShim } from '../../utils.mjs';

function createBlogPrefetchForRoute(adapter) {
  return async function prefetchForRoute(key, qc, params) {
    switch (key) {
      case "posts":
      case "drafts": {
        const published = key === "posts";
        const [result, tags] = await Promise.all([
          getAllPosts(adapter, { published, limit: 10 }),
          getAllTags(adapter)
        ]);
        qc.setQueryData(BLOG_QUERY_KEYS.postsList({ published, limit: 10 }), {
          pages: [result.items.map(serializePost)],
          pageParams: [0]
        });
        qc.setQueryData(BLOG_QUERY_KEYS.tagsList(), tags.map(serializeTag));
        break;
      }
      case "post":
      case "editPost": {
        const slug = params?.slug ?? "";
        if (slug) {
          const post = await getPostBySlug(adapter, slug);
          qc.setQueryData(
            BLOG_QUERY_KEYS.postDetail(slug),
            post ? serializePost(post) : null
          );
        }
        break;
      }
      case "tag": {
        const tagSlug = params?.tagSlug ?? "";
        const [result, tags] = await Promise.all([
          getAllPosts(adapter, { published: true, limit: 10, tagSlug }),
          getAllTags(adapter)
        ]);
        qc.setQueryData(
          BLOG_QUERY_KEYS.postsList({ published: true, limit: 10, tagSlug }),
          {
            pages: [result.items.map(serializePost)],
            pageParams: [0]
          }
        );
        qc.setQueryData(BLOG_QUERY_KEYS.tagsList(), tags.map(serializeTag));
        break;
      }
    }
  };
}
const PostListQuerySchema = z.object({
  slug: z.string().optional(),
  tagSlug: z.string().optional(),
  offset: z.coerce.number().int().min(0).optional(),
  limit: z.coerce.number().int().min(1).max(100).optional(),
  query: z.string().optional(),
  published: z.string().optional().transform((val) => {
    if (val === void 0) return void 0;
    if (val === "true") return true;
    if (val === "false") return false;
    return void 0;
  })
});
const NextPreviousPostsQuerySchema = z.object({
  date: z.coerce.date()
});
const blogBackendPlugin = (hooks) => defineBackendPlugin({
  name: "blog",
  dbPlugin: blogSchema,
  api: (adapter) => ({
    getAllPosts: (params) => getAllPosts(adapter, params),
    getPostBySlug: (slug) => getPostBySlug(adapter, slug),
    getAllTags: () => getAllTags(adapter),
    prefetchForRoute: createBlogPrefetchForRoute(adapter)
  }),
  routes: (adapter) => {
    const findOrCreateTags = async (tagInputs) => {
      if (tagInputs.length === 0) return [];
      const normalizeTagName = (name) => {
        return name.trim();
      };
      const tagsWithIds = [];
      const tagsToFindOrCreate = [];
      for (const tagInput of tagInputs) {
        if ("id" in tagInput && tagInput.id) {
          tagsWithIds.push({
            id: tagInput.id,
            name: normalizeTagName(tagInput.name),
            slug: tagInput.slug,
            createdAt: /* @__PURE__ */ new Date(),
            updatedAt: /* @__PURE__ */ new Date()
          });
        } else {
          tagsToFindOrCreate.push({ name: normalizeTagName(tagInput.name) });
        }
      }
      if (tagsToFindOrCreate.length === 0) {
        return tagsWithIds;
      }
      const allTags = await adapter.findMany({
        model: "tag"
      });
      const tagMapBySlug = /* @__PURE__ */ new Map();
      for (const tag of allTags) {
        tagMapBySlug.set(tag.slug, tag);
      }
      const tagSlugs = tagsToFindOrCreate.map((tag) => slugify(tag.name));
      const foundTags = [];
      for (const slug of tagSlugs) {
        const tag = tagMapBySlug.get(slug);
        if (tag) {
          foundTags.push(tag);
        }
      }
      const existingSlugs = /* @__PURE__ */ new Set([
        ...tagsWithIds.map((tag) => tag.slug),
        ...foundTags.map((tag) => tag.slug)
      ]);
      const tagsToCreate = tagsToFindOrCreate.filter(
        (tag) => !existingSlugs.has(slugify(tag.name))
      );
      const createdTags = [];
      for (const tag of tagsToCreate) {
        const normalizedName = normalizeTagName(tag.name);
        const newTag = await adapter.create({
          model: "tag",
          data: {
            name: normalizedName,
            slug: slugify(normalizedName),
            createdAt: /* @__PURE__ */ new Date(),
            updatedAt: /* @__PURE__ */ new Date()
          }
        });
        createdTags.push(newTag);
      }
      return [...tagsWithIds, ...foundTags, ...createdTags];
    };
    const listPosts = createEndpoint(
      "/posts",
      {
        method: "GET",
        query: PostListQuerySchema
      },
      async (ctx) => {
        const { query, headers } = ctx;
        const context = { query, headers };
        try {
          if (hooks?.onBeforeListPosts) {
            await runHookWithShim(
              () => hooks.onBeforeListPosts(query, context),
              ctx.error,
              "Unauthorized: Cannot list posts"
            );
          }
          const result = await getAllPosts(adapter, query);
          if (hooks?.onPostsRead) {
            await hooks.onPostsRead(result.items, query, context);
          }
          return result;
        } catch (error) {
          if (hooks?.onListPostsError) {
            await hooks.onListPostsError(error, context);
          }
          throw error;
        }
      }
    );
    const createPost = createEndpoint(
      "/posts",
      {
        method: "POST",
        body: createPostSchema
      },
      async (ctx) => {
        const context = {
          body: ctx.body,
          headers: ctx.headers
        };
        try {
          if (hooks?.onBeforeCreatePost) {
            await runHookWithShim(
              () => hooks.onBeforeCreatePost(ctx.body, context),
              ctx.error,
              "Unauthorized: Cannot create post"
            );
          }
          const { tags, ...postData } = ctx.body;
          const tagNames = tags || [];
          const slug = slugify(postData.slug || postData.title);
          if (!slug) {
            throw ctx.error(400, {
              message: "Invalid slug: must contain at least one alphanumeric character"
            });
          }
          const newPost = await adapter.create({
            model: "post",
            data: {
              ...postData,
              slug,
              tags: [],
              createdAt: /* @__PURE__ */ new Date(),
              updatedAt: /* @__PURE__ */ new Date()
            }
          });
          if (tagNames.length > 0) {
            const createdTags = await findOrCreateTags(tagNames);
            await adapter.transaction(async (tx) => {
              for (const tag of createdTags) {
                await tx.create({
                  model: "postTag",
                  data: {
                    postId: newPost.id,
                    tagId: tag.id
                  }
                });
              }
            });
            newPost.tags = createdTags.map((tag) => ({ ...tag }));
          } else {
            newPost.tags = [];
          }
          if (hooks?.onPostCreated) {
            await hooks.onPostCreated(newPost, context);
          }
          return newPost;
        } catch (error) {
          if (hooks?.onCreatePostError) {
            await hooks.onCreatePostError(error, context);
          }
          throw error;
        }
      }
    );
    const updatePost = createEndpoint(
      "/posts/:id",
      {
        method: "PUT",
        body: updatePostSchema
      },
      async (ctx) => {
        const context = {
          body: ctx.body,
          params: ctx.params,
          headers: ctx.headers
        };
        try {
          if (hooks?.onBeforeUpdatePost) {
            await runHookWithShim(
              () => hooks.onBeforeUpdatePost(ctx.params.id, ctx.body, context),
              ctx.error,
              "Unauthorized: Cannot update post"
            );
          }
          const { tags, slug: rawSlug, ...restPostData } = ctx.body;
          const tagNames = tags || [];
          const slugified = rawSlug ? slugify(rawSlug) : void 0;
          if (rawSlug && !slugified) {
            throw ctx.error(400, {
              message: "Invalid slug: must contain at least one alphanumeric character"
            });
          }
          const postData = {
            ...restPostData,
            ...slugified ? { slug: slugified } : {}
          };
          const updated = await adapter.transaction(async (tx) => {
            const existingPostTags = await tx.findMany({
              model: "postTag",
              where: [
                {
                  field: "postId",
                  value: ctx.params.id,
                  operator: "eq"
                }
              ]
            });
            const updatedPost = await tx.update({
              model: "post",
              where: [{ field: "id", value: ctx.params.id }],
              update: {
                ...postData,
                updatedAt: /* @__PURE__ */ new Date()
              }
            });
            if (!updatedPost) {
              throw ctx.error(404, {
                message: "Post not found"
              });
            }
            for (const postTag of existingPostTags) {
              await tx.delete({
                model: "postTag",
                where: [
                  {
                    field: "postId",
                    value: postTag.postId,
                    operator: "eq"
                  },
                  {
                    field: "tagId",
                    value: postTag.tagId,
                    operator: "eq"
                  }
                ]
              });
            }
            if (tagNames.length > 0) {
              const createdTags = await findOrCreateTags(tagNames);
              for (const tag of createdTags) {
                await tx.create({
                  model: "postTag",
                  data: {
                    postId: ctx.params.id,
                    tagId: tag.id
                  }
                });
              }
              updatedPost.tags = createdTags.map((tag) => ({ ...tag }));
            } else {
              updatedPost.tags = [];
            }
            return updatedPost;
          });
          if (hooks?.onPostUpdated) {
            await hooks.onPostUpdated(updated, context);
          }
          return updated;
        } catch (error) {
          if (hooks?.onUpdatePostError) {
            await hooks.onUpdatePostError(error, context);
          }
          throw error;
        }
      }
    );
    const deletePost = createEndpoint(
      "/posts/:id",
      {
        method: "DELETE"
      },
      async (ctx) => {
        const context = {
          params: ctx.params,
          headers: ctx.headers
        };
        try {
          if (hooks?.onBeforeDeletePost) {
            await runHookWithShim(
              () => hooks.onBeforeDeletePost(ctx.params.id, context),
              ctx.error,
              "Unauthorized: Cannot delete post"
            );
          }
          await adapter.delete({
            model: "post",
            where: [{ field: "id", value: ctx.params.id }]
          });
          if (hooks?.onPostDeleted) {
            await hooks.onPostDeleted(ctx.params.id, context);
          }
          return { success: true };
        } catch (error) {
          if (hooks?.onDeletePostError) {
            await hooks.onDeletePostError(error, context);
          }
          throw error;
        }
      }
    );
    const getNextPreviousPosts = createEndpoint(
      "/posts/next-previous",
      {
        method: "GET",
        query: NextPreviousPostsQuerySchema
      },
      async (ctx) => {
        const { query, headers } = ctx;
        const context = { query, headers };
        try {
          if (hooks?.onBeforeListPosts) {
            await runHookWithShim(
              () => hooks.onBeforeListPosts({ published: true }, context),
              ctx.error,
              "Unauthorized: Cannot list posts"
            );
          }
          const date = query.date;
          const previousPosts = await adapter.findMany({
            model: "post",
            limit: 1,
            where: [
              {
                field: "createdAt",
                value: date,
                operator: "lt"
              },
              {
                field: "published",
                value: true,
                operator: "eq"
              }
            ],
            sortBy: {
              field: "createdAt",
              direction: "desc"
            },
            join: {
              postTag: true
            }
          });
          const nextPosts = await adapter.findMany({
            model: "post",
            limit: 1,
            where: [
              {
                field: "createdAt",
                value: date,
                operator: "gt"
              },
              {
                field: "published",
                value: true,
                operator: "eq"
              }
            ],
            sortBy: {
              field: "createdAt",
              direction: "asc"
            },
            join: {
              postTag: true
            }
          });
          const tagIds = /* @__PURE__ */ new Set();
          const allPosts = [...previousPosts, ...nextPosts];
          for (const post of allPosts) {
            if (post.postTag) {
              for (const pt of post.postTag) {
                tagIds.add(pt.tagId);
              }
            }
          }
          const tagMap = /* @__PURE__ */ new Map();
          if (tagIds.size > 0) {
            const tags = await adapter.findMany({
              model: "tag"
            });
            for (const tag of tags) {
              if (tagIds.has(tag.id)) {
                tagMap.set(tag.id, tag);
              }
            }
          }
          const mapPostWithTags = (post) => {
            const tags = (post.postTag || []).map((pt) => {
              const tag = tagMap.get(pt.tagId);
              return tag ? { ...tag } : void 0;
            }).filter((tag) => tag !== void 0);
            const { postTag: _, ...postWithoutJoin } = post;
            return {
              ...postWithoutJoin,
              tags
            };
          };
          return {
            previous: previousPosts[0] ? mapPostWithTags(previousPosts[0]) : null,
            next: nextPosts[0] ? mapPostWithTags(nextPosts[0]) : null
          };
        } catch (error) {
          if (hooks?.onListPostsError) {
            await hooks.onListPostsError(error, context);
          }
          throw error;
        }
      }
    );
    const listTags = createEndpoint(
      "/tags",
      {
        method: "GET"
      },
      async () => {
        return await getAllTags(adapter);
      }
    );
    return {
      listPosts,
      createPost,
      updatePost,
      deletePost,
      getNextPreviousPosts,
      listTags
    };
  }
});

export { NextPreviousPostsQuerySchema, PostListQuerySchema, blogBackendPlugin };
