// 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 com.cloud.network.as;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;

import com.cloud.network.NetworkModel;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.affinity.AffinityGroupVO;
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd.HTTPMethod;
import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.admin.autoscale.CreateCounterCmd;
import org.apache.cloudstack.api.command.user.autoscale.CreateAutoScalePolicyCmd;
import org.apache.cloudstack.api.command.user.autoscale.CreateAutoScaleVmGroupCmd;
import org.apache.cloudstack.api.command.user.autoscale.CreateAutoScaleVmProfileCmd;
import org.apache.cloudstack.api.command.user.autoscale.CreateConditionCmd;
import org.apache.cloudstack.api.command.user.autoscale.ListAutoScalePoliciesCmd;
import org.apache.cloudstack.api.command.user.autoscale.ListAutoScaleVmGroupsCmd;
import org.apache.cloudstack.api.command.user.autoscale.ListAutoScaleVmProfilesCmd;
import org.apache.cloudstack.api.command.user.autoscale.ListConditionsCmd;
import org.apache.cloudstack.api.command.user.autoscale.ListCountersCmd;
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScalePolicyCmd;
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmGroupCmd;
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmProfileCmd;
import org.apache.cloudstack.api.command.user.autoscale.UpdateConditionCmd;
import org.apache.cloudstack.api.command.user.vm.DeployVMCmd;
import org.apache.cloudstack.config.ApiServiceConfiguration;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.userdata.UserDataManager;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;

import com.cloud.agent.AgentManager;
import com.cloud.agent.api.PerformanceMonitorAnswer;
import com.cloud.agent.api.PerformanceMonitorCommand;
import com.cloud.agent.api.VmStatsEntry;
import com.cloud.agent.api.routing.GetAutoScaleMetricsAnswer;
import com.cloud.agent.api.routing.GetAutoScaleMetricsCommand;
import com.cloud.agent.api.to.LoadBalancerTO.AutoScalePolicyTO;
import com.cloud.agent.api.to.LoadBalancerTO.AutoScaleVmGroupTO;
import com.cloud.agent.api.to.LoadBalancerTO.AutoScaleVmProfileTO;
import com.cloud.agent.api.to.LoadBalancerTO.ConditionTO;
import com.cloud.agent.api.to.LoadBalancerTO.CounterTO;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.dispatch.DispatchChainFactory;
import com.cloud.api.dispatch.DispatchTask;
import com.cloud.configuration.ConfigurationManager;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.event.ActionEvent;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes;
import com.cloud.event.EventVO;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InsufficientServerCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceInUseException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.network.Network;
import com.cloud.network.Network.Capability;
import com.cloud.network.Network.IpAddresses;
import com.cloud.network.as.AutoScaleCounter.AutoScaleCounterParam;
import com.cloud.network.as.dao.AutoScalePolicyConditionMapDao;
import com.cloud.network.as.dao.AutoScalePolicyDao;
import com.cloud.network.as.dao.AutoScaleVmGroupDao;
import com.cloud.network.as.dao.AutoScaleVmGroupPolicyMapDao;
import com.cloud.network.as.dao.AutoScaleVmGroupStatisticsDao;
import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao;
import com.cloud.network.as.dao.AutoScaleVmProfileDao;
import com.cloud.network.as.dao.ConditionDao;
import com.cloud.network.as.dao.CounterDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.LoadBalancerDao;
import com.cloud.network.dao.LoadBalancerVMMapDao;
import com.cloud.network.dao.LoadBalancerVMMapVO;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.lb.LoadBalancingRulesManager;
import com.cloud.network.lb.LoadBalancingRulesService;
import com.cloud.network.router.RouterControlHelper;
import com.cloud.network.router.VirtualRouter;
import com.cloud.network.router.VirtualRouterAutoScale.AutoScaleMetrics;
import com.cloud.network.router.VirtualRouterAutoScale.AutoScaleMetricsValue;
import com.cloud.network.router.VirtualRouterAutoScale.AutoScaleValueType;
import com.cloud.network.router.VirtualRouterAutoScale.VirtualRouterAutoScaleCounter;
import com.cloud.offering.DiskOffering;
import com.cloud.offering.NetworkOffering;
import com.cloud.offering.ServiceOffering;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.projects.Project.ListProjectResourcesCriteria;
import com.cloud.server.ResourceTag;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.template.TemplateManager;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountService;
import com.cloud.user.SSHKeyPairVO;
import com.cloud.user.User;
import com.cloud.user.dao.AccountDao;
import com.cloud.user.dao.SSHKeyPairDao;
import com.cloud.user.dao.UserDao;
import com.cloud.uservm.UserVm;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDao;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.JoinBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.SearchCriteria.Op;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.DomainRouterVO;
import com.cloud.vm.UserVmManager;
import com.cloud.vm.UserVmService;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.VmStats;
import com.cloud.vm.dao.DomainRouterDao;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class AutoScaleManagerImpl extends ManagerBase implements AutoScaleManager, AutoScaleService, Configurable {

    @Inject
    protected DispatchChainFactory dispatchChainFactory = null;
    @Inject
    EntityManager entityMgr;
    @Inject
    AccountDao accountDao;
    @Inject
    AccountManager accountMgr;
    @Inject
    ConfigurationManager configMgr;
    @Inject
    TemplateManager templateMgr;
    @Inject
    LoadBalancingRulesManager lbRulesMgr;
    @Inject
    NetworkDao networkDao;
    @Inject
    CounterDao counterDao;
    @Inject
    ConditionDao conditionDao;
    @Inject
    LoadBalancerDao lbDao;
    @Inject
    AutoScaleVmProfileDao autoScaleVmProfileDao;
    @Inject
    AutoScalePolicyDao autoScalePolicyDao;
    @Inject
    AutoScalePolicyConditionMapDao autoScalePolicyConditionMapDao;
    @Inject
    AutoScaleVmGroupDao autoScaleVmGroupDao;
    @Inject
    AutoScaleVmGroupPolicyMapDao autoScaleVmGroupPolicyMapDao;
    @Inject
    AutoScaleVmGroupVmMapDao autoScaleVmGroupVmMapDao;
    @Inject
    DataCenterDao dcDao = null;
    @Inject
    UserDao userDao;
    @Inject
    ConfigurationDao configDao;
    @Inject
    IPAddressDao ipAddressDao;
    @Inject
    AccountService accountService;
    @Inject
    UserVmService userVmService;
    @Inject
    LoadBalancerVMMapDao lbVmMapDao;
    @Inject
    LoadBalancingRulesService loadBalancingRulesService;
    @Inject
    private VMInstanceDao vmInstanceDao;
    @Inject
    private AgentManager agentMgr;
    @Inject
    private ServiceOfferingDao serviceOfferingDao;
    @Inject
    NetworkOrchestrationService networkMgr;
    @Inject
    NetworkModel networkModel;
    @Inject
    private UserVmManager userVmMgr;
    @Inject
    private UserDataManager userDataMgr;
    @Inject
    private UserVmDao userVmDao;
    @Inject
    private HostDao hostDao;
    @Inject
    private AutoScaleVmGroupStatisticsDao asGroupStatisticsDao;
    @Inject
    private DomainRouterDao routerDao;
    @Inject
    private AnnotationDao annotationDao;
    @Inject
    protected RouterControlHelper routerControlHelper;
    @Inject
    private DiskOfferingDao diskOfferingDao;
    @Inject
    private SSHKeyPairDao sshKeyPairDao;
    @Inject
    private AffinityGroupDao affinityGroupDao;
    @Inject
    private NetworkOfferingDao networkOfferingDao;
    @Inject
    private VirtualMachineManager virtualMachineManager;

    private static final String PARAM_ROOT_DISK_SIZE = "rootdisksize";
    private static final String PARAM_DISK_OFFERING_ID = "diskofferingid";
    private static final String PARAM_DISK_SIZE = "disksize";
    private static final String PARAM_OVERRIDE_DISK_OFFERING_ID = "overridediskofferingid";
    private static final String PARAM_SECURITY_GROUP_IDS = "securitygroupids";
    private static final String PARAM_SSH_KEYPAIRS = "keypairs";
    private static final String PARAM_AFFINITY_GROUP_IDS = "affinitygroupids";
    private static final String PARAM_NETWORK_IDS = "networkids";

    private static final List<String> supportedDeployParams = Arrays.asList(PARAM_ROOT_DISK_SIZE, PARAM_DISK_OFFERING_ID, PARAM_DISK_SIZE, PARAM_SECURITY_GROUP_IDS,
            PARAM_OVERRIDE_DISK_OFFERING_ID, PARAM_SSH_KEYPAIRS, PARAM_AFFINITY_GROUP_IDS, PARAM_NETWORK_IDS);

    protected static final String VM_HOSTNAME_PREFIX = "autoScaleVm-";
    protected static final int VM_HOSTNAME_RANDOM_SUFFIX_LENGTH = 6;

    private static final Long DEFAULT_HOST_ID = -1L;

    ExecutorService groupExecutor;

    CompletionService<Pair<Long, Boolean>> completionService;

    Map<Long, ScheduledExecutorService> vmGroupMonitorMaps = new HashMap<>();

    @Override
    public boolean start() {
        // create thread pool and blocking queue
        final int workersCount = AutoScaleStatsWorker.value();
        groupExecutor = Executors.newFixedThreadPool(workersCount);
        logger.info("AutoScale Manager created a thread pool to check autoscale vm groups. The pool size is : " + workersCount);

        final BlockingQueue<Future<Pair<Long, Boolean>>> queue = new LinkedBlockingQueue<>(workersCount);
        logger.info("AutoScale Manager created a blocking queue to check autoscale vm groups. The queue size is : " + workersCount);

        completionService = new ExecutorCompletionService<>(groupExecutor, queue);

        scheduleMonitorTasks();

        return true;
    }

    @Override
    public boolean stop() {
        if (groupExecutor != null) {
            groupExecutor.shutdown();
        }
        return true;
    }

    public List<AutoScaleCounter> getSupportedAutoScaleCounters(long networkid) {
        String capability = lbRulesMgr.getLBCapability(networkid, Capability.AutoScaleCounters.getName());
        if (capability == null) {
            return null;
        }
        Gson gson = new Gson();
        java.lang.reflect.Type listType = new TypeToken<List<AutoScaleCounter>>() {
        }.getType();
        List<AutoScaleCounter> result = gson.fromJson(capability, listType);
        return result;
    }

    public void validateNetworkCapability(long networkId) {
        Network network = networkDao.findById(networkId);
        if (network == null) {
            throw new CloudRuntimeException(String.format("Unable to find network with id: %s ", networkId));
        }
        NetworkOffering offering = networkOfferingDao.findByIdIncludingRemoved(network.getNetworkOfferingId());
        if (!offering.isSupportsVmAutoScaling()) {
            throw new InvalidParameterValueException("Vm AutoScaling is not supported by this network.");
        }
    }

    public void validateAutoScaleCounters(long networkid, List<Counter> counters, List<Pair<String, String>> counterParamPassed) {
        List<AutoScaleCounter> supportedCounters = getSupportedAutoScaleCounters(networkid);
        if (supportedCounters == null) {
            throw new InvalidParameterValueException("AutoScale is not supported in the network");
        }
        for (Counter counter : counters) {
            String counterName = counter.getSource().name();
            boolean isCounterSupported = false;
            for (AutoScaleCounter autoScaleCounter : supportedCounters) {
                if (autoScaleCounter.getName().equals(counterName)) {
                    isCounterSupported = true;
                    List<AutoScaleCounterParam> counterParams = autoScaleCounter.getParamList();
                    for (AutoScaleCounterParam autoScaleCounterParam : counterParams) {
                        boolean isRequiredParameter = autoScaleCounterParam.getRequired();
                        if (isRequiredParameter) {
                            boolean isRequiredParamPresent = false;
                            for (Pair<String, String> pair : counterParamPassed) {
                                if (pair.first().equals(autoScaleCounterParam.getParamName()))
                                    isRequiredParamPresent = true;

                            }
                            if (!isRequiredParamPresent) {
                                throw new InvalidParameterValueException("Parameter " + autoScaleCounterParam.getParamName() + " has to be set in AutoScaleVmProfile's " +
                                    ApiConstants.COUNTERPARAM_LIST);
                            }
                        }
                    }
                    break;
                }
            }
            if (!isCounterSupported) {
                throw new InvalidParameterValueException("AutoScale counter with source='" + counter.getSource().name() + "' is not supported " + "in the network");
            }
        }
    }

    private <VO extends ControlledEntity> VO getEntityInDatabase(Account caller, String paramName, Long id, GenericDao<VO, Long> dao) {

        VO vo = dao.findById(id);

        if (vo == null) {
            throw new InvalidParameterValueException("Unable to find " + paramName);
        }

        accountMgr.checkAccess(caller, null, false, (ControlledEntity)vo);

        return vo;
    }

    private boolean isAutoScaleScaleUpPolicy(AutoScalePolicy policyVO) {
        return policyVO.getAction().equals(AutoScalePolicy.Action.SCALEUP);
    }

    private List<AutoScalePolicyVO> getAutoScalePolicies(List<Long> policyIds, List<Counter> counters, int interval, boolean scaleUpPolicies) {
        SearchBuilder<AutoScalePolicyVO> policySearch = autoScalePolicyDao.createSearchBuilder();
        policySearch.and("ids", policySearch.entity().getId(), Op.IN);
        policySearch.done();
        SearchCriteria<AutoScalePolicyVO> sc = policySearch.create();

        sc.setParameters("ids", policyIds.toArray(new Object[0]));
        List<AutoScalePolicyVO> policies = autoScalePolicyDao.search(sc, null);

        for (AutoScalePolicyVO policy : policies) {
            int duration = policy.getDuration();
            if (duration < interval) {
                throw new InvalidParameterValueException("duration : " + duration + " specified in a policy cannot be less than vm group's interval : " + interval);
            }

            if (scaleUpPolicies) {
                if (!isAutoScaleScaleUpPolicy(policy)) {
                    throw new InvalidParameterValueException("Only scaleup policies can be specified in scaleuppolicyids");
                }
            } else {
                if (isAutoScaleScaleUpPolicy(policy)) {
                    throw new InvalidParameterValueException("Only scaledown policies can be specified in scaledownpolicyids");
                }
            }
            List<AutoScalePolicyConditionMapVO> policyConditionMapVOs = autoScalePolicyConditionMapDao.listByAll(policy.getId(), null);
            for (AutoScalePolicyConditionMapVO policyConditionMapVO : policyConditionMapVOs) {
                long conditionid = policyConditionMapVO.getConditionId();
                Condition condition = conditionDao.findById(conditionid);
                Counter counter = counterDao.findById(condition.getCounterId());
                counters.add(counter);
            }
        }
        return policies;
    }

    @DB
    protected AutoScaleVmProfileVO checkValidityAndPersist(AutoScaleVmProfileVO vmProfile, boolean isNew) {
        long templateId = vmProfile.getTemplateId();
        Long autoscaleUserId = vmProfile.getAutoScaleUserId();
        int expungeVmGracePeriod = vmProfile.getExpungeVmGracePeriod();

        Long serviceOfferingId = vmProfile.getServiceOfferingId();
        if (serviceOfferingId != null) {
            ServiceOffering serviceOffering = entityMgr.findByIdIncludingRemoved(ServiceOffering.class, serviceOfferingId);
            if (serviceOffering == null) {
                throw new InvalidParameterValueException("Unable to find service offering by id");
            }
            if (serviceOffering.isDynamic()) {
                throw new InvalidParameterValueException("Unable to use dynamic service offering in AutoScale vm profile");
            }
        }

        VirtualMachineTemplate template = entityMgr.findById(VirtualMachineTemplate.class, templateId);
        // Make sure a valid template ID was specified
        if (template == null) {
            throw new InvalidParameterValueException("Unable to use the given template.");
        }

        if (expungeVmGracePeriod < 0) {
            throw new InvalidParameterValueException("Expunge Vm Grace Period cannot be less than 0.");
        }

        if (!isNew) {
            List<AutoScaleVmGroupVO> vmGroups = autoScaleVmGroupDao.listByProfile(vmProfile.getId());
            for (AutoScaleVmGroupVO vmGroup : vmGroups) {
                Network.Provider provider = getLoadBalancerServiceProvider(vmGroup.getLoadBalancerId());
                if (Network.Provider.Netscaler.equals(provider)) {
                    checkAutoScaleUser(autoscaleUserId, vmProfile.getAccountId());
                }
            }
        }

        Integer autoScaleStatsInterval = AutoScaleStatsInterval.value();
        if (autoScaleStatsInterval <= 0) {
            throw new InvalidParameterValueException(String.format("Global setting %s has to be set to larger than 0", AutoScaleStatsInterval.key()));
        }

        List<Pair<String, String>> otherDeployParams = vmProfile.getOtherDeployParamsList();
        if (CollectionUtils.isNotEmpty(otherDeployParams)) {
            for (Pair<String, String> kvPair : otherDeployParams) {
                String key = kvPair.first();
                if (!supportedDeployParams.contains(key)) {
                    throw new InvalidParameterValueException(String.format("Unsupported otherdeployparams key: %s. Supported values are %s", key, supportedDeployParams));
                }
            }
        }
        vmProfile = autoScaleVmProfileDao.persist(vmProfile);

        return vmProfile;
    }

    @Override
    public void checkAutoScaleUser(Long autoscaleUserId, long accountId) {
        if (autoscaleUserId == null) {
            throw new InvalidParameterValueException("autoscaleuserid is required but not passed");
        }
        User user = userDao.findById(autoscaleUserId);
        if (user == null) {
            throw new InvalidParameterValueException("Unable to find autoscale user id: " + autoscaleUserId);
        }

        if (user.getAccountId() != accountId) {
            throw new InvalidParameterValueException("AutoScale User id does not belong to the same account");
        }

        String apiKey = user.getApiKey();
        String secretKey = user.getSecretKey();
        String csUrl = ApiServiceConfiguration.ApiServletPath.value();

        if (apiKey == null) {
            throw new InvalidParameterValueException("apiKey for user: " + user.getUsername() + " is empty. Please generate it");
        }

        if (secretKey == null) {
            throw new InvalidParameterValueException("secretKey for user: " + user.getUsername() + " is empty. Please generate it");
        }

        if (csUrl == null || csUrl.contains("localhost")) {
            throw new InvalidParameterValueException(String.format("Global setting %s has to be set to the Management Server's API end point", ApiServiceConfiguration.ApiServletPath.key()));
        }
    }
    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMPROFILE_CREATE, eventDescription = "creating autoscale vm profile", create = true)
    public AutoScaleVmProfile createAutoScaleVmProfile(CreateAutoScaleVmProfileCmd cmd) {

        Account caller = CallContext.current().getCallingAccount();
        Account owner = accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId());
        accountMgr.checkAccess(caller, null, true, owner);

        long zoneId = cmd.getZoneId();
        long serviceOfferingId = cmd.getServiceOfferingId();
        Long autoscaleUserId = cmd.getAutoscaleUserId();

        DataCenter zone = entityMgr.findById(DataCenter.class, zoneId);

        if (zone == null) {
            throw new InvalidParameterValueException("Unable to find zone by id");
        }

        ServiceOffering serviceOffering = entityMgr.findById(ServiceOffering.class, serviceOfferingId);
        if (serviceOffering == null) {
            throw new InvalidParameterValueException("Unable to find service offering by id");
        }

        VirtualMachineTemplate template = entityMgr.findById(VirtualMachineTemplate.class, cmd.getTemplateId());
        if (template == null) {
            throw new InvalidParameterValueException("Unable to find template by id " + cmd.getTemplateId());
        }

        // validations
        HashMap<String, String> deployParams = cmd.getDeployParamMap();
        /*
         * Just for making sure the values are right in other deploy params.
         * For ex. if projectId is given as a string instead of an long value, this
         * will be throwing an error.
         */
        dispatchChainFactory.getStandardDispatchChain().dispatch(new DispatchTask(ComponentContext.inject(DeployVMCmd.class), deployParams));

        AutoScaleVmProfileVO profileVO =
            new AutoScaleVmProfileVO(cmd.getZoneId(), owner.getDomainId(), owner.getAccountId(), cmd.getServiceOfferingId(), cmd.getTemplateId(), cmd.getOtherDeployParams(),
                cmd.getCounterParamList(), cmd.getUserData(), cmd.getExpungeVmGracePeriod(), autoscaleUserId);

        if (cmd.getDisplay() != null) {
            profileVO.setDisplay(cmd.getDisplay());
        }

        String userData = cmd.getUserData();
        Long userDataId = cmd.getUserDataId();
        String userDataDetails = null;
        if (MapUtils.isNotEmpty(cmd.getUserDataDetails())) {
            userDataDetails = cmd.getUserDataDetails().toString();
        }
        userData = userVmMgr.finalizeUserData(userData, userDataId, template);
        userData = userDataMgr.validateUserData(userData, cmd.getHttpMethod());
        if (userData != null) {
            profileVO.setUserData(userData);
        }
        if (userDataId != null) {
            profileVO.setUserDataId(userDataId);
        }
        if (userDataDetails != null) {
            profileVO.setUserDataDetails(userDataDetails);
        }

        profileVO = checkValidityAndPersist(profileVO, true);
        logger.info("Successfully create AutoScale Vm Profile: {}", profileVO);

        return profileVO;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMPROFILE_UPDATE, eventDescription = "updating autoscale vm profile")
    public AutoScaleVmProfile updateAutoScaleVmProfile(UpdateAutoScaleVmProfileCmd cmd) {
        Long profileId = cmd.getId();
        Long serviceOfferingId = cmd.getServiceOfferingId();
        Long templateId = cmd.getTemplateId();
        Long autoscaleUserId = cmd.getAutoscaleUserId();
        Map<String, HashMap<String, String>> otherDeployParams = cmd.getOtherDeployParams();
        Map counterParamList = cmd.getCounterParamList();
        String userData = cmd.getUserData();
        Long userDataId = cmd.getUserDataId();
        String userDataDetails = null;
        if (MapUtils.isNotEmpty(cmd.getUserDataDetails())) {
            userDataDetails = cmd.getUserDataDetails().toString();
        }
        boolean userdataUpdate = userData != null || userDataId != null || MapUtils.isNotEmpty(cmd.getUserDataDetails());

        Integer expungeVmGracePeriod = cmd.getExpungeVmGracePeriod();

        AutoScaleVmProfileVO vmProfile = getEntityInDatabase(CallContext.current().getCallingAccount(), "Auto Scale Vm Profile", profileId, autoScaleVmProfileDao);

        boolean physicalParameterUpdate = (templateId != null || autoscaleUserId != null || counterParamList != null
                || otherDeployParams != null || expungeVmGracePeriod != null || userdataUpdate);

        if (serviceOfferingId != null) {
            vmProfile.setServiceOfferingId(serviceOfferingId);
        }

        if (templateId != null) {
            vmProfile.setTemplateId(templateId);
        }

        if (autoscaleUserId != null) {
            vmProfile.setAutoscaleUserId(autoscaleUserId);
        }

        if (otherDeployParams != null) {
            vmProfile.setOtherDeployParamsForUpdate(otherDeployParams);
        }

        if (counterParamList != null) {
            vmProfile.setCounterParamsForUpdate(counterParamList);
        }

        if (expungeVmGracePeriod != null) {
            vmProfile.setExpungeVmGracePeriod(expungeVmGracePeriod);
        }

        if (cmd.getCustomId() != null) {
            vmProfile.setUuid(cmd.getCustomId());
        }

        if (cmd.getDisplay() != null) {
            vmProfile.setDisplay(cmd.getDisplay());
        }

        if (userdataUpdate) {
            if (templateId == null) {
                templateId = vmProfile.getTemplateId();
            }
            VirtualMachineTemplate template = entityMgr.findByIdIncludingRemoved(VirtualMachineTemplate.class, templateId);
            userData = userVmMgr.finalizeUserData(userData, userDataId, template);
            userData = userDataMgr.validateUserData(userData, cmd.getHttpMethod());
            vmProfile.setUserDataId(userDataId);
            vmProfile.setUserData(userData);
            vmProfile.setUserDataDetails(userDataDetails);
        }

        List<AutoScaleVmGroupVO> vmGroupList = autoScaleVmGroupDao.listByAll(null, profileId);
        for (AutoScaleVmGroupVO vmGroupVO : vmGroupList) {
            if (physicalParameterUpdate && !vmGroupVO.getState().equals(AutoScaleVmGroup.State.DISABLED)) {
                throw new InvalidParameterValueException("The AutoScale Vm Profile can be updated only if the Vm Group it is associated with is disabled in state");
            }
        }

        vmProfile = checkValidityAndPersist(vmProfile, false);
        logger.info("Updated Auto Scale Vm Profile: {}", vmProfile);

        return vmProfile;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMPROFILE_DELETE, eventDescription = "deleting autoscale vm profile")
    public boolean deleteAutoScaleVmProfile(long id) {
        /* Check if entity is in database */
        AutoScaleVmProfileVO vmProfile = getEntityInDatabase(CallContext.current().getCallingAccount(), "AutoScale Vm Profile", id, autoScaleVmProfileDao);
        if (autoScaleVmGroupDao.isProfileInUse(id)) {
            throw new InvalidParameterValueException("Cannot delete AutoScale Vm Profile when it is in use by one more vm groups");
        }

        boolean success = autoScaleVmProfileDao.remove(id);
        if (success) {
            logger.info("Successfully deleted AutoScale Vm Profile: {}", vmProfile);
        }
        return success;
    }

    @Override
    public List<? extends AutoScaleVmProfile> listAutoScaleVmProfiles(ListAutoScaleVmProfilesCmd cmd) {
        Long id = cmd.getId();
        Long templateId = cmd.getTemplateId();
        String otherDeployParams = cmd.getOtherDeployParams();
        Long serviceOffId = cmd.getServiceOfferingId();
        Long zoneId = cmd.getZoneId();
        Boolean display = cmd.getDisplay();

        SearchWrapper<AutoScaleVmProfileVO> searchWrapper = new SearchWrapper<>(autoScaleVmProfileDao, AutoScaleVmProfileVO.class, cmd, cmd.getId());
        SearchBuilder<AutoScaleVmProfileVO> sb = searchWrapper.getSearchBuilder();

        sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
        sb.and("templateId", sb.entity().getTemplateId(), SearchCriteria.Op.EQ);
        sb.and("serviceOfferingId", sb.entity().getServiceOfferingId(), SearchCriteria.Op.EQ);
        sb.and("otherDeployParams", sb.entity().getOtherDeployParams(), SearchCriteria.Op.LIKE);
        sb.and("zoneId", sb.entity().getZoneId(), SearchCriteria.Op.EQ);
        sb.and("display", sb.entity().isDisplay(), SearchCriteria.Op.EQ);
        SearchCriteria<AutoScaleVmProfileVO> sc = searchWrapper.buildSearchCriteria();

        if (id != null) {
            sc.setParameters("id", id);
        }
        if (templateId != null) {
            sc.setParameters("templateId", templateId);
        }
        if (otherDeployParams != null) {
            sc.addAnd("otherDeployParams", SearchCriteria.Op.LIKE, "%" + otherDeployParams + "%");
        }

        if (serviceOffId != null) {
            sc.setParameters("serviceOfferingId", serviceOffId);
        }

        if (zoneId != null) {
            sc.setParameters("zoneId", zoneId);
        }

        if (display != null) {
            sc.setParameters("display", display);
        }

        return searchWrapper.search();
    }

    @DB
    protected AutoScalePolicyVO checkValidityAndPersist(final AutoScalePolicyVO autoScalePolicyVOFinal, final List<Long> conditionIds) {
        final int duration = autoScalePolicyVOFinal.getDuration();
        final int quietTime = autoScalePolicyVOFinal.getQuietTime();

        if (duration < 0) {
            throw new InvalidParameterValueException("duration is an invalid value: " + duration);
        }

        if (quietTime < 0) {
            throw new InvalidParameterValueException("quietTime is an invalid value: " + quietTime);
        }

        return Transaction.execute(new TransactionCallback<AutoScalePolicyVO>() {
            @Override
            public AutoScalePolicyVO doInTransaction(TransactionStatus status) {
                AutoScalePolicyVO autoScalePolicyVO = autoScalePolicyDao.persist(autoScalePolicyVOFinal);

                if (conditionIds != null) {
                    if (CollectionUtils.isEmpty(conditionIds)) {
                        throw new InvalidParameterValueException("AutoScale policy must have at least one condition");
                    }
                    SearchBuilder<ConditionVO> conditionsSearch = conditionDao.createSearchBuilder();
                    conditionsSearch.and("ids", conditionsSearch.entity().getId(), Op.IN);
                    conditionsSearch.done();
                    SearchCriteria<ConditionVO> sc = conditionsSearch.create();

                    sc.setParameters("ids", conditionIds.toArray(new Object[0]));
                    List<ConditionVO> conditions = conditionDao.search(sc, null);

                    ControlledEntity[] sameOwnerEntities = conditions.toArray(new ControlledEntity[conditions.size() + 1]);
                    sameOwnerEntities[sameOwnerEntities.length - 1] = autoScalePolicyVO;
                    accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, sameOwnerEntities);

                    if (conditionIds.size() != conditions.size()) {
                        // TODO report the condition id which could not be found
                        throw new InvalidParameterValueException("Unable to find the condition specified");
                    }

                    ArrayList<Long> counterIds = new ArrayList<Long>();
                    for (ConditionVO condition : conditions) {
                        if (counterIds.contains(condition.getCounterId())) {
                            throw new InvalidParameterValueException(
                                "at least two conditions in the conditionids have the same counter. It is not right to apply two different conditions for the same counter");
                        }
                        counterIds.add(condition.getCounterId());
                    }

                    /* For update case remove the existing mappings and create fresh ones */
                    autoScalePolicyConditionMapDao.removeByAutoScalePolicyId(autoScalePolicyVO.getId());

                    for (Long conditionId : conditionIds) {
                        AutoScalePolicyConditionMapVO policyConditionMapVO = new AutoScalePolicyConditionMapVO(autoScalePolicyVO.getId(), conditionId);
                        autoScalePolicyConditionMapDao.persist(policyConditionMapVO);
                    }
                }

                return autoScalePolicyVO;
            }
        });
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEPOLICY_CREATE, eventDescription = "creating autoscale policy", create = true)
    public AutoScalePolicy createAutoScalePolicy(CreateAutoScalePolicyCmd cmd) {

        int duration = cmd.getDuration();
        Integer quietTime = cmd.getQuietTime();
        String action = cmd.getAction();

        if (quietTime == null) {
            quietTime = NetUtils.DEFAULT_AUTOSCALE_POLICY_QUIET_TIME;
        }

        AutoScalePolicy.Action scaleAction = null;
        try {
            scaleAction = AutoScalePolicy.Action.fromValue(action);
        } catch (IllegalArgumentException ex) {
            throw new InvalidParameterValueException("action is invalid. Supported actions are: " + Arrays.toString(AutoScalePolicy.Action.values()));
        }

        AutoScalePolicyVO policyVO = new AutoScalePolicyVO(cmd.getName(), cmd.getDomainId(), cmd.getAccountId(), duration, quietTime, null, scaleAction);

        policyVO = checkValidityAndPersist(policyVO, cmd.getConditionIds());
        logger.info("Successfully created AutoScale Policy: {}", policyVO);
        return policyVO;
    }

    @Override
    @DB
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEPOLICY_DELETE, eventDescription = "deleting autoscale policy")
    public boolean deleteAutoScalePolicy(final long id) {
        /* Check if entity is in database */
        AutoScalePolicyVO policy = getEntityInDatabase(CallContext.current().getCallingAccount(), "AutoScale Policy", id, autoScalePolicyDao);

        if (autoScaleVmGroupPolicyMapDao.isAutoScalePolicyInUse(id)) {
            throw new InvalidParameterValueException("Cannot delete AutoScale Policy when it is in use by one or more AutoScale Vm Groups");
        }

        return Transaction.execute(new TransactionCallback<Boolean>() {
            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                boolean success = true;
                success = autoScalePolicyDao.remove(id);
                if (!success) {
                    logger.warn("Failed to remove AutoScale Policy db object");
                    return false;
                }
                success = autoScalePolicyConditionMapDao.removeByAutoScalePolicyId(id);
                if (!success) {
                    logger.warn("Failed to remove AutoScale Policy Condition mappings");
                    return false;
                }
                logger.info("Successfully deleted autoscale policy: {}", policy);

                return success;
            }
        });
    }

    private class SearchWrapper<VO extends ControlledEntity> {
        GenericDao<VO, Long> dao;
        SearchBuilder<VO> searchBuilder;
        SearchCriteria<VO> searchCriteria;
        Long domainId;
        boolean isRecursive;
        List<Long> permittedAccounts = new ArrayList<Long>();
        ListProjectResourcesCriteria listProjectResourcesCriteria;
        Filter searchFilter;

        public SearchWrapper(GenericDao<VO, Long> dao, Class<VO> entityClass, BaseListProjectAndAccountResourcesCmd cmd, Long id)
        {
            this.dao = dao;
            this.searchBuilder = dao.createSearchBuilder();
            domainId = cmd.getDomainId();
            String accountName = cmd.getAccountName();
            isRecursive = cmd.isRecursive();
            boolean listAll = cmd.listAll();
            long startIndex = cmd.getStartIndex();
            long pageSizeVal = cmd.getPageSizeVal();
            Account caller = CallContext.current().getCallingAccount();

            Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<Long, Boolean,
                    ListProjectResourcesCriteria>(domainId, isRecursive, null);
            accountMgr.buildACLSearchParameters(caller, id, accountName, cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject,
                    listAll, false);
            domainId = domainIdRecursiveListProject.first();
            isRecursive = domainIdRecursiveListProject.second();
            listProjectResourcesCriteria = domainIdRecursiveListProject.third();

            accountMgr.buildACLSearchBuilder(searchBuilder, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
            searchFilter = new Filter(entityClass, "id", false, startIndex, pageSizeVal);
        }

        public SearchBuilder<VO> getSearchBuilder() {
            return searchBuilder;
        }

        public SearchCriteria<VO> buildSearchCriteria() {
            searchCriteria = searchBuilder.create();
            accountMgr.buildACLSearchCriteria(searchCriteria, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
            return searchCriteria;
        }

        public List<VO> search() {
            return dao.search(searchCriteria, searchFilter);
        }
    }

    @Override
    public List<? extends AutoScalePolicy> listAutoScalePolicies(ListAutoScalePoliciesCmd cmd) {
        SearchWrapper<AutoScalePolicyVO> searchWrapper = new SearchWrapper<>(autoScalePolicyDao, AutoScalePolicyVO.class, cmd, cmd.getId());
        SearchBuilder<AutoScalePolicyVO> sb = searchWrapper.getSearchBuilder();
        Long id = cmd.getId();
        String name = cmd.getName();
        Long conditionId = cmd.getConditionId();
        String action = cmd.getAction();
        Long vmGroupId = cmd.getVmGroupId();

        AutoScalePolicy.Action scaleAction = null;
        if (action != null) {
            try {
                scaleAction = AutoScalePolicy.Action.fromValue(action);
            } catch (IllegalArgumentException ex) {
                throw new InvalidParameterValueException("action is invalid. Supported actions are: " + Arrays.toString(AutoScalePolicy.Action.values()));
            }
        }

        sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
        sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
        sb.and("action", sb.entity().getAction(), SearchCriteria.Op.EQ);

        if (conditionId != null) {
            SearchBuilder<AutoScalePolicyConditionMapVO> asPolicyConditionSearch = autoScalePolicyConditionMapDao.createSearchBuilder();
            asPolicyConditionSearch.and("conditionId", asPolicyConditionSearch.entity().getConditionId(), SearchCriteria.Op.EQ);
            sb.join("asPolicyConditionSearch", asPolicyConditionSearch, sb.entity().getId(), asPolicyConditionSearch.entity().getPolicyId(), JoinBuilder.JoinType.INNER);
        }

        if (vmGroupId != null) {
            SearchBuilder<AutoScaleVmGroupPolicyMapVO> asVmGroupPolicySearch = autoScaleVmGroupPolicyMapDao.createSearchBuilder();
            asVmGroupPolicySearch.and("vmGroupId", asVmGroupPolicySearch.entity().getVmGroupId(), SearchCriteria.Op.EQ);
            sb.join("asVmGroupPolicySearch", asVmGroupPolicySearch, sb.entity().getId(), asVmGroupPolicySearch.entity().getPolicyId(), JoinBuilder.JoinType.INNER);
        }

        SearchCriteria<AutoScalePolicyVO> sc = searchWrapper.buildSearchCriteria();

        if (id != null) {
            sc.setParameters("id", id);
        }

        if (name != null) {
            sc.setParameters("name", name);
        }

        if (scaleAction != null) {
            sc.setParameters("action", scaleAction);
        }

        if (conditionId != null) {
            sc.setJoinParameters("asPolicyConditionSearch", "conditionId", conditionId);
        }

        if (vmGroupId != null) {
            sc.setJoinParameters("asVmGroupPolicySearch", "vmGroupId", vmGroupId);
        }

        return searchWrapper.search();
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEPOLICY_UPDATE, eventDescription = "updating autoscale policy")
    public AutoScalePolicy updateAutoScalePolicy(UpdateAutoScalePolicyCmd cmd) {
        Long policyId = cmd.getId();
        String name = cmd.getName();
        Integer duration = cmd.getDuration();
        Integer quietTime = cmd.getQuietTime();
        List<Long> conditionIds = cmd.getConditionIds();
        AutoScalePolicyVO policy = getEntityInDatabase(CallContext.current().getCallingAccount(), "Auto Scale Policy", policyId, autoScalePolicyDao);

        if (name != null) {
            policy.setName(name);
        }

        if (duration != null) {
            policy.setDuration(duration);
        }

        if (quietTime != null) {
            policy.setQuietTime(quietTime);
        }

        List<AutoScaleVmGroupPolicyMapVO> vmGroupPolicyList = autoScaleVmGroupPolicyMapDao.listByPolicyId(policyId);
        for (AutoScaleVmGroupPolicyMapVO vmGroupPolicy : vmGroupPolicyList) {
            AutoScaleVmGroupVO vmGroupVO = autoScaleVmGroupDao.findById(vmGroupPolicy.getVmGroupId());
            if (vmGroupVO == null) {
                logger.warn("Stale database entry! There is an entry in VmGroupPolicyMap but the vmGroup is missing:" + vmGroupPolicy.getVmGroupId());

                continue;

            }
            if (!vmGroupVO.getState().equals(AutoScaleVmGroup.State.DISABLED)) {
                throw new InvalidParameterValueException("The AutoScale Policy can be updated only if the Vm Group it is associated with is disabled in state");
            }
            if (policy.getDuration() < vmGroupVO.getInterval()) {
                throw new InvalidParameterValueException("duration is less than the associated AutoScaleVmGroup's interval");
            }
        }

        policy = checkValidityAndPersist(policy, conditionIds);
        logger.info("Successfully updated Auto Scale Policy: {}", policy);

        if (CollectionUtils.isNotEmpty(conditionIds)) {
            markStatisticsAsInactive(null, policyId);
        }

        return policy;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMGROUP_CREATE, eventDescription = "creating autoscale vm group", create = true)
    public AutoScaleVmGroup createAutoScaleVmGroup(CreateAutoScaleVmGroupCmd cmd) {
        String name = cmd.getName();
        int minMembers = cmd.getMinMembers();
        int maxMembers = cmd.getMaxMembers();
        Integer interval = cmd.getInterval();
        Boolean forDisplay = cmd.getDisplay();

        if (interval == null) {
            interval = NetUtils.DEFAULT_AUTOSCALE_POLICY_INTERVAL_TIME;
        }

        LoadBalancerVO loadBalancer = getEntityInDatabase(CallContext.current().getCallingAccount(), ApiConstants.LBID, cmd.getLbRuleId(), lbDao);

        Long zoneId = ipAddressDao.findById(loadBalancer.getSourceIpAddressId()).getDataCenterId();

        if (autoScaleVmGroupDao.isAutoScaleLoadBalancer(loadBalancer.getId())) {
            throw new InvalidParameterValueException("an AutoScaleVmGroup is already attached to the lb rule, the existing vm group has to be first deleted");
        }

        if (lbVmMapDao.isVmAttachedToLoadBalancer(loadBalancer.getId())) {
            throw new InvalidParameterValueException(
                "there are Vms already bound to the specified LoadBalancing Rule. User bound Vms and AutoScaled Vm Group cannot co-exist on a Load Balancing Rule");
        }

        AutoScaleVmGroupVO vmGroupVO = new AutoScaleVmGroupVO(cmd.getLbRuleId(), zoneId, loadBalancer.getDomainId(), loadBalancer.getAccountId(), name, minMembers, maxMembers,
            loadBalancer.getDefaultPortStart(), interval, null, cmd.getProfileId(), AutoScaleVmGroup.State.NEW);

        if (forDisplay != null) {
            vmGroupVO.setDisplay(forDisplay);
        }

        vmGroupVO = checkValidityAndPersist(vmGroupVO, cmd.getScaleUpPolicyIds(), cmd.getScaleDownPolicyIds());
        logger.info("Successfully created Autoscale Vm Group: {}", vmGroupVO);

        createInactiveDummyRecord(vmGroupVO.getId());
        scheduleMonitorTask(vmGroupVO.getId());

        return vmGroupVO;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMGROUP_CREATE, eventDescription = "creating autoscale vm group", async = true)
    public boolean configureAutoScaleVmGroup(CreateAutoScaleVmGroupCmd cmd) throws ResourceUnavailableException {
        return configureAutoScaleVmGroup(cmd.getEntityId(), AutoScaleVmGroup.State.NEW);
    }

    public boolean isLoadBalancerBasedAutoScaleVmGroup(AutoScaleVmGroup vmGroup) {
        return vmGroup.getLoadBalancerId() != null;
    }

    protected boolean configureAutoScaleVmGroup(long vmGroupid, AutoScaleVmGroup.State currentState) throws ResourceUnavailableException {
        AutoScaleVmGroup vmGroup = autoScaleVmGroupDao.findById(vmGroupid);

        if (isLoadBalancerBasedAutoScaleVmGroup(vmGroup)) {
            try {
                return lbRulesMgr.configureLbAutoScaleVmGroup(vmGroupid, currentState);
            } catch (ResourceUnavailableException re) {
                throw re;
            } catch (Exception e) {
                logger.warn("Exception during configureLbAutoScaleVmGroup in lb rules manager", e);
                return false;
            }
        }

        // This should never happen, because today loadbalancerruleid is manadatory for AutoScaleVmGroup.
        throw new InvalidParameterValueException("Only LoadBalancer based AutoScale is supported");
    }

    @Override
    @DB
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMGROUP_DELETE, eventDescription = "deleting autoscale vm group", async = true)
    public boolean deleteAutoScaleVmGroup(final long id, final Boolean cleanup) {
        AutoScaleVmGroupVO autoScaleVmGroupVO = getEntityInDatabase(CallContext.current().getCallingAccount(), "AutoScale Vm Group", id, autoScaleVmGroupDao);

        if (autoScaleVmGroupVO.getState().equals(AutoScaleVmGroup.State.NEW)) {
            /* This condition is for handling failures during creation command */
            return autoScaleVmGroupDao.remove(id);
        }

        if (!autoScaleVmGroupVO.getState().equals(AutoScaleVmGroup.State.DISABLED) && !Boolean.TRUE.equals(cleanup)) {
            throw new InvalidParameterValueException(String.format("Cannot delete autoscale vm group : %s because it is in %s state. Please disable it or pass cleanup=true flag which will destroy all VMs.", autoScaleVmGroupVO, autoScaleVmGroupVO.getState()));
        }

        Integer currentVM = autoScaleVmGroupVmMapDao.countByGroup(id);
        if (currentVM > 0 && !Boolean.TRUE.equals(cleanup)) {
            throw new InvalidParameterValueException(String.format("Cannot delete autoscale vm group : %s because there are %d VMs. Please remove the VMs or pass cleanup=true flag which will destroy all VMs.", autoScaleVmGroupVO, currentVM));
        }

        AutoScaleVmGroup.State bakupState = autoScaleVmGroupVO.getState();
        autoScaleVmGroupVO.setState(AutoScaleVmGroup.State.REVOKE);
        autoScaleVmGroupDao.persist(autoScaleVmGroupVO);
        boolean success = false;

        if (Boolean.TRUE.equals(cleanup)) {
            List<AutoScaleVmGroupVmMapVO> asGroupVmVOs = autoScaleVmGroupVmMapDao.listByGroup(id);
            for (AutoScaleVmGroupVmMapVO asGroupVmVO : asGroupVmVOs) {
                Long vmId = asGroupVmVO.getInstanceId();
                destroyVm(vmId);
            }
        }

        try {
            success = configureAutoScaleVmGroup(id, bakupState);
        } catch (ResourceUnavailableException e) {
            autoScaleVmGroupVO.setState(bakupState);
            autoScaleVmGroupDao.persist(autoScaleVmGroupVO);
        } finally {
            if (!success) {
                logger.warn("Could not delete AutoScale Vm Group : {}", autoScaleVmGroupVO);
                return false;
            }
        }

        // Remove comments (if any)
        annotationDao.removeByEntityType(AnnotationService.EntityType.AUTOSCALE_VM_GROUP.name(), autoScaleVmGroupVO.getUuid());

        return Transaction.execute(new TransactionCallback<Boolean>() {
            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                boolean success = autoScaleVmGroupDao.remove(id);

                if (!success) {
                    logger.warn("Failed to remove AutoScale Group db object");
                    return false;
                }

                cancelMonitorTask(id);

                success = autoScaleVmGroupPolicyMapDao.removeByGroupId(id);
                if (!success) {
                    logger.warn("Failed to remove AutoScale Group Policy mappings");
                    return false;
                }

                success = autoScaleVmGroupVmMapDao.removeByGroup(id);
                if (!success) {
                    logger.warn("Failed to remove AutoScale Group VM mappings");
                    return false;
                }

                success = asGroupStatisticsDao.removeByGroupId(id);
                if (!success) {
                    logger.warn("Failed to remove AutoScale Group statistics");
                    return false;
                }

                logger.info("Successfully deleted autoscale vm group: {}", autoScaleVmGroupVO);
                return success; // Successfull
            }
        });

    }

    @Override
    public List<? extends AutoScaleVmGroup> listAutoScaleVmGroups(ListAutoScaleVmGroupsCmd cmd) {
        Long id = cmd.getId();
        String name = cmd.getName();
        Long policyId = cmd.getPolicyId();
        Long loadBalancerId = cmd.getLoadBalancerId();
        Long profileId = cmd.getProfileId();
        Long zoneId = cmd.getZoneId();
        Boolean forDisplay = cmd.getDisplay();
        String keyword = cmd.getKeyword();

        SearchWrapper<AutoScaleVmGroupVO> searchWrapper = new SearchWrapper<>(autoScaleVmGroupDao, AutoScaleVmGroupVO.class, cmd, cmd.getId());
        SearchBuilder<AutoScaleVmGroupVO> sb = searchWrapper.getSearchBuilder();

        sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
        sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
        sb.and("loadBalancerId", sb.entity().getLoadBalancerId(), SearchCriteria.Op.EQ);
        sb.and("profileId", sb.entity().getProfileId(), SearchCriteria.Op.EQ);
        sb.and("zoneId", sb.entity().getZoneId(), SearchCriteria.Op.EQ);
        sb.and("display", sb.entity().isDisplay(), SearchCriteria.Op.EQ);
        sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE);

        if (policyId != null) {
            SearchBuilder<AutoScaleVmGroupPolicyMapVO> asVmGroupPolicySearch = autoScaleVmGroupPolicyMapDao.createSearchBuilder();
            asVmGroupPolicySearch.and("policyId", asVmGroupPolicySearch.entity().getPolicyId(), SearchCriteria.Op.EQ);
            sb.join("asVmGroupPolicySearch", asVmGroupPolicySearch, sb.entity().getId(), asVmGroupPolicySearch.entity().getVmGroupId(), JoinBuilder.JoinType.INNER);
        }

        SearchCriteria<AutoScaleVmGroupVO> sc = searchWrapper.buildSearchCriteria();
        if (id != null) {
            sc.setParameters("id", id);
        }
        if (name != null) {
            sc.setParameters("name", name);
        }
        if (loadBalancerId != null) {
            sc.setParameters("loadBalancerId", loadBalancerId);
        }
        if (profileId != null) {
            sc.setParameters("profileId", profileId);
        }
        if (zoneId != null) {
            sc.setParameters("zoneId", zoneId);
        }
        if (policyId != null) {
            sc.setJoinParameters("asVmGroupPolicySearch", "policyId", policyId);
        }
        if (forDisplay != null) {
            sc.setParameters("display", forDisplay);
        }
        if (StringUtils.isNotBlank(keyword)) {
            sc.setParameters("keyword", "%" + keyword + "%");
        }
        return searchWrapper.search();
    }

    @DB
    protected AutoScaleVmGroupVO checkValidityAndPersist(final AutoScaleVmGroupVO vmGroup, final List<Long> passedScaleUpPolicyIds,
        final List<Long> passedScaleDownPolicyIds) {

        checkAutoScaleVmGroupName(vmGroup.getName());

        int minMembers = vmGroup.getMinMembers();
        int maxMembers = vmGroup.getMaxMembers();
        int interval = vmGroup.getInterval();
        List<Counter> counters = new ArrayList<Counter>();
        List<AutoScalePolicyVO> policies = new ArrayList<AutoScalePolicyVO>();
        final List<Long> policyIds = new ArrayList<Long>();
        List<Long> currentScaleUpPolicyIds = new ArrayList<Long>();
        List<Long> currentScaleDownPolicyIds = new ArrayList<Long>();
        if (vmGroup.getCreated() != null) {
            ApiDBUtils.getAutoScaleVmGroupPolicyIds(vmGroup.getId(), currentScaleUpPolicyIds, currentScaleDownPolicyIds);
        }

        if (minMembers <= 0) {
            throw new InvalidParameterValueException(ApiConstants.MIN_MEMBERS + " is an invalid value: " + minMembers);
        }

        if (maxMembers <= 0) {
            throw new InvalidParameterValueException(ApiConstants.MAX_MEMBERS + " is an invalid value: " + maxMembers);
        }

        if (minMembers > maxMembers) {
            throw new InvalidParameterValueException(ApiConstants.MIN_MEMBERS + " (" + minMembers + ")cannot be greater than " + ApiConstants.MAX_MEMBERS + " (" +
                maxMembers + ")");
        }

        if (interval <= 0) {
            throw new InvalidParameterValueException("interval is an invalid value: " + interval);
        }

        if (passedScaleUpPolicyIds != null) {
            if (CollectionUtils.isEmpty(passedScaleUpPolicyIds)) {
                throw new InvalidParameterValueException("AutoScale VM group must have at least one ScaleUp policy");
            }
            policies.addAll(getAutoScalePolicies(passedScaleUpPolicyIds, counters, interval, true));
            policyIds.addAll(passedScaleUpPolicyIds);
        } else {
            // Run the interval check for existing policies
            getAutoScalePolicies(currentScaleUpPolicyIds, counters, interval, true);
            policyIds.addAll(currentScaleUpPolicyIds);
        }

        if (passedScaleDownPolicyIds != null) {
            if (CollectionUtils.isEmpty(passedScaleDownPolicyIds)) {
                throw new InvalidParameterValueException("AutoScale VM group must have at least one ScaleDown policy");
            }
            policies.addAll(getAutoScalePolicies(passedScaleDownPolicyIds, counters, interval, false));
            policyIds.addAll(passedScaleDownPolicyIds);
        } else {
            // Run the interval check for existing policies
            getAutoScalePolicies(currentScaleDownPolicyIds, counters, interval, false);
            policyIds.addAll(currentScaleDownPolicyIds);
        }
        AutoScaleVmProfileVO profileVO =
            getEntityInDatabase(CallContext.current().getCallingAccount(), ApiConstants.VMPROFILE_ID, vmGroup.getProfileId(), autoScaleVmProfileDao);

        LoadBalancerVO loadBalancer = getEntityInDatabase(CallContext.current().getCallingAccount(), ApiConstants.LBID, vmGroup.getLoadBalancerId(), lbDao);
        validateNetworkCapability(loadBalancer.getNetworkId());
        validateAutoScaleCounters(loadBalancer.getNetworkId(), counters, profileVO.getCounterParams());

        Network.Provider provider = getLoadBalancerServiceProvider(vmGroup.getLoadBalancerId());
        if (Network.Provider.Netscaler.equals(provider)) {
            checkAutoScaleUser(profileVO.getAutoScaleUserId(), vmGroup.getAccountId());
        }

        ControlledEntity[] sameOwnerEntities = policies.toArray(new ControlledEntity[policies.size() + 2]);
        sameOwnerEntities[sameOwnerEntities.length - 2] = loadBalancer;
        sameOwnerEntities[sameOwnerEntities.length - 1] = profileVO;
        accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, sameOwnerEntities);

        return Transaction.execute(new TransactionCallback<AutoScaleVmGroupVO>() {
            @Override
            public AutoScaleVmGroupVO doInTransaction(TransactionStatus status) {
                AutoScaleVmGroupVO vmGroupNew = autoScaleVmGroupDao.persist(vmGroup);

                if (passedScaleUpPolicyIds != null || passedScaleDownPolicyIds != null) {
                    autoScaleVmGroupPolicyMapDao.removeByGroupId(vmGroupNew.getId());

                    for (Long policyId : policyIds) {
                        autoScaleVmGroupPolicyMapDao.persist(new AutoScaleVmGroupPolicyMapVO(vmGroupNew.getId(), policyId));
                    }
                }

                return vmGroupNew;
            }
        });

    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMGROUP_UPDATE, eventDescription = "updating autoscale vm group", async = true)
    public AutoScaleVmGroup updateAutoScaleVmGroup(UpdateAutoScaleVmGroupCmd cmd) {
        Long vmGroupId = cmd.getId();
        String name = cmd.getName();
        Integer minMembers = cmd.getMinMembers();
        Integer maxMembers = cmd.getMaxMembers();
        Integer interval = cmd.getInterval();
        Boolean forDisplay = cmd.getDisplay();

        List<Long> scaleUpPolicyIds = cmd.getScaleUpPolicyIds();
        List<Long> scaleDownPolicyIds = cmd.getScaleDownPolicyIds();

        AutoScaleVmGroupVO vmGroupVO = getEntityInDatabase(CallContext.current().getCallingAccount(), "AutoScale Vm Group", vmGroupId, autoScaleVmGroupDao);
        int currentInterval = vmGroupVO.getInterval();

        boolean physicalParametersUpdate = (minMembers != null || maxMembers != null || (interval != null && interval != currentInterval) || CollectionUtils.isNotEmpty(scaleUpPolicyIds) || CollectionUtils.isNotEmpty(scaleDownPolicyIds));

        if (physicalParametersUpdate && !vmGroupVO.getState().equals(AutoScaleVmGroup.State.DISABLED)) {
            throw new InvalidParameterValueException("An AutoScale Vm Group can be updated with minMembers/maxMembers/Interval only when it is in disabled state");
        }

        if (StringUtils.isNotBlank(name)) {
            vmGroupVO.setName(name);
        }

        if (minMembers != null) {
            vmGroupVO.setMinMembers(minMembers);
        }

        if (maxMembers != null) {
            vmGroupVO.setMaxMembers(maxMembers);
        }

        if (interval != null) {
            vmGroupVO.setInterval(interval);
        }

        if (cmd.getCustomId() != null) {
            vmGroupVO.setUuid(cmd.getCustomId());
        }

        if (forDisplay != null) {
            vmGroupVO.setDisplay(forDisplay);
        }

        vmGroupVO = checkValidityAndPersist(vmGroupVO, scaleUpPolicyIds, scaleDownPolicyIds);
        if (vmGroupVO != null) {
            logger.debug("Updated Auto Scale VmGroup: {}", vmGroupVO);

            if ((interval != null && interval != currentInterval) || CollectionUtils.isNotEmpty(scaleUpPolicyIds) || CollectionUtils.isNotEmpty(scaleDownPolicyIds)) {
                markStatisticsAsInactive(vmGroupId, null);
            }

            return vmGroupVO;
        } else
            return null;
    }

    @Override
    @DB
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMGROUP_ENABLE, eventDescription = "enabling autoscale vm group", async = true)
    public AutoScaleVmGroup enableAutoScaleVmGroup(Long id) {
        AutoScaleVmGroupVO vmGroup = getEntityInDatabase(CallContext.current().getCallingAccount(), "AutoScale Vm Group", id, autoScaleVmGroupDao);
        boolean success = false;

        if (vmGroup.getState().equals(AutoScaleVmGroup.State.ENABLED)) {
            return vmGroup;
        }

        if (!vmGroup.getState().equals(AutoScaleVmGroup.State.DISABLED)) {
            throw new InvalidParameterValueException("Only a AutoScale Vm Group which is in Disabled state can be enabled.");
        }

        try {
            vmGroup.setState(AutoScaleVmGroup.State.ENABLED);
            vmGroup = autoScaleVmGroupDao.persist(vmGroup);
            success = configureAutoScaleVmGroup(id, AutoScaleVmGroup.State.DISABLED);
            scheduleMonitorTask(vmGroup.getId());
        } catch (ResourceUnavailableException e) {
            vmGroup.setState(AutoScaleVmGroup.State.DISABLED);
            autoScaleVmGroupDao.persist(vmGroup);
        } finally {
            if (!success) {
                logger.warn("Failed to enable AutoScale Vm Group: {}", vmGroup);
                return null;
            }
            logger.info("Successfully enabled AutoScale Vm Group: {}", vmGroup);
            createInactiveDummyRecord(vmGroup.getId());
        }
        return vmGroup;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_AUTOSCALEVMGROUP_DISABLE, eventDescription = "disabling autoscale vm group", async = true)
    @DB
    public AutoScaleVmGroup disableAutoScaleVmGroup(Long id) {
        AutoScaleVmGroupVO vmGroup = getEntityInDatabase(CallContext.current().getCallingAccount(), "AutoScale Vm Group", id, autoScaleVmGroupDao);
        boolean success = false;

        if (vmGroup.getState().equals(AutoScaleVmGroup.State.DISABLED)) {
            return vmGroup;
        }

        if (!vmGroup.getState().equals(AutoScaleVmGroup.State.ENABLED) && !vmGroup.getState().equals(AutoScaleVmGroup.State.SCALING)) {
            throw new InvalidParameterValueException("Only a AutoScale Vm Group which is in Enabled or Scaling state can be disabled.");
        }

        try {
            vmGroup.setState(AutoScaleVmGroup.State.DISABLED);
            vmGroup = autoScaleVmGroupDao.persist(vmGroup);
            success = configureAutoScaleVmGroup(id, AutoScaleVmGroup.State.ENABLED);
            createInactiveDummyRecord(vmGroup.getId());
            cancelMonitorTask(vmGroup.getId());
        } catch (ResourceUnavailableException e) {
            vmGroup.setState(AutoScaleVmGroup.State.ENABLED);
            autoScaleVmGroupDao.persist(vmGroup);
        } finally {
            if (!success) {
                logger.warn("Failed to disable AutoScale Vm Group: {}", vmGroup);
                return null;
            }
            logger.info("Successfully disabled AutoScale Vm Group: {}", vmGroup);
        }
        return vmGroup;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_COUNTER_CREATE, eventDescription = "Counter", create = true)
    @DB
    public Counter createCounter(CreateCounterCmd cmd) {
        String source = cmd.getSource().toUpperCase();
        String name = cmd.getName();
        Counter.Source src;
        // Validate Source
        try {
            src = Counter.Source.valueOf(source);
        } catch (Exception ex) {
            throw new InvalidParameterValueException("The Source " + source + " does not exist; Unable to create Counter");
        }

        // Validate Provider
        Network.Provider provider = Network.Provider.getProvider(cmd.getProvider());
        if (provider == null) {
            throw new InvalidParameterValueException("The Provider " + cmd.getProvider() + " does not exist; Unable to create Counter");
        }

        CounterVO counter = null;

        logger.debug("Adding Counter " + name);
        counter = counterDao.persist(new CounterVO(src, name, cmd.getValue(), provider));

        CallContext.current().setEventDetails(" Id: " + counter.getId() + " Name: " + name);
        return counter;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_CONDITION_CREATE, eventDescription = "Condition", create = true)
    public Condition createCondition(CreateConditionCmd cmd) {
        Account caller = CallContext.current().getCallingAccount();
        Account owner = accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId());
        accountMgr.checkAccess(caller, null, true, owner);

        String opr = cmd.getRelationalOperator().toUpperCase();
        long cid = cmd.getCounterId();
        long threshold = cmd.getThreshold();
        Condition.Operator op;
        // Validate Relational Operator
        try {
            op = Condition.Operator.valueOf(opr);
        } catch (IllegalArgumentException ex) {
            throw new InvalidParameterValueException("The Operator " + opr + " does not exist; Unable to create Condition.");
        }
        if (threshold < 0) {
            throw new InvalidParameterValueException("The threshold " + threshold + " must be equal to or greater than 0.");
        }

        CounterVO counter = counterDao.findById(cid);

        if (counter == null) {
            throw new InvalidParameterValueException("Unable to find counter");
        }
        ConditionVO condition = null;

        condition = conditionDao.persist(new ConditionVO(cid, threshold, owner.getAccountId(), owner.getDomainId(), op));
        logger.info("Successfully created condition: {}", condition);

        CallContext.current().setEventDetails(" Id: " + condition.getId());
        return condition;
    }

    @Override
    public List<? extends Counter> listCounters(ListCountersCmd cmd) {
        String name = cmd.getName();
        Long id = cmd.getId();
        String source = cmd.getSource();
        if (source != null) {
            source = source.toUpperCase();
        }
        String providerStr = cmd.getProvider();
        if (providerStr != null) {
            Network.Provider provider = Network.Provider.getProvider(providerStr);
            if (provider == null) {
                throw new InvalidParameterValueException("The Provider " + providerStr + " does not exist; Unable to list Counter");
            }
            providerStr = provider.getName();
        }

        Filter searchFilter = new Filter(CounterVO.class, "created", false, cmd.getStartIndex(), cmd.getPageSizeVal());

        return counterDao.listCounters(id, name, source, providerStr, cmd.getKeyword(), searchFilter);
    }

    @Override
    public List<? extends Condition> listConditions(ListConditionsCmd cmd) {
        Long id = cmd.getId();
        Long counterId = cmd.getCounterId();
        Long policyId = cmd.getPolicyId();
        SearchWrapper<ConditionVO> searchWrapper = new SearchWrapper<>(conditionDao, ConditionVO.class, cmd, cmd.getId());
        SearchBuilder<ConditionVO> sb = searchWrapper.getSearchBuilder();
        if (policyId != null) {
            SearchBuilder<AutoScalePolicyConditionMapVO> asPolicyConditionSearch = autoScalePolicyConditionMapDao.createSearchBuilder();
            asPolicyConditionSearch.and("policyId", asPolicyConditionSearch.entity().getPolicyId(), SearchCriteria.Op.EQ);
            sb.join("asPolicyConditionSearch", asPolicyConditionSearch, sb.entity().getId(), asPolicyConditionSearch.entity().getConditionId(),
                JoinBuilder.JoinType.INNER);
        }

        sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
        sb.and("counterId", sb.entity().getCounterId(), SearchCriteria.Op.EQ);

        // now set the SC criteria...
        SearchCriteria<ConditionVO> sc = searchWrapper.buildSearchCriteria();

        if (id != null) {
            sc.setParameters("id", id);
        }

        if (counterId != null) {
            sc.setParameters("counterId", counterId);
        }

        if (policyId != null) {
            sc.setJoinParameters("asPolicyConditionSearch", "policyId", policyId);
        }

        return searchWrapper.search();
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_COUNTER_DELETE, eventDescription = "counter")
    public boolean deleteCounter(long counterId) throws ResourceInUseException {
        // Verify Counter id
        CounterVO counter = counterDao.findById(counterId);
        if (counter == null) {
            throw new InvalidParameterValueException("Unable to find Counter");
        }

        // Verify if it is used in any Condition

        ConditionVO condition = conditionDao.findByCounterId(counterId);
        if (condition != null) {
            logger.info("Cannot delete counter {} as it is being used in a condition.", counter);
            throw new ResourceInUseException("Counter is in use.");
        }

        boolean success = counterDao.remove(counterId);
        if (success) {
            logger.info("Successfully deleted counter: {}", counter);
        }

        return success;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_CONDITION_DELETE, eventDescription = "delete a condition")
    public boolean deleteCondition(long conditionId) throws ResourceInUseException {
        /* Check if entity is in database */
        ConditionVO condition = getEntityInDatabase(CallContext.current().getCallingAccount(), "Condition", conditionId, conditionDao);
        if (condition == null) {
            throw new InvalidParameterValueException("Unable to find Condition");
        }

        // Verify if condition is used in any autoscale policy
        if (autoScalePolicyConditionMapDao.isConditionInUse(conditionId)) {
            logger.info("Cannot delete condition {} as it is being used in a condition.", condition);
            throw new ResourceInUseException("Cannot delete Condition when it is in use by one or more AutoScale Policies.");
        }
        boolean success = conditionDao.remove(conditionId);
        if (success) {
            logger.info("Successfully deleted condition {}", condition);
        }
        return success;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_CONDITION_UPDATE, eventDescription = "update a condition")
    public Condition updateCondition(UpdateConditionCmd cmd) throws ResourceInUseException {
        Long conditionId = cmd.getId();
        /* Check if entity is in database */
        ConditionVO condition = getEntityInDatabase(CallContext.current().getCallingAccount(), "Condition", conditionId, conditionDao);

        String operator = cmd.getRelationalOperator().toUpperCase();
        Long threshold = cmd.getThreshold();

        Condition.Operator op;
        // Validate Relational Operator
        try {
            op = Condition.Operator.valueOf(operator);
        } catch (IllegalArgumentException ex) {
            throw new InvalidParameterValueException("The Operator " + operator + " does not exist; Unable to update Condition.");
        }
        if (threshold < 0) {
            throw new InvalidParameterValueException("The threshold " + threshold + " must be equal to or greater than 0.");
        }

        // Verify if condition is used in any autoscale vmgroup
        GenericSearchBuilder<AutoScalePolicyConditionMapVO, Long> conditionSearch = autoScalePolicyConditionMapDao.createSearchBuilder(Long.class);
        conditionSearch.selectFields(conditionSearch.entity().getPolicyId());
        conditionSearch.and("conditionId", conditionSearch.entity().getConditionId(), Op.EQ);
        SearchCriteria<Long> sc = conditionSearch.create();
        sc.setParameters("conditionId", conditionId);
        List<Long> policyIds = autoScalePolicyConditionMapDao.customSearch(sc, null);

        if (CollectionUtils.isNotEmpty(policyIds)) {
            SearchBuilder<AutoScaleVmGroupPolicyMapVO> policySearch = autoScaleVmGroupPolicyMapDao.createSearchBuilder();
            policySearch.and("policyId", policySearch.entity().getPolicyId(), Op.IN);
            SearchBuilder<AutoScaleVmGroupVO> vmGroupSearch = autoScaleVmGroupDao.createSearchBuilder();
            vmGroupSearch.and("stateNEQ", vmGroupSearch.entity().getState(), Op.NEQ);
            vmGroupSearch.join("policySearch", policySearch, vmGroupSearch.entity().getId(), policySearch.entity().getVmGroupId(), JoinBuilder.JoinType.INNER);
            vmGroupSearch.done();

            SearchCriteria<AutoScaleVmGroupVO> sc2 = vmGroupSearch.create();
            sc2.setParameters("stateNEQ", AutoScaleVmGroup.State.DISABLED);
            sc2.setJoinParameters("policySearch", "policyId", policyIds.toArray((new Object[policyIds.size()])));
            List<AutoScaleVmGroupVO> groups = autoScaleVmGroupDao.search(sc2, null);
            if (CollectionUtils.isNotEmpty(groups)) {
                String msg = String.format("Cannot update condition %s as it is being used in %d vm groups NOT in Disabled state.", condition, groups.size());
                logger.info(msg);
                throw new ResourceInUseException(msg);
            }
        }

        condition.setRelationalOperator(op);
        condition.setThreshold(threshold);
        boolean success = conditionDao.update(conditionId, condition);
        if (success) {
            logger.info("Successfully updated condition {}", condition);

            for (Long policyId : policyIds) {
                markStatisticsAsInactive(null, policyId);
            }
        }
        return condition;
    }

    @Override
    public boolean deleteAutoScaleVmGroupsByAccount(Account account) {
        boolean success = true;
        List<AutoScaleVmGroupVO> groups = autoScaleVmGroupDao.listByAccount(account.getId());
        for (AutoScaleVmGroupVO group : groups) {
            logger.debug("Deleting AutoScale Vm Group {} for account: {}", group, account);
            try {
                deleteAutoScaleVmGroup(group.getId(), true);
                logger.debug("AutoScale Vm Group {} has been successfully deleted for account: {}", group, account);
            } catch (Exception e) {
                logger.warn("Failed to delete AutoScale Vm Group {} for account: {} due to: ", group, account, e);
                success = false;
            }
        }
        return success;
    }

    @Override
    public void cleanUpAutoScaleResources(Account account) {
        // cleans Autoscale VmProfiles, AutoScale Policies and Conditions belonging to an account
        int count = 0;
        count = autoScaleVmProfileDao.removeByAccountId(account.getId());
        if (count > 0) {
            logger.debug("Deleted {} AutoScale Vm Profile for account: {}", count, account);
        }
        count = autoScalePolicyDao.removeByAccountId(account.getId());
        if (count > 0) {
            logger.debug("Deleted {} AutoScale Policies for account: {}", count, account);
        }
        count = conditionDao.removeByAccountId(account.getId());
        if (count > 0) {
            logger.debug("Deleted {} Conditions for account: {}", count, account);
        }
    }

    private boolean checkConditionUp(AutoScaleVmGroupVO asGroup, Integer numVm) {
        // check maximum
        Integer currentVM = autoScaleVmGroupVmMapDao.countAvailableVmsByGroup(asGroup.getId());
        Integer maxVm = asGroup.getMaxMembers();
        if (currentVM + numVm > maxVm) {
            logger.warn("number of VM will greater than the maximum in this group if scaling up, so do nothing more");
            return false;
        }
        int erroredInstanceCount = autoScaleVmGroupVmMapDao.getErroredInstanceCount(asGroup.getId());
        if (erroredInstanceCount > AutoScaleManager.AutoScaleErroredInstanceThreshold.value()) {
            logger.warn("Number of Errored Instances are greater than the threshold in this group for scaling up, so do nothing more");
            return false;
        }
        return true;
    }

    private boolean checkConditionDown(AutoScaleVmGroupVO asGroup) {
        Integer currentVM = autoScaleVmGroupVmMapDao.countAvailableVmsByGroup(asGroup.getId());
        Integer minVm = asGroup.getMinMembers();
        if (currentVM - 1 < minVm) {
            logger.warn("number of VM will less than the minimum in this group if scaling down, so do nothing more");
            return false;
        }
        return true;
    }

    protected Map<String, String> getDeployParams (String otherDeployParams) {
        Map<String, String> deployParams = new HashMap<>();
        if (StringUtils.isNotBlank(otherDeployParams)) {
            for (String param : otherDeployParams.split("&")) {
                if (param.split("=").length >= 2) {
                    deployParams.put(param.split("=")[0], param.split("=")[1]);
                }
            }
        }
        return deployParams;
    }

    protected UserVm createNewVM(AutoScaleVmGroupVO asGroup) {
        AutoScaleVmProfileVO profileVo = autoScaleVmProfileDao.findById(asGroup.getProfileId());
        long templateId = profileVo.getTemplateId();
        long serviceOfferingId = profileVo.getServiceOfferingId();
        if (templateId == -1) {
            return null;
        }
        // create new VM into DB
        try {
            //Verify that all objects exist before passing them to the service
            Account owner = accountService.getActiveAccountById(profileVo.getAccountId());

            DataCenter zone = entityMgr.findById(DataCenter.class, profileVo.getZoneId());
            if (zone == null) {
                throw new InvalidParameterValueException("Unable to find zone by id=" + profileVo.getZoneId());
            }

            ServiceOffering serviceOffering = entityMgr.findById(ServiceOffering.class, serviceOfferingId);
            if (serviceOffering == null) {
                throw new InvalidParameterValueException("Unable to find service offering: " + serviceOfferingId);
            }

            DiskOffering diskOffering = entityMgr.findById(DiskOffering.class, serviceOffering.getDiskOfferingId());
            if (diskOffering == null) {
                throw new InvalidParameterValueException("Unable to find disk offering: " + serviceOffering.getDiskOfferingId());
            }

            VirtualMachineTemplate template = entityMgr.findById(VirtualMachineTemplate.class, templateId);
            // Make sure a valid template ID was specified
            if (template == null) {
                throw new InvalidParameterValueException("Unable to use template " + templateId);
            }

            if (!zone.isLocalStorageEnabled()) {
                if (diskOffering.isUseLocalStorage()) {
                    throw new InvalidParameterValueException(String.format("Zone is not configured to use local storage but disk offering %s associated to the service offering %s uses it", diskOffering, serviceOffering));
                }
            }

            String userData = profileVo.getUserData();
            Long userDataId = profileVo.getUserDataId();
            String userDataDetails = profileVo.getUserDataDetails();

            UserVm vm = null;
            IpAddresses addrs = new IpAddresses(null, null);
            HypervisorType hypervisorType = template.getHypervisorType();
            final Network network = getNetwork(asGroup.getLoadBalancerId());
            final Map<String, String> customParameters = new HashMap<>();
            final Map<String, String> deployParams = getDeployParams(profileVo.getOtherDeployParams());

            final List<Long> networkIds = getVmNetworkIds(deployParams, network.getId());
            Long overrideDiskOfferingId = getVmOverrideDiskOfferingId(deployParams);
            Long diskOfferingId = getVmDiskOfferingId(deployParams);
            Long dataDiskSize = getVmDataDiskSize(deployParams);
            List<String> sshKeyPairs = getVmSshKeyPairs(deployParams, owner);
            List<Long> affinityGroupIdList = getVmAffinityGroupId(deployParams);
            updateVmDetails(deployParams, customParameters);

            String vmHostName = getNextVmHostName(asGroup);
            asGroup.setNextVmSeq(asGroup.getNextVmSeq() + 1);
            autoScaleVmGroupDao.persist(asGroup);

            if (zone.getNetworkType() == NetworkType.Basic) {
                vm = userVmService.createBasicSecurityGroupVirtualMachine(zone, serviceOffering, template, null, owner, vmHostName,
                        vmHostName, diskOfferingId, dataDiskSize, null,
                        hypervisorType, HTTPMethod.GET, userData, userDataId, userDataDetails, sshKeyPairs,
                        null, null, true, null, affinityGroupIdList, customParameters, null, null, null,
                        null, true, overrideDiskOfferingId);
            } else {
                if (networkModel.checkSecurityGroupSupportForNetwork(owner, zone, networkIds,
                        Collections.emptyList())) {
                    vm = userVmService.createAdvancedSecurityGroupVirtualMachine(zone, serviceOffering, template, networkIds, null,
                            owner, vmHostName,vmHostName, diskOfferingId, dataDiskSize, null,
                            hypervisorType, HTTPMethod.GET, userData, userDataId, userDataDetails, sshKeyPairs,
                            null, null, true, null, affinityGroupIdList, customParameters, null, null, null,
                            null, true, overrideDiskOfferingId, null);
                } else {
                    vm = userVmService.createAdvancedVirtualMachine(zone, serviceOffering, template, networkIds, owner, vmHostName, vmHostName,
                            diskOfferingId, dataDiskSize, null,
                            hypervisorType, HTTPMethod.GET, userData, userDataId, userDataDetails, sshKeyPairs,
                            null, addrs, true, null, affinityGroupIdList, customParameters, null, null, null,
                            null, true, null, overrideDiskOfferingId);
                }
            }

            return vm;
        } catch (InsufficientCapacityException ex) {
            logger.info(ex);
            logger.trace(ex.getMessage(), ex);
            throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, ex.getMessage());
        } catch (ResourceUnavailableException ex) {
            logger.warn("Exception: ", ex);
            throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, ex.getMessage());
        } catch (ConcurrentOperationException ex) {
            logger.warn("Exception: ", ex);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage());
        } catch (ResourceAllocationException ex) {
            logger.warn("Exception: ", ex);
            throw new ServerApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR, ex.getMessage());
        }
    }

    protected List<Long> getVmNetworkIds(Map<String, String> deployParams, long defaultNetworkId) {
        final List<Long> networkIds = new ArrayList<>();
        networkIds.add(defaultNetworkId);
        if (deployParams.get(PARAM_NETWORK_IDS) != null) { // networkids, append to LB networkid
            String[] networkids = deployParams.get(PARAM_NETWORK_IDS).split(",");
            for (String networkid : networkids) {
                Network otherNetwork = networkDao.findByUuid(networkid);
                if (otherNetwork != null && otherNetwork.getId() != defaultNetworkId) {
                    networkIds.add(otherNetwork.getId());
                }
            }
        }
        return networkIds;
    }

    protected Long getVmOverrideDiskOfferingId(Map<String, String> deployParams) {
        Long overrideDiskOfferingId = null;     // override ROOT disk offering
        if (deployParams.get(PARAM_OVERRIDE_DISK_OFFERING_ID) != null) {
            String overrideDiskOfferingUuid = deployParams.get(PARAM_OVERRIDE_DISK_OFFERING_ID);
            DiskOffering overrideDiskOfferingInParam = diskOfferingDao.findByUuid(overrideDiskOfferingUuid);
            if (overrideDiskOfferingInParam != null) {
                overrideDiskOfferingId = overrideDiskOfferingInParam.getId();
            } else {
                logger.warn("Cannot find disk offering by overridediskofferingid from otherdeployparams in AutoScale Vm profile");
            }
        }
        return overrideDiskOfferingId;
    }

    protected final Long getVmDiskOfferingId(Map<String, String> deployParams) {
        Long diskOfferingId = null;     // DATA disk offering ID
        if (deployParams.get(PARAM_DISK_OFFERING_ID) != null) {
            String diskOfferingUuid = deployParams.get(PARAM_DISK_OFFERING_ID);
            DiskOffering diskOfferingInParam = diskOfferingDao.findByUuid(diskOfferingUuid);
            if (diskOfferingInParam != null) {
                diskOfferingId = diskOfferingInParam.getId();
            } else {
                logger.warn("Cannot find disk offering by diskofferingid from otherdeployparams in AutoScale Vm profile");
            }
        }
        return diskOfferingId;
    }

    protected Long getVmDataDiskSize(Map<String, String> deployParams) {
        Long dataDiskSize = null;       // DATA disk size
        if (deployParams.get(PARAM_DISK_SIZE) != null) {
            String dataDiskSizeInParam = deployParams.get(PARAM_DISK_SIZE);
            try {
                dataDiskSize = Long.parseLong(dataDiskSizeInParam);
            } catch (NumberFormatException ex) {
                logger.warn("Cannot parse size from otherdeployparams in AutoScale Vm profile");
            }
        }
        return dataDiskSize;
    }

    protected List<String> getVmSshKeyPairs(Map<String, String> deployParams, Account owner) {
        List<String> sshKeyPairs = new ArrayList<>();
        if (deployParams.get(PARAM_SSH_KEYPAIRS) != null) {   // SSH keypairs
            String[] keypairs = deployParams.get(PARAM_SSH_KEYPAIRS).split(",");
            for (String keypair : keypairs) {
                SSHKeyPairVO s = sshKeyPairDao.findByName(owner.getAccountId(), owner.getDomainId(), keypair);
                if (s != null) {
                    sshKeyPairs.add(s.getName());
                } else {
                    logger.warn("Cannot find ssh keypair by name in sshkeypairs from otherdeployparams in AutoScale Vm profile");
                }
            }
        }
        return sshKeyPairs;
    }

    protected List<Long> getVmAffinityGroupId(Map<String, String> deployParams) {
        List<Long> affinityGroupIdList = new ArrayList<>();
        if (deployParams.get(PARAM_AFFINITY_GROUP_IDS) != null) {   // Affinity groups
            String[] affinityGroupIds = deployParams.get(PARAM_AFFINITY_GROUP_IDS).split(",");
            for (String affinityGroupId : affinityGroupIds) {
                AffinityGroupVO affintyGroup = affinityGroupDao.findByUuid(affinityGroupId);
                if (affintyGroup != null) {
                    affinityGroupIdList.add(affintyGroup.getId());
                } else {
                    logger.warn("Cannot find affinity group by affinitygroupids from otherdeployparams in AutoScale Vm profile");
                }
            }
        }
        return affinityGroupIdList;
    }

    public void updateVmDetails(Map<String, String> deployParams, Map<String, String> customParameters) {
        if (deployParams.get(PARAM_ROOT_DISK_SIZE) != null) {     // ROOT disk size
            String value = deployParams.get(PARAM_ROOT_DISK_SIZE);
            try {
                Long rootDiskSize = Long.parseLong(value);
                customParameters.put(VmDetailConstants.ROOT_DISK_SIZE, String.valueOf(rootDiskSize));
            } catch (NumberFormatException ex) {
                logger.warn("Cannot parse rootdisksize from otherdeployparams in AutoScale Vm profile");
            }
        }
    }

    @Override
    public String getNextVmHostName(AutoScaleVmGroupVO asGroup) {
        String vmHostNameSuffix = "-" + asGroup.getNextVmSeq() + "-" +
                RandomStringUtils.random(VM_HOSTNAME_RANDOM_SUFFIX_LENGTH, 0, 0, true, false, (char[])null, new SecureRandom()).toLowerCase();
        // Truncate vm group name because max length of vm name is 63
        int subStringLength = Math.min(asGroup.getName().length(), 63 - VM_HOSTNAME_PREFIX.length() - vmHostNameSuffix.length());
        return VM_HOSTNAME_PREFIX + asGroup.getName().substring(0, subStringLength) + vmHostNameSuffix;
    }

    @Override
    public void checkAutoScaleVmGroupName(String groupName) {
        String errorMessage = "";
        if (groupName == null || groupName.length() > 255 || groupName.length() < 1) {
            errorMessage = "AutoScale Vm Group name must be between 1 and 255 characters long";
        } else if (!groupName.toLowerCase().matches("[a-z0-9-]*")) {
            errorMessage = "AutoScale Vm Group name may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner), " +
                    "the digits '0' through '9' and the hyphen ('-')";
        }
        if (StringUtils.isNotBlank(errorMessage)) {
            logger.warn(errorMessage);
            throw new InvalidParameterValueException("Invalid AutoScale VM group name. It can contain the ASCII letters 'a' through 'z', " +
                    "'A' through 'Z', the digits '0' through '9' and the hyphen ('-'), must be between 1 and 255 characters long.");
        }
    }

    private UserVmVO startNewVM(long vmId) {
        try {
            CallContext.current().setEventDetails("Vm Id: " + vmId);
            return userVmMgr.startVirtualMachine(vmId, null, new HashMap<>(), null).first();
        } catch (final ResourceUnavailableException ex) {
            logger.warn("Exception: ", ex);
            throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, ex.getMessage());
        } catch (ResourceAllocationException ex) {
            logger.warn("Exception: ", ex);
            throw new ServerApiException(ApiErrorCode.RESOURCE_ALLOCATION_ERROR, ex.getMessage());
        } catch (ConcurrentOperationException ex) {
            logger.warn("Exception: ", ex);
            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, ex.getMessage());
        } catch (InsufficientCapacityException ex) {
            StringBuilder message = new StringBuilder(ex.getMessage());
            if (ex instanceof InsufficientServerCapacityException) {
                if (((InsufficientServerCapacityException)ex).isAffinityApplied()) {
                    message.append(", Please check the affinity groups provided, there may not be sufficient capacity to follow them");
                }
            }
            logger.info(ex);
            logger.info(message.toString(), ex);
            throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, message.toString());
        }
    }

    private boolean assignLBruleToNewVm(long vmId, AutoScaleVmGroupVO asGroup) {
        List<Long> lstVmId = new ArrayList<Long>();
        long lbId = asGroup.getLoadBalancerId();

        List<LoadBalancerVMMapVO> lbVmMapVos = lbVmMapDao.listByLoadBalancerId(lbId);
        if (CollectionUtils.isNotEmpty(lbVmMapVos)) {
            for (LoadBalancerVMMapVO LbVmMapVo : lbVmMapVos) {
                long instanceId = LbVmMapVo.getInstanceId();
                if (instanceId == vmId) {
                    logger.warn("the new VM is already mapped to LB rule. What's wrong?");
                    return true;
                }
            }
        }
        lstVmId.add(new Long(vmId));
        try {
            return loadBalancingRulesService.assignToLoadBalancer(lbId, lstVmId, new HashMap<>(), true);
        } catch (CloudRuntimeException ex) {
            logger.warn("Caught exception: ", ex);
            return false;
        }
    }

    private long removeLBrule(AutoScaleVmGroupVO asGroup) {
        long lbId = asGroup.getLoadBalancerId();
        long instanceId = -1;
        List<LoadBalancerVMMapVO> lbVmMapVos = lbVmMapDao.listByLoadBalancerId(lbId);
        if (CollectionUtils.isNotEmpty(lbVmMapVos)) {
            for (LoadBalancerVMMapVO LbVmMapVo : lbVmMapVos) {
                instanceId = LbVmMapVo.getInstanceId();
            }
        }
        // take last VM out of the list
        List<Long> lstVmId = new ArrayList<Long>();
        if (instanceId != -1)
            lstVmId.add(instanceId);
        if (loadBalancingRulesService.removeFromLoadBalancer(lbId, lstVmId, new HashMap<>(), true))
            return instanceId;
        else
            return -1;
    }

    @Override
    public void doScaleUp(long groupId, Integer numVm) {
        AutoScaleVmGroupVO asGroup = autoScaleVmGroupDao.findById(groupId);
        if (asGroup == null) {
            logger.error("Can not find the groupid " + groupId + " for scaling up");
            return;
        }
        if (!checkConditionUp(asGroup, numVm)) {
            return;
        }
        AutoScaleVmGroup.State oldState = asGroup.getState();
        AutoScaleVmGroup.State newState = AutoScaleVmGroup.State.SCALING;
        if (!autoScaleVmGroupDao.updateState(groupId, oldState, newState)) {
            logger.error("Can not update vmgroup state from {} to {}, groupId: {}", oldState, newState, asGroup);
            return;
        }
        try {
            for (int i = 0; i < numVm; i++) {
                ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEUP,
                        "Scaling Up AutoScale VM group " + groupId, groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(),
                        true, 0);
                UserVm vm = createNewVM(asGroup);
                if (vm == null) {
                    logger.error("Can not deploy new VM for scaling up in the group {}. Waiting for next round", asGroup);
                    break;
                }
                // persist to DB
                AutoScaleVmGroupVmMapVO groupVmMapVO = new AutoScaleVmGroupVmMapVO(asGroup.getId(), vm.getId());
                autoScaleVmGroupVmMapDao.persist(groupVmMapVO);

                // Add an Inactive-dummy record to statistics table
                createInactiveDummyRecord(asGroup.getId());

                try {
                    startNewVM(vm.getId());
                    createInactiveDummyRecord(asGroup.getId());
                    if (assignLBruleToNewVm(vm.getId(), asGroup)) {
                        // update last_quietTime
                        List<AutoScaleVmGroupPolicyMapVO> groupPolicyVOs = autoScaleVmGroupPolicyMapDao
                                .listByVmGroupId(groupId);
                        for (AutoScaleVmGroupPolicyMapVO groupPolicyVO : groupPolicyVOs) {
                            AutoScalePolicyVO vo = autoScalePolicyDao
                                    .findById(groupPolicyVO.getPolicyId());
                            if (vo.getAction().equals(AutoScalePolicy.Action.SCALEUP)) {
                                vo.setLastQuietTime(new Date());
                                autoScalePolicyDao.persist(vo);
                                break;
                            }
                        }
                        ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEUP,
                                String.format("Started and assigned LB rule for VM %s in AutoScale VM group %s", vm, asGroup), groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
                    } else {
                        logger.error("Can not assign LB rule for this new VM");
                        ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEUP,
                                String.format("Failed to assign LB rule for VM %s in AutoScale VM group %s", vm, asGroup), groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
                        break;
                    }
                } catch (ServerApiException e) {
                    logger.error("Can not deploy new VM for scaling up in the group {}. Waiting for next round", asGroup);
                    ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEUP,
                            String.format("Failed to start VM %s in AutoScale VM group %s", vm, asGroup), groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
                    destroyVm(vm.getId());
                    break;
                }
            }
        } finally {
            if (!autoScaleVmGroupDao.updateState(groupId, newState, oldState)) {
                logger.error("Can not update vmgroup state from {} back to {}, group: {}", newState, oldState, asGroup);
            }
        }
    }

    @Override
    public void doScaleDown(final long groupId) {
        AutoScaleVmGroupVO asGroup = autoScaleVmGroupDao.findById(groupId);
        if (asGroup == null) {
            logger.error("Can not find the groupid " + groupId + " for scaling down");
            return;
        }
        if (!checkConditionDown(asGroup)) {
            return;
        }
        AutoScaleVmGroup.State oldState = asGroup.getState();
        AutoScaleVmGroup.State newState = AutoScaleVmGroup.State.SCALING;
        if (!autoScaleVmGroupDao.updateState(groupId, oldState, newState)) {
            logger.error("Can not update vmgroup state from {} to {}, groupId: {}", oldState, newState, asGroup);
            return;
        }
        ActionEventUtils.onStartedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEDOWN,
                String.format("Scaling down AutoScale VM group %s", asGroup), groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(),
                true, 0);
        try {
            long vmId = -1;
            try {
                vmId = removeLBrule(asGroup);
            } catch (Exception ex) {
                logger.info("Got exception when remove LB rule for a VM in AutoScale VM group: {}", asGroup, ex);
                ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEDOWN,
                        String.format("Failed to remove LB rule for a VM in AutoScale VM group %s", asGroup), groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
                throw ex;
            }
            if (vmId != -1) {
                long profileId = asGroup.getProfileId();

                // update group-vm mapping
                autoScaleVmGroupVmMapDao.remove(groupId, vmId);
                // update last_quietTime
                List<AutoScaleVmGroupPolicyMapVO> groupPolicyVOs = autoScaleVmGroupPolicyMapDao.listByVmGroupId(groupId);
                for (AutoScaleVmGroupPolicyMapVO groupPolicyVO : groupPolicyVOs) {
                    AutoScalePolicyVO vo = autoScalePolicyDao.findById(groupPolicyVO.getPolicyId());
                    if (vo.getAction().equals(AutoScalePolicy.Action.SCALEUP)) {
                        vo.setLastQuietTime(new Date());
                        autoScalePolicyDao.persist(vo);
                        break;
                    }
                }

                // Add an Inactive-dummy record to statistics table
                createInactiveDummyRecord(asGroup.getId());

                // get expungeVmGracePeriod param
                AutoScaleVmProfileVO asProfile = autoScaleVmProfileDao.findById(profileId);
                Integer expungeVmGracePeriod = asProfile.getExpungeVmGracePeriod();
                if (expungeVmGracePeriod > 0) {
                    try {
                        Thread.sleep(expungeVmGracePeriod * 1000L);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new CloudRuntimeException(String.format("Error while waiting %s seconds to destroy the VM %s", expungeVmGracePeriod, vmId));
                    }
                }
                if (destroyVm(vmId)) {
                    ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEDOWN,
                            String.format("Destroyed VM %d in AutoScale VM group %s", vmId, asGroup), groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
                } else {
                    ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEDOWN,
                            String.format("Failed to destroy VM %d in AutoScale VM group %s", vmId, asGroup), groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
                }
            } else {
                logger.error("Can not remove LB rule for the VM being destroyed. Do nothing more.");
                ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, asGroup.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_AUTOSCALEVMGROUP_SCALEDOWN,
                        String.format("Failed to remove LB rule for a VM in AutoScale VM group %s", asGroup), groupId, ApiCommandResourceType.AutoScaleVmGroup.toString(), 0);
            }
        } finally {
            if (!autoScaleVmGroupDao.updateState(groupId, newState, oldState)) {
                logger.error("Can not update vmgroup state from {} back to {}, groupId: {}", newState, oldState, asGroup);
            }
        }
    }

    @Override
    public String getConfigComponentName() {
        return AutoScaleManager.class.getSimpleName();
    }

    @Override
    public ConfigKey<?>[] getConfigKeys() {
        return new ConfigKey<?>[] {
                AutoScaleStatsInterval,
                AutoScaleStatsCleanupDelay,
                AutoScaleStatsWorker,
                AutoScaleErroredInstanceThreshold
        };
    }

    @Override
    public void checkAllAutoScaleVmGroups() {
        // list all AS VMGroups
        List<AutoScaleVmGroupVO> asGroups = autoScaleVmGroupDao.listAll();
        for (AutoScaleVmGroupVO asGroup : asGroups) {
            completionService.submit(new CheckAutoScaleVmGroupAsync(asGroup));
        }
        for (int i = 0; i < asGroups.size(); i++) {
            try {
                Future<Pair<Long, Boolean>> future = completionService.take();
                Pair<Long, Boolean> result = future.get();
                logger.debug("Checked AutoScale vm group " + result.first() + " with result: " + result.second());
            } catch (ExecutionException ex) {
                logger.warn("Failed to get result of checking AutoScale vm group due to Exception: " , ex);
            } catch (InterruptedException ex) {
                logger.warn("Failed to get result of checking AutoScale vm group due to Exception: " , ex);
                Thread.currentThread().interrupt();
            }
        }
    }

    protected class CheckAutoScaleVmGroupAsync implements Callable<Pair<Long, Boolean>> {
        AutoScaleVmGroupVO asGroup;

        public CheckAutoScaleVmGroupAsync(AutoScaleVmGroupVO asGroup) {
            this.asGroup = asGroup;
        }

        @Override
        public Pair<Long, Boolean> call() {
            try {
                logger.debug("Checking AutoScale vm group " + asGroup);
                checkAutoScaleVmGroup(asGroup);
            } catch (Exception ex) {
                logger.warn("Failed to check AutoScale vm group " + asGroup + " due to Exception: " , ex);
                return new Pair<>(asGroup.getId(), false);
            }
            return new Pair<>(asGroup.getId(), true);
        }
    }

    @Override
    public void checkAutoScaleVmGroup(AutoScaleVmGroupVO asGroup) {
        // check group state
        if (asGroup.getState().equals(AutoScaleVmGroup.State.ENABLED)) {
            Network.Provider provider = getLoadBalancerServiceProvider(asGroup.getLoadBalancerId());
            if (Network.Provider.Netscaler.equals(provider)) {
                checkNetScalerAsGroup(asGroup);
            } else if (Network.Provider.VirtualRouter.equals(provider) || Network.Provider.VPCVirtualRouter.equals(provider)) {
                checkVirtualRouterAsGroup(asGroup);
            }
        }
    }

    protected boolean isNative(AutoScaleVmGroupTO groupTO) {
        for (AutoScalePolicyTO policyTO : groupTO.getPolicies()) {
            for (ConditionTO conditionTO : policyTO.getConditions()) {
                CounterTO counterTO = conditionTO.getCounter();
                if (Counter.NativeSources.contains(counterTO.getSource())) {
                    return true;
                }
            }
        }
        return false;
    }

    protected boolean hasSourceVirtualRouter(AutoScaleVmGroupTO groupTO) {
        for (AutoScalePolicyTO policyTO : groupTO.getPolicies()) {
            for (ConditionTO conditionTO : policyTO.getConditions()) {
                CounterTO counterTO = conditionTO.getCounter();
                if (Counter.Source.VIRTUALROUTER.equals(counterTO.getSource())) {
                    return true;
                }
            }
        }
        return false;
    }

    protected Map<Long, List<Long>> getHostAndVmIdsMap(AutoScaleVmGroupTO groupTO) {
        Map<Long, List<Long>> hostAndVmIdsMap = new HashMap<>();

        List<AutoScaleVmGroupVmMapVO> asGroupVmVOs = autoScaleVmGroupVmMapDao.listByGroup(groupTO.getId());
        for (AutoScaleVmGroupVmMapVO asGroupVmVO : asGroupVmVOs) {
            updateHostAndVmIdsMap(hostAndVmIdsMap, asGroupVmVO);
        }
        return hostAndVmIdsMap;
    }

    protected void updateHostAndVmIdsMap(Map<Long, List<Long>> hostAndVmIdsMap, AutoScaleVmGroupVmMapVO asGroupVmVO) {
        Long vmId = asGroupVmVO.getInstanceId();
        UserVmVO vm = userVmDao.findById(vmId);
        Long vmHostId = DEFAULT_HOST_ID;
        if (VirtualMachine.State.Running.equals(vm.getState())) {
            vmHostId = vm.getHostId();
        } else if (VirtualMachine.State.Migrating.equals(vm.getState())) {
            vmHostId = vm.getLastHostId();
        }
        List<Long> vmIds = hostAndVmIdsMap.get(vmHostId);
        if (vmIds == null) {
            vmIds = new ArrayList<>();
        }
        vmIds.add(vmId);
        hostAndVmIdsMap.put(vmHostId, vmIds);
    }

    protected Map<Long, List<CounterTO>> getPolicyCounters(AutoScaleVmGroupTO groupTO) {
        Map<Long, List<CounterTO>> counters = new HashMap<>();
        for (AutoScalePolicyTO policyTO : groupTO.getPolicies()) {
            List<CounterTO> counterTOs = new ArrayList<>();
            for (ConditionTO conditionTO : policyTO.getConditions()) {
                CounterTO counterTO = conditionTO.getCounter();
                counterTOs.add(counterTO);
            }
            counters.put(policyTO.getId(), counterTOs);
        }
        return counters;
    }

    protected AutoScalePolicy.Action getAutoscaleAction(Map<String, Double> countersMap, Map<String, Integer> countersNumberMap, AutoScaleVmGroupTO groupTO) {
        logger.debug("[AutoScale] Getting autoscale action for group [id={}, uuid={}]", groupTO.getId(), groupTO.getUuid());

        Network.Provider provider = getLoadBalancerServiceProvider(groupTO.getLoadBalancerId());

        for (AutoScalePolicyTO policyTO : groupTO.getPolicies()) {
            if (isQuitTimePassForPolicy(policyTO)) {
                AutoScalePolicy.Action determinedAction = checkConditionsForPolicy(countersMap, countersNumberMap, policyTO, provider);
                if (determinedAction != null) {
                    return determinedAction;
                }
            }
        }
        return null;
    }

    protected boolean isQuitTimePassForPolicy(AutoScalePolicyTO policyTO) {
        int quietTime = policyTO.getQuietTime();
        Date quietTimeDate = policyTO.getLastQuietTime();
        long lastQuiettime = 0L;
        if (quietTimeDate != null) {
            lastQuiettime = policyTO.getLastQuietTime().getTime();
        }
        long currentTime = (new Date()).getTime();
        return (currentTime - lastQuiettime) >= quietTime * 1000L;
    }

    protected AutoScalePolicy.Action checkConditionsForPolicy(Map<String, Double> countersMap, Map<String, Integer> countersNumberMap, AutoScalePolicyTO policyTO, Network.Provider provider) {
        boolean bValid = true;
        for (ConditionTO conditionTO : policyTO.getConditions()) {
            CounterTO counter = conditionTO.getCounter();
            long thresholdValue = conditionTO.getThreshold();
            Double thresholdPercent = (double)thresholdValue;

            String key = generateKeyFromPolicyAndConditionAndCounter(policyTO.getId(), conditionTO.getId(), counter.getId());
            if (Network.Provider.Netscaler.equals(provider)) {
                key = generateKeyFromPolicyAndConditionAndCounter(0L, conditionTO.getId(), counter.getId());
            }
            Double sum = countersMap.get(key);
            Integer number = countersNumberMap.get(key);
            logger.debug("Checking policy [id: {}, uuid: {}], condition [id: {}, uuid: {}], counter = \"{}\", sum = {}, number = {}",
                    policyTO.getId(), policyTO.getUuid(), conditionTO.getId(), conditionTO.getUuid(), counter.getName(), sum, number);
            if (number == null || number == 0) {
                bValid = false;
                logger.debug("Skipping policy [id: {}, uuid: {}], condition [id: {}, uuid: {}], counter = \"{}\" because the number is {}",
                        policyTO.getId(), policyTO.getUuid(), conditionTO.getId(), conditionTO.getUuid(), counter.getName(), number);
                break;
            }
            Double avg = sum / number;
            Condition.Operator op = conditionTO.getRelationalOperator();
            boolean bConditionCheck = ((op == com.cloud.network.as.Condition.Operator.EQ) && (thresholdPercent.equals(avg)))
                    || ((op == com.cloud.network.as.Condition.Operator.GE) && (avg.doubleValue() >= thresholdPercent.doubleValue()))
                    || ((op == com.cloud.network.as.Condition.Operator.GT) && (avg.doubleValue() > thresholdPercent.doubleValue()))
                    || ((op == com.cloud.network.as.Condition.Operator.LE) && (avg.doubleValue() <= thresholdPercent.doubleValue()))
                    || ((op == com.cloud.network.as.Condition.Operator.LT) && (avg.doubleValue() < thresholdPercent.doubleValue()));

            logger.debug("Check result on policy [id: {}, uuid: {}], condition [id: {}, uuid: {}], counter = {} is : {}" +
                            " (actual result = {}, operator = {}, threshold = {})",
                    policyTO.getId(), policyTO.getUuid(), conditionTO.getId(), conditionTO.getUuid(), counter.getSource(), bConditionCheck, avg, op, thresholdPercent);

            if (!bConditionCheck) {
                bValid = false;
                break;
            }
        }
        AutoScalePolicy.Action action = bValid ? policyTO.getAction() : null;
        logger.debug("Check result on policy [id: {}, uuid: {}] is {}", policyTO.getId(), policyTO.getUuid(), action);

        return action;
    }

    private List<Pair<String, Integer>> getPairofCounternameAndDuration(AutoScaleVmGroupTO groupTO) {
        List<Pair<String, Integer>> result = new ArrayList<>();

        for (AutoScalePolicyTO policyTO : groupTO.getPolicies()) {
            //get duration
            Integer duration = policyTO.getDuration();
            for (ConditionTO conditionTO : policyTO.getConditions()) {
                CounterTO counter = conditionTO.getCounter();
                StringBuilder buff = new StringBuilder();
                buff.append(counter.getName());
                buff.append(",");
                buff.append(conditionTO.getId());
                // add to result
                Pair<String, Integer> pair = new Pair<>(buff.toString(), duration);
                result.add(pair);
            }
        }
        return result;
    }

    protected Network getNetwork(Long loadBalancerId) {
        final LoadBalancerVO loadBalancer = lbDao.findById(loadBalancerId);
        if (loadBalancer == null) {
            throw new CloudRuntimeException(String.format("Unable to find load balancer with id: %s ", loadBalancerId));
        }
        Network network = networkDao.findById(loadBalancer.getNetworkId());
        if (network == null) {
            throw new CloudRuntimeException(String.format("Unable to find network with id: %s for load balancer: %s", loadBalancer.getNetworkId(), loadBalancer));
        }
        return network;
    }

    protected Pair<String, Integer> getPublicIpAndPort(Long loadBalancerId) {
        final LoadBalancerVO loadBalancer = lbDao.findById(loadBalancerId);
        if (loadBalancer == null) {
            throw new CloudRuntimeException(String.format("Unable to find load balancer with id: %s ", loadBalancerId));
        }
        IPAddressVO ipAddress = ipAddressDao.findById(loadBalancer.getSourceIpAddressId());
        if (ipAddress == null) {
            throw new CloudRuntimeException(String.format("Unable to find IP Address with id: %s for load balancer: %s", loadBalancer.getSourceIpAddressId(), loadBalancer));
        }
        return new Pair<>(ipAddress.getAddress().addr(), loadBalancer.getSourcePortStart());
    }

    protected Network.Provider getLoadBalancerServiceProvider(Long loadBalancerId) {
        final LoadBalancerVO loadBalancer = lbDao.findById(loadBalancerId);
        if (loadBalancer == null) {
            throw new CloudRuntimeException(String.format("Unable to find load balancer with id: %s ", loadBalancerId));
        }
        return lbRulesMgr.getLoadBalancerServiceProvider(loadBalancer);
    }

    protected boolean checkAsGroupMaxAndMinMembers(AutoScaleVmGroupVO asGroup) {
        // check minimum vm of group
        Integer currentVM = autoScaleVmGroupVmMapDao.countAvailableVmsByGroup(asGroup.getId());
        if (currentVM < asGroup.getMinMembers()) {
            logger.debug(String.format("There are currently %s available VMs which is less than the minimum member of " +
                    "the AS group (%s), scaling up %d VMs", currentVM, asGroup.getMinMembers(), asGroup.getMinMembers() - currentVM));
            doScaleUp(asGroup.getId(), asGroup.getMinMembers() - currentVM);
            return false;
        }

        // check maximum vm of group
        if (currentVM > asGroup.getMaxMembers()) {
            logger.debug(String.format("There are currently %s available VMs which is more than the maximum member of " +
                    "the AS group (%s), scaling down %d VMs", currentVM, asGroup.getMaxMembers(), currentVM - asGroup.getMaxMembers()));
            for (int i = 0; i <  currentVM - asGroup.getMaxMembers(); i++) {
                doScaleDown(asGroup.getId());
            }
            return false;
        }
        return true;
    }

    protected void checkNetScalerAsGroup(AutoScaleVmGroupVO asGroup) {
        AutoScaleVmGroupTO groupTO = lbRulesMgr.toAutoScaleVmGroupTO(asGroup);

        if (!isNative(groupTO)) {
            return;
        }

        if (!checkAsGroupMaxAndMinMembers(asGroup)) {
            return;
        }

        //check interval
        long now = (new Date()).getTime();
        if (asGroup.getLastInterval() != null && (now - asGroup.getLastInterval().getTime()) < asGroup.getInterval()) {
            return;
        }

        // update last_interval
        asGroup.setLastInterval(new Date());
        autoScaleVmGroupDao.persist(asGroup);

        if (logger.isDebugEnabled()) {
            logger.debug("[Netscaler AutoScale] Collecting RRDs data...");
        }
        Map<String, String> params = new HashMap<>();
        List<AutoScaleVmGroupVmMapVO> asGroupVmVOs = autoScaleVmGroupVmMapDao.listByGroup(asGroup.getId());
        params.put("total_vm", String.valueOf(asGroupVmVOs.size()));
        for (int i = 0; i < asGroupVmVOs.size(); i++) {
            long vmId = asGroupVmVOs.get(i).getInstanceId();
            VMInstanceVO vmVO = vmInstanceDao.findById(vmId);
            //xe vm-list | grep vmname -B 1 | head -n 1 | awk -F':' '{print $2}'
            params.put("vmname" + (i + 1), vmVO.getInstanceName());
            params.put("vmid" + (i + 1), String.valueOf(vmVO.getId()));

        }
        // get random hostid because all vms are in a cluster
        long vmId = asGroupVmVOs.get(0).getInstanceId();
        VMInstanceVO vmVO = vmInstanceDao.findById(vmId);
        Long receiveHost = vmVO.getHostId();

        setPerformanceMonitorCommandParams(groupTO, params);

        PerformanceMonitorCommand perfMon = new PerformanceMonitorCommand(params, 20);

        try {
            PerformanceMonitorAnswer answer = (PerformanceMonitorAnswer) agentMgr.send(receiveHost, perfMon);
            if (answer == null || !answer.getResult()) {
                logger.debug("Failed to send data to node !");
            } else {
                String result = answer.getDetails();
                logger.debug("[AutoScale] RRDs collection answer: " + result);
                HashMap<String, Double> countersMap = new HashMap<>();
                HashMap<String, Integer> countersNumberMap = new HashMap<>();

                processPerformanceMonitorAnswer(countersMap, countersNumberMap, groupTO, params, result);

                AutoScalePolicy.Action scaleAction = getAutoscaleAction(countersMap, countersNumberMap, groupTO);
                if (scaleAction != null) {
                    logger.debug("[AutoScale] Doing scale action: {} for group {}", scaleAction, asGroup);
                    if (AutoScalePolicy.Action.SCALEUP.equals(scaleAction)) {
                        doScaleUp(asGroup.getId(), 1);
                    } else {
                        doScaleDown(asGroup.getId());
                    }
                }
            }

        } catch (Exception e) {
            logger.error("Cannot sent PerformanceMonitorCommand to host " + receiveHost + " or process the answer due to Exception: ", e);
        }
    }

    protected void setPerformanceMonitorCommandParams(AutoScaleVmGroupTO groupTO, Map<String, String> params) {
        // setup parameters phase: duration and counter
        // list pair [counter, duration]
        List<Pair<String, Integer>> lstPair = getPairofCounternameAndDuration(groupTO);
        int totalCounter = 0;
        String[] lstCounter = new String[lstPair.size()];
        for (int i = 0; i < lstPair.size(); i++) {
            Pair<String, Integer> pair = lstPair.get(i);
            String strCounterNames = pair.first();
            Integer duration = pair.second();

            lstCounter[i] = strCounterNames.split(",")[0];  // counter name
            totalCounter++;
            params.put("duration" + totalCounter, duration.toString());
            params.put("counter" + totalCounter, lstCounter[i]);
            params.put("con" + totalCounter, strCounterNames.split(",")[1]);    // condition id
        }
        params.put("totalCounter", String.valueOf(totalCounter));
    }

    protected void processPerformanceMonitorAnswer(Map<String, Double> countersMap, Map<String, Integer> countersNumberMap,
                                                 AutoScaleVmGroupTO groupTO, Map<String, String> params, String details) {
        // extract data
        String[] counterElements = details.split(",");
        if ((counterElements != null) && (counterElements.length > 0)) {
            for (String string : counterElements) {
                try {
                    String[] counterVals = string.split(":");
                    String[] counterVm = counterVals[0].split("\\.");

                    Long counterId = Long.parseLong(counterVm[1]);

                    ConditionTO condition = new ConditionTO(Long.parseLong(params.get("con" + counterVm[0])), null, 0L, null, null);

                    // For NetScaler, the policyId is not returned in PerformanceMonitorAnswer
                    AutoScalePolicyTO policy = new AutoScalePolicyTO(0L, null, 0, 0, null, null, null, false);

                    Double coVal = Double.parseDouble(counterVals[1]);

                    updateCountersMapWithInstantData(countersMap, countersNumberMap, groupTO, counterId, condition, policy, coVal, AutoScaleValueType.INSTANT_VM);

                } catch (Exception e) {
                    logger.error("Cannot process PerformanceMonitorAnswer due to Exception: ", e);
                }
            }
        }
    }

    protected void updateCountersMapWithInstantData(Map<String, Double> countersMap, Map<String, Integer> countersNumberMap, AutoScaleVmGroupTO groupTO,
                                                    Long counterId, ConditionTO condition, AutoScalePolicyTO policy, Double coVal, AutoScaleValueType valueType) {
        // Summary of all counter by counterId key
        String key = generateKeyFromPolicyAndConditionAndCounter(policy.getId(), condition.getId(), counterId);

        CounterVO counter = counterDao.findById(counterId);
        if (counter == null) {
            return;
        }
        if (Counter.Source.MEMORY.equals(counter.getSource())) {
            // calculate memory in percent
            AutoScaleVmProfileTO profile = groupTO.getProfile();
            ServiceOfferingVO serviceOff = serviceOfferingDao.findByUuidIncludingRemoved(profile.getServiceOfferingId());
            int maxRAM = serviceOff.getRamSize();

            // get current RAM percent
            coVal = coVal / maxRAM * 100;
        } else if (Counter.Source.CPU.equals(counter.getSource())) {
            // cpu
            coVal = coVal * 100;
        }

        if (AutoScaleValueType.INSTANT_VM_GROUP.equals(valueType)) {
            Integer currentVM = autoScaleVmGroupVmMapDao.countAvailableVmsByGroup(groupTO.getId());
            if (currentVM == 0) {
                logger.debug("Skipping updating countersMap for group [id={}, uuid={}] " +
                        "and policy [id={}, uuid={}] and counter [id={}, uuid={}] due to no VMs",
                        groupTO.getId(), groupTO.getUuid(), policy.getId(), policy.getUuid(), counter.getId(), counter.getUuid());
                return;
            }
            coVal = coVal / currentVM;
        }

        updateCountersMapWithProcessedData(countersMap, countersNumberMap, key, coVal);
    }

    protected void updateCountersMapWithProcessedData(Map<String, Double> countersMap, Map<String, Integer> countersNumberMap, String key, Double coVal) {
        /* initialize if data is not set */
        countersMap.computeIfAbsent(key, k -> Double.valueOf(0));
        countersNumberMap.computeIfAbsent(key, k -> 0);

        // update data entry
        countersMap.put(key, countersMap.get(key) + coVal);
        countersNumberMap.put(key, countersNumberMap.get(key) + 1);
    }

    protected void monitorVirtualRouterAsGroup(AutoScaleVmGroupVO asGroup) {
        if (!checkAsGroupMaxAndMinMembers(asGroup)) {
            return;
        }

        //check interval
        long now = (new Date()).getTime();
        if (asGroup.getLastInterval() != null && (now - asGroup.getLastInterval().getTime()) < asGroup.getInterval()) {
            return;
        }

        // update last_interval
        asGroup.setLastInterval(new Date());
        autoScaleVmGroupDao.persist(asGroup);

        logger.debug("[AutoScale] Collecting performance data ...");

        AutoScaleVmGroupTO groupTO = lbRulesMgr.toAutoScaleVmGroupTO(asGroup);

        if (isNative(groupTO)) {
            logger.debug("[AutoScale] Collecting performance data from hosts ...");
            getVmStatsFromHosts(groupTO);
        }

        if (hasSourceVirtualRouter(groupTO)) {
            logger.debug("[AutoScale] Collecting performance data from virtual router ...");
            getNetworkStatsFromVirtualRouter(groupTO);
        }
    }

    protected void checkVirtualRouterAsGroup(AutoScaleVmGroupVO asGroup) {
        AutoScaleVmGroupTO groupTO = lbRulesMgr.toAutoScaleVmGroupTO(asGroup);

        Map<String, Double> countersMap = new HashMap<>();
        Map<String, Integer> countersNumberMap = new HashMap<>();

        try {
            // update counter maps in memory
            if (!updateCountersMap(groupTO, countersMap, countersNumberMap)) {
                return;
            }
        } finally {
            // Remove old statistics from database
            cleanupAsVmGroupStatistics(groupTO);
        }

        // get scale action
        AutoScalePolicy.Action scaleAction = getAutoscaleAction(countersMap, countersNumberMap, groupTO);
        if (scaleAction != null) {
            logger.debug("[AutoScale] Doing scale action: {} for group {}", scaleAction, asGroup);
            if (AutoScalePolicy.Action.SCALEUP.equals(scaleAction)) {
                doScaleUp(asGroup.getId(), 1);
            } else {
                doScaleDown(asGroup.getId());
            }
        }
    }

    protected void getVmStatsFromHosts(AutoScaleVmGroupTO groupTO) {
        // group vms by host id
        Map<Long, List<Long>> hostAndVmIdsMap = getHostAndVmIdsMap(groupTO);

        Map<Long, List<CounterTO>> policyCountersMap = getPolicyCounters(groupTO);

        // get vm stats from each host and update database
        for (Map.Entry<Long, List<Long>> hostAndVmIds : hostAndVmIdsMap.entrySet()) {
            Long hostId = hostAndVmIds.getKey();
            List<Long> vmIds = hostAndVmIds.getValue();

            if (!DEFAULT_HOST_ID.equals(hostId)) {
                Map<Long, ? extends VmStats> vmStatsById = getVmStatsByIdFromHost(hostId, vmIds);
                processVmStatsByIdFromHost(groupTO, vmIds, vmStatsById, policyCountersMap);
            }
        }
    }

    protected Map<Long, ? extends VmStats> getVmStatsByIdFromHost(Long hostId, List<Long> vmIds) {
        Map<Long, ? extends VmStats> vmStatsById = new HashMap<>();
        HostVO host = hostDao.findById(hostId);
        if (host == null) {
            logger.debug("Failed to get VM stats from non-existing host : " + hostId);
            return vmStatsById;
        }
        try {
            vmStatsById = virtualMachineManager.getVirtualMachineStatistics(host, vmIds);
            if (MapUtils.isEmpty(vmStatsById)) {
                logger.warn("Got empty result for virtual machine statistics from host: " + host);
            }
        } catch (Exception e) {
            logger.debug("Failed to get VM stats from host : {}", host);
        }
        return vmStatsById;
    }

    protected void processVmStatsByIdFromHost(AutoScaleVmGroupTO groupTO, List<Long> vmIds, Map<Long, ? extends VmStats> vmStatsById, Map<Long, List<CounterTO>> policyCountersMap) {
        Date timestamp = new Date();
        for (Long vmId : vmIds) {
            VmStatsEntry vmStats = vmStatsById == null ? null : (VmStatsEntry)vmStatsById.get(vmId);
            for (Map.Entry<Long, List<CounterTO>> policyCounters : policyCountersMap.entrySet()) {
                Long policyId = policyCounters.getKey();
                List<CounterTO> counters = policyCounters.getValue();
                for (CounterTO counter : counters) {
                    if (!Counter.NativeSources.contains(counter.getSource())) {
                        continue;
                    }
                    Double counterValue = null;
                    if (vmStats != null) {
                        if (Counter.Source.CPU.equals(counter.getSource())) {
                            counterValue = vmStats.getCPUUtilization() / 100;
                        } else if (Counter.Source.MEMORY.equals(counter.getSource())) {
                            if (vmStats.getIntFreeMemoryKBs() >= 0 && vmStats.getIntFreeMemoryKBs() <= vmStats.getMemoryKBs()) {
                                counterValue = (vmStats.getMemoryKBs() - vmStats.getIntFreeMemoryKBs()) / 1024;
                            } else {
                                // In some scenarios, the free memory is greater than VM memory
                                // see https://github.com/apache/cloudstack/issues/4566
                                logger.warn(String.format("Getting virtual machine statistics return invalid free memory KBs for VM %d: %f", vmId, vmStats.getIntFreeMemoryKBs()));
                            }
                        }
                    }
                    if (counterValue == null) {
                        asGroupStatisticsDao.persist(new AutoScaleVmGroupStatisticsVO(groupTO.getId(), policyId, counter.getId(), vmId, ResourceTag.ResourceObjectType.UserVm,
                                AutoScaleValueType.INSTANT_VM, timestamp));
                    } else {
                        asGroupStatisticsDao.persist(new AutoScaleVmGroupStatisticsVO(groupTO.getId(), policyId, counter.getId(), vmId, ResourceTag.ResourceObjectType.UserVm,
                                counterValue, AutoScaleValueType.INSTANT_VM, timestamp));
                    }
                }
            }
        }
    }

    protected void getNetworkStatsFromVirtualRouter(AutoScaleVmGroupTO groupTO) {
        Network network = getNetwork(groupTO.getLoadBalancerId());
        Pair<String, Integer> publicIpAddr = getPublicIpAndPort(groupTO.getLoadBalancerId());
        List<DomainRouterVO> routers = routerDao.listByNetworkAndRole(network.getId(), VirtualRouter.Role.VIRTUAL_ROUTER);
        if (CollectionUtils.isEmpty(routers)) {
            processGetAutoScaleMetricsAnswer(groupTO, new ArrayList<>(), null);
            return;
        }
        List<AutoScaleMetrics> metrics = setGetAutoScaleMetricsCommandMetrics(groupTO);
        for (DomainRouterVO router : routers) {
            if (VirtualMachine.State.Running.equals(router.getState())) {
                final GetAutoScaleMetricsCommand command = new GetAutoScaleMetricsCommand(router.getPrivateIpAddress(), network.getVpcId() != null, publicIpAddr.first(), publicIpAddr.second(), metrics);
                command.setWait(30);
                GetAutoScaleMetricsAnswer answer = (GetAutoScaleMetricsAnswer) agentMgr.easySend(router.getHostId(), command);
                if (answer == null || !answer.getResult()) {
                    logger.error("Failed to get autoscale metrics from virtual router {}", router);
                    processGetAutoScaleMetricsAnswer(groupTO, new ArrayList<>(), router.getId());
                } else {
                    processGetAutoScaleMetricsAnswer(groupTO, answer.getValues(), router.getId());
                }
            } else {
                processGetAutoScaleMetricsAnswer(groupTO, new ArrayList<>(), router.getId());
            }
        }
    }
    protected List<AutoScaleMetrics> setGetAutoScaleMetricsCommandMetrics(AutoScaleVmGroupTO groupTO) {
        List<AutoScaleMetrics> metrics = new ArrayList<>();
        for (AutoScalePolicyTO policyTO : groupTO.getPolicies()) {
            for (ConditionTO conditionTO : policyTO.getConditions()) {
                CounterTO counter = conditionTO.getCounter();

                String provider = counter.getProvider();
                if (! Network.Provider.VirtualRouter.getName().equals(provider) && ! Network.Provider.VPCVirtualRouter.getName().equals(provider)) {
                    continue;
                }
                VirtualRouterAutoScaleCounter vrCounter = VirtualRouterAutoScaleCounter.fromValue(counter.getValue());
                if (vrCounter == null) {
                    continue;
                }
                metrics.add(new AutoScaleMetrics(vrCounter, policyTO.getId(), conditionTO.getId(), counter.getId(), policyTO.getDuration()));
            }
        }
        return metrics;
    }

    protected void processGetAutoScaleMetricsAnswer(AutoScaleVmGroupTO groupTO, List<AutoScaleMetricsValue> values, Long routerId) {
        Date timestamp = new Date();
        Map<Long, List<CounterTO>> policyCountersMap = getPolicyCounters(groupTO);
        for (Map.Entry<Long, List<CounterTO>> policyCounters : policyCountersMap.entrySet()) {
            Long policyId = policyCounters.getKey();
            List<CounterTO> counters = policyCounters.getValue();
            for (CounterTO counter : counters) {
                if (!Counter.Source.VIRTUALROUTER.equals(counter.getSource())) {
                    continue;
                }
                boolean found = false;
                for (AutoScaleMetricsValue value : values) {
                    AutoScaleMetrics metrics = value.getMetrics();
                    if (policyId.equals(metrics.getPolicyId()) && counter.getId().equals(metrics.getCounterId())) {
                        asGroupStatisticsDao.persist(new AutoScaleVmGroupStatisticsVO(groupTO.getId(), policyId, counter.getId(), routerId,
                                ResourceTag.ResourceObjectType.DomainRouter, value.getValue(), value.getType(), timestamp));
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    asGroupStatisticsDao.persist(new AutoScaleVmGroupStatisticsVO(groupTO.getId(), policyId, counter.getId(), routerId,
                            ResourceTag.ResourceObjectType.DomainRouter, AutoScaleValueType.INSTANT_VM, timestamp));
                }
            }
        }
    }

    protected boolean updateCountersMap(AutoScaleVmGroupTO groupTO, Map<String, Double> countersMap, Map<String, Integer> countersNumberMap) {
        logger.debug("Updating countersMap for as group [id={}, uuid={}]", groupTO.getId(), groupTO.getUuid());
        for (AutoScalePolicyTO policyTO : groupTO.getPolicies()) {
            Date afterDate = new Date(System.currentTimeMillis() - ((long)policyTO.getDuration() << 10));
            List<AutoScaleVmGroupStatisticsVO> dummyStats = asGroupStatisticsDao.listDummyRecordsByVmGroup(groupTO.getId(), afterDate);
            if (CollectionUtils.isNotEmpty(dummyStats)) {
                logger.error("Failed to update counters map as there are {} dummy statistics in as group {}", dummyStats.size(), groupTO.getId());
                return false;
            }
            List<AutoScaleVmGroupStatisticsVO> inactiveStats = asGroupStatisticsDao.listInactiveByVmGroupAndPolicy(groupTO.getId(), policyTO.getId(), afterDate);
            if (CollectionUtils.isNotEmpty(inactiveStats)) {
                logger.error("Failed to update counters map as there are {} Inactive " +
                        "statistics in as group [id={}, uuid={}] and policy [id={}, uuid={}]",
                        inactiveStats.size(), groupTO.getId(), groupTO.getUuid(), policyTO.getId(), policyTO.getUuid());
                continue;
            }
            for (ConditionTO conditionTO : policyTO.getConditions()) {
                updateCountersMapPerCondition(groupTO, policyTO, conditionTO, afterDate, countersMap, countersNumberMap);
            }
        }
        logger.debug("DONE Updating countersMap for as group  [id={}, uuid={}]", groupTO.getId(), groupTO.getUuid());
        return true;
    }

    private void updateCountersMapPerCondition(AutoScaleVmGroupTO groupTO, AutoScalePolicyTO policyTO, ConditionTO conditionTO, Date afterDate,
                                                  Map<String, Double> countersMap, Map<String, Integer> countersNumberMap) {
        Long conditionId = conditionTO.getId();
        CounterTO counter = conditionTO.getCounter();
        List<AutoScaleVmGroupStatisticsVO> stats = asGroupStatisticsDao.listByVmGroupAndPolicyAndCounter(groupTO.getId(), policyTO.getId(), counter.getId(), afterDate);
        if (CollectionUtils.isEmpty(stats)) {
            logger.debug("Skipping updating countersMap for group {} and policy {} and " +
                    "counter {} due to no stats", groupTO.getId(), groupTO.getUuid(), policyTO.getId());
            return;
        }
        logger.debug("Updating countersMap with {} stats for group {} and policy {} and " +
                "counter {}", stats.size(), groupTO.getId(), groupTO.getUuid(), policyTO.getId());
        Map<String, List<AutoScaleVmGroupStatisticsVO>> aggregatedRecords = new HashMap<>();
        List<String> incorrectRecords = new ArrayList<>();
        for (AutoScaleVmGroupStatisticsVO stat : stats) {
            if (Arrays.asList(AutoScaleValueType.INSTANT_VM, AutoScaleValueType.INSTANT_VM_GROUP).contains(stat.getValueType())) {
                updateCountersMapWithInstantData(countersMap, countersNumberMap, groupTO, counter.getId(), conditionTO, policyTO, stat.getRawValue(), stat.getValueType());
            } else if (Arrays.asList(AutoScaleValueType.AGGREGATED_VM, AutoScaleValueType.AGGREGATED_VM_GROUP).contains(stat.getValueType())) {
                String key = stat.getCounterId() + "-" + stat.getResourceId();
                if (incorrectRecords.contains(key)) {
                    continue;
                }
                if (!aggregatedRecords.containsKey(key)) {
                    List<AutoScaleVmGroupStatisticsVO> aggregatedRecordList = new ArrayList<>();
                    aggregatedRecordList.add(stat);
                    aggregatedRecords.put(key, aggregatedRecordList);
                    continue;
                }
                List<AutoScaleVmGroupStatisticsVO> aggregatedRecordList = aggregatedRecords.get(key);
                AutoScaleVmGroupStatisticsVO lastRecord = aggregatedRecordList.get(aggregatedRecordList.size() - 1);
                if (stat.getCreated().after(lastRecord.getCreated())) {
                    if (stat.getRawValue() >= lastRecord.getRawValue()) {
                        aggregatedRecordList.add(stat);
                    } else {
                        logger.info("The new raw value is less than the previous raw value, which means the data is incorrect. The key is " + key);
                        aggregatedRecords.remove(key);
                        incorrectRecords.add(key);
                    }
                }
            }
        }

        updateCountersMapByAggregatedRecords(countersMap, countersNumberMap, aggregatedRecords, conditionTO, policyTO, groupTO);
    }

    public void updateCountersMapByAggregatedRecords(Map<String, Double> countersMap, Map<String, Integer> countersNumberMap,
                                                     Map<String, List<AutoScaleVmGroupStatisticsVO>> aggregatedRecords,
                                                     ConditionTO condition, AutoScalePolicyTO policy, AutoScaleVmGroupTO group) {
        if (MapUtils.isNotEmpty(aggregatedRecords)) {
            logger.debug("Processing aggregated data");
            for (Map.Entry<String, List<AutoScaleVmGroupStatisticsVO>> aggregatedRecord : aggregatedRecords.entrySet()) {
                String recordKey = aggregatedRecord.getKey();
                Long counterId = Long.valueOf(recordKey.split("-")[0]);
                List<AutoScaleVmGroupStatisticsVO> records = aggregatedRecord.getValue();
                if (records.size() <= 1) {
                    logger.info(String.format("Ignoring aggregated records, condition [id=%d, uuid=%s], counterId = %s",
                            condition.getId(), condition.getUuid(), counterId));
                    continue;
                }
                AutoScaleVmGroupStatisticsVO firstRecord = records.get(0);
                AutoScaleVmGroupStatisticsVO lastRecord = records.get(records.size() - 1);
                Double coVal = (lastRecord.getRawValue() - firstRecord.getRawValue()) * 1000 / (lastRecord.getCreated().getTime() - firstRecord.getCreated().getTime());
                if (AutoScaleValueType.AGGREGATED_VM_GROUP.equals(firstRecord.getValueType())) {
                    Integer currentVM = autoScaleVmGroupVmMapDao.countAvailableVmsByGroup(group.getId());
                    if (currentVM == 0) {
                        logger.debug("Skipping updating countersMap for group [id={}," +
                                " uuid={}] and policy [id={}, uuid={}] and counter {} due to no VMs",
                                group.getId(), group.getUuid(), policy.getId(), policy.getUuid(), counterId);
                        return;
                    }
                    coVal = coVal / currentVM;
                }
                String key = generateKeyFromPolicyAndConditionAndCounter(policy.getId(), condition.getId(), counterId);
                updateCountersMapWithProcessedData(countersMap, countersNumberMap, key, coVal);
            }
        }
    }

    private String generateKeyFromPolicyAndConditionAndCounter(Long policyId, Long conditionId, Long counterId) {
        return policyId + "-" + conditionId + "-" + counterId;
    }

    protected void cleanupAsVmGroupStatistics(AutoScaleVmGroupTO groupTO) {
        Integer cleanupDelay = AutoScaleStatsCleanupDelay.value();
        Integer maxDelaySecs = cleanupDelay;
        for (AutoScalePolicyTO policyTO : groupTO.getPolicies()) {
            Integer duration = policyTO.getDuration();
            Integer delaySecs = cleanupDelay >= duration ?  cleanupDelay : duration;
            Date beforeDate = new Date(System.currentTimeMillis() - ((long)delaySecs * 1000));
            logger.debug("Removing stats for policy [id={}, uuid={}] in as group [id={}, uuid={}], before {}",
                    policyTO.getId(), policyTO.getUuid(), groupTO.getId(), groupTO.getUuid(), beforeDate);
            asGroupStatisticsDao.removeByGroupAndPolicy(groupTO.getId(), policyTO.getId(), beforeDate);
            if (delaySecs > maxDelaySecs) {
                maxDelaySecs = delaySecs;
            }
        }
        Date beforeDate = new Date(System.currentTimeMillis() - ((long)maxDelaySecs * 1000));
        logger.debug(String.format("Removing stats for other policies in as group [id=%d, uuid=%s], before %s", groupTO.getId(), groupTO.getUuid(), beforeDate));
        asGroupStatisticsDao.removeByGroupId(groupTO.getId(), beforeDate);
    }

    protected void scheduleMonitorTasks() {
        List<AutoScaleVmGroupVO> vmGroups = autoScaleVmGroupDao.listAll();
        for (AutoScaleVmGroupVO vmGroup : vmGroups) {
            if (vmGroup.getState().equals(AutoScaleVmGroup.State.ENABLED)) {
                scheduleMonitorTask(vmGroup.getId());
            }
        }
    }

    protected void scheduleMonitorTask(Long groupId) {
        ScheduledExecutorService vmGroupExecutor = vmGroupMonitorMaps.get(groupId);
        if (vmGroupExecutor == null) {
            AutoScaleVmGroupVO vmGroup = autoScaleVmGroupDao.findById(groupId);
            logger.debug("Scheduling monitor task for autoscale vm group " + vmGroup);
            vmGroupExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("VmGroup-Monitor-" + groupId));
            vmGroupExecutor.scheduleWithFixedDelay(new MonitorTask(groupId), vmGroup.getInterval(), vmGroup.getInterval(), TimeUnit.SECONDS);
            vmGroupMonitorMaps.put(groupId, vmGroupExecutor);
        }
    }

    protected void cancelMonitorTask(Long groupId) {
        ScheduledExecutorService vmGroupExecutor = vmGroupMonitorMaps.get(groupId);
        if (vmGroupExecutor != null) {
            logger.debug("Cancelling monitor task for autoscale vm group " + groupId);
            vmGroupExecutor.shutdown();
            vmGroupMonitorMaps.remove(groupId);
        }
    }

    protected class MonitorTask extends ManagedContextRunnable {

        Long groupId;

        public MonitorTask(Long groupId) {
            this.groupId = groupId;
        }

        @Override
        protected synchronized void runInContext() {
            try {
                AutoScaleVmGroupVO asGroup = autoScaleVmGroupDao.findById(groupId);
                if (asGroup == null) {
                    logger.error("Can not find the groupid " + groupId + " for monitoring");
                    return;
                }
                logger.debug("Start monitoring on AutoScale VmGroup " + asGroup);
                // check group state
                if (asGroup.getState().equals(AutoScaleVmGroup.State.ENABLED)) {
                    Network.Provider provider = getLoadBalancerServiceProvider(asGroup.getLoadBalancerId());
                    if (Network.Provider.Netscaler.equals(provider)) {
                        logger.debug("Skipping the monitoring on AutoScale VmGroup with Netscaler provider: " + asGroup);
                    } else if (Network.Provider.VirtualRouter.equals(provider) || Network.Provider.VPCVirtualRouter.equals(provider)) {
                        monitorVirtualRouterAsGroup(asGroup);
                    }
                }
            } catch (final Exception e) {
                logger.warn("Caught the following exception on monitoring AutoScale Vm Group", e);
            }
        }
    }

    @Override
    public void checkIfVmActionAllowed(Long vmId) {
        List<AutoScaleVmGroupVmMapVO> asGroupVmVOs = autoScaleVmGroupVmMapDao.listByVm(vmId);
        if (CollectionUtils.isNotEmpty(asGroupVmVOs)) {
            for (AutoScaleVmGroupVmMapVO asGroupVmVO : asGroupVmVOs) {
                AutoScaleVmGroupVO group = autoScaleVmGroupDao.findById(asGroupVmVO.getVmGroupId());
                if (group != null && !AutoScaleVmGroup.State.DISABLED.equals(group.getState())) {
                    throw new InvalidParameterValueException("Cannot perform actions on VM in an AutoScale VM group, Please disable the VM group and retry.");
                }
            }
        }
    }

    @Override
    public void removeVmFromVmGroup(Long vmId) {
        autoScaleVmGroupVmMapDao.removeByVm(vmId);
    }

    protected boolean destroyVm(Long vmId) {
        UserVmVO vm = userVmDao.findById(vmId);
        try {
            if (vm != null) {
                userVmMgr.destroyVm(vmId, true);
                userVmMgr.expunge(vm);
            }
            return true;
        } catch (Exception ex) {
            logger.error("Cannot destroy vm {} with id: {} due to Exception: ", vm, vmId, ex);
            return false;
        }
    }

    private void markStatisticsAsInactive(Long groupId, Long policyId) {
        asGroupStatisticsDao.updateStateByGroup(groupId, policyId, AutoScaleVmGroupStatisticsVO.State.INACTIVE);
    }

    private void createInactiveDummyRecord(Long groupId) {
        asGroupStatisticsDao.createInactiveDummyRecord(groupId);
    }
}
