/**
 * Authentication & Authorization Middleware
 *
 * JWT-based authentication with role-based access control
 */

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { JwtPayload, AuthenticationError } from '../types';
import { logger } from '../utils/logger';
import { prisma } from '../database/prisma.client';
import type { UserRole } from '@prisma/client';
import { isTokenBlacklisted } from '../database/redis.client';

/**
 * Extend Express Request to include user
 */
declare global {
  namespace Express {
    interface Request {
      user?: JwtPayload;
      correlationId?: string;
    }
  }
}

/**
 * Permission mappings by role
 */
const ROLE_PERMISSIONS: Record<UserRole, string[]> = {
  STUDENT: ['read:modules', 'read:exercises', 'create:attempts', 'read:progress', 'read:ranking'],
  TEACHER: ['read:modules', 'read:exercises', 'create:attempts', 'read:progress', 'read:ranking',
            'create:exercises', 'update:exercises', 'read:users', 'manage:modules'],
  ADMIN: ['read:modules', 'read:exercises', 'create:attempts', 'read:progress', 'read:ranking',
          'create:exercises', 'update:exercises', 'delete:exercises', 'read:users', 'manage:users',
          'manage:modules', 'admin:all'],
};

/**
 * Verify JWT token
 */
function verifyToken(token: string): JwtPayload {
  try {
    const secret = process.env.JWT_SECRET;
    if (!secret) {
      throw new Error('JWT_SECRET not configured');
    }

    const decoded = jwt.verify(token, secret) as JwtPayload;
    return decoded;
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      throw new AuthenticationError('Token expired');
    } else if (error instanceof jwt.JsonWebTokenError) {
      throw new AuthenticationError('Invalid token');
    }
    throw error;
  }
}

/**
 * Extract token from Authorization header
 */
function extractToken(req: Request): string | null {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    return null;
  }

  // Bearer token format
  const parts = authHeader.split(' ');
  if (parts.length === 2 && parts[0] === 'Bearer' && parts[1]) {
    return parts[1];
  }

  return null;
}

/**
 * Authentication middleware
 * Verifies JWT token and attaches user to request
 */
export async function authenticate(req: Request, res: Response, next: NextFunction): Promise<void> {
  try {
    const token = extractToken(req);

    if (!token) {
      throw new AuthenticationError('No token provided');
    }

    const user = verifyToken(token);
    req.user = user;

    // Check if token is blacklisted
    const blacklisted = await isTokenBlacklisted(token);
    if (blacklisted) {
      logger.warn({ userId: user.userId }, 'Attempted to use blacklisted token');
      res.status(401).json({
        success: false,
        error: {
          code: 'TOKEN_REVOKED',
          message: 'Token has been revoked',
        },
      });
      return;
    }

    logger.debug({
      userId: user.userId,
      email: user.email,
    }, 'User authenticated');

    next();
  } catch (error) {
    if (error instanceof AuthenticationError) {
      res.status(401).json({
        success: false,
        error: {
          code: error.code,
          message: error.message,
        },
      });
    } else {
      logger.error({ error }, 'Error checking token blacklist');
      // Continue on Redis error (fail-open for availability)
      next(error);
    }
  }
}

/**
 * Optional authentication
 * Attaches user if token is present, but doesn't require it
 */
export function optionalAuthenticate(req: Request, _res: Response, next: NextFunction): void {
  try {
    const token = extractToken(req);

    if (token) {
      const user = verifyToken(token);
      req.user = user;
    }

    next();
  } catch (error) {
    // Continue without authentication on error
    next();
  }
}

/**
 * Authorization middleware factory
 * Checks if user has required permissions based on their role
 */
export function authorize(requiredPermissions?: string[]) {
  return (req: Request, res: Response, next: NextFunction): void => {
    if (!req.user) {
      res.status(401).json({
        success: false,
        error: {
          code: 'AUTHENTICATION_ERROR',
          message: 'Authentication required',
        },
      });
      return;
    }

    // If no permissions required, just being authenticated is enough
    if (!requiredPermissions || requiredPermissions.length === 0) {
      return next();
    }

    // Check if user has required permissions based on their role
    const userPermissions = getUserPermissions(req.user.userId, req.user.role as UserRole);

    const hasPermission = requiredPermissions.some(
      permission => userPermissions.includes(permission)
    );

    if (!hasPermission) {
      res.status(403).json({
        success: false,
        error: {
          code: 'AUTHORIZATION_ERROR',
          message: 'Insufficient permissions',
          required: requiredPermissions,
        },
      });
      return;
    }

    next();
  };
}

/**
 * Get user permissions based on their role
 * Requires userId to be passed from JWT payload
 */
function getUserPermissions(_userId: string, role?: UserRole): string[] {
  // If role is provided (from JWT), use it directly
  if (role && ROLE_PERMISSIONS[role]) {
    return ROLE_PERMISSIONS[role];
  }

  // Fallback to STUDENT permissions if role is unknown
  return ROLE_PERMISSIONS.STUDENT;
}

/**
 * Admin-only middleware
 */
export async function requireAdmin(req: Request, res: Response, next: NextFunction): Promise<void> {
  try {
    if (!req.user) {
      res.status(401).json({
        success: false,
        error: {
          code: 'AUTHENTICATION_ERROR',
          message: 'Authentication required',
        },
      });
      return;
    }

    // Check if user is admin by role or by email in ADMIN_EMAILS
    const isAdminUser = await isAdmin(req.user.userId, req.user.email, req.user.role);

    if (!isAdminUser) {
      res.status(403).json({
        success: false,
        error: {
          code: 'AUTHORIZATION_ERROR',
          message: 'Admin access required',
        },
      });
      return;
    }

    next();
  } catch (error) {
    logger.error({ error, userId: req.user?.userId }, 'Error checking admin status');
    res.status(500).json({
      success: false,
      error: {
        code: 'INTERNAL_ERROR',
        message: 'Authorization check failed',
      },
    });
  }
}

/**
 * Check if user is admin
 * Checks both role in database and email in ADMIN_EMAILS env var
 */
async function isAdmin(userId: string, email?: string, role?: string): Promise<boolean> {
  // Check if role is already ADMIN in JWT
  if (role === 'ADMIN') {
    return true;
  }

  // Check by email first (from ADMIN_EMAILS environment variable)
  const adminEmails = process.env.ADMIN_EMAILS?.split(',').map(e => e.trim()) || [];
  if (email && adminEmails.includes(email)) {
    return true;
  }

  // Check by role in database
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: { role: true, email: true },
  });

  if (!user) {
    return false;
  }

  // Admin role always has admin access
  if (user.role === 'ADMIN') {
    return true;
  }

  // Also check email from database against ADMIN_EMAILS
  return adminEmails.includes(user.email);
}

/**
 * Rate limit key generator based on user
 */
export function getUserRateLimitKey(req: Request): string {
  if (req.user) {
    return `ratelimit:user:${req.user.userId}`;
  }

  // Fallback to IP address
  const ip = req.ip || req.socket.remoteAddress || 'unknown';
  return `ratelimit:ip:${ip}`;
}
