import { Router } from 'express';
import { getRepository } from 'typeorm';
import TheMovieDb from '../api/themoviedb';
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest';
import SeasonRequest from '../entity/SeasonRequest';
import { User } from '../entity/User';
import { RequestResultsResponse } from '../interfaces/api/requestInterfaces';
import { Permission } from '../lib/permissions';
import logger from '../logger';
import { isAuthenticated } from '../middleware/auth';

const requestRoutes = Router();

requestRoutes.get('/', async (req, res, next) => {
  try {
    const pageSize = req.query.take ? Number(req.query.take) : 10;
    const skip = req.query.skip ? Number(req.query.skip) : 0;
    const requestedBy = req.query.requestedBy
      ? Number(req.query.requestedBy)
      : null;

    let statusFilter: MediaRequestStatus[];

    switch (req.query.filter) {
      case 'approved':
      case 'processing':
      case 'available':
        statusFilter = [MediaRequestStatus.APPROVED];
        break;
      case 'pending':
        statusFilter = [MediaRequestStatus.PENDING];
        break;
      case 'unavailable':
        statusFilter = [
          MediaRequestStatus.PENDING,
          MediaRequestStatus.APPROVED,
        ];
        break;
      default:
        statusFilter = [
          MediaRequestStatus.PENDING,
          MediaRequestStatus.APPROVED,
          MediaRequestStatus.DECLINED,
        ];
    }

    let mediaStatusFilter: MediaStatus[];

    switch (req.query.filter) {
      case 'available':
        mediaStatusFilter = [MediaStatus.AVAILABLE];
        break;
      case 'processing':
      case 'unavailable':
        mediaStatusFilter = [
          MediaStatus.UNKNOWN,
          MediaStatus.PENDING,
          MediaStatus.PROCESSING,
          MediaStatus.PARTIALLY_AVAILABLE,
        ];
        break;
      default:
        mediaStatusFilter = [
          MediaStatus.UNKNOWN,
          MediaStatus.PENDING,
          MediaStatus.PROCESSING,
          MediaStatus.PARTIALLY_AVAILABLE,
          MediaStatus.AVAILABLE,
        ];
    }

    let sortFilter: string;

    switch (req.query.sort) {
      case 'modified':
        sortFilter = 'request.updatedAt';
        break;
      default:
        sortFilter = 'request.id';
    }

    let query = getRepository(MediaRequest)
      .createQueryBuilder('request')
      .leftJoinAndSelect('request.media', 'media')
      .leftJoinAndSelect('request.seasons', 'seasons')
      .leftJoinAndSelect('request.modifiedBy', 'modifiedBy')
      .leftJoinAndSelect('request.requestedBy', 'requestedBy')
      .where('request.status IN (:...requestStatus)', {
        requestStatus: statusFilter,
      })
      .andWhere(
        '((request.is4k = 0 AND media.status IN (:...mediaStatus)) OR (request.is4k = 1 AND media.status4k IN (:...mediaStatus)))',
        {
          mediaStatus: mediaStatusFilter,
        }
      );

    if (
      !req.user?.hasPermission(
        [Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
        { type: 'or' }
      )
    ) {
      if (requestedBy && requestedBy !== req.user?.id) {
        return next({
          status: 403,
          message: "You do not have permission to view this user's requests.",
        });
      }

      query = query.andWhere('requestedBy.id = :id', {
        id: req.user?.id,
      });
    } else if (requestedBy) {
      query = query.andWhere('requestedBy.id = :id', {
        id: requestedBy,
      });
    }

    const [requests, requestCount] = await query
      .orderBy(sortFilter, 'DESC')
      .take(pageSize)
      .skip(skip)
      .getManyAndCount();

    return res.status(200).json({
      pageInfo: {
        pages: Math.ceil(requestCount / pageSize),
        pageSize,
        results: requestCount,
        page: Math.ceil(skip / pageSize) + 1,
      },
      results: requests,
    } as RequestResultsResponse);
  } catch (e) {
    next({ status: 500, message: e.message });
  }
});

requestRoutes.post('/', async (req, res, next) => {
  const tmdb = new TheMovieDb();
  const mediaRepository = getRepository(Media);
  const requestRepository = getRepository(MediaRequest);
  const userRepository = getRepository(User);

  try {
    let requestUser = req.user;

    if (
      req.body.userId &&
      !req.user?.hasPermission([
        Permission.MANAGE_USERS,
        Permission.MANAGE_REQUESTS,
      ])
    ) {
      return next({
        status: 403,
        message: 'You do not have permission to modify the request user.',
      });
    } else if (req.body.userId) {
      requestUser = await userRepository.findOneOrFail({
        where: { id: req.body.userId },
      });
    }

    if (!requestUser) {
      return next({
        status: 500,
        message: 'User missing from request context.',
      });
    }

    if (
      req.body.mediaType === MediaType.MOVIE &&
      !req.user?.hasPermission(
        req.body.is4k
          ? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE]
          : [Permission.REQUEST, Permission.REQUEST_MOVIE],
        {
          type: 'or',
        }
      )
    ) {
      return next({
        status: 403,
        message: `You do not have permission to make ${
          req.body.is4k ? '4K ' : ''
        }movie requests.`,
      });
    } else if (
      req.body.mediaType === MediaType.TV &&
      !req.user?.hasPermission(
        req.body.is4k
          ? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV]
          : [Permission.REQUEST, Permission.REQUEST_TV],
        {
          type: 'or',
        }
      )
    ) {
      return next({
        status: 403,
        message: `You do not have permission to make ${
          req.body.is4k ? '4K ' : ''
        }series requests.`,
      });
    }

    const quotas = await requestUser.getQuota();

    if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
      return next({
        status: 403,
        message: 'Movie Quota Exceeded',
      });
    } else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
      return next({
        status: 403,
        message: 'Series Quota Exceeded',
      });
    }

    const tmdbMedia =
      req.body.mediaType === MediaType.MOVIE
        ? await tmdb.getMovie({ movieId: req.body.mediaId })
        : await tmdb.getTvShow({ tvId: req.body.mediaId });

    let media = await mediaRepository.findOne({
      where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
      relations: ['requests'],
    });

    if (!media) {
      media = new Media({
        tmdbId: tmdbMedia.id,
        tvdbId: req.body.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
        status: !req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
        status4k: req.body.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
        mediaType: req.body.mediaType,
      });
    } else {
      if (media.status === MediaStatus.UNKNOWN && !req.body.is4k) {
        media.status = MediaStatus.PENDING;
      }

      if (media.status4k === MediaStatus.UNKNOWN && req.body.is4k) {
        media.status4k = MediaStatus.PENDING;
      }
    }

    if (req.body.mediaType === MediaType.MOVIE) {
      const existing = await requestRepository
        .createQueryBuilder('request')
        .leftJoin('request.media', 'media')
        .where('request.is4k = :is4k', { is4k: req.body.is4k })
        .andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
        .andWhere('request.status != :requestStatus', {
          requestStatus: MediaRequestStatus.DECLINED,
        })
        .getOne();

      if (existing) {
        logger.warn('Duplicate request for media blocked', {
          tmdbId: tmdbMedia.id,
          mediaType: req.body.mediaType,
          is4k: req.body.is4k,
          label: 'Media Request',
        });
        return next({
          status: 409,
          message: 'Request for this media already exists.',
        });
      }

      await mediaRepository.save(media);

      const request = new MediaRequest({
        type: MediaType.MOVIE,
        media,
        requestedBy: requestUser,
        // If the user is an admin or has the "auto approve" permission, automatically approve the request
        status: req.user?.hasPermission(
          [
            req.body.is4k
              ? Permission.AUTO_APPROVE_4K
              : Permission.AUTO_APPROVE,
            req.body.is4k
              ? Permission.AUTO_APPROVE_4K_MOVIE
              : Permission.AUTO_APPROVE_MOVIE,
            Permission.MANAGE_REQUESTS,
          ],
          { type: 'or' }
        )
          ? MediaRequestStatus.APPROVED
          : MediaRequestStatus.PENDING,
        modifiedBy: req.user?.hasPermission(
          [
            req.body.is4k
              ? Permission.AUTO_APPROVE_4K
              : Permission.AUTO_APPROVE,
            req.body.is4k
              ? Permission.AUTO_APPROVE_4K_MOVIE
              : Permission.AUTO_APPROVE_MOVIE,
            Permission.MANAGE_REQUESTS,
          ],
          { type: 'or' }
        )
          ? req.user
          : undefined,
        is4k: req.body.is4k,
        serverId: req.body.serverId,
        profileId: req.body.profileId,
        rootFolder: req.body.rootFolder,
        tags: req.body.tags,
      });

      await requestRepository.save(request);
      return res.status(201).json(request);
    } else if (req.body.mediaType === MediaType.TV) {
      const requestedSeasons = req.body.seasons as number[];
      let existingSeasons: number[] = [];

      // We need to check existing requests on this title to make sure we don't double up on seasons that were
      // already requested. In the case they were, we just throw out any duplicates but still approve the request.
      // (Unless there are no seasons, in which case we abort)
      if (media.requests) {
        existingSeasons = media.requests
          .filter(
            (request) =>
              request.is4k === req.body.is4k &&
              request.status !== MediaRequestStatus.DECLINED
          )
          .reduce((seasons, request) => {
            const combinedSeasons = request.seasons.map(
              (season) => season.seasonNumber
            );

            return [...seasons, ...combinedSeasons];
          }, [] as number[]);
      }

      const finalSeasons = requestedSeasons.filter(
        (rs) => !existingSeasons.includes(rs)
      );

      if (finalSeasons.length === 0) {
        return next({
          status: 202,
          message: 'No seasons available to request',
        });
      }

      await mediaRepository.save(media);

      const request = new MediaRequest({
        type: MediaType.TV,
        media,
        requestedBy: requestUser,
        // If the user is an admin or has the "auto approve" permission, automatically approve the request
        status: req.user?.hasPermission(
          [
            req.body.is4k
              ? Permission.AUTO_APPROVE_4K
              : Permission.AUTO_APPROVE,
            req.body.is4k
              ? Permission.AUTO_APPROVE_4K_TV
              : Permission.AUTO_APPROVE_TV,
            Permission.MANAGE_REQUESTS,
          ],
          { type: 'or' }
        )
          ? MediaRequestStatus.APPROVED
          : MediaRequestStatus.PENDING,
        modifiedBy: req.user?.hasPermission(
          [
            req.body.is4k
              ? Permission.AUTO_APPROVE_4K
              : Permission.AUTO_APPROVE,
            req.body.is4k
              ? Permission.AUTO_APPROVE_4K_TV
              : Permission.AUTO_APPROVE_TV,
            Permission.MANAGE_REQUESTS,
          ],
          { type: 'or' }
        )
          ? req.user
          : undefined,
        is4k: req.body.is4k,
        serverId: req.body.serverId,
        profileId: req.body.profileId,
        rootFolder: req.body.rootFolder,
        languageProfileId: req.body.languageProfileId,
        tags: req.body.tags,
        seasons: finalSeasons.map(
          (sn) =>
            new SeasonRequest({
              seasonNumber: sn,
              status: req.user?.hasPermission(
                [
                  req.body.is4k
                    ? Permission.AUTO_APPROVE_4K
                    : Permission.AUTO_APPROVE,
                  req.body.is4k
                    ? Permission.AUTO_APPROVE_4K_TV
                    : Permission.AUTO_APPROVE_TV,
                  Permission.MANAGE_REQUESTS,
                ],
                { type: 'or' }
              )
                ? MediaRequestStatus.APPROVED
                : MediaRequestStatus.PENDING,
            })
        ),
      });

      await requestRepository.save(request);
      return res.status(201).json(request);
    }

    next({ status: 500, message: 'Invalid media type' });
  } catch (e) {
    next({ status: 500, message: e.message });
  }
});

requestRoutes.get('/count', async (_req, res, next) => {
  const requestRepository = getRepository(MediaRequest);

  try {
    const query = requestRepository
      .createQueryBuilder('request')
      .leftJoinAndSelect('request.media', 'media');

    const pendingCount = await query
      .where('request.status = :requestStatus', {
        requestStatus: MediaRequestStatus.PENDING,
      })
      .getCount();

    const approvedCount = await query
      .where('request.status = :requestStatus', {
        requestStatus: MediaRequestStatus.APPROVED,
      })
      .getCount();

    const processingCount = await query
      .where('request.status = :requestStatus', {
        requestStatus: MediaRequestStatus.APPROVED,
      })
      .andWhere(
        '(request.is4k = false AND media.status != :availableStatus) OR (request.is4k = true AND media.status4k != :availableStatus)',
        {
          availableStatus: MediaStatus.AVAILABLE,
        }
      )
      .getCount();

    const availableCount = await query
      .where('request.status = :requestStatus', {
        requestStatus: MediaRequestStatus.APPROVED,
      })
      .andWhere(
        '(request.is4k = false AND media.status = :availableStatus) OR (request.is4k = true AND media.status4k = :availableStatus)',
        {
          availableStatus: MediaStatus.AVAILABLE,
        }
      )
      .getCount();

    return res.status(200).json({
      pending: pendingCount,
      approved: approvedCount,
      processing: processingCount,
      available: availableCount,
    });
  } catch (e) {
    next({ status: 500, message: e.message });
  }
});

requestRoutes.get('/:requestId', async (req, res, next) => {
  const requestRepository = getRepository(MediaRequest);

  try {
    const request = await requestRepository.findOneOrFail({
      where: { id: Number(req.params.requestId) },
      relations: ['requestedBy', 'modifiedBy'],
    });

    return res.status(200).json(request);
  } catch (e) {
    next({ status: 404, message: 'Request not found' });
  }
});

requestRoutes.put<{ requestId: string }>(
  '/:requestId',
  async (req, res, next) => {
    const requestRepository = getRepository(MediaRequest);
    const userRepository = getRepository(User);
    try {
      const request = await requestRepository.findOne(
        Number(req.params.requestId)
      );

      if (!request) {
        return next({ status: 404, message: 'Request not found.' });
      }

      if (
        (request.requestedBy.id !== req.user?.id ||
          (req.body.mediaType !== 'tv' &&
            !req.user?.hasPermission(Permission.REQUEST_ADVANCED))) &&
        !req.user?.hasPermission(Permission.MANAGE_REQUESTS)
      ) {
        return next({
          status: 403,
          message: 'You do not have permission to modify this request.',
        });
      }

      let requestUser = req.user;

      if (
        req.body.userId &&
        req.body.userId !== req.user?.id &&
        !req.user?.hasPermission([
          Permission.MANAGE_USERS,
          Permission.MANAGE_REQUESTS,
        ])
      ) {
        return next({
          status: 403,
          message: 'You do not have permission to modify the request user.',
        });
      } else if (req.body.userId) {
        requestUser = await userRepository.findOneOrFail({
          where: { id: req.body.userId },
        });
      }

      if (req.body.mediaType === MediaType.MOVIE) {
        request.serverId = req.body.serverId;
        request.profileId = req.body.profileId;
        request.rootFolder = req.body.rootFolder;
        request.tags = req.body.tags;
        request.requestedBy = requestUser as User;

        requestRepository.save(request);
      } else if (req.body.mediaType === MediaType.TV) {
        const mediaRepository = getRepository(Media);
        request.serverId = req.body.serverId;
        request.profileId = req.body.profileId;
        request.rootFolder = req.body.rootFolder;
        request.languageProfileId = req.body.languageProfileId;
        request.tags = req.body.tags;
        request.requestedBy = requestUser as User;

        const requestedSeasons = req.body.seasons as number[] | undefined;

        if (!requestedSeasons || requestedSeasons.length === 0) {
          throw new Error(
            'Missing seasons. If you want to cancel a series request, use the DELETE method.'
          );
        }

        // Get existing media so we can work with all the requests
        const media = await mediaRepository.findOneOrFail({
          where: { tmdbId: request.media.tmdbId, mediaType: MediaType.TV },
          relations: ['requests'],
        });

        // Get all requested seasons that are not part of this request we are editing
        const existingSeasons = media.requests
          .filter(
            (r) =>
              r.is4k === request.is4k &&
              r.id !== request.id &&
              r.status !== MediaRequestStatus.DECLINED
          )
          .reduce((seasons, r) => {
            const combinedSeasons = r.seasons.map(
              (season) => season.seasonNumber
            );

            return [...seasons, ...combinedSeasons];
          }, [] as number[]);

        const filteredSeasons = requestedSeasons.filter(
          (rs) => !existingSeasons.includes(rs)
        );

        if (filteredSeasons.length === 0) {
          return next({
            status: 202,
            message: 'No seasons available to request',
          });
        }

        const newSeasons = requestedSeasons.filter(
          (sn) => !request.seasons.map((s) => s.seasonNumber).includes(sn)
        );

        request.seasons = request.seasons.filter((rs) =>
          filteredSeasons.includes(rs.seasonNumber)
        );

        if (newSeasons.length > 0) {
          logger.debug('Adding new seasons to request', {
            label: 'Media Request',
            newSeasons,
          });
          request.seasons.push(
            ...newSeasons.map(
              (ns) =>
                new SeasonRequest({
                  seasonNumber: ns,
                  status: MediaRequestStatus.PENDING,
                })
            )
          );
        }

        await requestRepository.save(request);
      }

      return res.status(200).json(request);
    } catch (e) {
      next({ status: 500, message: e.message });
    }
  }
);

requestRoutes.delete('/:requestId', async (req, res, next) => {
  const requestRepository = getRepository(MediaRequest);

  try {
    const request = await requestRepository.findOneOrFail({
      where: { id: Number(req.params.requestId) },
      relations: ['requestedBy', 'modifiedBy'],
    });

    if (
      !req.user?.hasPermission(Permission.MANAGE_REQUESTS) &&
      request.requestedBy.id !== req.user?.id &&
      request.status !== 1
    ) {
      return next({
        status: 401,
        message: 'You do not have permission to delete this request.',
      });
    }

    await requestRepository.remove(request);

    return res.status(204).send();
  } catch (e) {
    logger.error(e.message);
    next({ status: 404, message: 'Request not found.' });
  }
});

requestRoutes.post<{
  requestId: string;
}>(
  '/:requestId/retry',
  isAuthenticated(Permission.MANAGE_REQUESTS),
  async (req, res, next) => {
    const requestRepository = getRepository(MediaRequest);

    try {
      const request = await requestRepository.findOneOrFail({
        where: { id: Number(req.params.requestId) },
        relations: ['requestedBy', 'modifiedBy'],
      });

      await request.updateParentStatus();
      await request.sendMedia();
      return res.status(200).json(request);
    } catch (e) {
      logger.error('Error processing request retry', {
        label: 'Media Request',
        message: e.message,
      });
      next({ status: 404, message: 'Request not found.' });
    }
  }
);

requestRoutes.post<{
  requestId: string;
  status: 'pending' | 'approve' | 'decline';
}>(
  '/:requestId/:status',
  isAuthenticated(Permission.MANAGE_REQUESTS),
  async (req, res, next) => {
    const requestRepository = getRepository(MediaRequest);

    try {
      const request = await requestRepository.findOneOrFail({
        where: { id: Number(req.params.requestId) },
        relations: ['requestedBy', 'modifiedBy'],
      });

      let newStatus: MediaRequestStatus;

      switch (req.params.status) {
        case 'pending':
          newStatus = MediaRequestStatus.PENDING;
          break;
        case 'approve':
          newStatus = MediaRequestStatus.APPROVED;
          break;
        case 'decline':
          newStatus = MediaRequestStatus.DECLINED;
          break;
      }

      request.status = newStatus;
      request.modifiedBy = req.user;
      await requestRepository.save(request);

      return res.status(200).json(request);
    } catch (e) {
      logger.error('Error processing request update', {
        label: 'Media Request',
        message: e.message,
      });
      next({ status: 404, message: 'Request not found.' });
    }
  }
);

export default requestRoutes;
