/**
 * Authentication Guards
 * Middleware for protecting routes
 */

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '../../config';
import { db } from '../../shared/database';
import { UnauthorizedException, ForbiddenException } from '../filters/http-exception.filter';
import { logger } from '../../shared/utils/logger';
import type { AuthenticatedUser, UserRole } from '../../modules/auth/types/auth.types';
import { UserRoleEnum } from '../../modules/auth/types/auth.types';

// Authenticated request type - user is required and has all auth properties
export interface AuthenticatedRequest extends Request {
  user: AuthenticatedUser;
}

interface JwtPayload {
  sub: string;
  email: string;
  role: string;
  sessionId?: string;
  iat: number;
  exp: number;
}

/**
 * Require authentication
 * Validates JWT token and attaches user to request
 */
export async function requireAuth(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw new UnauthorizedException('No token provided', 'NO_TOKEN');
    }

    const token = authHeader.substring(7);

    // Verify token
    const decoded = jwt.verify(token, config.jwt.accessSecret) as JwtPayload;

    // Check if user exists and is active
    const userResult = await db.query<{
      id: string;
      email: string;
      role: string;
      status: string;
    }>(
      'SELECT id, email, role, status FROM users WHERE id = $1',
      [decoded.sub]
    );

    if (userResult.rows.length === 0) {
      throw new UnauthorizedException('User not found', 'USER_NOT_FOUND');
    }

    const user = userResult.rows[0];

    if (user.status !== 'active') {
      throw new UnauthorizedException('Account is not active', 'ACCOUNT_INACTIVE');
    }

    // Attach user to request (partial user info from guard, full info loaded by middleware)
    (req as AuthenticatedRequest).user = {
      id: user.id,
      email: user.email,
      role: user.role as UserRole,
    } as AuthenticatedUser;

    next();
  } catch (error) {
    if (error instanceof UnauthorizedException) {
      next(error);
    } else if ((error as Error).name === 'JsonWebTokenError') {
      next(new UnauthorizedException('Invalid token', 'INVALID_TOKEN'));
    } else if ((error as Error).name === 'TokenExpiredError') {
      next(new UnauthorizedException('Token expired', 'TOKEN_EXPIRED'));
    } else {
      logger.error('Auth guard error', { error: (error as Error).message });
      next(new UnauthorizedException('Authentication failed', 'AUTH_FAILED'));
    }
  }
}

/**
 * Optional authentication
 * Attaches user if token is valid, but doesn't require it
 */
export async function optionalAuth(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return next();
    }

    const token = authHeader.substring(7);
    const decoded = jwt.verify(token, config.jwt.accessSecret) as JwtPayload;

    const userResult = await db.query<{
      id: string;
      email: string;
      role: string;
      status: string;
    }>(
      'SELECT id, email, role, status FROM users WHERE id = $1 AND status = $2',
      [decoded.sub, 'active']
    );

    if (userResult.rows.length > 0) {
      const user = userResult.rows[0];
      (req as AuthenticatedRequest).user = {
        id: user.id,
        email: user.email,
        role: user.role as UserRole,
      } as AuthenticatedUser;
    }

    next();
  } catch {
    // Ignore errors for optional auth
    next();
  }
}

/**
 * Require specific roles
 * Must be used after requireAuth
 */
export function requireRoles(...roles: UserRoleEnum[]) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const authReq = req as AuthenticatedRequest;

    if (!authReq.user) {
      return next(new UnauthorizedException('Authentication required'));
    }

    if (!roles.includes(authReq.user.role as UserRoleEnum)) {
      return next(new ForbiddenException('Insufficient permissions'));
    }

    next();
  };
}

/**
 * Require admin role
 */
export const requireAdmin = requireRoles(UserRoleEnum.ADMIN, UserRoleEnum.SUPER_ADMIN);

/**
 * Require instructor role
 */
export const requireInstructor = requireRoles(
  UserRoleEnum.INSTRUCTOR,
  UserRoleEnum.ADMIN,
  UserRoleEnum.SUPER_ADMIN
);

/**
 * Resource ownership guard
 * Checks if user owns the resource or is admin
 */
export function requireOwnership(
  resourceUserIdExtractor: (req: Request) => string | Promise<string>
) {
  return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const authReq = req as AuthenticatedRequest;

      if (!authReq.user) {
        return next(new UnauthorizedException('Authentication required'));
      }

      // Admins can access any resource
      if ([UserRoleEnum.ADMIN, UserRoleEnum.SUPER_ADMIN].includes(authReq.user.role as UserRoleEnum)) {
        return next();
      }

      const resourceUserId = await resourceUserIdExtractor(req);

      if (authReq.user.id !== resourceUserId) {
        return next(new ForbiddenException('You do not have access to this resource'));
      }

      next();
    } catch (error) {
      next(error);
    }
  };
}

/**
 * Rate limit by user
 * Applies stricter rate limiting per user
 */
export function userRateLimit(maxRequests: number, windowMs: number) {
  const requests = new Map<string, { count: number; resetAt: number }>();

  return (req: Request, res: Response, next: NextFunction): void => {
    const authReq = req as AuthenticatedRequest;
    const key = authReq.user?.id || req.ip || 'anonymous';
    const now = Date.now();

    let userData = requests.get(key);

    if (!userData || now > userData.resetAt) {
      userData = { count: 0, resetAt: now + windowMs };
      requests.set(key, userData);
    }

    userData.count++;

    if (userData.count > maxRequests) {
      res.set('Retry-After', String(Math.ceil((userData.resetAt - now) / 1000)));
      return next(
        new ForbiddenException(
          `Rate limit exceeded. Try again in ${Math.ceil((userData.resetAt - now) / 1000)} seconds`
        )
      );
    }

    next();
  };
}
