package io.linuxserver.davos.transfer.ftp.connection;

import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.SftpException;

import io.linuxserver.davos.transfer.ftp.FTPFile;
import io.linuxserver.davos.transfer.ftp.connection.progress.ProgressListener;
import io.linuxserver.davos.transfer.ftp.connection.progress.SFTPProgressListener;
import io.linuxserver.davos.transfer.ftp.exception.DeleteFileException;
import io.linuxserver.davos.transfer.ftp.exception.DownloadFailedException;
import io.linuxserver.davos.transfer.ftp.exception.FTPException;
import io.linuxserver.davos.transfer.ftp.exception.FileListingException;
import io.linuxserver.davos.util.FileUtils;

public class SFTPConnection implements Connection {

    private static final Logger LOGGER = LoggerFactory.getLogger(SFTPConnection.class);

    private ChannelSftp channel;
    private FileUtils fileUtils = new FileUtils();
    private SFTPProgressListener progressListener;

    public SFTPConnection(ChannelSftp channel) {
        this.channel = channel;
    }

    @Override
    public String currentDirectory() {

        try {
            String pwd = channel.pwd();
            LOGGER.debug("{}", pwd);
            return pwd;
        } catch (SftpException e) {
            throw new FileListingException("Unable to print the working directory", e);
        }
    }

    @Override
    public void download(FTPFile file, String localFilePath) {

        String path = FileUtils.ensureTrailingSlash(file.getPath()) + file.getName();
        String cleanLocalPath = FileUtils.ensureTrailingSlash(localFilePath);
        
        LOGGER.debug("Download. Remote path: {}", path);
        LOGGER.debug("Download. Local path: {}", cleanLocalPath);

        try {

            if (file.isDirectory())
                downloadDirectoryAndContents(file, cleanLocalPath, path);
            else
                doGet(path, cleanLocalPath);

        } catch (SftpException e) {
            throw new DownloadFailedException("Unable to download file " + path, e);
        }
    }

    @Override
    public List<FTPFile> listFiles() {
        return listFiles(currentDirectory());
    }

    @Override
    public List<FTPFile> listFiles(String remoteDirectory) {

        try {

            String cleanRemoteDirectory = FileUtils.ensureTrailingSlash(remoteDirectory);

            List<FTPFile> files = new ArrayList<FTPFile>();

            LOGGER.debug("Listing files in {}", cleanRemoteDirectory);

            @SuppressWarnings("unchecked")
            Vector<LsEntry> lsEntries = channel.ls(cleanRemoteDirectory);

            for (LsEntry entry : lsEntries)
                files.add(toFtpFile(entry, cleanRemoteDirectory));

            LOGGER.debug("{}", files);
            LOGGER.debug("Listed {} items from remote directory {}", files.size(), cleanRemoteDirectory);

            return files;

        } catch (SftpException e) {
            throw new FileListingException(String.format("Unable to list files in directory %s", remoteDirectory), e);
        }
    }

    @Override
    public void setProgressListener(ProgressListener progressListener) {
        this.progressListener = (SFTPProgressListener) progressListener;
    }

    private void doGet(String fullRemotePath, String fullLocalDownloadPath) throws SftpException {

        LOGGER.debug("Performing channel.get from {} to {}", fullRemotePath, fullLocalDownloadPath);

        if (null != progressListener) {

            LOGGER.debug("Progress listener has been enabled");
            channel.get(fullRemotePath, fullLocalDownloadPath, progressListener);

        } else
            channel.get(fullRemotePath, fullLocalDownloadPath);
    }

    private void downloadDirectoryAndContents(FTPFile file, String localDownloadFolder, String path) throws SftpException {

        LOGGER.info("Item {} is a directory. Will now check sub-items", file.getName());
        List<FTPFile> subItems = listFiles(path).stream().filter(removeCurrentAndParentDirs()).collect(Collectors.toList());

        String fullLocalDownloadPath = FileUtils.ensureTrailingSlash(localDownloadFolder + file.getName());

        LOGGER.debug("Creating new local directory {}", fullLocalDownloadPath);
        fileUtils.createLocalDirectory(fullLocalDownloadPath);

        for (FTPFile subItem : subItems) {

            LOGGER.debug("{}", subItem);
            
            String subItemPath = FileUtils.ensureTrailingSlash(subItem.getPath()) + subItem.getName();
            
            if (subItem.isDirectory()) {

                String subLocalFilePath = FileUtils.ensureTrailingSlash(fullLocalDownloadPath);
                downloadDirectoryAndContents(subItem, subLocalFilePath, FileUtils.ensureTrailingSlash(subItemPath));
            }

            else {

                LOGGER.info("Downloading {} to {}", subItemPath, fullLocalDownloadPath);
                doGet(subItemPath, fullLocalDownloadPath);
            }
        }
    }

    private Predicate<? super FTPFile> removeCurrentAndParentDirs() {
        return file -> !file.getName().equals(".") && !file.getName().equals("..");
    }

    private FTPFile toFtpFile(LsEntry lsEntry, String filePath) throws SftpException {

        String name = lsEntry.getFilename();
        long fileSize = lsEntry.getAttrs().getSize();
        int mTime = lsEntry.getAttrs().getMTime();
        boolean directory = lsEntry.getAttrs().isDir();

        return new FTPFile(name, fileSize, filePath, (long) mTime * 1000, directory);
    }

    @Override
    public void deleteRemoteFile(FTPFile file) throws FTPException {

        String cleanRemotePath = FileUtils.ensureTrailingSlash(file.getPath()) + file.getName();
        LOGGER.debug("Deleting remote file {}", cleanRemotePath);

        try {

            if (file.isDirectory()) {
                deleteDirectoryAndContents(file, cleanRemotePath);
            } else
                doDelete(cleanRemotePath);

        } catch (SftpException e) {

            LOGGER.debug("channel threw exception. Assuming file not deleted");
            throw new DeleteFileException("Unable to delete file on remote server", e);
        }
    }
    
    private void deleteDirectoryAndContents(FTPFile file, String remoteDirectoryPath) throws SftpException {

        LOGGER.info("Item {} is a directory. Will now check sub-items", file.getName());
        List<FTPFile> subItems = listFiles(remoteDirectoryPath).stream().filter(removeCurrentAndParentDirs())
                .collect(Collectors.toList());
        
        for (FTPFile subItem : subItems) {
            
            LOGGER.debug("{}", subItem);
            
            String subItemPath = FileUtils.ensureTrailingSlash(subItem.getPath()) + subItem.getName();
            
            if (subItem.isDirectory())
                deleteDirectoryAndContents(subItem, subItemPath);
            else
                doDelete(subItemPath);
        }
        
        LOGGER.debug("Removing empty directory {}", remoteDirectoryPath);
        channel.rmdir(remoteDirectoryPath);
    }

    private void doDelete(String subItemPath) throws SftpException {
        
        LOGGER.debug("Deleting file: {}", subItemPath);
        channel.rm(subItemPath);
    }
}