/*
 * 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.machine;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.Arrays;

import org.apache.brooklyn.api.location.BasicMachineLocationCustomizer;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks.SshEffectorTaskFactory;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
import org.apache.brooklyn.util.core.text.TemplateProcessor;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.reflect.TypeToken;

/**
 * Sets the hostname on an ssh'able machine. Currently only CentOS and RHEL are supported.
 * <p>
 * The customizer can be configured with a hard-coded hostname, or with a freemarker template
 * whose value (after substitutions) will be used for the hostname. 
 */
// TODO basic hostname setting is done by JcloudsLocation, combine that code with this
public class SetHostnameCustomizer extends BasicMachineLocationCustomizer {

    public static final Logger log = LoggerFactory.getLogger(SetHostnameCustomizer.class);
    
    public static final ConfigKey<String> FIXED_HOSTNAME = ConfigKeys.newStringConfigKey(
            "hostname.fixed",
            "The statically defined hostname to be set on the machine (if non-null)");
    
    public static final ConfigKey<String> FIXED_DOMAIN = ConfigKeys.newStringConfigKey(
            "domain.fixed",
            "The statically defined domain name to be set on the machine (if non-null)");
    
    // the  var??  tests if it exists, passing value to ?string(if_present,if_absent)
    // the ! provides a default value afterwards, which is never used, but is required for parsing
    // when the config key is not available;
    // thus the below prefers the first private address, then the first public address, and then
    // substitutes dots for dashes.
    public static final ConfigKey<String> HOSTNAME_TEMPLATE = ConfigKeys.newStringConfigKey(
            "hostname.templated",
            "The hostname template, to be resolved and then set on the machine (if non-null). "
                    +"Assumed to be in free-marker format.",
            "ip-${("
                    + "(location.privateAddresses[0]??)?string("
                            + "location.privateAddresses[0]!'X', "
                            + "(location.publicAddresses[0]??)?string("
                                    + "location.publicAddresses[0]!'X', "
                                    + "\"none\"))"
                    + ")"
                    + "?replace(\".\",\"-\")}"
                    + "-${location.id}");

    public static final ConfigKey<String> LOCAL_HOSTNAME = ConfigKeys.newStringConfigKey(
            "hostname.local.hostname",
            "Host name, as known on the local box. Config is set on the location.");
    
    public static final ConfigKey<String> LOCAL_IP = ConfigKeys.newStringConfigKey(
            "hostname.local.address",
            "Host address, as known on the local box. Config is set on the location.");
    
    public static final ConfigKey<Predicate<? super MachineLocation>> MACHINE_FILTER = ConfigKeys.newConfigKey(
            new TypeToken<Predicate<? super MachineLocation>>() {},
            "machineFilter",
            "A filter to say which machines this should be applied to",
            Predicates.instanceOf(SshMachineLocation.class));
    
    private final ConfigBag config;

    public SetHostnameCustomizer(ConfigBag config) {
        // TODO Any checks that they've given us sufficient configuration?
        this.config = config;
    }
    
    @Override
    public void customize(MachineLocation machine) {
        if (config.get(MACHINE_FILTER).apply(machine)) {
            log.info("SetHostnameCustomizer setting hostname on "+machine);
        } else {
            log.info("SetHostnameCustomizer ignoring non-ssh machine "+machine);
            return;
        }

        try {
            String localHostname = setLocalHostname((SshMachineLocation) machine);
            machine.config().set(LOCAL_HOSTNAME, localHostname);
    
            String localIp = execHostnameMinusI((SshMachineLocation) machine);
            machine.config().set(LOCAL_IP, localIp);
        } catch (Exception e) {
            log.info("SetHostnameCustomizer failed to set hostname on "+machine+" (rethrowing)", e);
            throw e;
        }
    }

    protected String generateHostname(SshMachineLocation machine) {
        String hostnameTemplate = config.get(HOSTNAME_TEMPLATE);
        if (Strings.isNonBlank(hostnameTemplate)) {
            return TemplateProcessor.processTemplateContents(hostnameTemplate, machine, ImmutableMap.<String, Object>of());
        } else {
            return null;
        }
    }

    /**
     * Sets the machine's hostname to the value controlled by fixed_hostname and hostname_template. 
     * If these are blank (and fixed_domain is blank), then just return the current hostname of 
     * the machine.
     */
    public String setLocalHostname(SshMachineLocation machine) {
        String hostFixed = config.get(FIXED_HOSTNAME);
        String domainFixed = config.get(FIXED_DOMAIN);
        String hostnameTemplate = config.get(HOSTNAME_TEMPLATE);
        
        String hostname;
        if (Strings.isNonBlank(hostFixed)) {
            hostname = hostFixed;
        } else {
            if (Strings.isNonBlank(hostnameTemplate)) {
                hostname = generateHostname(machine);
            } else {
                hostname = execHostname(machine);
                if (Strings.isBlank(domainFixed)) {
                    return hostname;
                }
            }
        }
        
        return setLocalHostname(machine, hostname, domainFixed);
    }
    
    /**
     * Sets the machine's hostname to the given value, ensuring /etc/hosts and /etc/sysconfig/network are both 
     * correctly updated.
     */
    public String setLocalHostname(SshMachineLocation machine, String hostName, String domainFixed) {
        log.info("Setting local hostname of " + machine + " to " + hostName 
                + (Strings.isNonBlank(domainFixed) ? ", " + domainFixed : ""));
        
        boolean hasDomain = Strings.isNonBlank(domainFixed);
        String fqdn = hasDomain ? hostName+"."+domainFixed : hostName;
        
        exec(machine, true, 
                BashCommands.sudo(String.format("sed -i.bak -e '1i127.0.0.1 %s %s' -e '/^127.0.0.1/d' /etc/hosts", fqdn, hostName)),
                BashCommands.sudo(String.format("sed -i.bak -e 's/^HOSTNAME=.*$/HOSTNAME=%s/' /etc/sysconfig/network", fqdn)),
                BashCommands.sudo(String.format("hostname %s", fqdn)));

        return hostName;
    }
    
    protected void registerEtcHosts(SshMachineLocation machine, String ip, Iterable<String> hostnames) {
        log.info("Updating /etc/hosts of "+machine+": adding "+ip+" = "+hostnames);
        
        checkArgument(Strings.isNonBlank(ip) && Networking.isValidIp4(ip), "invalid IPv4 address %s", ip);
        if (Strings.isBlank(ip) || Iterables.isEmpty(hostnames)) return;
        String line = ip+" "+Joiner.on(" ").join(hostnames);
        exec(machine, true, "echo " + line + " >> /etc/hosts");
    }
    
    protected String execHostname(SshMachineLocation machine) {
        if (log.isDebugEnabled()) log.debug("Retrieve `hostname` via ssh for {}", machine);
        
        ProcessTaskWrapper<Integer> cmd = exec(machine, false, "echo hostname=`hostname`");
//        ProcessTaskWrapper<Integer> cmd = DynamicTasks.queue(SshEffectorTasks.ssh(machine, "echo hostname=`hostname`")
//                .summary("getHostname"))
//                .block();

        for (String line : cmd.getStdout().split("\n")) {
            if (line.contains("hostname=") && !line.contains("`hostname`")) {
                return line.substring(line.indexOf("hostname=") + "hostname=".length()).trim();
            }
        }
        log.info("No hostname found for {} (got {}; {})", new Object[] {machine, cmd.getStdout(), cmd.getStderr()});
        return null;
    }

    protected String execHostnameMinusI(SshMachineLocation machine) {
        if (log.isDebugEnabled()) log.debug("Retrieve `hostname -I` via ssh for {}", machine);
        
        ProcessTaskWrapper<Integer> cmd = exec(machine, false, "echo localip=`hostname -I`");

        for (String line : cmd.getStdout().split("\n")) {
            if (line.contains("localip=") && !line.contains("`hostname -I`")) {
                return line.substring(line.indexOf("localip=") + "localip=".length()).trim();
            }
        }
        log.info("No local ip found for {} (got {}; {})", new Object[] {machine, cmd.getStdout(), cmd.getStderr()});
        return null;
    }
    
    protected ProcessTaskWrapper<Integer> exec(SshMachineLocation machine, boolean asRoot, String... cmds) {
        SshEffectorTaskFactory<Integer> taskFactory = SshEffectorTasks.ssh(machine, cmds);
        if (asRoot) taskFactory.runAsRoot();
        ProcessTaskWrapper<Integer> result = DynamicTasks.queue(taskFactory).block();
        if (result.get() != 0) {
            throw new IllegalStateException("SetHostnameCustomizer got exit code "+result.get()+" executing on machine "+machine
                    +"; cmds="+Arrays.asList(cmds)+"; stdout="+result.getStdout()+"; stderr="+result.getStderr());
        }
        return result;
    }
}
