
import { Router, Request, Response, NextFunction } from 'express';
import {
  param,
  query,
  check,
} from 'express-validator';
import { camelCase, upperFirst } from 'lodash';
import { Inject, Service } from 'typedi';
import { IMediaLinkDTO } from '@/interfaces';
import fs from 'fs';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from './BaseController';
import MediaService from '@/services/Media/MediaService';
import { ServiceError } from '@/exceptions';

const fsPromises = fs.promises;

@Service()
export default class MediaController extends BaseController {
  @Inject()
  mediaService: MediaService;

  /**
   * Router constructor.
   */
  router() {
    const router = Router();

    router.post('/upload', [
      ...this.uploadValidationSchema,
    ],
      this.validationResult,
      asyncMiddleware(this.uploadMedia.bind(this)),
      this.handlerServiceErrors,
    );
    router.post('/:id/link', [
      ...this.mediaIdParamSchema,
      ...this.linkValidationSchema,
    ],
      this.validationResult,
      asyncMiddleware(this.linkMedia.bind(this)),
      this.handlerServiceErrors,
    );
    router.delete('/', [
      ...this.deleteValidationSchema,
    ],
      this.validationResult,
      asyncMiddleware(this.deleteMedia.bind(this)),
      this.handlerServiceErrors,
    );
    router.get('/:id', [
      ...this.mediaIdParamSchema,
    ],
      this.validationResult,
      asyncMiddleware(this.getMedia.bind(this)),
      this.handlerServiceErrors,
    );
    return router;
  }

  get uploadValidationSchema() {
    return [
      check('model_name').optional().trim(),
      check('model_id').optional().isNumeric(),
    ];
  }

  get linkValidationSchema() {
    return [
      check('model_name').exists().trim(),
      check('model_id').exists().isNumeric().toInt(),
    ]
  }

  get deleteValidationSchema() {
    return [
      query('ids').exists().isArray(),
      query('ids.*').exists().isNumeric().toInt(),
    ];
  }

  get mediaIdParamSchema() {
    return [
      param('id').exists().isNumeric().toInt(),
    ];
  }

  /**
   * Retrieve all or the given attachment ids.
   * @param {Request} req - 
   * @param {Response} req - 
   * @param {NextFunction} req - 
   */
  async getMedia(req: Request, res: Response, next: NextFunction) {
    const { tenantId } = req;
    const { id: mediaId } = req.params;

    try {
      const media = await this.mediaService.getMedia(tenantId, mediaId);
      return res.status(200).send({ media });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Uploads media.
   * @param {Request} req - 
   * @param {Response} req - 
   * @param {NextFunction} req - 
   */
  async uploadMedia(req: Request, res: Response, next: NextFunction) {
    const { tenantId } = req;
    const { attachment } = req.files
   
    const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
    const modelName = linkMediaDTO.modelName
      ? upperFirst(camelCase(linkMediaDTO.modelName)) : '';

    try {
      const media = await this.mediaService.upload(tenantId, attachment, modelName, linkMediaDTO.modelId);
      return res.status(200).send({ media_id: media.id });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Deletes the given attachment ids from file system and database.
   * @param {Request} req - 
   * @param {Response} req - 
   * @param {NextFunction} req - 
   */
  async deleteMedia(req: Request, res: Response, next: NextFunction) {
    const { tenantId } = req;
    const { ids: mediaIds } = req.query;

    try {
      await this.mediaService.deleteMedia(tenantId, mediaIds);
      return res.status(200).send({ 
        media_ids: mediaIds
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Links the given media to the specific resource model.
   * @param {Request} req 
   * @param {Response} res 
   * @param {NextFunction} next 
   */
  async linkMedia(req: Request, res: Response, next: NextFunction) {
    const { tenantId } = req;
    const { id: mediaId } = req.params;
    const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
    const modelName = upperFirst(camelCase(linkMediaDTO.modelName));

    try {
      await this.mediaService.linkMedia(tenantId, mediaId, linkMediaDTO.modelId, modelName);
      return res.status(200).send({ media_id: mediaId });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Handler service errors.
   * @param {Error} error 
   * @param {Request} req 
   * @param {Response} res 
   * @param {NextFunction} next 
   */
  handlerServiceErrors(error, req: Request, res: Response, next: NextFunction) {
    if (error instanceof ServiceError) {
      if (error.errorType === 'MINETYPE_NOT_SUPPORTED') {
        return res.boom.badRequest(null, {
          errors: [{ type: 'MINETYPE_NOT_SUPPORTED', code: 100, }]
        });
      }
      if (error.errorType === 'MEDIA_NOT_FOUND') {
        return res.boom.badRequest(null, {
          errors: [{ type: 'MEDIA_NOT_FOUND', code: 200 }]
        });
      }
      if (error.errorType === 'MODEL_NAME_HAS_NO_MEDIA') {
        return res.boom.badRequest(null, {
          errors: [{ type: 'MODEL_NAME_HAS_NO_MEDIA', code: 300 }]
        });
      }
      if (error.errorType === 'MODEL_ID_NOT_FOUND') {
        return res.boom.badRequest(null, {
          errors: [{ type: 'MODEL_ID_NOT_FOUND', code: 400 }]
        });
      }
      if (error.errorType === 'MEDIA_IDS_NOT_FOUND') {
        return res.boom.badRequest(null, {
          errors: [{ type: 'MEDIA_IDS_NOT_FOUND', code: 500 }],
        });
      }
      if (error.errorType === 'MEDIA_LINK_EXISTS') {
        return res.boom.badRequest(null, {
          errors: [{ type: 'MEDIA_LINK_EXISTS', code: 600 }],
        });
      }
    }
    next(error);
  }
};
