/**
 * Data transformation utilities
 */

import { GroupByResult, AggregationFunction, AggregationResult } 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];
}

/**
 * Group array of objects by a specific property
 * @param data - Array to group
 * @param key - Property to group by
 */
export function groupBy<T>(data: T[], key: keyof T): GroupByResult<T>[] {
  if (!Array.isArray(data) || data.length === 0) return [];

  const groups = new Map<any, T[]>();

  data.forEach(item => {
    const groupKey = getNestedValue(item, key);
    if (!groups.has(groupKey)) {
      groups.set(groupKey, []);
    }
    groups.get(groupKey)!.push(item);
  });

  return Array.from(groups.entries()).map(([groupKey, items]) => ({
    key: groupKey,
    items,
    count: items.length
  }));
}

/**
 * Group array by multiple properties
 * @param data - Array to group
 * @param keys - Properties to group by
 */
export function groupByMultiple<T>(data: T[], keys: (keyof T)[]): GroupByResult<T>[] {
  if (!Array.isArray(data) || data.length === 0) return [];
  if (!keys || keys.length === 0) return [{ key: null, items: data, count: data.length }];

  const groups = new Map<string, T[]>();

  data.forEach(item => {
    const groupKey = keys.map(key => String(getNestedValue(item, key))).join('|');
    if (!groups.has(groupKey)) {
      groups.set(groupKey, []);
    }
    groups.get(groupKey)!.push(item);
  });

  return Array.from(groups.entries()).map(([groupKey, items]) => ({
    key: groupKey,
    items,
    count: items.length
  }));
}

/**
 * Aggregate values using specified functions
 * @param data - Array to aggregate
 * @param key - Property to aggregate
 * @param functions - Aggregation functions to apply
 */
export function aggregate<T>(
  data: T[],
  key: keyof T,
  functions: AggregationFunction[]
): AggregationResult {
  if (!Array.isArray(data) || data.length === 0) {
    return functions.reduce((acc, fn) => ({ ...acc, [fn]: 0 }), {});
  }

  const values = data
    .map(item => getNestedValue(item, key))
    .filter(value => value != null && !isNaN(Number(value)))
    .map(value => Number(value));

  if (values.length === 0) {
    return functions.reduce((acc, fn) => ({ ...acc, [fn]: 0 }), {});
  }

  const result: AggregationResult = {};

  functions.forEach(fn => {
    switch (fn) {
      case 'sum':
        result['sum'] = values.reduce((acc, val) => acc + val, 0);
        break;
      case 'avg':
        result['avg'] = values.reduce((acc, val) => acc + val, 0) / values.length;
        break;
      case 'min':
        result['min'] = Math.min(...values);
        break;
      case 'max':
        result['max'] = Math.max(...values);
        break;
      case 'count':
        result['count'] = values.length;
        break;
    }
  });

  return result;
}

/**
 * Group by and aggregate in one operation
 * @param data - Array to process
 * @param groupKey - Property to group by
 * @param aggregateKey - Property to aggregate
 * @param functions - Aggregation functions
 */
export function groupAndAggregate<T>(
  data: T[],
  groupKey: keyof T,
  aggregateKey: keyof T,
  functions: AggregationFunction[]
): Array<{ group: any; aggregates: AggregationResult }> {
  const groups = groupBy(data, groupKey);
  
  return groups.map(group => ({
    group: group.key,
    aggregates: aggregate(group.items, aggregateKey, functions)
  }));
}

/**
 * Extract specific properties from objects (pluck)
 * @param data - Array of objects
 * @param keys - Properties to extract
 */
export function pluck<T extends Record<string, any>, K extends keyof T>(data: T[], keys: K[]): Pick<T, K>[] {
  if (!Array.isArray(data) || data.length === 0) return [];
  if (!keys || keys.length === 0) return [];

  return data.map(item => {
    const result = {} as Pick<T, K>;
    keys.forEach(key => {
      if (key in item) {
        result[key] = item[key];
      }
    });
    return result;
  });
}

/**
 * Extract single property values into array
 * @param data - Array of objects
 * @param key - Property to extract
 */
export function pluckProperty<T, K extends keyof T>(data: T[], key: K): T[K][] {
  if (!Array.isArray(data) || data.length === 0) return [];
  
  return data.map(item => getNestedValue(item, key)).filter(value => value !== undefined);
}

/**
 * Flatten nested arrays
 * @param data - Array that may contain nested arrays
 * @param depth - Maximum depth to flatten (default: 1)
 */
export function flatten<T>(data: any[], depth: number = 1): T[] {
  if (!Array.isArray(data)) return [];
  
  const flattenRecursive = (arr: any[], currentDepth: number): any[] => {
    return currentDepth > 0 
      ? arr.reduce((acc, val) => 
          Array.isArray(val) 
            ? acc.concat(flattenRecursive(val, currentDepth - 1))
            : acc.concat(val), [])
      : arr.slice();
  };

  return flattenRecursive(data, depth);
}

/**
 * Flatten array completely (all levels)
 * @param data - Array to flatten
 */
export function flattenDeep<T>(data: any[]): T[] {
  if (!Array.isArray(data)) return [];
  
  return data.reduce((acc, val) => 
    Array.isArray(val) 
      ? acc.concat(flattenDeep(val))
      : acc.concat(val), []);
}

/**
 * Create pivot table from data
 * @param data - Array to pivot
 * @param rowKey - Property for rows
 * @param colKey - Property for columns  
 * @param valueKey - Property for values
 * @param aggregateFunction - How to aggregate values
 */
export function pivot<T>(
  data: T[],
  rowKey: keyof T,
  colKey: keyof T,
  valueKey: keyof T,
  aggregateFunction: AggregationFunction = 'sum'
): { [row: string]: { [col: string]: number } } {
  if (!Array.isArray(data) || data.length === 0) return {};

  const result: { [row: string]: { [col: string]: number } } = {};

  data.forEach(item => {
    const row = String(getNestedValue(item, rowKey));
    const col = String(getNestedValue(item, colKey));
    const value = Number(getNestedValue(item, valueKey)) || 0;

    if (!result[row]) {
      result[row] = {};
    }

    if (!result[row][col]) {
      result[row][col] = 0;
    }

    switch (aggregateFunction) {
      case 'sum':
        result[row][col] += value;
        break;
      case 'count':
        result[row][col] += 1;
        break;
      case 'avg':
        // For avg, we need to track sum and count separately
        // This is a simplified version that treats each occurrence as separate
        result[row][col] = (result[row][col] + value) / 2;
        break;
      case 'min':
        result[row][col] = Math.min(result[row][col], value);
        break;
      case 'max':
        result[row][col] = Math.max(result[row][col], value);
        break;
    }
  });

  return result;
}

/**
 * Transpose 2D array (flip rows and columns)
 * @param data - 2D array to transpose
 */
export function transpose<T>(data: T[][]): T[][] {
  if (!Array.isArray(data) || data.length === 0) return [];
  if (!Array.isArray(data[0])) return [];

  const rows = data.length;
  const cols = data[0].length;
  const result: T[][] = [];

  for (let col = 0; col < cols; col++) {
    result[col] = [];
    for (let row = 0; row < rows; row++) {
      result[col][row] = data[row][col];
    }
  }

  return result;
}

/**
 * Create frequency map of values
 * @param data - Array of values or objects
 * @param key - Property key (if data contains objects)
 */
export function frequency<T>(data: T[], key?: keyof T): Map<any, number> {
  if (!Array.isArray(data)) return new Map();

  const freqMap = new Map<any, number>();

  data.forEach(item => {
    const value = key ? getNestedValue(item, key) : item;
    freqMap.set(value, (freqMap.get(value) || 0) + 1);
  });

  return freqMap;
}

/**
 * Get unique values from array or object property
 * @param data - Array of data
 * @param key - Property key (optional)
 */
export function unique<T>(data: T[], key?: keyof T): any[] {
  if (!Array.isArray(data)) return [];

  const values = key 
    ? data.map(item => getNestedValue(item, key))
    : data;

  return [...new Set(values)];
}

/**
 * Chunk array into smaller arrays of specified size
 * @param data - Array to chunk
 * @param size - Size of each chunk
 */
export function chunk<T>(data: T[], size: number): T[][] {
  if (!Array.isArray(data) || size <= 0) return [];

  const chunks: T[][] = [];
  for (let i = 0; i < data.length; i += size) {
    chunks.push(data.slice(i, i + size));
  }
  return chunks;
}

/**
 * Zip multiple arrays together
 * @param arrays - Arrays to zip
 */
export function zip<T>(...arrays: T[][]): T[][] {
  if (arrays.length === 0) return [];
  
  const length = Math.max(...arrays.map(arr => Array.isArray(arr) ? arr.length : 0));
  const result: T[][] = [];

  for (let i = 0; i < length; i++) {
    result[i] = arrays.map(arr => arr[i]);
  }

  return result;
}

/**
 * Create cross product of multiple arrays
 * @param arrays - Arrays to get cross product from
 */
export function cartesianProduct<T>(...arrays: T[][]): T[][] {
  if (arrays.length === 0) return [];
  if (arrays.some(arr => !Array.isArray(arr) || arr.length === 0)) return [];

  return arrays.reduce(
    (acc, curr) => acc.flatMap(a => curr.map(b => [...a, b])),
    [[]] as T[][]
  );
}

/**
 * Sample random items from array
 * @param data - Array to sample from
 * @param count - Number of items to sample
 * @param withReplacement - Whether to allow duplicate selections
 */
export function sample<T>(data: T[], count: number, withReplacement: boolean = false): T[] {
  if (!Array.isArray(data) || data.length === 0 || count <= 0) return [];

  if (withReplacement) {
    return Array.from({ length: count }, () => 
      data[Math.floor(Math.random() * data.length)]
    );
  } else {
    const shuffled = [...data].sort(() => Math.random() - 0.5);
    return shuffled.slice(0, Math.min(count, data.length));
  }
}