// React
import React, { useCallback, useState } from 'react';

// Types
import {
  ActionResponse,
  Column,
  NewSort,
  PageSize,
  RefreshProps,
  Search,
  Sort,
  TableExportType,
  UseTablePagination
} from '../types';

/**
 * Hook for managing table pagination, sorting, searching, and data manipulation
 * 
 * This hook provides a comprehensive solution for handling table data operations including:
 * - Pagination
 * - Sorting (single and multiple column)
 * - Searching/filtering
 * - Data export
 * - Local and server-side data handling
 * 
 * @param {Object} config - Configuration object for the hook
 * @param {Function} config.dispatch - Redux dispatch function for server-side operations
 * @param {Object} config.action - Redux action for fetching data
 * @param {string[]} config.searchFields - Fields to search in when performing text search
 * @param {boolean} [config.isLocalData=false] - Whether to handle data locally or server-side
 * @param {React.RefObject} config.tableRef - Reference to the table component
 * @param {Array} config.termsFields - Additional search terms to apply
 * @param {Function} config.sendGenericExport - Function to handle data export
 * @param {string} config.exportName - Name to use for exported files
 * 
 * @returns {Object} Table management utilities and state
 * @returns {Function} returns.onChangeRowsPerPage - Handler for changing page size
 * @returns {Function} returns.onSearchChange - Handler for search input changes
 * @returns {Function} returns.onChangeData - Handler for data changes
 * @returns {Function} returns.onChangeColumns - Handler for column configuration changes
 * @returns {Function} returns.onChangeSort - Handler for sort changes
 * @returns {Function} returns.onExport - Handler for data export
 * @returns {Function} returns.refresh - Function to refresh table data
 * @returns {Function} returns.onChangePage - Handler for page changes
 * @returns {Function} returns.addConditions - Function to add search conditions
 * @returns {Object} returns.state - Current table state (columns, sort, data, etc.)
 */

/**
 * Sorts an array of objects based on multiple criteria
 * @param {Array} data - Array of objects to sort
 * @param {Array} sorts - Array of sort configurations
 * @param {Array} [columns=[]] - Column definitions for custom sorting
 * @returns {Array} Sorted array
 * 
 * @example
 * const data = [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }];
 * const sorts = [{ key: 'age', order: 'asc' }];
 * const sorted = multipleSort(data, sorts);
 */
const multipleSort = (data: any[], sorts: Sort[], columns: Column[] = []) => {
  /**
   * Sorts an array of objects based on multiple criteria.
   * - Supports ascending and descending orders.
   * - Allows custom sorting logic via column definitions.
   */
  return [...data].sort((a, b) => {
    let i = 0,
      result = 0;

    while (i < sorts.length && result === 0) {
      // Find column definition for the current sort key.
      let column = columns?.find((column) => column?.key === sorts[i]?.key);

      // Determine sorting order (1 for ascending, -1 for descending).
      let order = sorts[i].order === 'asc' ? 1 : -1;

      // Get comparison values, using column's sortValue function if defined.
      let valueA = column?.sortValue
        ? getClearValue(column.sortValue(a))
        : getClearValue(a[sorts[i].key]);
      let valueB = column?.sortValue
        ? getClearValue(column.sortValue(b))
        : getClearValue(b[sorts[i].key]);

      // Compare values and multiply by order to respect ascending/descending direction.
      result = order * (valueA < valueB ? -1 : valueA > valueB ? 1 : 0);
      i++;
    }

    return result;
  });
};

/**
 * Normalizes and cleans input values for comparison
 * @param {string|number|undefined} value - Value to clean
 * @returns {string|number} Cleaned value
 * 
 * @example
 * getClearValue('Hello') // returns 'hello'
 * getClearValue(42) // returns 42
 * getClearValue(undefined) // returns ''
 */
const getClearValue = (value: string | number | undefined) => {
  /**
   * Cleans and normalizes input values:
   * - Converts undefined, null, or empty values to an empty string.
   * - Converts strings to lowercase.
   * - Ignores arrays and objects.
   * - Returns numbers as-is.
   */
  if (!value) return ''; // Return an empty string for falsy values.
  if (typeof value == 'undefined') return '';
  if (Array.isArray(value) || typeof value === 'object') return ''; // Ignore arrays and objects.
  if (typeof value !== 'number') return value.toLowerCase();
  return value;
};

/**
 * Filters data based on search text, searching through all object properties
 * @param {Array} data - Array of objects to search through
 * @param {string} searchText - Text to search for
 * @returns {Array} Filtered array of objects
 * 
 * @example
 * const data = [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }];
 * const filtered = filterDataBySearch(data, 'john'); // returns [{ name: 'John', age: 30 }]
 */
const filterDataBySearch = (data: any[], searchText: string) => {
  /**
   * Filters an array of objects by searching for a text string in object properties.
   * - Recursively searches nested objects.
   * - Case-insensitive matching.
   */
  function recursiveSearch(obj: any, text: string) {
    for (const prop in obj) {
      if (obj.hasOwnProperty(prop)) {
        // Check if the property is a string and contains the search text.
        if (typeof obj[prop] === 'string' && obj[prop].toLowerCase().includes(text)) {
          return true;
        }
        // If the property is an object, search it recursively.
        else if (typeof obj[prop] === 'object') {
          if (recursiveSearch(obj[prop], text)) return true;
        }
      }
    }
    return false; // Return false if no match is found.
  }

  if (!searchText || searchText.trim() === '') return data; // Return original data if search text is empty.
  const lowerSearchText = searchText.toLowerCase(); // Normalize search text to lowercase.
  return data.filter((item: any) => recursiveSearch(item, lowerSearchText)); // Filter data using recursive search.
};

/**
 * Main hook implementation for table management
 * 
 * @example
 * const {
 *   onChangePage,
 *   onChangeRowsPerPage,
 *   onSearchChange,
 *   data,
 *   total,
 *   page
 * } = useTablePagination({
 *   dispatch,
 *   action: fetchData,
 *   searchFields: ['name', 'email'],
 *   isLocalData: false,
 *   tableRef,
 *   termsFields: [],
 *   sendGenericExport,
 *   exportName: 'users'
 * });
 */
export const useTablePagination: UseTablePagination = ({
  dispatch,
  action,
  searchFields,
  isLocalData = false,
  tableRef,
  termsFields,
  sendGenericExport,
  exportName
}) => {
  // States
  const [sort, setSort] = useState<Sort | null>(null);
  const [searchText, setSearchText] = useState<string>('');
  const [pageSize, setPageSize] = useState<PageSize>(10);
  const [allData, setAllData] = useState<any[]>([]);
  const [filteredData, setFilteredData] = useState<any[]>([]);
  const [columns, setColumns] = useState<Column[]>([]);
  const [params, setParams] = useState({});
  const [total, setTotal] = useState<number>(0);
  const [data, setData] = useState<any[] | null>([]);
  const [page, setPage] = useState<number>(1);
  const [currentConditions, setCurrentConditions] = useState<any[]>([]);
  const [currentSort, setCurrentSort] = useState<NewSort | null>(null);
  const [externalConditions, setExternalConditions] = useState<any[]>([]);

  // Hooks
  const refresh = useCallback(
    ({ _page, _pageSize, _searchText, _parms, _sort = null, _data, _conditions }: RefreshProps) => {
      let newPage = _page || page;
      setPage(newPage);

      let newData = _data || allData;
      setAllData(newData);

      let newPageSize: number =
        _pageSize === 'ALL' ? newData.length : _pageSize || (pageSize as number);
      setPageSize(newPageSize);

      let newSort = _sort || sort;
      setSort(newSort);

      let newSearchText: string = _searchText != undefined ? _searchText : searchText;
      setSearchText(newSearchText);

      let newParams = { ...params, ..._parms };
      setParams(newParams);

      // ** Conditions
      const conditions: (Search | any)[] =
        searchFields && newSearchText
          ? [
              {
                fields: searchFields,
                terms: [newSearchText]
              }
            ]
          : [];

      if (termsFields?.length) {
        termsFields.map((item) => {
          conditions.push(item);
        });
      }

      let newExternalConditions = _conditions || externalConditions;
      setExternalConditions(newExternalConditions);
      if (_conditions?.length) {
        _conditions.map((item) => {
          conditions.push(item);
        });
      }

      if (!isLocalData) {
        setCurrentConditions(conditions);
        let formatSort: NewSort | null = null;
        if (newSort) {
          formatSort = {
            field: newSort?.key,
            order: newSort?.order === 'asc' ? 'ASC' : 'DESC'
          };
          setCurrentSort(formatSort);
        }
        if (!dispatch || !action) return;
        dispatch(
          action({
            limit: newPageSize,
            offset: (newPage - 1) * newPageSize,
            conditions,
            sort: formatSort || undefined
          }) as ActionResponse
        );
        return;
      }
      let filteredDataLocal = [];

      // ** Search
      const lowerSearchText = newSearchText.toLowerCase();
      filteredDataLocal = filterDataBySearch(newData, lowerSearchText);

      // ** Sort data
      let sorts = [];
      if (newSort) {
        sorts.push(newSort);
      }
      if (sorts && sorts.length > 0) {
        filteredDataLocal = multipleSort(filteredDataLocal || [], sorts || [], columns || []);
      }
      // ** Set Total
      setTotal(filteredDataLocal?.length || 0);
      setFilteredData(filteredDataLocal);
      // ** Pagination
      const startIndex = (newPage - 1) * newPageSize;
      const endIndex = startIndex + newPageSize;
      const paginatedData = filteredDataLocal.slice(startIndex, endIndex);
      setData(paginatedData);
    },
    [
      page,
      pageSize,
      searchText,
      params,
      sort,
      data,
      allData,
      searchFields,
      termsFields,
      externalConditions,
      isLocalData,
      columns
    ]
  );

  return {
    // ** Functions
    onChangeRowsPerPage: (_pageSize: number) => {
      refresh({ _page: 1, _pageSize });
    },
    onSearchChange: (value: any) => {
      setSearchText(value);
      if (window.typingTime) {
        clearTimeout(window.typingTime);
      }
      window.typingTime = setTimeout(() => {
        tableRef && tableRef.current?.setPage(1);
        refresh({ _page: 1, _searchText: value });
      }, 1000);
    },
    onChangeData: (_data: any, _sort?: Sort, _page?: number) => {
      refresh({ _page, _data, _sort });
    },
    onChangeColumns: (newColums: any) => {
      setColumns(newColums);
    },
    onChangeSort: (_sort: Sort) => {
      refresh({
        _sort
      });
    },
    onExport: (type: TableExportType) => {
      if (isLocalData || tableRef?.current?.selectedItems?.length) {
        if (type === 'Csv') {
          tableRef.current.exportCSV();
        } else {
          tableRef.current.exportPDF();
        }
        return;
      }
      if (!dispatch || !sendGenericExport || !action?.query || !exportName) return;

      // Apply same exportOnly logic as component exports
      const exportOnlyColumns = columns.filter(x => x.exportOnly === true && x.export);
      const visibleExportColumns = columns.filter(x => x.show && x.export);
      
      // Separate ID column from other export-only columns for proper ordering
      const idColumn = exportOnlyColumns.filter(x => x.key === 'id');
      const otherExportOnlyColumns = exportOnlyColumns.filter(x => x.key !== 'id');
      
      // Combine columns with deduplication (ID first, then visible, then other export-only)
      const exportColumns = [...idColumn, ...visibleExportColumns, ...otherExportOnlyColumns].filter((column, index, self) =>
        index === self.findIndex(col => col.key === column.key)
      );
      let headers = [];
      // Map columns to export headers based on export type
      if ( type === 'Csv' ) {
      // For CSV, include all exportable columns (including export-only) with their titles and keys
      const headersCSV = 
      exportColumns.map((x) => {
        return {
          header: x.title,
          column: x.key,
          functionString: x.functionString
          };
      }) || [];
        headers = headersCSV;
    } else {
      // For PDF, include only visible exportable columns (excluding export-only) with their titles and keys
      const headersPDF = 
        columns
          .filter((x) => x.show && x.export)
          .map((x) => {
        return {
          header: x.title,
          column: x.key,
          functionString: x.functionString
          };
      }) || [];
      headers = headersPDF;
    }
      dispatch(
        sendGenericExport(
          exportName,
          type,
          action.query,
          {
            conditions: currentConditions,
            sort: currentSort
          },
          headers,
          {
            unit: 'px',
            size: 'A4',
            orientation: 'landscape',
            fontSize: 10
          },
          null,
          null,
          true
        )
      );
    },
    refresh: (refreshProps: RefreshProps) => {
      refresh(refreshProps);
    },
    onChangePage: (_page: number) => {
      refresh({ _page });
    },
    addConditions: (_page: number, _conditions: any[]) => {
      refresh({ _page, _conditions });
    },
    // ** Params
    setColumns,
    setSort,
    setData,
    columns,
    defaultPage: page,
    rowsPerPage: pageSize,
    searchText,
    currentConditions,
    currentSort,
    tableRef,
    ref: tableRef,
    isLocalData,
    ...(isLocalData ? { data, total, allData, filteredData } : {}),
    exportName
  };
};
