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';

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,
  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) {
            const canList = await hooks.onBeforeListPosts(query, context);
            if (!canList) {
              throw ctx.error(403, {
                message: "Unauthorized: Cannot list posts"
              });
            }
          }
          let tagFilterPostIds = null;
          if (query.tagSlug) {
            const tag = await adapter.findOne({
              model: "tag",
              where: [
                {
                  field: "slug",
                  value: query.tagSlug,
                  operator: "eq"
                }
              ]
            });
            if (!tag) {
              return [];
            }
            const postTags = await adapter.findMany({
              model: "postTag",
              where: [
                {
                  field: "tagId",
                  value: tag.id,
                  operator: "eq"
                }
              ]
            });
            tagFilterPostIds = new Set(postTags.map((pt) => pt.postId));
            if (tagFilterPostIds.size === 0) {
              return [];
            }
          }
          const whereConditions = [];
          if (query.published !== void 0) {
            whereConditions.push({
              field: "published",
              value: query.published,
              operator: "eq"
            });
          }
          if (query.slug) {
            whereConditions.push({
              field: "slug",
              value: query.slug,
              operator: "eq"
            });
          }
          const posts = await adapter.findMany({
            model: "post",
            limit: query.query || query.tagSlug ? void 0 : query.limit ?? 10,
            offset: query.query || query.tagSlug ? void 0 : query.offset ?? 0,
            where: whereConditions,
            sortBy: {
              field: "createdAt",
              direction: "desc"
            },
            join: {
              postTag: true
            }
          });
          const tagIds = /* @__PURE__ */ new Set();
          for (const post of posts) {
            if (post.postTag) {
              for (const pt of post.postTag) {
                tagIds.add(pt.tagId);
              }
            }
          }
          const tags = tagIds.size > 0 ? await adapter.findMany({
            model: "tag"
          }) : [];
          const tagMap = /* @__PURE__ */ new Map();
          for (const tag of tags) {
            if (tagIds.has(tag.id)) {
              tagMap.set(tag.id, tag);
            }
          }
          let result = posts.map((post) => {
            const postTags = (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: postTags
            };
          });
          if (tagFilterPostIds) {
            result = result.filter((post) => tagFilterPostIds.has(post.id));
          }
          if (query.query) {
            const searchLower = query.query.toLowerCase();
            result = result.filter((post) => {
              const titleMatch = post.title?.toLowerCase().includes(searchLower);
              const contentMatch = post.content?.toLowerCase().includes(searchLower);
              const excerptMatch = post.excerpt?.toLowerCase().includes(searchLower);
              return titleMatch || contentMatch || excerptMatch;
            });
          }
          if (query.tagSlug || query.query) {
            const offset = query.offset ?? 0;
            const limit = query.limit ?? 10;
            result = result.slice(offset, offset + limit);
          }
          if (hooks?.onPostsRead) {
            await hooks.onPostsRead(result, 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) {
            const canCreate = await hooks.onBeforeCreatePost(
              ctx.body,
              context
            );
            if (!canCreate) {
              throw ctx.error(403, {
                message: "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) {
            const canUpdate = await hooks.onBeforeUpdatePost(
              ctx.params.id,
              ctx.body,
              context
            );
            if (!canUpdate) {
              throw ctx.error(403, {
                message: "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) {
            const canDelete = await hooks.onBeforeDeletePost(
              ctx.params.id,
              context
            );
            if (!canDelete) {
              throw ctx.error(403, {
                message: "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) {
            const canList = await hooks.onBeforeListPosts(
              { published: true },
              context
            );
            if (!canList) {
              throw ctx.error(403, {
                message: "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 adapter.findMany({
          model: "tag"
        });
      }
    );
    return {
      listPosts,
      createPost,
      updatePost,
      deletePost,
      getNextPreviousPosts,
      listTags
    };
  }
});

export { NextPreviousPostsQuerySchema, PostListQuerySchema, blogBackendPlugin };
