/**
 * DataCache service - LRU cache for parsed session data.
 *
 * Responsibilities:
 * - Cache parsed SessionDetail objects to avoid re-parsing
 * - LRU eviction policy with configurable max size
 * - TTL-based expiration
 * - Provide cache invalidation for file changes
 */

import { type SessionDetail, type SubagentDetail } from '@main/types';
import { createLogger } from '@shared/utils/logger';

const logger = createLogger('Service:DataCache');

interface CacheEntry<T> {
  value: T;

  timestamp: number;
  version: number; // Cache schema version
}

// Union type for cached values

type CachedValue = SessionDetail | SubagentDetail;

export class DataCache {
  private cache: Map<string, CacheEntry<CachedValue>>;
  private maxSize: number;
  private ttl: number; // Time-to-live in milliseconds
  private enabled: boolean; // Whether caching is enabled
  private disposed = false; // Flag to prevent reuse after disposal
  private static readonly CURRENT_VERSION = 2; // Increment when cache structure changes

  constructor(maxSize: number = 50, ttlMinutes: number = 10, enabled: boolean = true) {
    this.cache = new Map();
    this.maxSize = maxSize;
    this.ttl = ttlMinutes * 60 * 1000;
    this.enabled = enabled;
  }

  /**
   * Enable or disable caching.
   */
  setEnabled(enabled: boolean): void {
    this.enabled = enabled;
    if (!enabled) {
      // Clear cache when disabling
      this.cache.clear();
    }
  }

  /**
   * Check if caching is enabled.
   */
  isEnabled(): boolean {
    return this.enabled;
  }

  // ===========================================================================
  // Cache Operations
  // ===========================================================================

  /**
   * Gets a cached session detail.
   * @param key - Cache key in format "projectId/sessionId"
   * @returns The cached SessionDetail, or undefined if not found or expired
   */
  get(key: string): SessionDetail | undefined {
    if (!this.enabled) {
      return undefined;
    }

    const entry = this.cache.get(key);

    if (!entry) {
      return undefined;
    }

    // Check if entry version is outdated
    if (entry.version !== DataCache.CURRENT_VERSION) {
      logger.info(`DataCache: Invalidating outdated cache entry (v${entry.version}): ${key}`);
      this.cache.delete(key);
      return undefined;
    }

    // Check if entry has expired
    const now = Date.now();
    if (now - entry.timestamp > this.ttl) {
      this.cache.delete(key);
      return undefined;
    }

    // Move to end (mark as recently used)
    this.cache.delete(key);
    this.cache.set(key, entry);

    return entry.value as SessionDetail;
  }

  /**
   * Gets a cached subagent detail.
   * @param key - Cache key in format "subagent-projectId-sessionId-subagentId"
   * @returns The cached SubagentDetail, or undefined if not found or expired
   */
  getSubagent(key: string): SubagentDetail | undefined {
    if (!this.enabled) {
      return undefined;
    }

    const entry = this.cache.get(key);

    if (!entry) {
      return undefined;
    }

    // Check if entry version is outdated
    if (entry.version !== DataCache.CURRENT_VERSION) {
      logger.info(
        `DataCache: Invalidating outdated subagent cache entry (v${entry.version}): ${key}`
      );
      this.cache.delete(key);
      return undefined;
    }

    // Check if entry has expired
    const now = Date.now();
    if (now - entry.timestamp > this.ttl) {
      this.cache.delete(key);
      return undefined;
    }

    // Move to end (mark as recently used)
    this.cache.delete(key);
    this.cache.set(key, entry);

    return entry.value as SubagentDetail;
  }

  /**
   * Internal method to set a value in the cache.
   * Handles LRU eviction and cache entry creation.
   */
  private setInternal(key: string, value: CachedValue): void {
    if (!this.enabled) {
      return;
    }

    // If at max size, remove least recently used (first entry)
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      if (firstKey) {
        this.cache.delete(firstKey);
      }
    }

    this.cache.set(key, {
      value,
      timestamp: Date.now(),
      version: DataCache.CURRENT_VERSION,
    });
  }

  /**
   * Sets a value in the cache.
   * @param key - Cache key in format "projectId/sessionId"
   * @param value - The SessionDetail to cache
   */
  set(key: string, value: SessionDetail): void {
    this.setInternal(key, value);
  }

  /**
   * Sets a subagent detail value in the cache.
   * @param key - Cache key in format "subagent-projectId-sessionId-subagentId"
   * @param value - The SubagentDetail to cache
   */
  setSubagent(key: string, value: SubagentDetail): void {
    this.setInternal(key, value);
  }

  /**
   * Checks if a key exists in the cache and is not expired.
   * @param key - Cache key to check
   * @returns true if key exists and is valid, false otherwise
   */
  has(key: string): boolean {
    return this.get(key) !== undefined;
  }

  // ===========================================================================
  // Key Building
  // ===========================================================================

  /**
   * Build a cache key from project and session IDs.
   */
  static buildKey(projectId: string, sessionId: string): string {
    return `${projectId}/${sessionId}`;
  }

  /**
   * Parse a cache key into project and session IDs.
   */
  static parseKey(key: string): { projectId: string; sessionId: string } | null {
    const parts = key.split('/');
    if (parts.length !== 2) return null;
    return { projectId: parts[0], sessionId: parts[1] };
  }

  // ===========================================================================
  // Invalidation
  // ===========================================================================

  /**
   * Invalidates a specific cache entry.
   * @param key - Cache key to invalidate
   */
  invalidate(key: string): void {
    this.cache.delete(key);
  }

  /**
   * Invalidates a cache entry by project and session IDs.
   */
  invalidateSession(projectId: string, sessionId: string): void {
    const keysToDelete: string[] = [];
    const sessionToken = `-${sessionId}-`;

    for (const key of this.cache.keys()) {
      const parsed = DataCache.parseKey(key);
      if (
        parsed?.sessionId === sessionId &&
        this.matchesProjectOrComposite(parsed.projectId, projectId)
      ) {
        keysToDelete.push(key);
        continue;
      }

      if (this.isSubagentKeyForProject(key, projectId) && key.includes(sessionToken)) {
        keysToDelete.push(key);
      }
    }

    for (const key of keysToDelete) {
      this.cache.delete(key);
    }
  }

  /**
   * Invalidates all cached subagent details for a session.
   */
  invalidateSubagentSession(projectId: string, sessionId: string): void {
    const sessionToken = `-${sessionId}-`;
    const keysToDelete: string[] = [];

    for (const key of this.cache.keys()) {
      if (this.isSubagentKeyForProject(key, projectId) && key.includes(sessionToken)) {
        keysToDelete.push(key);
      }
    }

    for (const key of keysToDelete) {
      this.cache.delete(key);
    }
  }

  /**
   * Invalidates all cache entries for a project.
   * @param projectId - The project ID
   */
  invalidateProject(projectId: string): void {
    const keysToDelete: string[] = [];

    for (const key of this.cache.keys()) {
      const parsed = DataCache.parseKey(key);
      if (parsed && this.matchesProjectOrComposite(parsed.projectId, projectId)) {
        keysToDelete.push(key);
        continue;
      }

      if (this.isSubagentKeyForProject(key, projectId)) {
        keysToDelete.push(key);
      }
    }

    for (const key of keysToDelete) {
      this.cache.delete(key);
    }
  }

  /**
   * Clears the entire cache.
   */
  clear(): void {
    this.cache.clear();
  }

  // ===========================================================================
  // Cache Management
  // ===========================================================================

  /**
   * Gets current cache size.
   * @returns Number of entries in the cache
   */
  size(): number {
    return this.cache.size;
  }

  /**
   * Gets cache statistics.
   * @returns Object with cache stats
   */
  stats(): {
    size: number;
    maxSize: number;
    ttlMinutes: number;
    keys: string[];
  } {
    return {
      size: this.cache.size,
      maxSize: this.maxSize,
      ttlMinutes: this.ttl / 60000,
      keys: Array.from(this.cache.keys()),
    };
  }

  /**
   * Removes expired and outdated entries from the cache.
   * Should be called periodically to prevent memory bloat.
   */
  cleanExpired(): number {
    const now = Date.now();
    const keysToDelete: string[] = [];

    for (const [key, entry] of this.cache.entries()) {
      // Remove if expired OR outdated version
      if (now - entry.timestamp > this.ttl || entry.version !== DataCache.CURRENT_VERSION) {
        keysToDelete.push(key);
      }
    }

    for (const key of keysToDelete) {
      this.cache.delete(key);
    }

    if (keysToDelete.length > 0) {
      logger.info(`DataCache: Cleaned ${keysToDelete.length} expired/outdated entries`);
    }

    return keysToDelete.length;
  }

  /**
   * Starts automatic cleanup of expired entries.
   * @param intervalMinutes - How often to run cleanup (default: 5 minutes)
   * @returns Timer handle that can be used to stop cleanup
   */
  startAutoCleanup(intervalMinutes: number = 5): NodeJS.Timeout {
    const intervalMs = intervalMinutes * 60 * 1000;
    const timer = setInterval(() => {
      this.cleanExpired();
    }, intervalMs);
    // Background maintenance should not keep the process alive.
    timer.unref();
    return timer;
  }

  /**
   * Gets all cached session IDs for a project.
   */
  getProjectSessionIds(projectId: string): string[] {
    const sessionIds: string[] = [];

    for (const key of this.cache.keys()) {
      const parsed = DataCache.parseKey(key);
      if (parsed && this.matchesProjectOrComposite(parsed.projectId, projectId)) {
        sessionIds.push(parsed.sessionId);
      }
    }

    return sessionIds;
  }

  private matchesProjectOrComposite(projectId: string, baseProjectId: string): boolean {
    return projectId === baseProjectId || projectId.startsWith(`${baseProjectId}::`);
  }

  private isSubagentKeyForProject(key: string, baseProjectId: string): boolean {
    if (!key.startsWith('subagent-')) {
      return false;
    }
    const prefix = `subagent-${baseProjectId}`;
    return key.startsWith(`${prefix}-`) || key.startsWith(`${prefix}::`);
  }

  /**
   * Disposes the cache and prevents further use.
   * Clears all cached data and disables caching.
   *
   * Note: The auto-cleanup interval returned by startAutoCleanup() is managed
   * by the caller (ServiceContext), not stored internally, so we only need to
   * clear the cache and disable it.
   */
  dispose(): void {
    if (this.disposed) {
      logger.info('DataCache already disposed');
      return;
    }

    logger.info('Disposing DataCache');

    // Clear all cached data
    this.cache.clear();

    // Disable caching
    this.enabled = false;

    // Mark as disposed
    this.disposed = true;

    logger.info('DataCache disposed');
  }
}
