import { v1 as uuid } from "uuid";

/**
 * Convert string to boolean.
 *
 * @export
 * @param {string} input
 * @returns {boolean}
 */
export const StringToBoolean = (input: string): boolean => {
  try {
    return Boolean(JSON.parse(input.toLowerCase()));
  } catch {
    return false;
  }
};

/**
 * Removes a start line terminator character from a string.
 *
 * @export
 * @param {string} str
 * @param {string} char
 * @returns {string}
 */
export const trimStart = (str: string, char: string): string => {
  let res = str;
  while (res.charAt(0) === char) {
    res = res.substring(1, res.length);
  }
  return res;
};

/**
 * Removes a leading and trailing line terminator character from a string.
 *
 * @export
 * @param {string} str
 * @param {string} char
 * @returns {string}
 */
export const trimEnd = (str: string, char: string): string => {
  let res = str;
  while (res.charAt(res.length - 1) === char) {
    res = res.substring(0, res.length - 1);
  }
  return res;
};

/**
 * Removes a stating, leading and trailing line terminator character from a string.
 *
 * @export
 * @param {string} str
 * @param {string} char
 * @returns {string}
 */
export const trim = (str: string, char: string): string => {
  let res = trimStart(str, char);
  res = trimEnd(res, char);
  return res;
};

/**
 * Clone an object or an array.
 *
 * @param {*} obj
 * @returns {*}
 */
export const clone = (obj: any) =>
  Array.isArray(obj) ? Array.from(obj) : { ...obj };

/**
 * Generate user token.
 *
 * @returns {string}
 */
export const generateToken = () => `${uuid()}-${Date.now()}`;

/**
 * Generate code
 *
 * @returns {string}
 */
export const generateCode = (digits: number) => {
  if (digits <= 0) {
    throw new Error("El número de dígitos debe ser mayor que 0");
  }

  // Calcular el rango mínimo y máximo para el número de n dígitos
  const min = 0;
  const max = Math.pow(10, digits) - 1;

  // Generar un número aleatorio dentro del rango [min, max]
  const numeroAleatorio = Math.floor(Math.random() * (max - min + 1)) + min;

  // Convertir el número a string y rellenar con ceros a la izquierda si es necesario
  const numeroString = numeroAleatorio.toString().padStart(digits, "0");

  return numeroString;
};

/**
 * Filtra un objeto dejando solo los campos permitidos.
 * Útil para limpiar cuerpos de request antes de guardar en base de datos.
 *
 * @param obj - Objeto a filtrar
 * @param allowedFields - Lista de claves que se deben conservar
 * @returns Nuevo objeto con únicamente las claves permitidas
 */
export const filterObj = (obj: any, ...allowedFields: string[]) => {
  const newObj = {} as any;
  Object.keys(obj).forEach((key) => {
    if (allowedFields.includes(key)) newObj[key] = obj[key];
  });
  return newObj;
};

// Tipos para el resultado con unión discriminada: éxito o fallo, nunca ambos.
type Success<T> = {
  data: T;
  error: null;
};

type Failure<E> = {
  data: null;
  error: E;
};

type Result<T, E = Error> = Success<T> | Failure<E>;

/**
 * Wrapper para manejar promesas sin usar try/catch en cada llamada.
 * Inspirado en el patrón Go-style error handling.
 *
 * Retorna un objeto discriminado: si la promesa resuelve, `data` contiene
 * el resultado y `error` es null. Si rechaza, `data` es null y `error`
 * contiene la excepción capturada.
 *
 * @param promise - Promesa a ejecutar
 * @returns `{ data, error: null }` en éxito o `{ data: null, error }` en fallo
 *
 * @example
 * const { data, error } = await tryCatch(UserService.findById(id));
 * if (error) return res.status(500).json({ message: "Error" });
 * return res.json(data);
 */
export async function tryCatch<T, E = Error>(
  promise: Promise<T>,
): Promise<Result<T, E>> {
  try {
    const data = await promise;
    return { data, error: null };
  } catch (error) {
    return { data: null, error: error as E };
  }
}

/**
 * Verifica si una fecha corresponde al día de hoy (ignora la hora).
 *
 * @param date - Fecha a comparar
 * @returns `true` si la fecha es hoy, `false` en caso contrario
 */
export const isToday = (date: Date): boolean => {
  const today = new Date();

  // Comparamos año, mes y día
  return (
    date.getDate() === today.getDate() &&
    date.getMonth() === today.getMonth() &&
    date.getFullYear() === today.getFullYear()
  );
};

/**
 * Retorna un objeto `Date` apuntando al inicio del día actual (00:00:00.000).
 * Útil para construir rangos de consulta en base de datos.
 *
 * @returns `Date` con la hora en 00:00:00.000 del día actual
 */
export const startOfToday = () => {
  const now = new Date();
  now.setHours(0, 0, 0, 0); // Establece la hora al inicio del día
  return now;
};

/**
 * Retorna un objeto `Date` apuntando al inicio del día siguiente (00:00:00.000).
 * Útil como límite superior en rangos de consulta del día actual.
 *
 * @returns `Date` con la hora en 00:00:00.000 del día siguiente
 */
export const startOfTomorrow = () => {
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1); // Avanza un día
  tomorrow.setHours(0, 0, 0, 0); // Establece la hora al inicio del día
  return tomorrow;
};

/**
 * Convierte el estado interno de una operación de crédito a su etiqueta legible en español.
 *
 * @param status - Estado interno (`void`, `pending`, `approved`, `completed`, `rejected`)
 * @returns Nombre legible del estado
 */
export const getOperationStatusName = (status: string) => {
  switch (status) {
    case "void":
      return "Pendiente";
    case "pending":
      return "Pendiente";
    case "approved":
      return "Aprobado";
    case "completed":
      return "Completado";
    case "rejected":
      return "Rechazado";
    default:
      return "Desconocido";
  }
};

/**
 * Convierte el estado de pago de una operación a su etiqueta legible en español.
 *
 * @param status - Estado de pago (`void`, `pending-approval`, `pending`, `paid`, `overdue`, `inArrears`, `rejected`)
 * @returns Nombre legible del estado de pago
 */
export const getOperationPaymentStatusName = (status: string) => {
  switch (status) {
    case "void":
      return "Pendiente";
    case "pending-approval":
      return "Pendiente de aprobación";
    case "pending":
      return "Pendiente";
    case "paid":
      return "Pagado";
    case "overdue":
      return "Vencido";
    case "inArrears":
      return "En mora";
    case "rejected":
      return "Rechazado";
    default:
      return "Desconocido";
  }
};

/**
 * Convierte el estado de un pago individual a su etiqueta legible en español.
 *
 * @param status - Estado del pago (`void`, `pending`, `confirmed`, `error`)
 * @returns Nombre legible del estado del pago
 */
export const getPaymentStatusName = (status: string) => {
  switch (status) {
    case "void":
      return "Pendiente";
    case "pending":
      return "Pendiente";
    case "confirmed":
      return "Confirmado";
    case "error":
      return "Error";
    default:
      return "Desconocido";
  }
};

/**
 * Convierte el estado de un usuario a su etiqueta legible en español.
 *
 * @param status - Estado del usuario (`active`, `inactive`, `pending`, `deleted`, `rejected`)
 * @returns Nombre legible del estado del usuario
 */
export const getUserStatusName = (status: string) => {
  switch (status) {
    case "active":
      return "Activo";
    case "inactive":
      return "Inactivo";
    case "pending":
      return "Pendiente";
    case "deleted":
      return "Desafiliado";
    case "rejected":
      return "Rechazado";
    default:
      return "Desconocido";
  }
};

/**
 * Genera un número entero aleatorio de exactamente 14 dígitos.
 * Se usa para crear identificadores numéricos únicos en transacciones.
 *
 * @returns Número entero de 14 dígitos (entre 10^13 y 10^14 - 1)
 */
export const generarNumeroAleatorio14Digitos = (): number => {
  const numeroAleatorio = Math.random();

  const numeroGrande = numeroAleatorio * Math.pow(10, 14);

  const numeroEntero = Math.floor(numeroGrande);

  // Garantizar que el número tenga exactamente 14 dígitos
  const numeroFinal =
    numeroEntero >= Math.pow(10, 13)
      ? numeroEntero
      : numeroEntero + Math.pow(10, 13);

  return numeroFinal;
};

export const getDaysDifference = (date1: Date, date2: Date): number => {
  // Asegurarse de que las fechas sean objetos Date válidos
  if (!(date1 instanceof Date) || isNaN(date1.getTime())) {
    throw new Error(
      "El primer argumento debe ser una instancia válida de Date.",
    );
  }
  if (!(date2 instanceof Date) || isNaN(date2.getTime())) {
    throw new Error(
      "El segundo argumento debe ser una instancia válida de Date.",
    );
  }

  date1.setHours(0, 0, 0, 0);
  date2.setHours(0, 0, 0, 0);

  // Calcular la diferencia en milisegundos.
  // Usamos Math.abs para asegurar un resultado positivo,
  // independientemente del orden de las fechas.
  const diffTime = Math.abs(date2.getTime() - date1.getTime());

  // Definir la cantidad de milisegundos en un día
  const millisecondsInOneDay = 1000 * 60 * 60 * 24;

  // Calcular la diferencia en días y redondear al entero más cercano
  // (o Math.floor si solo quieres días completos)
  const diffDays = Math.round(diffTime / millisecondsInOneDay);

  return diffDays;
};

/**
 * Rellena un campo hasta la longitud indicada según su tipo, para construir
 * archivos de texto de posición fija (formato bancario, integración LA, etc.).
 *
 * - Tipo `"N"` (numérico): multiplica por 100 (convierte a centavos), convierte
 *   a entero y rellena con ceros a la izquierda.
 * - Tipo texto: rellena con espacios a la izquierda o derecha según `align`.
 *
 * @param value - Valor a formatear
 * @param length - Longitud total del campo resultante
 * @param type - `"N"` para numérico monetario, cualquier otro valor para texto
 * @param align - Alineación del texto: `"left"` (default) o `"right"`
 * @returns String del campo formateado con la longitud exacta
 */
export const padField = (
  value: any,
  length: number,
  type: string,
  align = "left",
) => {
  if (type === "N") {
    return Math.floor(value * 100)
      .toString()
      .padStart(length, "0");
  } else {
    return align === "left"
      ? value.toString().padEnd(length, " ")
      : value.toString().padStart(length, " ");
  }
};

/**
 * Rellena un número con ceros a la izquierda hasta la longitud indicada.
 *
 * @param value - Número a formatear
 * @param length - Longitud total del string resultante
 * @returns String del número con ceros a la izquierda
 */
export const padNumber = (value: number, length: number) => {
  return value.toString().padStart(length, "0");
};

/**
 * Formatea una fecha en el formato `YYYYMMDDHHmmss` (sin separadores).
 * Se usa para construir campos de fecha/hora en formatos de archivo bancario.
 *
 * @param date - Fecha a formatear
 * @returns String en formato `YYYYMMDDHHmmss`
 */
export const formatCustomeDate = (date: Date) => {
  const year = date.getFullYear();
  const month = (date.getMonth() + 1).toString().padStart(2, "0");
  const day = date.getDate().toString().padStart(2, "0");
  const hours = date.getHours().toString().padStart(2, "0");
  const minutes = date.getMinutes().toString().padStart(2, "0");
  const seconds = date.getSeconds().toString().padStart(2, "0");

  return `${year}${month}${day}${hours}${minutes}${seconds}`;
};

/**
 * Elimina tildes, caracteres especiales y símbolos de un texto,
 * dejando únicamente letras, números y espacios.
 * Útil para normalizar nombres o textos antes de enviarlos a APIs externas
 * que no aceptan caracteres Unicode.
 *
 * @param text - Texto original a limpiar
 * @returns Texto sin tildes ni caracteres especiales
 */
export const cleanText = (text: string) => {
  const normalizedText = text.normalize("NFD");
  return normalizedText
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-zA-Z0-9\s]/g, "");
};

/**
 * Genera un array con las 7 fechas de la semana (formato `YYYY-MM-DD`)
 * comenzando desde `startDate`.
 *
 * @param startDate - Fecha de inicio de la semana
 * @returns Array de 7 strings con las fechas en formato `YYYY-MM-DD`
 */
export const getDatesInWeek = (startDate: Date) => {
  const dates: string[] = [];
  let currentDate = new Date(startDate);
  for (let i = 0; i < 7; i++) {
    dates.push(currentDate.toISOString().split("T")[0]);
    currentDate.setDate(currentDate.getDate() + 1);
  }
  return dates;
};
