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

import static com.cloud.agent.api.sync.SyncNuageVspCmsIdCommand.SyncType;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;

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

import net.nuage.vsp.acs.client.api.NuageVspPluginClientLoader;
import net.nuage.vsp.acs.client.api.model.VspApiDefaults;
import net.nuage.vsp.acs.client.api.model.VspDomain;
import net.nuage.vsp.acs.client.api.model.VspDomainCleanUp;
import net.nuage.vsp.acs.client.api.model.VspDomainTemplate;
import net.nuage.vsp.acs.client.api.model.VspHost;
import net.nuage.vsp.acs.client.common.NuageVspApiVersion;
import net.nuage.vsp.acs.client.common.NuageVspConstants;
import net.nuage.vsp.acs.client.common.model.Pair;
import net.nuage.vsp.acs.client.exception.NuageVspException;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;

import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.context.CallContext;
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.framework.config.impl.ConfigurationVO;
import org.apache.cloudstack.framework.messagebus.MessageBus;
import org.apache.cloudstack.network.ExternalNetworkDeviceManager;
import org.apache.cloudstack.resourcedetail.VpcDetailVO;
import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao;

import com.cloud.agent.AgentManager;
import com.cloud.agent.Listener;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.PingNuageVspCommand;
import com.cloud.agent.api.manager.CleanUpDomainCommand;
import com.cloud.agent.api.manager.EntityExistsCommand;
import com.cloud.agent.api.manager.GetApiDefaultsAnswer;
import com.cloud.agent.api.manager.GetApiDefaultsCommand;
import com.cloud.agent.api.manager.ListVspDomainTemplatesAnswer;
import com.cloud.agent.api.manager.ListVspDomainTemplatesCommand;
import com.cloud.agent.api.manager.SupportedApiVersionCommand;
import com.cloud.agent.api.manager.UpdateNuageVspDeviceCommand;
import com.cloud.agent.api.sync.SyncDomainCommand;
import com.cloud.agent.api.sync.SyncNuageVspCmsIdAnswer;
import com.cloud.agent.api.sync.SyncNuageVspCmsIdCommand;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.commands.AddNuageVspDeviceCmd;
import com.cloud.api.commands.AssociateNuageVspDomainTemplateCmd;
import com.cloud.api.commands.DeleteNuageVspDeviceCmd;
import com.cloud.api.commands.DisableNuageUnderlayVlanIpRangeCmd;
import com.cloud.api.commands.EnableNuageUnderlayVlanIpRangeCmd;
import com.cloud.api.commands.ListNuageUnderlayVlanIpRangesCmd;
import com.cloud.api.commands.ListNuageVspDevicesCmd;
import com.cloud.api.commands.ListNuageVspDomainTemplatesCmd;
import com.cloud.api.commands.ListNuageVspGlobalDomainTemplateCmd;
import com.cloud.api.commands.UpdateNuageVspDeviceCmd;
import com.cloud.api.response.NuageVlanIpRangeResponse;
import com.cloud.api.response.NuageVspDeviceResponse;
import com.cloud.api.response.NuageVspDomainTemplateResponse;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.Vlan;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.VlanDao;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.domain.Domain;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.host.DetailVO;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
import com.cloud.host.dao.HostDetailsDao;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.Networks;
import com.cloud.network.NuageVspDeviceVO;
import com.cloud.network.PhysicalNetwork;
import com.cloud.network.PhysicalNetworkServiceProvider;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkDetailVO;
import com.cloud.network.dao.NetworkDetailsDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.NuageVspDao;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkServiceProviderDao;
import com.cloud.network.dao.PhysicalNetworkServiceProviderVO;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.resource.NuageVspResource;
import com.cloud.network.resource.NuageVspResourceConfiguration;
import com.cloud.network.vpc.VpcOffering;
import com.cloud.network.vpc.VpcOfferingServiceMapVO;
import com.cloud.network.vpc.VpcOfferingVO;
import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.VpcDao;
import com.cloud.network.vpc.dao.VpcOfferingDao;
import com.cloud.network.vpc.dao.VpcOfferingServiceMapDao;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
import com.cloud.resource.ResourceManager;
import com.cloud.resource.ResourceState;
import com.cloud.user.DomainManager;
import com.cloud.util.NuageVspEntityBuilder;
import com.cloud.util.NuageVspUtil;
import com.cloud.utils.component.ManagerBase;
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.fsm.StateListener;
import com.cloud.utils.fsm.StateMachine2;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.VMInstanceDao;

public class NuageVspManagerImpl extends ManagerBase implements NuageVspManager, Configurable, StateListener<Status, Status.Event, Host> {

    private static final Logger s_logger = Logger.getLogger(NuageVspManagerImpl.class);

    public static final Multimap<Network.Service, Network.Provider> DEFAULT_NUAGE_VSP_VPC_SERVICE_MAP;
    public static final Multimap<Network.Service, Network.Provider> SUPPORTED_NUAGE_VSP_VPC_SERVICE_MAP;
    private static final ConfigKey[] NUAGE_VSP_CONFIG_KEYS = new ConfigKey<?>[] { NuageVspConfigDns, NuageVspDnsExternal, NuageVspConfigGateway,
            NuageVspSharedNetworkDomainTemplateName, NuageVspVpcDomainTemplateName, NuageVspIsolatedNetworkDomainTemplateName };


    @Inject
    ResourceManager _resourceMgr;
    @Inject
    HostDetailsDao _hostDetailsDao;
    @Inject
    HostDao _hostDao;
    @Inject
    PhysicalNetworkDao _physicalNetworkDao;
    @Inject
    PhysicalNetworkServiceProviderDao _physicalNetworkServiceProviderDao;
    @Inject
    NetworkDao _networkDao;
    @Inject
    NetworkDetailsDao _networkDetailsDao;
    @Inject
    VpcOfferingDao _vpcOffDao;
    @Inject
    VpcOfferingServiceMapDao _vpcOffSvcMapDao;
    @Inject
    VpcDao _vpcDao;
    @Inject
    private VpcDetailsDao _vpcDetailsDao;
    @Inject
    NuageVspDao _nuageVspDao;
    @Inject
    DataCenterDao _dataCenterDao;
    @Inject
    ConfigurationDao _configDao;
    @Inject
    AgentManager _agentMgr;
    @Inject
    private DomainDao _domainDao;
    @Inject
    NetworkOfferingDao _networkOfferingDao;
    @Inject
    NetworkOfferingServiceMapDao _networkOfferingServiceMapDao;
    @Inject
    NuageVspEntityBuilder _nuageVspEntityBuilder;
    @Inject
    VlanDao _vlanDao;
    @Inject
    VlanDetailsDao _vlanDetailsDao;
    @Inject
    ResponseGenerator _responseGenerator;
    @Inject
    MessageBus _messageBus;
    @Inject
    VMInstanceDao _vmInstanceDao;
    @Inject
    NicDao _nicDao;
    @Inject
    NetworkModel _networkModel;

    static {
        Set<Network.Provider> nuageVspProviders = ImmutableSet.of(Network.Provider.NuageVsp);
        Set<Network.Provider> vrProviders = ImmutableSet.of(Network.Provider.VPCVirtualRouter);
        Set<Network.Provider> defaultLbProviders = ImmutableSet.of(Network.Provider.InternalLbVm);
        Set<Network.Provider> supportedLbProviders = ImmutableSet.of(Network.Provider.InternalLbVm);
        Set<Network.Provider> supportedUserDataProviders = ImmutableSet.of(Network.Provider.VPCVirtualRouter, Network.Provider.ConfigDrive);

        DEFAULT_NUAGE_VSP_VPC_SERVICE_MAP = ImmutableMultimap.<Network.Service, Network.Provider>builder()
                .putAll(Network.Service.Connectivity, nuageVspProviders)
                .putAll(Network.Service.Gateway, nuageVspProviders)
                .putAll(Network.Service.Dhcp, nuageVspProviders)
                .putAll(Network.Service.StaticNat, nuageVspProviders)
                .putAll(Network.Service.SourceNat, nuageVspProviders)
                .putAll(Network.Service.NetworkACL, nuageVspProviders)
                .putAll(Network.Service.UserData, vrProviders)
                .putAll(Network.Service.Lb, defaultLbProviders)
                .putAll(Network.Service.Dns, vrProviders)
                .build();

        Multimap<Network.Service, Network.Provider> builder = HashMultimap.create(DEFAULT_NUAGE_VSP_VPC_SERVICE_MAP);
        builder.putAll(Network.Service.UserData, supportedUserDataProviders);
        builder.putAll(Network.Service.Lb, supportedLbProviders);

        SUPPORTED_NUAGE_VSP_VPC_SERVICE_MAP = ImmutableMultimap.copyOf(builder);
    }

    private Listener _nuageVspResourceListener;

    @Override
    public List<Class<?>> getCommands() {
        return Lists.<Class<?>>newArrayList(
                AddNuageVspDeviceCmd.class,
                DeleteNuageVspDeviceCmd.class,
                UpdateNuageVspDeviceCmd.class,
                ListNuageVspDevicesCmd.class,

                DisableNuageUnderlayVlanIpRangeCmd.class,
                EnableNuageUnderlayVlanIpRangeCmd.class,
                ListNuageUnderlayVlanIpRangesCmd.class,

                ListNuageVspDomainTemplatesCmd.class,
                ListNuageVspGlobalDomainTemplateCmd.class,
                AssociateNuageVspDomainTemplateCmd.class
        );
    }

    @Override
    public NuageVspDeviceVO addNuageVspDevice(AddNuageVspDeviceCmd cmd) {
        final NuageVspResource resource = new NuageVspResource();
        final String deviceName = Network.Provider.NuageVsp.getName();
        ExternalNetworkDeviceManager.NetworkDevice networkDevice = ExternalNetworkDeviceManager.NetworkDevice.getNetworkDevice(deviceName);
        final Long physicalNetworkId = cmd.getPhysicalNetworkId();
        PhysicalNetworkVO physicalNetwork = _physicalNetworkDao.findById(physicalNetworkId);
        if (physicalNetwork == null) {
            throw new InvalidParameterValueException("Could not find phyical network with ID: " + physicalNetworkId);
        }
        long zoneId = physicalNetwork.getDataCenterId();

        final PhysicalNetworkServiceProviderVO ntwkSvcProvider = _physicalNetworkServiceProviderDao.findByServiceProvider(physicalNetwork.getId(),
                networkDevice.getNetworkServiceProvder());
        if (ntwkSvcProvider == null) {
            throw new CloudRuntimeException("Network Service Provider: " + networkDevice.getNetworkServiceProvder() + " is not enabled in the physical network: "
                    + physicalNetworkId + "to add this device");
        } else if (ntwkSvcProvider.getState() == PhysicalNetworkServiceProvider.State.Shutdown) {
            throw new CloudRuntimeException("Network Service Provider: " + ntwkSvcProvider.getProviderName() + " is in shutdown state in the physical network: "
                    + physicalNetworkId + "to add this device");
        }

        if (_nuageVspDao.listByPhysicalNetwork(physicalNetworkId).size() != 0) {
            throw new CloudRuntimeException("A NuageVsp device is already configured on this physical network");
        }

        // While the default VSD port is 8443, clustering via HAProxy will go over port 443 (CLOUD-58)
        int port = cmd.getPort() > 0 ? cmd.getPort() : 443;

        try {
            String apiVersion = null;

            String cmsUserPasswordBase64 = NuageVspUtil.encodePassword(cmd.getPassword());

            NuageVspResourceConfiguration resourceConfiguration = new NuageVspResourceConfiguration()
                    .guid(UUID.randomUUID().toString())
                    .zoneId(String.valueOf(physicalNetwork.getDataCenterId()))
                    .hostName(cmd.getHostName())
                    .cmsUser(cmd.getUserName())
                    .cmsUserPassword(cmsUserPasswordBase64)
                    .port(String.valueOf(port))
                    .apiVersion(NuageVspApiVersion.CURRENT.toString())
                    .retryCount(NuageVspConstants.DEFAULT_API_RETRY_COUNT.toString())
                    .retryInterval(NuageVspConstants.DEFAULT_API_RETRY_INTERVAL.toString())
                    .apiRelativePath("/nuage");

            VspHost vspHost = resourceConfiguration.buildVspHost();
            NuageVspPluginClientLoader clientLoader = NuageVspPluginClientLoader.getClientLoader(vspHost);
            VspApiDefaults apiDefaults = clientLoader.getNuageVspManagerClient().getApiDefaults();


            if (StringUtils.isNotBlank(cmd.getApiVersion())){
                apiVersion = cmd.getApiVersion();
                if (!clientLoader.getNuageVspManagerClient().isSupportedApiVersion(apiVersion)){
                    throw new CloudRuntimeException("Unsupported API version : " + cmd.getApiVersion());
                }
            } else {
                List<NuageVspApiVersion> supportedVsdVersions = clientLoader.getNuageVspManagerClient().getSupportedVersionList();
                supportedVsdVersions.retainAll(Arrays.asList(NuageVspApiVersion.SUPPORTED_VERSIONS));

                if(supportedVsdVersions.isEmpty()) {
                    throw new CloudRuntimeException("No supported API version found!");
                }

                supportedVsdVersions.sort(Comparator.reverseOrder());
                apiVersion = supportedVsdVersions.get(0).toString();
            }


            String retryCount = String.valueOf(MoreObjects.firstNonNull(cmd.getApiRetryCount(), apiDefaults.getRetryCount()));
            String retryInterval = String.valueOf(MoreObjects.firstNonNull(cmd.getApiRetryInterval(), apiDefaults.getRetryInterval()));

            resourceConfiguration
                    .apiVersion(apiVersion)
                    .apiRelativePath("/nuage/api/" + apiVersion)
                    .retryCount(retryCount)
                    .retryInterval(retryInterval);
            Map<String, String> hostDetails = resourceConfiguration.build();
            resource.configure("Nuage VSD - " + cmd.getHostName(), Maps.<String, Object>newHashMap(hostDetails));


            Host host = _resourceMgr.addHost(zoneId, resource, Host.Type.L2Networking, hostDetails);
            if (host == null) {
                throw new CloudRuntimeException("Failed to add Nuage Vsp Device due to internal error.");
            }

            NuageVspDeviceVO nuageVspDevice = new NuageVspDeviceVO(host.getId(), physicalNetworkId, ntwkSvcProvider.getProviderName(), deviceName);
            _nuageVspDao.persist(nuageVspDevice);

            DetailVO detail = new DetailVO(host.getId(), "nuagevspdeviceid", String.valueOf(nuageVspDevice.getId()));
            _hostDetailsDao.persist(detail);

            NuageVspDeviceVO matchingNuageVspDevice = findMatchingNuageVspDevice(nuageVspDevice);
            String cmsId;
            if (matchingNuageVspDevice != null) {
                cmsId = findNuageVspCmsIdForDeviceOrHost(matchingNuageVspDevice.getId(), matchingNuageVspDevice.getHostId());
            } else {
                SyncNuageVspCmsIdCommand syncCmd = new SyncNuageVspCmsIdCommand(SyncType.REGISTER, null);
                SyncNuageVspCmsIdAnswer answer = (SyncNuageVspCmsIdAnswer) _agentMgr.easySend(nuageVspDevice.getHostId(), syncCmd);
                if (answer != null && answer.getSuccess()) {
                    cmsId = answer.getNuageVspCmsId();
                } else {
                    throw new CloudRuntimeException("Failed to register CMS ID");
                }
            }

            host = findNuageVspHost(nuageVspDevice.getHostId());
            registerNewNuageVspDevice(host.getId(), cmsId);

            resourceConfiguration.nuageVspCmsId(cmsId);
            resource.configure(cmd.getHostName(), Maps.<String, Object>newHashMap(resourceConfiguration.build()));

            if (matchingNuageVspDevice == null) {
                auditDomainsOnVsp((HostVO) host, true);
            }

            return nuageVspDevice;
        } catch (ConfigurationException e) {
            s_logger.error("Failed to configure Nuage VSD resource " + cmd.getHostName(), e);
            throw new CloudRuntimeException("Failed to configure Nuage VSD resource " + cmd.getHostName(), e);
        } catch (NuageVspException ee) {
            s_logger.error("Failed to add Nuage VSP device " + cmd.getHostName(), ee);
            throw new CloudRuntimeException("Failed to add Nuage VSP device " + cmd.getHostName(), ee);
        }
    }

    @Override
    public NuageVspDeviceVO updateNuageVspDevice(UpdateNuageVspDeviceCmd command) {
        NuageVspResource resource = new NuageVspResource();
        final String deviceName = Network.Provider.NuageVsp.getName();
        ExternalNetworkDeviceManager.NetworkDevice networkDevice = ExternalNetworkDeviceManager.NetworkDevice.getNetworkDevice(deviceName);
        final Long physicalNetworkId = command.getPhysicalNetworkId();
        PhysicalNetworkVO physicalNetwork = _physicalNetworkDao.findById(physicalNetworkId);
        if (physicalNetwork == null) {
            throw new InvalidParameterValueException("Could not find phyical network with ID: " + physicalNetworkId);
        }

        final PhysicalNetworkServiceProviderVO ntwkSvcProvider = _physicalNetworkServiceProviderDao.findByServiceProvider(physicalNetwork.getId(),
                networkDevice.getNetworkServiceProvder());
        if (ntwkSvcProvider == null) {
            throw new CloudRuntimeException("Network Service Provider: " + networkDevice.getNetworkServiceProvder() + " is not enabled in the physical network: "
                    + physicalNetworkId + "to add this device");
        }
        if (ntwkSvcProvider.getState() == PhysicalNetworkServiceProvider.State.Shutdown) {
            throw new CloudRuntimeException("Network Service Provider: " + ntwkSvcProvider.getProviderName() + " is in shutdown state in the physical network: "
                    + physicalNetworkId + "to add this device");
        }

        HostVO nuageVspHost = null;
        NuageVspDeviceVO nuageVspDevice = null;
        List<NuageVspDeviceVO> nuageVspDevices = _nuageVspDao.listByPhysicalNetwork(physicalNetworkId);
        if (nuageVspDevices == null || nuageVspDevices.isEmpty()) {
            throw new CloudRuntimeException("Nuage VSD is not configured on physical network " + physicalNetworkId);
        } else {
            nuageVspDevice = nuageVspDevices.iterator().next();
            nuageVspHost = _hostDao.findById(nuageVspDevice.getHostId());
            _hostDao.loadDetails(nuageVspHost);
        }

        boolean resourceConfigurationChanged = false;
        NuageVspResourceConfiguration resourceConfiguration = NuageVspResourceConfiguration.fromConfiguration(nuageVspHost.getDetails());
        if (!Strings.isNullOrEmpty(command.getHostName()) &&
                !command.getHostName().equals(resourceConfiguration.hostName())) {
            resourceConfiguration.hostName(command.getHostName());
            resourceConfigurationChanged = true;
        }

        if (!Strings.isNullOrEmpty(command.getUserName()) &&
                !command.getUserName().equals(resourceConfiguration.cmsUser())) {
            resourceConfiguration.cmsUser(command.getUserName());
            resourceConfigurationChanged = true;
        }

        if (!Strings.isNullOrEmpty(command.getPassword())) {
            String encodedNewPassword = NuageVspUtil.encodePassword(command.getPassword());
            if (!encodedNewPassword.equals(resourceConfiguration.cmsUserPassword())) {
                resourceConfiguration.cmsUserPassword(encodedNewPassword);
                resourceConfigurationChanged = true;
            }
        }

        if (command.getPort() != null &&
                command.getPort() != Integer.parseInt(resourceConfiguration.port())) {
            resourceConfiguration.port(String.valueOf(command.getPort()));
            resourceConfigurationChanged = true;
        }

        String apiVersion = MoreObjects.firstNonNull(command.getApiVersion(), resourceConfiguration.apiVersion());
        NuageVspApiVersion apiVersionObj = NuageVspApiVersion.fromString(apiVersion);
        NuageVspApiVersion apiVersionCurrent = null;
        try {
            apiVersionCurrent = resourceConfiguration.getApiVersion();
        } catch (ConfigurationException e){
            throw new CloudRuntimeException("Current version is not configured correctly");
        }


        if(command.getApiVersion() != null){
            if(apiVersionObj.compareTo(apiVersionCurrent) < 0) {
                throw new CloudRuntimeException("Downgrading is not supported");
            }

            GetApiDefaultsCommand apiDefaultsCmd = new GetApiDefaultsCommand();
            GetApiDefaultsAnswer apiDefaultsAnswer = (GetApiDefaultsAnswer) _agentMgr.easySend(nuageVspHost.getId(), apiDefaultsCmd);

            SupportedApiVersionCommand supportedApiVersionCmd = new SupportedApiVersionCommand(apiVersion);
            Answer supportedApiVersionAnswer = _agentMgr.easySend(nuageVspHost.getId(), supportedApiVersionCmd);

            if (!supportedApiVersionAnswer.getResult()) {
                throw new CloudRuntimeException("Incorrect API version: Nuage plugin only supports " + apiDefaultsAnswer.getApiDefaults().getVersion());
            }

            String apiRelativePath = "/nuage/api/" + apiVersion;
            if (!apiRelativePath.equals(resourceConfiguration.apiRelativePath())) {
                resourceConfiguration.apiVersion(apiVersion);
                resourceConfiguration.apiRelativePath(apiRelativePath);
                resourceConfigurationChanged = true;
            }

        }

        if (command.getApiRetryCount() != null && resourceConfiguration.retryCount() != null) {
            final int retryCount = Integer.parseInt(resourceConfiguration.retryCount());
            if (command.getApiRetryCount() != retryCount) {
                resourceConfiguration.retryCount(String.valueOf(command.getApiRetryCount()));
                resourceConfigurationChanged = true;
            }
        }

        if (command.getApiRetryInterval() != null && resourceConfiguration.retryInterval() != null) {
            final int apiRetryInterval = Integer.parseInt(resourceConfiguration.retryInterval());
            if (command.getApiRetryInterval() != apiRetryInterval) {
                resourceConfiguration.retryInterval(String.valueOf(command.getApiRetryInterval()));
                resourceConfigurationChanged = true;
            }
        }

        if (!resourceConfigurationChanged) {
            if (s_logger.isDebugEnabled()) {
                s_logger.debug("No change in the NuageVsp device parameters. None of the NuageVsp device parameters are modified");
            }
            return nuageVspDevice;
        }

        Map<String, String> config = resourceConfiguration.build();
        try {
            resource.validate(config);

            UpdateNuageVspDeviceCommand cmd = new UpdateNuageVspDeviceCommand(resourceConfiguration);
            Answer answer = _agentMgr.easySend(nuageVspHost.getId(), cmd);
            if (answer == null || !answer.getResult()) {
                s_logger.error("UpdateNuageVspDeviceCommand failed");
                if ((null != answer) && (null != answer.getDetails())) {
                    throw new CloudRuntimeException(answer.getDetails());
                }
            }

            _hostDetailsDao.persist(nuageVspDevice.getHostId(), config);
        } catch (ConfigurationException e) {
            throw new CloudRuntimeException("Failed to update Nuage VSP device " + nuageVspDevice.getId() + " with parameters " + resourceConfiguration, e);
        }
        return nuageVspDevice;
    }

    @Override
    public NuageVspDeviceResponse createNuageVspDeviceResponse(NuageVspDeviceVO nuageVspDeviceVO) {
        HostVO nuageVspHost = _hostDao.findById(nuageVspDeviceVO.getHostId());
        _hostDao.loadDetails(nuageVspHost);

        NuageVspResourceConfiguration resourceConfiguration = NuageVspResourceConfiguration.fromConfiguration(nuageVspHost.getDetails());
        NuageVspDeviceResponse response = new NuageVspDeviceResponse();
        response.setDeviceName(nuageVspDeviceVO.getDeviceName());
        PhysicalNetwork pnw = ApiDBUtils.findPhysicalNetworkById(nuageVspDeviceVO.getPhysicalNetworkId());
        if (pnw != null) {
            response.setPhysicalNetworkId(pnw.getUuid());
        }
        response.setId(nuageVspDeviceVO.getUuid());
        response.setProviderName(nuageVspDeviceVO.getProviderName());
        response.setHostName(resourceConfiguration.hostName());
        response.setPort(Integer.parseInt(resourceConfiguration.port()));
        String apiRelativePath = resourceConfiguration.apiRelativePath();
        response.setApiVersion(apiRelativePath.substring(apiRelativePath.lastIndexOf('/') + 1));
        response.setApiRetryCount(Integer.parseInt(resourceConfiguration.retryCount()));
        response.setApiRetryInterval(Long.parseLong(resourceConfiguration.retryInterval()));
        response.setCmsId(resourceConfiguration.nuageVspCmsId());
        response.setObjectName("nuagevspdevice");
        return response;
    }

    @Override
    public boolean deleteNuageVspDevice(DeleteNuageVspDeviceCmd cmd) {
        Long nuageDeviceId = cmd.getNuageVspDeviceId();
        NuageVspDeviceVO nuageVspDevice = _nuageVspDao.findById(nuageDeviceId);
        if (nuageVspDevice == null) {
            throw new InvalidParameterValueException("Could not find a Nuage Vsp device with id " + nuageDeviceId);
        }

        // Find the physical network we work for
        Long physicalNetworkId = nuageVspDevice.getPhysicalNetworkId();
        PhysicalNetworkVO physicalNetwork = _physicalNetworkDao.findById(physicalNetworkId);
        if (physicalNetwork != null) {
            // Lets see if there are networks that use us
            // Find the nuage networks on this physical network
            List<NetworkVO> networkList = _networkDao.listByPhysicalNetwork(physicalNetworkId);

            // Networks with broadcast type lswitch are ours
            for (NetworkVO network : networkList) {
                if (network.getBroadcastDomainType() == Networks.BroadcastDomainType.Vsp) {
                    if ((network.getState() != Network.State.Shutdown) && (network.getState() != Network.State.Destroy)) {
                        throw new CloudRuntimeException("This Nuage Vsp device can not be deleted as there are one or more logical networks provisioned by Cloudstack.");
                    }
                }
            }
        }

        NuageVspDeviceVO matchingNuageVspDevice = findMatchingNuageVspDevice(nuageVspDevice);

        String nuageVspCmsId = findNuageVspCmsIdForDeviceOrHost(nuageVspDevice.getId(), nuageVspDevice.getHostId());
        if (matchingNuageVspDevice == null) {
            HostVO host = findNuageVspHost(nuageVspDevice.getHostId());
            if (!auditDomainsOnVsp(host, false)) {
                return false;
            }

            SyncNuageVspCmsIdCommand syncCmd = new SyncNuageVspCmsIdCommand(SyncType.UNREGISTER, nuageVspCmsId);
            SyncNuageVspCmsIdAnswer answer = (SyncNuageVspCmsIdAnswer) _agentMgr.easySend(nuageVspDevice.getHostId(), syncCmd);
            if (answer == null || !answer.getSuccess()) {
                return false;
            }
        }

        removeLegacyNuageVspDeviceCmsId(nuageVspDevice.getId());

        HostVO nuageHost = _hostDao.findById(nuageVspDevice.getHostId());
        Long hostId = nuageHost.getId();

        nuageHost.setResourceState(ResourceState.Maintenance);
        _hostDao.update(hostId, nuageHost);
        _resourceMgr.deleteHost(hostId, false, false);

        _nuageVspDao.remove(nuageDeviceId);
        return true;
    }

    private NuageVspDeviceVO findMatchingNuageVspDevice(NuageVspDeviceVO nuageVspDevice) {
        DetailVO nuageVspDeviceHost =  _hostDetailsDao.findDetail(nuageVspDevice.getHostId(), "hostname");
        String nuageVspDeviceHostName = (nuageVspDeviceHost != null) ? nuageVspDeviceHost.getValue(): null;

        List<NuageVspDeviceVO> otherNuageVspDevices = _nuageVspDao.listAll();
        for (NuageVspDeviceVO otherNuageVspDevice : otherNuageVspDevices) {
            if (otherNuageVspDevice.getId() == nuageVspDevice.getId()) continue;

            DetailVO otherNuageVspDeviceHostName = _hostDetailsDao.findDetail(otherNuageVspDevice.getHostId(), "hostname");
            if (otherNuageVspDeviceHostName != null && nuageVspDeviceHostName.equals(otherNuageVspDeviceHostName.getValue())) {
                return otherNuageVspDevice;
            }
        }
        return null;
    }

    @Override
    public List<NuageVspDeviceVO> listNuageVspDevices(ListNuageVspDevicesCmd cmd) {
        Long physicalNetworkId = cmd.getPhysicalNetworkId();
        Long nuageVspDeviceId = cmd.getNuageVspDeviceId();
        List<NuageVspDeviceVO> responseList = new ArrayList<NuageVspDeviceVO>();

        if (physicalNetworkId == null && nuageVspDeviceId == null) {
            throw new InvalidParameterValueException("Either physical network Id or Nuage device Id must be specified");
        }

        if (nuageVspDeviceId != null) {
            NuageVspDeviceVO nuageVspDevice = _nuageVspDao.findById(nuageVspDeviceId);
            if (nuageVspDevice == null) {
                throw new InvalidParameterValueException("Could not find Nuage Vsp device with id: " + nuageVspDeviceId);
            }
            responseList.add(nuageVspDevice);
        } else {
            PhysicalNetworkVO physicalNetwork = _physicalNetworkDao.findById(physicalNetworkId);
            if (physicalNetwork == null) {
                throw new InvalidParameterValueException("Could not find a physical network with id: " + physicalNetworkId);
            }
            responseList = _nuageVspDao.listByPhysicalNetwork(physicalNetworkId);
        }

        return responseList;
    }

    private void registerNewNuageVspDevice(long hostId, String cmsId) {
        DetailVO detail = new DetailVO(hostId, "nuagevspcmsid", cmsId);
        _hostDetailsDao.persist(detail);
    }

    @Deprecated
    private void removeLegacyNuageVspDeviceCmsId(long deviceId) {
        ConfigurationVO cmsIdConfig = _configDao.findByName(CMSID_CONFIG_KEY);
        if (cmsIdConfig != null) {
            if (!cmsIdConfig.getValue().contains(";") && cmsIdConfig.getValue().startsWith(deviceId + ":")) {
                _configDao.update(CMSID_CONFIG_KEY, "Advanced", "");
            } else {
                String newValue = cmsIdConfig.getValue().replace(String.format("(^|;)%d:[0-9a-f\\-]+;?", deviceId), ";");
                _configDao.update(CMSID_CONFIG_KEY, "Advanced", newValue);
            }
        }
    }

    public boolean executeSyncCmsId(NuageVspDeviceVO nuageVspDevice, SyncType syncType) {
        NuageVspDeviceVO matchingNuageVspDevice = findMatchingNuageVspDevice(nuageVspDevice);
        if (syncType == SyncType.REGISTER && matchingNuageVspDevice != null) {
            String cmsId = findNuageVspCmsIdForDeviceOrHost(matchingNuageVspDevice.getId(), matchingNuageVspDevice.getHostId());
            registerNewNuageVspDevice(nuageVspDevice.getHostId(), cmsId);
            return true;
        }

        String cmsId = findNuageVspCmsIdForDeviceOrHost(nuageVspDevice.getId(), nuageVspDevice.getHostId());

        SyncNuageVspCmsIdCommand syncCmd = new SyncNuageVspCmsIdCommand(syncType, cmsId);
        SyncNuageVspCmsIdAnswer answer = (SyncNuageVspCmsIdAnswer) _agentMgr.easySend(nuageVspDevice.getHostId(), syncCmd);
        if (answer != null) {
            if (answer.getSuccess()) {
                if (syncType == SyncType.REGISTER || answer.getSyncType() == SyncType.REGISTER) {
                    registerNewNuageVspDevice(nuageVspDevice.getHostId(), answer.getNuageVspCmsId());
                } else if (syncType == SyncType.UNREGISTER) {
                    removeLegacyNuageVspDeviceCmsId(nuageVspDevice.getId());
                }
            } else if (syncType == SyncType.AUDIT || syncType == SyncType.AUDIT_ONLY) {
                s_logger.fatal("Nuage VSP Device with ID " + nuageVspDevice.getId() + " is configured with an unknown CMS ID!");
            }
        }

        return answer != null && answer.getSuccess();
    }

    void auditHost(long hostId) {
        Host host = _hostDao.findById(hostId);
        auditHost((HostVO)host);
    }

    void auditHost(HostVO host) {
        if (host == null) return;

        _hostDao.loadDetails(host);

        boolean validateDomains = true;
        List<NuageVspDeviceVO> nuageVspDevices = _nuageVspDao.listByHost(host.getId());
        if (!CollectionUtils.isEmpty(nuageVspDevices)) {
            for (NuageVspDeviceVO nuageVspDevice : nuageVspDevices) {
                String nuageVspCmsId = findNuageVspCmsIdForDeviceOrHost(nuageVspDevice.getId(), nuageVspDevice.getHostId());
                SyncNuageVspCmsIdCommand syncCmd = new SyncNuageVspCmsIdCommand(SyncType.AUDIT, nuageVspCmsId);
                SyncNuageVspCmsIdAnswer answer = (SyncNuageVspCmsIdAnswer) _agentMgr.easySend(nuageVspDevice.getHostId(), syncCmd);

                if (answer != null && !answer.getSuccess()) {
                    s_logger.error("Nuage VSP Device with ID " + nuageVspDevice.getId() + " is configured with an unknown CMS ID!");
                    validateDomains = false;
                } else if (answer != null && answer.getSyncType() == SyncType.REGISTER) {
                    registerNewNuageVspDevice(nuageVspDevice.getHostId(), answer.getNuageVspCmsId());
                }
            }
        }

        if (validateDomains) {
            auditDomainsOnVsp(host, true);
        }
    }

    private boolean auditDomainsOnVsp(HostVO host, boolean add) {
        List<NuageVspDeviceVO> nuageVspDevices = _nuageVspDao.listByHost(host.getId());
        if (CollectionUtils.isEmpty(nuageVspDevices)) {
            return true;
        }

        final SyncDomainCommand.Type action = add ? SyncDomainCommand.Type.ADD : SyncDomainCommand.Type.REMOVE;

        _hostDao.loadDetails(host);
        List<DomainVO> allDomains = _domainDao.listAll();
        for (DomainVO domain : allDomains) {
            if (action == SyncDomainCommand.Type.REMOVE) {
                VspDomainCleanUp vspDomainCleanUp = _nuageVspEntityBuilder.buildVspDomainCleanUp(domain);
                CleanUpDomainCommand cmd = new CleanUpDomainCommand(vspDomainCleanUp);
                Answer answer = _agentMgr.easySend(host.getId(), cmd);
                if (!answer.getResult()) {
                    return false;
                }
            }

            VspDomain vspDomain = _nuageVspEntityBuilder.buildVspDomain(domain);
            SyncDomainCommand cmd = new SyncDomainCommand(vspDomain, action);
            Answer answer = _agentMgr.easySend(host.getId(), cmd);
            if (!answer.getResult()) {
                return false;
            }
        }
        return true;
    }

    private String findNuageVspCmsIdForDeviceOrHost(long deviceId, long hostId) {
        String cmsId = findNuageVspCmsIdForHostDevice(hostId);
        if(cmsId == null) {
            cmsId = findNuageVspCmsIdForDevice(deviceId);

            if (cmsId != null) {
                // Upgrade
                registerNewNuageVspDevice(hostId, cmsId);
                removeLegacyNuageVspDeviceCmsId(deviceId);
            }
        }

        return cmsId;
    }

    private String findNuageVspCmsIdForHostDevice(long hostId) {
        final DetailVO cmsIdDetailVO = _hostDetailsDao.findDetail(hostId, "nuagevspcmsid");
        if (cmsIdDetailVO != null) {
            return cmsIdDetailVO.getValue();
        }
        return null;
    }

    @Deprecated
    private String findNuageVspCmsIdForDevice(long deviceId) {
        ConfigurationVO cmsIdConfig = _configDao.findByName(CMSID_CONFIG_KEY);
        if(cmsIdConfig != null) {
            String configValue = cmsIdConfig.getValue();
            if (!Strings.isNullOrEmpty(configValue)) {
                String[] configuredNuageVspDevices = configValue.split(";");
                for (String configuredNuageVspDevice : configuredNuageVspDevices) {
                    if (configuredNuageVspDevice.startsWith(deviceId + ":")) {
                        String[] split = configuredNuageVspDevice.split(":");
                        if (split.length != 2 || (split.length > 1 && Strings.isNullOrEmpty(split[1]))) {
                            throw new IllegalArgumentException("The configured CMS ID for Nuage VSP device " + deviceId + " is in an incorrect format");
                        }
                        return split[1];
                    }
                }
            }
        }
        return null;
    }

    public List<String> getDnsDetails(long dataCenterId) {
        Boolean configureDns = Boolean.valueOf(_configDao.getValue(NuageVspManager.NuageVspConfigDns.key()));
        if (!configureDns) {
            return Lists.newArrayList();
        }

        Boolean configureExternalDns = Boolean.valueOf(_configDao.getValue(NuageVspManager.NuageVspDnsExternal.key()));
        DataCenterVO dc = _dataCenterDao.findById(dataCenterId);
        List<String> dnsServers = new ArrayList<String>();
        if (configureExternalDns) {
            if (!Strings.isNullOrEmpty(dc.getDns1())) {
                dnsServers.add(dc.getDns1());
            }
            if (!Strings.isNullOrEmpty(dc.getDns2())) {
                dnsServers.add(dc.getDns2());
            }
        } else {
            if (!Strings.isNullOrEmpty(dc.getInternalDns1())) {
                dnsServers.add(dc.getInternalDns1());
            }
            if (!Strings.isNullOrEmpty(dc.getInternalDns2())) {
                dnsServers.add(dc.getInternalDns2());
            }
        }
        return dnsServers;
    }

    public List<String> getGatewaySystemIds() {
        String gatewaySystemIds = String.valueOf(_configDao.getValue(NuageVspManager.NuageVspConfigGateway.key()));
        if (!Strings.isNullOrEmpty(gatewaySystemIds)) {
            return Lists.newArrayList(gatewaySystemIds.split(","));
        }
        return Lists.newArrayList();
    }


    @Override
    public List<NuageVspDomainTemplateResponse> listNuageVspDomainTemplates(ListNuageVspDomainTemplatesCmd cmd){
        long domainId;

        if (cmd.getDomainId() != null) {
            domainId = cmd.getDomainId();
        } else {
            domainId = CallContext.current().getCallingAccount().getDomainId();
        }

        return listNuageVspDomainTemplates(domainId, cmd.getKeyword(), cmd.getZoneId(), cmd.getPhysicalNetworkId());
    }

    @Override
    public List<NuageVspDomainTemplateResponse> listNuageVspDomainTemplates(long domainId, String keyword,  Long zoneId, Long passedPhysicalNetworkId) {
        Optional<Long> physicalNetworkId;
        Domain domain = _domainDao.findById(domainId);
        VspDomain vspDomain = _nuageVspEntityBuilder.buildVspDomain(domain);

        if (passedPhysicalNetworkId != null) {
            physicalNetworkId = Optional.of(passedPhysicalNetworkId);
        } else if (zoneId != null) {
            physicalNetworkId = Optional.ofNullable(getPhysicalNetworkBasedOnZone(zoneId));
        } else {
            throw new InvalidParameterValueException("No zoneid or physicalnetworkid specified.");
        }

        if (!physicalNetworkId.isPresent()) {
            return new LinkedList<>();
        }

        Long hostId = getNuageVspHostId(physicalNetworkId.get());
        if (hostId == null) {
            return new LinkedList<>();
        }

        ListVspDomainTemplatesCommand agentCmd = new ListVspDomainTemplatesCommand(vspDomain, keyword);
        ListVspDomainTemplatesAnswer answer = (ListVspDomainTemplatesAnswer) _agentMgr.easySend(hostId, agentCmd);
        List<VspDomainTemplate> domainTemplates = answer.getDomainTemplates();

        return domainTemplates.stream()
                       .map(NuageVspManagerImpl::createDomainTemplateResponse)
                       .collect(Collectors.toList());
    }

    private static NuageVspDomainTemplateResponse createDomainTemplateResponse(VspDomainTemplate dt) {
        return new NuageVspDomainTemplateResponse(dt.getName(), dt.getDescription());
    }

    /**
     * Returns the PhysicalNetworkId based on a zoneId
     * @param zoneId != null, the zone id for which we need to retrieve the PhysicalNetworkId
     * @return the physical network id if it's found otherwise null
     */
    private Long getPhysicalNetworkBasedOnZone(Long zoneId){

        Long physicalNetworkId = null;
        List<PhysicalNetworkVO> physicalNetworkVOs = _physicalNetworkDao.listByZoneAndTrafficType(zoneId, Networks.TrafficType.Guest);
        for (PhysicalNetworkVO physicalNetwok : physicalNetworkVOs) {
            if (physicalNetwok.getIsolationMethods().contains(NUAGE_VSP_ISOLATION)) {
                physicalNetworkId = physicalNetwok.getId();
                break;
            }
        }
        return physicalNetworkId;
    }

    @Override
    public void associateNuageVspDomainTemplate(AssociateNuageVspDomainTemplateCmd cmd){
        VpcVO vpc = _vpcDao.findById(cmd.getVpcId());
        Long physicalNetworkId;
        if (cmd.getPhysicalNetworkId() != null) {
            physicalNetworkId = cmd.getPhysicalNetworkId();
        } else if (cmd.getZoneId() != null) {
            physicalNetworkId = getPhysicalNetworkBasedOnZone(cmd.getZoneId());
        } else {
            throw new InvalidParameterValueException("No zoneid or physicalnetworkid specified.");
        }

        EntityExistsCommand entityCmd = new EntityExistsCommand(VpcVO.class, vpc.getUuid());
        boolean exists = entityExist(entityCmd, physicalNetworkId);
        if (exists) {
            throw new CloudRuntimeException("Failed to associate domain template, VPC is already pushed to the Nuage VSP device.");
        }

        if (!checkIfDomainTemplateExist(vpc.getDomainId(), cmd.getDomainTemplate(), cmd.getZoneId(), cmd.getPhysicalNetworkId())) {
            throw new InvalidParameterValueException("Could not find a Domain Template with name: " + cmd.getDomainTemplate());
        }
        setPreConfiguredDomainTemplateName(cmd.getVpcId(), cmd.getDomainTemplate());
    }

    @Override
    public boolean checkIfDomainTemplateExist(Long domainId, String domainTemplate, Long zoneId, Long physicalNetworkId){
        List<NuageVspDomainTemplateResponse> domainTemplateList = listNuageVspDomainTemplates(domainId, domainTemplate, zoneId, physicalNetworkId);
        if (domainTemplateList != null) {
            for (NuageVspDomainTemplateResponse val : domainTemplateList) {
                if (val.getName().equals(domainTemplate)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void updateBroadcastUri(Network network) throws InsufficientVirtualNetworkCapacityException {
        NetworkVO updatedNetwork = _networkDao.createForUpdate(network.getId());
        URI broadcastUri = calculateBroadcastUri(network);
        if (!broadcastUri.equals(network.getBroadcastUri())) {
            updatedNetwork.setBroadcastUri(broadcastUri);
            _networkDao.update(network.getId(), updatedNetwork);
        }
    }

    @Override
    public URI calculateBroadcastUri(Network network) throws InsufficientVirtualNetworkCapacityException {
        String vrIp = calculateVirtualRouterIp(network);
        return Networks.BroadcastDomainType.Vsp.toUri(network.getUuid() + "/" + vrIp);
    }

    private boolean usesVirtualRouter(long networkOfferingId) {
        return _networkOfferingServiceMapDao.isProviderForNetworkOffering(networkOfferingId, Network.Provider.VirtualRouter) ||
                _networkOfferingServiceMapDao.isProviderForNetworkOffering(networkOfferingId, Network.Provider.VPCVirtualRouter);
    }

    private String calculateVirtualRouterIp(Network network)
            throws InsufficientVirtualNetworkCapacityException {
        if (!usesVirtualRouter(network.getNetworkOfferingId())) {
            return null;
        }

        List<Pair<String, String>> ipAddressRanges =
                network.getGuestType() == Network.GuestType.Shared ? getSharedIpAddressRanges(network.getId()) : getIpAddressRanges(network);

        //check if a vr might be present already or not? CLOUD-1216 - before we always picked .2
        List<VMInstanceVO> vrs =_vmInstanceDao.listNonRemovedVmsByTypeAndNetwork(network.getId(), VirtualMachine.Type.DomainRouter);

        for (VMInstanceVO vr : vrs) {
            return _nicDao.listByVmIdAndNicIdAndNtwkId(vr.getId(), null, network.getId()).get(0).getIPv4Address();
        }

        ensureIpCapacity(network, ipAddressRanges);

        if(network.getGuestType() == Network.GuestType.Shared) {
            return ipAddressRanges.stream()
                                  .sorted(Comparator.comparingLong(p -> NetUtils.ip2Long(p.getLeft())))
                                  .findFirst()
                                  .map(Pair::getLeft)
                                  .orElseThrow(() -> new IllegalArgumentException("Shared network without ip ranges? How can this happen?"));
        }

        Network networkToCheck;
        if (isMigratingNetwork(network)) {
            networkToCheck = _networkDao.findById(network.getRelated());
        } else {
            networkToCheck = network;
        }

        Long freeIp = _networkModel.getAvailableIps(networkToCheck, null)
                                   .stream()
                                   .findFirst()
                                   .orElseThrow(() -> new InsufficientVirtualNetworkCapacityException("There is no free ip available for the VirtualRouter.",
                                                                                                      Network.class,
                                                                                                      network.getId()));

        return NetUtils.long2Ip(freeIp);
    }

    private List<Pair<String, String>> getSharedIpAddressRanges(long networkId) {
        List<VlanVO> vlans = _vlanDao.listVlansByNetworkId(networkId);
        List<Pair<String, String>> ipAddressRanges = Lists.newArrayList();
        for (VlanVO vlan : vlans) {
            Pair<String, String> ipAddressRange = NuageVspUtil.getIpAddressRange(vlan);
            if (ipAddressRange != null) {
                ipAddressRanges.add(ipAddressRange);
            }
        }
        return ipAddressRanges;
    }

    private List<Pair<String, String>> getIpAddressRanges(Network network) {
        List<Pair<String, String>> ipAddressRanges = Lists.newArrayList();
        String subnet = NetUtils.getCidrSubNet(network.getCidr());
        String netmask = NetUtils.getCidrNetmask(network.getCidr());
        long cidrSize = NetUtils.getCidrSize(netmask);
        Set<Long> allIPsInCidr = NetUtils.getAllIpsFromCidr(subnet, cidrSize, new HashSet<Long>());
        if (allIPsInCidr == null || !(allIPsInCidr instanceof TreeSet)) {
            throw new IllegalStateException("The IPs in CIDR for subnet " + subnet + " where null or returned in a non-ordered set.");
        }

        Iterator<Long> ipIterator = allIPsInCidr.iterator();
        long ip =  ipIterator.next();
        long gatewayIp = NetUtils.ip2Long(network.getGateway());
        String lastIp = NetUtils.getIpRangeEndIpFromCidr(subnet, cidrSize);
        if (gatewayIp == ip) {
            ip = ipIterator.next();
            ipAddressRanges.add(Pair.of(NetUtils.long2Ip(ip), lastIp));
        } else if (!network.getGateway().equals(lastIp)) {
            ipAddressRanges.add(Pair.of(NetUtils.long2Ip(ip), NetUtils.long2Ip(gatewayIp - 1)));
            ipAddressRanges.add(Pair.of(NetUtils.long2Ip(gatewayIp + 1), lastIp));
        } else {
            ipAddressRanges.add(Pair.of(NetUtils.long2Ip(ip), NetUtils.long2Ip(gatewayIp - 1)));
        }

        return ipAddressRanges;
    }

    private void ensureIpCapacity(Network network, List<Pair<String, String>> ipAddressRanges) throws InsufficientVirtualNetworkCapacityException {
        long ipCount = ipAddressRanges.stream()
                                      .mapToLong(this::getIpCount)
                                      .sum();

        if (ipCount == 0) {
            throw new InsufficientVirtualNetworkCapacityException("VSP allocates an IP for VirtualRouter." + " But no ip address ranges are specified", Network.class,
                                                                  network.getId());
        } else if (ipCount < 3) {
            throw new InsufficientVirtualNetworkCapacityException("VSP allocates an IP for VirtualRouter." + " So, subnet should have atleast minimum 3 hosts", Network.class,
                                                                  network.getId());
        }
    }

    private boolean isMigratingNetwork(Network network) {
        return network.getRelated() != network.getId();
    }

    private long getIpCount(Pair<String, String> ipAddressRange) {
        return NetUtils.ip2Long(ipAddressRange.getRight()) - NetUtils.ip2Long(ipAddressRange.getLeft()) + 1;
    }

    @Override
    public boolean entityExist(EntityExistsCommand cmd, Long physicalNetworkId){
        Long hostId = getNuageVspHostId(physicalNetworkId);
        if (hostId == null) {
            throw new CloudRuntimeException("There is no Nuage VSP device configured on physical network " + physicalNetworkId);
        }

        Answer answer = _agentMgr.easySend(hostId, cmd);
        if (answer != null) {
            return answer.getResult();
        }
        throw new CloudRuntimeException("No answer received from the client");
    }

    /**
     * Sets the preconfigured domain template of a vpc to the given value.
     * @param vpcId
     * @param domainTemplateName
     */
    private void setPreConfiguredDomainTemplateName(long vpcId, String domainTemplateName) {
        //remove the previous nuageDomainTemplate if it is present.
        if (_vpcDetailsDao.findDetail(vpcId, NuageVspManager.nuageDomainTemplateDetailName) != null) {
           _vpcDetailsDao.removeDetail(vpcId, NuageVspManager.nuageDomainTemplateDetailName);
        }
        VpcDetailVO vpcDetail = new VpcDetailVO(vpcId, NuageVspManager.nuageDomainTemplateDetailName, domainTemplateName, false);
        _vpcDetailsDao.persist(vpcDetail);
    }

    @Override
    public void setPreConfiguredDomainTemplateName(Network network, String domainTemplateName) {

        if (network.getVpcId() != null) {
            setPreConfiguredDomainTemplateName(network.getVpcId(), domainTemplateName);
        } else {
            NetworkDetailVO networkDetail = new NetworkDetailVO(network.getId(), NuageVspManager.nuageDomainTemplateDetailName, domainTemplateName, false);
            _networkDetailsDao.persist(networkDetail);
        }
    }

    @Override
    public String getPreConfiguredDomainTemplateName(Network network) {

        if (network.getVpcId() != null) {
            VpcDetailVO domainTemplateNetworkDetail = _vpcDetailsDao.findDetail(network.getVpcId(), NuageVspManager.nuageDomainTemplateDetailName);
            if (domainTemplateNetworkDetail != null) {
               return domainTemplateNetworkDetail.getValue();
            }

            return NuageVspVpcDomainTemplateName.value();
        } else {
            NetworkDetailVO domainTemplateNetworkDetail = _networkDetailsDao.findDetail(network.getId(), NuageVspManager.nuageDomainTemplateDetailName);
            if (domainTemplateNetworkDetail != null) {
                return domainTemplateNetworkDetail.getValue();
            }

            if (network.getGuestType() == Network.GuestType.Shared) {
                return NuageVspSharedNetworkDomainTemplateName.value();
            }

            return NuageVspIsolatedNetworkDomainTemplateName.value();
        }
    }

    @Override
    public HostVO getNuageVspHost(long physicalNetworkId) {
        HostVO nuageVspHost;
        List<NuageVspDeviceVO> nuageVspDevices = _nuageVspDao.listByPhysicalNetwork(physicalNetworkId);
        if (CollectionUtils.isEmpty(nuageVspDevices)) {
            // Perhaps another physical network is passed from within the same zone, find the VSP physical network in that case
            PhysicalNetwork physicalNetwork = _physicalNetworkDao.findById(physicalNetworkId);
            List<PhysicalNetworkVO> physicalNetworksInZone = _physicalNetworkDao.listByZone(physicalNetwork.getDataCenterId());
            for (PhysicalNetworkVO physicalNetworkInZone : physicalNetworksInZone) {
                if (physicalNetworkInZone.getIsolationMethods().contains(NUAGE_VSP_ISOLATION)) {
                    nuageVspDevices = _nuageVspDao.listByPhysicalNetwork(physicalNetworkInZone.getId());
                    break;
                }
            }
        }

        if (CollectionUtils.isNotEmpty(nuageVspDevices)) {
            NuageVspDeviceVO config = nuageVspDevices.iterator().next();
            nuageVspHost = _hostDao.findById(config.getHostId());
            _hostDao.loadDetails(nuageVspHost);
        } else {
            throw new CloudRuntimeException("There is no Nuage VSP device configured on physical network " + physicalNetworkId);
        }
        return nuageVspHost;
    }

    @Override
    public boolean updateNuageUnderlayVlanIpRange(long vlanIpRangeId, boolean enabled) {
        VlanVO staticNatVlan = _vlanDao.findById(vlanIpRangeId);
        HostVO nuageVspHost = getNuageVspHost(staticNatVlan.getPhysicalNetworkId());
        EntityExistsCommand<Vlan> cmd = new EntityExistsCommand<Vlan>(Vlan.class, staticNatVlan.getUuid());
        Answer answer = _agentMgr.easySend(nuageVspHost.getId(), cmd);
        if (answer != null && !answer.getResult()) {
            _vlanDetailsDao.addDetail(staticNatVlan.getId(), NuageVspManager.nuageUnderlayVlanIpRangeDetailKey, String.valueOf(enabled), false);
            return true;
        }

        return false;
    }

    @Override
    public List<NuageVlanIpRangeResponse> filterNuageVlanIpRanges(List<? extends Vlan> vlanIpRanges, Boolean underlay) {
        List<NuageVlanIpRangeResponse> nuageVlanIpRanges = Lists.newArrayList();
        for (Vlan vlanIpRange : vlanIpRanges) {
            NuageVlanIpRangeResponse nuageVlanIpRange = (NuageVlanIpRangeResponse) _responseGenerator.createVlanIpRangeResponse(NuageVlanIpRangeResponse.class, vlanIpRange);

            VlanDetailsVO nuageUnderlayDetail = _vlanDetailsDao.findDetail(vlanIpRange.getId(), NuageVspManager.nuageUnderlayVlanIpRangeDetailKey);
            boolean underlayEnabled = nuageUnderlayDetail != null && nuageUnderlayDetail.getValue().equalsIgnoreCase(String.valueOf(true));
            nuageVlanIpRange.setUnderlay(underlayEnabled);
            if (underlay == null || underlayEnabled == underlay) {
                nuageVlanIpRanges.add(nuageVlanIpRange);
            }
            nuageVlanIpRange.setObjectName("nuagevlaniprange");
        }
        return nuageVlanIpRanges;
    }

    @Override
    public boolean preStateTransitionEvent(Status oldState, Status.Event event, Status newState, Host host, boolean status, Object opaque) {
        return true;
    }

    @Override
    public boolean postStateTransitionEvent(StateMachine2.Transition<Status, Status.Event> transition, Host vo, boolean status, Object opaque) {
        // Whenever a Nuage VSP Host comes up, check if all CS domains are present and check if the CMS ID is valid
        if (transition.getToState() == Status.Up && vo instanceof HostVO) {
            auditHost((HostVO) vo);
        }
        return true;
    }

    @Override
    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
        initMessageBusListeners();
        initNuageVspResourceListeners();
        initNuageVspVpcOffering();
        Status.getStateMachine().registerListener(this);
        return true;
    }

    @DB
    private void initMessageBusListeners() {
        // Create corresponding enterprise and profile in VSP when creating a CS Domain
        _messageBus.subscribe(DomainManager.MESSAGE_ADD_DOMAIN_EVENT, (senderAddress, subject, args) -> {
            Long domainId = (Long) args;
            Domain domain = _domainDao.findById(domainId);

            try {
                _domainDao.acquireInLockTable(domain.getId());

                List<NuageVspDeviceVO> nuageVspDevices = _nuageVspDao.listAll();
                for (NuageVspDeviceVO nuageVspDevice : nuageVspDevices) {
                    VspDomain vspDomain = _nuageVspEntityBuilder.buildVspDomain(domain);
                    SyncDomainCommand cmd = new SyncDomainCommand(vspDomain, SyncDomainCommand.Type.ADD);
                    _agentMgr.easySend(nuageVspDevice.getHostId(), cmd);
                }
            } finally {
                _domainDao.releaseFromLockTable(domain.getId());
            }
        });

        // Clean up corresponding resources in VSP when deleting a CS Domain
        _messageBus.subscribe(DomainManager.MESSAGE_PRE_REMOVE_DOMAIN_EVENT, (senderAddress, subject, args) -> {
            DomainVO domain = (DomainVO) args;
            List<NuageVspDeviceVO> nuageVspDevices = _nuageVspDao.listAll();
            for (NuageVspDeviceVO nuageVspDevice : nuageVspDevices) {
                VspDomainCleanUp vspDomainCleanUp = _nuageVspEntityBuilder.buildVspDomainCleanUp(domain);
                CleanUpDomainCommand cmd = new CleanUpDomainCommand(vspDomainCleanUp);
                _agentMgr.easySend(nuageVspDevice.getHostId(), cmd);
            }
        });

        // Delete corresponding enterprise and profile in VSP when deleting a CS Domain
        _messageBus.subscribe(DomainManager.MESSAGE_REMOVE_DOMAIN_EVENT, (senderAddress, subject, args) -> {
            DomainVO domain = (DomainVO) args;
            List<NuageVspDeviceVO> nuageVspDevices = _nuageVspDao.listAll();
            for (NuageVspDeviceVO nuageVspDevice : nuageVspDevices) {
                VspDomain vspDomain = _nuageVspEntityBuilder.buildVspDomain(domain);
                SyncDomainCommand syncCmd = new SyncDomainCommand(vspDomain, SyncDomainCommand.Type.REMOVE);
                _agentMgr.easySend(nuageVspDevice.getHostId(), syncCmd);
            }
        });
    }

    private class NuageVspResourceListener extends AbstractListener {
        @Override
        public boolean processCommands(long agentId, long seq, Command[] commands) {
            if (commands != null && commands.length == 1) {
                Command command = commands[0];
                if (command instanceof PingNuageVspCommand) {
                    PingNuageVspCommand pingNuageVspCommand = (PingNuageVspCommand)command;
                    if (pingNuageVspCommand.shouldAudit()) {
                        auditHost(pingNuageVspCommand.getHostId());
                    }
                }
            }
            return true;
        }
    }

    @DB
    private void initNuageVspResourceListeners() {
        _agentMgr.registerForHostEvents(new NuageVspResourceListener(), false, true, false);
    }

    private Long getNuageVspHostId(long physicalNetworkId) {
        List<NuageVspDeviceVO> nuageVspDevices = _nuageVspDao.listByPhysicalNetwork(physicalNetworkId);
        if (nuageVspDevices != null && (!nuageVspDevices.isEmpty())) {
            NuageVspDeviceVO config = nuageVspDevices.iterator().next();
            return config.getHostId();
        }

        return null;
    }

    @DB
    private void initNuageVspVpcOffering() {
        //configure default Nuage VSP vpc offering
        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                VpcOffering offering = _vpcOffDao.findByUniqueName(nuageVPCOfferingName);
                if (offering == null) {
                    if (s_logger.isDebugEnabled()) {
                        s_logger.debug("Creating default Nuage VPC offering " + nuageVPCOfferingName);
                    }

                    createVpcOffering(nuageVPCOfferingName, nuageVPCOfferingDisplayText, DEFAULT_NUAGE_VSP_VPC_SERVICE_MAP, true, VpcOffering.State.Enabled, null);
                } else {
                    updateVpcOffering(offering, DEFAULT_NUAGE_VSP_VPC_SERVICE_MAP);
                }
            }
        });
    }

    @DB
    protected VpcOffering createVpcOffering(final String name, final String displayText, final Multimap<Network.Service, Network.Provider> svcProviderMap, final boolean isDefault,
                                            final VpcOffering.State state, final Long serviceOfferingId) {
        return Transaction.execute((TransactionCallback<VpcOffering>)status -> createVpcOfferingInTransaction(name, displayText, svcProviderMap, isDefault, state, serviceOfferingId));
    }

    private VpcOffering createVpcOfferingInTransaction(String name, String displayText, Multimap<Network.Service, Network.Provider> svcProviderMap, boolean isDefault,
            VpcOffering.State state, Long serviceOfferingId) {
        // create vpc offering object
        VpcOfferingVO offering = new VpcOfferingVO(name, displayText, isDefault, serviceOfferingId, false, false);

        if (state != null) {
            offering.setState(state);
        }
        if (s_logger.isDebugEnabled()) {
            s_logger.debug(String.format("Adding vpc offering %s", offering));
        }
        offering = _vpcOffDao.persist(offering);
        // populate services and providers
        if (svcProviderMap != null) {
            for (Map.Entry<Network.Service, Network.Provider> entry : svcProviderMap.entries()) {
                Network.Service service = entry.getKey();
                Network.Provider provider = entry.getValue();

                VpcOfferingServiceMapVO offService = new VpcOfferingServiceMapVO(offering.getId(), service, provider);
                _vpcOffSvcMapDao.persist(offService);
                if (s_logger.isTraceEnabled()) {
                    s_logger.trace(String.format("Added service for the vpc offering: %s with provider %s", offService, provider.getName()));
                }
            }
        }

        return offering;
    }

    @DB
    protected void updateVpcOffering(final VpcOffering offering, final Multimap<Network.Service, Network.Provider> svcProviderMap) {
        Transaction.execute((TransactionCallback<VpcOffering>)status -> updateVpcOfferingInTransaction(offering, svcProviderMap));
    }

    @Nonnull
    private VpcOffering updateVpcOfferingInTransaction(VpcOffering offering, Multimap<Network.Service, Network.Provider> svcProviderMap) {
        if (s_logger.isDebugEnabled()) {
            s_logger.debug(String.format("Updating vpc offering %s", offering));
        }

        List<VpcOfferingServiceMapVO> currentVpcOfferingServices = _vpcOffSvcMapDao.listByVpcOffId(offering.getId());
        Multimap<Network.Service, Network.Provider> currentSvcProviderMap = HashMultimap.create();
        for (VpcOfferingServiceMapVO vpcOfferingService : currentVpcOfferingServices) {
            Network.Service service = Network.Service.getService(vpcOfferingService.getService());
            Network.Provider provider = Network.Provider.getProvider(vpcOfferingService.getProvider());
            currentSvcProviderMap.put(service, provider);
        }

        for (Map.Entry<Network.Service, Network.Provider> entry : svcProviderMap.entries()) {
            Network.Service service = entry.getKey();
            Network.Provider provider = entry.getValue();

            if (!currentSvcProviderMap.containsEntry(service, provider)) {
                VpcOfferingServiceMapVO offService = new VpcOfferingServiceMapVO(offering.getId(), service, provider);
                _vpcOffSvcMapDao.persist(offService);
                if (s_logger.isDebugEnabled()) {
                    s_logger.debug(String.format("Added service for the vpc offering: %s", offService));
                }
            }

        }

        return offering;
    }

    private HostVO findNuageVspHost(long hostId) {
        HostVO host = _hostDao.findById(hostId);
        _hostDao.loadDetails(host);
        return host;
    }

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

    @Override
    public ConfigKey<?>[] getConfigKeys() {
        return Arrays.copyOf(NUAGE_VSP_CONFIG_KEYS, NUAGE_VSP_CONFIG_KEYS.length);
    }
}
