/**
 * Rate limiting for API endpoints
 *
 * Production: Uses Upstash Redis for distributed rate limiting across
 * serverless invocations. Falls back to in-memory Map() if Redis is
 * unavailable (local dev or connection failure).
 *
 * Env vars (pick one):
 *   UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN  (preferred)
 *   LIMITER_STORAGE_URI  (legacy redis:// URL — auto-converted)
 */

import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const WINDOW_MS = 15 * 60 * 1000; // 15 minutes
const MAX_REQUESTS = 100;

// ---------------------------------------------------------------------------
// In-memory fallback (local dev / Redis failure)
// ---------------------------------------------------------------------------

interface RateLimitEntry {
  count: number;
  resetTime: number;
}

const rateLimitStore = new Map<string, RateLimitEntry>();

function memoryCheckRateLimit(
  identifier: string,
  maxRequests: number,
  windowMs: number
): { allowed: boolean; remaining: number; resetTime: number; totalLimit: number } {
  const now = Date.now();
  const entry = rateLimitStore.get(identifier);

  if (!entry || now > entry.resetTime) {
    rateLimitStore.set(identifier, { count: 1, resetTime: now + windowMs });
    return { allowed: true, remaining: maxRequests - 1, resetTime: now + windowMs, totalLimit: maxRequests };
  }

  if (entry.count >= maxRequests) {
    return { allowed: false, remaining: 0, resetTime: entry.resetTime, totalLimit: maxRequests };
  }

  entry.count++;
  return { allowed: true, remaining: maxRequests - entry.count, resetTime: entry.resetTime, totalLimit: maxRequests };
}

// Cleanup interval to prevent memory leaks in long-running processes
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
let cleanupInterval: NodeJS.Timeout | null = null;

export function startRateLimitCleanup(): void {
  if (cleanupInterval) return;
  cleanupInterval = setInterval(() => {
    const now = Date.now();
    for (const [key, entry] of rateLimitStore.entries()) {
      if (now > entry.resetTime) rateLimitStore.delete(key);
    }
  }, CLEANUP_INTERVAL_MS);
  if (cleanupInterval.unref) cleanupInterval.unref();
}

export function stopRateLimitCleanup(): void {
  if (cleanupInterval) {
    clearInterval(cleanupInterval);
    cleanupInterval = null;
  }
}

// ---------------------------------------------------------------------------
// Redis client (singleton, lazy-init)
// ---------------------------------------------------------------------------

let redis: Redis | null = null;
let redisAvailable = true; // flipped to false on connection failure

function getRedis(): Redis | null {
  if (!redisAvailable) return null;
  if (redis) return redis;

  const restUrl = process.env.UPSTASH_REDIS_REST_URL;
  const restToken = process.env.UPSTASH_REDIS_REST_TOKEN;

  if (restUrl && restToken) {
    redis = new Redis({ url: restUrl, token: restToken });
    return redis;
  }

  // Parse legacy redis:// URI into Upstash REST format
  // Expected: redis://default:<token>@<host>.upstash.io:6379
  const legacyUri = process.env.LIMITER_STORAGE_URI;
  if (legacyUri) {
    try {
      const parsed = new URL(legacyUri);
      const host = parsed.hostname;
      const token = decodeURIComponent(parsed.password);
      if (host && token) {
        redis = new Redis({ url: `https://${host}`, token });
        return redis;
      }
    } catch {
      // Parsing failed — fall through to memory
    }
  }

  // No Redis config — use memory
  redisAvailable = false;
  return null;
}

// ---------------------------------------------------------------------------
// Upstash Ratelimit instances (cached per unique window config)
// ---------------------------------------------------------------------------

const limiterCache = new Map<string, Ratelimit>();

function getLimiter(maxRequests: number, windowMs: number): Ratelimit | null {
  const client = getRedis();
  if (!client) return null;

  const key = `${maxRequests}:${windowMs}`;
  let limiter = limiterCache.get(key);
  if (limiter) return limiter;

  const windowSec = Math.ceil(windowMs / 1000);
  const windowStr = `${windowSec} s` as `${number} s`;

  limiter = new Ratelimit({
    redis: client,
    limiter: Ratelimit.slidingWindow(maxRequests, windowStr),
    prefix: "rr:rl",
  });

  limiterCache.set(key, limiter);
  return limiter;
}

// ---------------------------------------------------------------------------
// Public API (unchanged signatures)
// ---------------------------------------------------------------------------

export function getClientIP(request: Request): string {
  const forwarded = request.headers.get("x-forwarded-for");
  if (forwarded) return forwarded.split(",")[0].trim();
  const realIP = request.headers.get("x-real-ip");
  if (realIP) return realIP;
  return "unknown";
}

export async function checkRateLimit(
  identifier: string,
  maxRequests?: number,
  windowMs?: number
): Promise<{
  allowed: boolean;
  remaining: number;
  resetTime: number;
  totalLimit: number;
}> {
  const limit = maxRequests ?? MAX_REQUESTS;
  const window = windowMs ?? WINDOW_MS;

  const limiter = getLimiter(limit, window);
  if (limiter) {
    try {
      const result = await limiter.limit(identifier);
      return {
        allowed: result.success,
        remaining: result.remaining,
        resetTime: result.reset,
        totalLimit: result.limit,
      };
    } catch {
      // Redis failed — fall through to memory
    }
  }

  return memoryCheckRateLimit(identifier, limit, window);
}

export function getRateLimitHeaders(result: {
  allowed: boolean;
  remaining: number;
  resetTime: number;
  totalLimit: number;
}): Record<string, string> {
  return {
    "X-RateLimit-Limit": result.totalLimit.toString(),
    "X-RateLimit-Remaining": Math.max(0, result.remaining).toString(),
    "X-RateLimit-Reset": new Date(result.resetTime).toISOString(),
  };
}

export function createRateLimitResponse(
  _identifier: string,
  retryAfterSeconds?: number
): Response {
  const headers = new Headers({
    "Content-Type": "application/json",
    "X-RateLimit-Limit": MAX_REQUESTS.toString(),
    "X-RateLimit-Remaining": "0",
  });

  if (retryAfterSeconds) {
    headers.set("Retry-After", retryAfterSeconds.toString());
  }

  return new Response(
    JSON.stringify({
      error: "Rate limit exceeded. Please try again later.",
      retryAfter: retryAfterSeconds,
    }),
    { status: 429, headers }
  );
}

// Auto-start memory cleanup
startRateLimitCleanup();

export { MAX_REQUESTS, WINDOW_MS };
