/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.sys.batch.service.impl;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.krad.exception.AuthorizationException;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants.SystemGroupParameterNames;
import org.kuali.kfs.sys.batch.BatchInputFileType;
import org.kuali.kfs.sys.batch.service.BatchInputFileService;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.exception.FileStorageException;
import org.kuali.kfs.sys.exception.ParseException;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.kim.api.identity.Person;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Provides batch input file management, including listing files, parsing, downloading, storing, and deleting.
 */
public class BatchInputFileServiceImpl implements BatchInputFileService {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BatchInputFileServiceImpl.class);

    /**
     * Delegates to the batch input file type to parse the file.
     *
     * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#parse(org.kuali.kfs.sys.batch.BatchInputFileType, byte[])
     */
    @Override
    public Object parse(BatchInputFileType batchInputFileType, byte[] fileByteContent) {
        try {
            return batchInputFileType.parse(fileByteContent);
        } catch (ParseException e) {
            LOG.error("Error encountered parsing file", e);
            throw e;
        }
    }

    /**
     * Defers to batch type to do any validation on the parsed contents.
     *
     * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#validate(org.kuali.kfs.sys.batch.BatchInputFileType, java.lang.Object)
     */
    @Override
    public boolean validate(BatchInputFileType batchInputFileType, Object parsedObject) {
        if (batchInputFileType == null || parsedObject == null) {
            LOG.error("an invalid(null) argument was given");
            throw new IllegalArgumentException("an invalid(null) argument was given");
        }

        boolean contentsValid = true;
        contentsValid = batchInputFileType.validate(parsedObject);
        return contentsValid;
    }

    /**
     * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#save(org.kuali.rice.kim.api.identity.Person,
     * org.kuali.kfs.sys.batch.BatchInputFileType, java.lang.String, java.io.InputStream)
     */
    @Override
    public String save(Person user, BatchInputFileType batchInputFileType, String fileUserIdentifier, InputStream fileContents, Object parsedObject) throws AuthorizationException, FileStorageException {
        if (user == null || batchInputFileType == null || fileContents == null) {
            LOG.error("an invalid(null) argument was given");
            throw new IllegalArgumentException("an invalid(null) argument was given");
        }

        if (!isFileUserIdentifierProperlyFormatted(fileUserIdentifier)) {
            LOG.error("The following file user identifer was not properly formatted: " + fileUserIdentifier);
            throw new IllegalArgumentException("The following file user identifer was not properly formatted: " + fileUserIdentifier);
        }

        // defer to batch input type to add any security or other needed information to the file name
        String saveFileName = batchInputFileType.getDirectoryPath() + "/" + batchInputFileType.getFileName(user.getPrincipalName(), parsedObject, fileUserIdentifier);
        if (!StringUtils.isBlank(batchInputFileType.getFileExtension())) {
            saveFileName += "." + batchInputFileType.getFileExtension();
        }

        if (batchInputFileType.shouldSave()) {
            // consruct the file object and check for existence
            File fileToSave = new File(saveFileName);
            if (fileToSave.exists()) {
                LOG.error("cannot store file, name already exists " + saveFileName);
                throw new FileStorageException("Cannot store file because the name " + saveFileName + " already exists on the file system.");
            }

            try {
                FileOutputStream fos = new FileOutputStream(fileToSave);
                while (fileContents.available() > 0) {
                    fos.write(fileContents.read());
                }
                fos.flush();
                fos.close();

                createDoneFile(fileToSave, batchInputFileType);

                batchInputFileType.process(saveFileName, parsedObject);
            } catch (IOException e) {
                LOG.error("unable to save contents to file " + saveFileName, e);
                throw new RuntimeException("errors encountered while writing file " + saveFileName, e);
            }
        } else {
            batchInputFileType.process(saveFileName, parsedObject);
        }
        return saveFileName;
    }

    /**
     * Creates a '.done' file with the name of the batch file.
     */
    protected void createDoneFile(File batchFile, BatchInputFileType batchInputFileType) {
        String fileExtension = batchInputFileType.getFileExtension();
        File doneFile = generateDoneFileObject(batchFile, fileExtension);
        String doneFileName = doneFile.getName();

        if (!doneFile.exists()) {
            boolean doneFileCreated = false;
            try {
                doneFileCreated = doneFile.createNewFile();
            } catch (IOException e) {
                LOG.error("unable to create done file " + doneFileName, e);
                throw new RuntimeException("Errors encountered while saving the file: Unable to create .done file " + doneFileName, e);
            }

            if (!doneFileCreated) {
                LOG.error("unable to create done file " + doneFileName);
                throw new RuntimeException("Errors encountered while saving the file: Unable to create .done file " + doneFileName);
            }
        }
    }

    /**
     * This method is responsible for creating a File object that represents the done file. The real file represented on disk may
     * not exist
     *
     * @param batchInputFile
     * @return a File object representing the done file. The real file may not exist on disk, but the return value can be used to
     * create that file.
     */
    protected File generateDoneFileObject(File batchInputFile, String fileExtension) {
        String doneFileName = fileExtension != null ? StringUtils.substringBeforeLast(batchInputFile.getPath(), ".") + ".done" :
            batchInputFile.getPath() + ".done";
        File doneFile = new File(doneFileName);
        return doneFile;
    }

    /**
     * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#isBatchInputTypeActive(org.kuali.kfs.sys.batch.BatchInputFileType)
     */
    @Override
    public boolean isBatchInputTypeActive(BatchInputFileType batchInputFileType) {
        if (batchInputFileType == null) {
            LOG.error("an invalid(null) argument was given");
            throw new IllegalArgumentException("an invalid(null) argument was given");
        }

        List<String> activeInputTypes = new ArrayList<String>(SpringContext.getBean(ParameterService.class).getParameterValuesAsString(KfsParameterConstants.FINANCIAL_SYSTEM_BATCH.class, SystemGroupParameterNames.ACTIVE_INPUT_TYPES_PARAMETER_NAME));

        boolean activeBatchType = false;
        if (activeInputTypes.size() > 0 && activeInputTypes.contains(batchInputFileType.getFileTypeIdentifer())) {
            activeBatchType = true;
        }

        return activeBatchType;
    }

    /**
     * Fetches workgroup for batch type from system parameter and verifies user is a member. Then a list of all files for the batch
     * type are retrieved. For each file, the file and user is sent through the checkAuthorization method of the batch input type
     * implementation for finer grained security. If the method returns true, the filename is added to the user's list.
     *
     * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#listBatchTypeFilesForUser(org.kuali.kfs.sys.batch.BatchInputFileType,
     * org.kuali.rice.kim.api.identity.Person)
     */
    @Override
    public List<String> listBatchTypeFilesForUser(BatchInputFileType batchInputFileType, Person user) throws AuthorizationException {
        if (batchInputFileType == null || user == null) {
            LOG.error("an invalid(null) argument was given");
            throw new IllegalArgumentException("an invalid(null) argument was given");
        }

        File[] filesInBatchDirectory = listFilesInBatchTypeDirectory(batchInputFileType);

        List<String> userFileNamesList = new ArrayList<String>();
        List<File> userFileList = listBatchTypeFilesForUserAsFiles(batchInputFileType, user);

        for (File userFile : userFileList) {
            userFileNamesList.add(userFile.getAbsolutePath());
        }

        return userFileNamesList;
    }

    protected List<File> listBatchTypeFilesForUserAsFiles(BatchInputFileType batchInputFileType, Person user) throws AuthorizationException {
        File[] filesInBatchDirectory = listFilesInBatchTypeDirectory(batchInputFileType);

        List<File> userFileList = new ArrayList<File>();
        if (filesInBatchDirectory != null) {
            for (int i = 0; i < filesInBatchDirectory.length; i++) {
                File batchFile = filesInBatchDirectory[i];
                String fileExtension = StringUtils.substringAfterLast(batchFile.getName(), ".");
                if (StringUtils.isBlank(batchInputFileType.getFileExtension()) || batchInputFileType.getFileExtension().equals(fileExtension)) {
                    if (user.getPrincipalName().equals(batchInputFileType.getAuthorPrincipalName(batchFile))) {
                        userFileList.add(batchFile);
                    }
                }
            }
        }
        return userFileList;
    }

    /**
     * Returns List of filenames for existing files in the directory given by the batch input type.
     */
    protected File[] listFilesInBatchTypeDirectory(BatchInputFileType batchInputFileType) {
        File batchTypeDirectory = new File(batchInputFileType.getDirectoryPath());
        return batchTypeDirectory.listFiles();
    }

    /**
     * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#listInputFileNamesWithDoneFile(org.kuali.kfs.sys.batch.BatchInputFileType)
     */
    @Override
    public List<String> listInputFileNamesWithDoneFile(BatchInputFileType batchInputFileType) {
        if (batchInputFileType == null) {
            LOG.error("an invalid(null) argument was given");
            throw new IllegalArgumentException("an invalid(null) argument was given");
        }

        File batchTypeDirectory = new File(batchInputFileType.getDirectoryPath());
        File[] doneFiles = batchTypeDirectory.listFiles(new DoneFilenameFilter());

        List<String> batchInputFiles = new ArrayList<String>();
        for (int i = 0; i < doneFiles.length; i++) {
            File doneFile = doneFiles[i];

            String dataFileName = StringUtils.substringBeforeLast(doneFile.getPath(), ".");
            if (!StringUtils.isBlank(batchInputFileType.getFileExtension())) {
                dataFileName += "." + batchInputFileType.getFileExtension();
            }

            File dataFile = new File(dataFileName);
            if (dataFile.exists()) {
                batchInputFiles.add(dataFile.getPath());
            }
        }

        return batchInputFiles;
    }

    /**
     * Retrieves files in a directory with the .done extension.
     */
    protected class DoneFilenameFilter implements FilenameFilter {
        /**
         * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String)
         */
        @Override
        public boolean accept(File dir, String name) {
            return name.endsWith(".done");
        }
    }

    /**
     * For this implementation, a file user identifier must consist of letters and digits
     *
     * @see org.kuali.kfs.sys.batch.service.BatchInputFileService#isFileUserIdentifierProperlyFormatted(java.lang.String)
     */
    @Override
    public boolean isFileUserIdentifierProperlyFormatted(String fileUserIdentifier) {
        if (ObjectUtils.isNull(fileUserIdentifier)) {
            return false;
        }
        for (int i = 0; i < fileUserIdentifier.length(); i++) {
            char c = fileUserIdentifier.charAt(i);
            if (!(Character.isLetterOrDigit(c))) {
                return false;
            }
        }
        return true;
    }
}

