/**
 * Filtering utilities for data manipulation
 */

import { FilterConfig, FilterOperator, DataType } from './types';

/**
 * Get nested property value from object
 * @param obj - Object to get value from
 * @param path - Property path (supports dot notation)
 */
function getNestedValue(obj: any, path: string | keyof any): any {
  if (typeof path === 'string' && path.includes('.')) {
    return path.split('.').reduce((current, key) => current?.[key], obj);
  }
  return obj?.[path];
}

/**
 * Convert value to appropriate type for comparison
 * @param value - Value to convert
 * @param dataType - Target data type
 */
function convertFilterValue(value: any, dataType: DataType): any {
  if (value == null) return value;

  switch (dataType) {
    case 'string':
      return String(value);
    case 'number':
      const num = Number(value);
      return isNaN(num) ? 0 : num;
    case 'date':
      if (value instanceof Date) return value;
      const date = new Date(value);
      return isNaN(date.getTime()) ? new Date(0) : date;
    case 'boolean':
      return Boolean(value);
    default:
      return value;
  }
}

/**
 * Check if value matches filter condition
 * @param itemValue - Value from the data item
 * @param filterValue - Value to compare against
 * @param operator - Filter operator
 * @param dataType - Data type for proper comparison
 */
function matchesFilter(
  itemValue: any,
  filterValue: any,
  operator: FilterOperator,
  dataType: DataType = 'string'
): boolean {
  // Handle empty values
  if (operator === 'is_empty') {
    return itemValue == null || itemValue === '' || (Array.isArray(itemValue) && itemValue.length === 0);
  }
  
  if (operator === 'is_not_empty') {
    return itemValue != null && itemValue !== '' && !(Array.isArray(itemValue) && itemValue.length === 0);
  }

  // Convert values for comparison
  const convertedItemValue = convertFilterValue(itemValue, dataType);
  
  // Handle array filter values (for 'in' and 'not_in')
  if (Array.isArray(filterValue)) {
    const convertedFilterValues = filterValue.map(v => convertFilterValue(v, dataType));
    
    switch (operator) {
      case 'in':
        return convertedFilterValues.some(fv => 
          dataType === 'string' 
            ? convertedItemValue?.toLowerCase() === fv?.toLowerCase()
            : convertedItemValue === fv
        );
      case 'not_in':
        return !convertedFilterValues.some(fv => 
          dataType === 'string' 
            ? convertedItemValue?.toLowerCase() === fv?.toLowerCase()
            : convertedItemValue === fv
        );
    }
  }

  const convertedFilterValue = convertFilterValue(filterValue, dataType);

  // Handle between operator (expects array with 2 values)
  if (operator === 'between') {
    if (!Array.isArray(filterValue) || filterValue.length !== 2) return false;
    const [min, max] = filterValue.map(v => convertFilterValue(v, dataType));
    return convertedItemValue >= min && convertedItemValue <= max;
  }

  // Handle null/undefined item values for operators that don't handle empty values
  if (convertedItemValue == null) {
    return operator === 'not_equals';
  }

  switch (operator) {
    case 'equals':
      return dataType === 'string'
        ? convertedItemValue?.toLowerCase() === convertedFilterValue?.toLowerCase()
        : convertedItemValue === convertedFilterValue;
    
    case 'not_equals':
      return dataType === 'string'
        ? convertedItemValue?.toLowerCase() !== convertedFilterValue?.toLowerCase()
        : convertedItemValue !== convertedFilterValue;
    
    case 'contains':
      if (dataType !== 'string') return false;
      return convertedItemValue?.toLowerCase()?.includes(convertedFilterValue?.toLowerCase()) || false;
    
    case 'not_contains':
      if (dataType !== 'string') return true;
      return !convertedItemValue?.toLowerCase()?.includes(convertedFilterValue?.toLowerCase());
    
    case 'starts_with':
      if (dataType !== 'string') return false;
      return convertedItemValue?.toLowerCase()?.startsWith(convertedFilterValue?.toLowerCase()) || false;
    
    case 'ends_with':
      if (dataType !== 'string') return false;
      return convertedItemValue?.toLowerCase()?.endsWith(convertedFilterValue?.toLowerCase()) || false;
    
    case 'greater_than':
      return convertedItemValue > convertedFilterValue;
    
    case 'greater_than_equal':
      return convertedItemValue >= convertedFilterValue;
    
    case 'less_than':
      return convertedItemValue < convertedFilterValue;
    
    case 'less_than_equal':
      return convertedItemValue <= convertedFilterValue;
    
    default:
      return false;
  }
}

/**
 * Filter array by single property
 * @param data - Array to filter
 * @param config - Filter configuration
 */
export function filterBy<T>(data: T[], config: FilterConfig<T>): T[] {
  if (!Array.isArray(data) || data.length === 0) return data;

  return data.filter(item => {
    const itemValue = getNestedValue(item, config.key);
    return matchesFilter(
      itemValue,
      config.value,
      config.operator,
      config.dataType || 'string'
    );
  });
}

/**
 * Filter array by multiple conditions (AND logic)
 * @param data - Array to filter
 * @param configs - Array of filter configurations
 */
export function filterByMultiple<T>(data: T[], configs: FilterConfig<T>[]): T[] {
  if (!Array.isArray(data) || data.length === 0) return data;
  if (!configs || configs.length === 0) return data;

  return data.filter(item => {
    return configs.every(config => {
      const itemValue = getNestedValue(item, config.key);
      return matchesFilter(
        itemValue,
        config.value,
        config.operator,
        config.dataType || 'string'
      );
    });
  });
}

/**
 * Filter array by multiple conditions with OR logic
 * @param data - Array to filter
 * @param configs - Array of filter configurations
 */
export function filterByAny<T>(data: T[], configs: FilterConfig<T>[]): T[] {
  if (!Array.isArray(data) || data.length === 0) return data;
  if (!configs || configs.length === 0) return data;

  return data.filter(item => {
    return configs.some(config => {
      const itemValue = getNestedValue(item, config.key);
      return matchesFilter(
        itemValue,
        config.value,
        config.operator,
        config.dataType || 'string'
      );
    });
  });
}

/**
 * Search filter - searches across multiple properties
 * @param data - Array to search
 * @param searchTerm - Search term
 * @param searchKeys - Properties to search in
 * @param caseSensitive - Whether search is case sensitive
 */
export function searchFilter<T>(
  data: T[],
  searchTerm: string,
  searchKeys: (keyof T)[],
  caseSensitive: boolean = false
): T[] {
  if (!Array.isArray(data) || data.length === 0) return data;
  if (!searchTerm || searchTerm.trim() === '') return data;
  if (!searchKeys || searchKeys.length === 0) return data;

  const term = caseSensitive ? searchTerm : searchTerm.toLowerCase();

  return data.filter(item => {
    return searchKeys.some(key => {
      const value = getNestedValue(item, key);
      if (value == null) return false;
      
      const stringValue = String(value);
      const searchValue = caseSensitive ? stringValue : stringValue.toLowerCase();
      return searchValue.includes(term);
    });
  });
}

/**
 * Advanced search with highlighting matches
 * @param data - Array to search
 * @param searchTerm - Search term
 * @param searchKeys - Properties to search in
 * @param caseSensitive - Whether search is case sensitive
 */
export function searchWithHighlight<T extends Record<string, any>>(
  data: T[],
  searchTerm: string,
  searchKeys: (keyof T)[],
  caseSensitive: boolean = false
): Array<T & { _searchMatches: { [key: string]: string } }> {
  if (!Array.isArray(data) || data.length === 0) return [];
  if (!searchTerm || searchTerm.trim() === '') {
    return data.map(item => ({ ...item, _searchMatches: {} }));
  }
  if (!searchKeys || searchKeys.length === 0) return [];

  const term = caseSensitive ? searchTerm : searchTerm.toLowerCase();
  const highlightStartTag = '<mark>';
  const highlightEndTag = '</mark>';

  return data.filter(item => {
    const matches: { [key: string]: string } = {};
    let hasMatch = false;

    searchKeys.forEach(key => {
      const value = getNestedValue(item, key);
      if (value == null) return;
      
      const stringValue = String(value);
      const searchValue = caseSensitive ? stringValue : stringValue.toLowerCase();
      
      if (searchValue.includes(term)) {
        hasMatch = true;
        // Create highlighted version
        const regex = new RegExp(
          caseSensitive ? searchTerm : searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 
          caseSensitive ? 'g' : 'gi'
        );
        matches[key as string] = stringValue.replace(regex, `${highlightStartTag}$&${highlightEndTag}`);
      }
    });

    if (hasMatch) {
      return Object.assign(item, { _searchMatches: matches });
    }
    return false;
  }).map(item => item as T & { _searchMatches: { [key: string]: string } });
}

/**
 * Create a filter predicate function
 * @param config - Filter configuration
 */
export function createFilterPredicate<T>(config: FilterConfig<T>) {
  return (item: T): boolean => {
    const itemValue = getNestedValue(item, config.key);
    return matchesFilter(
      itemValue,
      config.value,
      config.operator,
      config.dataType || 'string'
    );
  };
}

/**
 * Get unique values from a property across all items
 * @param data - Array of data
 * @param key - Property key
 */
export function getUniqueValues<T>(data: T[], key: keyof T): any[] {
  if (!Array.isArray(data) || data.length === 0) return [];

  const values = new Set();
  data.forEach(item => {
    const value = getNestedValue(item, key);
    if (value != null) {
      values.add(value);
    }
  });

  return Array.from(values).sort();
}

/**
 * Get filter suggestions based on current data and partial input
 * @param data - Array of data
 * @param key - Property key
 * @param partialValue - Partial value to match
 * @param maxSuggestions - Maximum number of suggestions
 */
export function getFilterSuggestions<T>(
  data: T[],
  key: keyof T,
  partialValue: string,
  maxSuggestions: number = 10
): string[] {
  if (!Array.isArray(data) || data.length === 0) return [];
  if (!partialValue || partialValue.trim() === '') return [];

  const term = partialValue.toLowerCase();
  const suggestions = new Set<string>();

  data.forEach(item => {
    const value = getNestedValue(item, key);
    if (value != null) {
      const stringValue = String(value);
      if (stringValue.toLowerCase().includes(term)) {
        suggestions.add(stringValue);
      }
    }
  });

  return Array.from(suggestions)
    .sort()
    .slice(0, maxSuggestions);
}