/**
 * @file cache.ts
 * @description Multi-layer caching for Sanity Edge Fetcher
 * @author Invisible Cities Agency
 * @license MIT
 */

import { edgeSanityFetch, type EdgeSanityFetchOptions, type QueryParams } from './core';
import { Redis } from '@upstash/redis';

// Check if Upstash Redis is configured
const REDIS_URL = process.env.KV_REST_API_URL || process.env.UPSTASH_REDIS_REST_URL;
const REDIS_TOKEN = process.env.KV_REST_API_TOKEN || process.env.UPSTASH_REDIS_REST_TOKEN;
const REDIS_READ_ONLY_TOKEN = process.env.KV_REST_API_READ_ONLY_TOKEN;

const isRedisConfigured = !!(REDIS_URL && (REDIS_TOKEN || REDIS_READ_ONLY_TOKEN));

// Initialize Redis client if configured
let redis: Redis | null = null;
let redisWriter: Redis | null = null;

if (isRedisConfigured && REDIS_URL && (REDIS_TOKEN || REDIS_READ_ONLY_TOKEN)) {
  try {
    redis = new Redis({
      url: REDIS_URL,
      token: (REDIS_READ_ONLY_TOKEN || REDIS_TOKEN) as string,
      automaticDeserialization: true,
    });
    
    // Separate writer client if write token available
    if (REDIS_TOKEN) {
      redisWriter = new Redis({
        url: REDIS_URL,
        token: REDIS_TOKEN,
        automaticDeserialization: true,
      });
    } else {
      redisWriter = redis;
    }
  } catch {
    // Failed to initialize Redis client
    redis = null;
    redisWriter = null;
  }
}

interface CacheEntry<T> {
  value: T;
  timestamp: number;
  validUntil: number;
}

interface CachedFetchOptions extends EdgeSanityFetchOptions {
  /** Cache configuration */
  cache?: {
    /** Time to live in seconds */
    ttl?: number;
    /** Cache key prefix */
    prefix?: string;
    /** Force cache refresh */
    force?: boolean;
    /** Enable Redis caching if available */
    useRedis?: boolean;
    /** Enable Next.js cache */
    useNextCache?: boolean;
  };
}

/**
 * Generate cache key from query and params
 */
function generateCacheKey(
  dataset: string,
  query: string,
  params?: QueryParams
): string {
  const baseKey = `sanity:${dataset}:${query}`;
  if (!params || Object.keys(params).length === 0) {
    return baseKey;
  }
  
  // Sort params for consistent key generation
  const sortedParams = Object.keys(params)
    .sort()
    .map(key => `${key}=${JSON.stringify(params[key])}`)
    .join('&');
  
  return `${baseKey}:${sortedParams}`;
}

/**
 * In-memory LRU cache for edge runtime
 */
class MemoryCache {
  private cache = new Map<string, CacheEntry<unknown>>();
  private maxSize = 100;
  
  get<T>(key: string): T | null {
    const entry = this.cache.get(key);
    if (!entry) return null;
    
    if (Date.now() > entry.validUntil) {
      this.cache.delete(key);
      return null;
    }
    
    // Move to end (LRU)
    this.cache.delete(key);
    this.cache.set(key, entry);
    
    return entry.value as T;
  }
  
  set<T>(key: string, value: T, ttl: number): void {
    // Evict oldest if at capacity
    if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
      const firstKey = this.cache.keys().next().value;
      if (firstKey !== undefined) {
        this.cache.delete(firstKey);
      }
    }
    
    this.cache.set(key, {
      value,
      timestamp: Date.now(),
      validUntil: Date.now() + (ttl * 1000)
    });
  }
  
  delete(key: string): void {
    this.cache.delete(key);
  }
  
  clear(): void {
    this.cache.clear();
  }
  
  size(): number {
    return this.cache.size;
  }
}

// Global memory cache instance
const memoryCache = new MemoryCache();

/**
 * Fetches data from Sanity with multi-layer caching
 * 
 * Cache layers (in order):
 * 1. In-memory LRU cache (fastest, ~1ms)
 * 2. Upstash Redis (if configured, ~10-30ms)
 * 3. Origin fetch with Next.js cache
 */
export async function cachedSanityFetch<T>(
  options: CachedFetchOptions
): Promise<T> {
  const {
    dataset,
    query,
    params,
    cache = {}
  } = options;
  
  const {
    ttl = 60, // 1 minute default
    prefix = '',
    force = false,
    useRedis = true
  } = cache;
  
  const cacheKey = prefix + generateCacheKey(dataset, query, params);
  
  // Layer 1: Memory cache (unless force refresh)
  if (!force) {
    const memoryResult = memoryCache.get<T>(cacheKey);
    if (memoryResult !== null) {
      // Cache hit from memory
      return memoryResult;
    }
  }
  
  // Layer 2: Redis cache (if configured and enabled)
  if (!force && useRedis && redis) {
    try {
      const redisEntry = await redis.get<CacheEntry<T>>(cacheKey);
      if (redisEntry && Date.now() <= redisEntry.validUntil) {
        // Cache hit from Redis
        
        // Populate memory cache
        memoryCache.set(cacheKey, redisEntry.value, ttl);
        
        return redisEntry.value;
      }
    } catch {
      // Redis cache read error, continue to fetch from origin
      // Continue to fetch from origin
    }
  }
  
  // Layer 3: Fetch from origin
  // Cache miss, fetching from origin
  
  // Build safe options for edge fetch (remove cache, avoid undefined params)
  const { cache: _cache, params: _params, ...rest } = options;
  const edgeOptions = (params !== undefined)
    ? { ...rest, params }
    : { ...rest };
  
  // Fetch from origin (Next.js cache removed for edge compatibility)
  const result = await edgeSanityFetch<T>(edgeOptions as EdgeSanityFetchOptions);
  
  // Populate caches
  memoryCache.set(cacheKey, result, ttl);
  
  if (useRedis && redisWriter) {
    try {
      const entry: CacheEntry<T> = {
        value: result,
        timestamp: Date.now(),
        validUntil: Date.now() + (ttl * 1000)
      };
      await redisWriter.set(cacheKey, entry, { ex: ttl });
    } catch {
      // Redis cache write error, continue without caching
      // Continue without caching
    }
  }
  
  return result;
}

/**
 * Create a cached fetcher with default options
 */
export function createCachedFetcher(
  dataset: string,
  defaultCacheOptions?: CachedFetchOptions['cache']
) {
  return <T>(
    query: string,
    params?: QueryParams,
    cacheOverrides?: CachedFetchOptions['cache']
  ) => {
    return cachedSanityFetch<T>({
      dataset,
      query,
      ...(params !== undefined ? { params } : {}),
      cache: { ...(defaultCacheOptions || {}), ...(cacheOverrides || {}) }
    });
  };
}

/**
 * Clear caches for a specific dataset or pattern
 */
export async function clearSanityCache(options?: {
  dataset?: string;
  pattern?: string;
  clearMemory?: boolean;
  clearRedis?: boolean;
}): Promise<void> {
  const {
    dataset,
    pattern,
    clearMemory = true,
    clearRedis = true
  } = options || {};
  
  // Clear memory cache
  if (clearMemory) {
    if (!dataset && !pattern) {
      memoryCache.clear();
    } else {
      // Note: Memory cache doesn't support pattern matching
      // Would need to iterate all keys for pattern support
      // Pattern-based memory cache clearing not implemented
    }
  }
  
  // Clear Redis cache
  if (clearRedis && redisWriter && redis) {
    try {
      const keyPattern = pattern || (dataset ? `sanity:${dataset}:*` : 'sanity:*');
      const keys = await redis.keys(keyPattern);
      if (keys.length > 0) {
        await redisWriter.del(...keys);
      }
    } catch {
      // Failed to clear Redis cache
    }
  }
}

/**
 * Warm cache by pre-fetching common queries
 */
export async function warmSanityCache(
  queries: Array<{
    dataset: string;
    query: string;
    params?: QueryParams;
    ttl?: number;
  }>
): Promise<void> {
  await Promise.all(
    queries.map(({ dataset, query, params, ttl }) =>
      cachedSanityFetch({
        dataset,
        query,
        ...(params !== undefined ? { params } : {}),
        cache: { ...(ttl !== undefined ? { ttl } : {}) }
      }).catch(() => {
        // Failed to warm cache for query
      })
    )
  );
}

// Export cache status utility
export function getCacheStatus() {
  return {
    memory: {
      available: true,
      size: memoryCache.size()
    },
    redis: {
      available: isRedisConfigured && redis !== null,
      configured: isRedisConfigured,
      url: REDIS_URL ? new URL(REDIS_URL).hostname : null
    },
    nextCache: {
      available: typeof window === 'undefined'
    }
  };
}