const express = require('express');
const router = express.Router();
const pathlib = require('path');
const mongoose = require('mongoose');
const multer  = require('multer');
const passport = require('passport');
const mime = require('mime-types');
const path = require('path');
const sharp = require('sharp');
const verifyFileContent = require('../middleware/file-type.js');

require('../middleware/passport.js')(passport);
const validtoken = require('../middleware/valid-token.js')
const roleChecker = require('../middleware/has-role.js');
const winston = require('../config/winston.js');
const FileGridFsService = require('../services/fileGridFsService.js');
const roleConstants = require('../models/roleConstants.js');
const faq_kb = require('../models/faq_kb');
const project_user = require('../models/project_user');

const fileService = new FileGridFsService("files");
const fallbackFileService = new FileGridFsService("images");;


let MAX_UPLOAD_FILE_SIZE = process.env.MAX_UPLOAD_FILE_SIZE || 1024000; // 1MB
let uploadlimits = undefined;

if (MAX_UPLOAD_FILE_SIZE) {
  uploadlimits = { fileSize: parseInt(MAX_UPLOAD_FILE_SIZE) } ;
  winston.info("Max upload file size is : " + MAX_UPLOAD_FILE_SIZE);
} else {
  winston.info("Max upload file size is infinity");
}

/**
 * Default: '2592000' (30 days)
 * Examples:
 * - '30' (30 seconds)
 */
const chatFileExpirationTime = parseInt(process.env.CHAT_FILE_EXPIRATION_TIME || '2592000', 10);

/**
 * Default: ".jpg,.jpeg,.png,.gif,.pdf,.txt"
 * Examples: 
 * - '* /*' without spaces (all extension)
 * Deprecated: "application/pdf,image/png,..."
 */
const default_chat_allowed_extensions = process.env.CHAT_FILES_ALLOW_LIST || ".jpg,.jpeg,.png,.gif,.pdf,.txt"; 
const default_assets_allowed_extensions = process.env.ASSETS_FILES_ALLOW_LIST || ".jpg,.jpeg,.png,.gif,.pdf,.txt,.csv,.doc,.docx"; //,.xls,.xlsx,.ppt,.pptx,.zip,.rar
const images_extensions = [ ".png", ".jpg", ".jpeg", ".gif" ];

const fileFilter = (extensionsSource = 'chat') => {
  return (req, file, cb) => {

    const project = req.project;
    const pu = req.projectuser;

    let allowed_extensions;
    let allowed_mime_types;

    if (extensionsSource === 'avatar') {
      // Avatar only accepts image extensions
      allowed_extensions = images_extensions.join(',');
    } else if (extensionsSource === 'assets') {
      allowed_extensions = default_assets_allowed_extensions;
    } else if (pu.roleType === 2 || pu.role === roleConstants.GUEST) {
      allowed_extensions = project?.widget?.allowedUploadExtentions || default_chat_allowed_extensions;
    } else {
      allowed_extensions = project?.settings?.allowed_upload_extentions || default_chat_allowed_extensions;
    }

    if (allowed_extensions !== "*/*") {
      allowed_mime_types = getMimeTypes(allowed_extensions);
      if (!file.originalname) {
        return cb(new Error("File original name is required"));
      }
      const ext = path.extname(file.originalname).toLowerCase();

      if (!allowed_extensions.includes(ext)) {
        const error = new Error(`File extension ${ext} is not allowed${extensionsSource === 'avatar' ? ' for avatar' : ''}`);
        error.status = 403;
        return cb(error);
      }

      const expectedMimeType = mime.lookup(ext);
      if (expectedMimeType && !areMimeTypesEquivalent(file.mimetype, expectedMimeType)) {
        const error = new Error(`File content does not match mimetype. Detected: ${file.mimetype}, provided: ${expectedMimeType}`);
        error.status = 403;
        return cb(error);
      }

      return cb(null, true);
    } else {
      return cb(null, true);
    }
  }
}

function getMimeTypes(allowed_extension) {
  const extension = allowed_extension.split(',').map(e => e.trim().toLowerCase());
  const allowedMimeTypes = extension.map(ext => mime.lookup(ext)).filter(Boolean);
  return allowedMimeTypes;
}

/**
 * Checks if two MIME types are equivalent, accepting common aliases
 * Examples:
 * - audio/wav === audio/wave
 * - audio/x-wav === audio/wave
 * - image/jpeg === image/jpg
 */
function areMimeTypesEquivalent(mimeType1, mimeType2) {
  if (!mimeType1 || !mimeType2) return false;
  if (mimeType1 === mimeType2) return true;
  
  // Normalize to lowercase for comparison
  const m1 = mimeType1.toLowerCase();
  const m2 = mimeType2.toLowerCase();
  if (m1 === m2) return true;
  
  // Common MIME type aliases
  const aliases = {
    'audio/wav': ['audio/wave', 'audio/x-wav', 'audio/vnd.wave'],
    'audio/wave': ['audio/wav', 'audio/x-wav', 'audio/vnd.wave'],
    'audio/x-wav': ['audio/wav', 'audio/wave', 'audio/vnd.wave'],
    'audio/vnd.wave': ['audio/wav', 'audio/wave', 'audio/x-wav'],
    'audio/mpeg': ['audio/opus', 'audio/mp3', 'audio/webm'],
    'audio/mp3': ['audio/mpeg', 'audio/opus', 'audio/webm'],
    'audio/opus': ['audio/mpeg', 'audio/mp3', 'audio/webm'],
    'audio/webm': ['audio/mpeg', 'audio/mp3', 'audio/opus'],
    'image/jpeg': ['image/jpg'],
    'image/jpg': ['image/jpeg'],
    'application/x-zip-compressed': ['application/zip'],
    'application/zip': ['application/x-zip-compressed'],
  };
  
  // Check if m1 is an alias of m2 or vice versa
  if (aliases[m1] && aliases[m1].includes(m2)) return true;
  if (aliases[m2] && aliases[m2].includes(m1)) return true;
  
  return false;
}

const uploadChat = multer({
  storage: fileService.getStorage("files"),
  fileFilter: fileFilter('chat'),
  limits: uploadlimits
}).single('file');

const uploadAssets = multer({
  storage: fileService.getStorageProjectAssets("files"),
  fileFilter: fileFilter('assets'),
  limits: uploadlimits
}).single('file');

const uploadAvatar = multer({
  storage: fileService.getStorageAvatarFiles("files"),
  fileFilter: fileFilter('avatar'),
  limits: uploadlimits
}).single('file');


// *********************** //
// ****** Endpoints ****** //
// *********************** //

router.post('/chat', [
  passport.authenticate(['basic', 'jwt'], { session: false }),
  validtoken,
  roleChecker.hasRoleOrTypes('guest', ['bot','subscription'])
], async (req, res) => {

  const expireAt = new Date(Date.now() + chatFileExpirationTime * 1000);
  req.expireAt = expireAt;
  uploadChat(req, res, async (err) => {
    if (err instanceof multer.MulterError) {
      // A Multer error occurred when uploading
      winston.error(`Multer replied with code ${err?.code} and message "${err?.message}"`);
      let status = 400;
      if (err?.code === 'LIMIT_FILE_SIZE') {
        status = 413;
      }
      return res.status(status).send({ success: false, error: err?.message || 'An error occurred while uploading the file', code: err.code });
    } else if (err) {
      // An unknown error occurred when uploading.
      winston.error(`Multer replied with status ${err?.status} and message "${err?.message}"`);
      let status = err?.status || 400;
      return res.status(status).send({ success: false, error: err.message || "An error occurred while uploading the file" })
    }
    try {
      const buffer = await fileService.getFileDataAsBuffer(req.file.filename);
      await verifyFileContent(buffer, req.file.mimetype);
      await mongoose.connection.db.collection('files.chunks').updateMany(
        { files_id: req.file.id },
        { $set: { "metadata.expireAt": req.file.metadata.expireAt }}
      )
      return res.status(201).send({ message: "File uploaded successfully", filename: req.file.filename })
    } catch (err) {
      if (err?.source === "FileContentVerification") {
        let error_message = err?.message || "Content verification failed";
        winston.warn("File content verification failed. Message: ", error_message);
        return res.status(403).send({ success: false, error: error_message })
      }
      winston.error("Error saving file: ", err);
      return res.status(500).send({ success: false, error: "Error updating file chunks" });
    }
  })

})


router.post('/assets', [
  passport.authenticate(['basic', 'jwt'], { session: false }),
  validtoken,
  roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])
], async (req, res) => {
  // Assets have no retention by default, but can be set via query parameter
  let customExpiration = parseInt(req.query?.expiration, 10);
  if (customExpiration && !isNaN(customExpiration) && customExpiration > 0) {
    req.expireAt = new Date(Date.now() + customExpiration * 1000);
  }


  uploadAssets(req, res, async (err) => {
    if (err instanceof multer.MulterError) {
      // A Multer error occurred when uploading
      winston.error(`Multer replied with code ${err?.code} and message "${err?.message}"`);
      let status = 400;
      if (err?.code === 'LIMIT_FILE_SIZE') {
        status = 413;
      }
      return res.status(status).send({ success: false, error: err?.message || 'An error occurred while uploading the file', code: err.code });
    } else if (err) {
      // An unknown error occurred when uploading.
      winston.error(`Multer replied with status ${err?.status} and message "${err?.message}"`);
      let status = err?.status || 400;
      return res.status(status).send({ success: false, error: err.message || "An error occurred while uploading the file" })
    }

    try {

      const buffer = await fileService.getFileDataAsBuffer(req.file.filename);
      await verifyFileContent(buffer, req.file.mimetype);

      if (req.file.metadata && req.file.metadata.expireAt) {
        await mongoose.connection.db.collection('files.chunks').updateMany(
          { files_id: req.file.id },
          { $set: { "metadata.expireAt": req.file.metadata.expireAt }}
        );
      }

      const ext = path.extname(req.file.originalname).toLowerCase();
      let thumbnail;

      // Generate thumbnail for images
      if (images_extensions.includes(ext)) {
        const buffer = await fileService.getFileDataAsBuffer(req.file.filename);
        const thumbFilename = req.file.filename.replace(/([^/]+)$/, "thumbnails_200_200-$1");
        const resized = await sharp(buffer).resize(200, 200).toBuffer();
        
        const thumbMetadata = req.expireAt ? { metadata: { expireAt: req.expireAt } } : undefined;
        // Use the same contentType as the original file for the thumbnail
        await fileService.createFile(thumbFilename, resized, undefined, req.file.mimetype, thumbMetadata);
        
        if (req.expireAt) {
          await mongoose.connection.db.collection('files.chunks').updateMany(
            { files_id: ( await fileService.find(thumbFilename))._id },
            { $set: { "metadata.expireAt": req.expireAt }}
          );
        }
        thumbnail = thumbFilename;
      }

      return res.status(201).send({
        message: 'File uploaded successfully',
        filename: encodeURIComponent(req.file.filename),
        thumbnail: thumbnail ? encodeURIComponent(thumbnail) : undefined
      })

    } catch (err) {
      if (err?.source === "FileContentVerification") {
        let error_message = err?.message || "Content verification failed";
        winston.warn("File content verification failed. Message: ", error_message);
        return res.status(403).send({ success: false, error: error_message })
      }

      winston.error("Error uploading asset", err);
      return res.status(500).send({ success: false, error: "Error uploading asset" });

    }
  })
})

/**
 * Upload user profile photo or bot avatar
 * Path: uploads/users/{user_id|bot_id}/images/photo.jpg
 * This maintains compatibility with clients that expect fixed paths.
 * Profile photos/avatars have no retention.
 */
router.post('/users/photo', [
  passport.authenticate(['basic', 'jwt'], { session: false }),
  validtoken,
  roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])
], async (req, res) => {

  uploadAvatar(req, res, async (err) => {
    if (err instanceof multer.MulterError) {
      // A Multer error occurred when uploading
      winston.error(`Multer replied with code ${err?.code} and message "${err?.message}"`);
      let status = 400;
      if (err?.code === 'LIMIT_FILE_SIZE') {
        status = 413;
      }
      return res.status(status).send({ success: false, error: err?.message || 'An error occurred while uploading the file', code: err.code });
    } else if (err) {
      // An unknown error occurred when uploading.
      winston.error(`Multer replied with status ${err?.status} and message "${err?.message}"`);
      let status = err?.status || 400;
      return res.status(status).send({ success: false, error: err.message || "An error occurred while uploading the file" })
    }

    try {
      winston.debug("/users/photo");
  
      if (!req.file) {
        return res.status(400).send({ success: false, error: 'No file uploaded' });
      }
  
      let userid = req.user.id;
      let bot_id;
      let entity_id = userid;
  
      if (req.query.bot_id) {
        bot_id = req.query.bot_id;
  
        let chatbot = await faq_kb.findById(bot_id).catch((err) => {
          winston.error("Error finding bot ", err);
          return res.status(500).send({ success: false, error: "Unable to find chatbot with id " + bot_id });
        });
  
        if (!chatbot) {
          return res.status(404).send({ success: false, error: "Chatbot not found" });
        }
  
        let id_project = chatbot.id_project;
  
        let puser = await project_user.findOne({ id_user: userid, id_project: id_project }).catch((err) => {
          winston.error("Error finding project user: ", err);
          return res.status(500).send({ success: false, error: "Unable to find project user for user " + userid + "in project " + id_project });
        });
  
        if (!puser) {
          winston.warn("User " + userid + " doesn't belong to the project " + id_project);
          return res.status(401).send({ success: false, error: "You don't belong to the chatbot's project" });
        }
  
        if ((puser.role !== roleConstants.ADMIN) && (puser.role !== roleConstants.OWNER)) {
          winston.warn("User with role " + puser.role + " can't modify the chatbot");
          return res.status(403).send({ success: false, error: "You don't have the role required to modify the chatbot" });
        }
  
        entity_id = bot_id;
      }
  
      var destinationFolder = 'uploads/users/' + entity_id + "/images/";
      winston.debug("destinationFolder:" + destinationFolder);
  
      var thumFilename = destinationFolder + 'thumbnails_200_200-photo.jpg';
  
      winston.debug("req.file.filename:" + req.file.filename);
      const buffer = await fileService.getFileDataAsBuffer(req.file.filename);
  
      try {
        const resizeImage = await sharp(buffer).resize(200, 200).toBuffer();
        // Use the same contentType as the original file for the thumbnail
        await fileService.createFile(thumFilename, resizeImage, undefined, req.file.mimetype);
        let thumFile = await fileService.find(thumFilename);
        winston.debug("thumFile", thumFile);
  
        return res.status(201).json({
          message: 'Image uploaded successfully',
          filename: encodeURIComponent(req.file.filename),
          thumbnail: encodeURIComponent(thumFilename)
        });
      } catch (thumbErr) {
        winston.error("Error generating or creating thumbnail", thumbErr);
        // Still return success for the main file, but log thumbnail error
        return res.status(201).json({
          message: 'Image uploaded successfully',
          filename: encodeURIComponent(req.file.filename),
          thumbnail: undefined
        });
      }
  
    } catch (error) {
      winston.error('Error uploading user image.', error);
      return res.status(500).send({ success: false, error: 'Error uploading user image.' });
    }

  })
})


router.get("/", [
  passport.authenticate(['basic', 'jwt'], { session: false }), 
  validtoken,
], async (req, res) => {
  winston.debug('path', req.query.path);

  if (req.query.as_attachment) {
    res.set({ "Content-Disposition": "attachment; filename=\""+req.query.path+"\"" });
  }
  
  let fService = fileService;
  try {
    let file = await fileService.find(req.query.path);
    res.set({ "Content-Length": file.length});
    res.set({ "Content-Type": file.contentType});
  } catch (e) {
    if (e.code == "ENOENT") {
      winston.debug(`File ${req.query.path} not found on primary file service. Fallback to secondary.`)
      
      // To instantiate fallbackFileService here where needed you need to wait for the open event.
      // Instance moved on top
      // await new Promise(r => fallbackFileService.conn.once("open", r));

      try {
        let file = await fallbackFileService.find(req.query.path)
        res.set({ "Content-Length": file.length });
        res.set({ "Content-Type": file.contentType });
        fService = fallbackFileService;
      } catch (e) {
        if (e.code == "ENOENT") {
          winston.debug(`File ${req.query.path} not found on seconday file service.`)
          return res.status(404).send({ success: false, error: 'File not found.' });
        }else {
          winston.error('Error getting file: ', e);
          return res.status(500).send({success: false, error: 'Error getting file.'});
        }
      }

    } else {
      winston.error('Error getting file', e);
      return res.status(500).send({success: false, error: 'Error getting file.'});
    }
  }
  
  fService.getFileDataAsStream(req.query.path).pipe(res);
});

router.get("/download", [
  passport.authenticate(['basic', 'jwt'], { session: false }), 
  validtoken,
], (req, res) => {
  winston.debug('path', req.query.path);

  let filename = pathlib.basename(req.query.path);
  winston.debug("filename:"+filename);

  res.attachment(filename);
  fileService.getFileDataAsStream(req.query.path).pipe(res);
});

/**
 * Delete a file (and its thumbnail if it's an image)
 * Works for both profile photos/avatars and project assets
 * 
 * Example:
 * curl -v -X DELETE -u user:pass \
 *   http://localhost:3000/filesp?path=uploads%2Fusers%2F65c5f3599faf2d04cd7da528%2Fimages%2Fphoto.jpg
 * 
 * curl -v -X DELETE -u user:pass \
 *   http://localhost:3000/filesp?path=uploads%2Fprojects%2F65c5f3599faf2d04cd7da528%2Ffiles%2Fuuid%2Flogo.png
 */
router.delete("/", [
  passport.authenticate(['basic', 'jwt'], { session: false }), 
  validtoken,
], async (req, res) => {
  try {
    winston.debug("delete file");
    
    let filePath = req.query.path;
    if (!filePath) {
      return res.status(400).send({ success: false, error: 'Path parameter is required' });
    }
    
    winston.debug("path:" + filePath);

    let filename = pathlib.basename(filePath);
    winston.debug("filename:" + filename);

    if (!filename) {
      winston.warn('Error deleting file. No filename specified:' + filePath);
      return res.status(400).send({ success: false, error: 'No filename specified in path' });
    }

    // Determine which service to use based on path
    // Try primary service first (files bucket)
    let fService = fileService;
    let fileExists = false;
    
    try {
      await fileService.find(filePath);
      fileExists = true;
    } catch (e) {
      if (e.code == "ENOENT") {
        winston.debug(`File ${filePath} not found on primary file service. Trying fallback.`);
        try {
          await fallbackFileService.find(filePath);
          fService = fallbackFileService;
          fileExists = true;
        } catch (e2) {
          if (e2.code == "ENOENT") {
            winston.debug(`File ${filePath} not found on fallback file service.`);
            return res.status(404).send({ success: false, error: 'File not found.' });
          } else {
            winston.error('Error checking file on fallback service: ', e2);
            return res.status(500).send({ success: false, error: 'Error checking file existence.' });
          }
        }
      } else {
        winston.error('Error checking file on primary service: ', e);
        return res.status(500).send({ success: false, error: 'Error checking file existence.' });
      }
    }

    // Delete the main file
    try {
      const deletedFile = await fService.deleteFile(filePath);
      winston.debug("File deleted successfully:", deletedFile.filename);

      // Check if this is an image and try to delete thumbnail
      // Thumbnail pattern: thumbnails_200_200-{filename}
      // For profile photos: thumbnails_200_200-photo.jpg
      // For assets: thumbnails_200_200-{original_filename}
      const isImage = images_extensions.some(ext => filename.toLowerCase().endsWith(ext));
      
      if (isImage) {
        let thumbFilename = 'thumbnails_200_200-' + filename;
        let thumbPath = filePath.replace(filename, thumbFilename);
        winston.debug("thumbPath:" + thumbPath);

        try {
          // Try to delete thumbnail from the same service
          await fService.deleteFile(thumbPath);
          winston.debug("Thumbnail deleted successfully:" + thumbPath);
        } catch (thumbErr) {
          // Thumbnail might not exist or be in different service, try fallback
          if (thumbErr.code == "ENOENT" || thumbErr.msg == "File not found") {
            winston.debug(`Thumbnail ${thumbPath} not found on ${fService === fileService ? 'primary' : 'fallback'} service. Trying other service.`);
            
            const otherService = fService === fileService ? fallbackFileService : fileService;
            try {
              await otherService.deleteFile(thumbPath);
              winston.debug("Thumbnail deleted from fallback service:" + thumbPath);
            } catch (fallbackThumbErr) {
              // Thumbnail doesn't exist, that's ok
              winston.debug(`Thumbnail ${thumbPath} not found on fallback service either. Skipping.`);
            }
          } else {
            winston.error('Error deleting thumbnail:', thumbErr);
            // Don't fail the whole request if thumbnail deletion fails
          }
        }
      }

      return res.status(200).json({
        message: 'File deleted successfully',
        filename: encodeURIComponent(deletedFile.filename)
      });

    } catch (deleteErr) {
      winston.error('Error deleting file:', deleteErr);
      return res.status(500).send({ success: false, error: 'Error deleting file.' });
    }

  } catch (error) {
    winston.error('Error in delete endpoint:', error);
    return res.status(500).send({ success: false, error: 'Error deleting file.' });
  }
});


module.exports = router;