export const isEmptyObject = (obj) =>
  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 formatDate = (date, withClock = false) => {
  const currentDate = new Date(date);
  const hours = currentDate.getHours();
  const minutes = currentDate.getMinutes();

  return (
    currentDate.toLocaleDateString("id-ID", {
      year: "numeric",
      month: "long",
      day: "numeric",
    }) +
    (withClock
      ? ` ${hours.toString().padStart(2, "0")}:${minutes
          .toString()
          .padStart(2, "0")}`
      : "")
  );
};

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

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 isPromise = (fn) => fn.constructor.name === "AsyncFunction";

export const checkPasswordStrength = (password) => {
  let score = 0;

  if (!password) return "";

  // Check password length
  if (password.length > 8) score += 1;
  // Contains lowercase
  if (/[a-z]/.test(password)) score += 1;
  // Contains uppercase
  if (/[A-Z]/.test(password)) score += 1;
  // Contains numbers
  if (/\d/.test(password)) score += 1;
  // Contains special characters
  if (/[^A-Za-z0-9]/.test(password)) score += 1;

  switch (score) {
    case 0:
    case 1:
    case 2:
      return "Weak";
    case 3:
      return "Medium";
    case 4:
    case 5:
      return "Strong";
  }
};

export const unmergingCategory = (data) => {
  if (!data || data?.length === 0) return [];
  return data
    .map((item) =>
      [
        Object.fromEntries(
          Object.entries(item).filter(([k, _]) => k !== "children")
        ),
        item.children.length > 0 ? unmergingCategory(item.children) : null,
      ]
        .flat()
        .filter((item) => item)
    )
    .flat();
};

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

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

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

export const filterEmptyTag = (stringdata) => {
  return stringdata.replace(/<(\w+)[^>]*>(\s|&nbsp;|<br\s*\/?>)*<\/\1>/gi, "");
};

export const debounce = (fn, delay, fnForceDelay) => {
  let timeoutId;
  return (...args) => {
    if (fnForceDelay) fn(...args);
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
};

export const generateColor = (alpha = 1) => {
  const r = Math.floor(Math.random() * 256);
  const g = Math.floor(Math.random() * 256);
  const b = Math.floor(Math.random() * 256);
  const a = Math.round(alpha * 255); // convert alpha (0–1) to 0–255

  // Convert to 2-digit hex and pad with 0 if needed
  const toHex = (n) => n.toString(16).padStart(2, "0");

  return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(a)}`;
};

/**
 * @description
 * Group an array of objects by given key.
 * @param {string} key - The key to group by.
 * @param {Array} arr - The array to group.
 * @returns {Object} - An object with grouped objects as values, and the key used as the key.
 * @example
 * const data = [
 *   { name: 'John', age: 18 },
 *   { name: 'Jane', age: 19 },
 *   { name: 'John', age: 18 },
 *   { name: 'Jane', age: 19 },
 * ];
 * const grouped = arrayGroupBy('name', data);
 * console.log(grouped);
 *  {
 *    'John': [
 *      { name: 'John', age: 18 },
 *      { name: 'John', age: 18 }
 *    ],
 *    'Jane': [
 *      { name: 'Jane', age: 19 },
 *      { name: 'Jane', age: 19 }
 *    ]
 *  }
 */
export const arrayGroupBy = (key, arr) =>
  arr.length > 0
    ? arr.reduce(
        (acc, item) =>
          !!acc[item[key]]
            ? { ...acc, [item[key]]: [...acc[item[key]], item] }
            : { ...acc, [item[key]]: [item] },
        {}
      )
    : {};

export const getImageSize = async (url) => {
  if (!url) return;
  try {
    const response = await fetch(url, { method: "HEAD" });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const contentLength = response.headers.get("content-length");
    const type = response.headers.get("content-type");

    if (contentLength) {
      const bytes = parseInt(contentLength);
      const mb = bytes / (1024 * 1024);

      return {
        bytes: bytes,
        kb: (bytes / 1024).toFixed(2),
        mb: mb.toFixed(2),
        size: formatBytes(bytes),
        type: type,
      };
    }

    return null;
  } catch (error) {
    return { error: error.message };
  }
};

export const getImageDimension = async (file) => {
  if (!file) return;
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      resolve({
        width: img.width,
        height: img.height,
      });
      URL.revokeObjectURL(img.src);
    };
    img.onerror = reject;
    img.src = isObject(file) ? URL.createObjectURL(file) : file;
  });
};

export const formatBytes = (bytes, decimals = 2) => {
  if (bytes === undefined) return "";
  if (bytes === 0) return "0 B";

  const k = 1024;
  const sizes = ["B", "KB", "MB", "GB", "TB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  const value = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals));
  return `${value} ${sizes[i]}`;
};

export const slugify = (str) =>
  str
    .toLowerCase()
    .replace(/[^\w\s-]/g, "")
    .trim()
    .replace(/\s+/g, "-");

export const removeDuplicateProp = (data, prop) => {
  const unique = new Set();
  return data.filter((item) => {
    const key = item?.[prop];
    if (!key || unique.has(key)) return false;

    unique.add(key);
    return true;
  });
};

export const toRelativeDay = (date) => {
  const today = new Date();
  const targetDate = new Date(date);

  // get diff minutes
  today.setSeconds(0, 0, 0, 0);
  targetDate.setSeconds(0, 0, 0, 0);

  let diffTime = targetDate - today;
  const diffMinutes = Math.abs(Math.ceil(diffTime / (1000 * 60)));

  if (diffMinutes < 1) return "Just now";
  if (diffMinutes < 59) return `${diffMinutes} minutes ago`;

  // get diff hours
  today.setMinutes(0, 0, 0, 0);
  targetDate.setMinutes(0, 0, 0, 0);

  diffTime = targetDate - today;
  const diffHours = Math.abs(Math.ceil(diffTime / (1000 * 60 * 60)));

  if (diffHours < 1) return "Just now";
  if (diffHours < 23) return `${diffHours} hours ago`;

  today.setHours(0, 0, 0, 0);
  targetDate.setHours(0, 0, 0, 0);

  diffTime = targetDate - today;
  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));

  return [
    { on: diffDays === 0, text: "Today" },
    { on: diffDays === 1, text: "Tomorrow" },
    { on: diffDays === -1, text: "Yesterday" },
    { on: diffDays > 1, text: `in ${Math.abs(diffDays)} days` },
    {
      on: diffDays < -1 && diffDays > -7,
      text: `${Math.abs(diffDays)} days ago`,
    },
    {
      on: diffDays <= -7 && diffDays >= -31,
      text: `${Math.abs(Math.round(diffDays / 7))} weeks ago`,
    },
    {
      on: diffDays < -31 && diffDays >= -365,
      text: `${Math.abs(Math.round(diffDays / 30))} months ago`,
    },
    {
      on: diffDays < -365,
      text: `${formatDate(date)}`,
    },
  ].find((item) => item.on)?.text;
};

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

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

export const capitalizeEachWord = (string) =>
  string.replace(
    /\w\S*/g,
    (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
  );

export const objectFilterKey = (obj, key) =>
  Object.fromEntries(
    Object.entries(obj)
      .filter(([k, _]) => k !== key)
      .map(([k, v]) => [k, isObject(v) ? objectFilterKey(v, key) : v])
  );

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

export const objecthasKeys = (obj, keys) =>
  keys.every((key) => objecthasKey(obj, key));

export const filterEmptyValue = (obj) =>
  Object.fromEntries(
    Object.entries(obj)
      .filter(([_, v]) => v)
      .map(([k, v]) => [k, isObject(v) ? filterEmptyValue(v) : v])
  );

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 getByKeys = (obj, keys, initialData = {}) => {
  // create multidimentional array
  const newKeys = keys.map((k) => (k.match(/\./gi) ? k.split(".") : k));

  const data = newKeys.reduce(
    (acc, key) =>
      // my expectation is key is array obj[key[0]] is object
      Array.isArray(key) && key[0] in obj
        ? {
            ...acc,
            // recursively when key is array from splited with dot
            // call again with joined key with dot
            ...getByKeys(obj[key[0]], [key.slice(1).join(".")], acc),
          }
        : key in obj
        ? { ...acc, [key]: obj[key] }
        : acc,
    {}
  );

  return { ...initialData, ...data };
};

export const generateIndexes = (start, end) => {
  const _start = Math.min(start, end);
  const _end = Math.max(start, end);

  const length = _end - _start + 1;
  return Array.from({ length }, (_, index) => index + _start);
};

export const generateRange = (...numbers) => {
  const [from, to] = [Math.min(...numbers), Math.max(...numbers)];
  return Array(to - from + 1)
    .fill()
    .map((_, i) => i + from);
};

export const formatDateRange = (startDate, endDate) => {
  const start = new Date(startDate);
  const end = new Date(endDate);

  // helper cek valid date
  const isValid = (d) => d instanceof Date && !isNaN(d);

  if (!isValid(start)) return startDate;
  if (!isValid(end)) return endDate;

  const pad = (num) => num.toString().padStart(2, "0");

  const sameDay = start.toDateString() === end.toDateString();
  const sameMonth = start.getMonth() === end.getMonth();
  const sameYear = start.getFullYear() === end.getFullYear();

  if (sameDay) {
    // Kalau start & end sama → tampilkan 1 tanggal
    return `${pad(start.getDate())} ${start.toLocaleDateString("id-ID", {
      month: "long",
      year: "numeric",
    })}`;
  }

  if (sameMonth && sameYear) {
    // Contoh: 01 - 05 Januari 2025
    return `${pad(start.getDate())} - ${pad(
      end.getDate()
    )} ${start.toLocaleDateString("id-ID", {
      month: "long",
      year: "numeric",
    })}`;
  } else {
    // Contoh: 30 Jan - 02 Feb 25
    return `${pad(start.getDate())} ${start.toLocaleDateString("id-ID", {
      month: "short",
    })} - ${pad(end.getDate())} ${end.toLocaleDateString("id-ID", {
      month: "short",
      year: "2-digit",
    })}`;
  }
};

export const normalizeURL = (url) => {
  return url
    .replace(/^https?:\/\//, "")
    .replace(/^www\./, "")
    .replace(/\/$/, "");
};
