// Stale-while-revalidate caches for the three heavy endpoints:
// /api/thomas, /api/tools, /api/pricing.
//
// All three need to scan payload BLOBs to detect outcomes / tool use,
// so even with indexes a cold compute is 5-30s on a 1+GB trace store.
// TTL-only caches still pay that cost when the TTL window expires
// between user clicks.
//
// Strategy: keep the cache fresh in the background. The user-facing
// handler always serves the cached value (synchronous wait only on
// the very first request after daemon boot, or after 30+ minutes of
// inactivity). A maintenance loop refreshes each entry on its own
// schedule. The loop skips work entirely if no user has touched the
// endpoint in the last 30 min — so the daemon idles instead of
// burning CPU overnight.

import { logger } from '../../core/logger.ts'

export type CacheEntry<T> = {
  data: T
  at: number
}

type Loader<T> = () => Promise<T>

const IDLE_THRESHOLD_MS = 30 * 60_000
const STALE_LIMIT_MS = 5 * 60_000

export class WarmCache<K, T> {
  private readonly entries = new Map<K, CacheEntry<T>>()
  private readonly inFlight = new Map<K, Promise<T>>()
  private readonly subscribers = new Map<K, number>()
  private readonly loaders = new Map<K, Loader<T>>()
  private readonly label: string
  private timer: ReturnType<typeof setInterval> | null = null
  private warming = false

  constructor(label: string) {
    this.label = label
  }

  /** Register a (key, loader) pair. Loader will be called by the
   *  background refresher and on synchronous cache misses. */
  register(key: K, loader: Loader<T>): void {
    this.loaders.set(key, loader)
  }

  /**
   * Fetch a value. Synchronous on the very first call (no cache),
   * always cached for subsequent calls within STALE_LIMIT_MS.
   * Records that someone asked, so the keep-warm loop knows the
   * endpoint is in use.
   */
  async get(key: K): Promise<T> {
    this.subscribers.set(key, Date.now())
    const hit = this.entries.get(key)
    if (hit && Date.now() - hit.at < STALE_LIMIT_MS) {
      return hit.data
    }
    const pending = this.inFlight.get(key)
    if (pending) return pending
    const loader = this.loaders.get(key)
    if (!loader) {
      throw new Error(`WarmCache(${this.label}): no loader for key ${String(key)}`)
    }
    return this.refresh(key, loader)
  }

  /** Drop everything we cached. Used when a write invalidates state. */
  invalidate(key?: K): void {
    if (key !== undefined) {
      this.entries.delete(key)
      this.inFlight.delete(key)
      return
    }
    this.entries.clear()
    this.inFlight.clear()
  }

  /**
   * Start the background refresh loop. Refreshes every `periodMs`,
   * but only for keys that had a user request in the last
   * IDLE_THRESHOLD_MS. The loader runs sequentially so we don't slam
   * the DB with parallel scans.
   */
  startKeepWarm(periodMs: number): void {
    if (this.timer) return
    const tick = async () => {
      if (this.warming) return
      this.warming = true
      try {
        const now = Date.now()
        const active = [...this.subscribers.entries()]
          .filter(([, at]) => now - at < IDLE_THRESHOLD_MS)
          .map(([k]) => k)
        for (const key of active) {
          const loader = this.loaders.get(key)
          if (!loader) continue
          try {
            await this.refresh(key, loader)
          } catch (err) {
            logger.warn(`keep-warm ${this.label}/${String(key)} failed: ${(err as Error).message}`)
          }
        }
      } finally {
        this.warming = false
      }
    }
    this.timer = setInterval(() => void tick(), periodMs)
    this.timer.unref()
  }

  stopKeepWarm(): void {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }

  private async refresh(key: K, loader: Loader<T>): Promise<T> {
    const promise = (async () => {
      try {
        const data = await loader()
        this.entries.set(key, { data, at: Date.now() })
        return data
      } finally {
        this.inFlight.delete(key)
      }
    })()
    this.inFlight.set(key, promise)
    return promise
  }
}
