package com.workfusion.odf.impl.connector;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Named;

import groovy.lang.Binding;
import jcifs.smb.SmbException;
import jcifs.smb.SmbFile;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import com.workfusion.odf.api.connector.TransactionSupplier;
import com.workfusion.odf.api.domain.Document;
import com.workfusion.odf.api.domain.Transaction;
import com.workfusion.odf.impl.s3.S3Uploader;
import com.workfusion.rpa.core.storage.S3Manager;

/**
 * Share Folder (SMB) connector.
 * <p>
 * NOTE: JCIFS supports SMB v1 only (https://stackoverflow.com/questions/45015298/smb-version-2-upgrade-issues)
 * <p>
 * SMB URL examples:
 * smb://[[[domain;]username[:password]@]server[:port]/[[share/[dir/]file]]][?[param=value[param2=value2[...]]]
 * smb://storage15/public/foo.txt
 * smb://tor:P%40ss@msmith1/c/WINDOWS/Desktop/foo.txt
 * smb://username:password@server/share/path/to/file.txt
 *
 * @author Andrei Urbanovich
 */
public class ShareFolderTransactionSupplier implements TransactionSupplier {

    private static final String S3_FOLDER = "shared-folder";
    public static final String SMB_PROTOCOL = "smb://";

    // Parameter names ////////////////////////////////////////////////////////
    public static final String PARAM_HOST = "sf_host";
    public static final String PARAM_PATH = "sf_path";
    public static final String PARAM_ALIAS = "sf_alias";
    public static final String PARAM_WILDCARD = "sf_wildcard";

    public static final String PARAM_ACTION_ON_READ = "sf_action_on_read";
    public static final String PARAM_ACTION_ON_READ_MARK_AS_READ = "mark_as_read";
    public static final String PARAM_ACTION_ON_READ_DELETE_ON_READ = "delete_on_read";
    public static final String PARAM_ACTION_ON_READ_DO_NOTHING = "do_nothing";

    // Parameter default values ///////////////////////////////////////////////
    public static final String PARAM_WILDCARD_DEFAULT_VALUE = "*";

    public static final String MARK_AS_READ_POSTFIX = "_processed";

    private final SmbFileBuilder smbFileBuilder;
    private final Logger logger;
    private final Map<String, String> params;
    private final S3Uploader s3Uploader;


    @Inject
    public ShareFolderTransactionSupplier(final S3Manager s3Manager, final SmbFileBuilder smbFileBuilder,
    		                              final Logger logger, final Binding binding,
                                          @Named("botConfigParams") final Map<String, String> params) {
        this.smbFileBuilder = smbFileBuilder;
        this.logger = logger;
        this.params = params;
        s3Uploader = new S3Uploader(s3Manager, logger, S3_FOLDER);
    }

    @Override
    public Collection<Transaction> get() {
        String homeDirPath = buildHomeDirPath(params);
        WildcardFileFilter fileFilter = buildFileFilter(params);
        String actionOnRead = params.get(PARAM_ACTION_ON_READ);

        logger.debug("Downloading files from SMB path '{}', file filter='{}', " + PARAM_ACTION_ON_READ + "={}",
        		homeDirPath, fileFilter.toString(), actionOnRead);

        return doLoad(homeDirPath, fileFilter, actionOnRead);
    }

    private Collection<Transaction> doLoad(final String homeDirPath, final WildcardFileFilter fileFilter, final String actionOnRead) {
        List<Transaction> transactions = new ArrayList<>();

        boolean removeOnRead = PARAM_ACTION_ON_READ_DELETE_ON_READ.equals(actionOnRead);
        boolean markAsRead = PARAM_ACTION_ON_READ_MARK_AS_READ.equals(actionOnRead);

        // Tree of the ALL listed SMB files
        //  - key: file/folder name
        //  - value:
        //        - SmbFile, if the entry is a file
        //        - Map of the folder SmbFile files, if the entry is a folder
        Map<String, Object> filesTree = new HashMap<>();

        try {
            SmbFile homeDir = smbFileBuilder.buildSmbFileByPath(homeDirPath);

            // Cache ALL 1-st level files and 2-nd level files
            SmbFile[] firstLevelALLSmbFiles = homeDir.listFiles();
            for (SmbFile firstLevelSmbFile : firstLevelALLSmbFiles) {
                if (firstLevelSmbFile.isDirectory()) {
                    Map<String, Object> folderFiles = new HashMap<>();
                    SmbFile[] smbFiles = firstLevelSmbFile.listFiles();
                    for (SmbFile smbFile : smbFiles) {
                        if (smbFile.isFile()) {
                            folderFiles.put(FilenameUtils.getName(smbFile.getName()), smbFile);
                        }
                    }

                    filesTree.put(FilenameUtils.getName(firstLevelSmbFile.getName()), folderFiles);
                } else {
                    filesTree.put(FilenameUtils.getName(firstLevelSmbFile.getName()), firstLevelSmbFile);
                }
            }

            SmbFile[] firstLevelSmbFiles = homeDir.listFiles((dir, name) -> {
                File file = new File(name);
                return fileFilter.accept(file);
            });
            for (SmbFile firstLevelSmbFile : firstLevelSmbFiles) {
                String firstLevelFileName = FilenameUtils.getName(firstLevelSmbFile.getName());
                // Don't touch the "_processed" file
                if (isMarkAsReadFile(firstLevelFileName)) {
                	continue;
                }

                if (firstLevelSmbFile.isDirectory()) {
                    logger.debug("[>] 1-st level SMB folder: {}", firstLevelSmbFile.getPath());

                    // List the folder files
                    Transaction transaction = new Transaction();
                    transaction.setId(generateTransactionId());
                    transaction.setDocs(new ArrayList<>());
                    SmbFile[] smbFiles = firstLevelSmbFile.listFiles();
                    for (SmbFile smbFile : smbFiles) {
                        String fileName = FilenameUtils.getName(smbFile.getName());
                        if (isMarkAsReadFile(fileName)) {
                        	continue;
                        }

                        if (smbFile.isFile()) {
                            logger.debug("[>] 2-nd level SMB file: {}", smbFile.getPath());

                            Document doc = new Document();
                            doc.setId(generateDocumentId());
                            transaction.getDocs().add(doc);

                            // Upload to S3
                            String fileLink = s3Uploader.upload(smbFile);
                            doc.setOriginalDocumentLink(fileLink);
                            doc.setName(fileName);

                            String processedFileName = getFileNameWithMarkAsReadPostfix(fileName);

                            if (removeOnRead) {
                                deleteSmbFile(smbFile);

                                // Remove 'processed' file, if any
                                @SuppressWarnings("unchecked")
								Map<String, Object> folderFiles = (Map<String, Object>) filesTree.get(firstLevelFileName);
                                Object processedFileObj = folderFiles.get(processedFileName);
                                if (processedFileObj != null) {
                                    deleteSmbFile((SmbFile) processedFileObj);
                                }
                            } else if (markAsRead) {
                                @SuppressWarnings("unchecked")
								Map<String, Object> folderFiles = (Map<String, Object>) filesTree.get(firstLevelFileName);
                                Object processedFileObj = folderFiles.get(processedFileName);
                                if (processedFileObj == null) {
                                    SmbFile processedSmbFile = smbFileBuilder.buildSmbFileByPath(getFileNameWithMarkAsReadPostfix(smbFile.getPath()));
                                    logger.debug("Rename '{}' file -> to '{}'", smbFile.getPath(), processedSmbFile.getPath());
                                    smbFile.renameTo(processedSmbFile);
                                }
                            }
                        } else {
                            logger.debug("[>] 2-nd level SMB folder '{}' is not processed", smbFile.getPath());
                        }
                    }

                    if (transaction.getDocs().size() > 0) {
                        logger.debug("Creating transactions with {} documents", transaction.getDocs().size());
                        transactions.add(transaction);
                    }

                    if (removeOnRead) {
                        deleteSmbFile(firstLevelSmbFile);
                    }
                    //////////////////////////////////////////////////
                    // NOTE: SMB folders are NOT allowed to be renamed
                    //////////////////////////////////////////////////
                } else {
                    logger.debug("[>] 1-st level SMB file: {}", firstLevelSmbFile.getPath());

                    Transaction transaction = new Transaction();
                    transaction.setId(generateTransactionId());

                    Document doc = new Document();
                    doc.setId(generateDocumentId());
                    List<Document> docs = new ArrayList<>();
                    docs.add(doc);
                    transaction.setDocs(docs);

                    // Upload to S3
                    String fileLink = s3Uploader.upload(firstLevelSmbFile);
                    doc.setOriginalDocumentLink(fileLink);
                    doc.setName(firstLevelFileName);

                    transactions.add(transaction);

                    String processedFileName = getFileNameWithMarkAsReadPostfix(firstLevelFileName);

                    if (removeOnRead) {
                        deleteSmbFile(firstLevelSmbFile);

                        // Remove 'processed' file, if any
                        Object processedFileObj = filesTree.get(processedFileName);
                        if (processedFileObj != null) {
                            deleteSmbFile((SmbFile) processedFileObj);
                        }
                    } else if (markAsRead) {
                        if (filesTree.get(processedFileName) == null) {
                            SmbFile processedSmbFile = smbFileBuilder.buildSmbFileByPath(getFileNameWithMarkAsReadPostfix(firstLevelSmbFile.getPath()));
                            logger.debug("Rename '{}' file -> to '{}'", firstLevelSmbFile.getPath(), processedSmbFile.getPath());
                            firstLevelSmbFile.renameTo(processedSmbFile);
                        }
                    }
                }
            }
        } catch (IOException ex) {
            logger.error("SMB error:", ex);
            throw new RuntimeException("Can't download files from Share Folder", ex);
        }

        return transactions;
    }

    private WildcardFileFilter buildFileFilter(final Map<String, String> params) {
        String filter = params.get(PARAM_WILDCARD);
        if (StringUtils.isBlank(filter)) {
            filter = PARAM_WILDCARD_DEFAULT_VALUE;
        }
        return new WildcardFileFilter(filter);
    }

    private String buildHomeDirPath(final Map<String, String> params) {
        String host = params.get(PARAM_HOST);
        String path = params.get(PARAM_PATH);
        if (StringUtils.isBlank(host)) {
            throw new IllegalArgumentException("'" + PARAM_HOST + "' parameter is required");
        }
        if (StringUtils.isBlank(path)) {
            throw new IllegalArgumentException("'" + PARAM_PATH + "' parameter is required");
        }
        if (path != null && !path.endsWith("/")) {
            path = path + "/";
        }

        return buildFullPath(host, path);
    }

    private String buildFullPath(final String host, final String path) {
        if (path.startsWith(SMB_PROTOCOL)) {
            return path;
        }

        StringBuilder sb = new StringBuilder();
        sb.append(SMB_PROTOCOL).append(host);
        if (!path.startsWith("/")) {
            sb.append('/');
        }
        sb.append(path);

        return sb.toString();
    }

    private String generateTransactionId() {
        return UUID.randomUUID().toString();
    }

    private String generateDocumentId() {
        return UUID.randomUUID().toString();
    }

    /**
     * Remove SMB file.
     * @param smbFile
     */
    private void deleteSmbFile(final SmbFile smbFile) throws SmbException {
        logger.debug("[-] Delete SMB {}: {}", smbFile.isDirectory() ? "folder" : "file", smbFile.getPath());
        smbFile.delete();
    }

    /**
     * Adds {@link #MARK_AS_READ_POSTFIX} to the given file name.
     *
     * @param fileName File name to convert to the "Mark as read" format.
     * @return File name, converted to "Mark as read" format.
     */
    private String getFileNameWithMarkAsReadPostfix(final String fileName) {
        String extension = FilenameUtils.getExtension(fileName);
        int iDot = fileName.lastIndexOf('.');
        String name = (iDot > 0) ? fileName.substring(0, iDot) : fileName;

        if (StringUtils.isNotEmpty(extension)) {
            return name + MARK_AS_READ_POSTFIX + "." + extension;
        } else {
            return name + MARK_AS_READ_POSTFIX;
        }
    }

    /**
     * Checks if given file is the "Mark as read" file.
     * @param fileName File name to check if it's the "Mark as read".
     * @return
     */
    private boolean isMarkAsReadFile(final String fileName) {
        int iDot = fileName.lastIndexOf('.');
        String name = (iDot > 0) ? fileName.substring(0, iDot) : fileName;

        return name.endsWith(MARK_AS_READ_POSTFIX);
    }
}
