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

import java.util.List;

import javax.inject.Inject;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.cloud.entity.api.db.VMNetworkMapVO;
import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.resourcedetail.FirewallRuleDetailVO;
import org.apache.cloudstack.resourcedetail.VpcDetailVO;
import org.apache.cloudstack.resourcedetail.dao.FirewallRuleDetailsDao;
import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao;

import com.cloud.configuration.Resource;
import com.cloud.dc.DataCenter;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeployDestination;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.Host;
import com.cloud.host.dao.HostDao;
import com.cloud.network.dao.FirewallRulesCidrsDao;
import com.cloud.network.dao.FirewallRulesCidrsVO;
import com.cloud.network.dao.FirewallRulesDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkDetailVO;
import com.cloud.network.dao.NetworkDetailsDao;
import com.cloud.network.dao.NetworkDomainDao;
import com.cloud.network.dao.NetworkDomainVO;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.RouterNetworkDao;
import com.cloud.network.dao.RouterNetworkVO;
import com.cloud.network.guru.NetworkGuru;
import com.cloud.network.router.VirtualRouter;
import com.cloud.network.rules.FirewallRule;
import com.cloud.network.rules.FirewallRuleVO;
import com.cloud.network.rules.RulesManager;
import com.cloud.network.vpc.NetworkACLVO;
import com.cloud.network.vpc.Vpc;
import com.cloud.network.vpc.VpcGatewayVO;
import com.cloud.network.vpc.VpcManager;
import com.cloud.network.vpc.VpcService;
import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.NetworkACLDao;
import com.cloud.network.vpc.dao.VpcDao;
import com.cloud.network.vpc.dao.VpcGatewayDao;
import com.cloud.offering.NetworkOffering;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
import com.cloud.server.ResourceTag;
import com.cloud.tags.ResourceTagVO;
import com.cloud.tags.dao.ResourceTagDao;
import com.cloud.user.Account;
import com.cloud.user.AccountVO;
import com.cloud.user.ResourceLimitService;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallback;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.DomainRouterVO;
import com.cloud.vm.Nic;
import com.cloud.vm.NicDetailVO;
import com.cloud.vm.NicProfile;
import com.cloud.vm.NicVO;
import com.cloud.vm.ReservationContext;
import com.cloud.vm.ReservationContextImpl;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VirtualMachineProfileImpl;
import com.cloud.vm.dao.DomainRouterDao;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.NicDetailsDao;
import com.cloud.vm.dao.NicIpAliasDao;
import com.cloud.vm.dao.NicSecondaryIpDao;
import com.cloud.vm.dao.UserVmDao;

public class NetworkMigrationManagerImpl implements NetworkMigrationManager {
    protected Logger logger = LogManager.getLogger(getClass());

    @Inject
    private DataCenterDao _dcDao = null;
    @Inject
    private NetworkDetailsDao _networkDetailsDao = null;
    @Inject
    private AccountDao _accountDao = null;
    @Inject
    private NetworkDomainDao _networkDomainDao = null;
    @Inject
    private NetworkOrchestrationService _networkMgr = null;
    @Inject
    private ResourceLimitService _resourceLimitMgr = null;
    @Inject
    private NetworkDao _networksDao = null;
    @Inject
    private NetworkOfferingDao _networkOfferingDao = null;
    @Inject
    private NetworkOfferingServiceMapDao _networkOfferingServiceDao = null;
    @Inject
    private NicDao _nicDao = null;
    @Inject
    private NicSecondaryIpDao _nicSecondaryIpDao = null;
    @Inject
    private NicIpAliasDao _nicIpAliasDao = null;
    @Inject
    private NicDetailsDao _nicDetailsDao = null;
    @Inject
    private FirewallRulesDao _firewallDao = null;
    @Inject
    private FirewallRulesCidrsDao _firewallRulesCidrDao = null;
    @Inject
    private FirewallRuleDetailsDao _firewallRuleDetailsDao = null;
    @Inject
    private EntityManager _entityMgr = null;
    @Inject
    private RouterNetworkDao _routerNetworkDao = null;
    @Inject
    private DomainRouterDao _routerDao = null;
    @Inject
    private NetworkService _networkService = null;
    @Inject
    private UserVmDao _vmDao = null;
    @Inject
    private NetworkModel _networkModel= null;
    @Inject
    private VMNetworkMapDao _vmNetworkMapDao = null;
    @Inject
    private HostDao _hostDao = null;
    @Inject
    private VirtualMachineManager _itMgr = null;
    @Inject
    private IPAddressDao _ipAddressDao = null;
    @Inject
    private RulesManager _rulesMgr = null;
    @Inject
    private VpcDao _vpcDao = null;
    @Inject
    private VpcDetailsDao _vpcDetailsDao = null;
    @Inject
    private IpAddressManager _ipAddressManager = null;
    @Inject
    private VpcService _vpcService = null;
    @Inject
    private NetworkACLDao _networkACLDao = null;
    @Inject
    private VpcManager _vpcManager = null;
    @Inject
    private VpcGatewayDao _vpcGatewayDao = null;
    @Inject
    private ResourceTagDao _resourceTagDao = null;

    @Override public long makeCopyOfNetwork(Network network, NetworkOffering networkOffering, Long vpcId) {
        if (logger.isDebugEnabled()) {
            logger.debug("Making a copy of network {} for migration.", network);
        }
        long originalNetworkId = network.getId();
        NetworkDomainVO domainNetworkMapByNetworkId = _networkDomainDao.getDomainNetworkMapByNetworkId(originalNetworkId);
        AccountVO networkAccount = _accountDao.findById(network.getAccountId());

        boolean subdomainAccess = true;
        if (domainNetworkMapByNetworkId != null) {
            subdomainAccess = domainNetworkMapByNetworkId.isSubdomainAccess();
        }
        DataCenterDeployment plan = new DataCenterDeployment(network.getDataCenterId(), null, null, null, null, network.getPhysicalNetworkId());

        List<? extends Network> networks = _networkMgr.setupNetwork(networkAccount, networkOffering, network, plan, network.getName(), network.getDisplayText(), true,
                                                                    network.getDomainId(), network.getAclType(), subdomainAccess,
                                                                    vpcId, true);
        _resourceLimitMgr.incrementResourceCount(network.getAccountId(), Resource.ResourceType.network, network.isDisplay());

        long networkCopyId;
        if (networks == null || networks.isEmpty()) {
            throw new CloudRuntimeException("Fail to create a network");
        } else {
            DataCenter zone = _dcDao.findById(network.getDataCenterId());
            String guestNetworkCidr = zone.getGuestNetworkCidr();

            if (networks.get(0).getGuestType() == Network.GuestType.Isolated
                    && networks.get(0).getTrafficType() == Networks.TrafficType.Guest) {
                Network networkCopy = networks.get(0);
                for (final Network nw : networks) {
                    if (nw.getCidr() != null && nw.getCidr().equals(guestNetworkCidr)) {
                        networkCopy = nw;
                    }
                }
                networkCopyId = networkCopy.getId();
            } else {
                // For shared network
                networkCopyId = networks.get(0).getId();
            }
        }

        //Update the related network
        NetworkVO originalNetwork = _networksDao.findById(originalNetworkId);
        originalNetwork.setRelated(networkCopyId);
        _networksDao.update(originalNetworkId, originalNetwork);

        NetworkVO copiedNetwork = _networksDao.findById(networkCopyId);
        copiedNetwork.setRelated(originalNetworkId);
        copiedNetwork.setDisplayNetwork(false);
        copiedNetwork.setBroadcastUri(network.getBroadcastUri());
        copiedNetwork.setState(network.getState());
        _networksDao.update(networkCopyId, copiedNetwork);

        copyNetworkDetails(originalNetworkId, networkCopyId);
        copyFirewallRulesToNewNetwork(network, copiedNetwork);
        assignUserNicsToNewNetwork(originalNetworkId, networkCopyId);
        assignRouterNicsToNewNetwork(network.getId(), networkCopyId);

        if (logger.isDebugEnabled()) {
            logger.debug("Successfully created a copy of network  " + originalNetwork.getName() + "(" + originalNetwork.getUuid() + ") id is " + originalNetwork.getId() + " for migration. The network copy has uuid " + network.getUuid() + " and id " + network.getId());
        }
        return networkCopyId;
    }

    @DB
    private void copyNetworkDetails(long srcNetworkId, long dstNetworkId) {
        List<NetworkDetailVO> networkDetails = _networkDetailsDao.listDetails(srcNetworkId);

        for (NetworkDetailVO networkDetail : networkDetails) {
            _networkDetailsDao.persist(new NetworkDetailVO(dstNetworkId, networkDetail.getName(), networkDetail.getValue(), networkDetail.isDisplay()));
        }
    }

    /**
     * reassigns the nics to the new network from the src network.
     * @param srcNetworkId
     * @param dstNetworkId
     */
    private void assignUserNicsToNewNetwork(long srcNetworkId, long dstNetworkId) {
        List<NicVO> nics = _nicDao.listByNetworkId(srcNetworkId);

        for (NicVO nic : nics) {
            if (nic.getVmType() == VirtualMachine.Type.User) {
                nic.setNetworkId(dstNetworkId);
                _nicDao.persist(nic);

                //update the number of active nics in both networks after migration.
                if (nic.getState() == Nic.State.Reserved) {
                    _networksDao.changeActiveNicsBy(srcNetworkId, -1);
                    _networksDao.changeActiveNicsBy(dstNetworkId, 1);
                }
            }
        }

        List<? extends IPAddressVO> publicIps = _ipAddressDao.listByAssociatedNetwork(srcNetworkId, null);

        for (IPAddressVO ipAddress : publicIps) {
                ipAddress.setAssociatedWithNetworkId(dstNetworkId);
                _ipAddressDao.persist(ipAddress);
        }
    }

    @Override
    public Long makeCopyOfVpc(long vpcId, long vpcOfferingId) {
        VpcVO vpc = _vpcDao.findById(vpcId);
        if (logger.isDebugEnabled()) {
            logger.debug("Making a copy of vpc {} for migration.", vpc);
        }
        if (vpc == null) {
            InvalidParameterValueException ex = new InvalidParameterValueException("Specified vpc id doesn't exist in the system");
            ex.addProxyObject(String.valueOf(vpcId), "vpcId");
            throw ex;
        }

        Vpc copyOfVpc;
        long copyOfVpcId;
        try {

            copyOfVpc = _vpcService.createVpc(vpc.getZoneId(), vpcOfferingId, vpc.getAccountId(), vpc.getName(),
                    vpc.getDisplayText(), vpc.getCidr(), vpc.getNetworkDomain(), vpc.getIp4Dns1(), vpc.getIp4Dns2(),
                    vpc.getIp6Dns1(), vpc.getIp6Dns2(), vpc.isDisplay(), vpc.getPublicMtu(), null, null, null);

            copyOfVpcId = copyOfVpc.getId();
            //on resume of migration the uuid will be swapped already. So the copy will have the value of the original vpcid.
            _resourceTagDao.persist(new ResourceTagVO(MIGRATION, Long.toString(vpcId), vpc.getAccountId(), vpc.getDomainId(), copyOfVpcId, ResourceTag.ResourceObjectType.Vpc, null, vpc.getUuid()));
            VpcVO copyVpcVO = _vpcDao.findById(copyOfVpcId);
            vpc.setDisplay(false);
            swapUuids(vpc, copyVpcVO);
            reassignACLRulesToNewVpc(vpcId, copyOfVpcId);
            reassignPublicIpsToNewVpc(vpcId, copyOfVpc);
            copyVpcDetails(vpcId, copyOfVpcId);
            reassignGatewayToNewVpc(vpcId, copyOfVpcId);
            copyVpcResourceTagsToNewVpc(vpcId, copyOfVpcId);
            if (logger.isDebugEnabled()) {
                logger.debug("Successfully created a copy of network  " + vpc.getName() + "(" + vpc.getUuid() + ") id is " + vpc.getId() + " for migration. The network copy has uuid " + copyVpcVO.getUuid() + " and id " + copyOfVpc.getId());
            }
        } catch (ResourceAllocationException e) {
            throw new CloudRuntimeException(e.getMessage());
        }

        return copyOfVpcId;
    }

    @Override
    public void startVpc(Vpc vpc) {
        if (vpc.getState() != Vpc.State.Enabled) {
            try {
                _vpcService.startVpc(vpc.getId(), true);
            } catch (ResourceUnavailableException | InsufficientCapacityException e) {
                logger.error("Vpc can not be started. Aborting migration process");
                throw new CloudRuntimeException("Vpc can not be started.", e);
            }
        }
    }


    private void copyVpcDetails(long srcVpcId, long dstVpcId) {
        List<VpcDetailVO> vpcDetails = _vpcDetailsDao.listDetails(srcVpcId);

        for (VpcDetailVO vpcDetail : vpcDetails) {
            _vpcDetailsDao.persist(new VpcDetailVO(dstVpcId, vpcDetail.getName(), vpcDetail.getValue(), vpcDetail.isDisplay()));
        }
    }

    /**
     * Reassign the ACL rules from the original vpc to the new VPC
     * @param srcVpcId
     * @param dstVpcId
     */
    private void reassignACLRulesToNewVpc(long srcVpcId, long dstVpcId){
        List<NetworkACLVO> networkACL = _networkACLDao.listByVpcId(srcVpcId);

        for (NetworkACLVO aclList : networkACL) {
            aclList.setVpcId(dstVpcId);
            _networkACLDao.persist(aclList);
        }
    }

    private void reassignPublicIpsToNewVpc(long srcVpcId, Vpc dstVpc) {
        List<? extends IPAddressVO> publicIps = _ipAddressDao.listByAssociatedVpc(srcVpcId, _vpcManager.isSrcNatIpRequired(dstVpc.getVpcOfferingId()) ? null : false);

        for(IPAddressVO publicIp : publicIps) {
            publicIp.setVpcId(dstVpc.getId());
            _ipAddressDao.persist(publicIp);
        }
    }

    private void reassignGatewayToNewVpc(long srcVpcId, long dstVpcId){
        List<VpcGatewayVO> vpcGateways = _vpcGatewayDao.listByVpcId(srcVpcId);
        for (VpcGatewayVO vpcGateway : vpcGateways) {
            vpcGateway.setVpcId(dstVpcId);
            _vpcGatewayDao.persist(vpcGateway);
        }
    }

    private void copyVpcResourceTagsToNewVpc(long srcVpcId, long dstVpcId){
        List<? extends ResourceTag> resourceTags = _resourceTagDao.listBy(srcVpcId, ResourceTag.ResourceObjectType.Vpc);
        for (ResourceTag resourceTag : resourceTags) {
            resourceTag.setResourceId(dstVpcId);
            _resourceTagDao.persist(
                    new ResourceTagVO(
                            resourceTag.getKey(),
                            resourceTag.getValue(),
                            resourceTag.getAccountId(),
                            resourceTag.getDomainId(),
                            dstVpcId,
                            resourceTag.getResourceType(),
                            resourceTag.getCustomer(),
                            resourceTag.getResourceUuid()));
        }
    }

    private void copyFirewallRulesToNewNetwork(Network srcNetwork, Network dstNetwork) {
        List<FirewallRuleVO> firewallRules = _firewallDao.listByNetworkPurposeTrafficType(srcNetwork.getId(), FirewallRule.Purpose.Firewall, FirewallRule.TrafficType.Egress);
        firewallRules.addAll(_firewallDao.listByNetworkPurposeTrafficType(srcNetwork.getId(), FirewallRule.Purpose.Firewall, FirewallRule.TrafficType.Ingress));
        if (logger.isDebugEnabled()) {
            logger.debug("Copying firewall rules from network {} to network {}", srcNetwork, dstNetwork);
        }

        //Loop over all the firewall rules in the original network and copy all values to a new firewall rule
        //Copy all objects that are dependant on the firewall rules
        for (FirewallRuleVO originalFirewallRule : firewallRules) {
            FirewallRuleVO ruleVO = new FirewallRuleVO(originalFirewallRule.getXid(),
                                                       originalFirewallRule.getSourceIpAddressId(),
                                                       originalFirewallRule.getSourcePortStart(),
                                                       originalFirewallRule.getSourcePortEnd(),
                                                       originalFirewallRule.getProtocol(),
                                                       dstNetwork.getId(),
                                                       srcNetwork.getAccountId(),
                                                       srcNetwork.getDomainId(),
                                                       originalFirewallRule.getPurpose(),
                                                       originalFirewallRule.getSourceCidrList(),
                                                       originalFirewallRule.getDestinationCidrList(),
                                                       originalFirewallRule.getIcmpCode(),
                                                       originalFirewallRule.getIcmpType(),
                                                       originalFirewallRule.getRelated(),
                                                       originalFirewallRule.getTrafficType(),
                                                       originalFirewallRule.getType());

            ruleVO = _firewallDao.persist(ruleVO);

            //Firewall rule cidrs
            List<FirewallRulesCidrsVO> firewallRulesCidrsVOS = _firewallRulesCidrDao.listByFirewallRuleId(originalFirewallRule.getId());
            for (FirewallRulesCidrsVO firewallRulesCidrVO: firewallRulesCidrsVOS) {
                _firewallRulesCidrDao.persist(new FirewallRulesCidrsVO(ruleVO.getId(), firewallRulesCidrVO.getSourceCidrList()));
            }

            //Firewall rules details
            List<FirewallRuleDetailVO> originalFirewallRuleDetails = _firewallRuleDetailsDao.listDetails(originalFirewallRule.getId());
            for (FirewallRuleDetailVO originalFirewallRuleDetail : originalFirewallRuleDetails) {
                _firewallRuleDetailsDao.persist(new FirewallRuleDetailVO(ruleVO.getId(), originalFirewallRuleDetail.getName(), originalFirewallRuleDetail.getValue(), originalFirewallRuleDetail.isDisplay()));
            }
        }
    }

    private void assignRouterNicsToNewNetwork(long srcNetworkId, long dstNetworkId) {
        final List<DomainRouterVO> routers = _routerDao.listByNetworkAndRole(srcNetworkId, VirtualRouter.Role.VIRTUAL_ROUTER);
        for (DomainRouterVO domainRouter : routers) {
            NicVO vrNic = _nicDao.findByNetworkIdAndType(srcNetworkId, VirtualMachine.Type.DomainRouter);
            vrNic.setNetworkId(dstNetworkId);
            _nicDao.update(vrNic.getId(), vrNic);

            RouterNetworkVO routerNetwork = _routerNetworkDao.findByRouterAndNetwork(domainRouter.getId(), srcNetworkId);
            routerNetwork.setNetworkId(dstNetworkId);
            _routerNetworkDao.persist(routerNetwork);
        }
    }

    @Override
    public Network upgradeNetworkToNewNetworkOffering(long networkId, long newPhysicalNetworkId, long networkOfferingId, Long vpcId) {
        logger.debug("upgrading network to network with new offering.");
        NetworkVO network = _networksDao.findById(networkId);
        NetworkOffering newOffering = _networkOfferingDao.findByIdIncludingRemoved(networkOfferingId);
        long gurusImplementing = 0;
        network.setBroadcastUri(null);
        AccountVO networkAccount = _accountDao.findById(network.getAccountId());
        DataCenterDeployment plan = new DataCenterDeployment(network.getDataCenterId(), null, null, null, null, newPhysicalNetworkId);
        for (final NetworkGuru guru : _networkMgr.getNetworkGurus()) {

            final Network designedNetwork = guru.design(newOffering, plan, network, network.getName(), vpcId, networkAccount);
            if (designedNetwork == null) {
                continue;
            }

            gurusImplementing++;
            if (gurusImplementing > 1) {
                throw new CloudRuntimeException("Failed to migrate network to new physical network. Multiple network guru's for the same network are currently not supported.");
            }

            network.setTrafficType(designedNetwork.getTrafficType());
            network.setMode(designedNetwork.getMode());
            network.setBroadcastDomainType(designedNetwork.getBroadcastDomainType());
            network.setBroadcastUri(designedNetwork.getBroadcastUri());
            network.setNetworkOfferingId(designedNetwork.getNetworkOfferingId());
            network.setState(designedNetwork.getState());
            network.setPhysicalNetworkId(designedNetwork.getPhysicalNetworkId());
            network.setRedundant(designedNetwork.isRedundant());
            network.setGateway(designedNetwork.getGateway());
            network.setCidr(designedNetwork.getCidr());
            network.setGuruName(guru.getName());
            network.setVpcId(vpcId);
        }
        _networksDao.update(network.getId(), network, _networkMgr.finalizeServicesAndProvidersForNetwork(_entityMgr.findById(NetworkOffering.class, networkOfferingId), newPhysicalNetworkId));
        return network;
    }

    @Override
    public void deleteCopyOfNetwork(long networkCopyId, long originalNetworkId) {
        NetworkVO networkCopy = _networksDao.findById(networkCopyId);

        NicVO userNic = _nicDao.findByNetworkIdAndType(networkCopyId, VirtualMachine.Type.User);
        if (userNic != null) {
            logger.error("Something went wrong while migrating nics from the old network to the new network. Failed to delete copy of network. There are still user nics present in the network.");
            throw new CloudRuntimeException("Failed to delete copy of network. There are still user nics present in the network.");
        }

        NetworkVO originalNetwork = _networksDao.findById(originalNetworkId);

        swapUuids(originalNetwork, networkCopy);
        try {
            if (!_networkService.deleteNetwork(networkCopy.getId(), true)) {
                throw new CloudRuntimeException("Failed to delete network. Clean up not successful.");
            }
        } finally {
            swapUuids(networkCopy, originalNetwork);
        }
        originalNetwork.setRelated(originalNetworkId);
        _networksDao.persist(originalNetwork);
    }

    @Override
    public void deleteCopyOfVpc(long vpcCopyId, long originalVpcId) {
        VpcVO copyOfvpc = _vpcDao.findById(vpcCopyId);
        VpcVO originalVpc = _vpcDao.findById(originalVpcId);

        //Be sure that when we delete the vpc, it has the uuid with what it was created.
        swapUuids(copyOfvpc, originalVpc);
        try {
            if(!_vpcService.deleteVpc(vpcCopyId)) {
                throw new CloudRuntimeException("Deletion of VPC failed. Clean up was not successful.");
            }
        } catch (ResourceUnavailableException e) {
            throw new CloudRuntimeException(e.getMessage());
        } finally {
            swapUuids(originalVpc, copyOfvpc);
        }
        _resourceTagDao.removeByResourceIdAndKey(originalVpcId, ResourceTag.ResourceObjectType.Vpc, MIGRATION);
    }

    private Boolean migrateNicsInDB(NicVO originalNic, Network networkInNewPhysicalNet, DataCenter dc, ReservationContext context) {
        logger.debug("migrating nics in database.");
        UserVmVO vmVO = _vmDao.findById(originalNic.getInstanceId());
        VirtualMachineProfile vmProfile = new VirtualMachineProfileImpl(vmVO, null, null, null, null);
        NicProfile nicProfile = new NicProfile(originalNic, networkInNewPhysicalNet, null, null, null, _networkModel.isSecurityGroupSupportedInNetwork(networkInNewPhysicalNet), null);
        try {
            nicProfile = _networkMgr.allocateNic(nicProfile, networkInNewPhysicalNet, originalNic.isDefaultNic(), nicProfile.getDeviceId(), vmProfile).first();
        } catch (InsufficientVirtualNetworkCapacityException | InsufficientAddressCapacityException e) {
            throw new CloudRuntimeException("Allocation of new nicProfile failed during migration", e);
        }

        //Update vm_network_map table
        if (vmProfile.getType() == VirtualMachine.Type.User) {
            final VMNetworkMapVO vno = new VMNetworkMapVO(vmVO.getId(), networkInNewPhysicalNet.getId());
            _vmNetworkMapDao.persist(vno);
        }

        NicVO newNic = _nicDao.findById(nicProfile.getId());

        copyNicDetails(originalNic.getId(), newNic.getId());
        //Update nic uuid here
        moveServices(originalNic, newNic);

        if (originalNic.getState() == Nic.State.Reserved) {
            final VirtualMachine vm = vmProfile.getVirtualMachine();
            final Host host = _hostDao.findById(vm.getHostId());
            final DeployDestination dest = new DeployDestination(dc, null, null, host);

            try {
                nicProfile = _networkMgr.prepareNic(vmProfile, dest, context, nicProfile.getId(), networkInNewPhysicalNet);
                 _itMgr.replugNic(networkInNewPhysicalNet, _itMgr.toNicTO(nicProfile, host.getHypervisorType()), _itMgr.toVmTO(vmProfile), dest.getHost());
            } catch (ResourceUnavailableException | InsufficientCapacityException e) {
                throw new CloudRuntimeException("Migration of Nic failed", e);
            }
        }

        //Mark the old nic as removed
        markAsNonDefault(originalNic);
        _networkMgr.removeNic(vmProfile, originalNic);

        if (logger.isDebugEnabled()) {
            logger.debug("Nic is migrated successfully for vm " + vmVO + " to " + networkInNewPhysicalNet);
        }
        return true;
    }

    @Override
    public void assignNicsToNewPhysicalNetwork(Network srcNetwork, Network networkInNewPhysicalNet) {
        List<NicVO> nics = _nicDao.listByNetworkId(srcNetwork.getId());

        final CallContext cctx = CallContext.current();
        final ReservationContext context = new ReservationContextImpl(null, null, cctx.getCallingUser(), cctx.getCallingAccount());
        final DataCenter dc = _entityMgr.findById(DataCenter.class, networkInNewPhysicalNet.getDataCenterId());

        //For each nic in the old network check if the nic belongs to a guest vm and migrate it to the new network.
        for (NicVO originalNic : nics) {
            if (!VirtualMachine.Type.User.equals(originalNic.getVmType())) {
                continue;
            }
            Transaction.execute((TransactionCallback<Boolean>)
                                            (status) -> migrateNicsInDB(originalNic, networkInNewPhysicalNet, dc, context));
        }

        //Now that nics are migrated we can migrate the static nats on those nics
        reapplyPublicIps(srcNetwork, networkInNewPhysicalNet);
    }

    private void reapplyPublicIps(Network networkInOldPhysicalNetwork, Network networkInNewPhysicalNet) {
        CallContext ctx = CallContext.current();
        long callerUserId = ctx.getCallingUserId();
        Account caller = ctx.getCallingAccount();

        AccountVO networkAccount = _accountDao.findById(networkInNewPhysicalNet.getAccountId());
        List<? extends IPAddressVO> staticNatIps = _ipAddressDao.listStaticNatPublicIps(networkInOldPhysicalNetwork.getId());

        List<String> providers = _networkOfferingServiceDao.listProvidersForServiceForNetworkOffering(networkInNewPhysicalNet.getNetworkOfferingId(), Network.Service.SourceNat);
        boolean isSrcNatIpNeeded = providers.stream().anyMatch(provider -> provider.contains(Network.Provider.VirtualRouter.getName()));

        for (IPAddressVO ipAddress : staticNatIps) {
            if (!ipAddress.isSourceNat() || isSrcNatIpNeeded) {
                ipAddress.setAssociatedWithNetworkId(networkInNewPhysicalNet.getId());
                _ipAddressDao.persist(ipAddress);
            } else {
                _ipAddressManager.disassociatePublicIpAddress(ipAddress, callerUserId, caller);
            }
        }

        _rulesMgr.applyStaticNatsForNetwork(networkInNewPhysicalNet, false, networkAccount);
    }

    private void copyNicDetails(long originalNicId, long dstNicId) {
        List<NicDetailVO> nicDetails = _nicDetailsDao.listDetails(originalNicId);

        for (NicDetailVO nicDetail : nicDetails) {
            _nicDetailsDao.persist(new NicDetailVO(dstNicId, nicDetail.getName(), nicDetail.getValue(), nicDetail.isDisplay()));
        }
    }

    private void moveServices(NicVO originalNic, NicVO newNic) {
        _nicIpAliasDao.moveIpAliases(originalNic.getId(), newNic.getId());
        _nicSecondaryIpDao.moveSecondaryIps(originalNic.getId(), newNic.getId());
        swapUuids(originalNic, newNic);
    }

    private void markAsNonDefault(NicVO nic) {
        nic.setDefaultNic(false);
        _nicDao.persist(nic);
    }

    /**
     * Swaps the UUID's of the given nics's
     * @param oldNic
     * @param newNic
     */
    private void swapUuids(NicVO oldNic, NicVO newNic) {
        final String realUuid = oldNic.getUuid();
        final String dummyUuid = newNic.getUuid();

        oldNic.setUuid(dummyUuid.replace("-", "+"));
        newNic.setUuid(realUuid);
        _nicDao.persist(oldNic);
        _nicDao.persist(newNic);

        oldNic.setUuid(dummyUuid);
        _nicDao.persist(oldNic);
    }

    /**
     * Swaps the UUID's of the given networks
     * @param oldNetwork
     * @param newNetwork
     */
    private void swapUuids(NetworkVO oldNetwork, NetworkVO newNetwork) {
        String realUuid = oldNetwork.getUuid();
        String dummyUuid = newNetwork.getUuid();

        oldNetwork.setUuid(dummyUuid.replace("-","+"));
        newNetwork.setUuid(realUuid);
        _networksDao.update(oldNetwork.getId(), oldNetwork);
        _networksDao.update(newNetwork.getId(), newNetwork);

        oldNetwork.setUuid(dummyUuid);
        _networksDao.update(oldNetwork.getId(), oldNetwork);
    }

    /**
     * Swaps the UUID's of the given vpcs
     * @param oldVpc
     * @param newVpc
     */
    private void swapUuids(VpcVO oldVpc, VpcVO newVpc) {
        String realUuid = oldVpc.getUuid();
        String dummyUuid = newVpc.getUuid();

        oldVpc.setUuid(dummyUuid.replace("-","+"));
        newVpc.setUuid(realUuid);
        _vpcDao.update(oldVpc.getId(), oldVpc);
        _vpcDao.update(newVpc.getId(), newVpc);

        oldVpc.setUuid(dummyUuid);
        _vpcDao.update(oldVpc.getId(), oldVpc);
    }

}
