
/**
 * Performance Optimization Utilities
 * Handles caching, memoization, and performance monitoring
 */

import { unstable_cache } from 'next/cache';

// Request deduplication map
const pendingRequests = new Map<string, Promise<any>>();

/**
 * Deduplicate concurrent requests with the same key
 */
export function dedupeRequest<T>(
  key: string,
  fetchFn: () => Promise<T>
): Promise<T> {
  if (pendingRequests.has(key)) {
    return pendingRequests.get(key)!;
  }

  const promise = fetchFn()
    .finally(() => {
      pendingRequests.delete(key);
    });

  pendingRequests.set(key, promise);
  return promise;
}

/**
 * Simple in-memory cache with TTL
 */
class CacheStore<T> {
  private cache = new Map<string, { value: T; expires: number }>();

  set(key: string, value: T, ttl: number = 5 * 60 * 1000) {
    this.cache.set(key, {
      value,
      expires: Date.now() + ttl,
    });
  }

  get(key: string): T | null {
    const entry = this.cache.get(key);
    if (!entry) return null;

    if (Date.now() > entry.expires) {
      this.cache.delete(key);
      return null;
    }

    return entry.value;
  }

  has(key: string): boolean {
    return this.get(key) !== null;
  }

  clear() {
    this.cache.clear();
  }

  // Clean up expired entries
  cleanup() {
    const now = Date.now();
    for (const [key, entry] of this.cache.entries()) {
      if (now > entry.expires) {
        this.cache.delete(key);
      }
    }
  }
}

// Global cache instance
const cache = new CacheStore();

// Run cleanup every 5 minutes
setInterval(() => cache.cleanup(), 5 * 60 * 1000);

export { cache };

/**
 * Memoize async functions with cache
 */
export function memoize<T extends (...args: any[]) => Promise<any>>(
  fn: T,
  keyGenerator?: (...args: Parameters<T>) => string,
  ttl: number = 5 * 60 * 1000
): T {
  return (async (...args: Parameters<T>) => {
    const key = keyGenerator
      ? keyGenerator(...args)
      : `${fn.name}_${JSON.stringify(args)}`;

    // Check cache first
    const cached = cache.get(key);
    if (cached !== null) {
      return cached;
    }

    // Dedupe concurrent requests
    const result = await dedupeRequest(key, () => fn(...args));
    
    // Store in cache
    cache.set(key, result, ttl);
    
    return result;
  }) as T;
}

/**
 * Batch multiple requests into a single call
 */
export class RequestBatcher<K, V> {
  private queue: Array<{ key: K; resolve: (value: V) => void; reject: (error: any) => void }> = [];
  private timer: NodeJS.Timeout | null = null;
  private batchFn: (keys: K[]) => Promise<Map<K, V>>;
  private wait: number;

  constructor(batchFn: (keys: K[]) => Promise<Map<K, V>>, wait: number = 10) {
    this.batchFn = batchFn;
    this.wait = wait;
  }

  async load(key: K): Promise<V> {
    return new Promise<V>((resolve, reject) => {
      this.queue.push({ key, resolve, reject });
      
      if (!this.timer) {
        this.timer = setTimeout(() => this.flush(), this.wait);
      }
    });
  }

  private async flush() {
    const queue = this.queue;
    this.queue = [];
    this.timer = null;

    if (queue.length === 0) return;

    try {
      const keys = queue.map(item => item.key);
      const results = await this.batchFn(keys);

      queue.forEach(({ key, resolve, reject }) => {
        const result = results.get(key);
        if (result !== undefined) {
          resolve(result);
        } else {
          reject(new Error(`No result for key: ${key}`));
        }
      });
    } catch (error) {
      queue.forEach(({ reject }) => reject(error));
    }
  }
}

/**
 * Performance monitoring
 */
export class PerformanceMonitor {
  private metrics: Map<string, number[]> = new Map();

  start(label: string): () => void {
    const start = performance.now();
    
    return () => {
      const duration = performance.now() - start;
      
      if (!this.metrics.has(label)) {
        this.metrics.set(label, []);
      }
      
      this.metrics.get(label)!.push(duration);
    };
  }

  getMetrics(label: string) {
    const durations = this.metrics.get(label) || [];
    if (durations.length === 0) {
      return { count: 0, avg: 0, min: 0, max: 0 };
    }

    const sum = durations.reduce((a, b) => a + b, 0);
    return {
      count: durations.length,
      avg: sum / durations.length,
      min: Math.min(...durations),
      max: Math.max(...durations),
    };
  }

  getAllMetrics() {
    const result: Record<string, any> = {};
    for (const [label, _] of this.metrics) {
      result[label] = this.getMetrics(label);
    }
    return result;
  }

  clear() {
    this.metrics.clear();
  }
}

// Global performance monitor
export const perfMonitor = new PerformanceMonitor();

/**
 * Retry failed requests with exponential backoff
 */
export async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  let lastError: any;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      if (attempt < maxRetries - 1) {
        const delay = baseDelay * Math.pow(2, attempt);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  
  throw lastError;
}

/**
 * Debounce function calls
 */
export function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: NodeJS.Timeout | null = null;
  
  return function executedFunction(...args: Parameters<T>) {
    const later = () => {
      timeout = null;
      func(...args);
    };
    
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

/**
 * Throttle function calls
 */
export function throttle<T extends (...args: any[]) => any>(
  func: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle: boolean = false;
  
  return function executedFunction(...args: Parameters<T>) {
    if (!inThrottle) {
      func(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

/**
 * Create a cached fetch function using Next.js caching
 */
export function createCachedFetch<T>(
  keyPrefix: string,
  fetchFn: () => Promise<T>,
  revalidate: number = 60
) {
  return unstable_cache(
    fetchFn,
    [keyPrefix],
    {
      revalidate,
      tags: [keyPrefix],
    }
  );
}
