/**
 * Rate Limiter Middleware
 */

import { Request, Response, NextFunction } from 'express';
import { RateLimiterRedis, RateLimiterMemory } from 'rate-limiter-flexible';

// Use memory-based rate limiter when Redis is not available
const createRateLimiter = (points: number, duration: number) => {
  try {
    // Try Redis first (if available)
    const { getRedis } = require('../utils/redis');
    const redis = getRedis();

    return new RateLimiterRedis({
      storeClient: redis,
      keyPrefix: 'middleware',
      points,
      duration,
    });
  } catch (error) {
    // Fallback to memory-based limiter
    return new RateLimiterMemory({
      keyPrefix: 'middleware',
      points,
      duration,
    });
  }
};

const generalLimiter = createRateLimiter(300, 60); // 300 requests per minute (accommodates batched foundational context + normal usage)

export const rateLimiter = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  // Skip rate limiting for CORS preflight OPTIONS requests
  if (req.method === 'OPTIONS') {
    next();
    return;
  }

  try {
    const key = req.ip || 'unknown';
    await generalLimiter.consume(key);
    next();
  } catch (rejRes) {
    res.status(429).json({
      error: 'Too Many Requests',
      message: 'Please try again later'
    });
  }
};

// Strict rate limiter for auth endpoints
// Allows 100 requests per minute to accommodate password reset attempts with CAPTCHA flow
const authLimiter = createRateLimiter(100, 60); // 100 requests per minute

export const authRateLimiter = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  try {
    const key = req.ip || 'unknown';
    await authLimiter.consume(key);
    next();
  } catch (rejRes) {
    res.status(429).json({
      error: 'Too Many Requests',
      message: 'Please try again in a minute'
    });
  }
};

// Registration limiter.
//
// authRateLimiter (100/min) is sized for endpoints a legitimate user retries —
// a mistyped password, a reset flow. Account creation is not that: a real person
// registers once, so 100 a minute leaves the endpoint open to bulk signup abuse
// that costs a database write, a company record and a super-admin notification
// every time. An hour window rather than a minute is what makes this bite —
// a burst limiter just paces an attacker instead of stopping them.
//
// Keyed on IP, so a shared office NAT draws from one bucket; 10/hour still
// covers a whole team signing up together while stopping automated abuse.
const registerLimiter = createRateLimiter(10, 3600); // 10 registrations per hour

export const registerRateLimiter = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  if (req.method === 'OPTIONS') {
    next();
    return;
  }

  try {
    const key = req.ip || 'unknown';
    await registerLimiter.consume(key);
    next();
  } catch (rejRes) {
    // The user-facing sentence goes in `error` because that is the field the
    // frontend api client surfaces (it prefers `error` over `message`).
    res.status(429).json({
      error: 'Too many registration attempts from this network. Please try again in an hour.',
      message: 'Too many registration attempts from this network. Please try again in an hour.',
    });
  }
};

// Password-reset limiter.
//
// Two buckets, because the two abuses are different shapes:
//
//   per IP    — one source hammering the endpoint
//   per email — "email bombing": burying someone's inbox in reset links. An
//               attacker rotating IPs slips past an IP-only limit entirely,
//               and it is the victim's inbox that suffers, not the attacker's.
//
// The address is the target rather than the caller, so limiting it is what
// actually protects the person being bombed. Both are hour-length: a real user
// asks for one link, waits for the mail, and clicks it.
const passwordResetIpLimiter = createRateLimiter(5, 3600);     // 5/hour per IP
const passwordResetEmailLimiter = createRateLimiter(3, 3600);  // 3/hour per address

export const passwordResetRateLimiter = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  if (req.method === 'OPTIONS') {
    next();
    return;
  }

  const tooMany = () => {
    // The sentence goes in `error` because that is the field the frontend api
    // client surfaces (it prefers `error` over `message`).
    res.status(429).json({
      error: 'Too many password reset requests. Please wait an hour before trying again.',
      message: 'Too many password reset requests. Please wait an hour before trying again.',
    });
  };

  try {
    await passwordResetIpLimiter.consume(req.ip || 'unknown');
  } catch {
    tooMany();
    return;
  }

  const email = typeof req.body?.email === 'string' ? req.body.email.toLowerCase().trim() : '';
  if (email) {
    try {
      await passwordResetEmailLimiter.consume(`pwreset:${email}`);
    } catch {
      tooMany();
      return;
    }
  }

  next();
};

// Two-factor verification limiter.
//
// Much tighter than authRateLimiter (100/min) because the search space is far
// smaller: a 6-digit code is one in a million, so a few hundred attempts a
// minute is a real threat where the same volume against a password is not.
//
// Keyed on IP here; twoFactorService additionally counts failures per account in
// the database. Both are needed — this one stops a burst from one source, the
// database counter stops a patient attacker spreading attempts across IPs, and
// it survives restarts and works across instances.
const twoFactorLimiter = createRateLimiter(10, 300); // 10 attempts per 5 minutes

export const twoFactorRateLimiter = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  if (req.method === 'OPTIONS') {
    next();
    return;
  }

  try {
    // Prefer the authenticated user when there is one (self-service endpoints);
    // fall back to IP for the login challenge, where there is no session yet.
    const key = (req as any).user?.id || req.ip || 'unknown';
    await twoFactorLimiter.consume(key);
    next();
  } catch (rejRes) {
    res.status(429).json({
      error: 'Too Many Requests',
      message: 'Too many verification attempts. Please wait a few minutes and try again.',
    });
  }
};

// OTP delivery limiter.
//
// Separate from twoFactorRateLimiter because sending and verifying fail
// differently: a wrong code costs nothing, whereas every send costs an email —
// real money with most providers, and a way to use the platform to spam a third
// party's inbox. Tighter than verification for that reason. The per-challenge
// cooldown and resend cap in the policy sit on top of this.
const otpSendLimiter = createRateLimiter(5, 300); // 5 sends per 5 minutes

export const otpSendRateLimiter = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  if (req.method === 'OPTIONS') {
    next();
    return;
  }

  try {
    const key = (req as any).user?.id || req.ip || 'unknown';
    await otpSendLimiter.consume(key);
    next();
  } catch (rejRes) {
    res.status(429).json({
      error: 'Too Many Requests',
      message: 'Too many verification codes requested. Please wait a few minutes and try again.',
    });
  }
};

// Claude-specific rate limiter: 30 requests per minute per authenticated user
// Keyed on user ID (not IP) since these are authenticated endpoints
const claudeLimiter = createRateLimiter(30, 60); // 30 requests per minute per user

export const claudeRateLimiter = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
  try {
    // Use authenticated user ID as the rate limit key; fall back to IP for unauthenticated requests
    const key = (req as any).user?.id || req.ip || 'unknown';
    await claudeLimiter.consume(key);
    next();
  } catch (rejRes) {
    res.status(429).json({
      error: 'Too Many Requests',
      message: 'Claude API rate limit reached. Please try again in a minute.',
    });
  }
};
