const Redis = require('ioredis');

// Redis is a cache, not a source of truth. If it's unreachable, callers
// should fall back to the underlying data store (Supabase). All higher-level
// helpers here return null on connection failure rather than throwing —
// that keeps every consumer single-branch (`if (cached) return cached`).

let client = null;
let degraded = false;

function getRedisClient() {
  if (!client) {
    client = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379', {
      // Eager connect — but allow commands to QUEUE while the TCP/auth
      // handshake completes. Without offline queueing, pollers booting
      // alongside the API throw "Stream isn't writeable" before Redis
      // is ready. With queueing, ioredis flushes the backlog the moment
      // the connection enters READY state.
      lazyConnect: false,
      enableOfflineQueue: true,
      maxRetriesPerRequest: 1,
      retryStrategy(times) {
        // Exponential backoff up to 30s. ioredis keeps trying forever on its
        // own; we just slow it down so the logs aren't a hose.
        return Math.min(times * 1000, 30_000);
      },
    });
    client.on('error', (err) => {
      // Only log the first failure to avoid log spam during outages.
      if (!degraded) {
        console.warn('[redis] entering degraded mode:', err.message);
        degraded = true;
      }
    });
    client.on('ready', () => {
      // Surface the ready transition in PM2/container logs so operators
      // can confirm the connection actually established. Distinct from
      // reconnect-after-outage which the line below logs at info.
      if (degraded) console.info('[redis] reconnected, leaving degraded mode');
      else console.log('[redis] connected and ready');
      degraded = false;
    });
  }
  return client;
}

function isDegraded() {
  return degraded;
}

async function cacheGet(key) {
  if (degraded) return null;
  try {
    const raw = await getRedisClient().get(key);
    if (!raw) return null;
    try {
      return JSON.parse(raw);
    } catch {
      // Non-JSON values are returned as-is (some callers store plain strings).
      return raw;
    }
  } catch (err) {
    if (!degraded) console.warn('[redis] cacheGet failed:', err.message);
    return null;
  }
}

async function cacheSet(key, value, ttlSeconds) {
  if (degraded) return false;
  try {
    const payload = typeof value === 'string' ? value : JSON.stringify(value);
    if (ttlSeconds && ttlSeconds > 0) {
      await getRedisClient().set(key, payload, 'EX', ttlSeconds);
    } else {
      await getRedisClient().set(key, payload);
    }
    return true;
  } catch (err) {
    if (!degraded) console.warn('[redis] cacheSet failed:', err.message);
    return false;
  }
}

async function cacheDel(key) {
  if (degraded) return false;
  try {
    await getRedisClient().del(key);
    return true;
  } catch (err) {
    if (!degraded) console.warn('[redis] cacheDel failed:', err.message);
    return false;
  }
}

module.exports = { getRedisClient, cacheGet, cacheSet, cacheDel, isDegraded };
