/**
 * Semantic Cache Implementation - Job 029
 *
 * Caches LLM responses keyed by prompt hash to avoid redundant API calls.
 * Features:
 * - SHA256 or simple hashing
 * - TTL-based expiration
 * - LRU eviction when full
 * - Serialization for persistence
 */

import { sha256Text } from "../../../../src/core/artifacts/hash";
import type { CacheEntry, CacheStats, CacheConfig, SerializedCache } from "./types";

const DEFAULT_CONFIG: CacheConfig = {
  maxEntries: 1000,
  defaultTtl: 0,  // No expiry by default
  hashAlgorithm: 'sha256'
};

/**
 * Simple hash function for testing (faster but less secure)
 */
function simpleHash(s: string): string {
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = ((h << 5) - h) + s.charCodeAt(i);
    h = h & h;  // Convert to 32-bit integer
  }
  return `simple:${Math.abs(h).toString(16)}`;
}

export class SemanticCache {
  private cache = new Map<string, CacheEntry>();
  private hits = 0;
  private misses = 0;
  private config: CacheConfig;

  constructor(config: Partial<CacheConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  /**
   * Hash a prompt for cache key
   */
  private hash(prompt: string): string {
    if (this.config.hashAlgorithm === 'simple') {
      return simpleHash(prompt);
    }
    return sha256Text(prompt);
  }

  /**
   * Get cached response for a prompt
   * @returns CacheEntry if found and not expired, undefined otherwise
   */
  get(prompt: string): CacheEntry | undefined {
    const hash = this.hash(prompt);
    const entry = this.cache.get(hash);

    if (!entry) {
      this.misses++;
      return undefined;
    }

    // Check TTL expiration
    if (entry.ttl && entry.ttl > 0 && Date.now() - entry.timestamp > entry.ttl) {
      this.cache.delete(hash);
      this.misses++;
      return undefined;
    }

    // Update hit count
    entry.hits++;
    this.hits++;
    return entry;
  }

  /**
   * Store a response in the cache
   */
  set(prompt: string, response: string, model: string, ttl?: number): void {
    const hash = this.hash(prompt);

    // Evict oldest if at capacity
    if (this.cache.size >= this.config.maxEntries) {
      this.evictOldest();
    }

    this.cache.set(hash, {
      prompt,
      promptHash: hash,
      response,
      timestamp: Date.now(),
      model,
      hits: 0,
      ttl: ttl ?? this.config.defaultTtl
    });
  }

  /**
   * Check if prompt is in cache (without counting as hit/miss)
   */
  has(prompt: string): boolean {
    const hash = this.hash(prompt);
    const entry = this.cache.get(hash);

    if (!entry) return false;

    // Check TTL
    if (entry.ttl && entry.ttl > 0 && Date.now() - entry.timestamp > entry.ttl) {
      this.cache.delete(hash);
      return false;
    }

    return true;
  }

  /**
   * Delete a specific entry
   */
  delete(prompt: string): boolean {
    const hash = this.hash(prompt);
    return this.cache.delete(hash);
  }

  /**
   * Clear all cache entries
   */
  clear(): void {
    this.cache.clear();
    this.hits = 0;
    this.misses = 0;
  }

  /**
   * Get cache statistics
   */
  stats(): CacheStats {
    const total = this.hits + this.misses;
    return {
      hits: this.hits,
      misses: this.misses,
      entries: this.cache.size,
      savedCalls: this.hits,
      hitRate: total > 0 ? this.hits / total : 0
    };
  }

  /**
   * Get number of entries
   */
  size(): number {
    return this.cache.size;
  }

  /**
   * Evict the oldest entry (LRU approximation)
   */
  private evictOldest(): void {
    let oldest: string | undefined;
    let oldestTime = Infinity;

    for (const [hash, entry] of this.cache) {
      if (entry.timestamp < oldestTime) {
        oldestTime = entry.timestamp;
        oldest = hash;
      }
    }

    if (oldest) {
      this.cache.delete(oldest);
    }
  }

  /**
   * Serialize cache to JSON string
   */
  toJSON(): string {
    const data: SerializedCache = {
      entries: Array.from(this.cache.values()),
      stats: { hits: this.hits, misses: this.misses }
    };
    return JSON.stringify(data, null, 2);
  }

  /**
   * Create cache from serialized JSON
   */
  static fromJSON(json: string, config?: Partial<CacheConfig>): SemanticCache {
    const cache = new SemanticCache(config);
    const data = JSON.parse(json) as SerializedCache;

    for (const entry of data.entries) {
      cache.cache.set(entry.promptHash, entry);
    }
    cache.hits = data.stats?.hits ?? 0;
    cache.misses = data.stats?.misses ?? 0;

    return cache;
  }

  /**
   * Get all entries (for debugging/export)
   */
  all(): CacheEntry[] {
    return Array.from(this.cache.values());
  }
}

// Singleton instance for global cache
export const semanticCache = new SemanticCache();

// Named export for class
export { SemanticCache as default };
