import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { GlobalService } from 'src/modules/backend/global/global.service';
import { RedisService } from 'src/modules/backend/redis/redis.service';

/**
 * RolePermissionsMiddleware - Middleware for role-based access control (RBAC) validation
 *
 * This middleware intercepts requests to verify if a user's role permissions have been
 * modified since they logged in. It's a security measure to prevent users from accessing
 * resources with outdated permissions, ensuring that permission changes take immediate effect.
 *
 * Typical use case: When an admin modifies a user's permissions, this middleware ensures
 * the user can't continue using old permissions on their next request.
 *
 * Workflow:
 * 1. Extract session ID from request (cookie or Authorization header)
 * 2. Validate session existence and expiration via Redis
 * 3. Extract user ID from session data
 * 4. Verify if user's role permissions have been modified
 * 5. Block request (424) if permissions modified, otherwise allow
 *
 * @example
 * // Apply to specific routes in a module
 * export class AdminModule implements NestModule {
 *   configure(consumer: MiddlewareConsumer) {
 *     consumer
 *       .apply(RolePermissionsMiddleware)
 *       .forRoutes('admin/*', 'settings/*');
 *   }
 * }
 */
@Injectable()
export class RolePermissionsMiddleware implements NestMiddleware {
  /**
   * Dependency injection of services required for session and permission validation
   *
   * @param globalService - Service for checking role permission modifications
   * @param redisService - Service for session data retrieval from Redis cache
   */
  constructor(
    private readonly globalService: GlobalService,
    private readonly redisService: RedisService,
  ) {}

  /**
   * Main middleware execution method
   *
   * Processes incoming requests to validate session and check for permission modifications
   * Returns appropriate HTTP responses if validation fails, otherwise calls next()
   *
   * @param req - Express Request object
   * @param res - Express Response object
   * @param next - NextFunction to continue processing if validation passes
   *
   * @returns Either a JSON error response or calls next() to continue
   */
  async use(req: Request, res: Response, next: NextFunction) {
    // Step 1: Extract session identifier from the request
    const sessionId = this.extractSessionId(req);

    // Validation: Session ID must be present
    if (!sessionId) {
      return res.status(401).json({
        message: 'Session ID not found',
        code: 'SESSION_ID_MISSING',
      });
    }

    // Step 2: Retrieve session data from Redis cache
    const sessionData = await this.redisService.getSession(sessionId);

    // Validation: Session must exist and not be expired
    if (!sessionData) {
      return res.status(401).json({
        message: 'Invalid or expired session',
        code: 'SESSION_INVALID',
      });
    }

    // Step 3: Extract user ID from session data
    const userId = sessionData?.user?.id;

    // Validation: User ID must be present in session
    if (!userId) {
      return res.status(401).json({
        message: 'User ID not found in session',
        code: 'USER_ID_MISSING',
      });
    }

    // Step 4: Verify if role permissions have been modified since session creation
    const systemConfig =
      await this.globalService.verifyRolePermissionsModification(userId);

    // Step 5: If permissions were modified, block the request
    if (systemConfig === true) {
      return res.status(424).json({
        message: 'Role permissions modified. Please log in again.',
        code: 'PERMISSIONS_MODIFIED',
      });
    }

    // All validations passed - continue to the next middleware/controller
    next();
  }

  /**
   * Extracts session identifier from various request sources
   *
   * Supports multiple authentication methods in order of priority:
   * 1. Cookie-based sessions (session_id cookie)
   * 2. Bearer token in Authorization header
   *
   * @param request - Express Request object
   * @returns Session ID string if found, null otherwise
   *
   * @note Consider using a dedicated authentication strategy for more complex scenarios
   */
  private extractSessionId(request: any): string | null {
    // Method 1: Extract from cookies (traditional session-based authentication)
    const cookies = request.headers.cookie;

    if (cookies) {
      // Find the session_id cookie among all cookies
      const sessionCookie = cookies
        .split(';')
        .find((cookie: string) => cookie.trim().startsWith('session_id='));

      if (sessionCookie) {
        // Extract value after '='
        return sessionCookie.split('=')[1];
      }
    }

    // Method 2: Extract from Authorization header (Bearer token)
    const authHeader = request.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) {
      // Remove 'Bearer ' prefix to get the token
      return authHeader.substring(7);
    }

    // No session identifier found
    return null;
  }
}
