/**
 * Anonymous & per-IP rate limiting with Redis.
 *
 * Why a singleton: each Next.js Node lambda/runtime spawns a new module scope,
 * but within the running process we want exactly one connection pool.
 *
 * Defensive: if REDIS_URL is missing or the broker is down, calls return a
 * "fail-open" decision so the user-facing route still works (we'd rather pay
 * for a few extra OpenAI calls than 500 a real human request).
 */

import Redis, { type Redis as RedisType } from "ioredis";

let client: RedisType | null = null;
let warned = false;

export function getRedis(): RedisType | null {
  if (client) return client;
  const url = process.env.REDIS_URL;
  if (!url) {
    if (!warned) {
      console.warn("[redis] REDIS_URL not set — rate limiting disabled");
      warned = true;
    }
    return null;
  }
  try {
    client = new Redis(url, {
      maxRetriesPerRequest: 2,
      enableReadyCheck: true,
      lazyConnect: false,
      connectTimeout: 5000,
    });
    client.on("error", (err) => {
      // Avoid log flood: ioredis retries internally.
      if (!warned) {
        console.error("[redis] connection error:", err.message);
        warned = true;
      }
    });
    return client;
  } catch (err) {
    console.error("[redis] init failed:", err);
    return null;
  }
}

export type RateCheck = {
  allowed: boolean;
  used: number;
  limit: number;
  remaining: number;
  /** True if Redis was unavailable — caller decides whether to allow */
  failOpen?: boolean;
};

/**
 * Increment-and-check rolling counter.
 * Returns RateCheck. On Redis error, `failOpen=true` and `allowed=true`
 * (we don't block real users on infra issues — we'd rather pay).
 */
export async function checkAndIncrement(
  key: string,
  limit: number,
  windowSeconds: number
): Promise<RateCheck> {
  const r = getRedis();
  if (!r) {
    return { allowed: true, used: 0, limit, remaining: limit, failOpen: true };
  }
  try {
    // Use multi to make INCR + EXPIRE atomic-ish (EXPIRE only on first hit).
    const pipeline = r.multi();
    pipeline.incr(key);
    pipeline.ttl(key);
    const results = await pipeline.exec();
    if (!results) {
      return { allowed: true, used: 0, limit, remaining: limit, failOpen: true };
    }
    const used = (results[0]?.[1] as number) ?? 0;
    const ttl = (results[1]?.[1] as number) ?? -1;
    if (ttl < 0) {
      // First hit — set expiry. We do this AFTER incr so the window starts
      // at the first request (rolling), not the most recent one.
      await r.expire(key, windowSeconds);
    }
    return {
      allowed: used <= limit,
      used,
      limit,
      remaining: Math.max(0, limit - used),
    };
  } catch (err) {
    console.error("[redis] checkAndIncrement failed:", err);
    return { allowed: true, used: 0, limit, remaining: limit, failOpen: true };
  }
}

/**
 * Read counter without incrementing. Used by quota status endpoints.
 */
export async function getCounter(key: string): Promise<number> {
  const r = getRedis();
  if (!r) return 0;
  try {
    const v = await r.get(key);
    return v ? parseInt(v, 10) : 0;
  } catch {
    return 0;
  }
}
