/**
 * LRU-based seen event tracker with TTL support.
 * Prevents unbounded memory growth under high load or abuse.
 */

export interface SeenTrackerOptions {
  /** Maximum number of entries to track (default: 100,000) */
  maxEntries?: number;
  /** TTL in milliseconds (default: 1 hour) */
  ttlMs?: number;
  /** Prune interval in milliseconds (default: 10 minutes) */
  pruneIntervalMs?: number;
}

export interface SeenTracker {
  /** Check if an ID has been seen (also marks it as seen if not) */
  has: (id: string) => boolean;
  /** Mark an ID as seen */
  add: (id: string) => void;
  /** Check if ID exists without marking */
  peek: (id: string) => boolean;
  /** Delete an ID */
  delete: (id: string) => void;
  /** Clear all entries */
  clear: () => void;
  /** Get current size */
  size: () => number;
  /** Stop the pruning timer */
  stop: () => void;
  /** Pre-seed with IDs (useful for restart recovery) */
  seed: (ids: string[]) => void;
}

interface Entry {
  seenAt: number;
  // For LRU: track order via doubly-linked list
  prev: string | null;
  next: string | null;
}

/**
 * Create a new seen tracker with LRU eviction and TTL expiration.
 */
export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
  const maxEntries = options?.maxEntries ?? 100_000;
  const ttlMs = options?.ttlMs ?? 60 * 60 * 1000; // 1 hour
  const pruneIntervalMs = options?.pruneIntervalMs ?? 10 * 60 * 1000; // 10 minutes

  // Main storage
  const entries = new Map<string, Entry>();

  // LRU tracking: head = most recent, tail = least recent
  let head: string | null = null;
  let tail: string | null = null;

  // Move an entry to the front (most recently used)
  function moveToFront(id: string): void {
    const entry = entries.get(id);
    if (!entry) return;

    // Already at front
    if (head === id) return;

    // Remove from current position
    if (entry.prev) {
      const prevEntry = entries.get(entry.prev);
      if (prevEntry) prevEntry.next = entry.next;
    }
    if (entry.next) {
      const nextEntry = entries.get(entry.next);
      if (nextEntry) nextEntry.prev = entry.prev;
    }

    // Update tail if this was the tail
    if (tail === id) {
      tail = entry.prev;
    }

    // Move to front
    entry.prev = null;
    entry.next = head;
    if (head) {
      const headEntry = entries.get(head);
      if (headEntry) headEntry.prev = id;
    }
    head = id;

    // If no tail, this is also the tail
    if (!tail) tail = id;
  }

  // Remove an entry from the linked list
  function removeFromList(id: string): void {
    const entry = entries.get(id);
    if (!entry) return;

    if (entry.prev) {
      const prevEntry = entries.get(entry.prev);
      if (prevEntry) prevEntry.next = entry.next;
    } else {
      head = entry.next;
    }

    if (entry.next) {
      const nextEntry = entries.get(entry.next);
      if (nextEntry) nextEntry.prev = entry.prev;
    } else {
      tail = entry.prev;
    }
  }

  // Evict the least recently used entry
  function evictLRU(): void {
    if (!tail) return;
    const idToEvict = tail;
    removeFromList(idToEvict);
    entries.delete(idToEvict);
  }

  // Prune expired entries
  function pruneExpired(): void {
    const now = Date.now();
    const toDelete: string[] = [];

    for (const [id, entry] of entries) {
      if (now - entry.seenAt > ttlMs) {
        toDelete.push(id);
      }
    }

    for (const id of toDelete) {
      removeFromList(id);
      entries.delete(id);
    }
  }

  // Start pruning timer
  let pruneTimer: ReturnType<typeof setInterval> | undefined;
  if (pruneIntervalMs > 0) {
    pruneTimer = setInterval(pruneExpired, pruneIntervalMs);
    // Don't keep process alive just for pruning
    if (pruneTimer.unref) pruneTimer.unref();
  }

  function add(id: string): void {
    const now = Date.now();

    // If already exists, update and move to front
    const existing = entries.get(id);
    if (existing) {
      existing.seenAt = now;
      moveToFront(id);
      return;
    }

    // Evict if at capacity
    while (entries.size >= maxEntries) {
      evictLRU();
    }

    // Add new entry at front
    const newEntry: Entry = {
      seenAt: now,
      prev: null,
      next: head,
    };

    if (head) {
      const headEntry = entries.get(head);
      if (headEntry) headEntry.prev = id;
    }

    entries.set(id, newEntry);
    head = id;
    if (!tail) tail = id;
  }

  function has(id: string): boolean {
    const entry = entries.get(id);
    if (!entry) {
      add(id);
      return false;
    }

    // Check if expired
    if (Date.now() - entry.seenAt > ttlMs) {
      removeFromList(id);
      entries.delete(id);
      add(id);
      return false;
    }

    // Mark as recently used
    entry.seenAt = Date.now();
    moveToFront(id);
    return true;
  }

  function peek(id: string): boolean {
    const entry = entries.get(id);
    if (!entry) return false;

    // Check if expired
    if (Date.now() - entry.seenAt > ttlMs) {
      removeFromList(id);
      entries.delete(id);
      return false;
    }

    return true;
  }

  function deleteEntry(id: string): void {
    if (entries.has(id)) {
      removeFromList(id);
      entries.delete(id);
    }
  }

  function clear(): void {
    entries.clear();
    head = null;
    tail = null;
  }

  function size(): number {
    return entries.size;
  }

  function stop(): void {
    if (pruneTimer) {
      clearInterval(pruneTimer);
      pruneTimer = undefined;
    }
  }

  function seed(ids: string[]): void {
    const now = Date.now();
    // Seed in reverse order so first IDs end up at front
    for (let i = ids.length - 1; i >= 0; i--) {
      const id = ids[i];
      if (!entries.has(id) && entries.size < maxEntries) {
        const newEntry: Entry = {
          seenAt: now,
          prev: null,
          next: head,
        };

        if (head) {
          const headEntry = entries.get(head);
          if (headEntry) headEntry.prev = id;
        }

        entries.set(id, newEntry);
        head = id;
        if (!tail) tail = id;
      }
    }
  }

  return {
    has,
    add,
    peek,
    delete: deleteEntry,
    clear,
    size,
    stop,
    seed,
  };
}
