/**
 * Rate Limiting Middleware
 *
 * Redis-based rate limiting with tier-based limits
 */

import { Request, Response, NextFunction } from 'express';
import rateLimit, { RateLimitRequestHandler } from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';
import { config } from '../config';
import { SubscriptionPlan } from '../types';
import { tierService } from '../services/tierService';
import { logger } from '../utils/logger';
import { errorResponse } from '../utils/errorHandler';

// Create Redis client
const redis = new Redis({
  host: config.redisHost,
  port: config.redisPort,
  password: config.redisPassword,
  db: config.redisDb,
  retryStrategy: (times) => {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
});

redis.on('error', (error) => {
  logger.error('Redis connection error', { error });
});

redis.on('connect', () => {
  logger.info('Redis connected for rate limiting');
});

/**
 * Create a rate limiter with specified window and max requests
 */
export function createRateLimiter(
  windowMs: number,
  max: number | ((_request: Request) => number | Promise<number>)
): RateLimitRequestHandler {
  return rateLimit({
    windowMs,
    max,
    standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
    legacyHeaders: false, // Disable `X-RateLimit-*` headers
    store: new RedisStore({
      // @ts-ignore - Redis type compatibility
      sendCommand: (...args: string[]) => redis.call(...args),
      prefix: 'rl:',
    }),
    keyGenerator: (req: Request) => {
      // Use user ID or API key ID for rate limiting
      if (req.user) {
        return `user:${req.user.userId}`;
      } else if (req.apiKey) {
        return `apikey:${req.apiKey.id}`;
      } else {
        // Fallback to IP address for unauthenticated requests
        return req.ip || req.socket.remoteAddress || 'unknown';
      }
    },
    handler: (req: Request, res: Response) => {
      logger.warn('Rate limit exceeded', {
        userId: req.user?.userId,
        apiKeyId: req.apiKey?.id,
        ip: req.ip,
        path: req.path,
      });

      res.status(429).json(errorResponse('RATE_LIMIT_EXCEEDED', 'Too many requests. Please slow down and try again later.'));
    },
  });
}

/**
 * Pre-created per-plan rate limiter instances.
 * ALL instances are created at module load (not inside request handlers)
 * to avoid the express-rate-limit ERR_ERL_CREATED_IN_REQUEST_HANDLER error.
 */
const requestRateLimits = new WeakMap<Request, Promise<number>>();

export function resolveRequestRateLimit(req: Request): Promise<number> {
  const pendingLimit = requestRateLimits.get(req);
  if (pendingLimit) {
    return pendingLimit;
  }
  if (!req.user?.plugin) {
    return Promise.reject(new Error('Authenticated plugin context is required for rate limiting'));
  }
  const resolvedLimit = tierService.getRateLimit(
    req.user.plan ?? SubscriptionPlan.STARTER,
    req.user.plugin
  );
  requestRateLimits.set(req, resolvedLimit);
  return resolvedLimit;
}

const tierLimiter = createRateLimiter(
  60 * 1000,
  resolveRequestRateLimit
);

/**
 * API rate limiter middleware that applies tier-based limits.
 * Must be placed after authentication middleware.
 */
export async function apiRateLimiter(req: Request, res: Response, next: NextFunction): Promise<void> {
  try {
    const limit = await resolveRequestRateLimit(req);

    if (limit === 0) {
      next();
      return;
    }

    tierLimiter(req, res, next);
  } catch (error) {
    next(error);
  }
}

/**
 * General rate limiter for public endpoints (e.g., registration, login)
 */
export const publicRateLimiter = createRateLimiter(
  15 * 60 * 1000, // 15 minutes
  100 // 100 requests per 15 minutes
);

/**
 * Strict rate limiter for sensitive endpoints (e.g., password reset)
 */
export const strictRateLimiter = createRateLimiter(
  60 * 60 * 1000, // 1 hour
  5 // 5 requests per hour
);

/**
 * Rate limiter for admin endpoints
 */
export const adminRateLimiter = createRateLimiter(
  60 * 1000, // 1 minute
  300 // 300 requests per minute
);
