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

import java.util.List;

import javax.inject.Inject;

import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.context.CallContext;
import org.apache.commons.lang3.StringUtils;

import com.cloud.agent.AgentManager;
import com.cloud.agent.api.CreateBcfAttachmentCommand;
import com.cloud.agent.api.CreateBcfRouterCommand;
import com.cloud.agent.api.CreateBcfRouterInterfaceCommand;
import com.cloud.agent.api.CreateBcfSegmentCommand;
import com.cloud.agent.api.DeleteBcfSegmentCommand;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.HostPodDao;
import com.cloud.dc.dao.VlanDao;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes;
import com.cloud.event.EventVO;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InsufficientVirtualNetworkCapacityException;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.network.BigSwitchBcfDeviceVO;
import com.cloud.network.Network;
import com.cloud.network.Network.GuestType;
import com.cloud.network.Network.State;
import com.cloud.network.NetworkMigrationResponder;
import com.cloud.network.NetworkProfile;
import com.cloud.network.NetworkService;
import com.cloud.network.Networks.BroadcastDomainType;
import com.cloud.network.PhysicalNetwork;
import com.cloud.network.PhysicalNetwork.IsolationMethod;
import com.cloud.network.bigswitch.BigSwitchBcfUtils;
import com.cloud.network.dao.BigSwitchBcfDao;
import com.cloud.network.dao.FirewallRulesCidrsDao;
import com.cloud.network.dao.FirewallRulesDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkVO;
import com.cloud.network.vpc.NetworkACLItemCidrsDao;
import com.cloud.network.vpc.NetworkACLItemDao;
import com.cloud.network.vpc.Vpc;
import com.cloud.network.vpc.dao.VpcDao;
import com.cloud.offering.NetworkOffering;
import com.cloud.user.Account;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.vm.NicProfile;
import com.cloud.vm.ReservationContext;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.DomainRouterDao;
import com.cloud.vm.dao.VMInstanceDao;

/**
 * BigSwitchBcfGuestNetworkGuru is responsible for creating and removing virtual networks.
 * Current implementation leverages GuestNetworkGuru to create VLAN based virtual networks
 * and map it to a Big Switch Big Cloud Fabric (BCF) Segment.  It is called by the NetworkOrchestrator.
 *
 * When a VM is created and needs to be plugged into a BCF segment network, BigSwitchBcfElement is
 * called by NetworkOrchestrator to create a "port" and an "attachment" for each nic, and
 * register them with the controller to be plugged into the corresponding network. It also
 * removes them when the VM is destroyed.
 */
public class BigSwitchBcfGuestNetworkGuru extends GuestNetworkGuru implements NetworkMigrationResponder {

    @Inject
    PhysicalNetworkDao _physicalNetworkDao;
    @Inject
    BigSwitchBcfDao _bigswitchBcfDao;
    @Inject
    HostDao _hostDao;
    @Inject
    AgentManager _agentMgr;
    @Inject
    DomainRouterDao _routerDao;
    @Inject
    VMInstanceDao _vmDao;
    @Inject
    AccountDao _accountDao;
    @Inject
    VpcDao _vpcDao;
    @Inject
    IPAddressDao _ipAddressDao;
    @Inject
    HostPodDao _podDao;
    @Inject
    NetworkService _netService;
    @Inject
    VlanDao _vlanDao;
    @Inject
    FirewallRulesDao _fwRulesDao;
    @Inject
    FirewallRulesCidrsDao _fwCidrsDao;
    @Inject
    NetworkACLItemDao _aclItemDao;
    @Inject
    NetworkACLItemCidrsDao _aclItemCidrsDao;

    private BigSwitchBcfUtils _bcfUtils = null;

    public BigSwitchBcfGuestNetworkGuru() {
        super();
        _isolationMethods = new IsolationMethod[] {new IsolationMethod("BCF_SEGMENT")};
    }

    @Override
    protected boolean canHandle(NetworkOffering offering, NetworkType networkType, PhysicalNetwork physicalNetwork) {
        if (networkType == NetworkType.Advanced && isMyTrafficType(offering.getTrafficType()) && offering.getGuestType() == Network.GuestType.Isolated &&
            isMyIsolationMethod(physicalNetwork)) {
            return true;
        } else {
            logger.trace("We only take care of Guest networks of type   " + GuestType.Isolated + " in zone of type " + NetworkType.Advanced);
            return false;
        }
    }

    @Override
    public Network design(NetworkOffering offering, DeploymentPlan plan, Network userSpecified, String name, Long vpcId, Account owner) {
        // Check if the isolation type of the physical network is BCF_SEGMENT, then delegate GuestNetworkGuru to design
        PhysicalNetworkVO physnet = _physicalNetworkDao.findById(plan.getPhysicalNetworkId());
        if (physnet == null || physnet.getIsolationMethods() == null || !physnet.getIsolationMethods().contains("BCF_SEGMENT")) {
            logger.debug("Refusing to design this network, the physical isolation type is not BCF_SEGMENT");
            return null;
        }

        List<BigSwitchBcfDeviceVO> devices = _bigswitchBcfDao.listByPhysicalNetwork(physnet.getId());
        if (devices.isEmpty()) {
            logger.error(String.format("No BigSwitch Controller on physical network %s", physnet));
            return null;
        }
        for (BigSwitchBcfDeviceVO d: devices){
            logger.debug(String.format("BigSwitch Controller %s found on physical network %s", d, physnet));
        }

        logger.debug("Physical isolation type is BCF_SEGMENT, asking GuestNetworkGuru to design this network");
        NetworkVO networkObject = (NetworkVO)super.design(offering, plan, userSpecified, name, vpcId, owner);
        if (networkObject == null) {
            return null;
        }
        // Override the broadcast domain type
        networkObject.setBroadcastDomainType(BroadcastDomainType.Vlan);

        return networkObject;
    }

    @Override
    public Network implement(Network network, NetworkOffering offering, DeployDestination dest, ReservationContext context)
        throws InsufficientVirtualNetworkCapacityException {
        assert (network.getState() == State.Implementing) : "Why are we implementing " + network;

        bcfUtilsInit();

        long dcId = dest.getDataCenter().getId();

        long physicalNetworkId = _networkModel.findPhysicalNetworkId(dcId, offering.getTags(), offering.getTrafficType());

        NetworkVO implemented =
            new NetworkVO(network.getTrafficType(), network.getMode(), network.getBroadcastDomainType(), network.getNetworkOfferingId(), State.Allocated,
                network.getDataCenterId(), physicalNetworkId, false);

        if (network.getGateway() != null) {
            implemented.setGateway(network.getGateway());
        }

        if (network.getCidr() != null) {
            implemented.setCidr(network.getCidr());
        }

        String vnetId = "";
        if (network.getBroadcastUri() == null) {
            vnetId = _dcDao.allocateVnet(dcId, physicalNetworkId, network.getAccountId(), context.getReservationId(), UseSystemGuestVlans.valueIn(network.getAccountId()));
            if (vnetId == null) {
                throw new InsufficientVirtualNetworkCapacityException("Unable to allocate vnet as a " +
                        "part of network " + network + " implement ", DataCenter.class, dcId);
            }
            implemented.setBroadcastUri(BroadcastDomainType.Vlan.toUri(vnetId));
            ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), network.getAccountId(),
                    EventVO.LEVEL_INFO, EventTypes.EVENT_ZONE_VLAN_ASSIGN, "Assigned Zone Vlan: " + vnetId + " Network Id: " + network.getId(), network.getId(), ApiCommandResourceType.Network.toString(), 0);
        } else {
            implemented.setBroadcastUri(network.getBroadcastUri());
        }

        // Name is either the given name or the uuid
        String name = network.getName();
        if (name == null || name.isEmpty()) {
            name = ((NetworkVO)network).getUuid();
        }
        if (name.length() > 64) {
            name = name.substring(0, 63); // max length 64
        }

        // update fields in network object
        NetworkVO networkObject = (NetworkVO) network;

        // determine whether this is VPC network or stand-alone network
        Vpc vpc = null;
        if(network.getVpcId()!=null){
            vpc = _vpcDao.acquireInLockTable(network.getVpcId());
        }

        // use uuid of networkVO as network id in BSN
        String networkId = networkObject.getUuid();

        String tenantId;
        String tenantName;
        if (vpc != null) {
            tenantId = vpc.getUuid();
            tenantName = vpc.getName();
            _vpcDao.releaseFromLockTable(vpc.getId());
        } else {
            // use network in CS as tenant in BSN
            // for non-VPC networks, use network name and id as tenant name and id
            tenantId = networkId;
            tenantName = name;
        }

        // store tenantId in networkObject for NetworkOrchestrator implementNetwork use
        networkObject.setNetworkDomain(tenantId);
        // store tenant Id in implemented object for future actions (e.g., shutdown)
        implemented.setNetworkDomain(tenantId);
        String vlanStr = BroadcastDomainType.getValue(implemented.getBroadcastUri());

        // get public net info - needed to set up source nat gateway
        NetworkVO pubNet = _bcfUtils.getPublicNetwork(physicalNetworkId);

        // locate subnet info
        SearchCriteria<VlanVO> sc = _vlanDao.createSearchCriteria();
        sc.setParameters("network_id", pubNet.getId());
        VlanVO pubVlanVO = _vlanDao.findOneBy(sc);
        String pubVlanStr = pubVlanVO.getVlanTag();

        Integer vlan;
        if(StringUtils.isNumeric(vlanStr)){
            vlan = Integer.valueOf(vlanStr);
        } else {
            vlan = 0;
        }
        CreateBcfSegmentCommand cmd1 = new CreateBcfSegmentCommand(tenantId, tenantName, networkId, name, vlan);

        _bcfUtils.sendBcfCommandWithNetworkSyncCheck(cmd1, networkObject);

        if(_bcfUtils.isNatEnabled()){
            Integer pvlan;
            if(StringUtils.isNumeric(pubVlanStr)){
                pvlan = Integer.valueOf(pubVlanStr);
            } else {
                pvlan = 0;
            }
            CreateBcfSegmentCommand cmd2 = new CreateBcfSegmentCommand("external", "external", "external", "external", pvlan);

            _bcfUtils.sendBcfCommandWithNetworkSyncCheck(cmd2, networkObject);

            CreateBcfRouterCommand cmd3 = new CreateBcfRouterCommand(tenantId);

            _bcfUtils.sendBcfCommandWithNetworkSyncCheck(cmd3, network);

            CreateBcfRouterInterfaceCommand cmd5 = new CreateBcfRouterInterfaceCommand(tenantId, network.getUuid(), network.getCidr(),
                    network.getGateway(), network.getName());

            _bcfUtils.sendBcfCommandWithNetworkSyncCheck(cmd5, network);
        }
        return implemented;
    }



    @Override
    public void reserve(NicProfile nic, Network network, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context)
        throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException {
        // Have GuestNetworkGuru reserve for us
        super.reserve(nic, network, vm, dest, context);
    }

    @Override
    public boolean release(NicProfile nic, VirtualMachineProfile vm, String reservationId) {
        return super.release(nic, vm, reservationId);
    }

    @Override
    public void shutdown(NetworkProfile profile, NetworkOffering offering) {
        NetworkVO networkObject = _networkDao.findById(profile.getId());
        if (networkObject.getBroadcastDomainType() != BroadcastDomainType.Vlan || networkObject.getBroadcastUri() == null) {
            logger.warn(String.format("BroadcastUri is empty or incorrect for guest network %s", networkObject));
            return;
        }

        bcfUtilsInit();

        // tenantId stored in network domain field at creation
        String tenantId = networkObject.getNetworkDomain();

        String networkId = networkObject.getUuid();

        DeleteBcfSegmentCommand cmd = new DeleteBcfSegmentCommand(tenantId, networkId);

        _bcfUtils.sendBcfCommandWithNetworkSyncCheck(cmd, networkObject);

        super.shutdown(profile, offering);
    }

    @Override
    public boolean trash(Network network, NetworkOffering offering) {
        return super.trash(network, offering);
    }

    @Override
    public boolean prepareMigration(NicProfile nic, Network network,
            VirtualMachineProfile vm, DeployDestination dest,
            ReservationContext context) {
        bcfUtilsInit();

        // get arguments for CreateBcfAttachmentCommand
        // determine whether this is VPC network or stand-alone network
        Vpc vpc = null;
        if(network.getVpcId()!=null){
            vpc = _vpcDao.acquireInLockTable(network.getVpcId());
        }

        String networkId = network.getUuid();

        String tenantId;
        String tenantName;
        if (vpc != null) {
            tenantId = vpc.getUuid();
            tenantName = vpc.getName();
            boolean released = _vpcDao.releaseFromLockTable(vpc.getId());
            logger.debug(String.format("BCF guru release lock vpc: %s released? %s", vpc, released));
        } else {
            // use network id in CS as tenant in BSN
            // use network uuid as tenant id for non-VPC networks
            tenantId = networkId;
            tenantName = network.getName();
        }

        String hostname = dest.getHost().getName();
        long zoneId = network.getDataCenterId();
        String vmwareVswitchLabel = _networkModel.getDefaultGuestTrafficLabel(zoneId, HypervisorType.VMware);
        String[] labelArray = null;
        String vswitchName = null;
        if(vmwareVswitchLabel!=null){
            labelArray=vmwareVswitchLabel.split(",");
            vswitchName = labelArray[0];
        }

        // hypervisor type:
        //   kvm: ivs port name
        //   vmware: specific portgroup naming convention
        String pgName = "";
        if (dest.getHost().getHypervisorType() == HypervisorType.KVM){
            pgName = hostname;
        } else if (dest.getHost().getHypervisorType() == HypervisorType.VMware){
            pgName = hostname + "-" + vswitchName;
        }

        String nicId = nic.getUuid();
        Integer vlan = Integer.valueOf(BroadcastDomainType.getValue(nic.getIsolationUri()));
        String ipv4 = nic.getIPv4Address();
        String mac = nic.getMacAddress();

        CreateBcfAttachmentCommand cmd = new CreateBcfAttachmentCommand(tenantId,
                tenantName, networkId, pgName, nicId, vlan, ipv4, mac);

        _bcfUtils.sendBcfCommandWithNetworkSyncCheck(cmd, network);

        return true;
    }

    @Override
    public void rollbackMigration(NicProfile nic, Network network,
            VirtualMachineProfile vm, ReservationContext src,
            ReservationContext dst) {
        logger.debug("BCF guru rollback migration");
    }

    @Override
    public void commitMigration(NicProfile nic, Network network,
            VirtualMachineProfile vm, ReservationContext src,
            ReservationContext dst) {
        logger.debug("BCF guru commit migration");
    }

    private void bcfUtilsInit(){
        if (_bcfUtils == null) {
            _bcfUtils = new BigSwitchBcfUtils(_networkDao, _nicDao,
                    _vmDao, _hostDao, _vpcDao, _bigswitchBcfDao,
                    _agentMgr, _vlanDao, _ipAddressDao, _fwRulesDao,
                    _fwCidrsDao, _aclItemDao, _aclItemCidrsDao, _networkModel);
        }
    }
}
