// Cache abstraction with two implementations:
// - memory: per-process Map with TTL expiry (single instance)
// - redis: shared GET / SETEX / DEL with JSON values (all instances)
//
// Values are JSON-serialized. Selection happens once based on REDIS_URL. On any
// Redis error the cache behaves as a miss so callers fall back to their source.

import { getRedis, isRedisEnabled } from '../redis.js';

export interface Cache {
  readonly backend: 'memory' | 'redis';
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
  del(key: string): Promise<void>;
}

// ==================== Memory implementation ====================

interface Entry {
  value: unknown;
  expiresAt: number;
}

class MemoryCache implements Cache {
  readonly backend = 'memory' as const;
  private store = new Map<string, Entry>();

  constructor() {
    const cleanup = setInterval(() => {
      const now = Date.now();
      for (const [key, entry] of this.store) {
        if (now > entry.expiresAt) this.store.delete(key);
      }
    }, 60_000);
    (cleanup as any).unref?.();
  }

  async get<T>(key: string): Promise<T | null> {
    const entry = this.store.get(key);
    if (!entry) return null;
    if (Date.now() > entry.expiresAt) {
      this.store.delete(key);
      return null;
    }
    return entry.value as T;
  }

  async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
    this.store.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
  }

  async del(key: string): Promise<void> {
    this.store.delete(key);
  }
}

// ==================== Redis implementation ====================

class RedisCache implements Cache {
  readonly backend = 'redis' as const;

  async get<T>(key: string): Promise<T | null> {
    const redis = getRedis();
    if (!redis) return null;
    try {
      const raw = await redis.get(`cache:${key}`);
      if (raw === null) return null;
      return JSON.parse(raw) as T;
    } catch (err: any) {
      console.error('[cache] redis get error:', err?.message || err);
      return null;
    }
  }

  async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
    const redis = getRedis();
    if (!redis) return;
    try {
      await redis.set(`cache:${key}`, JSON.stringify(value), 'EX', ttlSeconds);
    } catch (err: any) {
      console.error('[cache] redis set error:', err?.message || err);
    }
  }

  async del(key: string): Promise<void> {
    const redis = getRedis();
    if (!redis) return;
    try {
      await redis.del(`cache:${key}`);
    } catch (err: any) {
      console.error('[cache] redis del error:', err?.message || err);
    }
  }
}

// ==================== Selection ====================

let instance: Cache | null = null;

export function getCache(): Cache {
  if (!instance) {
    instance = isRedisEnabled() ? new RedisCache() : new MemoryCache();
  }
  return instance;
}
