/*******************************************************************************
 * Copyright 2013-2020 QaProSoft (http://www.qaprosoft.com).
 *
 * Licensed 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 com.qaprosoft.amazon;

import java.io.File;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.HttpMethod;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.auth.SystemPropertiesCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.amazonaws.services.s3.transfer.Download;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.qaprosoft.carina.core.foundation.commons.SpecialKeywords;
import com.qaprosoft.carina.core.foundation.crypto.CryptoTool;
import com.qaprosoft.carina.core.foundation.utils.Configuration;
import com.qaprosoft.carina.core.foundation.utils.Configuration.Parameter;
import com.qaprosoft.carina.core.foundation.utils.common.CommonUtils;

public class AmazonS3Manager {
    private static final Logger LOGGER = Logger.getLogger(AmazonS3Manager.class);
    private static volatile AmazonS3Manager instance = null;
    private static AmazonS3 s3client = null;

    private AmazonS3Manager() {
    }

    public static AmazonS3Manager getInstance() {
        if (instance == null) {
            synchronized (AmazonS3Manager.class) {
                if (instance == null) {
                    instance = new AmazonS3Manager();
                    CryptoTool cryptoTool = new CryptoTool(Configuration.get(Parameter.CRYPTO_KEY_PATH));
                    Pattern CRYPTO_PATTERN = Pattern.compile(SpecialKeywords.CRYPT);
                    
                    String accessKey = cryptoTool.decryptByPattern(Configuration.get(Parameter.ACCESS_KEY_ID), CRYPTO_PATTERN);
                    String secretKey = cryptoTool.decryptByPattern(Configuration.get(Parameter.SECRET_KEY), CRYPTO_PATTERN);

                    System.setProperty("aws.accessKeyId", accessKey);
                    System.setProperty("aws.secretKey", secretKey);

                    s3client = new AmazonS3Client(new SystemPropertiesCredentialsProvider());
                }
            }
        }
        return instance;
    }

    public AmazonS3 getClient() {
        return s3client;
    }

    /**
     * Put any file to Amazon S3 storage.
     * 
     * @param bucket
     *            - S3 bucket name
     * @param key
     *            - S3 storage path. Example:
     *            DEMO/TestSuiteName/TestMethodName/file.txt
     * @param filePath
     *            - local storage path. Example: C:/Temp/file.txt
     * 
     */
    public void put(String bucket, String key, String filePath) {
        put(bucket, key, filePath, null);
    }

    /**
     * Put any file to Amazon S3 storage.
     * 
     * @param bucket
     *            - S3 bucket name
     * @param key
     *            - S3 storage path. Example:
     *            DEMO/TestSuiteName/TestMethodName/file.txt
     * @param filePath
     *            - local storage path. Example: C:/Temp/file.txt
     * @param metadata
     *            - custom tags metadata like name etc
     * 
     */
    public void put(String bucket, String key, String filePath, ObjectMetadata metadata) {

        /*
         * if (mode != S3Mode.WRITE) {
         * if (mode == S3Mode.READ) {
         * LOGGER.warn("Unable to put data in READ mode!");
         * }
         * return;
         * }
         */

        if (key == null) {
            throw new RuntimeException("Key is null!");
        }
        if (key.isEmpty()) {
            throw new RuntimeException("Key is empty!");
        }

        if (filePath == null) {
            throw new RuntimeException("FilePath is null!");
        }
        if (filePath.isEmpty()) {
            throw new RuntimeException("FilePath is empty!");
        }

        File file = new File(filePath);
        if (!file.exists()) {
            throw new RuntimeException("File does not exist! " + filePath);
        }

        try {
            LOGGER.debug("Uploading a new object to S3 from a file: "
                    + filePath);

            PutObjectRequest object = new PutObjectRequest(bucket, key, file);
            if (metadata != null) {
                object.setMetadata(metadata);
            }

            s3client.putObject(object);
            LOGGER.debug("Uploaded to S3: '" + filePath + "' with key '" + key
                    + "'");

        } catch (AmazonServiceException ase) {
            LOGGER.error("Caught an AmazonServiceException, which "
                    + "means your request made it "
                    + "to Amazon S3, but was rejected with an error response for some reason.\n"
                    + "Error Message:    " + ase.getMessage() + "\n"
                    + "HTTP Status Code: " + ase.getStatusCode() + "\n"
                    + "AWS Error Code:   " + ase.getErrorCode() + "\n"
                    + "Error Type:       " + ase.getErrorType() + "\n"
                    + "Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            LOGGER.error("Caught an AmazonClientException, which "
                    + "means the client encountered "
                    + "an internal error while trying to "
                    + "communicate with S3, "
                    + "such as not being able to access the network.\n"
                    + "Error Message: " + ace.getMessage());
        }
    }

    /**
     * Get any file from Amazon S3 storage as S3Object.
     * 
     * @param bucket
     *            - S3 Bucket name.
     * @param key
     *            - S3 storage path. Example:
     *            DEMO/TestSuiteName/TestMethodName/file.txt
     * @return S3Object
     */
    public S3Object get(String bucket, String key) {
        // S3Object s3object = null;

        /*
         * if (mode != S3Mode.WRITE && mode != S3Mode.READ) {
         * throw new RuntimeException("Unable to get S3 object in OFF mode!");
         * }
         */

        if (bucket == null) {
            throw new RuntimeException("Bucket is null!");
        }
        if (bucket.isEmpty()) {
            throw new RuntimeException("Bucket is empty!");
        }

        if (key == null) {
            throw new RuntimeException("Key is null!");
        }
        if (key.isEmpty()) {
            throw new RuntimeException("Key is empty!");
        }

        try {
            LOGGER.info("Finding an s3object...");
            // TODO investigate possibility to add percentage of completed
            // downloading
            S3Object s3object = s3client.getObject(new GetObjectRequest(bucket,
                    key));
            LOGGER.info("Content-Type: "
                    + s3object.getObjectMetadata().getContentType());
            return s3object;
            /*
             * GetObjectRequest rangeObjectRequest = new GetObjectRequest(
             * bucketName, key); rangeObjectRequest.setRange(0, 10); S3Object
             * objectPortion = s3client.getObject(rangeObjectRequest);
             */
        } catch (AmazonServiceException ase) {
            LOGGER.error("Caught an AmazonServiceException, which "
                    + "means your request made it "
                    + "to Amazon S3, but was rejected with an error response for some reason.\n"
                    + "Error Message:    " + ase.getMessage() + "\n"
                    + "HTTP Status Code: " + ase.getStatusCode() + "\n"
                    + "AWS Error Code:   " + ase.getErrorCode() + "\n"
                    + "Error Type:       " + ase.getErrorType() + "\n"
                    + "Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            LOGGER.error("Caught an AmazonClientException, which "
                    + "means the client encountered "
                    + "an internal error while trying to "
                    + "communicate with S3, "
                    + "such as not being able to access the network.\n"
                    + "Error Message: " + ace.getMessage());
        }
        // TODO investigate pros and cons returning null
        throw new RuntimeException("Unable to download '" + key
                + "' from Amazon S3 bucket '" + bucket + "'");
    }

    /**
     * Delete file from Amazon S3 storage.
     * 
     * @param bucket
     *            - S3 Bucket name.
     * @param key
     *            - S3 storage path. Example:
     *            DEMO/TestSuiteName/TestMethodName/file.txt
     */
    public void delete(String bucket, String key) {
        if (key == null) {
            throw new RuntimeException("Key is null!");
        }
        if (key.isEmpty()) {
            throw new RuntimeException("Key is empty!");
        }

        try {
            s3client.deleteObject(new DeleteObjectRequest(bucket, key));
        } catch (AmazonServiceException ase) {
            LOGGER.error("Caught an AmazonServiceException, which "
                    + "means your request made it "
                    + "to Amazon S3, but was rejected with an error response for some reason.\n"
                    + "Error Message:    " + ase.getMessage() + "\n"
                    + "HTTP Status Code: " + ase.getStatusCode() + "\n"
                    + "AWS Error Code:   " + ase.getErrorCode() + "\n"
                    + "Error Type:       " + ase.getErrorType() + "\n"
                    + "Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            LOGGER.error("Caught an AmazonClientException.\n"
                    + "Error Message: " + ace.getMessage());
        }
    }

    /**
     * Get latest build artifact from Amazon S3 storage as S3Object.
     * 
     * @param bucket
     *            - S3 Bucket name.
     * @param key
     *            - S3 storage path to your project. Example:
     *            android/MyProject
     * @param pattern
     *            - pattern to find single build artifact Example:
     *            .*prod-google-release.*
     * @return S3ObjectSummary
     */
    public S3ObjectSummary getLatestBuildArtifact(String bucket, String key, Pattern pattern) {
        if (pattern == null) {
            throw new RuntimeException("pattern is null!");
        }

        S3ObjectSummary latestBuild = null;

        ObjectListing objBuilds = s3client.listObjects(bucket, key);

        int i = 0;
        int limit = 100;
        // by default S3 return only 1000 objects summary so need while cycle here
        do {
            LOGGER.info("looking for s3 artifact using iteration #" + i);

            for (S3ObjectSummary obj : objBuilds.getObjectSummaries()) {
                LOGGER.debug("Existing S3 artifact: " + obj.getKey());
                Matcher matcher = pattern.matcher(obj.getKey());
                if (matcher.find()) {
                    if (latestBuild == null) {
                        latestBuild = obj;
                    }

                    if (obj.getLastModified().after(latestBuild.getLastModified())) {
                        latestBuild = obj;
                    }
                }
            }
            objBuilds = s3client.listNextBatchOfObjects(objBuilds);
        } while (objBuilds.isTruncated() && ++i < limit);

        if (latestBuild == null) {
            LOGGER.error("Unable to find S3 build artifact by pattern: " + pattern);
        } else {
            LOGGER.info("latest artifact: " + latestBuild.getKey());
        }
        return latestBuild;
    }

    /**
     * Method to download file from s3 to local file system
     * 
     * @param bucketName AWS S3 bucket name
     * @param key (example: android/apkFolder/ApkName.apk)
     * @param file (local file name)
     */
    public void download(final String bucketName, final String key, final File file) {
        download(bucketName, key, file, 10);
    }

    /**
     * Method to download file from s3 to local file system
     * 
     * @param bucketName AWS S3 bucket name
     * @param key (example: android/apkFolder/ApkName.apk)
     * @param file (local file name)
     * @param pollingInterval (polling interval in sec for S3 download status determination)
     */
    public void download(final String bucketName, final String key, final File file, long pollingInterval) {
        LOGGER.info("App will be downloaded from s3.");
        LOGGER.info(String.format("[Bucket name: %s] [Key: %s] [File: %s]", bucketName, key, file.getAbsolutePath()));
        DefaultAWSCredentialsProviderChain credentialProviderChain = new DefaultAWSCredentialsProviderChain();
        TransferManager tx = new TransferManager(
                credentialProviderChain.getCredentials());
        Download appDownload = tx.download(bucketName, key, file);
        try {
            LOGGER.info("Transfer: " + appDownload.getDescription());
            LOGGER.info("	State: " + appDownload.getState());
            LOGGER.info("	Progress: ");
            // You can poll your transfer's status to check its progress
            while (!appDownload.isDone()) {
                LOGGER.info("		transferred: " + (int) (appDownload.getProgress().getPercentTransferred() + 0.5) + "%");
                CommonUtils.pause(pollingInterval);
            }
            LOGGER.info("	State: " + appDownload.getState());
            // appDownload.waitForCompletion();
        } catch (AmazonClientException e) {
            throw new RuntimeException("File wasn't downloaded from s3. See log: ".concat(e.getMessage()));
        }
        // tx.shutdownNow();
    }

    /**
     * Method to generate pre-signed object URL to s3 object
     * 
     * @param bucketName AWS S3 bucket name
     * @param key (example: android/apkFolder/ApkName.apk)
     * @param ms espiration time in ms, i.e. 1 hour is 1000*60*60
     * @return url String pre-signed URL
     */
    public URL generatePreSignUrl(final String bucketName, final String key, long ms) {

        java.util.Date expiration = new java.util.Date();
        long msec = expiration.getTime();
        msec += ms;
        expiration.setTime(msec);

        GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, key);
        generatePresignedUrlRequest.setMethod(HttpMethod.GET);
        generatePresignedUrlRequest.setExpiration(expiration);

        URL url = s3client.generatePresignedUrl(generatePresignedUrlRequest);
        return url;
    }

    /*
     * public void read(S3Object s3object) {
     * displayTextInputStream(s3object.getObjectContent()); }
     * 
     * private void displayTextInputStream(InputStream input) { // Read one text
     * line at a time and display. LOGGER.info("File content is: ");
     * BufferedReader reader = new BufferedReader(new InputStreamReader(input));
     * while (true) { String line = null; try { line = reader.readLine(); }
     * catch (IOException e) { LOGGER.error("Failed to read file", e); } if
     * (line == null) break;
     * 
     * System.out.println("    " + line); } }
     */
}