/**
 * Data chunking utilities for handling large datasets efficiently
 */

export interface ChunkConfig {
  chunkSize: number;
  maxConcurrentChunks: number;
  retryAttempts: number;
  retryDelay: number;
}

export interface ChunkResult<T> {
  data: T[];
  page: number;
  totalPages: number;
  hasMore: boolean;
  chunkId: string;
}

export interface DataChunk<T> {
  id: string;
  data: T[];
  page: number;
  timestamp: number;
  size: number;
}

export type FilterValue = string | number | boolean | undefined | null;
export type Filters = Record<string, FilterValue>;
export type FetchFunction = (url: string, signal?: AbortSignal) => Promise<Response>;

/**
 * Default chunking configuration
 */
export const defaultChunkConfig: ChunkConfig = {
  chunkSize: 20,
  maxConcurrentChunks: 3,
  retryAttempts: 3,
  retryDelay: 1000,
};

/**
 * Data Chunking Manager for handling large datasets
 */
export class DataChunkManager<T = unknown> {
  private chunks = new Map<string, DataChunk<T>>();
  private loadingChunks = new Set<string>();
  private config: ChunkConfig;
  private abortControllers = new Map<string, AbortController>();

  constructor(config: Partial<ChunkConfig> = {}) {
    this.config = { ...defaultChunkConfig, ...config };
  }

  /**
   * Generate unique chunk ID
   */
  private generateChunkId(endpoint: string, page: number, filters: Filters = {}): string {
    const filterStr = Object.keys(filters)
      .sort()
      .map(key => `${key}:${filters[key]}`)
      .join('|');
    return `chunk_${endpoint}_page${page}_${filterStr}`.replace(/[^a-zA-Z0-9_]/g, '_');
  }

  /**
   * Load a chunk of data
   */
  async loadChunk(
    endpoint: string,
    page: number,
    filters: Filters = {},
    fetchFn?: FetchFunction
  ): Promise<ChunkResult<T>> {
    const chunkId = this.generateChunkId(endpoint, page, filters);

    // Return cached chunk if available and recent
    const existingChunk = this.chunks.get(chunkId);
    if (existingChunk && Date.now() - existingChunk.timestamp < 5 * 60 * 1000) {
      return {
        data: existingChunk.data,
        page: existingChunk.page,
        totalPages: Math.ceil(1000 / this.config.chunkSize), // Estimate
        hasMore: existingChunk.page * this.config.chunkSize < 1000,
        chunkId,
      };
    }

    // Prevent concurrent loading of same chunk
    if (this.loadingChunks.has(chunkId)) {
      return this.waitForChunk(chunkId);
    }

    this.loadingChunks.add(chunkId);

    try {
      // Create abort controller for this request
      const abortController = new AbortController();
      this.abortControllers.set(chunkId, abortController);

      // Build URL with pagination and filters
      const url = this.buildUrl(endpoint, page, filters);
      
      // Use custom fetch function or default fetch
      const response = fetchFn 
        ? await fetchFn(url, abortController.signal)
        : await fetch(url, { signal: abortController.signal });

      if (!response.ok) {
        throw new Error(`Failed to load chunk: ${response.statusText}`);
      }

      const result = await response.json();
      const data = Array.isArray(result) ? result : result.data || [];

      // Create and cache chunk
      const chunk: DataChunk<T> = {
        id: chunkId,
        data,
        page,
        timestamp: Date.now(),
        size: JSON.stringify(data).length,
      };

      this.chunks.set(chunkId, chunk);

      // Cleanup old chunks if we have too many
      this.cleanupOldChunks();

      return {
        data,
        page,
        totalPages: result.totalPages || Math.ceil((result.total || 1000) / this.config.chunkSize),
        hasMore: result.hasMore ?? (data.length === this.config.chunkSize),
        chunkId,
      };

    } catch (error) {
      console.error(`Failed to load chunk ${chunkId}:`, error);
      throw error;
    } finally {
      this.loadingChunks.delete(chunkId);
      this.abortControllers.delete(chunkId);
    }
  }

  /**
   * Load multiple chunks concurrently
   */
  async loadChunks(
    endpoint: string,
    pages: number[],
    filters: Filters = {},
    fetchFn?: FetchFunction
  ): Promise<ChunkResult<T>[]> {
    const chunkPromises = pages.map(page => 
      this.loadChunk(endpoint, page, filters, fetchFn)
    );

    // Limit concurrent requests
    const results: ChunkResult<T>[] = [];
    for (let i = 0; i < chunkPromises.length; i += this.config.maxConcurrentChunks) {
      const batch = chunkPromises.slice(i, i + this.config.maxConcurrentChunks);
      const batchResults = await Promise.allSettled(batch);
      
      batchResults.forEach(result => {
        if (result.status === 'fulfilled') {
          results.push(result.value);
        } else {
          console.error('Chunk loading failed:', result.reason);
        }
      });
    }

    return results;
  }

  /**
   * Preload next chunks for smoother scrolling
   */
  async preloadNextChunks(
    endpoint: string,
    currentPage: number,
    filters: Filters = {},
    count: number = 2,
    fetchFn?: FetchFunction
  ): Promise<void> {
    const nextPages = Array.from({ length: count }, (_, i) => currentPage + i + 1);
    
    // Don't await - preload in background
    this.loadChunks(endpoint, nextPages, filters, fetchFn)
      .catch(error => console.warn('Preload failed:', error));
  }

  /**
   * Get all loaded data in order
   */
  getAllLoadedData(endpoint: string): T[] {
    const allData: T[] = [];
    const chunks = Array.from(this.chunks.values())
      .filter(chunk => chunk.id.includes(endpoint))
      .sort((a, b) => a.page - b.page);

    chunks.forEach(chunk => {
      allData.push(...chunk.data);
    });

    return allData;
  }

  /**
   * Clear chunks for specific endpoint
   */
  clearChunks(endpoint?: string): void {
    if (endpoint) {
      const toDelete = Array.from(this.chunks.keys())
        .filter(key => key.includes(endpoint));
      toDelete.forEach(key => this.chunks.delete(key));
    } else {
      this.chunks.clear();
    }
  }

  /**
   * Get chunking statistics
   */
  getStats(): {
    totalChunks: number;
    totalSize: number;
    averageChunkSize: number;
    loadingChunks: number;
    oldestChunk: number;
  } {
    const chunks = Array.from(this.chunks.values());
    const totalSize = chunks.reduce((sum, chunk) => sum + chunk.size, 0);
    const oldestTimestamp = Math.min(...chunks.map(chunk => chunk.timestamp));

    return {
      totalChunks: chunks.length,
      totalSize,
      averageChunkSize: chunks.length > 0 ? totalSize / chunks.length : 0,
      loadingChunks: this.loadingChunks.size,
      oldestChunk: Date.now() - oldestTimestamp,
    };
  }

  /**
   * Cancel all pending requests
   */
  cancelAllRequests(): void {
    this.abortControllers.forEach(controller => controller.abort());
    this.abortControllers.clear();
    this.loadingChunks.clear();
  }

  /**
   * Build URL with pagination and filters
   */
  private buildUrl(endpoint: string, page: number, filters: Filters): string {
    const url = new URL(endpoint, window.location.origin);
    url.searchParams.set('page', page.toString());
    url.searchParams.set('limit', this.config.chunkSize.toString());

    Object.entries(filters).forEach(([key, value]) => {
      if (value !== undefined && value !== null && value !== '') {
        url.searchParams.set(key, value.toString());
      }
    });

    return url.toString();
  }

  /**
   * Wait for a chunk that's currently loading
   */
  private async waitForChunk(chunkId: string): Promise<ChunkResult<T>> {
    let attempts = 0;
    const maxAttempts = 50; // 5 seconds max wait

    while (this.loadingChunks.has(chunkId) && attempts < maxAttempts) {
      await new Promise(resolve => setTimeout(resolve, 100));
      attempts++;
    }

    const chunk = this.chunks.get(chunkId);
    if (chunk) {
      return {
        data: chunk.data,
        page: chunk.page,
        totalPages: Math.ceil(1000 / this.config.chunkSize),
        hasMore: chunk.page * this.config.chunkSize < 1000,
        chunkId,
      };
    }

    throw new Error(`Failed to load chunk ${chunkId}`);
  }

  /**
   * Cleanup old chunks to prevent memory issues
   */
  private cleanupOldChunks(): void {
    const maxChunks = 50; // Keep maximum 50 chunks
    const maxAge = 15 * 60 * 1000; // 15 minutes

    if (this.chunks.size <= maxChunks) return;

    const now = Date.now();
    const chunksToDelete: string[] = [];

    // Remove old chunks first
    this.chunks.forEach((chunk, id) => {
      if (now - chunk.timestamp > maxAge) {
        chunksToDelete.push(id);
      }
    });

    // If still too many, remove oldest ones
    if (this.chunks.size - chunksToDelete.length > maxChunks) {
      const sortedChunks = Array.from(this.chunks.entries())
        .sort(([, a], [, b]) => a.timestamp - b.timestamp);
      
      const additionalToDelete = sortedChunks
        .slice(0, this.chunks.size - maxChunks)
        .map(([id]) => id);
      
      chunksToDelete.push(...additionalToDelete);
    }

    chunksToDelete.forEach(id => this.chunks.delete(id));

    if (chunksToDelete.length > 0) {
      console.log(`Cleaned up ${chunksToDelete.length} old chunks`);
    }
  }
}

/**
 * Global chunk manager instance
 */
export const dataChunkManager = new DataChunkManager();

/**
 * Hook for using chunked data loading
 */
export function useChunkedData<T = unknown>(
  endpoint: string,
  filters: Filters = {},
  config: Partial<ChunkConfig> = {}
) {
  const manager = new DataChunkManager<T>(config);
  
  return {
    loadChunk: (page: number) => manager.loadChunk(endpoint, page, filters),
    loadChunks: (pages: number[]) => manager.loadChunks(endpoint, pages, filters),
    preloadNext: (currentPage: number, count?: number) => 
      manager.preloadNextChunks(endpoint, currentPage, filters, count),
    getAllData: () => manager.getAllLoadedData(endpoint),
    clearChunks: () => manager.clearChunks(endpoint),
    getStats: () => manager.getStats(),
    cancelRequests: () => manager.cancelAllRequests(),
  };
}

export default DataChunkManager;