/*
* JBoss, Home of Professional Open Source.
* Copyright 2013, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

package org.jboss.as.host.controller.discovery;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MASTER;
import static org.jboss.as.host.controller.discovery.Constants.ACCESS_KEY;
import static org.jboss.as.host.controller.discovery.Constants.LOCATION;
import static org.jboss.as.host.controller.discovery.Constants.PREFIX;
import static org.jboss.as.host.controller.discovery.Constants.PRE_SIGNED_DELETE_URL;
import static org.jboss.as.host.controller.discovery.Constants.PRE_SIGNED_PUT_URL;
import static org.jboss.as.host.controller.discovery.Constants.SECRET_ACCESS_KEY;
import static org.jboss.as.host.controller.logging.HostControllerLogger.ROOT_LOGGER;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.host.controller.discovery.S3Util.AWSAuthConnection;
import org.jboss.as.host.controller.discovery.S3Util.Bucket;
import org.jboss.as.host.controller.discovery.S3Util.GetResponse;
import org.jboss.as.host.controller.discovery.S3Util.ListAllMyBucketsResponse;
import org.jboss.as.host.controller.discovery.S3Util.PreSignedUrlParser;
import org.jboss.as.host.controller.discovery.S3Util.S3Object;
import org.jboss.as.host.controller.logging.HostControllerLogger;
import org.jboss.as.remoting.Protocol;
import org.jboss.dmr.ModelNode;

/**
 * Handle domain controller discovery via Amazon's S3 storage.
 * The S3 access code reuses the example shipped by Amazon.
 *
 * @author Farah Juma
 */
public class S3Discovery implements DiscoveryOption {

    // The name of the S3 file that will store the domain controller's host and port
    private static final String DC_FILE_NAME="jboss-domain-master-data";

    // The access key to AWS (S3)
    private String access_key = null;

    // The secret access key to AWS (S3)
    private String secret_access_key = null;

    // The name of the S3 bucket
    private String location = null;

    // The name of the S3 bucket prefix
    private String prefix = null;

    // The pre-signed URL for PUTs
    private String pre_signed_put_url = null;

    // The pre-signed URL for DELETEs
    private String pre_signed_delete_url = null;

    private AWSAuthConnection conn = null;

    /**
     * Create the S3Discovery option.
     *
     * @param properties map of properties needed to access the S3 bucket
     */
    public S3Discovery(Map<String, ModelNode> properties) {
        ModelNode accessKeyNode = properties.get(ACCESS_KEY);
        access_key = (accessKeyNode == null || !accessKeyNode.isDefined()) ? null : accessKeyNode.asString();

        ModelNode secretAccessKeyNode = properties.get(SECRET_ACCESS_KEY);
        secret_access_key = (secretAccessKeyNode == null || !secretAccessKeyNode.isDefined()) ? null : secretAccessKeyNode.asString();

        ModelNode locationNode = properties.get(LOCATION);
        location = (locationNode == null || !locationNode.isDefined()) ? null : locationNode.asString();

        ModelNode prefixNode = properties.get(PREFIX);
        prefix = (prefixNode == null || !prefixNode.isDefined()) ? null : prefixNode.asString();

        ModelNode preSignedPutUrlNode = properties.get(PRE_SIGNED_PUT_URL);
        pre_signed_put_url = (preSignedPutUrlNode == null || !preSignedPutUrlNode.isDefined()) ? null : preSignedPutUrlNode.asString();

        ModelNode preSignedDeleteUrlNode = properties.get(PRE_SIGNED_DELETE_URL);
        pre_signed_delete_url = (preSignedDeleteUrlNode == null || !preSignedDeleteUrlNode.isDefined()) ? null : preSignedDeleteUrlNode.asString();
    }

    @Override
    public void allowDiscovery(List<DomainControllerManagementInterface> interfaces) {
        try {
            // Write the domain controller data to an S3 file
            List<DomainControllerData> data = new ArrayList<>(interfaces.size());
            for (DomainControllerManagementInterface managementInterface : interfaces) {
                data.add(new DomainControllerData(managementInterface.getProtocol().toString(), managementInterface.getHost(), managementInterface.getPort()));
            }
            Collections.sort(data, new Comparator<DomainControllerData>() {
                @Override
                public int compare(DomainControllerData data, DomainControllerData otherData) {
                    Protocol protocol = Protocol.forName(data.getProtocol());
                    if (Protocol.REMOTE == protocol) {
                        return 1;
                    }
                    if (Protocol.HTTPS_REMOTING == protocol || Protocol.REMOTE_HTTPS == protocol) {
                        Protocol otherProtocol  = Protocol.forName(otherData.getProtocol());
                        if (Protocol.REMOTE == otherProtocol) {
                            return -1;
                        }
                        return 1;
                    }
                    return -1;
                }});
            writeToFile(data, MASTER);
        } catch (Exception e) {
            ROOT_LOGGER.cannotWriteDomainControllerData(e);
        }
    }

    /**
     * Discovers the information to connect to the Domain Controller.
     *
     * @return An instance of the class that holds the connection parameters to a remote domain controller.
     */
    @Override
    public List<RemoteDomainControllerConnectionConfiguration> discover() {
        // Read the domain controller data from an S3 file
        DcFileNameMode fileNameMode = getFileNameMode();
        List<DomainControllerData> dataDc = readFromFile(fileNameMode);
        List<RemoteDomainControllerConnectionConfiguration> options = new ArrayList<>(dataDc.size());
        for (DomainControllerData data : dataDc) {
            if (data != null) {
                // Validate and set the host and port
                RemoteDomainControllerConnectionConfiguration discovery = new RemoteDomainControllerConnectionConfiguration(
                        data.getProtocol(), data.getHost(), data.getPort());
                String host = data.getHost();
                try {
                    // Use the static discovery AD's. They don't allow undefined.
                    StaticDiscoveryResourceDefinition.HOST.getValidator()
                            .validateParameter(StaticDiscoveryResourceDefinition.HOST.getName(),
                                    host == null ? new ModelNode() : new ModelNode(host));
                    StaticDiscoveryResourceDefinition.PORT.getValidator()
                            .validateParameter(StaticDiscoveryResourceDefinition.PORT.getName(), new ModelNode(discovery.getPort()));
                    StaticDiscoveryResourceDefinition.PROTOCOL.getValidator()
                            .validateParameter(StaticDiscoveryResourceDefinition.PROTOCOL.getName(), new ModelNode(discovery.getProtocol()));
                    options.add(discovery);
                } catch (OperationFailedException e) {
                }
            } else {
                throw HostControllerLogger.ROOT_LOGGER.failedMarshallingDomainControllerData();
            }
        }
        if (options.isEmpty()) {
            throw HostControllerLogger.ROOT_LOGGER.failedMarshallingDomainControllerData();
        }
        return options;
    }

    @Override
    public void cleanUp() {
        // Remove the S3 file
        remove(MASTER);
    }

    @Override
        public String toString() {
        // TODO consider including 'location' but that may be sensitive data not wanted in logs
        return getClass().getSimpleName();
    }

    /**
     * Determine whether or not pre-signed URLs will be used.
     */
    private boolean usingPreSignedUrls() {
        return pre_signed_put_url != null;
    }

    /**
     * Make sure {@code pre_signed_put_url} and {@code pre_signed_delete_url} are valid.
     */
    private void validatePreSignedUrls() {
        if (pre_signed_put_url != null && pre_signed_delete_url != null) {
            PreSignedUrlParser parsedPut = new PreSignedUrlParser(pre_signed_put_url);
            PreSignedUrlParser parsedDelete = new PreSignedUrlParser(pre_signed_delete_url);
            if (!parsedPut.getBucket().equals(parsedDelete.getBucket()) ||
                    !parsedPut.getPrefix().equals(parsedDelete.getPrefix())) {
                throw HostControllerLogger.ROOT_LOGGER.preSignedUrlsMustHaveSamePath();
            }
        } else if (pre_signed_put_url != null || pre_signed_delete_url != null) {
            throw HostControllerLogger.ROOT_LOGGER.preSignedUrlsMustBeSetOrUnset();
        }
    }

    /**
     * Do the set-up that's needed to access Amazon S3.
     */
    private void init() {
        validatePreSignedUrls();

        try {
            conn = new AWSAuthConnection(access_key, secret_access_key);
            // Determine the bucket name if prefix is set or if pre-signed URLs are being used
            if (prefix != null && prefix.length() > 0) {
                ListAllMyBucketsResponse bucket_list = conn.listAllMyBuckets(null);
                List buckets = bucket_list.entries;
                if (buckets != null) {
                    boolean found = false;
                    for (Object tmp : buckets) {
                        if (tmp instanceof Bucket) {
                            Bucket bucket = (Bucket) tmp;
                            if (bucket.name.startsWith(prefix)) {
                                location = bucket.name;
                                found = true;
                            }
                        }
                    }
                    if (!found) {
                        location = prefix + "-" + java.util.UUID.randomUUID().toString();
                    }
                }
            }
            if (usingPreSignedUrls()) {
                PreSignedUrlParser parsedPut = new PreSignedUrlParser(pre_signed_put_url);
                location = parsedPut.getBucket();
            }
            if (!conn.checkBucketExists(location)) {
                conn.createBucket(location, AWSAuthConnection.LOCATION_DEFAULT, null).connection.getResponseMessage();
            }
        } catch (Exception e) {
            throw HostControllerLogger.ROOT_LOGGER.cannotAccessS3Bucket(location, e.getLocalizedMessage());
        }
    }

    /**
     * Read the domain controller data from an S3 file.
     *
     * @param dcFileNameMode the name of the directory in the bucket that contains the S3 file
     * @return the domain controller data
     */
    private List<DomainControllerData> readFromFile(DcFileNameMode dcFileNameMode) {
        List<DomainControllerData> data = new ArrayList<>();
        if (dcFileNameMode.getFileName() == null) {
            return data;
        }

        if (conn == null) {
            init();
        }

        try {
            String preSignedDirectoryName = null;
            if (usingPreSignedUrls()) {
                PreSignedUrlParser parsedPut = new PreSignedUrlParser(pre_signed_put_url);
                preSignedDirectoryName = parsedPut.getPrefix();
            }
            String key = S3Util.sanitize(preSignedDirectoryName != null ? preSignedDirectoryName : dcFileNameMode.getDirName()) + "/" + S3Util.sanitize(dcFileNameMode.getFileName());
            GetResponse val = conn.get(location, key, null);
            if (val.object != null) {
                byte[] buf = val.object.data;
                if (buf != null && buf.length > 0) {
                    try {
                        data = S3Util.domainControllerDataFromByteBuffer(buf, dcFileNameMode);
                    } catch (Exception e) {
                        throw HostControllerLogger.ROOT_LOGGER.failedMarshallingDomainControllerData();
                    }
                }
            }
            return data;
        } catch (IOException e) {
            throw HostControllerLogger.ROOT_LOGGER.cannotAccessS3File(e.getLocalizedMessage());
        }
    }

    /**
     * Write the domain controller data to an S3 file.
     *
     * @param data the domain controller data
     * @param domainName the name of the directory in the bucket to write the S3 file to
     * @throws IOException
     */
    private void writeToFile(List<DomainControllerData> data, String domainName) throws IOException {
        if(domainName == null || data == null) {
            return;
        }

        if (conn == null) {
            init();
        }

        try {
            String key = S3Util.sanitize(domainName) + "/" + S3Util.sanitize(DC_FILE_NAME);
            byte[] buf = S3Util.domainControllerDataToByteBuffer(data);
            S3Object val = new S3Object(buf, null);
            if (usingPreSignedUrls()) {
                Map headers = new TreeMap();
                headers.put("x-amz-acl", Arrays.asList("public-read"));
                conn.put(pre_signed_put_url, val, headers).connection.getResponseMessage();
            } else {
                Map headers = new TreeMap();
                headers.put("Content-Type", Arrays.asList("text/plain"));
                conn.put(location, key, val, headers).connection.getResponseMessage();
            }
        }
        catch(Exception e) {
            throw HostControllerLogger.ROOT_LOGGER.cannotWriteToS3File(e.getLocalizedMessage());
        }
    }

    /**
     * Remove the S3 file that contains the domain controller data.
     *
     * @param directoryName the name of the directory that contains the S3 file
     */
    private void remove(String directoryName) {
        if ((directoryName == null) || (conn == null))
            return;

        String key = S3Util.sanitize(directoryName) + "/" + S3Util.sanitize(DC_FILE_NAME);
        try {
            Map headers = new TreeMap();
            headers.put("Content-Type", Arrays.asList("text/plain"));
            if (usingPreSignedUrls()) {
                conn.delete(pre_signed_delete_url).connection.getResponseMessage();
            } else {
                conn.delete(location, key, headers).connection.getResponseMessage();
            }
        }
        catch(Exception e) {
            ROOT_LOGGER.cannotRemoveS3File(e);
        }
    }

    /**
     * Check which file mode needs to be used.
     *
     * @return
     */
    private DcFileNameMode getFileNameMode() {
        if (conn == null) {
            init();
        }

        try {
            String key = S3Util.sanitize(DcFileNameMode.STANDARD.getDirName()) + "/" + S3Util.sanitize(DcFileNameMode.STANDARD.getFileName());
            if (conn.checkBucketExists(location, key)) {
                return DcFileNameMode.STANDARD;
            }
            return DcFileNameMode.LEGACY;
        } catch (IOException e) {
            throw HostControllerLogger.ROOT_LOGGER.cannotAccessS3Bucket(location, e.getLocalizedMessage());
        }
    }

    public static class DcFileNameMode {
        final String dirName;
        final String fileName;
        final String hostKey;
        final String portKey;
        final String protocolKey;

        // This is the format used by the S3 bucket when the Domain Controller is running on 7.4.x and below
        public static final DcFileNameMode LEGACY = new DcFileNameMode("master", "jboss-domain-master-data", "master_host", "master_port", "master_protocol");
        // This is the format used by the S3 bucket when the Domain Controller is running on EAP 8 and above
        public static final DcFileNameMode STANDARD = new DcFileNameMode("primary", "jboss-domain-primary-data", "primary_host", "primary_port", "primary_protocol");

        public String getDirName() {
            return dirName;
        }

        public String getFileName() {
            return fileName;
        }

        public String getHostKey() {
            return hostKey;
        }

        public String getPortKey() {
            return portKey;
        }

        public String getProtocolKey() {
            return protocolKey;
        }

        /**
         * Encapsulates the path and the key names of the S3 bucket metadata
         *
         * @param dirName the name of the directory in the bucket that contains the S3 file
         * @param fileName the name of the S3 file that contains the Domain Controller configuration
         * @param hostKey the name of the key used to store the Domain Controller hostname
         * @param portKey the name of the key used to store the Domain Controller port number
         * @param protocolKey the name of the key used to store the protocol to use connect to the Domain Controller
         */
        private DcFileNameMode(String dirName, String fileName, String hostKey, String portKey, String protocolKey) {
            this.dirName= dirName;
            this.fileName = fileName;
            this.hostKey = hostKey;
            this.portKey = portKey;
            this.protocolKey = protocolKey;
        }
    }
}
