import CryptoJS from 'crypto-js';
import { setCookie as setNextCookie, getCookie as getNextCookie, deleteCookie } from 'cookies-next';
import moment from 'moment';

const ENCRYPTION_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY || '';

/**
 * Retrieves the screen width of the current device.
 * @returns {number} The screen width of the current device.
 */
export function getScreenWidth() {
  return window.innerWidth || document.documentElement.clientWidth || screen.width;
}

/**
 * Divides a total value into a specified number of parts and returns an array of evenly spaced values.
 * @param {number} totalValue - The total value to be divided.
 * @param {number} parts - The number of parts to divide the total value into.
 * @returns {number[]} An array of evenly spaced values.
 */

export function SpeedometerRange(totalValue: number, parts: number) {
  const step = totalValue / (parts - 1); // Calculate the step size
  const result = []; // Initialize an empty array to store the result

  for (let i = 0; i < parts; i++) {
    result.push(Math.round(i * step)); // Round the result to the nearest integer
  }

  return result; // Return the result array
}

/**
 * Sets an encrypted cookie
 * @param {string} key - Cookie key
 * @param {any} value - Value to encrypt and store
 * @param {Object} options - Cookie options (e.g., maxAge)
 */
export const setEncryptedCookie = (key: string, value: any, options?: any) => {
  try {
    const encryptedData = CryptoJS.AES.encrypt(
      JSON.stringify(value),
      ENCRYPTION_KEY
    ).toString();
    setNextCookie(key, encryptedData, options);
  } catch (error) {
  }
};

/**
 * Gets and decrypts a cookie value
 * @param {string} key - Cookie key
 * @returns {any} Decrypted and parsed cookie value, or null if invalid
 */
export const getEncryptedCookie = (key: string) => {
  try {
    const encryptedData = getNextCookie(key);
    if (typeof encryptedData !== 'string') return null;

    const decryptedBytes = CryptoJS.AES.decrypt(
      encryptedData,
      ENCRYPTION_KEY
    );
    const decryptedData = decryptedBytes.toString(CryptoJS.enc.Utf8);
    return JSON.parse(decryptedData);
  } catch (error) {
    return null;
  }
};

// Re-export the original functions alongside our encrypted versions
export {
  deleteCookie,
  setNextCookie as setCookie,
  getNextCookie as getCookie
};

/**
 * Capitalizes the first letter of each word in a hyphenated string, and removes the last part if it contains any digit.
 * @param str - The hyphenated string to capitalize.
 * @returns The capitalized string.
 */
export function getCapitalizeHyphenated(str: string) {
  const parts = str.split('-');

  // Remove the last part if it contains any digit
  if (/\d/.test(parts[parts.length - 1])) {
    parts.pop();
  }

  const result = parts
    .join(' ')
    .toLowerCase()
    .replace(/(?:^|\s)\w/g, match => match.toUpperCase());

  return result;
}

/**
* Formats a given number of seconds into a time string (minutes:seconds).
* @param seconds - The number of seconds to format.
* @returns A string representing the time in minutes and seconds.
*/
export function FormatTime(seconds: number) {
  const minutes = Math.floor(seconds / 60)
  const remainingSeconds = seconds % 60

  const formattedMinutes = String(minutes).padStart(2, '0')
  const formattedSeconds = String(remainingSeconds).padStart(2, '0')

  return `${formattedMinutes}:${formattedSeconds}`
}

/**
 * Formats a given timestamp into a specific format.
 * @param timestamp - The timestamp to format.
 * @returns A string representing the formatted date.
 */
export function getTimestampToDate(timestamp: string) {
  return moment(Number(timestamp)).format('DD/MM/YYYY');
}

/**
 * Formats a date string to "Day Month, Year" format,from the input date.
 * @param {string} dateString - The input date string.
 * @returns {string} The formatted date string in "Day Month, Year" format.
*/
export function formatDateMain(dateString) {
  const inputDate = new Date(dateString);

  inputDate.setDate(inputDate.getDate());

  // Format the date
  const day = inputDate.getDate();
  const month = inputDate.toLocaleString('default', { month: 'short' });
  const year = inputDate.getFullYear();

  return `${day} ${month}, ${year}`;
}

/**
 * 
 * @param dateString The input date string.
 * @returns arrya of 7 dates starting from the input date
 */
export function getNext7Days(dateString) {
  const startDate = new Date(dateString);
  const result = [];

  for (let i = 0; i < 6; i++) {
    const nextDate = new Date(startDate);
    nextDate.setDate(startDate.getDate() + i);
    result.push(nextDate);
  }

  return result;
}

/**
 * 
 * @param {string} dateString The input date string.
 * @returns {Date[]} Array of dates before the input date
 */
export function getPrevious7Days(dateString) {
  const startDate = new Date(dateString);
  const result = [];

  for (let i = 1; i <= 5; i++) {
    const prevDate = new Date(startDate);
    prevDate.setDate(startDate.getDate() - i);
    result.push(prevDate);
  }

  return result;
}

/**
 * 
 * @param {string} dateStr The input date string.
 * @returns {Date} single date from the input date
 */
export function getNext7Day(dateStr) {
  const baseDate = new Date(dateStr);
  const next7Days = [];

  for (let i = 1; i <= 7; i++) {
    const nextDate = new Date(baseDate);
    nextDate.setDate(baseDate.getDate() + i);
    next7Days.push(nextDate.toDateString()); // or just push `nextDate` if you want raw Date objects
  }

  return next7Days;
}

/**
 * Generates an array of month objects starting from a given date
 * and covering the next 12 months. 
 * @param {Date} startDate - The date from which to start (defaults to current date)
 * @returns {Array} List of month objects
 */
export function getNext12Months(startDate = new Date()) {
  const months = [];

  // List of month names (0 = January, 11 = December)
  const monthNames = [
    "Jan", "Feb", "Mar", "Apr", "May", "June",
    "July", "Aug", "Sept", "Oct", "Nov", "Dec"
  ];

  const monthFullNames = [
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December"
  ];

  // Loop through the next 12 months
  for (let i = 0; i < 12; i++) {
    // Create a new date object offset by i months from the start date
    const date = new Date(startDate.getFullYear(), startDate.getMonth() + i);

    // Push the formatted month object into the array
    months.push({
      name: monthNames[date.getMonth()],
      fullName: monthFullNames[date.getMonth()],
      year: date.getFullYear(),
      id: i + 1
    });
  }

  return months;
}

/**
 * 
 * @param {string} dateString The input future date string.
 * @returns {Date[]} Array of dates from today to the day before the input date
 */
export function getDateRangeTo(dateString) {
  const endDate = new Date(dateString);
  const today = new Date();

  // Normalize both dates to remove time portion
  today.setHours(0, 0, 0, 0);
  endDate.setHours(0, 0, 0, 0);

  // Exclude the input date itself
  endDate.setDate(endDate.getDate() - 1);

  const result = [];

  for (let d = new Date(today); d <= endDate; d.setDate(d.getDate() + 1)) {
    result.push(new Date(d));
  }

  return result;
}

export function formatDateFromTimestamp(timestamp) {
  const date = new Date(timestamp);

  const day = date.getDate(); // 1-31
  const month = date.toLocaleString('default', { month: 'short' }); // "May"
  const year = date.getFullYear(); // 2025

  return `${day} ${month}, ${year}`;
}

/**
 * Handles the firstname value to set first character capital.
 * @param event The event is the change event value for the input elements.
 */
export function setFirstCharCapital(event: React.ChangeEvent<HTMLInputElement>) {
  let value = event.target.value;
  if (value) {
    value = value.charAt(0).toUpperCase() + value.slice(1);
  }
  if (/\S/.test(value)) {
    return value;
  } else {
    return value.trim();
  }
};

/**
 * Formats a date string to "Day Month, Year" format,from the input date.
 * @param {string} dateString - The input date string.
 * @returns {string} The formatted date string in "Day Month" format.
*/
export function formatDateDayMonth(dateString: any) {
  const inputDate = new Date(dateString);

  inputDate.setDate(inputDate.getDate());

  // Format the date
  const day = inputDate.getDate();
  const month = inputDate.toLocaleString('default', { month: 'short' });

  return `${day} ${month}`;

}
/*
 * Converts a given date to a formatted string in "YYYY-MM-DD" format.
 *
 * @param {Date | string} date - The input date to format. Can be a JavaScript Date object or a valid date string.
 * @returns {string} The formatted date string in "YYYY-MM-DD" format.
 */
export function formatDateYYYYMMDD(date) {
  // Create a Moment.js object from the input date and format it as "YYYY-MM-DD"
  return moment(date).format('YYYY-MM-DD');
}

/**
 * Get the date that is 7 days before the given date.
 *
 * @param {string} dateStr - The input date string (e.g., "2025-06-01").
 * @returns {Date} - A `Date` object representing the date 7 days earlier.
 */
export function getPrevious7Day(dateStr) {
  const baseDate = new Date(dateStr);
  const previousDate = new Date(baseDate);
  previousDate.setDate(baseDate.getDate() - 6);
  return previousDate;
}

/**
 * 
 * @param {Date|string} inputDate - The starting date (Date object or date string)
 * @returns {boolean} true if the month after 7 days is different from the input date's month, else false
 */
export function isMonthDifferentAfter7Days(inputDate: any) {
  const baseDate = new Date(inputDate);
  const next7thDate = new Date(baseDate);
  next7thDate.setDate(baseDate.getDate() + 7);

  // Compare months (0-based index)
  return baseDate.getMonth() !== next7thDate.getMonth();
}

/**
 * Fetches the user's geolocation coordinates using openstreet.
 * @returns {Promise<{ lat: number | null, long: number | null }>} A promise that resolves to an object containing the latitude and longitude of the user's location.
 */
export const fetchGeoLocation = async (
  address: string,
  isMultiple: boolean
) => {
  const baseUrl = process.env.NEXT_PUBLIC_MAP_JSON_API_URL;
  const query = new URLSearchParams({
    q: address,
    format: 'json',
    ...(isMultiple ? {} : { limit: '1' }), // only add limit=1 if !isMultiple
  });

  const url = `${baseUrl}search?${query.toString()}`;
  try {
    const response = await fetch(url, {
      headers: {
        'Accept-Language': 'en'
      }
    });

    if (!response.ok) {
      throw new Error(`API returned status ${response.status}`);
    }

    const data = await response.json();

    if (data) {
      return isMultiple ? data : data[0]; // likely `data[0]`, not `data.address`
    }
  } catch (err) {
    return null;
  }
};

/**
 * Fetches the user's geolocation coordinates using openstreet.
 * @returns {Promise<{ lat: number | null, long: number | null }>} A promise that resolves to an object containing the latitude and longitude of the user's location.
 */
export const fetchGoogleGeoLocation = async (
  address: string,
) => {
  const baseUrl = process.env.NEXT_PUBLIC_GOOGLE_MAP_JSON_API_URL;
  const mapApiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;

  const url = `${baseUrl}?input=${encodeURIComponent(address)}&key=${mapApiKey}`;
  try {
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Accept-Language': 'en',
      },
    });
    const data = await response.json();

    if (data.status !== 'OK') {
      throw new Error(`API returned status ${response.status}`);
    } else {
      return data.predictions;
    }
  } catch (err) {
    console.error('Error fetching place suggestions:', err);
    return null;
  }
};

/**
 * Recursively converts all empty string values in an object or array to null.
 * @param {any} obj - The input object, array, or value to process.
 * @returns {any} A new object, array, or value where all empty strings are replaced with null.
 */
export function convertEmptyStringsToNull(obj: any): any {
  if (Array.isArray(obj)) {
    return obj.map(convertEmptyStringsToNull);
  } else if (obj !== null && typeof obj === 'object') {
    const result: any = {};
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        const value = obj[key];
        result[key] = value === "" ? null : convertEmptyStringsToNull(value);
      }
    }
    return result;
  }
  return obj;
}

/**
 * Removes specified keys from an object and returns a new object without those keys.
 * @param {object} obj - The source object to remove keys from.
 * @param {string | string[]} keysToRemove - A single key or array of keys to be removed.
 * @returns {object} A new object with the specified keys removed.
 */
export function removeKeysFromObject(obj: any, keysToRemove: any) {
  // Ensure keysToRemove is an array, even if only one key is passed
  const keys = Array.isArray(keysToRemove) ? keysToRemove : [keysToRemove];

  // Create a new object by filtering out the specified keys
  const result = Object?.keys(obj)?.reduce((acc, key) => {
    if (!keys.includes(key)) {
      acc[key] = obj[key];
    }
    return acc;
  }, {});

  return result;
}

/**
 * Formats a location object into a single string with comma-separated values.
 * @param {Record<string, string>} location - An object containing parts of an address (streetName, city, state, country, zip).
 * @returns {string} A formatted address string with available fields joined by commas.
 */
export const formatLocation = ({ streetName, city, state, country, zip }: Record<string, string>): string => {
  return [streetName, city, state, country, zip].filter(Boolean).join(', ');
};

/**
 * Fills short gaps between dates in an array, ensuring a continuous sequence of dates.
 * 
 * @param {string[]} dates - An array of date strings in ISO format (e.g., '2023-01-01').
 * @returns {string[]} - A sorted array of date strings in ISO format, with gaps of less than 7 days filled.
 * 
 * The function first converts the input date strings into Date objects and sorts them.
 * It identifies gaps between consecutive dates that are greater than 1 day but less than 7 days.
 * For each identified gap, it fills the missing days by generating new date strings and adds them to the result.
 * The final output is a sorted array of date strings, including the filled dates.
 */
export function fillShortGapsDates(dates) {
  const MS_PER_DAY = 24 * 60 * 60 * 1000;

  const today = new Date();
  const todayStr = today.toISOString().split('T')[0];

  // Set to track original dates
  const fullSet = new Set(dates);

  // Step 1–3: Add current date if it's not in the set and future 7 days contain any date from input
  if (!fullSet.has(todayStr)) {
    for (let i = 1; i <= 7; i++) {
      const futureDate = new Date(today.getTime() + i * MS_PER_DAY);
      const futureDateStr = futureDate.toISOString().split('T')[0];
      if (fullSet.has(futureDateStr)) {
        fullSet.add(todayStr); // Add current date if condition met
        break;
      }
    }
  }

  // Convert strings to Date objects and sort
  //@ts-ignore
  const dateObjects = Array.from(fullSet).map(d => new Date(d)).sort((a, b) => a - b);
  const result = [...dateObjects];

  for (let i = 1; i < dateObjects.length; i++) {
    const prevDate = dateObjects[i - 1];
    const currDate = dateObjects[i];
    //@ts-ignore
    const diffDays = (currDate - prevDate) / MS_PER_DAY;

    if (diffDays > 1 && diffDays < 9) {
      for (let j = 1; j < diffDays; j++) {
        const newDate = new Date(prevDate.getTime() + j * MS_PER_DAY);
        const isoDate = newDate.toISOString().split('T')[0];
        if (!fullSet.has(isoDate)) {
          fullSet.add(isoDate);
          result.push(newDate);
        }
      }
    }
  }

  // Format to `T16:32:01+05:30` and return Date objects
  return Array.from(fullSet)
    .map(dateStr => `${dateStr}T16:32:01+05:30`)
    .map(dateTimeStr => new Date(dateTimeStr))
    .sort((a: any, b: any) => a - b);
}

/**
 * Prevents the user from entering specific keys in a number input field.
 * @param {React.KeyboardEvent<HTMLInputElement>} e - The event object from the onKeyPress event.
 * Prevents the specified keys from being entered in the input field.
 */
export const numberFieldKeyPressValidation = (e: React.KeyboardEvent<HTMLInputElement>) => {
  const invalidKeys = ["e", "+", "-", "*", "/", "ArrowUp", "ArrowDown"];

  if (invalidKeys.includes(e.key)) {
    e.preventDefault();
  }
};

/**
 * Handles the wheel event on a number input field to prevent accidental changes.
 * @param {React.WheelEvent<HTMLInputElement>} e - The event object from the onWheel event.
 * Stops the scroll event from propagating and removes focus from the input to prevent accidental changes.
 */
export const handleWheelOnNumberInput = (e: React.WheelEvent<HTMLInputElement>) => {
  e.stopPropagation(); // Stops the scroll event from propagating
  (e.target as HTMLInputElement).blur(); // Removes focus from the input to prevent accidental changes
};
interface DropdownOption {
  value: number;
  label: string;
}
/**
 * Generates an array of age options for a dropdown.
 * @returns {DropdownOption[]} An array of age options for a dropdown.
 */
export const ageOptions: DropdownOption[] = Array.from({ length: 18 }, (_, i) => ({
  value: i,
  label: `${i} year${i > 1 ? 's' : ''}`
}));

/**
 * Converts 24-hour time format (e.g., "13:45") to 12-hour format with AM/PM (e.g., "1:45 PM").
 *
 * @param {string} time24 - Time string in 24-hour format ("HH:MM").
 * @returns {string} Time string in 12-hour format with AM/PM.
 */
export function convertTo12HourAMPM(time24: string | undefined | null): string {
  if (!time24 || typeof time24 !== "string" || !time24.includes(":")) {
    return "Invalid Time"; // or return an empty string, or handle as needed
  }

  const [hourStr, minuteStr] = time24.split(":");
  const hour = Number(hourStr);
  const minute = Number(minuteStr);

  if (isNaN(hour) || isNaN(minute)) {
    return "Invalid Time";
  }

  const period = hour >= 12 ? "PM" : "AM";
  const hour12 = hour % 12 === 0 ? 12 : hour % 12;

  return `${hour12}:${minute.toString().padStart(2, "0")} ${period}`;
}

/**
 * Capitalizes the first letter of a given string.
 * 
 * @param str - The input string to capitalize.
 * @returns A new string with the first letter capitalized and the rest unchanged.
 *          Returns an empty string if the input is not a valid string or is empty.
 */
export function capitalizeFirstLetter(str: string) {
  if (typeof str !== 'string' || !str.length) return '';
  return str.charAt(0).toUpperCase() + str.slice(1);
}

/**
 * Converts a number to days based on the duration type.
 * @param number - The number to convert.
 * @param durationType - The type of duration (Nights, Weeks, Months, Years).
 * @returns The number of days.
 */
export function ConvertToDays(number: number, durationType: string, less: boolean = false) {
  switch (durationType) {
    case "days":
      return number;
    case "nights":
      return number; // Already in days/nights
    case "weeks":
      return (number * 7) - (less ? 1 : 0);
    case "months":
      return number * 30; // Approximate
    case "years":
      return number * 365; // Approximate
    default:
      return 0;
  }
}


/**
 * Calculates the number of days between two dates.
 * 
 * @param checkinDate - The start date in string format.
 * @param checkoutDate - The end date in string format.
 * @returns The number of days between the checkin and checkout dates, 
 *          including both days. Returns 0 if the checkin date is after
 *          the checkout date.
 */
export function getDaysCount(checkinDate: string, checkoutDate: string) {
  const startDate: any = new Date(checkinDate);
  const endDate: any = new Date(checkoutDate);

  if (startDate > endDate) return 0;

  // Calculate difference in milliseconds
  const diffTime = endDate - startDate;

  // Convert milliseconds to days and add 1 to include both checkin and checkout dates
  const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)) + 1;

  return diffDays;
}

/**
 * Scrolls the page to the top of the window.
 * @category Utilities
 * @function scrollToTop
 */
export function scrollToTop() {
  window.scrollTo(0, 0);
}

/**
 * Checks if an object has a `meta` property that contains a `status` property.
 * This is typically used to check if an API response object has a valid `meta.status` property.
 *
 * @param obj - The object to check.
 *
 * @returns {boolean} `true` if the object has a valid `meta.status` property, `false` otherwise.
 */
export function hasMeta(obj: any): obj is { meta: { status: number } } {
  return obj && typeof obj === 'object' && 'meta' in obj && 'status' in obj.meta;
}

/**
 * Checks if the given Open Graph type is valid.
 * @param {string} type - The Open Graph type to check.
 * @returns {boolean} `true` if the type is valid, `false` otherwise.
 * @see https://ogp.me/#types for more information on valid Open Graph types.
 */
export function isValidOGType(type: string) {
  const validTypes = ['website', 'article', 'book', 'profile'];
  return validTypes.includes(type);
}