Source: index.js

/**
 * @fileoverview myFlix API - A RESTful API for movie information and user management
 * @description This API provides endpoints for managing movies, users, and user favorites.
 * It includes authentication, authorization, and data validation features.
 * @author Sourav Das
 * @version 1.0.0
 */

const path = require('path');
const express = require("express");
const morgan = require("morgan");
const bodyParser = require("body-parser");
const uuid = require("uuid");
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const Models = require("./models.js");
const cors = require("cors");
const { check, validationResult } = require('express-validator');

/**
 * List of allowed origins for CORS policy
 * @type {string[]}
 */
let allowedOrigins = [
  'http://localhost:4200',
  'http://localhost:8080', 
  'http://testsite.com', 
  'http://localhost:1234', 
  'https://my-flix-clients.netlify.app',
  'https://my-flix-clients.netlify.app/',
  'https://souravdas090300.github.io',
  'https://souravdas090300.github.io/myFlix-Angular-client'
];

/**
 * Express application instance
 * @type {Express}
 */
const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(morgan("common"));
app.use("/documentation", express.static(path.join(__dirname, "out")));
app.use(express.static("public"));

/**
 * Configure CORS (Cross-Origin Resource Sharing)
 * Allows requests only from specified origins
 */
app.use(cors({
  origin: (origin, callback) => {
    if (!origin) return callback(null, true);
    if (allowedOrigins.indexOf(origin) === -1) { // If a specific origin isn't found on the list of allowed origins
      let message = 'The CORS policy for this application doesn\'t allow access from origin ' + origin;
      return callback(new Error(message), false);
    }
    return callback(null, true);
  }
}));

const passport = require("passport");
let auth = require("./auth")(app);
require("./passport"); // Import the passport configuration

/**
 * Movie model from mongoose schemas
 * @type {mongoose.Model}
 */
const Movies = Models.Movie;

/**
 * User model from mongoose schemas
 * @type {mongoose.Model}
 */
const Users = Models.User;

/**
 * Connect to MongoDB database using mongoose
 * Uses CONNECTION_URI environment variable
 */
mongoose.connect(process.env.CONNECTION_URI);

/**
 * @function getWelcome
 * @description - Welcome message endpoint
 * @route GET /
 * @returns {string} - Welcome message with usage instructions
 * @example
 * // Response data format
 * "Welcome to myFlix API! Use /movies for movies or /users for user operations."
 */
app.get("/", (req, res) => {
  res.send(
    "Welcome to myFlix API! Use /movies for movies or /users for user operations."
  );
});

/**
 * @function getAllMovies
 * @description - Return a list of ALL movies to the user
 * @route GET /movies
 * @returns {Array} - Array of all movie objects
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * [
 *   {
 *     "_id": "507f1f77bcf86cd799439011",
 *     "Title": "The Shawshank Redemption",
 *     "Description": "Two imprisoned men bond over years...",
 *     "Genre": {
 *       "Name": "Drama",
 *       "Description": "Drama genre description"
 *     },
 *     "Director": {
 *       "Name": "Frank Darabont",
 *       "Bio": "Director biography",
 *       "Birth": "10/10/1970",
 *       "Death": "10/10/2025"
 *     },
 *     "Actors": ["Tim Robbins", "Morgan Freeman"],
 *     "ImagePath": "shawshank.png",
 *     "Featured": true
 *   }
 * ]
 */
app.get(
  "/movies",
  passport.authenticate("jwt", { session: false }),
  async (req, res) => {
    try {
      const movies = await Movies.find();
      res.status(200).json(movies);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function getMovieByTitle
 * @description - Return data about a single movie by title to the user
 * @route GET /movies/:title
 * @param {Query_Parameters} - :title
 * @returns {object} - Movie object with description, genre, director, image URL, featured status
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "_id": "507f1f77bcf86cd799439011",
 *   "Title": "The Shawshank Redemption",
 *   "Description": "Two imprisoned men bond over years...",
 *   "Genre": {
 *     "Name": "Drama",
 *     "Description": "Drama genre description"
 *   },
 *   "Director": {
 *     "Name": "Frank Darabont",
 *     "Bio": "Director biography",
 *     "Birth": "10/10/1970",
 *     "Death": "10/10/2025"
 *   },
 *   "ImagePath": "shawshank.png",
 *   "Featured": true
 * }
 */
app.get(
  "/movies/:title",
  [
    passport.authenticate("jwt", { session: false }),
    check('title', 'Title is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      const movie = await Movies.findOne({ Title: req.params.title });
      if (!movie) {
        return res.status(404).json({ error: "Movie not found" });
      }
      res.status(200).json(movie);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function getMovieById
 * @description - Get movie by ID
 * @route GET /movies/id/:id
 * @param {Query_Parameters} - :id
 * @returns {object} - Movie object
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "_id": "507f1f77bcf86cd799439011",
 *   "Title": "The Shawshank Redemption",
 *   "Description": "Two imprisoned men bond over years...",
 *   "Genre": {
 *     "Name": "Drama",
 *     "Description": "Drama genre description"
 *   },
 *   "Director": {
 *     "Name": "Frank Darabont",
 *     "Bio": "Director biography"
 *   },
 *   "ImagePath": "shawshank.png",
 *   "Featured": true
 * }
 */
// Get movie by ID
app.get(
  "/movies/id/:id",
  [
    passport.authenticate("jwt", { session: false }),
    check('id', 'Invalid movie ID').isMongoId()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      const movie = await Movies.findById(req.params.id);
      if (!movie) {
        return res.status(404).json({ error: "Movie not found" });
      }
      res.status(200).json(movie);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function getAllGenres
 * @description - Get all genres
 * @route GET /genres
 * @returns {Array} - Array of all genre objects
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * [
 *   {
 *     "_id": "Drama",
 *     "description": "Drama is a category of narrative fiction..."
 *   }
 * ]
 */
// Get all genres
app.get(
  "/genres",
  passport.authenticate("jwt", { session: false }),
  async (req, res) => {
    try {
      const genres = await Movies.aggregate([
        {
          $group: {
            _id: "$Genre.Name",
            description: { $first: "$Genre.Description" },
          },
        },
      ]);
      res.status(200).json(genres);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function getGenreByName
 * @description - Return data about a genre (description) by name/title
 * @route GET /genres/:name
 * @param {Query_Parameters} - :name
 * @returns {object} - Genre object with name and description
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "Name": "Drama",
 *   "Description": "Drama is a category of narrative fiction..."
 * }
 */
app.get(
  "/genres/:name",
  [
    passport.authenticate("jwt", { session: false }),
    check('name', 'Genre name is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      const movie = await Movies.findOne({ "Genre.Name": req.params.name });
      if (!movie) {
        return res.status(404).json({ error: "Genre not found" });
      }
      const genre = movie.Genre;
      res.status(200).json(genre);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function getDirectorByName
 * @description - Return data about a director (bio, birth year, death year) by name
 * @route GET /directors/:name
 * @param {Query_Parameters} - :name
 * @returns {object} - Director object with name, bio, birth year, death year
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "name": "Frank Darabont",
 *   "bio": "Director biography",
 *   "birth": "10/10/1970",
 *   "death": "10/10/2025",
 *   "movies": "The Shawshank Redemption"
 * }
 */
app.get(
  "/directors/:name",
  [
    passport.authenticate("jwt", { session: false }),
    check('name', 'Director name is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      const movie = await Movies.findOne({ "Director.Name": req.params.name });
      if (!movie) {
        return res.status(404).json({ error: "Director not found" });
      }
      res.status(200).json({
        name: movie.Director.Name,
        bio: movie.Director.Bio,
        birth: movie.Director.Birth,
        death: movie.Director.Death,
        movies: movie.Title,
      });
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function getAllActors
 * @description - Get all unique actors
 * @route GET /actors
 * @returns {Array} - Array of actor names
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * ["Tim Robbins", "Morgan Freeman", "Tom Hanks"]
 */
// Get all unique actors
app.get(
  "/actors",
  passport.authenticate("jwt", { session: false }),
  async (req, res) => {
    try {
      const actors = await Movies.aggregate([
        { $unwind: "$Actors" },
        { $group: { _id: "$Actors" } },
        { $sort: { _id: 1 } },
      ]);
      const actorNames = actors.map((a) => a._id);
      res.status(200).json(actorNames);
    } catch (error) {
      console.error("Error fetching actors:", error);
      res.status(500).json({ error: "Internal server error" });
    }
  }
);

/**
 * @function getAllActresses
 * @description - Get all unique actress names
 * @route GET /actresses
 * @returns {object} - Object containing array of actress names
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "actresses": ["Actress 1", "Actress 2"]
 * }
 */
// Get all unique actress names
app.get(
  "/actresses",
  passport.authenticate("jwt", { session: false }),
  async (req, res) => {
    try {
      const actresses = await Movies.aggregate([
        { $unwind: "$Actresses" }, // Fixed: should be "Actresses" not "actresses"
        {
          $group: {
            _id: "$Actresses",
            count: { $sum: 1 },
          },
        },
        { $sort: { _id: 1 } },
      ]);
      res.status(200).json({
        actresses: actresses.map((a) => a._id),
      });
    } catch (error) {
      console.error("Error fetching actresses:", error);
      res.status(500).json({
        error: "Failed to fetch actresses",
        details: error.message,
      });
    }
  }
);

// === USER ROUTES ===

/**
 * @function getAllUsers
 * @description - Get all users (should be protected and restricted to admins)
 * @route GET /users
 * @returns {Array} - Array of all user objects
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * [
 *   {
 *     "_id": "507f1f77bcf86cd799439011",
 *     "Username": "johndoe",
 *     "Email": "john@example.com",
 *     "Birthday": "1990-01-01",
 *     "FavoriteMovies": []
 *   }
 * ]
 */
// Get all users (should be protected and restricted to admins)
app.get(
  "/users",
  passport.authenticate("jwt", { session: false }),
  async (req, res) => {
    try {
      const users = await Users.find();
      res.status(200).json(users);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function registerUser
 * @description - Register a new user account
 * @route POST /users
 * @param {Request_Body} - JSON object
 * @returns {object} - Created user object
 * @example
 * // Request data format
 * {
 *   "Username": "johndoe",
 *   "Password": "password123",
 *   "Email": "john@example.com",
 *   "Birthday": "1990-01-01"
 * }
 * @example
 * // Response data format
 * {
 *   "_id": "507f1f77bcf86cd799439011",
 *   "Username": "johndoe",
 *   "Email": "john@example.com",
 *   "Birthday": "1990-01-01",
 *   "FavoriteMovies": []
 * }
 */
app.post('/users',
  [
    check('Username', 'Username is required').isLength({ min: 5 }),
    check('Username', 'Username contains non alphanumeric characters - not allowed.').isAlphanumeric(),
    check('Password', 'Password is required').not().isEmpty(),
    check('Email', 'Email does not appear to be valid').isEmail()
  ], async (req, res) => {

    let errors = validationResult(req);

    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }

    let hashedPassword = Users.hashPassword(req.body.Password);
    await Users.findOne({ Username: req.body.Username })
      .then((user) => {
        if (user) {
          return res.status(400).json({ error: req.body.Username + ' already exists' });
        } else {
          Users
            .create({
              Username: req.body.Username,
              Password: hashedPassword,
              Email: req.body.Email,
              Birthday: req.body.Birthday
            })
            .then((user) => { res.status(201).json(user) })
            .catch((error) => {
              console.error(error);
              res.status(500).json({ error: 'Error: ' + error });
            });
        }
      })
      .catch((error) => {
        console.error(error);
        res.status(500).json({ error: 'Error: ' + error });
      });
  });

/**
 * @function updateUser
 * @description - Updates the logged in user's information
 * @route PUT /users/:Username
 * @param {Query_Parameters} - :Username
 * @param {Request_Body} - JSON object
 * @param {Response} - JSON object
 * @param {authentication} - Bearer token (JWT)
 * @returns {object} - Updated user object
 * @example
 * // Request data format
 * {
 *   "Username": "johnsmith",
 *   "Password": "newpassword123",
 *   "Email": "johnsmith@example.com",
 *   "Birthday": "1990-01-01"
 * }
 * @example
 * // Response data format
 * {
 *   "_id": "507f1f77bcf86cd799439011",
 *   "Username": "johnsmith",
 *   "Email": "johnsmith@example.com",
 *   "Birthday": "1990-01-01",
 *   "FavoriteMovies": ["507f1f77bcf86cd799439012"]
 * }
 */
app.put(
  "/users/:Username",
  [
    passport.authenticate("jwt", { session: false }),
    check('Username', 'Username is required').isLength({ min: 5 }),
    check('Username', 'Username contains non alphanumeric characters - not allowed.').isAlphanumeric(),
    check('Password', 'Password is required').not().isEmpty(),
    check('Email', 'Email does not appear to be valid').isEmail()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    if (req.user.Username !== req.params.Username) {
      return res.status(400).json({ error: "Permission denied" });
    }

    let hashedPassword = Users.hashPassword(req.body.Password);
    await Users.findOneAndUpdate(
      { Username: req.params.Username },
      {
        $set: {
          Username: req.body.Username,
          Password: hashedPassword,
          Email: req.body.Email,
          Birthday: req.body.Birthday,
        },
      },
      { new: true }
    )
      .then((updatedUser) => {
        res.json(updatedUser);
      })
      .catch((err) => {
        console.log(err);
        res.status(500).json({ error: "Error: " + err });
      });
  }
);


/**
 * @function getUserByUsername
 * @description - Get user by username
 * @route GET /users/:username
 * @param {Query_Parameters} - :username
 * @returns {object} - User object
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "_id": "507f1f77bcf86cd799439011",
 *   "Username": "johndoe",
 *   "Email": "john@example.com",
 *   "Birthday": "1990-01-01",
 *   "FavoriteMovies": ["507f1f77bcf86cd799439012"]
 * }
 */
// Get user by username
app.get(
  "/users/:username",
  [
    passport.authenticate("jwt", { session: false }),
    check('username', 'Username is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      if (req.user.Username !== req.params.username && !req.user.isAdmin) {
        return res.status(403).json({ error: "Not authorized to view this user" });
      }
      const user = await Users.findOne({ Username: req.params.username });
      if (!user) {
        return res.status(404).json({ error: "User not found" });
      }
      res.status(200).json(user);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function getUserFavorites
 * @description - Get user's favorite movies with full movie details
 * @route GET /users/:username/favorites
 * @param {Query_Parameters} - :username
 * @returns {object} - Object containing username, favorite movies array, and count
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "username": "johndoe",
 *   "favoriteMovies": [
 *     {
 *       "_id": "507f1f77bcf86cd799439012",
 *       "Title": "The Godfather",
 *       "Description": "Movie description..."
 *     }
 *   ],
 *   "count": 1
 * }
 */
// Get user's favorite movies with full movie details
app.get(
  "/users/:username/favorites",
  [
    passport.authenticate("jwt", { session: false }),
    check('username', 'Username is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      if (req.user.Username !== req.params.username && !req.user.isAdmin) {
        return res.status(403).json({ error: "Not authorized to view this user's favorites" });
      }
      const user = await Users.findOne({ Username: req.params.username }).populate('FavoriteMovies');
      if (!user) {
        return res.status(404).json({ error: "User not found" });
      }
      res.status(200).json({
        username: user.Username,
        favoriteMovies: user.FavoriteMovies,
        count: user.FavoriteMovies.length
      });
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function addToFavorites
 * @description - Add a movie to user's list of favorite movies
 * @route POST /users/:username/movies/:movieId
 * @param {Query_Parameters} - :username, :movieId
 * @returns {object} - Updated user object
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "_id": "507f1f77bcf86cd799439011",
 *   "Username": "johndoe",
 *   "Email": "john@example.com",
 *   "FavoriteMovies": ["507f1f77bcf86cd799439012", "507f1f77bcf86cd799439013"]
 * }
 */
app.post(
  "/users/:username/movies/:movieId",
  [
    passport.authenticate("jwt", { session: false }),
    check('username', 'Username is required').notEmpty(),
    check('movieId', 'Invalid movie ID').isMongoId()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      if (req.user.Username !== req.params.username) {
        return res
          .status(403)
          .json({ error: "Not authorized to update this user's favorites" });
      }
      const updatedUser = await Users.findOneAndUpdate(
        { Username: req.params.username },
        { $addToSet: { FavoriteMovies: req.params.movieId } },
        { new: true }
      );
      if (!updatedUser) {
        return res.status(404).json({ error: "User not found" });
      }
      res.status(200).json(updatedUser);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function removeFromFavorites
 * @description - Remove a movie from user's list of favorite movies
 * @route DELETE /users/:username/movies/:movieId
 * @param {Query_Parameters} - :username, :movieId
 * @returns {object} - Updated user object
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "_id": "507f1f77bcf86cd799439011",
 *   "Username": "johndoe",
 *   "Email": "john@example.com",
 *   "FavoriteMovies": ["507f1f77bcf86cd799439013"]
 * }
 */
app.delete(
  "/users/:username/movies/:movieId",
  [
    passport.authenticate("jwt", { session: false }),
    check('username', 'Username is required').notEmpty(),
    check('movieId', 'Invalid movie ID').isMongoId()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      if (req.user.Username !== req.params.username) {
        return res
          .status(403)
          .json({ error: "Not authorized to update this user's favorites" });
      }
      const updatedUser = await Users.findOneAndUpdate(
        { Username: req.params.username },
        { $pull: { FavoriteMovies: req.params.movieId } },
        { new: true }
      );
      if (!updatedUser) {
        return res.status(404).json({ error: "User not found" });
      }
      res.status(200).json(updatedUser);
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function deleteUser
 * @description - Delete a user account
 * @route DELETE /users/:username
 * @param {Query_Parameters} - :username
 * @returns {object} - Deletion confirmation message
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "message": "johndoe was deleted."
 * }
 */
app.delete(
  "/users/:username",
  [
    passport.authenticate("jwt", { session: false }),
    check('username', 'Username is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      if (req.user.Username !== req.params.username && !req.user.isAdmin) {
        return res.status(403).json({ error: "Not authorized to delete this user" });
      }
      const user = await Users.findOneAndDelete({
        Username: req.params.username,
      });
      if (!user) {
        return res.status(404).json({ error: "User not found" });
      }
      res.status(200).json({ message: req.params.username + " was deleted." });
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

// === SEARCH ROUTES ===

/**
 * @function getFeaturedMovies
 * @description - Get featured movies (commonly needed for homepage)
 * @route GET /movies/featured
 * @returns {Array} - Array of featured movie objects
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * [
 *   {
 *     "_id": "507f1f77bcf86cd799439011",
 *     "Title": "The Shawshank Redemption",
 *     "Featured": true
 *   }
 * ]
 */
// Get featured movies (commonly needed for homepage)
app.get(
  "/movies/featured",
  passport.authenticate("jwt", { session: false }),
  async (req, res) => {
    try {
      const featuredMovies = await Movies.find({ Featured: true });
      res.status(200).json(featuredMovies);
    } catch (error) {
      console.error("Error fetching featured movies:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function getMoviesByGenre
 * @description - Get movies by genre (commonly needed for filtering)
 * @route GET /movies/genre/:genre
 * @param {Query_Parameters} - :genre
 * @returns {Array} - Array of movies matching the genre
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * [
 *   {
 *     "_id": "507f1f77bcf86cd799439011",
 *     "Title": "The Shawshank Redemption",
 *     "Genre": {
 *       "Name": "Drama",
 *       "Description": "Drama genre description"
 *     }
 *   }
 * ]
 */
// Get movies by genre (commonly needed for filtering)
app.get(
  "/movies/genre/:genre",
  [
    passport.authenticate("jwt", { session: false }),
    check('genre', 'Genre is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    try {
      const movies = await Movies.find({ "Genre.Name": new RegExp(req.params.genre, 'i') });
      res.status(200).json(movies);
    } catch (error) {
      console.error("Error fetching movies by genre:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function generalSearch
 * @description - General search endpoint - searches across multiple fields
 * @route GET /search
 * @param {Query_Parameters} - ?q=searchQuery
 * @returns {object} - Search results object with query, results array, and count
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "query": "drama",
 *   "results": [
 *     {
 *       "_id": "507f1f77bcf86cd799439011",
 *       "Title": "The Shawshank Redemption"
 *     }
 *   ],
 *   "count": 1
 * }
 */
// General search endpoint - searches across multiple fields
app.get(
  "/search",
  [
    passport.authenticate("jwt", { session: false }),
    check('q', 'Search query is required').notEmpty().isLength({ min: 1 })
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    
    try {
      const searchQuery = req.query.q;
      const searchRegex = new RegExp(searchQuery, 'i'); // Case-insensitive search
      
      const movies = await Movies.find({
        $or: [
          { Title: searchRegex },
          { Description: searchRegex },
          { "Genre.Name": searchRegex },
          { "Director.Name": searchRegex },
          { Actors: { $in: [searchRegex] } }
        ]
      });
      
      res.status(200).json({
        query: searchQuery,
        results: movies,
        count: movies.length
      });
    } catch (error) {
      console.error("Error in search:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function searchMoviesByTitle
 * @description - Search movies by title
 * @route GET /search/movies
 * @param {Query_Parameters} - ?title=searchQuery
 * @returns {object} - Search results object
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "query": "shawshank",
 *   "results": [
 *     {
 *       "_id": "507f1f77bcf86cd799439011",
 *       "Title": "The Shawshank Redemption"
 *     }
 *   ],
 *   "count": 1
 * }
 */
// Search movies by title
app.get(
  "/search/movies",
  [
    passport.authenticate("jwt", { session: false }),
    check('title', 'Title search query is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    
    try {
      const titleQuery = req.query.title;
      const searchRegex = new RegExp(titleQuery, 'i');
      
      const movies = await Movies.find({ Title: searchRegex });
      
      res.status(200).json({
        query: titleQuery,
        results: movies,
        count: movies.length
      });
    } catch (error) {
      console.error("Error in movie title search:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function searchMoviesByGenre
 * @description - Search movies by genre
 * @route GET /search/genres
 * @param {Query_Parameters} - ?genre=searchQuery
 * @returns {object} - Search results object
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "query": "drama",
 *   "results": [
 *     {
 *       "_id": "507f1f77bcf86cd799439011",
 *       "Title": "The Shawshank Redemption",
 *       "Genre": {"Name": "Drama"}
 *     }
 *   ],
 *   "count": 1
 * }
 */
// Search movies by genre
app.get(
  "/search/genres",
  [
    passport.authenticate("jwt", { session: false }),
    check('genre', 'Genre search query is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    
    try {
      const genreQuery = req.query.genre;
      const searchRegex = new RegExp(genreQuery, 'i');
      
      const movies = await Movies.find({ "Genre.Name": searchRegex });
      
      res.status(200).json({
        query: genreQuery,
        results: movies,
        count: movies.length
      });
    } catch (error) {
      console.error("Error in genre search:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function searchMoviesByDirector
 * @description - Search movies by director
 * @route GET /search/directors
 * @param {Query_Parameters} - ?director=searchQuery
 * @returns {object} - Search results object
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "query": "darabont",
 *   "results": [
 *     {
 *       "_id": "507f1f77bcf86cd799439011",
 *       "Title": "The Shawshank Redemption",
 *       "Director": {"Name": "Frank Darabont"}
 *     }
 *   ],
 *   "count": 1
 * }
 */
// Search movies by director
app.get(
  "/search/directors",
  [
    passport.authenticate("jwt", { session: false }),
    check('director', 'Director search query is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    
    try {
      const directorQuery = req.query.director;
      const searchRegex = new RegExp(directorQuery, 'i');
      
      const movies = await Movies.find({ "Director.Name": searchRegex });
      
      res.status(200).json({
        query: directorQuery,
        results: movies,
        count: movies.length
      });
    } catch (error) {
      console.error("Error in director search:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function searchMoviesByActor
 * @description - Search movies by actor
 * @route GET /search/actors
 * @param {Query_Parameters} - ?actor=searchQuery
 * @returns {object} - Search results object
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "query": "tim robbins",
 *   "results": [
 *     {
 *       "_id": "507f1f77bcf86cd799439011",
 *       "Title": "The Shawshank Redemption",
 *       "Actors": ["Tim Robbins", "Morgan Freeman"]
 *     }
 *   ],
 *   "count": 1
 * }
 */
// Search movies by actor
app.get(
  "/search/actors",
  [
    passport.authenticate("jwt", { session: false }),
    check('actor', 'Actor search query is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    
    try {
      const actorQuery = req.query.actor;
      const searchRegex = new RegExp(actorQuery, 'i');
      
      const movies = await Movies.find({ Actors: { $in: [searchRegex] } });
      
      res.status(200).json({
        query: actorQuery,
        results: movies,
        count: movies.length
      });
    } catch (error) {
      console.error("Error in actor search:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function advancedSearch
 * @description - Advanced search with multiple filters
 * @route GET /search/advanced
 * @param {Query_Parameters} - ?title=&genre=&director=&actor=&year=
 * @returns {object} - Search results object with filters applied
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "filters": {
 *     "title": "shawshank",
 *     "genre": "drama"
 *   },
 *   "results": [
 *     {
 *       "_id": "507f1f77bcf86cd799439011",
 *       "Title": "The Shawshank Redemption"
 *     }
 *   ],
 *   "count": 1
 * }
 */
// Advanced search with multiple filters
app.get(
  "/search/advanced",
  passport.authenticate("jwt", { session: false }),
  async (req, res) => {
    try {
      const { title, genre, director, actor, year } = req.query;
      let searchCriteria = {};
      
      if (title) {
        searchCriteria.Title = new RegExp(title, 'i');
      }
      
      if (genre) {
        searchCriteria["Genre.Name"] = new RegExp(genre, 'i');
      }
      
      if (director) {
        searchCriteria["Director.Name"] = new RegExp(director, 'i');
      }
      
      if (actor) {
        searchCriteria.Actors = { $in: [new RegExp(actor, 'i')] };
      }
      
      if (year) {
        searchCriteria.Year = year;
      }
      
      if (Object.keys(searchCriteria).length === 0) {
        return res.status(400).json({ error: "At least one search parameter is required" });
      }
      
      const movies = await Movies.find(searchCriteria);
      
      res.status(200).json({
        filters: req.query,
        results: movies,
        count: movies.length
      });
    } catch (error) {
      console.error("Error in advanced search:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function searchSuggestions
 * @description - Auto-suggestions endpoint for search-as-you-type functionality
 * @route GET /search/suggestions
 * @param {Query_Parameters} - ?q=searchQuery&limit=10
 * @returns {object} - Suggestions object with different categories
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "query": "shaw",
 *   "suggestions": {
 *     "movies": [{"type": "movie", "value": "The Shawshank Redemption"}],
 *     "genres": [{"type": "genre", "value": "Drama"}],
 *     "directors": [{"type": "director", "value": "Frank Darabont"}],
 *     "actors": [{"type": "actor", "value": "Tim Robbins"}]
 *   }
 * }
 */
// Auto-suggestions endpoint for search-as-you-type functionality
app.get(
  "/search/suggestions",
  [
    passport.authenticate("jwt", { session: false }),
    check('q', 'Search query is required').notEmpty().isLength({ min: 1, max: 50 })
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    
    try {
      const searchQuery = req.query.q;
      const limit = parseInt(req.query.limit) || 10; // Default to 10 suggestions
      const searchRegex = new RegExp(searchQuery, 'i');
      
      // Get movie title suggestions
      const movieTitles = await Movies.find(
        { Title: searchRegex },
        { Title: 1, _id: 0 }
      ).limit(limit);
      
      // Get genre suggestions
      const genres = await Movies.aggregate([
        { $match: { "Genre.Name": searchRegex } },
        { $group: { _id: "$Genre.Name" } },
        { $limit: 5 },
        { $project: { _id: 0, name: "$_id" } }
      ]);
      
      // Get director suggestions
      const directors = await Movies.aggregate([
        { $match: { "Director.Name": searchRegex } },
        { $group: { _id: "$Director.Name" } },
        { $limit: 5 },
        { $project: { _id: 0, name: "$_id" } }
      ]);
      
      // Get actor suggestions
      const actors = await Movies.aggregate([
        { $unwind: "$Actors" },
        { $match: { "Actors": searchRegex } },
        { $group: { _id: "$Actors" } },
        { $limit: 5 },
        { $project: { _id: 0, name: "$_id" } }
      ]);
      
      res.status(200).json({
        query: searchQuery,
        suggestions: {
          movies: movieTitles.map(m => ({ type: 'movie', value: m.Title })),
          genres: genres.map(g => ({ type: 'genre', value: g.name })),
          directors: directors.map(d => ({ type: 'director', value: d.name })),
          actors: actors.map(a => ({ type: 'actor', value: a.name }))
        }
      });
    } catch (error) {
      console.error("Error in suggestions:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function quickSearch
 * @description - Quick search endpoint optimized for fast results (limited fields)
 * @route GET /search/quick
 * @param {Query_Parameters} - ?q=searchQuery&limit=20
 * @returns {object} - Quick search results with essential fields only
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "query": "drama",
 *   "results": [
 *     {
 *       "_id": "507f1f77bcf86cd799439011",
 *       "Title": "The Shawshank Redemption",
 *       "Genre": {"Name": "Drama"},
 *       "Director": {"Name": "Frank Darabont"},
 *       "Year": 1994,
 *       "ImagePath": "shawshank.png"
 *     }
 *   ],
 *   "count": 1,
 *   "isQuickSearch": true
 * }
 */
// Quick search endpoint optimized for fast results (limited fields)
app.get(
  "/search/quick",
  [
    passport.authenticate("jwt", { session: false }),
    check('q', 'Search query is required').notEmpty().isLength({ min: 1 })
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    
    try {
      const searchQuery = req.query.q;
      const limit = parseInt(req.query.limit) || 20;
      const searchRegex = new RegExp(searchQuery, 'i');
      
      // Return only essential fields for quick display
      const movies = await Movies.find({
        $or: [
          { Title: searchRegex },
          { "Genre.Name": searchRegex },
          { "Director.Name": searchRegex },
          { Actors: { $in: [searchRegex] } }
        ]
      }, {
        Title: 1,
        "Genre.Name": 1,
        "Director.Name": 1,
        Year: 1,
        ImagePath: 1
      }).limit(limit);
      
      res.status(200).json({
        query: searchQuery,
        results: movies,
        count: movies.length,
        isQuickSearch: true
      });
    } catch (error) {
      console.error("Error in quick search:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

/**
 * @function paginatedSearch
 * @description - Search with pagination for better performance
 * @route GET /search/paginated
 * @param {Query_Parameters} - ?q=searchQuery&page=1&limit=20
 * @returns {object} - Search results with pagination information
 * @param {authentication} - Bearer token (JWT)
 * @example
 * // Response data format
 * {
 *   "query": "drama",
 *   "results": [
 *     {
 *       "_id": "507f1f77bcf86cd799439011",
 *       "Title": "The Shawshank Redemption"
 *     }
 *   ],
 *   "pagination": {
 *     "currentPage": 1,
 *     "totalPages": 1,
 *     "totalResults": 1,
 *     "resultsPerPage": 20,
 *     "hasNextPage": false,
 *     "hasPrevPage": false
 *   }
 * }
 */
// Search with pagination for better performance
app.get(
  "/search/paginated",
  [
    passport.authenticate("jwt", { session: false }),
    check('q', 'Search query is required').notEmpty()
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }
    
    try {
      const searchQuery = req.query.q;
      const page = parseInt(req.query.page) || 1;
      const limit = parseInt(req.query.limit) || 20;
      const skip = (page - 1) * limit;
      const searchRegex = new RegExp(searchQuery, 'i');
      
      const searchCriteria = {
        $or: [
          { Title: searchRegex },
          { Description: searchRegex },
          { "Genre.Name": searchRegex },
          { "Director.Name": searchRegex },
          { Actors: { $in: [searchRegex] } }
        ]
      };
      
      // Get total count for pagination
      const totalResults = await Movies.countDocuments(searchCriteria);
      const totalPages = Math.ceil(totalResults / limit);
      
      // Get paginated results
      const movies = await Movies.find(searchCriteria)
        .skip(skip)
        .limit(limit);
      
      res.status(200).json({
        query: searchQuery,
        results: movies,
        pagination: {
          currentPage: page,
          totalPages: totalPages,
          totalResults: totalResults,
          resultsPerPage: limit,
          hasNextPage: page < totalPages,
          hasPrevPage: page > 1
        }
      });
    } catch (error) {
      console.error("Error in paginated search:", error);
      res.status(500).json({ error: "Error: " + error });
    }
  }
);

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: "Something broke!" });
});

// Start server
const port = process.env.PORT || 8080;
app.listen(port, '0.0.0.0', () => {
  console.log('Listening on port ' + port);
});