import clientPromise, { database, objectId } from "@/db.config";
import {
  executeWithTransaction,
  filterByKey,
  getOrigin,
  mergeCat,
  modifyByKeys,
  normalizeMongoDoc,
  normalizePost,
  parseToObjectId,
  ProcessPostLock,
} from "./helper";
import {
  isEmptyObject,
  objecthasKey,
  objecthasKeys,
  spliceObject,
} from "@/utils/library";
import { getCategoriesById, getTagsByIds } from "./term";

export const getPostMaintainById = async (id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = (
    await db
      .collection("posts_maintain")
      .aggregate([
        { $match: { _id: objectId(id), status: { $ne: "trash" } } },
        // media
        {
          $lookup: {
            from: "media",
            localField: "media._id",
            foreignField: "_id",
            as: "tempMedia",
          },
        },
        {
          $addFields: {
            "media.imagedata": {
              $mergeObjects: [{ $arrayElemAt: ["$tempMedia", 0] }],
            },
          },
        },
        {
          $unset: ["tempMedia"],
        },
        // metadata for SEO
        {
          $lookup: {
            from: "media",
            localField: "metadata.image",
            foreignField: "_id",
            as: "tempMetadata",
          },
        },
        {
          $addFields: {
            "metadata.imagedata": {
              $mergeObjects: [{ $arrayElemAt: ["$tempMetadata", 0] }],
            },
          },
        },
        {
          $unset: ["tempMetadata"],
        },
        {
          $lookup: {
            from: "posts",
            localField: "_id",
            foreignField: "parentId",
            pipeline: [
              {
                $project: {
                  slug: 1,
                },
              },
            ],
            as: "post_publish",
          },
        },
        {
          $unwind: {
            path: "$post_publish",
            preserveNullAndEmptyArrays: true,
          },
        },
        {
          $lookup: {
            from: "posts_lock",
            let: { postId: "$_id" },
            pipeline: [
              {
                $match: {
                  $expr: { $eq: ["$post_id", "$$postId"] },
                },
              },
              // join with users inside post_lock
              {
                $lookup: {
                  from: "users",
                  let: { userId: "$user_id" },
                  pipeline: [
                    {
                      $match: {
                        $expr: { $eq: ["$_id", "$$userId"] },
                      },
                    },
                    { $project: { name: 1 } },
                  ],
                  as: "user",
                },
              },
              { $unwind: { path: "$user", preserveNullAndEmptyArrays: true } },
            ],
            as: "post_lock",
          },
        },
        {
          $unwind: {
            path: "$post_lock",
            preserveNullAndEmptyArrays: true,
          },
        },
      ])
      .toArray()
  )?.[0];

  return normalizePost(result);
};

export const getPostMaintainByBridgeId = async (id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const result = (
    await db
      .collection("posts_maintain")
      .aggregate([
        { $match: { bridgeId: objectId(id), status: { $ne: "trash" } } },
        // media
        {
          $lookup: {
            from: "media",
            localField: "media._id",
            foreignField: "_id",
            as: "tempMedia",
          },
        },
        {
          $addFields: {
            "media.imagedata": {
              $mergeObjects: [{ $arrayElemAt: ["$tempMedia", 0] }],
            },
          },
        },
        {
          $unset: ["tempMedia"],
        },
        // metadata for SEO
        {
          $lookup: {
            from: "media",
            localField: "metadata.image",
            foreignField: "_id",
            as: "tempMetadata",
          },
        },
        {
          $addFields: {
            "metadata.imagedata": {
              $mergeObjects: [{ $arrayElemAt: ["$tempMetadata", 0] }],
            },
          },
        },
        {
          $unset: ["tempMetadata"],
        },
        {
          $lookup: {
            from: "posts",
            localField: "_id",
            foreignField: "parentId",
            pipeline: [
              {
                $project: {
                  slug: 1,
                },
              },
            ],
            as: "post_publish",
          },
        },
        {
          $unwind: {
            path: "$post_publish",
            preserveNullAndEmptyArrays: true,
          },
        },
        {
          $lookup: {
            from: "posts_lock",
            let: { postId: "$_id" },
            pipeline: [
              {
                $match: {
                  $expr: { $eq: ["$post_id", "$$postId"] },
                },
              },
              // join with users inside post_lock
              {
                $lookup: {
                  from: "users",
                  let: { userId: "$user_id" },
                  pipeline: [
                    {
                      $match: {
                        $expr: { $eq: ["$_id", "$$userId"] },
                      },
                    },
                    { $project: { name: 1 } },
                  ],
                  as: "user",
                },
              },
              { $unwind: { path: "$user", preserveNullAndEmptyArrays: true } },
            ],
            as: "post_lock",
          },
        },
        {
          $unwind: {
            path: "$post_lock",
            preserveNullAndEmptyArrays: true,
          },
        },
      ])
      .toArray()
  )?.[0];

  return normalizePost(result);
};

export const getPostByMatchField = async (
  matchField = {},
  objectIdKeys = [],
  joinAll = false
) => {
  if (
    objecthasKeys(matchField, objectIdKeys) &&
    !isEmptyObject(matchField) &&
    objectIdKeys.length > 0
  )
    matchField = parseToObjectId(matchField, objectIdKeys);

  const client = await clientPromise;
  const db = client.db(database);
  const result = (
    await db
      .collection("posts")
      .aggregate([
        { $match: matchField },
        // media
        {
          $lookup: {
            from: "media",
            localField: "media._id",
            foreignField: "_id",
            as: "tempMedia",
          },
        },
        {
          $addFields: {
            "media.imagedata": {
              $mergeObjects: [{ $arrayElemAt: ["$tempMedia", 0] }],
            },
          },
        },
        {
          $unset: ["tempMedia"],
        },
        // metadata for SEO
        {
          $lookup: {
            from: "media",
            localField: "metadata.image",
            foreignField: "_id",
            as: "tempMetadata",
          },
        },
        {
          $addFields: {
            "metadata.imagedata": {
              $mergeObjects: [{ $arrayElemAt: ["$tempMetadata", 0] }],
            },
          },
        },
        {
          $unset: ["tempMetadata"],
        },
        ...(joinAll
          ? [
              {
                $lookup: {
                  from: "categories",
                  let: { categoryIds: { $ifNull: ["$categories", []] } },
                  pipeline: [
                    { $match: { $expr: { $in: ["$_id", "$$categoryIds"] } } },
                    {
                      $addFields: {
                        __order: { $indexOfArray: ["$$categoryIds", "$_id"] },
                      },
                    },
                    {
                      $lookup: {
                        from: "categories",
                        localField: "parents",
                        foreignField: "_id",
                        as: "parents",
                      },
                    },
                    { $sort: { __order: 1 } },
                    { $unset: "__order" },
                  ],
                  as: "categories",
                },
              },
              {
                $lookup: {
                  from: "tags",
                  let: { tagIds: { $ifNull: ["$tags", []] } },
                  pipeline: [
                    { $match: { $expr: { $in: ["$_id", "$$tagIds"] } } },
                    {
                      $addFields: {
                        __order: { $indexOfArray: ["$$tagIds", "$_id"] },
                      },
                    },
                    { $sort: { __order: 1 } },
                    { $unset: "__order" },
                  ],
                  as: "tags",
                },
              },
            ]
          : []),
      ])
      .toArray()
  )?.[0];

  return normalizePost(result);
};

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

  const parents = await cat
    .find({ slug: { $in: ["paket-wisata", "paket-wisata-overseas"] } })
    .toArray();
  const allCategories = await cat
    .find({ parents: { $in: parents.map((p) => p._id) } })
    .toArray();
  const maxDepth = Math.max(...allCategories.map((c) => c.parents.length));

  const merged = parents.map((p) => mergeCat(p, allCategories, 0, maxDepth));
  return normalizeMongoDoc(merged);
};

export const updatePostMaintainById = async (data) => {
  const client = await clientPromise;
  const db = client.db(database);
  const keys = ["categories", "tags", "author", "media._id", "metadata.image"];

  data = parseToObjectId(data, keys);
  // timestamp
  data = {
    ...data,
    createdAt: new Date(data.createdAt),
    modifiedAt: new Date(),
  };

  // update image alt
  if (objecthasKey(data, "alt")) {
    await db
      .collection("media")
      .updateOne(
        { _id: objectId(data.media._id) },
        { $set: { alt: data.media.alt } }
      );
  }

  const result = await db.collection("posts_maintain").updateOne(
    { _id: objectId(data._id) },
    {
      $set: {
        ...spliceObject(data, ["_id"]).spliced,
        sync: false, // sync with post published or not
      },
    }
  );

  const doc = await getPostMaintainById(data._id);
  return doc;
};

export const increasePostView = async (slug) => {
  const client = await clientPromise;
  const db = client.db(database);
  const POST_MAINTAIN = await db
    .collection("posts_maintain")
    .updateOne({ slug: slug }, { $inc: { views: 1 } }, { upsert: true });
  const POST_DATA = await db
    .collection("posts")
    .updateOne({ slug: slug }, { $inc: { views: 1 } }, { upsert: true });

  return true;
};

export const publishPost = async (data) => {
  const client = await clientPromise;
  const db = client.db(database);
  const keyOfId = [
    "categories",
    "tags",
    "author",
    "media._id",
    "metadata.image",
  ];
  const unwantedData = ["media.imagedata", "metadata.imagedata", "createdAt"];

  data = filterByKey(data, unwantedData);
  data = parseToObjectId(data, keyOfId);

  // update posts
  const resultPosts = await db.collection("posts_maintain").updateOne(
    { _id: objectId(data._id) },
    {
      $set: spliceObject(
        { ...data, status: "published", modifiedAt: new Date(), sync: true },
        ["_id"]
      ).spliced,
    }
  );

  // first get post by id
  const currentPost = await db
    .collection("posts")
    .findOne({ parentId: objectId(data._id) });
  const postMaintain = await db.collection("posts_maintain").findOne({
    _id: objectId(data._id),
  });

  if (currentPost) {
    await db.collection("posts").updateOne(
      { parentId: objectId(data._id) },
      {
        $set: {
          ...spliceObject(postMaintain, ["_id", "sync"]).spliced,
          modifiedAt: new Date(),
          status: "published",
        },
      }
    );
  } else {
    await db.collection("posts").insertOne({
      ...spliceObject(postMaintain, ["_id", "sync"]).spliced,
      createdAt: new Date(),
      modifiedAt: new Date(),
      status: "published",
      parentId: objectId(data._id),
    });
  }

  const doc = await getPostMaintainById(data._id);
  return doc;
};

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

  const postPublished = await db
    .collection("posts")
    .findOne(
      { parentId: objectId(id) },
      { projection: { _id: 0, createdAt: 0, modifiedAt: 0, parentId: 0 } }
    );

  const result = await db
    .collection("posts_maintain")
    .updateOne(
      { _id: objectId(id) },
      { $set: { ...postPublished, sync: true } }
    );

  const doc = await getPostMaintainById(id);
  return doc;
};

export const unpublish = async (id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const data = { status: "draft", modifiedAt: new Date() };

  // set to draft posts and posts_maintain
  const postsMaintain = await db
    .collection("posts_maintain")
    .updateOne({ _id: objectId(id) }, { $set: data });
  const posts = await db
    .collection("posts")
    .updateOne({ parentId: objectId(id) }, { $set: data });

  const doc = await getPostMaintainById(id);
  return doc;
};

export const onUpdating = async (id, data = null) => {
  const client = await clientPromise;
  const db = client.db(database);
  return await db
    .collection("posts_maintain")
    .updateOne(
      { _id: objectId(id) },
      { $set: { isEdited: data ? objectId(data) : data } }
    );
};

export const toTrashPost = async (id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const data = { status: "trash", modifiedAt: new Date() };

  // set to trash posts and posts_maintain
  const postsMaintain = await db
    .collection("posts_maintain")
    .updateOne({ _id: objectId(id) }, { $set: data });
  const posts = await db
    .collection("posts")
    .updateOne({ parentId: objectId(id) }, { $set: data });

  const doc = await db
    .collection("posts_maintain")
    .findOne({ _id: objectId(id) });
  return normalizeMongoDoc(doc);
};

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

export const createPostMaintain = async (
  { author = null },
  additionalField
) => {
  const client = await clientPromise;
  const db = client.db(database);
  const post = db.collection("posts_maintain");

  const data = {
    createdAt: new Date(),
    modifiedAt: new Date(),
    author: objectId(author),
    status: "draft",
    sync: false,
    ...(additionalField ? additionalField : {}),
  };

  const result = await post.insertOne(data);
  return await getPostMaintainById(result.insertedId);
};

export const getPosts = async (
  { page, limit = 100 },
  sort = { createdAt: -1 },
  search = null
) => {
  const client = await clientPromise;
  const db = client.db(database);
  const origin = await getOrigin();

  const [result, total] = await Promise.all([
    await db
      .collection("posts_maintain")
      .aggregate([
        {
          $match: search
            ? {
                $or: [
                  { title: { $regex: search, $options: "i" } },
                  { subtitle: { $regex: search, $options: "i" } },
                ],
                status: { $ne: "trash" },
              }
            : { status: { $ne: "trash" } },
        },
        // media
        {
          $lookup: {
            from: "media",
            localField: "media._id",
            foreignField: "_id",
            as: "tempMedia",
          },
        },
        {
          $addFields: {
            "media.imagedata": {
              $mergeObjects: [{ $arrayElemAt: ["$tempMedia", 0] }],
            },
          },
        },
        {
          $unset: ["tempMedia"],
        },
        // metadata for SEO
        {
          $lookup: {
            from: "media",
            localField: "metadata.image",
            foreignField: "_id",
            as: "tempMetadata",
          },
        },
        {
          $addFields: {
            "metadata.imagedata": {
              $mergeObjects: [{ $arrayElemAt: ["$tempMetadata", 0] }],
            },
          },
        },
        {
          $unset: ["tempMetadata"],
        },
      ])
      .sort(sort)
      .skip((page - 1) * limit)
      .limit(limit)
      .toArray(),
    await db
      .collection("posts_maintain")
      .countDocuments({ status: { $ne: "trash" } }),
  ]);

  // Get categories
  const categoriesId = [
    ...new Set(
      result
        .map((item) => item.categories)
        .flat()
        .filter((item) => item)
        .map((item) => item.toString())
    ),
  ];
  const categories = await getCategoriesById(categoriesId);
  result.map(
    (item) =>
      (item.categories = categories.filter((category) =>
        item.categories?.some((id) => id.toString() == category._id.toString())
      ))
  );

  // Get tags
  const tagsId = [
    ...new Set(
      result
        .map((item) => item.tags)
        .flat()
        .filter((item) => item)
        .map((item) => item?.toString())
    ),
  ];
  const tags = await getTagsByIds(tagsId);
  result.map(
    (item) =>
      (item.tags = tags.filter((tag) =>
        item.tags?.some((id) => id.toString() == tag._id.toString())
      ))
  );

  const totalPage = Math.ceil(total / limit);
  return {
    page,
    limit,
    total,
    totalPage,
    data: normalizeMongoDoc(result).map((data) =>
      modifyByKeys(
        data,
        ["media.imagedata.url"],
        // (url) => `${origin}/api${url}`,
        (url) => `${origin}${url}`,
        true
      )
    ),
  };
};

export const searchPosts = async (keyword) => {
  const client = await clientPromise;
  const mongo = client.db(database);
  const posts = mongo.collection("posts_maintain");
  const origin = await getOrigin();

  const result = await posts
    .aggregate([
      {
        $match: {
          $or: [
            { title: { $regex: keyword, $options: "i" } },
            { subtitle: { $regex: keyword, $options: "i" } },
          ],
        },
      },
      // media
      {
        $lookup: {
          from: "media",
          localField: "media._id",
          foreignField: "_id",
          as: "tempMedia",
        },
      },
      {
        $addFields: {
          "media.imagedata": {
            $mergeObjects: [{ $arrayElemAt: ["$tempMedia", 0] }],
          },
        },
      },
      {
        $unset: ["tempMedia"],
      },
      // metadata for SEO
      {
        $lookup: {
          from: "media",
          localField: "metadata.image",
          foreignField: "_id",
          as: "tempMetadata",
        },
      },
      {
        $addFields: {
          "metadata.imagedata": {
            $mergeObjects: [{ $arrayElemAt: ["$tempMetadata", 0] }],
          },
        },
      },
      {
        $unset: ["tempMetadata"],
      },
    ])
    .sort({ createdAt: -1 })
    .toArray();

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

export const addLockPost = async (data) => {
  const client = await clientPromise;
  const db = client.db(database);
  const postslock = db.collection("posts_lock");

  return await postslock.insertOne(data);
};

export const getLockPostByUid = async (uid) => {
  const client = await clientPromise;
  const db = client.db(database);
  const postslock = db.collection("posts_lock");

  const result = await postslock.find({ uid }).toArray();

  return normalizeMongoDoc(result);
};

export const realeseLockPost = async (uid) => {
  const client = await clientPromise;
  const db = client.db(database);
  const postslock = db.collection("posts_lock");

  return await postslock.deleteOne({ uid });
};

export const getAllLockPost = async () => {
  const client = await clientPromise;
  const db = client.db(database);
  const postslock = db.collection("posts_lock");

  const result = await postslock
    .aggregate([
      {
        $lookup: {
          from: "users",
          localField: "user_id",
          foreignField: "_id",
          as: "user",
          pipeline: [{ $project: { password: 0 } }],
        },
      },
      { $unwind: "$user" },
    ])
    .toArray();

  return normalizeMongoDoc(result);
};

export const changeUserLockPost = async (uid, user_id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const postslock = db.collection("posts_lock");

  return postslock.updateOne({ uid }, { $set: { user_id: objectId(user_id) } });
};

// heatbeat
export const cleanUpExpiredLockPost = async () => {
  const client = await clientPromise;
  const db = client.db(database);
  const postslock = db.collection("posts_lock");

  return await postslock.deleteMany({
    heartBeat: { $lt: new Date(Date.now() - 1 * 60 * 1000) },
  });
};

export const switchLockPost = async (uid, post_id, user_id) => {
  const client = await clientPromise;
  const db = client.db(database);
  const postslock = db.collection("posts_lock");
  const session = new ProcessPostLock(post_id);

  return await session.execute(async () => {
    const current = await postslock.findOne({ post_id: objectId(post_id) });

    await postslock.updateOne(
      { post_id: objectId(post_id) },
      { $set: { user_id: objectId(user_id), uid } }
    );

    const newData = await postslock.findOne({ post_id: objectId(post_id) });
    return normalizeMongoDoc(newData);
  });
};
