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

import java.util.Map;

import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.entity.drivers.DriverDependentEntity;
import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.config.MapConfigKey;
import org.apache.brooklyn.core.effector.EffectorBody;
import org.apache.brooklyn.core.effector.Effectors;
import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.EntityTasks;
import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
import org.apache.brooklyn.entity.brooklynnode.BrooklynNodeDriver;
import org.apache.brooklyn.entity.software.base.SoftwareProcess;
import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters;
import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.Beta;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;

@SuppressWarnings("serial")
/** Upgrades a brooklyn node in-place on the box, 
 * by creating a child brooklyn node and ensuring it can rebind in HOT_STANDBY
 * <p>
 * Requires the target node to have persistence enabled. 
 */
public class BrooklynNodeUpgradeEffectorBody extends EffectorBody<Void> {

    private static final Logger log = LoggerFactory.getLogger(BrooklynNodeUpgradeEffectorBody.class);
    
    public static final ConfigKey<String> DOWNLOAD_URL = BrooklynNode.DOWNLOAD_URL.getConfigKey();
    public static final ConfigKey<Boolean> DO_DRY_RUN_FIRST = ConfigKeys.newBooleanConfigKey(
            "doDryRunFirst", "Test rebinding with a temporary instance before stopping the entity for upgrade.", true);
    public static final ConfigKey<Map<String,Object>> EXTRA_CONFIG = MapConfigKey.builder(new TypeToken<Map<String,Object>>() {})
        .name("extraConfig")
        .description("Additional new config to set on the BrooklynNode as part of upgrading")
        .build();

    public static final Effector<Void> UPGRADE = Effectors.effector(Void.class, "upgrade")
        .description("Changes the Brooklyn build used to run this node, "
            + "by spawning a dry-run node then copying the installed files across. "
            + "This node must be running for persistence for in-place upgrading to work.")
        .parameter(BrooklynNode.SUGGESTED_VERSION)
        .parameter(DOWNLOAD_URL)
        .parameter(DO_DRY_RUN_FIRST)
        .parameter(EXTRA_CONFIG)
        .impl(new BrooklynNodeUpgradeEffectorBody()).build();
    
    @Override
    public Void call(ConfigBag parametersO) {
        if (!isPersistenceModeEnabled(entity())) {
            // would could try a `forcePersistNow`, but that's sloppy; 
            // for now, require HA/persistence for upgrading
            DynamicTasks.queue( Tasks.warning("Check persistence", 
                new IllegalStateException("Persistence does not appear to be enabled at this cluster. "
                + "In-place node upgrade will not succeed unless a custom launch script enables it.")) );
        }

        final ConfigBag parameters = ConfigBag.newInstanceCopying(parametersO);

        /*
         * all parameters are passed to children, apart from EXTRA_CONFIG
         * whose value (as a map) is so passed; it provides an easy way to set extra config in the gui.
         * (IOW a key-value mapping can be passed either inside EXTRA_CONFIG or as a sibling to EXTRA_CONFIG)  
         */
        if (parameters.containsKey(EXTRA_CONFIG)) {
            Map<String, Object> extra = parameters.get(EXTRA_CONFIG);
            parameters.remove(EXTRA_CONFIG);
            parameters.putAll(extra);
        }
        log.debug(this+" upgrading, using "+parameters);
        
        final String bkName;
        boolean doDryRunFirst = parameters.get(DO_DRY_RUN_FIRST);
        if(doDryRunFirst) {
            bkName = dryRunUpdate(parameters);
        } else {
            bkName = "direct-"+Identifiers.makeRandomId(4);
        }
        
        // Stop running instance
        DynamicTasks.queue(Tasks.builder().displayName("shutdown node")
                .add(Effectors.invocation(entity(), BrooklynNode.STOP_NODE_BUT_LEAVE_APPS, ImmutableMap.of(StopSoftwareParameters.STOP_MACHINE_MODE, StopMode.NEVER)))
                .build());

        // backup old files
        DynamicTasks.queue(Tasks.builder().displayName("backup old version").body(new Runnable() {
            @Override
            public void run() {
                String runDir = entity().getAttribute(SoftwareProcess.RUN_DIR);
                String bkDir = Urls.mergePaths(runDir, "..", Urls.getBasename(runDir)+"-backups", bkName);
                log.debug(this+" storing backup of previous version in "+bkDir);
                DynamicTasks.queue(SshEffectorTasks.ssh(
                    "cd "+runDir,
                    "mkdir -p "+bkDir,
                    "mv * "+bkDir
                    // By removing the run dir of the entity we force it to go through
                    // the customize step again on start and re-generate local-brooklyn.properties.
                    ).summary("move files"));
            }
        }).build());
        
        // Reconfigure entity
        DynamicTasks.queue(Tasks.builder().displayName("reconfigure").body(new Runnable() {
            @Override
            public void run() {
                DynamicTasks.waitForLast();
                entity().sensors().set(SoftwareProcess.INSTALL_DIR, (String)null);
                entity().config().set(SoftwareProcess.INSTALL_UNIQUE_LABEL, (String)null);
                entity().config().putAll(parameters.getAllConfig());
                entity().sensors().set(BrooklynNode.DOWNLOAD_URL, entity().getConfig(DOWNLOAD_URL));

                // Setting SUGGESTED_VERSION will result in an new empty INSTALL_FOLDER, but clear it
                // just in case the user specified already installed version.
                ((BrooklynNodeDriver)((DriverDependentEntity<?>)entity()).getDriver()).clearInstallDir();
            }
        }).build());
        
        // Start this entity, running the new version.
        // This will download and install the new dist (if not already done by the dry run node).
        DynamicTasks.queue(Effectors.invocation(entity(), BrooklynNode.START, ConfigBag.EMPTY));

        return null;
    }

    private String dryRunUpdate(ConfigBag parameters) {
        // TODO require entity() node state master or hot standby AND require persistence enabled, or a new 'force_attempt_upgrade' parameter to be applied
        // TODO could have a 'skip_dry_run_upgrade' parameter
        // TODO could support 'dry_run_only' parameter, with optional resumption tasks (eg new dynamic effector)

        // 1 add new brooklyn version entity as child (so uses same machine), with same config apart from things in parameters
        final Entity dryRunChild = entity().addChild(createDryRunSpec()
            .displayName("Upgraded Version Dry-Run Node")
            // install dir and label are recomputed because they are not inherited, and download_url will normally be different
            .configure(parameters.getAllConfig()));

        //force this to start as hot-standby
        // TODO alternatively could use REST API as in BrooklynClusterUpgradeEffectorBody
        // TODO Want better way to append to the config (so can do it all in the spec)
        String launchParameters = dryRunChild.getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS);
        if (Strings.isBlank(launchParameters)) launchParameters = "";
        else launchParameters += " ";
        launchParameters += "--highAvailability "+HighAvailabilityMode.HOT_STANDBY;
        ((EntityInternal)dryRunChild).config().set(BrooklynNode.EXTRA_LAUNCH_PARAMETERS, launchParameters);

        final String dryRunNodeUid = dryRunChild.getId();
        ((EntityInternal)dryRunChild).setDisplayName("Dry-Run Upgraded Brooklyn Node ("+dryRunNodeUid+")");

        DynamicTasks.queue(Effectors.invocation(dryRunChild, BrooklynNode.START, ConfigBag.EMPTY));

        // 2 confirm hot standby status
        DynamicTasks.queue(EntityTasks.requiringAttributeEventually(dryRunChild, BrooklynNode.MANAGEMENT_NODE_STATE, 
            Predicates.equalTo(ManagementNodeState.HOT_STANDBY), Duration.FIVE_MINUTES));

        // 3 stop new version
        DynamicTasks.queue(Tasks.builder().displayName("shutdown transient node")
            .add(Effectors.invocation(dryRunChild, BrooklynNode.STOP_NODE_BUT_LEAVE_APPS, ImmutableMap.of(StopSoftwareParameters.STOP_MACHINE_MODE, StopMode.NEVER)))
            .build());

        DynamicTasks.queue(Tasks.<Void>builder().displayName("remove transient node").body(
            new Runnable() {
                @Override
                public void run() {
                    Entities.unmanage(dryRunChild);
                }
            }
        ).build());

        return dryRunChild.getId();
    }

    protected EntitySpec<? extends BrooklynNode> createDryRunSpec() {
        return EntitySpec.create(BrooklynNode.class);
    }

    @Beta
    static boolean isPersistenceModeEnabled(Entity entity) {
        // TODO when there are PERSIST* options in BrooklynNode, look at them here!
        // or, even better, make a REST call to check persistence
        String params = null;
        if (entity instanceof BrooklynCluster) {
            EntitySpec<?> spec = entity.getConfig(BrooklynCluster.MEMBER_SPEC);
            params = Strings.toString( spec.getConfig().get(BrooklynNode.EXTRA_LAUNCH_PARAMETERS) );
        }
        if (params==null) params = entity.getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS);
        if (params==null) return false;
        if (params.indexOf("persist")==0) return false;
        return true;
    }

}
