import {
  DateFilter,
  FieldValueType,
  FilterConfigItem,
  FilterState,
  NumberFilter,
  PaginatedResult,
  RelationFilterConfig,
  StringFilter,
} from '@types'

export function matchFilter(itemValue: any, filterValues: any[] | undefined): boolean {
  if (!filterValues || filterValues.length === 0) {
    return true
  }
  if (itemValue === null || itemValue === undefined || itemValue === '') {
    return filterValues.includes('')
  }
  const itemString = itemValue instanceof Date ? itemValue.toISOString() : String(itemValue)
  return filterValues.some((f) => String(f) === itemString)
}

export function matchBooleanFilter(
  itemValue: boolean | null | undefined,
  filterValues: boolean[] | undefined,
): boolean {
  if (!filterValues || filterValues.length === 0) {
    return true
  }
  // Treat null/undefined as false (e.g. nullable `isResolved` field: null means "not resolved")
  const effectiveValue = itemValue ?? false
  return filterValues.includes(effectiveValue)
}

export function matchNumberFilter(itemValue: number | null, filter: NumberFilter | undefined): boolean {
  if (!filter) {
    return true
  }
  const { values, min, max } = filter

  // Check range
  if (itemValue == null) {
    // If entry is empty, it fails range check if min or max is set
    if ((min !== undefined && min !== null) || (max !== undefined && max !== null)) {
      return false
    }
  } else {
    if (min !== undefined && min !== null && itemValue < min) {
      return false
    }
    if (max !== undefined && max !== null && itemValue > max) {
      return false
    }
  }

  return matchFilter(itemValue, values)
}

export function matchDateFilter(itemValue: Date | null, filter: DateFilter | undefined): boolean {
  if (!filter) {
    return true
  }
  const { values, start, end } = filter
  const itemString = itemValue?.toISOString() ?? null

  // Check range
  if (itemString) {
    if (start && itemString < start) {
      return false
    }
    if (end && itemString > end) {
      return false
    }
  } else if (start || end) {
    // If entry is empty, it fails range check if start or end is set
    return false
  }

  return matchFilter(itemValue, values)
}

/**
 * Matches a string value against a StringFilter.
 * @param itemValue - The raw value to match (used for values array matching)
 * @param filter - The StringFilter to apply
 * @param displayValue - Optional display value for text filter matching (contains, startsWith, endsWith, exclude).
 *                       Used for relation fields where the raw value is an ID but text should match against the label.
 */
export function matchStringFilter(
  itemValue: string | null | undefined,
  filter: StringFilter | undefined,
  displayValue?: string,
): boolean {
  if (!filter) {
    return true
  }

  // Use displayValue for text matching if provided, otherwise use the raw value
  const textValue = displayValue ?? itemValue ?? ''
  const lowerTextValue = textValue.toLowerCase()

  // Check startsWith (against display value)
  if (filter.startsWith && !lowerTextValue.startsWith(filter.startsWith.toLowerCase())) {
    return false
  }
  // Check contains (against display value)
  if (filter.contains && !lowerTextValue.includes(filter.contains.toLowerCase())) {
    return false
  }
  // Check endsWith (against display value)
  if (filter.endsWith && !lowerTextValue.endsWith(filter.endsWith.toLowerCase())) {
    return false
  }
  // Check exclude (against display value) - if value contains exclude string, reject it
  if (filter.exclude && lowerTextValue.includes(filter.exclude.toLowerCase())) {
    return false
  }

  // Check values array (exact match against raw value)
  return matchFilter(itemValue, filter.values)
}

/**
 * Matches an item against a global search term by checking all filterable fields.
 * Uses OR logic - if any field matches, the item is included.
 * @param referencedItemsMap - Optional map of field keys to their referenced items Map.
 *                             Used for relation fields to resolve IDs to display labels via getLabel.
 * @param accessors - Optional map of field keys to accessor functions.
 *                    Used when filterKey doesn't map directly to item property (e.g., discriminated union fields).
 */
export function matchGlobalSearch<
  T extends { filterKey: string; getLabel?: (value: unknown, refs?: Map<string, any>) => string },
>(
  searchTerm: string,
  item: Record<string, unknown>,
  filterConfig: readonly T[],
  referencedItemsMap?: Map<string, Map<string, any> | undefined>,
  accessors?: Map<string, (item: any) => any>,
): boolean {
  const term = searchTerm.toLowerCase()

  for (const config of filterConfig) {
    // Use custom accessor if provided, otherwise fall back to direct property access
    const accessor = accessors?.get(config.filterKey)
    const value = accessor ? accessor(item) : item[config.filterKey]
    if (value == null) {
      continue
    }

    // Pass referenced items to getLabel for relation fields to resolve IDs to labels
    const referencedItems = referencedItemsMap?.get(config.filterKey)
    const displayValue = config.getLabel ? config.getLabel(value, referencedItems) : String(value)

    if (displayValue.toLowerCase().includes(term)) {
      return true // OR logic - any match includes the row
    }
  }

  return false
}

/**
 * Checks if a StringFilter has text-based matching criteria set.
 * Used to determine if referenced items need to be loaded for label resolution.
 */
export function hasTextMatching(filter: StringFilter | undefined): boolean {
  if (!filter) {
    return false
  }
  return !!(filter.contains || filter.startsWith || filter.endsWith || filter.exclude)
}

/**
 * Type for hierarchy node data used in filter expansion.
 * Must have id and childIds fields for tree traversal.
 */
export type HierarchyNodeLike = {
  id: string
  childIds: string[]
}

/**
 * Expands a set of hierarchy node IDs to include all descendant IDs.
 * Used for hierarchy filtering where selecting a parent should include all children.
 *
 * @param selectedIds - The IDs selected in the filter
 * @param hierarchyNodesMap - Map of all hierarchy nodes with their childIds
 * @returns Set of IDs including selected nodes and all their descendants
 */
export function expandHierarchyFilterValues(
  selectedIds: string[] | undefined,
  hierarchyNodesMap: Map<string, HierarchyNodeLike> | undefined,
): Set<string> {
  const expandedIds = new Set<string>()

  if (!selectedIds || selectedIds.length === 0 || !hierarchyNodesMap) {
    return expandedIds
  }

  // Recursive function to add a node and all its descendants
  const addWithDescendants = (nodeId: string) => {
    if (expandedIds.has(nodeId)) {
      return // Already processed
    }
    expandedIds.add(nodeId)

    const node = hierarchyNodesMap.get(nodeId)
    if (node?.childIds) {
      for (const childId of node.childIds) {
        addWithDescendants(childId)
      }
    }
  }

  // Process each selected ID
  for (const id of selectedIds) {
    addWithDescendants(id)
  }

  return expandedIds
}

/**
 * Matches a hierarchy value against a filter, expanding to include descendants.
 * When a parent node is selected in the filter, all descendants are considered matches.
 *
 * @param itemValue - The hierarchy node ID on the item being filtered
 * @param filter - The filter containing selected node IDs in the values array
 * @param hierarchyNodesMap - Map of all hierarchy nodes for expansion
 */
export function matchHierarchyFilter(
  itemValue: string | null | undefined,
  filter: StringFilter | undefined,
  hierarchyNodesMap: Map<string, HierarchyNodeLike> | undefined,
): boolean {
  if (!filter?.values?.length) {
    return true
  }

  // Expand selected values to include descendants
  const expandedValues = expandHierarchyFilterValues(filter.values, hierarchyNodesMap)

  // Check if item value matches any expanded value
  if (itemValue === null || itemValue === undefined || itemValue === '') {
    return expandedValues.has('')
  }
  return expandedValues.has(itemValue)
}

/**
 * Applies range filters for number and date fields.
 */
export function applyRangeFilters<T, TFilters extends FilterState>(
  items: T[],
  accessor: (item: T) => any,
  config: FilterConfigItem<TFilters>,
  filters: TFilters,
): T[] {
  let filteredItems = items
  if (config.valueType === 'number') {
    const filter = filters[config.filterKey] as NumberFilter | undefined
    if (filter) {
      const { min, max } = filter
      if (min !== undefined || max !== undefined) {
        // Apply range filter manually. We only check the range part here, ignoring 'values'
        filteredItems = filteredItems.filter((item) => matchNumberFilter(accessor(item), { min, max }))
      }
    }
  } else if (config.valueType === 'date') {
    const filter = filters[config.filterKey] as DateFilter | undefined
    if (filter) {
      const { start, end } = filter
      if (start !== undefined || end !== undefined) {
        // Apply range filter manually. We only check the range part here, ignoring 'values'
        filteredItems = filteredItems.filter((item) => matchDateFilter(accessor(item), { start, end }))
      }
    }
  }
  return filteredItems
}

/**
 * Applies string filters (contains, startsWith, endsWith, exclude) for string fields.
 * For relation fields, pass getLabel and referencedItems to match against labels instead of raw IDs.
 */
export function applyStringFilters<T>(
  items: T[],
  accessor: (item: T) => any,
  filter: StringFilter | undefined,
  getLabel?: (value: unknown, referencedItems?: Map<string, any>) => string,
  referencedItems?: Map<string, any>,
): T[] {
  if (!filter) {
    return items
  }

  const { contains, startsWith, endsWith, exclude } = filter

  // If no text filters are set, return all items
  if (!contains && !startsWith && !endsWith && !exclude) {
    return items
  }

  // Apply string filters manually. We only check the text filter parts here, ignoring 'values'
  return items.filter((item) => {
    const value = accessor(item)
    const displayValue = getLabel ? getLabel(value, referencedItems) : String(value)
    return matchStringFilter(value, { contains, startsWith, endsWith, exclude }, displayValue)
  })
}

/**
 * Collects matches from the filtered items.
 */
export function getMatches<T>(filteredItems: T[], accessor: (item: T) => any): Set<string> {
  const matches = new Set<string>()
  for (const item of filteredItems) {
    const val = accessor(item)
    if (val === null || val === undefined || val === '') {
      matches.add('')
    } else if (val instanceof Date) {
      matches.add(val.toISOString())
    } else {
      matches.add(String(val))
    }
  }
  return matches
}

/**
 * Generates all possible options based on field type.
 */
export function getOptions<T>(
  allItems: T[],
  accessor: (item: T) => any,
  config: FilterConfigItem<any>,
  referencedItems?: Map<string, any>,
): { value: string; label: string }[] {
  if (config.kind === 'relation') {
    return referencedItems ? getRelationOptions(allItems, accessor, config, referencedItems) : []
  }

  if (config.kind === 'enum') {
    return config.enumValues.map((s: string) => ({ value: s, label: s }))
  }

  if (config.kind === 'discriminatedUnion') {
    return config.duMemberTypes.map((m: { type: string; label: string }) => ({ value: m.type, label: m.label }))
  }

  if (config.valueType === 'boolean') {
    return [
      { value: 'true', label: 'Yes' },
      { value: 'false', label: 'No' },
    ]
  }

  return getScalarOptions(allItems, accessor)
}

/**
 * Generates relation options from referenced items.
 */
function getRelationOptions<T>(
  allItems: T[],
  accessor: (item: T) => any,
  config: RelationFilterConfig<any>,
  referencedItems: Map<string, any>,
): { value: string; label: string }[] {
  const options: { value: string; label: string }[] = []
  const usedIds = new Set<any>()

  for (const item of allItems) {
    usedIds.add(accessor(item))
  }

  for (const id of usedIds) {
    if (id === null || id === undefined) {
      options.push({ value: '', label: 'Empty' })
      continue
    }
    const ref = referencedItems.get(id)
    if (ref) {
      const label = String(ref[config.referencedModelLabelField] ?? ref[config.referencedModelIdField])
      options.push({ value: String(ref[config.referencedModelIdField]), label })
    }
  }

  return options
}

/**
 * Generates scalar options from item values.
 */
function getScalarOptions<T>(allItems: T[], accessor: (item: T) => any): { value: string; label: string }[] {
  const values = new Set<string>()
  for (const item of allItems) {
    const raw = accessor(item)
    const normalized = raw instanceof Date ? raw.toISOString() : String(raw ?? '')
    values.add(normalized)
  }

  const options: { value: string; label: string }[] = []
  for (const v of values) {
    options.push({ value: v, label: v === '' ? 'Empty' : v })
  }
  return options
}

/**
 * Formats and sorts the filter options.
 */
export function formatAndSortOptions(
  options: { value: string; label: string }[],
  matches: Set<string>,
): { value: string; label: string; hasMatches: boolean }[] {
  return options
    .map((opt) => ({
      ...opt,
      hasMatches: matches.has(opt.value),
    }))
    .sort((a, b) => {
      if (a.hasMatches && !b.hasMatches) {
        return -1
      }
      if (!a.hasMatches && b.hasMatches) {
        return 1
      }
      if (a.value === '' && b.value !== '') {
        return -1
      }
      if (a.value !== '' && b.value === '') {
        return 1
      }
      return a.label.localeCompare(b.label)
    })
}

// ============================================================================
// Sorting Utilities
// ============================================================================

/**
 * Compares two values for sorting purposes based on their value type.
 * Handles null/undefined values by placing them at the end (null < non-null).
 * @returns negative if a < b, positive if a > b, 0 if equal
 */
export function compareValues(a: unknown, b: unknown, valueType: FieldValueType): number {
  // Handle null/undefined - nulls sort to the end
  if (a == null && b == null) return 0
  if (a == null) return 1
  if (b == null) return -1

  switch (valueType) {
    case 'number':
      return Number(a) - Number(b)
    case 'date': {
      const dateA = a instanceof Date ? a : new Date(a as string)
      const dateB = b instanceof Date ? b : new Date(b as string)
      return dateA.getTime() - dateB.getTime()
    }
    case 'boolean':
      // false < true (false = 0, true = 1)
      return (a === true ? 1 : 0) - (b === true ? 1 : 0)
    case 'string':
    default:
      if (typeof a === 'object' || typeof b === 'object') return 0
      return String(a).localeCompare(String(b))
  }
}

type SortFieldConfig = {
  valueType: FieldValueType
  getLabel?: (value: unknown, refs?: Map<string, any>) => string
}

/**
 * Compares two items for a single sort field, handling label resolution and null values.
 * @returns negative if a < b, positive if a > b, 0 if equal
 */
export function compareFieldValues(
  aRaw: unknown,
  bRaw: unknown,
  direction: 'asc' | 'desc',
  config: SortFieldConfig,
  referencedItems?: Map<string, any>,
): number {
  const aValue = config.getLabel ? config.getLabel(aRaw, referencedItems) : aRaw
  const bValue = config.getLabel ? config.getLabel(bRaw, referencedItems) : bRaw

  const aIsNull = aValue == null || aValue === ''
  const bIsNull = bValue == null || bValue === ''
  if (aIsNull && bIsNull) return 0
  if (aIsNull) return 1
  if (bIsNull) return -1

  const comparison = compareValues(aValue, bValue, config.valueType)
  return direction === 'asc' ? comparison : -comparison
}

// ============================================================================
// Pagination Utilities
// ============================================================================

const LIMIT_ALL = -1

/**
 * Paginates an array of data.
 * @param data - The full array to paginate
 * @param cursor - Page number (1-indexed)
 * @param limit - Items per page. Use -1 (LIMIT_ALL) to return all items.
 * @returns Paginated result with data slice and metadata
 */
export function paginate<T>(data: T[], cursor: number, limit: number): PaginatedResult<T> {
  if (data.length === 0) {
    return {
      data: [],
      total: 0,
      perPage: limit,
      currentPage: cursor,
      cursor: null,
    }
  }

  const paginatedData = limit === LIMIT_ALL ? data : data.slice((cursor - 1) * limit, cursor * limit)
  return {
    data: paginatedData,
    total: data.length,
    perPage: limit,
    currentPage: cursor,
    cursor: cursor * limit < data.length ? cursor + 1 : null,
  }
}
