import type { Prisma } from "@prisma/client";
import db from "../db.server";

/**
 * 2-tier cache (Section XIV CA-01/02/03).
 *
 * Tier 1: in-memory `Map` keyed by string, capped TTL of MEMORY_TIER_MAX_TTL_MS.
 *         Fast path; per-instance, lost on cold starts.
 * Tier 2: Postgres `CacheEntry` table; durable across instances and restarts.
 *
 * Reads check memory first; on miss they fall through to Postgres and hydrate
 * memory. Writes go to both tiers. No background timers — expired entries are
 * pruned opportunistically on each `get`/`set`, plus a 1% probabilistic sweep
 * of the Postgres tier on every `get`.
 */

const MEMORY_TIER_MAX_TTL_MS = 60_000;
const DEFAULT_TTL_SECONDS = 300;
const CLEANUP_PROBABILITY = 0.01;

interface MemoryEntry {
  value: unknown;
  expiresAt: number; // epoch ms
}

const memoryTier = new Map<string, MemoryEntry>();

function nowMs(): number {
  return Date.now();
}

function pruneMemoryTier(): void {
  const now = nowMs();
  for (const [key, entry] of memoryTier) {
    if (entry.expiresAt <= now) {
      memoryTier.delete(key);
    }
  }
}

function setMemory(key: string, value: unknown, ttlMs: number): void {
  const cappedTtl = Math.min(ttlMs, MEMORY_TIER_MAX_TTL_MS);
  memoryTier.set(key, { value, expiresAt: nowMs() + cappedTtl });
}

export async function cacheGet<T>(key: string): Promise<T | null> {
  pruneMemoryTier();

  // Fire-and-forget probabilistic cleanup of the Postgres tier — keeps the
  // table from growing without needing a worker process or setInterval.
  if (Math.random() < CLEANUP_PROBABILITY) {
    cacheCleanupExpired().catch(() => {
      // Swallow: cleanup is best-effort and must never break a read.
    });
  }

  const memHit = memoryTier.get(key);
  if (memHit && memHit.expiresAt > nowMs()) {
    return memHit.value as T;
  }
  if (memHit) {
    memoryTier.delete(key);
  }

  const row = await db.cacheEntry.findUnique({ where: { key } });
  if (!row) return null;

  const expiresAtMs = row.expiresAt.getTime();
  if (expiresAtMs <= nowMs()) {
    // Stale row — drop it and treat as a miss.
    await db.cacheEntry.delete({ where: { key } }).catch(() => {
      // Another instance may have already deleted it; ignore.
    });
    return null;
  }

  // Hydrate the memory tier so subsequent reads stay on the fast path.
  setMemory(key, row.value, expiresAtMs - nowMs());
  return row.value as T;
}

export async function cacheSet<T>(
  key: string,
  value: T,
  ttlSeconds: number = DEFAULT_TTL_SECONDS,
): Promise<void> {
  pruneMemoryTier();

  const ttlMs = ttlSeconds * 1000;
  const expiresAt = new Date(nowMs() + ttlMs);

  setMemory(key, value, ttlMs);

  // Prisma's Json input requires a JSON-serialisable value; cast through the
  // generated `InputJsonValue` to keep this strongly typed.
  const jsonValue = value as unknown as Prisma.InputJsonValue;

  await db.cacheEntry.upsert({
    where: { key },
    create: { key, value: jsonValue, expiresAt },
    update: { value: jsonValue, expiresAt },
  });
}

export async function cacheDelete(key: string): Promise<void> {
  memoryTier.delete(key);
  await db.cacheEntry.delete({ where: { key } }).catch(() => {
    // Missing row is a no-op; mirrors `Map.delete` semantics.
  });
}

export async function cacheCleanupExpired(): Promise<number> {
  pruneMemoryTier();
  const result = await db.cacheEntry.deleteMany({
    where: { expiresAt: { lt: new Date() } },
  });
  return result.count;
}
