import clientPromise, { objectId, database } from "@/db.config";
import { isObject } from "@/utils/library";
import { ObjectId } from "mongodb";
import { headers } from "next/headers";

export const normalizeMongoDoc = (data) => {
  const checkIsClassObject = (data) =>
    typeof data === "object" &&
    !Array.isArray(data) &&
    data?.constructor !== Object;
  const normalize = (value) => {
    if (value instanceof ObjectId) return value.toString();
    if (value instanceof Date) return value.toISOString();

    return value;
  };

  if (checkIsClassObject(data)) return normalize(data);

  if (isObject(data))
    return Object.fromEntries(
      Object.entries(data).map(([key, value]) => [
        key,
        (() => {
          if (checkIsClassObject(value)) return normalize(value);
          if (Array.isArray(value))
            return value.map((item) => normalizeMongoDoc(item));
          if (isObject(value)) return normalizeMongoDoc(value);
          return value;
        })(),
      ])
    );

  if (Array.isArray(data)) return data.map((item) => normalizeMongoDoc(item));
};

export const parseToObjectId = (_object, keys = []) => {
  if (!_object) return;
  if (!keys || keys.length === 0) return objectId(_object);

  const parsing = (value, _keys) => {
    if (Array.isArray(value)) return value.map((item) => parsing(item));
    // only for multilevel if key has dot and value is object
    if (isObject(value) && _keys.some((key) => key.match(/\./gi)))
      return parseToObjectId(
        value,
        _keys
          // filter keys get only key has dot
          .filter((key) => key.match(/\./gi))
          // remove first word before dot and join with dot
          .map((key) => key.split(".").slice(1).join("."))
      );

    return objectId(value);
  };

  return Object.fromEntries(
    Object.entries(_object).map(([key, value]) => [
      key,
      keys
        // mapping keys if key has dot, get first word before dot
        .map((key) => (key.match(/\./gi) ? key.split(".")[0] : key))
        .includes(key)
        ? parsing(value, keys)
        : value,
    ])
  );
};

export const mergeCat = (parents, categories = [], currentDepth, depth) => {
  if (categories.length === 0) return parents;
  return {
    ...parents,
    children: (() => {
      const cat = categories.filter(
        (cat) => cat.parents[0].toString() === parents._id.toString()
      );

      if (currentDepth < depth)
        return cat.map((c) => mergeCat(c, categories, currentDepth + 1, depth));

      return cat;
    })(),
  };
};

export const modifyByKeys = (obj, keys, callback, keepData = false) => {
  if (!isObject(obj)) return obj;
  const startOfKey = keys.map((k) => (k.match(/\./gi) ? k.split(".")[0] : k));
  const endOfKey = keys.map((k) =>
    k.match(/\./gi) ? k.split(".").slice(-1) : k
  );

  return Object.fromEntries(
    Object.entries({
      ...obj,
      ...(keepData
        ? Object.fromEntries(
            endOfKey.filter((k) => k in obj).map((k) => [`_${k}`, obj[k]])
          )
        : {}),
    }).map(([k, v]) => [
      k,
      startOfKey.includes(k)
        ? isObject(v)
          ? modifyByKeys(
              v,
              keys
                .filter((k) => k.match(/\./gi)) // filter only key has dot
                .map((k) => k.split(".").slice(1).join(".")), // remove first word before dot
              callback,
              keepData
            ) // recursively
          : callback(v)
        : v,
    ])
  );
};

export const filterByKey = (obj, key) => {
  if (!isObject(obj)) return obj;
  const startOfKey = key.map((k) =>
    k.match(/\./gi) ? k.split(".").slice(-1)[0] : k
  );

  return Object.fromEntries(
    Object.entries(obj)
      .filter(([k, _]) => !startOfKey.includes(k))
      .map(([k, v]) => [k, isObject(v) ? filterByKey(v, startOfKey) : v])
  );
};

export const getOrigin = async () => {
  const headersList = await headers();
  const host = headersList.get("host");
  const protocol = headersList.get("x-forwarded-proto") || "http";
  return `${protocol}://${host}`;
};

export const normalizePost = async (post) => {
  const origin = await getOrigin();
  return modifyByKeys(
    normalizeMongoDoc(post),
    ["metadata.imagedata.url", "media.imagedata.url"],
    // (url) => `${origin}/api${url}`
    (url) => `${origin}${url}`
  );
};

export const executeWithTransaction = async (client, callback) => {
  const session = client.startSession();
  try {
    while (true) {
      try {
        await session.withTransaction(
          async () => {
            await callback(session);
          },
          {
            readConcern: { level: "snapshot" },
            writeConcern: { w: "majority" },
            readPreference: "primary",
          }
        );

        break;
      } catch (error) {
        if (
          error.hasErrorLabel?.("TransientTransactionError") ||
          error.hasErrorLabel?.("UnknownTransactionCommitResult")
        ) {
          console.warn(
            "Retrying transaction due to transient error:",
            error.message
          );
          continue;
        }
        throw error;
      }
    }
  } finally {
    await session.endSession();
  }
};

export class ProcessPostLock {
  constructor(post_id) {
    this.post_id = post_id;
    this.post_lock = null;
    this.query = null;
  }

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

    this.query = postslock;
    return postslock;
  }

  async checkPostLock() {
    const current = await this.query.findOne({
      post_id: objectId(this.post_id),
    });

    this.post_lock = current;
  }

  async startSession() {
    const setPending = async () => {
      return await this.query.updateOne(
        { post_id: objectId(this.post_id) },
        { $set: { status: "pending" } }
      );
    };

    const checkLockSession = async () => {
      const lockSession = await this.query.findOne({
        post_id: objectId(this.post_id),
        status: "pending",
      });

      if (!lockSession) {
        await setPending();

        return true;
      } else return await checkLockSession();
    };

    if (!this.post_lock) {
      await setPending();
      return true;
    } else await checkLockSession();
  }

  async realeseSession() {
    return this.query.updateOne(
      { post_id: objectId(this.post_id) },
      { $set: { status: "realese" } }
    );
  }

  async execute(callback) {
    await this.initQuery();
    await this.checkPostLock();

    // prevent race condition
    await this.startSession();

    try {
      return await callback();
    } finally {
      await this.realeseSession();
    }
  }
}
