'use strict';

//libs
const fs = require('fs');
const path = require('path');

//local libs
const UtilFileUploadData = require('../../utils/UtilFileUploadData');
const ErrorCodes = require('../../../shared/constants/ErrorCodes');
const UtilDocumentId = require('../../../shared/utils/UtilDocumentId');
const UtilError = require('../../utils/UtilError');

// global constants
const DEFAULT_UPLOAD_DIR = 'files'; // relative to root project folder

//globals
let fileStorageConfig = null;

/**
 * Retrieves the upload path from the configuration settings.
 * Attemps to create the directory if not present and tests if read/write is possible on that directory.
 *
 * @returns {string} This method returns null if there is a file system error or a permission error.
 */
let uploadDirPath = null;

function getUploadDirectory(cb){

    if(uploadDirPath != null){
        cb(null, uploadDirPath);
        return;
    }

    fileStorageConfig = fileStorageConfig ? fileStorageConfig : config.get('fileStorage');

    // use default upload directory if settings not present
    if(!fileStorageConfig || !fileStorageConfig.path){

        fileStorageConfig = {
            path : DEFAULT_UPLOAD_DIR
        };
    }

    // convert to absolute path
    uploadDirPath = path.resolve(fileStorageConfig.path);

    // check if upload directory exists and create if not present
    UtilFileUploadData.existsAndWritable(uploadDirPath, function (err){

        if(!err){
            // directory already exists
            cb(null, uploadDirPath);
            return;
        }

        logger.error(`Uploads folder doesn't exist or isn't accessible.`);

        // prepare response error:
        const message = "Error in upload. Please see an administrator and quote error code: " +
            ErrorCodes.ERROR_UPLOAD_DIR_DOES_NOT_EXIST;
        err = new Error(message);
        err.errorCode = ErrorCodes.ERROR_UPLOAD_DIR_DOES_NOT_EXIST;
        cb(err);

    });
}

/**
 * Retrieves the absolute filename of the file.
 * @param fid
 * @returns {string}
 */
function getFilePath(fid, basePath, cb){

    getUploadDirectory(function(err, uploadDirPath){

        if(err){
            return cb(err);
        }

        // Use join instead of resolve so that it's always the normalized path merging two paths together that's returned
        // See https://stackoverflow.com/a/39111164
        let filePath = path.join(uploadDirPath, fid);

        if (basePath) {
            filePath = path.join(uploadDirPath, basePath, fid);
        }


        // Check if there was directory traversal.
        if (filePath.indexOf(uploadDirPath) !== 0) {
            const errorMsg = `Attempted directory traversal: '${fid}'; To path: '${filePath}' from uploads dir: '${uploadDirPath}'`;
            logger.error(errorMsg);
            cb(new UtilError.InvalidRequestError(errorMsg));
            return;
        }

        cb(err, filePath);
    });

}

/**
 * @deprecated - file upload is now handled by multer, this can be removed if no longer used
 * PlainDirectoryProvider assumes that the client assigns the fileNo. Hence it will terminate without it.
 * @param uploadInfo - blob data, fileNo, isImage
 */
exports.uploadFiles = async function(uploadInfo) {

    //generate fileId if not present
    const fileNo    = uploadInfo.fileNo != null ? uploadInfo.fileNo : UtilDocumentId.generateId();
    const fileData  = uploadInfo.fileData;
    const file      = uploadInfo.file;

    // write actual file to directory
    let filePath = null;
    try{
        filePath = await getFilePathPromised(fileNo);
        let result = {};

        const alreadyExists = await fileAlreadyExists(filePath);
        if(alreadyExists){
            // don't write file.
            // Report success to client. See (https://mantis.formbird.com/view.php?id=13615)
            logger.error(`PlainDirectoryProvider > File already exists: ${fileNo}`);
            result.alreadyExists = true; // alreadyExists flag used in testing
            result.fileNo = fileNo;
            return result;
        }

        // persist file in uploads directory
        if(fileData != null){
            const buf = UtilFileUploadData._base64ToBuffer(fileData);
            result = getFileInfoFromBuffer(buf);
            await saveBufferFile(filePath, buf);
        }else if(file){
            await saveFile(filePath, file);
        }else{
            const message = `Invalid file data uploaded for fileNo: ${fileNo}`;
            logger.error(`PlainDirectoryProvider > ${message}`);
            throw new UtilError.InvalidRequestError(message);
        }

        logger.info("PlainDirectoryProvider > Successfully written file: " + filePath);

        result.fileNo = fileNo;
        return result;

    }catch(err){
        // These should be server errors.
        logger.error(`PlainDirectoryProvider > Error writing file:  ${filePath != null ? filePath : fileNo}. See details:`);
        logger.error(err);
        throw err;
    }

    function getFilePathPromised(fileNo, basePath) {
        return new Promise((resolve, reject) => {
            getFilePath(fileNo, basePath, function(err, filePath){
                if(err){
                    reject(err);
                }else{
                    resolve(filePath);
                }
            });

        });
    }

    function fileAlreadyExists(filePath){
        return new Promise((resolve) => {
            // If existsAndWritable does not produce an error, the the file already exists.
            UtilFileUploadData.existsAndWritable(filePath, function(err){
                const alreadyExists = (err == null);
                resolve(alreadyExists);
            });
        });
    }

    function saveBufferFile(filePath, bufferData){
        return new Promise((resolve, reject) => {
            UtilFileUploadData.saveBufferFile(filePath, bufferData, function(err){
                if(err){
                    reject(err);
                }
                resolve();
            });
        });
    }

    function saveFile(filePath, file){
        return new Promise((resolve, reject) => {
            UtilFileUploadData.saveFile(filePath, file, function(err){
                if(err){
                    reject(err);
                }
                resolve();
            });
        });
    }

    //https://mantis.formbird.com/view.php?id=11543#c39505
    //Using data from the file's contents, extract the extension and mime type and use that for checking if it's whitelisted.
    //This is the second whitelist checking. It reads a sample of the file and extracts its magic number to get the file type.
    //The first check is at UploadFileController.uploadFiles where we only checked the declared file type and not the content.
    function getFileInfoFromBuffer(fileDataBuffer){
        let result = UtilFileUploadData.bufferFileType(fileDataBuffer); //returns {ext, mime}
        result = result != null ? result : {};

        const blackListed = !UtilFileUploadData.whiteListed(result.ext);
        if(blackListed){
            throw new UtilError.InvalidRequestError("File type not allowed.");
        }
        return result;
    }

};

// fileRequest.fileNo
exports.getFile = function(fileRequest, cb) {
    const fileNo = fileRequest.fileNo;
    const basePath = fileRequest.basePath;

    getFilePath(fileNo, basePath, function(err, filePath){
        if(err){
            return cb(err);
        }
        UtilFileUploadData.existsAndWritable(filePath, function(err){
            if(err){
                cb(new UtilError.NotFoundError("File not found: " + fileNo));
            }else{
                cb(null, { provider: "PlainDirectory", filePath : filePath });
            }
        });
    });
};

// Deletes the file permanently in the file system. We don't normally use this in deletes at the platform.
// Files are normally deleted by destroying their file reference document so they are no longer accessible
// by the UploadFileController. Destroying the file reference uses 'soft' delete where its systemHeader.deleted
// flag is set to false. See UploadFileController.deleteFile and UtilFileReference.destroyFileReference
// Use this in utility functions only.
exports.deleteFile = function(fileNo, basePath, cb) {

    getFilePath(fileNo, basePath, function(err, filePath){
        if(err){
            cb(err);
            return;
        }
        UtilFileUploadData.deleteFiles(filePath, function(err){
            if(err){
                logger.error("PlainDirectoryProvider > Failed to remove file: " + filePath + ".");
                logger.error(err);
            }else{
                logger.info("PlainDirectoryProvider > Successfully removed file: " + filePath + ".");
            }
            cb(err);
        });
    });

};
