import clientPromise, { database, objectId } from "@/db.config";
import {
  getOrigin,
  mergeCat,
  modifyByKeys,
  normalizeMongoDoc,
  parseToObjectId,
} from "@/models/helper";
import { spliceObject } from "@/utils/library";

export const getChildrenCategory = async (id) => {
  const client = await clientPromise;
  const mongo = client.db(database);
  const cat = mongo.collection("categories");

  const current = await cat.findOne({ _id: objectId(id) });

  const getChild = async (id) => {
    const children = await cat.find({ "parents.0": objectId(id) }).toArray();

    if (children.length > 0) {
      return (
        await Promise.all(
          children.map(async (child) => {
            return [child, ...(await getChild(child._id))].flat();
          })
        )
      ).flat();
    }

    return children;
  };

  return normalizeMongoDoc([current, ...(await getChild(current._id))].flat());
};

export const getChildrenCategoryMultipleId = async (ids) => {
  return (
    await Promise.all(ids.map(async (id) => await getChildrenCategory(id)))
  ).flat();
};

export const getPostCategories = async () => {
  const client = await clientPromise;
  const mongo = client.db(database);
  const cat = mongo.collection("categories");

  const parentsExcept = await cat
    .find({ slug: { $in: ["paket-wisata", "paket-wisata-overseas"] } })
    .toArray();

  const allCategories = await cat
    .find({
      parents: {
        $nin: parentsExcept.map((p) => p._id),
      },
      _id: { $nin: parentsExcept.map((p) => p._id) },
    })
    .toArray();

  const maxDepth = Math.max(...allCategories.map((c) => c?.parents?.length));
  const categories = {
    parents: allCategories.filter((c) => c?.parents?.length === 0),
    children: allCategories.filter((c) => c?.parents?.length > 0),
  };

  const merged = categories.parents.map((p) =>
    mergeCat(p, categories.children, 0, maxDepth)
  );

  return normalizeMongoDoc(merged);
};

export const getAllCategories = async () => {
  const client = await clientPromise;
  const mongo = client.db(database);
  const cat = mongo.collection("categories");

  const allCategories = await cat.find().toArray();

  const maxDepth = Math.max(
    ...allCategories
      .filter((c) => c?.parents && Array.isArray(c?.parents))
      .map((c) => c.parents.length)
  );
  const categories = {
    parents: allCategories.filter((c) => c?.parents?.length === 0),
    children: allCategories.filter((c) => c?.parents?.length > 0),
  };

  const merged = categories.parents.map((p) =>
    mergeCat(p, categories.children, 0, maxDepth)
  );

  return normalizeMongoDoc(merged);
};

export const getCategories = async (
  { page = 1, limit = 100 },
  sort = { name: 1 },
  search = null
) => {
  const client = await clientPromise;
  const db = client.db(database);
  const skip = (page - 1) * limit;
  const categories = db.collection("categories");

  // Note if not set _order, the parents of item will be in random order
  const [result, total] = await Promise.all([
    await categories
      .aggregate([
        {
          $match: search
            ? {
                $or: [
                  { name: { $regex: search, $options: "i" } },
                  { slug: { $regex: search, $options: "i" } },
                  { description: { $regex: search, $options: "i" } },
                ],
              }
            : {},
        },
        {
          $lookup: {
            from: "categories",
            let: { parentIds: { $ifNull: ["$parents", []] } },
            pipeline: [
              { $match: { $expr: { $in: ["$_id", "$$parentIds"] } } },
              {
                $addFields: {
                  __order: { $indexOfArray: ["$$parentIds", "$_id"] },
                },
              },
              { $sort: { __order: 1 } },
              { $unset: "__order" },
            ],
            as: "parents",
          },
        },
        { $sort: sort },
        { $skip: skip },
        { $limit: limit },
      ])
      .toArray(),
    await categories.countDocuments(),
  ]);

  const totalPage = Math.ceil(total / limit);

  return {
    data: normalizeMongoDoc(result),
    page,
    limit,
    total,
    keyword: search,
    totalPage,
  };
};

export const searchCategories = async (keyword) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db
    .collection("categories")
    .aggregate([
      {
        $match: {
          $or: [
            { name: { $regex: keyword, $options: "i" } },
            { slug: { $regex: keyword, $options: "i" } },
            { description: { $regex: keyword, $options: "i" } },
          ],
        },
      },
      {
        $lookup: {
          from: "categories",
          let: { parentIds: { $ifNull: ["$parents", []] } },
          pipeline: [
            { $match: { $expr: { $in: ["$_id", "$$parentIds"] } } },
            {
              $addFields: {
                __order: { $indexOfArray: ["$$parentIds", "$_id"] },
              },
            },
            { $sort: { __order: 1 } },
            { $unset: "__order" },
          ],
          as: "parents",
        },
      },
    ])
    .toArray();

  return normalizeMongoDoc(result);
};

export const getCategoriesById = async (id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const origin = await getOrigin();

  const result = await db
    .collection("categories")
    .aggregate([
      {
        $match: { _id: { $in: id.map((id) => objectId(id)) } },
      },
      {
        $lookup: {
          from: "categories",
          let: { parentIds: { $ifNull: ["$parents", []] } },
          pipeline: [
            { $match: { $expr: { $in: ["$_id", "$$parentIds"] } } },
            {
              $addFields: {
                __order: { $indexOfArray: ["$$parentIds", "$_id"] },
              },
            },
            { $sort: { __order: 1 } },
            { $unset: "__order" },
          ],
          as: "parents",
        },
      },
      {
        $lookup: {
          from: "media",
          localField: "image",
          foreignField: "_id",
          as: "media",
        },
      },
      {
        $addFields: {
          media: { $arrayElemAt: ["$media", 0] },
        },
      },
    ])
    .toArray();

  return result.map((item) =>
    modifyByKeys(
      normalizeMongoDoc(item),
      ["media.url"],
      // (url) => `${origin}/api${url}`
      (url) => `${origin}${url}`
    )
  );
};

export const getCategoryById = async (id) =>
  (await getCategoriesById([id]))?.[0];

export const createCategory = async (data) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db.collection("categories").insertOne(data);

  return await getCategoryById(result.insertedId);
};

export const updateCategory = async (data) => {
  const client = await clientPromise;
  const db = client.db(database);
  const category = db.collection("categories");

  data = parseToObjectId(data, ["_id", "image", "parents"]);
  const { selected, spliced } = spliceObject(data, ["_id"]);

  const result = await category.updateOne(
    { _id: objectId(selected._id) },
    { $set: spliced }
  );

  // update child parents
  await category.updateMany(
    { "parents.0": objectId(selected._id) },
    { $set: { parents: [selected._id, ...spliced.parents] } }
  );

  return await getCategoryById(data._id);
};

export const deleteCategoryById = async (id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const category = db.collection("categories");

  // check if cat has children
  const deletedItem = await getChildrenCategory(id);
  const ids = deletedItem.map((data) => objectId(data._id));

  const result = await category.deleteMany({ _id: { $in: ids } });
  return normalizeMongoDoc(deletedItem);
};

export const deleteCategoryMultipleId = async (ids) => {
  return (
    await Promise.all(ids.map(async (id) => await deleteCategoryById(id)))
  ).flat();
};

export const getTags = async () => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db.collection("tags").find().toArray();

  return normalizeMongoDoc(result);
};

export const getTagsPagination = async (
  { page = 1, limit = 100 },
  sort = { name: 1 },
  search = null,
  searchId = false
) => {
  const client = await clientPromise;
  const db = client.db(database);
  const skip = (page - 1) * limit;
  const tags = db.collection("tags");

  // make search as array if search by id
  if (searchId) {
    search = Array.isArray(search) ? search : [search];
  }

  const [result, total] = await Promise.all([
    await tags
      .aggregate([
        {
          $match: search
            ? searchId
              ? // search by id
                { _id: { $in: search.map((id) => objectId(id)) } }
              : // search by name, slug, description
                {
                  $or: [
                    { name: { $regex: search, $options: "i" } },
                    { slug: { $regex: search, $options: "i" } },
                    { description: { $regex: search, $options: "i" } },
                  ],
                }
            : {},
        },
        { $sort: sort },
        { $skip: skip },
        { $limit: limit },
      ])
      .toArray(),
    await tags.countDocuments(),
  ]);

  const totalPage = Math.ceil(total / limit);

  return {
    data: normalizeMongoDoc(result),
    page,
    limit,
    keyword: search,
    total,
    totalPage,
  };
};

export const deleteTag = async (id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const tag = db.collection("tags");
  const _id = objectId(id);

  const item = await tag.findOne({ _id });
  const deleted = await tag.deleteOne({ _id });

  if (deleted.deletedCount === 0) return null;
  return normalizeMongoDoc(item);
};

export const deleteMultipleTag = async (ids) => {
  return await Promise.all(ids.map(async (id) => await deleteTag(id)));
};

export const searchTags = async (keyword) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db
    .collection("tags")
    .find({
      $or: [
        { name: { $regex: keyword, $options: "i" } },
        { slug: { $regex: keyword, $options: "i" } },
        { description: { $regex: keyword, $options: "i" } },
      ],
    })
    .toArray();

  return normalizeMongoDoc(result);
};

export const getTagBySlug = async (slug) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db.collection("tags").findOne({ slug });

  if (!result) return null;

  return normalizeMongoDoc(result);
};

export const getTagsBySlugs = async (slugs) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db
    .collection("tags")
    .find({ slug: { $in: slugs } })
    .toArray();

  return normalizeMongoDoc(result);
};

export const getTagsByIds = async (ids) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db
    .collection("tags")
    .find({ _id: { $in: ids.map((id) => objectId(id)) } })
    .toArray();

  if (!result) return null;

  return normalizeMongoDoc(result);
};

export const getTagById = async (id) => (await getTagsByIds([id]))?.[0];

export const createTag = async (tag) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db.collection("tags").insertOne(tag);

  return await getTagById(result.insertedId);
};

export const createTags = async (tags) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = await db.collection("tags").insertMany(tags);

  return await getTagsByIds(Object.values(result.insertedIds));
};

export const updateTag = async (data) => {
  const client = await clientPromise;
  const db = client.db(database);
  const { selected, spliced } = spliceObject(data, ["_id"]);

  const result = await db
    .collection("tags")
    .updateOne({ _id: objectId(selected._id) }, { $set: spliced });

  return await getTagById(data._id);
};
