/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.component.dropbox.core;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import com.dropbox.core.DbxDownloader;
import com.dropbox.core.DbxException;
import com.dropbox.core.v2.DbxClientV2;
import com.dropbox.core.v2.files.FileMetadata;
import com.dropbox.core.v2.files.ListFolderErrorException;
import com.dropbox.core.v2.files.ListFolderResult;
import com.dropbox.core.v2.files.Metadata;
import com.dropbox.core.v2.files.SearchOptions;
import com.dropbox.core.v2.files.SearchV2Result;
import com.dropbox.core.v2.files.WriteMode;
import org.apache.camel.Exchange;
import org.apache.camel.component.dropbox.dto.DropboxDelResult;
import org.apache.camel.component.dropbox.dto.DropboxFileDownloadResult;
import org.apache.camel.component.dropbox.dto.DropboxFileUploadResult;
import org.apache.camel.component.dropbox.dto.DropboxMoveResult;
import org.apache.camel.component.dropbox.dto.DropboxSearchResult;
import org.apache.camel.component.dropbox.util.DropboxConstants;
import org.apache.camel.component.dropbox.util.DropboxException;
import org.apache.camel.component.dropbox.util.DropboxResultCode;
import org.apache.camel.component.dropbox.util.DropboxUploadMode;
import org.apache.camel.support.builder.OutputStreamBuilder;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.camel.component.dropbox.util.DropboxConstants.HEADER_PUT_FILE_NAME;

public final class DropboxAPIFacade {

    private static final Logger LOG = LoggerFactory.getLogger(DropboxAPIFacade.class);

    private final DbxClientV2 client;

    private final Exchange exchange;

    /**
     * @param client   the DbxClient performing dropbox low level operations
     * @param exchange the current Exchange
     */
    public DropboxAPIFacade(DbxClientV2 client, Exchange exchange) {
        this.client = client;
        this.exchange = exchange;
    }

    /**
     * Put or upload a new file or an entire directory to dropbox
     *
     * @param  localPath        the file path or the dir path on the local filesystem
     * @param  remotePath       the remote path destination on dropbox
     * @param  mode             how a file should be saved on dropbox; in case of "add" the new file will be renamed in
     *                          case a file with the same name already exists on dropbox. in case of "force" the file
     *                          already existing with the same name will be overridden.
     * @return                  a result object reporting for each remote path the result of the operation.
     * @throws DropboxException if an error occurs
     */
    public DropboxFileUploadResult put(String localPath, String remotePath, DropboxUploadMode mode) throws DropboxException {
        // in case the remote path is not specified, the remotePath = localPath
        String dropboxPath = remotePath == null ? localPath : remotePath;

        boolean isPresent = true;
        try {
            client.files().getMetadata(dropboxPath);
        } catch (DbxException e) {
            isPresent = false;
        }

        if (localPath != null) {
            return putFile(localPath, mode, dropboxPath, isPresent);
        } else {
            return putBody(exchange, mode, dropboxPath, isPresent);
        }
    }

    private DropboxFileUploadResult putFile(String localPath, DropboxUploadMode mode, String dropboxPath, boolean isPresent)
            throws DropboxException {
        File fileLocalPath = new File(localPath);
        // verify uploading of a single file
        if (fileLocalPath.isFile()) {
            // check if dropbox file exists
            if (isPresent && !DropboxUploadMode.force.equals(mode)) {
                throw new DropboxException(dropboxPath + " exists on dropbox. Use force upload mode to override");
            }
            // in case the entry not exists on dropbox check if the filename
            // should be appended
            if (!isPresent && dropboxPath.endsWith(DropboxConstants.DROPBOX_FILE_SEPARATOR)) {
                dropboxPath = dropboxPath + fileLocalPath.getName();
            }

            LOG.debug("Uploading: {},{}", fileLocalPath, dropboxPath);
            DropboxFileUploadResult result;
            try {
                FileMetadata uploadedFile = putSingleFile(fileLocalPath, dropboxPath, mode);
                if (uploadedFile == null) {
                    result = new DropboxFileUploadResult(dropboxPath, DropboxResultCode.KO);
                } else {
                    result = new DropboxFileUploadResult(dropboxPath, DropboxResultCode.OK);
                }
            } catch (Exception ex) {
                result = new DropboxFileUploadResult(dropboxPath, DropboxResultCode.KO);
            }
            return result;
        } else if (fileLocalPath.isDirectory()) {
            // verify uploading of a list of files inside a dir
            LOG.debug("Uploading a dir...");
            // check if dropbox folder exists
            if (isPresent && !DropboxUploadMode.force.equals(mode)) {
                throw new DropboxException(dropboxPath + " exists on dropbox and is not a folder!");
            }
            if (!dropboxPath.endsWith(DropboxConstants.DROPBOX_FILE_SEPARATOR)) {
                dropboxPath = dropboxPath + DropboxConstants.DROPBOX_FILE_SEPARATOR;
            }
            // revert to old path
            String oldDropboxPath = dropboxPath;
            // list all files in a dir
            Collection<File> listFiles = FileUtils.listFiles(fileLocalPath, null, true);
            if (listFiles.isEmpty()) {
                throw new DropboxException(localPath + " does not contain any files");
            }

            HashMap<String, DropboxResultCode> resultMap = new HashMap<>(listFiles.size());
            for (File file : listFiles) {
                String absPath = file.getAbsolutePath();
                int indexRemainingPath = localPath.length();
                if (!localPath.endsWith("/")) {
                    indexRemainingPath += 1;
                }
                String remainingPath = absPath.substring(indexRemainingPath);
                dropboxPath = dropboxPath + remainingPath;
                try {
                    LOG.debug("Uploading: {},{}", fileLocalPath, dropboxPath);
                    FileMetadata uploadedFile = putSingleFile(file, dropboxPath, mode);
                    if (uploadedFile == null) {
                        resultMap.put(dropboxPath, DropboxResultCode.KO);
                    } else {
                        resultMap.put(dropboxPath, DropboxResultCode.OK);
                    }
                } catch (Exception ex) {
                    resultMap.put(dropboxPath, DropboxResultCode.KO);
                }
                dropboxPath = oldDropboxPath;
            }
            return new DropboxFileUploadResult(resultMap);
        } else {
            return null;
        }
    }

    private DropboxFileUploadResult putBody(Exchange exchange, DropboxUploadMode mode, String dropboxPath, boolean isPresent) {
        String name = exchange.getIn().getHeader(HEADER_PUT_FILE_NAME, String.class);
        if (name == null) {
            // fallback to use CamelFileName
            name = exchange.getIn().getHeader(Exchange.FILE_NAME, String.class);
        }
        if (name == null) {
            // use message id as file name
            name = exchange.getIn().getMessageId();
        }

        // in case the entry not exists on dropbox check if the filename should
        // be appended
        if (!isPresent && dropboxPath.endsWith(DropboxConstants.DROPBOX_FILE_SEPARATOR)) {
            dropboxPath = dropboxPath + name;
        }

        LOG.debug("Uploading message body: {}", dropboxPath);

        DropboxFileUploadResult result;
        try {
            FileMetadata uploadedFile = putSingleBody(exchange, dropboxPath, mode);
            if (uploadedFile == null) {
                result = new DropboxFileUploadResult(dropboxPath, DropboxResultCode.KO);
            } else {
                result = new DropboxFileUploadResult(dropboxPath, DropboxResultCode.OK);
            }
        } catch (Exception ex) {
            result = new DropboxFileUploadResult(dropboxPath, DropboxResultCode.KO);
        }
        return result;
    }

    private FileMetadata putSingleFile(File inputFile, String dropboxPath, DropboxUploadMode mode) throws Exception {
        try (FileInputStream inputStream = new FileInputStream(inputFile)) {
            WriteMode uploadMode;
            if (mode == DropboxUploadMode.force) {
                uploadMode = WriteMode.OVERWRITE;
            } else {
                uploadMode = WriteMode.ADD;
            }
            return client.files().uploadBuilder(dropboxPath).withMode(uploadMode).uploadAndFinish(inputStream,
                    inputFile.length());
        }
    }

    private FileMetadata putSingleBody(Exchange exchange, String dropboxPath, DropboxUploadMode mode) throws Exception {
        byte[] data = exchange.getIn().getMandatoryBody(byte[].class);
        try (InputStream is = new ByteArrayInputStream(data)) {
            final WriteMode uploadMode;
            if (mode == DropboxUploadMode.force) {
                uploadMode = WriteMode.OVERWRITE;
            } else {
                uploadMode = WriteMode.ADD;
            }
            return client.files().uploadBuilder(dropboxPath).withMode(uploadMode).uploadAndFinish(is, data.length);
        }
    }

    /**
     * Search inside a remote path including its subdirectories. The query param can be null.
     *
     * @param  remotePath       the remote path where starting the search from
     * @param  query            a space-separated list of substrings to search for. A file matches only if it contains
     *                          all the substrings
     * @return                  a result object containing all the files found.
     * @throws DropboxException if an error occurs
     */
    public DropboxSearchResult search(String remotePath, String query) throws DropboxException {
        if (query == null) {
            LOG.debug("Search no query");
        } else {
            LOG.debug("Search by query: {}", query);
        }
        try {
            SearchV2Result listing = client.files().searchV2Builder(query)
                    .withOptions(SearchOptions.newBuilder().withPath(remotePath).build())
                    .start();
            return new DropboxSearchResult(listing.getMatches());
        } catch (DbxException e) {
            throw new DropboxException(String.format("%s does not exist or cannot obtain metadata", remotePath), e);
        }
    }

    /**
     * Delete every file and subdirectory inside the remote directory. In case the remotePath is a file, delete the
     * file.
     *
     * @param  remotePath       the remote location to delete
     * @return                  a result object with the result of the delete operation.
     * @throws DropboxException if an error occurs
     */
    public DropboxDelResult del(String remotePath) throws DropboxException {
        try {
            client.files().deleteV2(remotePath);
        } catch (DbxException e) {
            throw new DropboxException(String.format("%s does not exist or cannot obtain metadata", remotePath), e);
        }
        return new DropboxDelResult(remotePath);
    }

    /**
     * Rename a remote path with the new path location.
     *
     * @param  remotePath       the existing remote path to be renamed
     * @param  newRemotePath    the new remote path substituting the old one
     * @return                  a result object with the result of the move operation.
     * @throws DropboxException if an error occurs
     */
    public DropboxMoveResult move(String remotePath, String newRemotePath) throws DropboxException {
        try {
            client.files().moveV2(remotePath, newRemotePath);
            return new DropboxMoveResult(remotePath, newRemotePath);
        } catch (DbxException e) {
            throw new DropboxException(remotePath + " does not exist or cannot obtain metadata", e);
        }
    }

    /**
     * Get the content of every file inside the remote path.
     *
     * @param  remotePath       the remote path where to download from
     * @return                  a result object with the content (ByteArrayOutputStream) of every files inside the
     *                          remote path.
     * @throws DropboxException if an error occurs
     */
    public DropboxFileDownloadResult get(String remotePath) throws DropboxException {
        return new DropboxFileDownloadResult(downloadFilesInFolder(remotePath));
    }

    private Map<String, Object> downloadFilesInFolder(String path) throws DropboxException {
        try {
            ListFolderResult folderResult = client.files().listFolder(path.equals("/") ? "" : path);
            Map<String, Object> returnMap = new LinkedHashMap<>();
            for (Metadata entry : folderResult.getEntries()) {
                Map.Entry<String, Object> singleFile = downloadSingleFile(entry.getPathDisplay());
                if (singleFile == null) {
                    continue;
                }
                returnMap.put(entry.getPathDisplay(), singleFile.getValue());
            }
            return returnMap;
        } catch (ListFolderErrorException e) {
            try (DbxDownloader<FileMetadata> listing = client.files().download(path)) {
                if (listing == null) {
                    return Map.of();
                } else {
                    LOG.debug("downloading a single file...");
                    Map.Entry<String, Object> entry = downloadSingleFile(path);
                    if (entry == null) {
                        return Map.of();
                    }
                    return Map.of(entry.getKey(), entry.getValue());
                }
            } catch (DbxException dbxException) {
                throw new DropboxException(dbxException);
            }
        } catch (DbxException e) {
            throw new DropboxException(e);
        }
    }

    private Map.Entry<String, Object> downloadSingleFile(String path) throws DropboxException {
        try (OutputStreamBuilder target = OutputStreamBuilder.withExchange(exchange);
             DbxDownloader<FileMetadata> downloadedFile = client.files().download(path)) {
            if (downloadedFile == null) {
                return null;
            }
            downloadedFile.download(target);
            LOG.debug("downloaded path={}", path);
            return new AbstractMap.SimpleEntry<>(path, target.build());
        } catch (DbxException e) {
            throw new DropboxException(path + " does not exist or cannot obtain metadata", e);
        } catch (IOException e) {
            throw new DropboxException(path + " cannot obtain a stream", e);
        }
    }
}
