import bcrypt from "bcrypt";
import path from "path";
import fs from "fs";

export const isEmptyObject = (obj) =>
  Object.keys(obj).length === 0 && obj.constructor === Object;

export const isEmptyValue = (obj) =>
  Object.values(obj).some(
    (x) => x === "" || x === 0 || x === null || x === undefined
  );

export const isObject = (obj) =>
  obj !== null && typeof obj === "object" && !Array.isArray(obj);

export const isPlainObject = (obj) =>
  obj !== null && typeof obj === "object" && obj.constructor === Object;

export const spliceObject = (obj, keys = [], removeEmpty = true) => {
  if (!obj) return;
  return {
    selected: Object.entries(obj)
      .filter(
        ([key, value]) =>
          keys.includes(key) &&
          (!removeEmpty || value || typeof value === "boolean")
      )
      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
    spliced: Object.entries(obj)
      .filter(
        ([key, value]) =>
          !keys.includes(key) &&
          (!removeEmpty || value || typeof value === "boolean")
      )
      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
  };
};

export const flatObjectKeys = (obj) =>
  isObject(obj)
    ? Object.entries(obj)
        .map(([k, v]) => [k, flatObjectKeys(v)].flat())
        .flat()
    : [];

/**
 * @description
 * Check if object has key recursively
 */
export const objecthasKey = (obj, key) => flatObjectKeys(obj).includes(key);

/**
 * Find if object has keys [key1, key2, key3]
 * Warning: its every
 *
 * @param {object} obj
 * @param {string} keys
 * @returns {boolean}
 */
export const objecthasKeys = (obj, keys) =>
  keys.every((key) => objecthasKey(obj, key));

export class CustomError extends Error {
  constructor(optionalData = {}, ...params) {
    // Pass remaining arguments (including vendor specific ones) to parent constructor
    super(...params);

    // Maintains proper stack trace for where our error was thrown (non-standard)
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, CustomError);
    }

    if (Object.keys(optionalData))
      Object.entries(optionalData).forEach(([key, value]) => {
        this[key] = value;
      });

    this.date = new Date();
  }
}

export const objectFilterEmpty = (obj) =>
  Object.fromEntries(Object.entries(obj).filter(([_, v]) => v));

export const parseCookie = (request) =>
  Object.fromEntries(request.cookies.getAll().map((c) => Object.values(c)));

class Bcrypt {
  constructor() {}

  hashPassword(password) {
    const salt = bcrypt.genSaltSync(10);
    return bcrypt.hashSync(password, salt);
  }

  comparePassword(password, hash) {
    return bcrypt.compareSync(password, hash);
  }
}

export const { hashPassword, comparePassword } = new Bcrypt();

export const getUploadPath = () => {
  const date = new Date();
  const year = date.getFullYear();
  const month = date.getMonth() + 1;

  const uploadDir = path.join(
    process.cwd(),
    "/public/media",
    `${year}`,
    `${month}`
  );
  fs.mkdirSync(uploadDir, { recursive: true });

  return { dir: uploadDir, date: `${year}/${month}` };
};

export const deleteFile = (paths) => {
  const fullpath = path.join(process.cwd(), "public", paths);

  if (!fs.existsSync(fullpath)) return;
  fs.unlinkSync(fullpath);
};

export const getSecondOf = (definition = "1s") => {
  const role = {
    s: (value) => value,
    m: (value) => value * 60,
    h: (value) => value * 60 * 60,
    d: (value) => value * 60 * 60 * 24,
  };

  const { number, unit } = definition
    .split("")
    .reduce(
      (acc, item) =>
        Number.isInteger(parseInt(item))
          ? { ...acc, number: [...acc.number, item] }
          : { ...acc, unit: [...acc.unit, item] },
      { number: [], unit: [] }
    );
  const value = parseInt(number.join(""));

  return role[unit[0]](value);
};

export const isValidDate = (date) => {
  const currentDate = new Date(date);
  return !isNaN(currentDate.getTime());
};

export const parseDateValue = (data) =>
  Object.fromEntries(
    Object.entries(data).map(([key, value]) => [
      key,
      typeof value === "string" && isValidDate(value) ? new Date(value) : value,
    ])
  );

/**
 * Write JSON data to file (create file/folders if missing)
 * @param {string} filePath - Full path to the JSON file
 * @param {object|array} data - JSON data to write
 * @param {boolean} [pretty=true] - Pretty-print JSON
 */
export function writeJsonFile(filePath, data, pretty = true) {
  const dir = path.dirname(filePath);

  // Create folder if it doesn't exist
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }

  // Create file if it doesn't exist
  if (!fs.existsSync(filePath)) {
    fs.writeFileSync(filePath, "{}", "utf-8");
  }

  // Write JSON data
  const json = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
  fs.writeFileSync(filePath, json, "utf-8");

  return data;
}
