// 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.element;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.inject.Inject;
import javax.naming.ConfigurationException;

import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.network.ExternalNetworkDeviceManager.NetworkDevice;
import org.apache.cloudstack.region.gslb.GslbServiceProvider;
import org.json.JSONException;
import org.json.JSONObject;

import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.NetScalerImplementNetworkCommand;
import com.cloud.agent.api.routing.GlobalLoadBalancerConfigCommand;
import com.cloud.agent.api.routing.HealthCheckLBConfigAnswer;
import com.cloud.agent.api.routing.HealthCheckLBConfigCommand;
import com.cloud.agent.api.routing.LoadBalancerConfigCommand;
import com.cloud.agent.api.routing.SetStaticNatRulesAnswer;
import com.cloud.agent.api.routing.SetStaticNatRulesCommand;
import com.cloud.agent.api.to.LoadBalancerTO;
import com.cloud.agent.api.to.StaticNatRuleTO;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.commands.AddNetscalerLoadBalancerCmd;
import com.cloud.api.commands.ConfigureNetscalerLoadBalancerCmd;
import com.cloud.api.commands.DeleteNetscalerControlCenterCmd;
import com.cloud.api.commands.DeleteNetscalerLoadBalancerCmd;
import com.cloud.api.commands.DeleteServicePackageOfferingCmd;
import com.cloud.api.commands.DeployNetscalerVpxCmd;
import com.cloud.api.commands.ListNetscalerControlCenterCmd;
import com.cloud.api.commands.ListNetscalerLoadBalancerNetworksCmd;
import com.cloud.api.commands.ListNetscalerLoadBalancersCmd;
import com.cloud.api.commands.ListRegisteredServicePackageCmd;
import com.cloud.api.commands.RegisterNetscalerControlCenterCmd;
import com.cloud.api.commands.RegisterServicePackageCmd;
import com.cloud.api.commands.StopNetScalerVMCmd;
import com.cloud.api.response.NetScalerServicePackageResponse;
import com.cloud.api.response.NetscalerControlCenterResponse;
import com.cloud.api.response.NetscalerLoadBalancerResponse;
import com.cloud.configuration.Config;
import com.cloud.configuration.ConfigurationManager;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType;
import com.cloud.dc.DataCenterIpAddressVO;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.HostPodVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.DataCenterIpAddressDao;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InsufficientNetworkCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.Host;
import com.cloud.host.Host.Type;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.host.dao.HostDetailsDao;
import com.cloud.network.ExternalLoadBalancerDeviceManager;
import com.cloud.network.ExternalLoadBalancerDeviceManagerImpl;
import com.cloud.network.IpAddress;
import com.cloud.network.IpAddressManager;
import com.cloud.network.NetScalerControlCenterVO;
import com.cloud.network.NetScalerPodVO;
import com.cloud.network.NetScalerServicePackageVO;
import com.cloud.network.Network;
import com.cloud.network.Network.Capability;
import com.cloud.network.Network.Provider;
import com.cloud.network.Network.Service;
import com.cloud.network.NetworkModel;
import com.cloud.network.Networks.TrafficType;
import com.cloud.network.PhysicalNetwork;
import com.cloud.network.PhysicalNetworkServiceProvider;
import com.cloud.network.PublicIpAddress;
import com.cloud.network.as.AutoScaleCounter;
import com.cloud.network.as.AutoScaleCounter.AutoScaleCounterType;
import com.cloud.network.dao.ExternalLoadBalancerDeviceDao;
import com.cloud.network.dao.ExternalLoadBalancerDeviceVO;
import com.cloud.network.dao.ExternalLoadBalancerDeviceVO.LBDeviceState;
import com.cloud.network.dao.NetScalerControlCenterDao;
import com.cloud.network.dao.NetScalerPodDao;
import com.cloud.network.dao.NetScalerServicePackageDao;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkExternalLoadBalancerDao;
import com.cloud.network.dao.NetworkExternalLoadBalancerVO;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.lb.LoadBalancingRule;
import com.cloud.network.lb.LoadBalancingRulesManager;
import com.cloud.network.lb.LoadBalancingRule.LbDestination;
import com.cloud.network.resource.NetScalerControlCenterResource;
import com.cloud.network.resource.NetscalerResource;
import com.cloud.network.router.VirtualRouter;
import com.cloud.network.rules.FirewallRule;
import com.cloud.network.rules.LbStickinessMethod;
import com.cloud.network.rules.LbStickinessMethod.StickinessMethodType;
import com.cloud.network.rules.LoadBalancerContainer;
import com.cloud.network.rules.StaticNat;
import com.cloud.network.vm.NetScalerVMManager;
import com.cloud.offering.NetworkOffering;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.resource.ResourceManager;
import com.cloud.resource.ResourceState;
import com.cloud.resource.ServerResource;
import com.cloud.user.Account;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.crypt.DBEncryptionUtil;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.db.TransactionCallbackNoReturn;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.UrlUtil;
import com.cloud.vm.NicProfile;
import com.cloud.vm.ReservationContext;
import com.cloud.vm.VirtualMachineProfile;
import com.google.gson.Gson;

public class NetscalerElement extends ExternalLoadBalancerDeviceManagerImpl
implements LoadBalancingServiceProvider, NetscalerLoadBalancerElementService, ExternalLoadBalancerDeviceManager,
IpDeployer, StaticNatServiceProvider, GslbServiceProvider {


    @Inject
    NetworkModel _networkManager;
    @Inject
    ConfigurationManager _configMgr;
    @Inject
    NetworkServiceMapDao _ntwkSrvcDao;
    @Inject
    AgentManager _agentMgr;
    @Inject
    NetworkModel _networkMgr;
    @Inject
    HostDao _hostDao;
    @Inject
    DataCenterDao _dcDao;
    @Inject
    ExternalLoadBalancerDeviceDao _lbDeviceDao;
    @Inject
    NetScalerControlCenterDao _netscalerControlCenterDao;
    @Inject
    NetworkExternalLoadBalancerDao _networkLBDao;
    @Inject
    PhysicalNetworkDao _physicalNetworkDao;
    @Inject
    NetworkDao _networkDao;
    @Inject
    HostDetailsDao _detailsDao;
    @Inject
    ConfigurationDao _configDao;
    @Inject
    NetScalerPodDao _netscalerPodDao;
    @Inject
    DataCenterIpAddressDao _privateIpAddressDao;
    @Inject
    ExternalLoadBalancerDeviceDao _externalLoadBalancerDeviceDao;
    @Inject
    NetScalerServicePackageDao _netscalerServicePackageDao;
    @Inject
    ResourceManager _resourceMgr;
    @Inject
    HostDetailsDao _hostDetailDao;
    @Inject
    IpAddressManager _ipAddrMgr;
    @Inject
    NetworkOrchestrationService _networkService;
    @Inject
    NetworkOfferingDao _networkOfferingDao = null;
    @Inject
    NetScalerVMManager _netScalerVMManager;
    @Inject
    LoadBalancingRulesManager lbRulesManager;

    private boolean canHandle(Network config, Service service) {
        DataCenter zone = _dcDao.findById(config.getDataCenterId());
        // Create a NCC Resource on Demand for the zone.

        boolean handleInAdvanceZone = (zone.getNetworkType() == NetworkType.Advanced
                && (config.getGuestType() == Network.GuestType.Isolated
                || config.getGuestType() == Network.GuestType.Shared)
                && config.getTrafficType() == TrafficType.Guest);
        boolean handleInBasicZone = (zone.getNetworkType() == NetworkType.Basic
                && config.getGuestType() == Network.GuestType.Shared && config.getTrafficType() == TrafficType.Guest);

        if (!(handleInAdvanceZone || handleInBasicZone)) {
            logger.trace("Not handling network with Type  " + config.getGuestType() + " and traffic type "
                    + config.getTrafficType() + " in zone of type " + zone.getNetworkType());
            return false;
        }

        return (_networkManager.isProviderForNetwork(getProvider(), config.getId()) && _ntwkSrvcDao
                .canProviderSupportServiceInNetwork(config.getId(), service, Network.Provider.Netscaler));
    }

    private boolean isBasicZoneNetwok(Network config) {
        DataCenter zone = _dcDao.findById(config.getDataCenterId());
        return (zone.getNetworkType() == NetworkType.Basic && config.getGuestType() == Network.GuestType.Shared
                && config.getTrafficType() == TrafficType.Guest);
    }

    @Override
    public boolean implement(Network guestConfig, NetworkOffering offering, DeployDestination dest,
            ReservationContext context) throws ResourceUnavailableException, ConcurrentOperationException,
    InsufficientNetworkCapacityException {

        if (!canHandle(guestConfig, Service.Lb)) {
            return false;
        }

        if (_ntwkSrvcDao.canProviderSupportServiceInNetwork(guestConfig.getId(), Service.StaticNat,
                Network.Provider.Netscaler) && !isBasicZoneNetwok(guestConfig)) {
            logger.error("NetScaler provider can not be Static Nat service provider for the network "
                    + guestConfig.getGuestType() + " and traffic type " + guestConfig.getTrafficType());
            return false;
        }

        try {
            if (offering.getServicePackage() == null) {
                return manageGuestNetworkWithExternalLoadBalancer(true, guestConfig);
            } else {
                // if the network offering has service package implement it with
                // Netscaler Control Center
                return manageGuestNetworkWithNetscalerControlCenter(true, guestConfig, offering);
            }
        } catch (InsufficientCapacityException capacityException) {
            throw new ResourceUnavailableException(
                    "There are no NetScaler load balancer devices with the free capacity for implementing this network",
                    DataCenter.class, guestConfig.getDataCenterId());
        } catch (ConfigurationException e) {
            throw new ResourceUnavailableException(
                    "There are no NetScaler load balancer devices with the free capacity for implementing this network : "
                            + e.getMessage(),
                            DataCenter.class, guestConfig.getDataCenterId());
        }
    }

    @Override
    public HostVO getNetScalerControlCenterForNetwork(Network guestConfig) {
        long zoneId = guestConfig.getDataCenterId();
        return _hostDao.findByTypeNameAndZoneId(zoneId, "NetscalerControlCenter", Type.NetScalerControlCenter);
    }

    public HostVO allocateNCCResourceForNetwork(Network guestConfig) throws ConfigurationException {

        Map<String, String> _configs;
        List<NetScalerControlCenterVO> ncc = _netscalerControlCenterDao.listAll();
        HostVO hostVO = null;
        if (ncc.size() > 0) {
            NetScalerControlCenterVO nccVO = ncc.get(0);
            String ipAddress = nccVO.getNccip();
            Map hostDetails = new HashMap<String, String>();
            String hostName = "NetscalerControlCenter";
            hostDetails.put("name", hostName);
            hostDetails.put("guid", UUID.randomUUID().toString());
            hostDetails.put("zoneId", Long.toString(guestConfig.getDataCenterId()));
            hostDetails.put("ip", ipAddress);
            hostDetails.put("username", nccVO.getUsername());
            hostDetails.put("password", DBEncryptionUtil.decrypt(nccVO.getPassword()));
            hostDetails.put("deviceName", "netscaler control center");
            hostDetails.put("cmdTimeOut", Long.toString(NumbersUtil.parseInt(_configDao.getValue(Config.NCCCmdTimeOut.key()), 600000)));
            ServerResource resource = new NetScalerControlCenterResource();
            resource.configure(hostName, hostDetails);
            final Host host = _resourceMgr.addHost(guestConfig.getDataCenterId(), resource, Host.Type.NetScalerControlCenter, hostDetails);
            hostVO = _hostDao.findById(host.getId());
        }
        return hostVO;
    }

    public boolean manageGuestNetworkWithNetscalerControlCenter(boolean add, Network guestConfig,
            NetworkOffering offering)
                    throws ResourceUnavailableException, InsufficientCapacityException, ConfigurationException {

        if (guestConfig.getTrafficType() != TrafficType.Guest) {
            logger.trace("External load balancer can only be used for guest networks.");
            return false;
        }

        long zoneId = guestConfig.getDataCenterId();
        DataCenterVO zone = _dcDao.findById(zoneId);
        HostVO netscalerControlCenter = null;

        if (add) {
            HostVO lbDeviceVO = null;
            // on restart network, device could have been allocated already,
            // skip allocation if a device is assigned
            lbDeviceVO = getNetScalerControlCenterForNetwork(guestConfig);
            if (lbDeviceVO == null) {
                // allocate a load balancer device for the network
                lbDeviceVO = allocateNCCResourceForNetwork(guestConfig);
                if (lbDeviceVO == null) {
                    String msg = String.format("failed to allocate Netscaler ControlCenter Resource for the zone in the network %s", guestConfig);
                    logger.error(msg);
                    throw new InsufficientNetworkCapacityException(msg, DataCenter.class,
                            guestConfig.getDataCenterId());
                }
            }
            netscalerControlCenter = _hostDao.findById(lbDeviceVO.getId());
            logger.debug("Allocated Netscaler Control Center device: {} for the network: {}", lbDeviceVO, guestConfig);
        } else {
            // find the load balancer device allocated for the network

            HostVO lbDeviceVO = null;
            // on restart network, device could have been allocated already, skip allocation if a device is assigned
            lbDeviceVO = getNetScalerControlCenterForNetwork(guestConfig);
            if (lbDeviceVO == null) {
                logger.warn(
                        "Network shutdwon requested on external load balancer element, which did not implement the network."
                                + " Either network implement failed half way through or already network shutdown is completed. So just returning.");
                return true;
            }

            netscalerControlCenter = _hostDao.findById(lbDeviceVO.getId());
            assert(netscalerControlCenter != null) : "There is no device assigned to this network how did shutdown network ended up here??";
        }
        JSONObject networkDetails = new JSONObject();
        JSONObject networkPayload = new JSONObject();

        String selfIp = null;
        try {
            networkDetails.put("id", guestConfig.getId());
            networkDetails.put("vlan", guestConfig.getBroadcastUri());
            networkDetails.put("cidr", guestConfig.getCidr());
            networkDetails.put("gateway", guestConfig.getGateway());
            networkDetails.put("servicepackage_id", offering.getServicePackage());
            networkDetails.put("zone_id", zone.getUuid());
            networkDetails.put("account_id", guestConfig.getAccountId());
            networkDetails.put("add", Boolean.toString(add));
            selfIp = _ipAddrMgr.acquireGuestIpAddress(guestConfig, null);
            if (selfIp == null) {
                String msg = "failed to acquire guest IP address so not implementing the network on the NetscalerControlCenter";
                logger.error(msg);
                throw new InsufficientNetworkCapacityException(msg, Network.class, guestConfig.getId());
            }
            networkDetails.put("snip", selfIp);
            // TODO region is hardcoded make it dynamic
            networkDetails.put("region_id", 1);
            networkDetails.put("name", guestConfig.getName());
            networkPayload.put("network", networkDetails);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        NetScalerImplementNetworkCommand cmd = new NetScalerImplementNetworkCommand(zone.getUuid(), netscalerControlCenter.getId(), networkPayload.toString());
        Answer answer = _agentMgr.easySend(netscalerControlCenter.getId(), cmd);
        if (add) {
            //TODO After getting the answer check with the job id and do poll on the job and then save the selfip or acquired guest ip to the Nics table
            if (answer != null) {
                if (answer.getResult() == true) {
                    if (add) {
                        // Insert a new NIC for this guest network to reserve the self IP
                        _networkService.savePlaceholderNic(guestConfig, selfIp, null, null);
                    }
                } else {
                    return false;
                }
            }
        } else {
            if (answer != null) {
                if (answer.getResult() == true) {
                    return true;
                } else {
                    return false;
                }
            }
            return false;
        }
        return true;
    }

    @Override
    public boolean prepare(Network config, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest,
            ReservationContext context) throws ConcurrentOperationException, InsufficientNetworkCapacityException,
    ResourceUnavailableException {
        return true;
    }

    @Override
    public boolean release(Network config, NicProfile nic, VirtualMachineProfile vm, ReservationContext context) {
        return true;
    }

    @Override
    public boolean shutdown(Network guestConfig, ReservationContext context, boolean cleanup)
            throws ResourceUnavailableException, ConcurrentOperationException {
        if (!canHandle(guestConfig, Service.Lb)) {
            return false;
        }

        try {

            NetworkOffering networkOffering = _networkOfferingDao.findById(guestConfig.getNetworkOfferingId());
            if (networkOffering.getServicePackage() == null) {
                return manageGuestNetworkWithExternalLoadBalancer(true, guestConfig);
            } else {
                // if the network offering has service package implement it with Netscaler Control Center
                return manageGuestNetworkWithNetscalerControlCenter(false, guestConfig, networkOffering);
            }
        } catch (InsufficientCapacityException capacityException) {
            // TODO: handle out of capacity exception gracefully in case of
            // multple providers available
            return false;
        } catch (ConfigurationException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean destroy(Network config, ReservationContext context) {
        return true;
    }

    @Override
    public boolean validateLBRule(Network network, LoadBalancingRule rule) {
        if (canHandle(network, Service.Lb)) {
            String algo = rule.getAlgorithm();
            return (algo.equals("roundrobin") || algo.equals("leastconn") || algo.equals("source"));
        }
        return true;
    }

    @Override
    public boolean applyLBRules(Network config, List<LoadBalancingRule> rules) throws ResourceUnavailableException {
        if (!canHandle(config, Service.Lb)) {
            return false;
        }

        if (!canHandleLbRules(rules)) {
            return false;
        }

        if (isBasicZoneNetwok(config)) {
            return applyElasticLoadBalancerRules(config, rules);
        } else {
            return applyLoadBalancerRules(config, rules);
        }
    }

    @Override
    public Map<Service, Map<Capability, String>> getCapabilities() {
        Map<Service, Map<Capability, String>> capabilities = new HashMap<Service, Map<Capability, String>>();

        // Set capabilities for LB service
        Map<Capability, String> lbCapabilities = new HashMap<Capability, String>();

        // Specifies that the RoundRobin and Leastconn algorithms are supported for load balancing rules
        lbCapabilities.put(Capability.SupportedLBAlgorithms, "roundrobin, leastconn, source");

        // specifies that Netscaler network element can provided both shared and
        // isolation modes
        lbCapabilities.put(Capability.SupportedLBIsolation, "dedicated, shared");

        // Specifies that load balancing rules can be made for either TCP or UDP
        // traffic
        lbCapabilities.put(Capability.SupportedProtocols, "tcp,udp,http");

        // Specifies that this element can measure network usage on a per public
        // IP basis
        lbCapabilities.put(Capability.TrafficStatistics, "per public ip");

        // Specifies that load balancing rules can only be made with public IPs
        // that aren't source NAT IPs
        lbCapabilities.put(Capability.LoadBalancingSupportedIps, "additional");

        // Supports only Public load balancing
        lbCapabilities.put(Capability.LbSchemes, LoadBalancerContainer.Scheme.Public.toString());

        // Support inline mode with firewall
        lbCapabilities.put(Capability.InlineMode, "true");

        // Specifies that load balancing rules can support autoscaling and the list of counters it supports
        // list of counters it supports
        AutoScaleCounter counter;
        List<AutoScaleCounter> counterList = new ArrayList<AutoScaleCounter>();
        counter = new AutoScaleCounter(AutoScaleCounterType.Snmp);
        counterList.add(counter);
        counter.addParam("snmpcommunity", true,
                "the community string that has to be used to do a SNMP GET on the AutoScaled Vm", false);
        counter.addParam("snmpport", false, "the port at which SNMP agent is running on the AutoScaled Vm", false);

        counter = new AutoScaleCounter(AutoScaleCounterType.Netscaler);
        counterList.add(counter);

        Gson gson = new Gson();
        String autoScaleCounterList = gson.toJson(counterList);
        lbCapabilities.put(Capability.AutoScaleCounters, autoScaleCounterList);
        lbCapabilities.put(Capability.VmAutoScaling, "true");

        LbStickinessMethod method;
        List<LbStickinessMethod> methodList = new ArrayList<LbStickinessMethod>();
        method = new LbStickinessMethod(StickinessMethodType.LBCookieBased,
                "This is cookie based sticky method, can be used only for http");
        methodList.add(method);
        method.addParam("holdtime", false, "time period in minutes for which persistence is in effect.", false);

        method = new LbStickinessMethod(StickinessMethodType.AppCookieBased,
                "This is app session based sticky method, can be used only for http");
        methodList.add(method);
        method.addParam("name", true, "cookie name passed in http header by apllication to the client", false);

        method = new LbStickinessMethod(StickinessMethodType.SourceBased,
                "This is source based sticky method, can be used for any type of protocol.");
        methodList.add(method);
        method.addParam("holdtime", false, "time period for which persistence is in effect.", false);

        String stickyMethodList = gson.toJson(methodList);
        lbCapabilities.put(Capability.SupportedStickinessMethods, stickyMethodList);

        lbCapabilities.put(Capability.ElasticLb, "true");
        // Setting HealthCheck Capability to True for Netscaler element
        lbCapabilities.put(Capability.HealthCheckPolicy, "true");
        capabilities.put(Service.Lb, lbCapabilities);

        Map<Capability, String> staticNatCapabilities = new HashMap<Capability, String>();
        staticNatCapabilities.put(Capability.ElasticIp, "true");
        capabilities.put(Service.StaticNat, staticNatCapabilities);

        // Supports SSL offloading
        lbCapabilities.put(Capability.SslTermination, "true");

        // TODO - Murali, please put correct capabilities here
        Map<Capability, String> firewallCapabilities = new HashMap<Capability, String>();
        firewallCapabilities.put(Capability.TrafficStatistics, "per public ip");
        firewallCapabilities.put(Capability.SupportedProtocols, "tcp,udp,icmp");
        firewallCapabilities.put(Capability.MultipleIps, "true");
        capabilities.put(Service.Firewall, firewallCapabilities);

        return capabilities;
    }

    @Override
    public ExternalLoadBalancerDeviceVO addNetscalerLoadBalancer(AddNetscalerLoadBalancerCmd cmd) {
        String deviceName = cmd.getDeviceType();

        if (!isNetscalerDevice(deviceName)) {
            throw new InvalidParameterValueException("Invalid Netscaler device type");
        }

        URI uri;
        try {
            uri = new URI(cmd.getUrl());
        } catch (Exception e) {
            String msg = "Error parsing the url parameter specified in addNetscalerLoadBalancer command due to "
                    + e.getMessage();
            logger.debug(msg);
            throw new InvalidParameterValueException(msg);
        }
        Map<String, String> configParams = new HashMap<String, String>();
        UrlUtil.parseQueryParameters(uri.getQuery(), false, configParams);
        boolean dedicatedUse = (configParams.get(ApiConstants.LOAD_BALANCER_DEVICE_DEDICATED) != null)
                ? Boolean.parseBoolean(configParams.get(ApiConstants.LOAD_BALANCER_DEVICE_DEDICATED)) : false;

                if (dedicatedUse && !deviceName.equals(NetworkDevice.NetscalerVPXLoadBalancer.getName())) {
                    String msg = "Only Netscaler VPX load balancers can be specified for dedicated use";
                    logger.debug(msg);
                    throw new InvalidParameterValueException(msg);
                }

                if (cmd.isGslbProvider()) {

                    if (!deviceName.equals(NetworkDevice.NetscalerVPXLoadBalancer.getName())
                            && !deviceName.equals(NetworkDevice.NetscalerMPXLoadBalancer.getName())) {
                        String msg = "Only Netscaler VPX or MPX load balancers can be specified as GSLB service provider";
                        logger.debug(msg);
                        throw new InvalidParameterValueException(msg);
                    }

                    if (cmd.getSitePublicIp() == null || cmd.getSitePrivateIp() == null) {
                        String msg = "Public and Privae IP needs to provided for NetScaler that will be GSLB provider";
                        logger.debug(msg);
                        throw new InvalidParameterValueException(msg);
                    }

                    if (dedicatedUse) {
                        throw new InvalidParameterValueException(
                                "NetScaler provisioned to be GSLB service provider can only be configured for shared usage.");
                    }

                }

                if (cmd.isExclusiveGslbProvider() && !cmd.isGslbProvider()) {
                    throw new InvalidParameterValueException(
                            "NetScaler can be provisioned to be exclusive GSLB service provider"
                                    + " only if its being configured as GSLB service provider also.");
                }

                ExternalLoadBalancerDeviceVO lbDeviceVO = addExternalLoadBalancer(cmd.getPhysicalNetworkId(), cmd.getUrl(),
                        cmd.getUsername(), cmd.getPassword(), deviceName, new NetscalerResource(), cmd.isGslbProvider(),
                        cmd.isExclusiveGslbProvider(), cmd.getSitePublicIp(), cmd.getSitePrivateIp());

                return lbDeviceVO;
    }

    @Override
    public boolean deleteNetscalerLoadBalancer(DeleteNetscalerLoadBalancerCmd cmd) {
        Long lbDeviceId = cmd.getLoadBalancerDeviceId();

        ExternalLoadBalancerDeviceVO lbDeviceVo = _lbDeviceDao.findById(lbDeviceId);
        if ((lbDeviceVo == null) || !isNetscalerDevice(lbDeviceVo.getDeviceName())) {
            throw new InvalidParameterValueException("No netscaler device found with ID: " + lbDeviceId);
        }

        return deleteExternalLoadBalancer(lbDeviceVo.getHostId());
    }

    @Override
    public ExternalLoadBalancerDeviceVO configureNetscalerLoadBalancer(ConfigureNetscalerLoadBalancerCmd cmd) {
        Long lbDeviceId = cmd.getLoadBalancerDeviceId();
        Boolean dedicatedUse = cmd.getLoadBalancerDedicated();
        Long capacity = cmd.getLoadBalancerCapacity();
        List<Long> podIds = cmd.getPodIds();
        try {
            return configureNetscalerLoadBalancer(lbDeviceId, capacity, dedicatedUse, podIds);
        } catch (Exception e) {
            throw new CloudRuntimeException("failed to configure netscaler device due to " + e.getMessage());
        }
    }

    @DB
    private ExternalLoadBalancerDeviceVO configureNetscalerLoadBalancer(final long lbDeviceId, Long capacity,
            Boolean dedicatedUse, List<Long> newPodsConfig) {
        final ExternalLoadBalancerDeviceVO lbDeviceVo = _lbDeviceDao.findById(lbDeviceId);
        final Map<String, String> lbDetails = _detailsDao.findDetails(lbDeviceVo.getHostId());

        if ((lbDeviceVo == null) || !isNetscalerDevice(lbDeviceVo.getDeviceName())) {
            throw new InvalidParameterValueException("No netscaler device found with ID: " + lbDeviceId);
        }

        List<Long> currentPodsConfig = new ArrayList<Long>();
        List<NetScalerPodVO> currentPodVOs = _netscalerPodDao.listByNetScalerDeviceId(lbDeviceVo.getId());
        if (currentPodVOs != null && currentPodVOs.size() > 0) {
            for (NetScalerPodVO nsPodVo : currentPodVOs) {
                currentPodsConfig.add(nsPodVo.getPodId());
            }
        }

        final List<Long> podsToAssociate = new ArrayList<Long>();
        if (newPodsConfig != null && newPodsConfig.size() > 0) {
            for (Long podId : newPodsConfig) {
                HostPodVO pod = _podDao.findById(podId);
                if (pod == null) {
                    throw new InvalidParameterValueException("Can't find pod by id " + podId);
                }
            }

            for (Long podId : newPodsConfig) {
                if (!currentPodsConfig.contains(podId)) {
                    podsToAssociate.add(podId);
                }
            }
        }

        final List<Long> podsToDeassociate = new ArrayList<Long>();
        for (Long podId : currentPodsConfig) {
            if (!newPodsConfig.contains(podId)) {
                podsToDeassociate.add(podId);
            }
        }

        String deviceName = lbDeviceVo.getDeviceName();
        if (dedicatedUse != null || capacity != null) {
            if (NetworkDevice.NetscalerSDXLoadBalancer.getName().equalsIgnoreCase(deviceName)
                    || NetworkDevice.NetscalerMPXLoadBalancer.getName().equalsIgnoreCase(deviceName)) {
                if (dedicatedUse != null && dedicatedUse == true) {
                    throw new InvalidParameterValueException(
                            "Netscaler MPX and SDX device should be shared and can not be dedicated to a single account.");
                }
            }

            // check if any networks are using this netscaler device
            List<NetworkExternalLoadBalancerVO> networks = _networkLBDao.listByLoadBalancerDeviceId(lbDeviceId);
            if ((networks != null) && !networks.isEmpty()) {
                if (capacity != null && capacity < networks.size()) {
                    throw new CloudRuntimeException(
                            "There are more number of networks already using this netscaler device than configured capacity");
                }

                if (dedicatedUse != null && dedicatedUse == true) {
                    throw new CloudRuntimeException(
                            "There are networks already using this netscaler device to make device dedicated");
                }
            }
        }

        if (!NetworkDevice.NetscalerSDXLoadBalancer.getName().equalsIgnoreCase(deviceName)) {
            if (capacity != null) {
                lbDeviceVo.setCapacity(capacity);
            }
        } else {
            // FIXME how to interpret configured capacity of the SDX device
        }

        if (dedicatedUse != null) {
            lbDeviceVo.setIsDedicatedDevice(dedicatedUse);
        }

        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                _lbDeviceDao.update(lbDeviceId, lbDeviceVo);

                for (Long podId : podsToAssociate) {
                    NetScalerPodVO nsPodVo = new NetScalerPodVO(lbDeviceId, podId);
                    _netscalerPodDao.persist(nsPodVo);
                }

                for (Long podId : podsToDeassociate) {
                    NetScalerPodVO nsPodVo = _netscalerPodDao.findByPodId(podId);
                    _netscalerPodDao.remove(nsPodVo.getId());
                }

                // FIXME get the row lock to avoid race condition
                _detailsDao.persist(lbDeviceVo.getHostId(), lbDetails);

            }
        });
        HostVO host = _hostDao.findById(lbDeviceVo.getHostId());

        try {
            _agentMgr.reconnect(host.getId());
        } catch (AgentUnavailableException e) {
            logger.warn("failed to reconnect host " + host, e);
        }
        return lbDeviceVo;
    }

    @Override
    public List<Class<?>> getCommands() {
        List<Class<?>> cmdList = new ArrayList<Class<?>>();
        cmdList.add(AddNetscalerLoadBalancerCmd.class);
        cmdList.add(ConfigureNetscalerLoadBalancerCmd.class);
        cmdList.add(DeleteNetscalerLoadBalancerCmd.class);
        cmdList.add(ListNetscalerLoadBalancerNetworksCmd.class);
        cmdList.add(ListNetscalerLoadBalancersCmd.class);
        cmdList.add(RegisterServicePackageCmd.class);
        cmdList.add(RegisterNetscalerControlCenterCmd.class);
        cmdList.add(ListRegisteredServicePackageCmd.class);
        cmdList.add(ListNetscalerControlCenterCmd.class);
        cmdList.add(DeleteServicePackageOfferingCmd.class);
        cmdList.add(DeleteNetscalerControlCenterCmd.class);
        cmdList.add(DeployNetscalerVpxCmd.class);
        cmdList.add(StopNetScalerVMCmd.class);
        return cmdList;
    }

    @Override
    public List<? extends Network> listNetworks(ListNetscalerLoadBalancerNetworksCmd cmd) {
        Long lbDeviceId = cmd.getLoadBalancerDeviceId();
        List<NetworkVO> networks = new ArrayList<NetworkVO>();

        ExternalLoadBalancerDeviceVO lbDeviceVo = _lbDeviceDao.findById(lbDeviceId);
        if (lbDeviceVo == null || !isNetscalerDevice(lbDeviceVo.getDeviceName())) {
            throw new InvalidParameterValueException(
                    "Could not find Netscaler load balancer device with ID " + lbDeviceId);
        }

        List<NetworkExternalLoadBalancerVO> networkLbMaps = _networkLBDao.listByLoadBalancerDeviceId(lbDeviceId);
        if (networkLbMaps != null && !networkLbMaps.isEmpty()) {
            for (NetworkExternalLoadBalancerVO networkLbMap : networkLbMaps) {
                NetworkVO network = _networkDao.findById(networkLbMap.getNetworkId());
                networks.add(network);
            }
        }

        return networks;
    }

    @Override
    public List<ExternalLoadBalancerDeviceVO> listNetscalerLoadBalancers(ListNetscalerLoadBalancersCmd cmd) {
        Long physcialNetworkId = cmd.getPhysicalNetworkId();
        Long lbDeviceId = cmd.getLoadBalancerDeviceId();
        PhysicalNetworkVO pNetwork = null;
        List<ExternalLoadBalancerDeviceVO> lbDevices = new ArrayList<ExternalLoadBalancerDeviceVO>();

        if (physcialNetworkId == null && lbDeviceId == null) {
            throw new InvalidParameterValueException(
                    "Either physical network Id or load balancer device Id must be specified");
        }

        if (lbDeviceId != null) {
            ExternalLoadBalancerDeviceVO lbDeviceVo = _lbDeviceDao.findById(lbDeviceId);
            if (lbDeviceVo == null || !isNetscalerDevice(lbDeviceVo.getDeviceName())) {
                throw new InvalidParameterValueException(
                        "Could not find Netscaler load balancer device with ID: " + lbDeviceId);
            }
            lbDevices.add(lbDeviceVo);
            return lbDevices;
        }

        if (physcialNetworkId != null) {
            pNetwork = _physicalNetworkDao.findById(physcialNetworkId);
            if (pNetwork == null) {
                throw new InvalidParameterValueException(
                        "Could not find phyical network with ID: " + physcialNetworkId);
            }
            lbDevices = _lbDeviceDao.listByPhysicalNetworkAndProvider(physcialNetworkId, Provider.Netscaler.getName());
            return lbDevices;
        }

        return null;
    }

    @Override
    public List<NetScalerServicePackageVO> listRegisteredServicePackages(ListRegisteredServicePackageCmd cmd) {

        List<NetScalerServicePackageVO> lrsPackages = new ArrayList<NetScalerServicePackageVO>();
        lrsPackages = _netscalerServicePackageDao.listAll();
        return lrsPackages;
    }

    @Override
    public List<NetScalerControlCenterVO> listNetscalerControlCenter(ListNetscalerControlCenterCmd cmd) {
        return _netscalerControlCenterDao.listAll();
    }

    @Override
    public boolean deleteServicePackageOffering(DeleteServicePackageOfferingCmd cmd) throws CloudRuntimeException {
        NetScalerServicePackageVO result = null;
        boolean flag=false;
        try {
            result = _netscalerServicePackageDao.findByUuid(cmd.getId());
            if (result == null) {
                throw new CloudRuntimeException("Record does not Exists in the Table");
            }

            if(_networkOfferingDao.isUsingServicePackage(result.getUuid()))
            {
                throw new CloudRuntimeException("Network offering is using the service package. First delete the NeworkOffering and then delete ServicePackage");
            }

            flag = _netscalerServicePackageDao.remove(result.getId());

        } catch (Exception e) {
            if (e instanceof InvalidParameterValueException) {
                throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage());
            } else {
                throw e;
            }

        }
        return flag;

    }

    @DB
    @Override
    public boolean deleteNetscalerControlCenter(DeleteNetscalerControlCenterCmd cmd) throws CloudRuntimeException {

        NetScalerControlCenterVO result = _netscalerControlCenterDao.findByUuid(cmd.getId());
        if (result == null) {
            throw new CloudRuntimeException("External Netscaler Control Center Table does not contain record with this ID");
        } else {
            //ID list of Network Offering which are not removed and have service Package Uuid field not null.
            List<Long> servicePackageId_list = _networkOfferingDao.listNetworkOfferingID();

            if (servicePackageId_list.size() != 0) {
                //VO list of Networks  which are using Network Offering.
                List<NetworkVO> networkVO_list = _networkDao.listNetworkVO(servicePackageId_list);
                if (networkVO_list != null && networkVO_list.size() != 0) {
                    throw new CloudRuntimeException(
                            "ServicePackages published by NetScalerControlCenter are being used by NetworkOfferings. Try deleting NetworkOffering with ServicePackages and then delete NetScalerControlCenter.");
                }
            }
        }
        try {
            _netscalerServicePackageDao.removeAll();
        } catch (CloudRuntimeException ce) {
            throw new CloudRuntimeException("Service Package is being used by Network Offering, Try deleting Network Offering and then delete Service Package.");
        }

        // delete Netscaler Control Center
        _netscalerControlCenterDao.remove(result.getId());

        //Removal of  NCC from Host Table
        List<HostVO> ncc_list = _hostDao.listAll();
        if (ncc_list == null) {
            throw new CloudRuntimeException("Could not find Netscaler Control Center in Database");
        }
        for (HostVO ncc : ncc_list) {
            if (ncc.getType().equals(Host.Type.NetScalerControlCenter)) {
                try {
                    // put the host in maintenance state in order for it to be deleted
                    ncc.setResourceState(ResourceState.Maintenance);
                    _hostDao.update(ncc.getId(), ncc);
                    _resourceMgr.deleteHost(ncc.getId(), false, false);
                } catch (Exception e) {
                    logger.debug(e);
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public NetscalerLoadBalancerResponse createNetscalerLoadBalancerResponse(ExternalLoadBalancerDeviceVO lbDeviceVO) {
        NetscalerLoadBalancerResponse response = new NetscalerLoadBalancerResponse();
        Host lbHost = _hostDao.findById(lbDeviceVO.getHostId());
        Map<String, String> lbDetails = _detailsDao.findDetails(lbDeviceVO.getHostId());
        response.setId(lbDeviceVO.getUuid());
        response.setIpAddress(lbHost.getPrivateIpAddress());
        PhysicalNetwork pnw = ApiDBUtils.findPhysicalNetworkById(lbDeviceVO.getPhysicalNetworkId());
        if (pnw != null) {
            response.setPhysicalNetworkId(pnw.getUuid());
        }
        response.setPublicInterface(lbDetails.get("publicInterface"));
        response.setPrivateInterface(lbDetails.get("privateInterface"));
        response.setDeviceName(lbDeviceVO.getDeviceName());
        if (lbDeviceVO.getCapacity() == 0) {
            long defaultLbCapacity = NumbersUtil
                    .parseLong(_configDao.getValue(Config.DefaultExternalLoadBalancerCapacity.key()), 50);
            response.setDeviceCapacity(defaultLbCapacity);
        } else {
            response.setDeviceCapacity(lbDeviceVO.getCapacity());
        }
        response.setDedicatedLoadBalancer(lbDeviceVO.getIsDedicatedDevice());
        response.setProvider(lbDeviceVO.getProviderName());
        response.setDeviceState(lbDeviceVO.getState().name());
        response.setObjectName("netscalerloadbalancer");

        response.setGslbProvider(lbDeviceVO.getGslbProvider());
        response.setExclusiveGslbProvider(lbDeviceVO.getExclusiveGslbProvider());
        response.setGslbSitePublicIp(lbDeviceVO.getGslbSitePublicIP());
        response.setGslbSitePrivateIp(lbDeviceVO.getGslbSitePrivateIP());

        List<Long> associatedPods = new ArrayList<Long>();
        List<NetScalerPodVO> currentPodVOs = _netscalerPodDao.listByNetScalerDeviceId(lbDeviceVO.getId());
        if (currentPodVOs != null && currentPodVOs.size() > 0) {
            for (NetScalerPodVO nsPodVo : currentPodVOs) {
                associatedPods.add(nsPodVo.getPodId());
            }
        }
        response.setAssociatedPods(associatedPods);
        return response;
    }

    @Override
    public NetscalerControlCenterResponse createNetscalerControlCenterResponse(NetScalerControlCenterVO lncCentersVO) {
        NetscalerControlCenterResponse response = new NetscalerControlCenterResponse(lncCentersVO);
        response.setObjectName("netscalercontrolcenter");
        return response;
    }

    @Override
    public NetScalerServicePackageResponse createRegisteredServicePackageResponse(
            NetScalerServicePackageVO lrsPackageVO) {
        NetScalerServicePackageResponse response = new NetScalerServicePackageResponse();
        response.setId(lrsPackageVO.getUuid());
        response.setName(lrsPackageVO.getName());
        response.setDescription(lrsPackageVO.getDescription());
        response.setObjectName("registeredServicepackage");
        return response;
    }

    @Override
    public Provider getProvider() {
        return Provider.Netscaler;
    }

    @Override
    public boolean isReady(PhysicalNetworkServiceProvider provider) {
        List<ExternalLoadBalancerDeviceVO> lbDevices = _lbDeviceDao
                .listByPhysicalNetworkAndProvider(provider.getPhysicalNetworkId(), Provider.Netscaler.getName());

        // true if at-least one Netscaler device is added in to physical network
        // and is in configured (in enabled state)
        // state
        if (lbDevices != null && !lbDevices.isEmpty()) {
            for (ExternalLoadBalancerDeviceVO lbDevice : lbDevices) {
                if (lbDevice.getState() == LBDeviceState.Enabled) {
                    return true;
                }
            }
        }
        return true;
    }

    @Override
    public boolean shutdownProviderInstances(PhysicalNetworkServiceProvider provider, ReservationContext context)
            throws ConcurrentOperationException, ResourceUnavailableException {
        // TODO reset the configuration on all of the netscaler devices in this
        // physical network
        return true;
    }

    @Override
    public boolean canEnableIndividualServices() {
        return true;
    }

    private boolean isNetscalerDevice(String deviceName) {
        if ((deviceName == null) || ((!deviceName.equalsIgnoreCase(NetworkDevice.NetscalerMPXLoadBalancer.getName()))
                && (!deviceName.equalsIgnoreCase(NetworkDevice.NetscalerSDXLoadBalancer.getName()))
                && (!deviceName.equalsIgnoreCase(NetworkDevice.NetscalerVPXLoadBalancer.getName())))) {
            return false;
        } else {
            return true;
        }
    }

    @Override
    public boolean verifyServicesCombination(Set<Service> services) {
        Set<Service> netscalerServices = new HashSet<Service>();
        netscalerServices.add(Service.Lb);
        netscalerServices.add(Service.StaticNat);

        // NetScaler can only act as Lb and Static Nat service provider
        if (services != null && !services.isEmpty() && !netscalerServices.containsAll(services)) {
            logger.warn(
                    "NetScaler network element can only support LB and Static NAT services and service combination "
                            + services + " is not supported.");

            StringBuffer buff = new StringBuffer();
            for (Service service : services) {
                buff.append(service.getName());
                buff.append(" ");
            }
            logger.warn(
                    "NetScaler network element can only support LB and Static NAT services and service combination "
                            + buff.toString() + " is not supported.");
            logger.warn(
                    "NetScaler network element can only support LB and Static NAT services and service combination "
                            + services + " is not supported.");
            return false;
        }

        return true;
    }

    @Override
    public boolean applyIps(Network network, List<? extends PublicIpAddress> ipAddress, Set<Service> service)
            throws ResourceUnavailableException {
        // return true, as IP will be associated as part of LB rule
        // configuration
        return true;
    }

    @Override
    public IpDeployer getIpDeployer(Network network) {

        if (_networkMgr.isNetworkInlineMode(network)) {
            return getIpDeployerForInlineMode(network);
        }

        return this;
    }

    public boolean applyElasticLoadBalancerRules(Network network, List<LoadBalancingRule> loadBalancingRules)
            throws ResourceUnavailableException {

        if (loadBalancingRules == null || loadBalancingRules.isEmpty()) {
            return true;
        }

        String errMsg = null;
        ExternalLoadBalancerDeviceVO lbDeviceVO = getExternalLoadBalancerForNetwork(network);
        if (lbDeviceVO == null) {
            try {
                lbDeviceVO = allocateLoadBalancerForNetwork(network);
            } catch (Exception e) {
                errMsg = "Could not allocate a NetSclaer load balancer for configuring elastic load balancer rules due to "
                        + e.getMessage();
                logger.error(errMsg);
                throw new ResourceUnavailableException(errMsg, this.getClass(), 0);
            }
        }

        if (!isNetscalerDevice(lbDeviceVO.getDeviceName())) {
            errMsg = "There are no NetScaler load balancer assigned for this network. So NetScaler element can not be handle elastic load balancer rules.";
            logger.error(errMsg);
            throw new ResourceUnavailableException(errMsg, this.getClass(), 0);
        }

        List<LoadBalancerTO> loadBalancersToApply = new ArrayList<LoadBalancerTO>();
        for (int i = 0; i < loadBalancingRules.size(); i++) {
            LoadBalancingRule rule = loadBalancingRules.get(i);
            boolean revoked = (rule.getState().equals(FirewallRule.State.Revoke));
            String protocol = rule.getProtocol();
            String algorithm = rule.getAlgorithm();
            String lbUuid = rule.getUuid();
            String srcIp = rule.getSourceIp().addr();
            int srcPort = rule.getSourcePortStart();
            List<LbDestination> destinations = rule.getDestinations();

            if ((destinations != null && !destinations.isEmpty()) || rule.isAutoScaleConfig()) {
                LoadBalancerTO loadBalancer = new LoadBalancerTO(lbUuid, srcIp, srcPort, protocol, algorithm, revoked,
                        false, false, destinations, rule.getStickinessPolicies(), rule.getHealthCheckPolicies(),
                        rule.getLbSslCert(), rule.getLbProtocol());
                if (rule.isAutoScaleConfig()) {
                    loadBalancer.setAutoScaleVmGroupTO(lbRulesManager.toAutoScaleVmGroupTO(rule.getAutoScaleVmGroup()));
                }
                loadBalancersToApply.add(loadBalancer);
            }
        }

        if (loadBalancersToApply.size() > 0) {
            int numLoadBalancersForCommand = loadBalancersToApply.size();
            LoadBalancerTO[] loadBalancersForCommand = loadBalancersToApply
                    .toArray(new LoadBalancerTO[numLoadBalancersForCommand]);
            LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(loadBalancersForCommand, null);

            HostVO externalLoadBalancer = _hostDao.findById(lbDeviceVO.getHostId());
            Answer answer = _agentMgr.easySend(externalLoadBalancer.getId(), cmd);
            if (answer == null || !answer.getResult()) {
                String details = (answer != null) ? answer.getDetails() : "details unavailable";
                String msg = "Unable to apply elastic load balancer rules to the external load balancer appliance in zone "
                        + network.getDataCenterId() + " due to: " + details + ".";
                logger.error(msg);
                throw new ResourceUnavailableException(msg, DataCenter.class, network.getDataCenterId());
            }
        }

        return true;
    }

    @Override
    public boolean applyStaticNats(Network config, List<? extends StaticNat> rules)
            throws ResourceUnavailableException {

        if (!canHandle(config, Service.StaticNat)) {
            return false;
        }

        boolean multiNetScalerDeployment = Boolean
                .valueOf(_configDao.getValue(Config.EIPWithMultipleNetScalersEnabled.key()));

        try {
            if (!multiNetScalerDeployment) {
                String errMsg;
                ExternalLoadBalancerDeviceVO lbDevice = getExternalLoadBalancerForNetwork(config);
                if (lbDevice == null) {
                    try {
                        lbDevice = allocateLoadBalancerForNetwork(config);
                    } catch (Exception e) {
                        errMsg = "Could not allocate a NetSclaer load balancer for configuring static NAT rules due to"
                                + e.getMessage();
                        logger.error(errMsg);
                        throw new ResourceUnavailableException(errMsg, this.getClass(), 0);
                    }
                }

                if (!isNetscalerDevice(lbDevice.getDeviceName())) {
                    errMsg = "There are no NetScaler load balancer assigned for this network. So NetScaler element will not be handling the static nat rules.";
                    logger.error(errMsg);
                    throw new ResourceUnavailableException(errMsg, this.getClass(), 0);
                }
                SetStaticNatRulesAnswer answer = null;
                List<StaticNatRuleTO> rulesTO = null;
                if (rules != null) {
                    rulesTO = new ArrayList<StaticNatRuleTO>();
                    for (StaticNat rule : rules) {
                        IpAddress sourceIp = _networkMgr.getIp(rule.getSourceIpAddressId());
                        StaticNatRuleTO ruleTO = new StaticNatRuleTO(0, sourceIp.getAddress().addr(), null, null,
                                rule.getDestIpAddress(), null, null, null, rule.isForRevoke(), false);
                        rulesTO.add(ruleTO);
                    }
                }

                SetStaticNatRulesCommand cmd = new SetStaticNatRulesCommand(rulesTO, null);
                answer = (SetStaticNatRulesAnswer)_agentMgr.send(lbDevice.getHostId(), cmd);
                if (answer == null) {
                    return false;
                } else {
                    return answer.getResult();
                }
            } else {
                if (rules != null) {
                    for (StaticNat rule : rules) {
                        // validate if EIP rule can be configured.
                        ExternalLoadBalancerDeviceVO lbDevice = getNetScalerForEIP(rule);
                        if (lbDevice == null) {
                            String errMsg = "There is no NetScaler device configured to perform EIP to guest IP address: "
                                    + rule.getDestIpAddress();
                            logger.error(errMsg);
                            throw new ResourceUnavailableException(errMsg, this.getClass(), 0);
                        }

                        List<StaticNatRuleTO> rulesTO = new ArrayList<StaticNatRuleTO>();
                        IpAddress sourceIp = _networkMgr.getIp(rule.getSourceIpAddressId());
                        StaticNatRuleTO ruleTO = new StaticNatRuleTO(0, sourceIp.getAddress().addr(), null, null,
                                rule.getDestIpAddress(), null, null, null, rule.isForRevoke(), false);
                        rulesTO.add(ruleTO);
                        SetStaticNatRulesCommand cmd = new SetStaticNatRulesCommand(rulesTO, null);

                        // send commands to configure INAT rule on the NetScaler
                        // device
                        SetStaticNatRulesAnswer answer = (SetStaticNatRulesAnswer)_agentMgr.send(lbDevice.getHostId(),
                                cmd);
                        if (answer == null) {
                            String errMsg = "Failed to configure INAT rule on NetScaler device " + lbDevice.getHostId();
                            logger.error(errMsg);
                            throw new ResourceUnavailableException(errMsg, this.getClass(), 0);
                        }
                    }
                    return true;
                }
            }
            return true;
        } catch (Exception e) {
            logger.error("Failed to configure StaticNat rule due to " + e.getMessage());
            return false;
        }
    }

    // returns configured NetScaler device that is associated with the pod that
    // owns guest IP
    private ExternalLoadBalancerDeviceVO getNetScalerForEIP(StaticNat rule) {
        String guestIP = rule.getDestIpAddress();
        List<DataCenterIpAddressVO> dcGuestIps = _privateIpAddressDao.listAll();
        if (dcGuestIps != null) {
            for (DataCenterIpAddressVO dcGuestIp : dcGuestIps) {
                if (dcGuestIp.getIpAddress().equalsIgnoreCase(guestIP)) {
                    long podId = dcGuestIp.getPodId();
                    NetScalerPodVO nsPodVO = _netscalerPodDao.findByPodId(podId);
                    if (nsPodVO != null) {
                        ExternalLoadBalancerDeviceVO lbDeviceVO = _lbDeviceDao.findById(nsPodVO.getNetscalerDeviceId());
                        return lbDeviceVO;
                    }
                }
            }
        }
        return null;
    }

    public List<LoadBalancerTO> getElasticLBRulesHealthCheck(Network network,
            List<LoadBalancingRule> loadBalancingRules) throws ResourceUnavailableException {

        HealthCheckLBConfigAnswer answer = null;

        if (loadBalancingRules == null || loadBalancingRules.isEmpty()) {
            return null;
        }

        String errMsg = null;
        ExternalLoadBalancerDeviceVO lbDeviceVO = getExternalLoadBalancerForNetwork(network);

        if (lbDeviceVO == null) {
            logger.warn(
                    "There is no external load balancer device assigned to this network either network is not implement are already shutdown so just returning");
            return null;
        }

        if (!isNetscalerDevice(lbDeviceVO.getDeviceName())) {
            errMsg = "There are no NetScaler load balancer assigned for this network. So NetScaler element can not be handle elastic load balancer rules.";
            logger.error(errMsg);
            throw new ResourceUnavailableException(errMsg, this.getClass(), 0);
        }

        List<LoadBalancerTO> loadBalancersToApply = new ArrayList<LoadBalancerTO>();
        for (int i = 0; i < loadBalancingRules.size(); i++) {
            LoadBalancingRule rule = loadBalancingRules.get(i);
            boolean revoked = (rule.getState().equals(FirewallRule.State.Revoke));
            String protocol = rule.getProtocol();
            String algorithm = rule.getAlgorithm();
            String lbUuid = rule.getUuid();
            String srcIp = rule.getSourceIp().addr();
            int srcPort = rule.getSourcePortStart();
            List<LbDestination> destinations = rule.getDestinations();

            if ((destinations != null && !destinations.isEmpty()) || rule.isAutoScaleConfig()) {
                LoadBalancerTO loadBalancer = new LoadBalancerTO(lbUuid, srcIp, srcPort, protocol, algorithm, revoked,
                        false, false, destinations, null, rule.getHealthCheckPolicies(), rule.getLbSslCert(),
                        rule.getLbProtocol());
                loadBalancersToApply.add(loadBalancer);
            }
        }

        if (loadBalancersToApply.size() > 0) {
            int numLoadBalancersForCommand = loadBalancersToApply.size();
            LoadBalancerTO[] loadBalancersForCommand = loadBalancersToApply
                    .toArray(new LoadBalancerTO[numLoadBalancersForCommand]);
            HealthCheckLBConfigCommand cmd = new HealthCheckLBConfigCommand(loadBalancersForCommand, network.getId());
            HostVO externalLoadBalancer = _hostDao.findById(lbDeviceVO.getHostId());
            answer = (HealthCheckLBConfigAnswer)_agentMgr.easySend(externalLoadBalancer.getId(), cmd);
            return answer.getLoadBalancers();
        }
        return null;
    }

    @Override
    public List<LoadBalancerTO> updateHealthChecks(Network network, List<LoadBalancingRule> lbrules) {

        if (canHandle(network, Service.Lb) && canHandleLbRules(lbrules)) {
            try {

                if (isBasicZoneNetwok(network)) {
                    return getElasticLBRulesHealthCheck(network, lbrules);
                } else {
                    return getLBHealthChecks(network, lbrules);
                }
            } catch (ResourceUnavailableException e) {
                logger.error("Error in getting the LB Rules from NetScaler " + e);
            }
        } else {
            logger.error("Network cannot handle to LB service ");
        }
        return null;
    }

    @Override
    public boolean handlesOnlyRulesInTransitionState() {
        return true;
    }

    @Override
    public List<LoadBalancerTO> getLBHealthChecks(Network network, List<LoadBalancingRule> rules)
            throws ResourceUnavailableException {
        return super.getLBHealthChecks(network, rules);
    }

    @Override
    public NetScalerServicePackageResponse listNetscalerServicePackage(RegisterServicePackageCmd cmd) {
        return null;
    }

    @Override
    public NetScalerServicePackageResponse deleteNetscalerServicePackage(RegisterServicePackageCmd cmd) {
        return null;
    }

    @Override
    public NetScalerServicePackageResponse createNetscalerServicePackageResponse(NetScalerServicePackageVO servicePackageVO) {
        return null;
    }

    @Override
    public boolean applyGlobalLoadBalancerRule(long zoneId, long physicalNetworkId,
            GlobalLoadBalancerConfigCommand gslbConfigCmd) throws ResourceUnavailableException {

        long zoneGslbProviderHosId = 0;

        // find the NetScaler device configured as gslb service provider in the
        // zone
        ExternalLoadBalancerDeviceVO nsGslbProvider = findGslbProvider(zoneId, physicalNetworkId);
        if (nsGslbProvider == null) {
            String msg = "Unable to find a NetScaler configured as gslb service provider in zone " + zoneId;
            logger.debug(msg);
            throw new ResourceUnavailableException(msg, DataCenter.class, zoneId);
        }

        // get the host Id corresponding to NetScaler acting as GSLB service
        // provider in the zone
        zoneGslbProviderHosId = nsGslbProvider.getHostId();

        // send gslb configuration to NetScaler device
        Answer answer = _agentMgr.easySend(zoneGslbProviderHosId, gslbConfigCmd);
        if (answer == null || !answer.getResult()) {
            String msg = "Unable to apply global load balancer rule to the gslb service provider in zone " + zoneId;
            logger.debug(msg);
            throw new ResourceUnavailableException(msg, DataCenter.class, zoneId);
        }

        return true;
    }

    private ExternalLoadBalancerDeviceVO findGslbProvider(long zoneId, long physicalNetworkId) {
        List<PhysicalNetworkVO> pNtwks = _physicalNetworkDao.listByZoneAndTrafficType(zoneId, TrafficType.Guest);

        if (pNtwks == null || pNtwks.isEmpty()) {
            throw new InvalidParameterValueException(
                    "Unable to get physical network: " + physicalNetworkId + " in zone id = " + zoneId);
        } else {
            for (PhysicalNetwork physicalNetwork : pNtwks) {
                if (physicalNetwork.getId() == physicalNetworkId) {
                    PhysicalNetworkVO physNetwork = pNtwks.get(0);
                    ExternalLoadBalancerDeviceVO nsGslbProvider = _externalLoadBalancerDeviceDao
                            .findGslbServiceProvider(physNetwork.getId(), Provider.Netscaler.getName());
                    return nsGslbProvider;
                }
            }
        }

        return null;
    }

    @Override
    public boolean isServiceEnabledInZone(long zoneId, long physicalNetworkId) {

        ExternalLoadBalancerDeviceVO nsGslbProvider = findGslbProvider(zoneId, physicalNetworkId);
        // return true if a NetScaler device is configured in the zone
        return (nsGslbProvider != null);
    }

    @Override
    public String getZoneGslbProviderPublicIp(long zoneId, long physicalNetworkId) {
        ExternalLoadBalancerDeviceVO nsGslbProvider = findGslbProvider(zoneId, physicalNetworkId);
        if (nsGslbProvider != null) {
            return nsGslbProvider.getGslbSitePublicIP();
        }
        return null;
    }

    @Override
    public String getZoneGslbProviderPrivateIp(long zoneId, long physicalNetworkId) {
        ExternalLoadBalancerDeviceVO nsGslbProvider = findGslbProvider(zoneId, physicalNetworkId);
        if (nsGslbProvider != null) {
            return nsGslbProvider.getGslbSitePrivateIP();
        }
        return null;
    }

    private boolean canHandleLbRules(List<LoadBalancingRule> rules) {
        Map<Capability, String> lbCaps = getCapabilities().get(Service.Lb);
        if (!lbCaps.isEmpty()) {
            String schemeCaps = lbCaps.get(Capability.LbSchemes);
            if (schemeCaps != null) {
                for (LoadBalancingRule rule : rules) {
                    if (!schemeCaps.contains(rule.getScheme().toString())) {
                        logger.debug("Scheme " + rules.get(0).getScheme() + " is not supported by the provider "
                                + getName());
                        return false;
                    }
                }
            }
        }
        return true;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_NETSCALER_SERVICEPACKAGE_ADD, eventDescription = "Registering NetScaler Service Package")
    public NetScalerServicePackageResponse registerNetscalerServicePackage(RegisterServicePackageCmd cmd) {
        NetScalerServicePackageVO servicePackage = new NetScalerServicePackageVO(cmd);
        NetScalerServicePackageResponse response = null;
        _netscalerServicePackageDao.persist(servicePackage);
        response = new NetScalerServicePackageResponse(servicePackage);
        return response;
    }

    @Override
    @DB
    public NetScalerControlCenterVO registerNetscalerControlCenter(RegisterNetscalerControlCenterCmd cmd) {

        if (_netscalerControlCenterDao.listAll() != null && _netscalerControlCenterDao.listAll().size() != 0) {
            throw new CloudRuntimeException("One Netscaler Control Center already exist in the DataBase. At a time only one Netscaler Control Center is allowed");
        }

        final RegisterNetscalerControlCenterCmd cmdinfo = cmd;
        String ipAddress = cmd.getIpaddress();
        Map hostDetails = new HashMap<String, String>();
        String hostName = "NetscalerControlCenter";
        hostDetails.put("name", hostName);
        hostDetails.put("guid", UUID.randomUUID().toString());
        List<DataCenterVO> dcVO = _dcDao.listEnabledZones();
        if (dcVO.size() == 0) {
            throw new CloudRuntimeException("There is no single enabled zone. Please add a zone, enable it and then add Netscaler ControlCenter");
        }
        hostDetails.put("zoneId", "1");
        hostDetails.put("ip", ipAddress);
        hostDetails.put("username", cmd.getUsername());
        hostDetails.put("password", cmd.getPassword());
        hostDetails.put("deviceName", "Netscaler ControlCenter");
        ServerResource resource = new NetScalerControlCenterResource();
        try {
            resource.configure(hostName, hostDetails);
            return Transaction.execute(new TransactionCallback<NetScalerControlCenterVO>() {
                @Override
                public NetScalerControlCenterVO doInTransaction(TransactionStatus status) {
                    NetScalerControlCenterVO nccVO = new NetScalerControlCenterVO(cmdinfo.getUsername(), DBEncryptionUtil.encrypt(cmdinfo.getPassword()),
                            cmdinfo.getIpaddress(), cmdinfo.getNumretries());
                    _netscalerControlCenterDao.persist(nccVO);
                    return nccVO;
                }
            });
        } catch (ConfigurationException e) {
            resource = null;
            throw new CloudRuntimeException(e.getMessage());
        }
    }

    @Override
    public Map<String,Object> deployNetscalerServiceVm(DeployNetscalerVpxCmd cmd) {
        DataCenter zone = _dcDao.findById(cmd.getZoneId());
        DeployDestination dest = new DeployDestination(zone, null, null, null);
        Map<String,Object> resp = new HashMap<String, Object>();
        Long templateId = cmd.getTemplateId();
        Long serviceOfferingId = cmd.getServiceOfferingId();
        DeploymentPlan plan = new DataCenterDeployment(dest.getDataCenter().getId());
        try {
            resp =  _netScalerVMManager.deployNsVpx(cmd.getAccount(), dest, plan, serviceOfferingId, templateId);
        } catch (InsufficientCapacityException e) {
            e.printStackTrace();
        }
        return resp;
    }

    @Override
    public VirtualRouter stopNetscalerServiceVm(Long id, boolean forced, Account callingAccount, long callingUserId) throws ConcurrentOperationException,
    ResourceUnavailableException {
        return _netScalerVMManager.stopNetScalerVm(id, forced, callingAccount, callingUserId);
    }
}
