package org.replicadb.manager;


import alex.mojaki.s3upload.MultiPartOutputStream;
import alex.mojaki.s3upload.StreamTransferManager;
import com.amazonaws.SDKGlobalConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ObjectMetadata;
import de.siegmar.fastcsv.writer.CsvAppender;
import de.siegmar.fastcsv.writer.CsvWriter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.replicadb.cli.ToolOptions;

import java.io.PrintWriter;
import java.net.URI;
import java.sql.*;
import java.util.List;
import java.util.Properties;

public class S3Manager extends SqlManager {

    private static final Logger LOG = LogManager.getLogger(S3Manager.class.getName());

    private String accessKey;
    private String secretKey;
    private boolean secuereConnection;
    private boolean objectPerRow;
    private String rowKeyColumnName;
    private String rowContentColumnName;
    private String csvKeyFileName;
    private char csvFieldSeparator;
    private char csvTextDelimiter;
    private String csvLineDelimiter;
    private boolean csvAlwaysDelimitText;
    private boolean csvHeader;

    private void setAccessKey(String accessKey) {
        if (accessKey != null && !accessKey.isEmpty())
            this.accessKey = accessKey;
        else
            throw new IllegalArgumentException("accessKey property cannot be null");
    }

    private void setSecretKey(String secretKey) {
        if (secretKey != null && !secretKey.isEmpty())
            this.secretKey = secretKey;
        else
            throw new IllegalArgumentException("secretKey property cannot be null");
    }

    private void setSecuereConnection(String secuereConnection) {
        // Secure connection default true
        if (secuereConnection != null && !secuereConnection.isEmpty())
            this.secuereConnection = Boolean.parseBoolean(secuereConnection);
        else
            this.secuereConnection = true;
    }

    private void setObjectPerRow(boolean objectPerRow) {
        this.objectPerRow = objectPerRow;
    }

    private void setRowKeyColumnName(String rowKeyColumnName) {
        if (rowKeyColumnName != null && !rowKeyColumnName.isEmpty())
            this.rowKeyColumnName = rowKeyColumnName;
        else
            throw new IllegalArgumentException("row.keyColumn property cannot be null");
    }

    private void setRowContentColumnName(String rowContentColumnName) {
        if (rowContentColumnName != null && !rowContentColumnName.isEmpty())
            this.rowContentColumnName = rowContentColumnName;
        else
            throw new IllegalArgumentException("row.contentColumn property cannot be null");
    }

    private void setCsvKeyFileName(String csvKeyFileName) {
        if (csvKeyFileName != null && !csvKeyFileName.isEmpty())
            this.csvKeyFileName = csvKeyFileName;
        else
            throw new IllegalArgumentException("csv.keyFileName property cannot be null");
    }

    private void setCsvFieldSeparator(String csvFieldSeparator) {
        if (csvFieldSeparator != null && csvFieldSeparator.length() > 1)
            throw new IllegalArgumentException("FieldSeparator must be a single char");

        if (csvFieldSeparator != null && !csvFieldSeparator.isEmpty())
            this.csvFieldSeparator = csvFieldSeparator.charAt(0);
    }


    private void setCsvTextDelimiter(String csvTextDelimiter) {
        if (csvTextDelimiter != null && csvTextDelimiter.length() > 1)
            throw new IllegalArgumentException("TextDelimiter must be a single char");

        if (csvTextDelimiter != null && !csvTextDelimiter.isEmpty())
            this.csvTextDelimiter = csvTextDelimiter.charAt(0);
    }

    private void setCsvLineDelimiter(String csvLineDelimiter) {
        this.csvLineDelimiter = csvLineDelimiter;
    }

    private void setCsvAlwaysDelimitText(boolean csvAlwaysDelimitText) {
        this.csvAlwaysDelimitText = csvAlwaysDelimitText;
    }

    private void setCsvHeader(boolean csvHeader) {
        this.csvHeader = csvHeader;
    }

    private boolean isSecuereConnection() {
        return secuereConnection;
    }

    private boolean isObjectPerRow() {
        return objectPerRow;
    }

    private boolean isCsvHeader() {
        return csvHeader;
    }


    /**
     * Constructs the SqlManager.
     *
     * @param opts the ReplicaDB ToolOptions describing the user's requested action.
     */
    public S3Manager(ToolOptions opts, DataSourceType dsType) {
        super(opts);
        this.dsType = dsType;
        loadS3CustomProperties();
    }

    /**
     * Load ReplicaDB custom options for Amazon S3
     */
    private void loadS3CustomProperties() {
        Properties s3Props;

        if (dsType == DataSourceType.SOURCE)
            s3Props = options.getSourceConnectionParams();
        else
            s3Props = options.getSinkConnectionParams();

        // S3 Client properties
        setAccessKey(s3Props.getProperty("accessKey"));
        setSecretKey(s3Props.getProperty("secretKey"));
        setSecuereConnection(s3Props.getProperty("secure-connection"));

        // S3 ReplicaDB upload Mode
        setObjectPerRow(Boolean.parseBoolean(s3Props.getProperty("row.isObject")));

        if (isObjectPerRow()) {
            // One object per row
            setRowKeyColumnName(s3Props.getProperty("row.keyColumn"));
            setRowContentColumnName(s3Props.getProperty("row.contentColumn"));
        } else {
            // One CSV for all rows
            setCsvKeyFileName(s3Props.getProperty("csv.keyFileName"));
            setCsvFieldSeparator(s3Props.getProperty("csv.FieldSeparator"));
            setCsvTextDelimiter(s3Props.getProperty("csv.TextDelimiter"));
            setCsvLineDelimiter(s3Props.getProperty("csv.LineDelimiter"));
            setCsvHeader(Boolean.parseBoolean(s3Props.getProperty("csv.Header")));
            setCsvAlwaysDelimitText(Boolean.parseBoolean(s3Props.getProperty("csv.AlwaysDelimitText")));
        }

    }

    @Override
    public String toString() {
        return "S3Manager{" +
                "accessKey='" + accessKey + '\'' +
                ", secretKey='" + secretKey + '\'' +
                ", secuereConnection=" + secuereConnection +
                ", objectPerRow=" + objectPerRow +
                ", rowKeyColumnName='" + rowKeyColumnName + '\'' +
                ", rowContentColumnName='" + rowContentColumnName + '\'' +
                ", csvKeyFileName='" + csvKeyFileName + '\'' +
                ", csvFieldSeparator=" + csvFieldSeparator +
                ", csvTextDelimiter=" + csvTextDelimiter +
                ", csvLineDelimiter='" + csvLineDelimiter + '\'' +
                ", csvAlwaysDelimitText=" + csvAlwaysDelimitText +
                ", csvHeader=" + csvHeader +
                '}';
    }

    @Override
    protected Connection makeSinkConnection() {
        /*Not necessary for S3*/
        return null;
    }

    @Override
    protected void truncateTable() {
        /*Not necessary for S3*/
    }

    @Override
    public String getDriverClass() {
        return JdbcDrivers.S3.getDriverClass();
    }

    @Override
    public int insertDataToTable(ResultSet resultSet, int taskId) throws Exception {

        URI s3Uri = new URI(options.getSinkConnect());
        // Get Service Endpoint
        // Secure by default
        String serviceEndpoint = "https://";
        if (!isSecuereConnection()) {
            serviceEndpoint = "http://";
        }

        // Get Service Port
        String servicePort = "";
        if (s3Uri.getPort() != -1) {
            servicePort = ":" + s3Uri.getPort();
        }

        serviceEndpoint = serviceEndpoint + s3Uri.getHost() + servicePort;
        LOG.debug("Using serviceEndpoint: " + serviceEndpoint);

        // Get Bucket name
        String bucketName = s3Uri.getPath().replaceAll("/$", "");

        LOG.debug(this.toString());

        // S3 Client
        System.setProperty(SDKGlobalConfiguration.DISABLE_CERT_CHECKING_SYSTEM_PROPERTY, "true");
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        AmazonS3 s3Client = AmazonS3ClientBuilder
                .standard()
                .withRegion("")
                .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(serviceEndpoint, null))
                .withPathStyleAccessEnabled(true)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();


        if (!isObjectPerRow()) {
            // All rows are only one CSV object in s3
            putCsvToS3(resultSet, taskId, s3Client, bucketName);
        } else {
            // Each row is a different object in s3
            putObjectToS3(resultSet, taskId, s3Client, bucketName);
        }

        return 0;

    }

    private void putObjectToS3(ResultSet resultSet, int taskId, AmazonS3 s3Client, String bucketName) throws SQLException {

        ResultSetMetaData rsmd = resultSet.getMetaData();
        int columnCount = rsmd.getColumnCount();

        // Get content column index
        int rowContentColumnIndex = 0;
        for (int i = 1; i <= columnCount; i++) {
            if (rsmd.getColumnName(i).toUpperCase().equals(rowContentColumnName.toUpperCase())) {
                rowContentColumnIndex = i;
            }
        }

        ObjectMetadata binMetadata = new ObjectMetadata();

        while (resultSet.next()) {

            switch (rsmd.getColumnType(rowContentColumnIndex)) {
                case Types.BINARY:
                case Types.BLOB:
                case Types.CLOB:
                    s3Client.putObject(bucketName, resultSet.getString(rowKeyColumnName), resultSet.getBinaryStream(rowContentColumnName), binMetadata);
                    break;
                case Types.SQLXML:
                    throw new IllegalArgumentException("SQLXML Data Type is not supported. You should convert it to BLOB or CLOB");
                default:
                    s3Client.putObject(bucketName, resultSet.getString(rowKeyColumnName), resultSet.getString(rowContentColumnName));
                    break;
            }
        }
    }


    private void putCsvToS3(ResultSet resultSet, int taskId, AmazonS3 s3Client, String bucketName) throws SQLException {

        ResultSetMetaData rsmd = resultSet.getMetaData();
        int columnCount = rsmd.getColumnCount();

        // Set File Name.
        // It is not possible to append to an existing S3 file, ReplicaDB will generate one file per job
        // renaming file name with the taskid
        String keyFileName = csvKeyFileName;
        if (options.getJobs() > 1)
            keyFileName = keyFileName.substring(0, keyFileName.lastIndexOf(".")) + "_" + taskId + keyFileName.substring(keyFileName.lastIndexOf("."));

        // Setting up Streaming transfer
        int numStreams = 1;
        final StreamTransferManager manager = new StreamTransferManager(bucketName, keyFileName, s3Client)
                .numStreams(numStreams)
                .numUploadThreads(2)
                .queueCapacity(2)
                .partSize(5);

        final List<MultiPartOutputStream> streams = manager.getMultiPartOutputStreams();

        try {
            MultiPartOutputStream outputStream = streams.get(0);

            PrintWriter pw = new PrintWriter(outputStream, true);

            CsvWriter csvWriter = new CsvWriter();
            // Custom user csv options
            if ((int) csvFieldSeparator != 0) csvWriter.setFieldSeparator(csvFieldSeparator);
            if ((int) csvTextDelimiter != 0) csvWriter.setTextDelimiter(csvTextDelimiter);
            if (csvLineDelimiter != null && !csvLineDelimiter.isEmpty())
                csvWriter.setLineDelimiter(csvLineDelimiter.toCharArray());
            csvWriter.setAlwaysDelimitText(csvAlwaysDelimitText);

            // Write the CSV
            try (CsvAppender csvAppender = csvWriter.append(pw)) {

                // Put headers in the file
                if (isCsvHeader()) {
                    for (int i = 1; i <= columnCount; i++) {
                        csvAppender.appendField(rsmd.getColumnName(i));
                    }
                    csvAppender.endLine();
                }

                String colValue;
                String[] colValues;

                // lines
                if (resultSet.next()) {
                    // Create Bandwidth Throttling
                    bandwidthThrottlingCreate(resultSet, rsmd);
                    do {
                        bandwidthThrottlingAcquiere();

                        colValues = new String[columnCount];

                        // Iterate over the columns of the row
                        for (int i = 1; i <= columnCount; i++) {
                            colValue = resultSet.getString(i);

                            if (!this.options.isSinkDisableEscape() && !resultSet.wasNull())
                                colValues[i - 1] = colValue.replace("\n", "\\n").replace("\r", "\\r");
                            else
                                colValues[i - 1] = colValue;
                        }
                        csvAppender.appendLine(colValues);
                    } while (resultSet.next());
                }
            }

            // The stream must be closed once all the data has been written
            pw.close();
            outputStream.close();
        } catch (Exception e) {
            // Aborts all uploads
            throw manager.abort(e);
        }

        // Finishing off
        manager.complete();

    }


    @Override
    protected void createStagingTable() {
    }

    @Override
    protected void mergeStagingTable() {/*Not implemented*/}

    @Override
    public void preSourceTasks() {/*Not implemented*/}

    @Override
    public void postSourceTasks() {/*Not implemented*/}


    @Override
    public void postSinkTasks() {/*Not implemented*/}

    @Override
    public void cleanUp() {/*Not implemented*/}


}