/*
 * 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.brooklyn.entity.java;

import java.util.EnumSet;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.location.Locations;
import org.apache.brooklyn.core.location.access.BrooklynAccessUtils;
import org.apache.brooklyn.feed.jmx.JmxHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.BrooklynMavenArtifacts;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.jmx.jmxmp.JmxmpAgent;
import org.apache.brooklyn.util.jmx.jmxrmi.JmxRmiAgent;
import org.apache.brooklyn.util.maven.MavenArtifact;
import org.apache.brooklyn.util.maven.MavenRetriever;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.text.Strings;

import com.google.common.base.Preconditions;
import com.google.common.net.HostAndPort;

public class JmxSupport implements UsesJmx {

    private static final Logger log = LoggerFactory.getLogger(JmxSupport.class);

    private final Entity entity;
    private final String runDir;

    private Boolean isJmx;
    private Boolean isSecure;
    private JmxAgentModes jmxAgentMode;

    private static boolean warnedAboutNotOnClasspath = false;

    /** run dir may be null if it is not accessed */
    public JmxSupport(Entity entity, @Nullable String runDir) {
        this.entity = Preconditions.checkNotNull(entity, "entity must be supplied");
        this.runDir = runDir;
    }

    @Nonnull
    public String getRunDir() {
        return Preconditions.checkNotNull(runDir, "runDir must have been supplied to perform this operation");
    }

    public Entity getEntity() {
        return entity;
    }

    <T> T getConfig(ConfigKey<T> key) {
        return getEntity().getConfig(key);
    }

    <T> T getConfig(HasConfigKey<T> key) {
        return getEntity().getConfig(key);
    }

    <T> void setConfig(ConfigKey<T> key, T value) {
        getEntity().config().set(key, value);
    }

    public Maybe<SshMachineLocation> getMachine() {
        return Locations.findUniqueSshMachineLocation(entity.getLocations());
    }

    public boolean isJmx() {
        init();
        return isJmx;
    }

    public JmxAgentModes getJmxAgentMode() {
        init();
        if (jmxAgentMode==null) return JmxAgentModes.NONE;
        return jmxAgentMode;
    }

    public boolean isSecure() {
        init();
        if (isSecure==null) return false;
        return isSecure;
    }

    protected synchronized void init() {
        if (isJmx!=null)
            return;

        if (Boolean.FALSE.equals(entity.getConfig(USE_JMX))) {
            isJmx = false;
            return;
        }
        isJmx = true;
        jmxAgentMode = entity.getConfig(JMX_AGENT_MODE);
        if (jmxAgentMode==null) jmxAgentMode = JmxAgentModes.AUTODETECT;

        isSecure = entity.getConfig(JMX_SSL_ENABLED);
        if (isSecure==null) isSecure = false;

        if (jmxAgentMode==JmxAgentModes.AUTODETECT) {
            if (isSecure()) {
                jmxAgentMode = JmxAgentModes.JMXMP;
            } else {
                jmxAgentMode = JmxAgentModes.JMXMP_AND_RMI;
                if (!ResourceUtils.create(this).doesUrlExist(getJmxAgentJarUrl())) {
                    // can happen e.g. if eclipse build
                    log.warn("JMX agent JAR not found ("+getJmxAgentJarUrl()+") when auto-detecting JMX settings for "+entity+"; " +
                            "likely cause is an incomplete build (e.g. from Eclipse; run a maven build then retry in the IDE); "+
                            "reverting to NONE (use built-in Java JMX support, which will not go through firewalls)");
                    jmxAgentMode = JmxAgentModes.NONE;
                }
            }

            entity.config().set(JMX_AGENT_MODE, jmxAgentMode);
        }

        if (isSecure && jmxAgentMode!=JmxAgentModes.JMXMP) {
            String msg = "JMX SSL is specified, but it requires JMXMP which is disabled, when configuring "+entity;
            log.warn(msg);
            throw new IllegalStateException(msg);
        }
    }

    public void setJmxUrl() {
        ((EntityInternal)entity).sensors().set(JMX_URL, getJmxUrl());
    }

    public String getJmxUrl() {
        init();

        HostAndPort jmx = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, entity.getAttribute(JMX_PORT));

        if (EnumSet.of(JmxAgentModes.JMXMP, JmxAgentModes.JMXMP_AND_RMI).contains(getJmxAgentMode())) {
            return JmxHelper.toJmxmpUrl(jmx.getHostText(), jmx.getPort());
        } else {
            if (getJmxAgentMode() == JmxAgentModes.NONE) {
                fixPortsForModeNone();
            }
            // this will work for agent or agentless
            HostAndPort rmi = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, entity.getAttribute(RMI_REGISTRY_PORT));
            return JmxHelper.toRmiJmxUrl(jmx.getHostText(), jmx.getPort(), rmi.getPort(),
                    entity.getAttribute(JMX_CONTEXT));
        }
    }

    /** mode NONE cannot set a JMX (RMI server) port; it needs an RMI registry port,
     * then gets redirected to an anonymous RMI server port;
     * both the hostname and the anonymous port must be accessible to use this mode
     * (hence the use of the other agents in most cases) */
    protected int fixPortsForModeNone() {
        assert getJmxAgentMode()==JmxAgentModes.NONE;
        Integer jmxRemotePort = getEntity().getAttribute(JMX_PORT);
        Integer rmiRegistryPort = getEntity().getAttribute(RMI_REGISTRY_PORT);
        if (rmiRegistryPort!=null && rmiRegistryPort>0) {
            if (jmxRemotePort==null || jmxRemotePort!=rmiRegistryPort) {
                if (jmxRemotePort!=null && jmxRemotePort>0) {
                    // ignore RMI registry port when mode 'none' is set -- set same as JMX port here
                    // (bit irritating, but JMX_PORT will be ignored in this mode)
                    log.warn("Ignoring JMX_PORT "+jmxRemotePort+" when configuring agentless JMX on "+getEntity()+"; will use RMI_REGISTRY_PORT "+rmiRegistryPort);
                }
                jmxRemotePort = rmiRegistryPort;
                getEntity().sensors().set(JMX_PORT, jmxRemotePort);
            }
        } else {
            if (jmxRemotePort==null || jmxRemotePort<=0) {
                throw new IllegalStateException("Invalid JMX_PORT "+jmxRemotePort+" and RMI_REGISTRY_PORT "+rmiRegistryPort+" when configuring JMX "+getJmxAgentMode()+" on "+getEntity());
            }
            getEntity().sensors().set(RMI_REGISTRY_PORT, jmxRemotePort);
        }
        return jmxRemotePort;
    }

    public List<String> getJmxJavaConfigOptions() {
        if (EnumSet.<JmxAgentModes>of(JmxAgentModes.NONE, JmxAgentModes.JMX_RMI).contains(getJmxAgentMode())) {
            return MutableList.of();
        } else {
            return MutableList.of(String.format("-javaagent:%s", getJmxAgentJarDestinationFilePath()));
        }
    }

    public String getJmxAgentJarDestinationFilePath() {
        // cache the local path so we continue to work post-rebind to a different version
        String result = getEntity().getAttribute(JMX_AGENT_LOCAL_PATH);
        if (Strings.isNonBlank(result)) return result;
        result = getJmxAgentJarDestinationFilePathDefault();
        ((EntityInternal)getEntity()).sensors().set(JMX_AGENT_LOCAL_PATH, result);
        return result;
    }
    
    public String getJmxAgentJarDestinationFilePathDefault() {
        return Urls.mergePaths(getRunDir(), getJmxAgentJarBasename());
    }

    @Nullable public MavenArtifact getJmxAgentJarMavenArtifact() {
        switch (getJmxAgentMode()) {
        case JMXMP:
        case JMXMP_AND_RMI:
            MavenArtifact result = BrooklynMavenArtifacts.artifact(null, "brooklyn-jmxmp-agent", "jar", "with-dependencies");
            // the "with-dependencies" variant is needed; however the filename then has the classifier segment _replaced_ by "shaded" when this filename is created
            result.setCustomFileNameAfterArtifactMarker("shaded");
            result.setClassifierFileNameMarker("");
            return result;
        case JMX_RMI_CUSTOM_AGENT:
            return BrooklynMavenArtifacts.jar("brooklyn-jmxrmi-agent");
        default:
            return null;
        }
    }

    /** @deprecated since 0.6.0; use {@link #getJmxAgentJarMavenArtifact()} */
    @Deprecated
    public String getJmxAgentJarBasename() {
        MavenArtifact artifact = getJmxAgentJarMavenArtifact();
        if (artifact==null)
            throw new IllegalStateException("Either JMX is not enabled or there is an error in the configuration (JMX mode "+getJmxAgentMode()+" does not support agent JAR)");
        return artifact.getFilename();
    }

    /** returns URL for accessing the java agent, throwing if not applicable;
     * prefers on classpath where it should be, but will fall back to taking from maven hosted
     * (known problem in Eclipse where JARs are not always copied)
     */
    public String getJmxAgentJarUrl() {
        MavenArtifact artifact = getJmxAgentJarMavenArtifact();
        if (artifact==null)
            throw new IllegalStateException("Either JMX is not enabled or there is an error in the configuration (JMX mode "+getJmxAgentMode()+" does not support agent JAR)");
        String jar = "classpath://" + artifact.getFilename();
        if (ResourceUtils.create(this).doesUrlExist(jar))
            return jar;

        String result = MavenRetriever.localUrl(artifact);
        if (warnedAboutNotOnClasspath) {
            log.debug("JMX JAR for "+artifact+" is not on the classpath; taking from "+result);
        } else {
            log.warn("JMX JAR for "+artifact+" is not on the classpath; taking from "+result+" (subsequent similar messages will be logged at debug)");
            warnedAboutNotOnClasspath = true;
        }
        return result;
    }

    /** applies _some_ of the common settings needed to connect via JMX */
    public void applyJmxJavaSystemProperties(MutableMap.Builder<String,Object> result) {
        if (!isJmx()) return ;

        Integer jmxPort = Preconditions.checkNotNull(entity.getAttribute(JMX_PORT), "jmx port must not be null for %s", entity);
        HostAndPort jmx = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, jmxPort);
        Integer jmxRemotePort = getEntity().getAttribute(JMX_PORT);
        String hostName = jmx.getHostText();

        result.put("com.sun.management.jmxremote", null);
        result.put("java.rmi.server.hostname", hostName);

        switch (getJmxAgentMode()) {
        case JMXMP_AND_RMI:
            Integer rmiRegistryPort = Preconditions.checkNotNull(entity.getAttribute(UsesJmx.RMI_REGISTRY_PORT), "registry port (config val %s)", entity.getConfig(UsesJmx.RMI_REGISTRY_PORT));
            result.put(JmxmpAgent.RMI_REGISTRY_PORT_PROPERTY, rmiRegistryPort);
        case JMXMP:
            if (jmxRemotePort==null || jmxRemotePort<=0)
                throw new IllegalStateException("Unsupported JMX port "+jmxRemotePort+" - when applying system properties ("+getJmxAgentMode()+" / "+getEntity()+")");
            result.put(JmxmpAgent.JMXMP_PORT_PROPERTY, jmxRemotePort);
            // with JMXMP don't try to tell it the hostname -- it isn't needed for JMXMP, and if specified
            // it will break if the hostname we see is not known at the server, e.g. a forwarding public IP
            result.remove("java.rmi.server.hostname");
            break;
        case JMX_RMI_CUSTOM_AGENT:
            if (jmxRemotePort==null || jmxRemotePort<=0)
                throw new IllegalStateException("Unsupported JMX port "+jmxRemotePort+" - when applying system properties ("+getJmxAgentMode()+" / "+getEntity()+")");
            result.put(JmxRmiAgent.RMI_REGISTRY_PORT_PROPERTY, Preconditions.checkNotNull(entity.getAttribute(UsesJmx.RMI_REGISTRY_PORT), "registry port"));
            result.put(JmxRmiAgent.JMX_SERVER_PORT_PROPERTY, jmxRemotePort);
            break;
        case NONE:
            jmxRemotePort = fixPortsForModeNone();
        case JMX_RMI:
            result.put("com.sun.management.jmxremote.port", jmxRemotePort);
            result.put("java.rmi.server.useLocalHostname", "true");
            break;
        default:
            throw new IllegalStateException("Unsupported JMX mode - when applying system properties ("+getJmxAgentMode()+" / "+getEntity()+")");
        }

        if (isSecure()) {
            // set values true, and apply keys pointing to keystore / truststore
            getJmxSslSupport().applyAgentJmxJavaSystemProperties(result);
        } else {
            result.
                put("com.sun.management.jmxremote.ssl", false).
                put("com.sun.management.jmxremote.authenticate", false);
        }
    }

    /** installs files needed for JMX, to the runDir given in constructor, assuming the runDir has been created */
    public void install() {
        if (EnumSet.of(JmxAgentModes.JMXMP_AND_RMI, JmxAgentModes.JMXMP, JmxAgentModes.JMX_RMI_CUSTOM_AGENT).contains(getJmxAgentMode())) {
            Tasks.setBlockingDetails("Copying JMX agent jar to server.");
            try {
                getMachine().get().copyTo(ResourceUtils.create(this).getResourceFromUrl(
                        getJmxAgentJarUrl()), getJmxAgentJarDestinationFilePath());
            } finally {
                Tasks.resetBlockingDetails();
            }
        }
        if (isSecure()) {
            getJmxSslSupport().install();
        }
    }

    protected JmxmpSslSupport getJmxSslSupport() {
        return new JmxmpSslSupport(this);
    }

    /** sets JMR_RMI_CUSTOM_AGENT as the connection mode for the indicated apps.
     * <p>
     * TODO callers of this method have RMI dependencies in the actual app;
     * we should look at removing them, so that those pieces of software can run behind
     * forwarding public IP's and over SSL (both reasons JMXMP is preferred by us!)
     */
    public void recommendJmxRmiCustomAgent() {
        // set JMX_RMI because the registry is needed (i think)
        Maybe<Object> jmx = ((EntityInternal)entity).config().getRaw(UsesJmx.JMX_AGENT_MODE);
        if (!jmx.isPresentAndNonNull()) {
            setConfig(UsesJmx.JMX_AGENT_MODE, JmxAgentModes.JMX_RMI_CUSTOM_AGENT);
        } else if (jmx.get()!=JmxAgentModes.JMX_RMI_CUSTOM_AGENT) {
            log.warn("Entity "+entity+" may not function unless running JMX_RMI_CUSTOM_AGENT mode (asked to use "+jmx.get()+")");
        }
    }

}
