import type { DefaultOptionType } from 'ant-design-vue/lib/select'
import type { SortableOptions } from 'sortablejs'
import type { AutoScrollOptions } from 'sortablejs/plugins'
import type { UserType } from 'nocodb-sdk'
import { ncIsArray } from 'nocodb-sdk'
import GraphemeSplitter from 'grapheme-splitter'

export const modalSizes = {
  xs: {
    width: 'min(calc(100vw - 32px), 448px)',
    height: 'min(90vh, 448px)',
  },
  sm: {
    width: 'min(calc(100vw - 32px), 640px)',
    height: 'min(90vh, 424px)',
  },
  md: {
    width: 'min(80vw, 900px)',
    height: 'min(90vh, 540px)',
  },
  lg: {
    width: 'min(80vw, 1280px)',
    height: 'min(90vh, 864px)',
  },
  fullscreen: {
    width: '100vw',
    height: '100vh',
  },
  extensionFullscreen: {
    width: 'calc(100vw - 32px)',
    height: 'calc(100vh - var(--topbar-height) - 16px)',
  },
}

/**
 * Creates a promise that resolves after a specified delay.
 *
 * @param ms - The delay in milliseconds.
 * @returns A promise that resolves after the specified delay.
 *
 * @example
 * ```ts
 * // Wait for 2 seconds
 * await delay(2000);
 * console.log('2 seconds have passed');
 * ```
 */
export const ncDelay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

/**
 * Generates an array of a given length with content generated by the provided callback function.
 *
 * @param length - The length of the array to be created.
 * @param contentCallback - Optional function to generate content for each index. Defaults to using the index as content.
 * @returns The generated array with content.
 *
 * @example
 * // Generate an array of length 5 with default content
 * const array = ncArrayFrom(5);
 * console.log(array); // ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']
 *
 * @example
 * // Generate an array of length 3 with custom content
 * const customArray = ncArrayFrom(3, (i) => `Custom Content ${i}`);
 * console.log(customArray); // ['Custom Content 0', 'Custom Content 1', 'Custom Content 2']
 */
export const ncArrayFrom = <T>(
  length: number,
  contentCallback: (i: number) => T = (i) => `Item ${i + 1}` as unknown as T,
): T[] => {
  return Array.from({ length }, (_, i) => contentCallback(i))
}

/**
 * Checks if a string contains Unicode emojis.
 *
 * @param emoji - The string to check.
 * @returns A boolean indicating if the string contains Unicode emojis.
 *
 * @example
 * ```ts
 * const hasEmoji = isUnicodeEmoji('Hello World 😊');
 * console.log(hasEmoji); // Output: true
 * ```
 */
export const isUnicodeEmoji = (emoji: string) => {
  return !!emoji?.match(/(\p{Emoji}|\p{Extended_Pictographic})/gu)
}

/**
 * Get safe initials, emoji-safe & surrogate-safe.
 *
 * Rules: if limitEmojiToOne is true, then:
 * - Emoji + Letter → keep both
 * - Emoji + Emoji → keep only first emoji
 */
export const getSafeInitials = (title?: string, initialsLength: number = 2, limitEmojiToOne: boolean = false): string => {
  if (!title?.trim()) return ''

  const splitter = new GraphemeSplitter()
  const splitGraphemes = (str: string) => splitter.splitGraphemes(str)

  const words = title.trim().split(/\s+/).filter(Boolean)
  const initials: string[] = []

  // First take initials from each word
  for (const word of words) {
    if (initials.length >= initialsLength) break
    const g = splitGraphemes(word)
    if (g.length > 0) initials.push(g[0]!)
  }

  // If only 1 word or initialsLength > words.length
  if (initials.length < initialsLength && words.length > 0) {
    const g = splitGraphemes(words[0] ?? '')
    const remaining = initialsLength - initials.length
    initials.push(...g.slice(1, 1 + remaining)) // continue slicing same word
  }

  // Final slice by allowed length
  const resultClusters = splitGraphemes(initials.join('')).slice(0, initialsLength)

  // ✅ Smart emoji limit:
  // If ALL characters are emoji & more than 1 → limit to 1
  if (limitEmojiToOne && resultClusters.length > 1 && resultClusters.every((ch) => isUnicodeEmoji(ch))) {
    return resultClusters[0] ?? '' // only first emoji
  }

  return resultClusters.join('')
}

/**
 * Performs a case-insensitive search to check if the `query` exists within the `source`.
 *
 * - Handles strings, numbers, and arrays (including nested arrays) of strings/numbers.
 * - Treats `undefined` as an empty string.
 *
 * @param source - The value to search within. Can be:
 *   - A string or number.
 *   - A single-level or nested array of strings/numbers.
 * @param query - The value to search for. Treated as an empty string if `undefined`.
 * @returns `true` if the `query` is found within the `source` (case-insensitively), otherwise `false`.
 *
 * @example
 * ```typescript
 * // Single string or number search
 * searchCompare("Hello World", "world"); // true
 * searchCompare(12345, "234"); // true
 *
 * // Array search
 * searchCompare(["apple", "banana", "cherry"], "Banana"); // true
 * searchCompare([123, 456, 789], "456"); // true
 *
 * // Nested array search
 * searchCompare(["apple", ["banana", ["cherry"]]], "cherry"); // true
 * searchCompare([123, [456, [789]]], "456"); // true
 *
 * // Handling undefined
 * searchCompare(undefined, "test"); // false
 * searchCompare("test", undefined); // true
 * ```
 */
export const searchCompare = (
  source?: NestedArray<string | number | undefined>,
  query?: string,
  onMatch?: (source: string | number | undefined) => void,
): boolean => {
  if (ncIsArray(source)) {
    return source.some((item) => searchCompare(item, query, onMatch))
  }

  const isMatch = (source || '')
    .toString()
    .toLowerCase()
    .includes((query || '').toLowerCase())

  if (isMatch && onMatch) {
    onMatch(source)
  }

  return isMatch
}

/**
 * Filters options for an Ant Design Select component based on an input value.
 *
 * @param inputValue - The input value to filter against.
 * @param option - The option to evaluate for filtering.
 * @param searchKey - The key(s) in the option object to compare against the input value. Defaults to 'key'.
 * @returns `true` if the option matches the input value, otherwise `false`.
 */
export const antSelectFilterOption = (
  inputValue: string,
  option?: DefaultOptionType | NcListItemType,
  searchKey: keyof DefaultOptionType | keyof NcListItemType | (keyof NcListItemType)[] | (keyof DefaultOptionType)[] = 'key',
) => {
  if (!option) return false

  const optionValue = ncIsArray(searchKey) ? searchKey.map((key) => option[key]) : [option[searchKey]]

  return searchCompare(optionValue, inputValue)
}

/**
 * Extracts the name from an email address.
 *
 * @param email - The email address to extract the name from.
 * @returns The name extracted from the email address.
 *
 * @example
 * ```typescript
 * const name = extractNameFromEmail('john.doe@example.com');
 * console.log(name); // Output: 'john.doe'
 * ```
 */
export const extractNameFromEmail = (email?: string) => {
  if (!email) return ''

  return email?.slice(0, email.indexOf('@')) ?? ''
}

/**
 * Extracts the display name or email from a user object.
 *
 * @param user - The user object to extract the display name or email from.
 * @returns The display name or email extracted from the user object.
 *
 * @example
 * ```typescript
 * const name = extractUserDisplayNameOrEmail({ display_name: 'John Doe', email: 'john.doe@example.com' });
 * console.log(name); // Output: 'John Doe'
 */
export const extractUserDisplayNameOrEmail = (user?: UserType | Record<string, string>) => {
  if (!user) return ''

  if (user?.display_name) return user.display_name.trim()

  return extractNameFromEmail(user.email)
}

/**
 * Wait for a condition to be truthy
 * @param conditionFn - Function that returns the condition to check
 * @param interval - Polling interval in milliseconds (default: 100)
 * @returns Promise that resolves when condition becomes truthy
 */
export function waitForCondition(conditionFn: () => unknown, interval: number = 100): Promise<void> {
  return new Promise((resolve) => {
    const check = (): void => {
      if (conditionFn()) {
        resolve()
      } else {
        setTimeout(check, interval)
      }
    }
    check()
  })
}

export const pollUntil = <T>(conditionFn: () => T | null | undefined | false, interval: number = 100): Promise<T> => {
  return new Promise((resolve, reject) => {
    const check = () => {
      try {
        const result = conditionFn()
        if (result) {
          resolve(result)
        } else {
          setTimeout(check, interval)
        }
      } catch (error) {
        reject(error)
      }
    }

    check()
  })
}

export const getDraggableAutoScrollOptions = (
  params: Partial<AutoScrollOptions & { direction: SortableOptions['direction'] }> = {},
): Partial<AutoScrollOptions & { direction: SortableOptions['direction'] }> => {
  return {
    /**
     * scroll property is used to enable auto scroll plugin
     */
    scroll: true,
    /**
     * force the autoscroll fallback to kick in
     * if this value is false then updated `scrollSensitivity` will not work
     */
    forceAutoScrollFallback: true,
    /**
     * px, how near the mouse must be to an edge to start scrolling.
     */
    scrollSensitivity: 50,
    direction: 'vertical',
    ...params,
  }
}

/**
 * Sorts two objects based on the provided sorting options.
 * @param a - The first object to sort.
 * @param b - The second object to sort.
 * @param sortOption - The sorting options to apply.
 * @returns The sorted objects.
 */
export const ncArrSortCallback = <T>(
  a: T,
  b: T,
  sortOption: {
    /** The key of the object being used for sorting. */
    key: keyof T & string
    /**
     * The type of sorting to apply (e.g., string, number).
     * @default 'number'
     */
    sortType?: 'string' | 'number' | 'boolean' | 'count'
    /** Optional formatter to preprocess the value before sorting. */
    format?: (value: any) => any
  },
) => {
  const { key, sortType = 'number', format } = sortOption || {}

  const valueA = format ? format(a[key]) : a[key]
  const valueB = format ? format(b[key]) : b[key]

  switch (sortType) {
    case 'number':
      return (valueA ?? Infinity) - (valueB ?? Infinity)
    case 'string': {
      return String(valueA).localeCompare(String(valueB))
    }
    case 'boolean': {
      return valueA === valueB ? 0 : valueA ? 1 : -1
    }
    case 'count': {
      const lengthA = ncIsString(valueA) || ncIsArray(valueA) ? valueA.length : Infinity
      const lengthB = ncIsString(valueB) || ncIsArray(valueB) ? valueB.length : Infinity

      return lengthA === lengthB ? 0 : lengthA > lengthB ? 1 : -1
    }

    default: {
      return 0
    }
  }
}
