// Rate limiter abstraction with two implementations:
// - memory: per-process fixed window (the original behavior)
// - redis: shared fixed window across all instances (INCR + PEXPIRE)
//
// Selection happens once based on REDIS_URL. On any Redis error the limiter
// fails open (allows the request) so a Redis blip never takes the API down.

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

export interface RateLimitResult {
  allowed: boolean;
  retryAfter?: number;
}

export interface RateLimiter {
  readonly backend: 'memory' | 'redis';
  consume(key: string, max: number, windowMs: number): Promise<RateLimitResult>;
}

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

interface Bucket {
  count: number;
  resetAt: number;
}

class MemoryRateLimiter implements RateLimiter {
  readonly backend = 'memory' as const;
  private buckets = new Map<string, Bucket>();

  constructor() {
    // Periodically drop expired buckets so the Map does not grow unbounded.
    const cleanup = setInterval(() => {
      const now = Date.now();
      for (const [key, bucket] of this.buckets) {
        if (now > bucket.resetAt) this.buckets.delete(key);
      }
    }, 60_000);
    (cleanup as any).unref?.();
  }

  async consume(key: string, max: number, windowMs: number): Promise<RateLimitResult> {
    const now = Date.now();
    const bucket = this.buckets.get(key);

    if (!bucket || now > bucket.resetAt) {
      this.buckets.set(key, { count: 1, resetAt: now + windowMs });
      return { allowed: true };
    }

    bucket.count++;
    if (bucket.count > max) {
      return { allowed: false, retryAfter: Math.ceil((bucket.resetAt - now) / 1000) };
    }
    return { allowed: true };
  }
}

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

class RedisRateLimiter implements RateLimiter {
  readonly backend = 'redis' as const;

  async consume(key: string, max: number, windowMs: number): Promise<RateLimitResult> {
    const redis = getRedis();
    if (!redis) return { allowed: true };

    const redisKey = `rl:${key}`;
    try {
      const count = await redis.incr(redisKey);
      if (count === 1) {
        // First hit in this window: set the expiry that defines the window.
        await redis.pexpire(redisKey, windowMs);
      }
      if (count > max) {
        const ttl = await redis.pttl(redisKey);
        const retryAfter = ttl > 0 ? Math.ceil(ttl / 1000) : Math.ceil(windowMs / 1000);
        return { allowed: false, retryAfter };
      }
      return { allowed: true };
    } catch (err: any) {
      // Fail open: never block traffic because Redis is unavailable.
      console.error('[rateLimiter] redis error, allowing request:', err?.message || err);
      return { allowed: true };
    }
  }
}

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

let instance: RateLimiter | null = null;

export function getRateLimiter(): RateLimiter {
  if (!instance) {
    instance = isRedisEnabled() ? new RedisRateLimiter() : new MemoryRateLimiter();
  }
  return instance;
}
