import moment, { Moment } from "moment"
import "moment-timezone"
import { messages } from "../constants/messages"
import * as bcrypt from "bcrypt"
import { countryStateJson } from "../static-data/country-state.json"
import * as fs from "node:fs"
import process from "node:process"
import axios from "axios"
import tzLookup from "tz-lookup"
import * as semver from "semver"
import { VERSION_NEUTRAL } from "@nestjs/common"

/**
 * Check if the input data is empty or not.
 * @param data - The data to be checked for emptiness.
 * @returns True if the data is considered empty, false otherwise.
 */
export const isEmpty = (data: any): boolean => {
  // Return true if the data is null or undefined
  if (data == null) {
    return true
  }

  // Check if the data is a string and includes only empty values
  if (typeof data === "string") {
    return ["", null, "null", undefined].includes(data)
  }

  // Check if the data is an array and has no elements
  if (Array.isArray(data)) {
    return data.length === 0
  }

  // Check if the data is a date object and has no keys
  if (data instanceof Date) {
    return isNaN(data.getTime())
  }

  // Check if the data is an object and has no keys
  if (typeof data === "object") {
    return Object.keys(data).length === 0
  }

  // Default case: return true if the data length is 0
  return data.length === 0
}

/**
 * Generates a new object by removing specified nested keys from the original object.
 *
 * @template T - The type of the original object.
 * @param {T} obj - The original object.
 * @param {string[]} keys - An array of keys to be removed from the original object.
 * @return {T} - A new object with the specified keys removed.
 */
export const generateResponseObject = <T>(obj: T, keys: string[]): T => {
  const result = { ...obj }
  keys.forEach((key) => {
    const nestedKeys = key.split(".")
    let currentObject = result
    for (const k of nestedKeys) {
      if (currentObject[k]) {
        delete currentObject[k]
      }
      currentObject = currentObject[k]
    }
  })
  return result
}

/**
 * Returns the current timestamp using the moment.js library.
 *
 * @return {Moment} The current timestamp.
 */
export const currentTimestamp = (): Moment => {
  return moment()
}

/**
 * Calculates the difference in minutes between the current time and the given timestamp.
 *
 * @param {number} timestamp - The timestamp to calculate the difference from.
 * @return {number} The difference in minutes between the current time and the given timestamp.
 */
export const findMinutes = (timestamp: number): number => {
  const currentTime = moment.utc()

  const parsedTimestamp = moment.unix(timestamp) // Ensure the timestamp is parsed as UTC

  return currentTime.diff(parsedTimestamp, "minutes")
}

/**
 * Returns the current year.
 *
 * @return {number} The current year.
 */
export const getCurrentYear = (): number => {
  return new Date().getFullYear()
}

const getMessage = (type: string, key: string, replaceKeys: any): string => {
  let message = messages[type][key]

  if (!isEmpty(replaceKeys)) {
    Object.keys(replaceKeys).forEach((key) => {
      message = message.replace(key, replaceKeys[key])
    })
  }

  return message
}

export const successMessage = (key: string, replaceKeys?: any): string => {
  return getMessage("success", key, replaceKeys)
}

export const errorMessage = (key: string, replaceKeys?: any): string => {
  return getMessage("error", key, replaceKeys)
}

export const validationMessage = (key: string, replaceKeys?: any): string => {
  return getMessage("validation", key, replaceKeys)
}

export const encryptPassword = async (password: string) => {
  const salt = await bcrypt.genSalt()
  return await bcrypt.hash(password, salt)
}

export const getControllerVersion = () => {
  return process.env.NODE_ENV === "production" ? VERSION_NEUTRAL : "1"
}

export const sendEmailNotification = async (
  toEmail: string,
  html: any,
  subject: string,
  cc?: string,
  bcc?: string,
) => {
  // Import the EmailService dynamically to avoid circular dependencies
  const { EmailService } = await import("./email.service")

  // Create an instance of EmailService
  const emailService = new EmailService()

  try {
    const messageId = await emailService.sendEmail(
      toEmail,
      subject,
      { html },
      cc,
      bcc,
    )
    console.log("Mail sent successfully: ", messageId)
  } catch (error) {
    console.log("Error sending mail:", error.message)
  }
}

/**
 * Fetches the city from the provided state and country from the countryStateJson object.
 *
 * @param {string} state - The state to search for in the countryStateJson object.
 * @param {string} country - The country to search for in the countryStateJson object.
 * @return {Array} An array containing the cities matching the provided state, or an empty array if no match is found.
 */
export const fetchCityFromJson = (state: string, country: string) => {
  // Loop through each object in the array
  for (const obj of countryStateJson[country]) {
    // Check if the object has the given province as a key
    if (obj.hasOwnProperty(state)) {
      return obj[state]
    }
  }
  // Return null or an empty array if the province key is not found
  return []
}

/**
 * Creates a folder at the specified path if it does not already exist.
 *
 * @param {string} path - The path of the folder to create.
 * @return {void} This function does not return anything.
 */
export const createFolderIfNotExist = (path: string): void => {
  if (!fs.existsSync(path)) {
    fs.mkdirSync(path, { recursive: true, mode: 0o775 })
  }
}

export const getCurrentEnvironment = () => {
  return process.env.NODE_ENV || "production"
}

export const generatePassword = () => {
  const minLength = 10
  const minLowercase = 2
  const minNumbers = 2
  const minSymbols = 1
  const minUppercase = 2

  const lowercaseChars = "abcdefghijklmnopqrstuvwxyz"
  const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  const numberChars = "0123456789"
  const symbolChars = "!@#$%^&*()_+[]{}|;:,.<>?"

  const getRandomChar = (chars) =>
    chars[Math.floor(Math.random() * chars.length)]

  let password = ""

  // Ensure minimum counts for each type
  for (let i = 0; i < minLowercase; i++)
    password += getRandomChar(lowercaseChars)
  for (let i = 0; i < minUppercase; i++)
    password += getRandomChar(uppercaseChars)
  for (let i = 0; i < minNumbers; i++) password += getRandomChar(numberChars)
  for (let i = 0; i < minSymbols; i++) password += getRandomChar(symbolChars)

  // Add random characters until reaching the desired length
  const allChars = lowercaseChars + uppercaseChars + numberChars + symbolChars
  while (password.length < minLength) {
    password += getRandomChar(allChars)
  }

  // Shuffle the password to ensure random order
  password = password
    .split("")
    .sort(() => Math.random() - 0.5)
    .join("")

  return password
}
export const subtractMinutes = (datetime: string, minutes: number): string => {
  return moment(datetime, "YYYY-MM-DD HH:mm")
    .subtract(minutes, "minutes")
    .format("YYYY-MM-DD HH:mm")
}

export const addMinutes = (datetime: string, minutes: number): string => {
  return moment(datetime, "YYYY-MM-DD HH:mm")
    .add(minutes, "minutes")
    .format("YYYY-MM-DD HH:mm")
}

export const formateDate = (
  date: string | Date | moment.Moment,
  format = "YYYY-MM-DD HH:mm",
): string => {
  return moment(date).format(format)
}

export const sendErrorSlackNotification = async (
  url: string,
  message: any,
): Promise<void> => {
  try {
    let slackText: string

    if (typeof message === "string") {
      slackText = message
    } else if (message instanceof Error) {
      slackText = `${message.message}\n\n${message.stack}`
    } else {
      slackText = JSON.stringify(message, null, 2)
    }

    await axios.post(url, { text: slackText })
  } catch (error: any) {}
}

/**
 * Converts a UTC datetime to the specified timezone.
 *
 * This function takes a UTC datetime and converts it to the given timezone using moment-timezone.
 * It ensures proper handling of daylight saving time and timezone offsets.
 *
 * @param datetime - The UTC datetime to convert. Accepts string (ISO format recommended), Date object, or Moment object.
 * @param timezone - The target timezone identifier (e.g., 'America/New_York', 'Europe/London').
 * @param format - Optional format string for the output (e.g., 'YYYY-MM-DD HH:mm:ss'). If provided, returns a formatted string; otherwise, returns a Moment object.
 * @returns A Moment object or formatted string representing the datetime in the specified timezone.
 * @throws Error if the datetime is invalid or the timezone is not recognized.
 *
 * @example
 * // Convert UTC string to Eastern Time (returns Moment object)
 * const utcTime = '2023-10-01T12:00:00Z';
 * const easternTime = convertUtcToTimezone(utcTime, 'America/New_York');
 * console.log(easternTime.format()); // Output: 2023-10-01T08:00:00-04:00
 *
 * @example
 * // Convert UTC string to Eastern Time with custom format (returns string)
 * const utcTime = '2023-10-01T12:00:00Z';
 * const easternTimeFormatted = convertUtcToTimezone(utcTime, 'America/New_York', 'YYYY-MM-DD HH:mm:ss');
 * console.log(easternTimeFormatted); // Output: 2023-10-01 08:00:00
 *
 * @example
 * // Convert Moment object to Pacific Time
 * const utcMoment = moment.utc('2023-07-01 15:30:00');
 * const pacificTime = convertUtcToTimezone(utcMoment, 'America/Los_Angeles');
 * console.log(pacificTime.format('YYYY-MM-DD HH:mm:ss')); // Output: 2023-07-01 08:30:00
 *
 * @example
 * // Using with tripTimeZone values
 * const timezone = tripTimeZone.find(tz => tz.label.includes('Pacific'))?.value;
 * const converted = convertUtcToTimezone('2023-12-25T00:00:00Z', timezone);
 * console.log(converted.format());
 *
 * Alternative approaches:
 * - Use date-fns-tz library: More modern, tree-shakable, but requires separate installation.
 * - Native JavaScript: Use Intl.DateTimeFormat with timeZone option for formatting, but conversion logic is more complex.
 * - Manual offset calculation: Simple but doesn't handle DST properly.
 */
export const convertUtcToTimezone = (
  datetime: string | Date | Moment,
  timezone: string = "America/New_York",
  format?: string,
): Moment | string => {
  try {
    console.debug("datetime", datetime, "timezone", timezone, "format", format)

    if (!datetime) {
      return "N/A"
    }

    const utcMoment = moment.utc(datetime)

    console.debug("utcMoment", utcMoment)

    // Validate the parsed datetime
    if (!utcMoment.isValid()) {
      throw new Error(
        "Invalid datetime provided. Ensure it's in a recognizable format (e.g., ISO string, Date object, or Moment).",
      )
    }

    // Convert to the specified timezone
    const convertedMoment = utcMoment.tz(timezone)

    console.debug("convertedMoment", convertedMoment)

    // Validate the timezone conversion
    if (!convertedMoment.isValid()) {
      throw new Error(
        `Invalid timezone '${timezone}' provided. Use IANA timezone identifiers (e.g., 'America/New_York').`,
      )
    }

    // Return formatted string if format is provided, otherwise return Moment object
    const result = format ? convertedMoment.format(format) : convertedMoment
    console.debug("result", result)
    return result
  } catch (error) {
    // Re-throw with additional context if it's not already our custom error
    if (error.message.includes("Failed to convert")) {
      throw error
    }
    throw new Error(
      `Failed to convert UTC datetime to timezone '${timezone}': ${error.message}`,
    )
  }
}

export const addOnsEnum = {
  CHILD_SEAT: "Child Seat",
}

export const dutyStatus = {
  OFF_DUTY: "off-duty",
}

export const tripTimeZone = [
  {
    label: "Eastern Time – US & Canada",
    value: "America/New_York",
    abbreviation: "EST/EDT",
    utcOffset: "-05:00",
  },
  {
    label: "Central Time – US & Canada",
    value: "America/Chicago",
    abbreviation: "CST/CDT",
    utcOffset: "-06:00",
  },
  {
    label: "Mountain Time – US & Canada",
    value: "America/Denver",
    abbreviation: "MST/MDT",
    utcOffset: "-07:00",
  },
  {
    label: "Arizona Time – No DST",
    value: "America/Phoenix",
    abbreviation: "MST",
    utcOffset: "-07:00",
  },
  {
    label: "Pacific Time – US & Canada",
    value: "America/Los_Angeles",
    abbreviation: "PST/PDT",
    utcOffset: "-08:00",
  },
  {
    label: "Alaska Time – US & Canada",
    value: "America/Anchorage",
    abbreviation: "AKST/AKDT",
    utcOffset: "-09:00",
  },
  {
    label: "Hawaii–Aleutian Time",
    value: "Pacific/Honolulu",
    abbreviation: "HST",
    utcOffset: "-10:00",
  },
  {
    label: "Atlantic Time – Puerto Rico & US Virgin Islands",
    value: "America/Puerto_Rico",
    abbreviation: "AST",
    utcOffset: "-04:00",
  },
]

export const getTimezone = (lat, lng) => {
  try {
    const timezone = tzLookup(lat, lng)
    return timezone
  } catch (err) {
    console.error("Error finding timezone:", err)
    return "America/New_York" // Default fallback
  }
}

export const getLatLongFromAddress = async (address: string) => {
  try {
    const apiKey = process.env.GOOGLE_MAPS_API_KEY

    const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
      address,
    )}&key=${apiKey}`

    const response = await axios.get(url)

    if (response.data.status !== "OK") {
      return null
    }

    const location = response.data.results[0].geometry.location

    return {
      latitude: location.lat,
      longitude: location.lng,
    }
  } catch (error) {
    console.error("Google Maps API error:", error.message)
    return null
  }
}

/**
 * Fetch Google Place ID (and optionally lat/lng) from an address via Geocoding API.
 */
export const getPlaceIdFromAddress = async (
  address: string,
): Promise<{
  place_id: string
  latitude?: number
  longitude?: number
} | null> => {
  try {
    const apiKey = process.env.GOOGLE_MAPS_API_KEY
    if (!apiKey) return null

    const trimmed = address?.trim()
    if (!trimmed) return null

    const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
      trimmed,
    )}&key=${apiKey}`

    const response = await axios.get(url)
    if (response.data.status !== "OK" || !response.data.results?.length) {
      return null
    }

    const first = response.data.results[0]
    const place_id = first.place_id
    const loc = first.geometry?.location

    if (!place_id) return null

    return {
      place_id,
      latitude: loc?.lat,
      longitude: loc?.lng,
    }
  } catch (error) {
    console.error("Google Maps Geocoding API error:", (error as Error).message)
    return null
  }
}

export const normalizeVersion = (version: string): string => {
  const coerced = semver.coerce(version)
  return coerced ? coerced.version : version
}

export const BASELINE_VERSIONS = {
  android: {
    driver: "7.0.0",
    USER: "6.0.0",
  },
  ios: {
    driver: "2.0.1",
    USER: "1.2.0",
  },
}
