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

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.cloudstack.network.topology.NetworkTopologyVisitor;

import com.cloud.agent.api.Command;
import com.cloud.agent.api.NetworkUsageCommand;
import com.cloud.agent.manager.Commands;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.IpAddress;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.Networks.BroadcastDomainType;
import com.cloud.network.Networks.IsolationType;
import com.cloud.network.PublicIpAddress;
import com.cloud.network.VpcVirtualNetworkApplianceService;
import com.cloud.network.dao.FirewallRulesDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.router.VirtualRouter;
import com.cloud.network.vpc.VpcManager;
import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.VpcDao;
import com.cloud.user.UserStatisticsVO;
import com.cloud.user.dao.UserStatisticsDao;
import com.cloud.utils.Pair;
import com.cloud.vm.Nic;
import com.cloud.vm.NicProfile;
import com.cloud.vm.NicVO;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.dao.NicDao;

import org.apache.cloudstack.network.topology.NetworkTopology;
import org.apache.cloudstack.network.topology.NetworkTopologyContext;

public class NicPlugInOutRules extends RuleApplier {


    private final List<? extends PublicIpAddress> _ipAddresses;

    private Commands _netUsageCommands;

    public NicPlugInOutRules(final Network network, final List<? extends PublicIpAddress> ipAddresses) {
        super(network);
        _ipAddresses = ipAddresses;
    }

    @Override
    public boolean accept(final NetworkTopologyVisitor visitor, final VirtualRouter router) throws ResourceUnavailableException {
        _router = router;

        Pair<Map<String, PublicIpAddress>, Map<String, PublicIpAddress>> nicsToChange = getNicsToChangeOnRouter(visitor);

        Map<String, PublicIpAddress> nicsToPlug = nicsToChange.first();
        Map<String, PublicIpAddress> nicsToUnplug = nicsToChange.second();

        NetworkModel networkModel = visitor.getVirtualNetworkApplianceFactory().getNetworkModel();
        VirtualMachineManager itMgr = visitor.getVirtualNetworkApplianceFactory().getItMgr();
        NicDao nicDao = visitor.getVirtualNetworkApplianceFactory().getNicDao();
        VpcVirtualNetworkApplianceService routerService = visitor.getVirtualNetworkApplianceFactory().getRouterService();

        // de-associate IPs before unplugging nics
        if (!nicsToUnplug.isEmpty()) {
            NetworkTopologyContext networkTopologyContext = visitor.getVirtualNetworkApplianceFactory().getNetworkTopologyContext();
            final DataCenterDao dcDao = visitor.getVirtualNetworkApplianceFactory().getDcDao();
            final DataCenterVO dcVO = dcDao.findById(router.getDataCenterId());
            final NetworkTopology networkTopology = networkTopologyContext.retrieveNetworkTopology(dcVO);

            final String typeString = "vpc ip association before unplugging nics";
            final boolean isPodLevelException = false;
            final boolean failWhenDisconnect = false;
            final Long podId = null;
            final VpcIpAssociationRules ipAssociationRules = new VpcIpAssociationRules(_network, _ipAddresses);
            final boolean result = networkTopology.applyRules(_network, router, typeString, isPodLevelException, podId, failWhenDisconnect,
                    new RuleApplierWrapper<RuleApplier>(ipAssociationRules));
            if (!result) {
                logger.warn("Failed to de-associate IPs before unplugging nics");
                return false;
            }
        }

        // 1) Unplug the nics
        for (Entry<String, PublicIpAddress> entry : nicsToUnplug.entrySet()) {
            PublicIpAddress ip = entry.getValue();
            NicVO nic = nicDao.findByIp4AddressAndNetworkIdAndInstanceId(ip.getNetworkId(), _router.getId(), ip.getAddress().addr());
            if (nic != null) {
                logger.info("Collect network statistics for nic " + nic + " from router " + _router);
                routerService.collectNetworkStatistics(_router, nic);
            }
            Network publicNtwk = null;
            try {
                publicNtwk = networkModel.getNetwork(entry.getValue().getNetworkId());
                URI broadcastUri = BroadcastDomainType.Vlan.toUri(entry.getKey());
                itMgr.removeVmFromNetwork(_router, publicNtwk, broadcastUri);
            } catch (ConcurrentOperationException e) {
                logger.warn("Failed to remove router " + _router + " from vlan " + entry.getKey() + " in public network " + publicNtwk + " due to ", e);
                return false;
            }
        }

        _netUsageCommands = new Commands(Command.OnError.Continue);
        VpcDao vpcDao = visitor.getVirtualNetworkApplianceFactory().getVpcDao();
        VpcVO vpc = vpcDao.findById(_router.getVpcId());

        // 2) Plug the nics
        for (String vlanTag : nicsToPlug.keySet()) {
            PublicIpAddress ip = nicsToPlug.get(vlanTag);
            // have to plug the nic(s)
            NicProfile defaultNic = new NicProfile();
            if (ip.isSourceNat()) {
                defaultNic.setDefaultNic(true);
            }
            defaultNic.setIPv4Address(ip.getAddress().addr());
            defaultNic.setIPv4Gateway(ip.getGateway());
            defaultNic.setIPv4Netmask(ip.getNetmask());
            defaultNic.setMacAddress(ip.getMacAddress());
            defaultNic.setBroadcastType(BroadcastDomainType.Vlan);
            defaultNic.setBroadcastUri(BroadcastDomainType.Vlan.toUri(ip.getVlanTag()));
            defaultNic.setIsolationUri(IsolationType.Vlan.toUri(ip.getVlanTag()));

            NicProfile publicNic = null;
            Network publicNtwk = null;
            try {
                publicNtwk = networkModel.getNetwork(ip.getNetworkId());
                publicNic = itMgr.addVmToNetwork(_router, publicNtwk, defaultNic);
            } catch (ConcurrentOperationException e) {
                logger.warn("Failed to add router " + _router + " to vlan " + vlanTag + " in public network " + publicNtwk + " due to ", e);
            } catch (InsufficientCapacityException e) {
                logger.warn("Failed to add router " + _router + " to vlan " + vlanTag + " in public network " + publicNtwk + " due to ", e);
            } finally {
                if (publicNic == null) {
                    logger.warn("Failed to add router " + _router + " to vlan " + vlanTag + " in public network " + publicNtwk);
                    return false;
                }
            }
            // Create network usage commands. Send commands to router after
            // IPAssoc
            NetworkUsageCommand netUsageCmd = new NetworkUsageCommand(_router.getPrivateIpAddress(), _router.getInstanceName(), true, defaultNic.getIPv4Address(), vpc.getCidr());
            _netUsageCommands.addCommand(netUsageCmd);

            UserStatisticsDao userStatsDao = visitor.getVirtualNetworkApplianceFactory().getUserStatsDao();
            UserStatisticsVO stats = userStatsDao.findBy(_router.getAccountId(), _router.getDataCenterId(), publicNtwk.getId(), publicNic.getIPv4Address(), _router.getId(),
                    _router.getType().toString());
            if (stats == null) {
                stats = new UserStatisticsVO(_router.getAccountId(), _router.getDataCenterId(), publicNic.getIPv4Address(), _router.getId(), _router.getType().toString(),
                        publicNtwk.getId());
                userStatsDao.persist(stats);
            }
        }

        // The visit will be done from the AdvancedNetworkTopology, after the
        // VpcIpAssociation is done.
        return true;
    }

    public List<? extends PublicIpAddress> getIpAddresses() {
        return _ipAddresses;
    }

    public Commands getNetUsageCommands() {
        return _netUsageCommands;
    }

    private Pair<Map<String, PublicIpAddress>, Map<String, PublicIpAddress>> getNicsToChangeOnRouter(final NetworkTopologyVisitor visitor) {
        // 1) check which nics need to be plugged/unplugged and plug/unplug them

        final Map<String, PublicIpAddress> nicsToPlug = new HashMap<String, PublicIpAddress>();
        final Map<String, PublicIpAddress> nicsToUnplug = new HashMap<String, PublicIpAddress>();

        VpcManager vpcMgr = visitor.getVirtualNetworkApplianceFactory().getVpcMgr();
        NicDao nicDao = visitor.getVirtualNetworkApplianceFactory().getNicDao();
        IPAddressDao ipAddressDao = visitor.getVirtualNetworkApplianceFactory().getIpAddressDao();
        FirewallRulesDao rulesDao = visitor.getVirtualNetworkApplianceFactory().getFirewallRulesDao();

        // find out nics to unplug
        for (PublicIpAddress ip : _ipAddresses) {
            long publicNtwkId = ip.getNetworkId();

            // if ip is not associated to any network, and there are no firewall
            // rules, release it on the backend
            if (!vpcMgr.isIpAllocatedToVpc(ip)) {
                ip.setState(IpAddress.State.Releasing);
            }

            if (ip.getState() == IpAddress.State.Releasing) {
                NicVO nic = nicDao.findByIp4AddressAndNetworkIdAndInstanceId(publicNtwkId, _router.getId(), ip.getAddress().addr());
                if (nic != null) {
                    final List<IPAddressVO> allIps = ipAddressDao.listByAssociatedVpc(ip.getVpcId(), null);
                    boolean ipUpdated = false;
                    for (IPAddressVO allIp : allIps) {
                        if (allIp.getId() != ip.getId() && allIp.getVlanId() == ip.getVlanId()
                                && (allIp.isSourceNat()
                                || rulesDao.countRulesByIpIdAndState(allIp.getId(), FirewallRule.State.Active) > 0
                                || (allIp.isOneToOneNat() && allIp.getRuleState() == null))) {
                            logger.debug("Updating the nic " + nic + " with new ip address " + allIp.getAddress().addr());
                            nic.setIPv4Address(allIp.getAddress().addr());
                            nicDao.update(nic.getId(), nic);
                            ipUpdated = true;
                            break;
                        }
                    }
                    if (!ipUpdated) {
                        nicsToUnplug.put(ip.getVlanTag(), ip);
                        logger.debug("Need to unplug the nic for ip=" + ip + "; vlan=" + ip.getVlanTag() + " in public network id =" + publicNtwkId);
                    }
                }
            }
        }

        // find out nics to plug
        for (PublicIpAddress ip : _ipAddresses) {
            URI broadcastUri = BroadcastDomainType.Vlan.toUri(ip.getVlanTag());
            long publicNtwkId = ip.getNetworkId();

            // if ip is not associated to any network, and there are no firewall
            // rules, release it on the backend
            if (!vpcMgr.isIpAllocatedToVpc(ip)) {
                ip.setState(IpAddress.State.Releasing);
            }

            if (ip.getState() == IpAddress.State.Allocated || ip.getState() == IpAddress.State.Allocating) {
                // nic has to be plugged only when there are no nics for this
                // vlan tag exist on VR
                Nic nic = nicDao.findByNetworkIdInstanceIdAndBroadcastUri(publicNtwkId, _router.getId(), broadcastUri.toString());

                if (nic == null && nicsToPlug.get(ip.getVlanTag()) == null) {
                    nicsToPlug.put(ip.getVlanTag(), ip);
                    logger.debug("Need to plug the nic for ip=" + ip + "; vlan=" + ip.getVlanTag() + " in public network id =" + publicNtwkId);
                } else {
                    final PublicIpAddress nicToUnplug = nicsToUnplug.get(ip.getVlanTag());
                    if (nicToUnplug != null) {
                        NicVO nicVO = nicDao.findByIp4AddressAndNetworkIdAndInstanceId(publicNtwkId, _router.getId(), nicToUnplug.getAddress().addr());
                        nicVO.setIPv4Address(ip.getAddress().addr());
                        nicDao.update(nicVO.getId(), nicVO);
                        logger.debug("Updated the nic " + nicVO + " with the new ip address " + ip.getAddress().addr());
                        nicsToUnplug.remove(ip.getVlanTag());
                    }
                }
            }
        }

        Pair<Map<String, PublicIpAddress>, Map<String, PublicIpAddress>> nicsToChange = new Pair<Map<String, PublicIpAddress>, Map<String, PublicIpAddress>>(nicsToPlug,
                nicsToUnplug);

        return nicsToChange;
    }
}
