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

import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.brooklyn.api.catalog.CatalogConfig;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.entity.Group;
import org.apache.brooklyn.api.entity.ImplementedBy;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.annotation.Effector;
import org.apache.brooklyn.core.annotation.EffectorParam;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.effector.MethodEffector;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.trait.MemberReplaceable;
import org.apache.brooklyn.core.sensor.BasicAttributeSensor;
import org.apache.brooklyn.core.sensor.BasicNotificationSensor;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.entity.group.zoneaware.BalancingNodePlacementStrategy;
import org.apache.brooklyn.entity.group.zoneaware.ProportionalZoneFailureDetector;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.time.Duration;

import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.common.reflect.TypeToken;

/**
 * A {@link Cluster} of entities that can dynamically increase or decrease the number of members.
 * <p>
 * When quarantine is enabled:
 * <ul>
 *   <li>The cluster will have a child entity named <em>quarantine</em>, which is a {@link Group}
 *       for nodes that failed to start correctly.
 *   <li>The cluster's other children will be all nodes in the cluster (that have not been
 *       unmanaged/deleted).
 *   <li>The cluster's members will be all live nodes in the cluster.
 *   <li>The <em>quarantine</em> group's members will be any problem nodes (all nodes that failed
 *       to start correctly)
 * </ul>
 * When quarantine is disabled, the cluster will not have a <em>quarantine</em> child. Nodes that
 * fail to start will be removed from the cluster (i.e. stopped and deleted).
 * <p>
 * Advanced users will wish to examine the configuration for the {@link NodePlacementStrategy} and
 * {@link ZoneFailureDetector} interfaces and their implementations, which are used here to control
 * the placement of nodes in particular availability zones and locations when the cluster is resized.
 * 
 * @see DynamicGroup
 * @see DynamicFabric
 */
// TODO document use of advanced availability zone configuration and features
@ImplementedBy(DynamicClusterImpl.class)
@SuppressWarnings("serial")
public interface DynamicCluster extends AbstractGroup, Cluster, MemberReplaceable {

    @Beta
    interface NodePlacementStrategy {
        List<Location> locationsForAdditions(Multimap<Location, Entity> currentMembers, Collection<? extends Location> locs, int numToAdd);
        List<Entity> entitiesToRemove(Multimap<Location, Entity> currentMembers, int numToRemove);
    }

    @Beta
    interface ZoneFailureDetector {
        // TODO Would like to add entity-down reporting
        // TODO Should we push any of this into the AvailabilityZoneExtension, rather than on the dynamic cluster?
        void onStartupSuccess(Location loc, Entity entity);
        void onStartupFailure(Location loc, Entity entity, Throwable cause);
        boolean hasFailed(Location loc);
    }

    MethodEffector<Collection<Entity>> RESIZE_BY_DELTA = new MethodEffector<Collection<Entity>>(DynamicCluster.class, "resizeByDelta");

    @SetFromFlag("restartMode")
    ConfigKey<String> RESTART_MODE = ConfigKeys.newStringConfigKey(
            "dynamiccluster.restartMode", 
            "How this cluster should handle restarts; "
                    + "by default it is disallowed, but this key can specify a different mode. "
                    + "Modes supported by dynamic cluster are 'off', 'sequential', or 'parallel'. "
                    + "However subclasses can define their own modes or may ignore this.", 
            null);

    @SetFromFlag("quarantineFailedEntities")
    ConfigKey<Boolean> QUARANTINE_FAILED_ENTITIES = ConfigKeys.newBooleanConfigKey(
            "dynamiccluster.quarantineFailedEntities", 
            "If true, will quarantine entities that fail to start; if false, will get rid of them (i.e. delete them)", 
            true);

    @SetFromFlag("quarantineFilter")
    ConfigKey<Predicate<? super Throwable>> QUARANTINE_FILTER = ConfigKeys.newConfigKey(
            new TypeToken<Predicate<? super Throwable>>() {},
            "dynamiccluster.quarantineFilter", 
            "Quarantine the failed nodes that pass this filter (given the exception thrown by the node). "
                    + "Default is those that did not fail with NoMachinesAvailableException "
                    + "(Config ignored if quarantineFailedEntities is false)", 
            null);

    AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = Attributes.SERVICE_STATE_ACTUAL;

    BasicNotificationSensor<Entity> ENTITY_QUARANTINED = new BasicNotificationSensor<Entity>(Entity.class, "dynamiccluster.entityQuarantined", "Entity failed to start, and has been quarantined");

    AttributeSensor<QuarantineGroup> QUARANTINE_GROUP = Sensors.newSensor(QuarantineGroup.class, "dynamiccluster.quarantineGroup", "Group of quarantined entities that failed to start");

    @SetFromFlag("initialQuorumSize")
    ConfigKey<Integer> INITIAL_QUORUM_SIZE = ConfigKeys.newIntegerConfigKey(
            "cluster.initial.quorumSize",
            "Initial cluster quorum size - number of initial nodes that must have been successfully started to report success (if < 0, then use value of INITIAL_SIZE)",
            -1);

    @CatalogConfig(label = "Member spec")
    @SetFromFlag("memberSpec")
    ConfigKey<EntitySpec<?>> MEMBER_SPEC = ConfigKeys.newConfigKey(
            new TypeToken<EntitySpec<?>>() { },
            "dynamiccluster.memberspec", "entity spec for creating new cluster members", null);

    @SetFromFlag("firstMemberSpec")
    ConfigKey<EntitySpec<?>> FIRST_MEMBER_SPEC = ConfigKeys.newConfigKey(
            new TypeToken<EntitySpec<?>>() { },
            "dynamiccluster.firstmemberspec", 
            "entity spec for creating the first member of the cluster (if unset, will use the member spec for all)", 
            null);

    @SetFromFlag("removalStrategy")
    ConfigKey<Function<Collection<Entity>, Entity>> REMOVAL_STRATEGY = ConfigKeys.newConfigKey(
            new TypeToken<Function<Collection<Entity>, Entity>>() {},
            "dynamiccluster.removalstrategy", "strategy for deciding what to remove when down-sizing", null);

    @SuppressWarnings("rawtypes")
    @SetFromFlag("customChildFlags")
    ConfigKey<Map> CUSTOM_CHILD_FLAGS = ConfigKeys.newConfigKey(
            Map.class, "dynamiccluster.customChildFlags", "Additional flags to be passed to children when they are being created", ImmutableMap.of());

    @SetFromFlag("enableAvailabilityZones")
    ConfigKey<Boolean> ENABLE_AVAILABILITY_ZONES = ConfigKeys.newBooleanConfigKey(
            "dynamiccluster.zone.enable", "Whether to use availability zones, or just deploy everything into the generic location", false);

    @SetFromFlag("zoneFailureDetector")
    ConfigKey<ZoneFailureDetector> ZONE_FAILURE_DETECTOR = ConfigKeys.newConfigKey(
            ZoneFailureDetector.class, "dynamiccluster.zone.failureDetector", "Zone failure detector", new ProportionalZoneFailureDetector(2, Duration.ONE_HOUR, 0.9));

    @SetFromFlag("zonePlacementStrategy")
    ConfigKey<NodePlacementStrategy> ZONE_PLACEMENT_STRATEGY = ConfigKeys.newConfigKey(
            NodePlacementStrategy.class, "dynamiccluster.zone.placementStrategy", "Node placement strategy", new BalancingNodePlacementStrategy());

    @SetFromFlag("availabilityZoneNames")
    ConfigKey<Collection<String>> AVAILABILITY_ZONE_NAMES = ConfigKeys.newConfigKey(
            new TypeToken<Collection<String>>() {},
            "dynamiccluster.availabilityZones", "availability zones to use (if non-null, overrides other configuration)", null);

    @SetFromFlag("numAvailabilityZones")
    ConfigKey<Integer> NUM_AVAILABILITY_ZONES = ConfigKeys.newIntegerConfigKey(
            "dynamiccluster.numAvailabilityZones", "number of availability zones to use (will attempt to auto-discover this number)");

    @SetFromFlag("clusterMemberId")
    ConfigKey<Integer> CLUSTER_MEMBER_ID = ConfigKeys.newIntegerConfigKey(
            "cluster.member.id", "The unique ID number (sequential) of a member of a cluster");

    ConfigKey<Duration> START_TIMEOUT = newConfigKey(
            Duration.class,
            "start.timeout", 
            "Time to wait (after members' start() effectors return) for SERVICE_UP before failing (default is not to wait)",
            null);

    ConfigKey<Integer> MAX_SIZE = ConfigKeys.newIntegerConfigKey("cluster.max.size", 
            "Size after which it will throw InsufficientCapacityException", 
            Integer.MAX_VALUE);

    @Beta
    @SetFromFlag("maxConcurrentChildCommands")
    ConfigKey<Integer> MAX_CONCURRENT_CHILD_COMMANDS = ConfigKeys.builder(Integer.class)
            .name("dynamiccluster.maxConcurrentChildCommands")
            .description("[Beta] The maximum number of effector invocations that will be made on children at once " +
                    "(e.g. start, stop, restart). Any value null or less than or equal to zero means invocations are unbounded")
            .defaultValue(0)
            .build();

    AttributeSensor<List<Location>> SUB_LOCATIONS = new BasicAttributeSensor<List<Location>>(
            new TypeToken<List<Location>>() {},
            "dynamiccluster.subLocations", "Locations for each availability zone to use");

    AttributeSensor<Set<Location>> FAILED_SUB_LOCATIONS = new BasicAttributeSensor<Set<Location>>(
            new TypeToken<Set<Location>>() {},
            "dynamiccluster.failedSubLocations", "Sub locations that seem to have failed");

    AttributeSensor<Boolean> CLUSTER_MEMBER = Sensors.newBooleanSensor(
            "cluster.member", "Set on an entity if it is a member of a cluster");

    AttributeSensor<Entity> CLUSTER = Sensors.newSensor(Entity.class,
            "cluster.entity", "The cluster an entity is a member of");

    AttributeSensor<Boolean> CLUSTER_ONE_AND_ALL_MEMBERS_UP = Sensors.newBooleanSensor(
            "cluster.one_and_all.members.up", "True if the cluster is running, there is at least one member, and all members are service.isUp");

    /**
     * Changes the cluster size by the given number.
     *
     * @param delta number of nodes to add or remove
     * @return successfully added or removed nodes
     * @see #grow(int)
     */
    @Effector(description="Changes the size of the cluster.")
    Collection<Entity> resizeByDelta(@EffectorParam(name="delta", description="The change in number of nodes") int delta);

    void setRemovalStrategy(Function<Collection<Entity>, Entity> val);

    void setZonePlacementStrategy(NodePlacementStrategy val);

    void setZoneFailureDetector(ZoneFailureDetector val);

    void setMemberSpec(EntitySpec<?> memberSpec);

    Entity addNode(Location loc, Map<?,?> extraFlags);
}
