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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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

import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.lb.dao.ApplicationLoadBalancerRuleDao;

import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.manager.Commands;
import com.cloud.api.commands.DeployNetscalerVpxCmd;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.dc.dao.VlanDao;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.deploy.DeployDestination;
import com.cloud.deploy.DeploymentPlan;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.exception.StorageUnavailableException;
import com.cloud.network.IpAddressManager;
import com.cloud.network.Network;
import com.cloud.network.Network.Provider;
import com.cloud.network.NetworkModel;
import com.cloud.network.Networks.TrafficType;
import com.cloud.network.PhysicalNetwork;
import com.cloud.network.PhysicalNetworkServiceProvider;
import com.cloud.network.VirtualRouterProvider;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkServiceProviderDao;
import com.cloud.network.dao.VirtualRouterProviderDao;
import com.cloud.network.element.VirtualRouterProviderVO;
import com.cloud.network.lb.LoadBalancingRulesManager;
import com.cloud.network.router.VirtualRouter;
import com.cloud.network.router.VirtualRouter.RedundantState;
import com.cloud.network.router.VirtualRouter.Role;
import com.cloud.offering.NetworkOffering;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.resource.ResourceManager;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.DomainRouterVO;
import com.cloud.vm.NicProfile;
import com.cloud.vm.ReservationContext;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineGuru;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachineName;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VirtualMachineProfile.Param;
import com.cloud.vm.dao.DomainRouterDao;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.VMInstanceDao;

public class NetScalerVMManagerImpl extends ManagerBase implements NetScalerVMManager, VirtualMachineGuru {
    static final private String NetScalerLbVmNamePrefix = "NS";

    @Inject
    IpAddressManager _ipAddrMgr;
    @Inject
    VirtualMachineManager _itMgr;
    @Inject
    DomainRouterDao _internalLbVmDao;
    @Inject
    ConfigurationDao _configDao;
    @Inject
    AgentManager _agentMgr;
    @Inject
    DataCenterDao _dcDao;
    @Inject
    VirtualRouterProviderDao _vrProviderDao;
    @Inject
    ApplicationLoadBalancerRuleDao _lbDao;
    @Inject
    NetworkModel _ntwkModel;
    @Inject
    LoadBalancingRulesManager _lbMgr;
    @Inject
    NicDao _nicDao;
    @Inject
    AccountManager _accountMgr;
    @Inject
    NetworkDao _networkDao;
    @Inject
    NetworkOrchestrationService _networkMgr;
    @Inject
    ServiceOfferingDao _serviceOfferingDao;
    @Inject
    PhysicalNetworkServiceProviderDao _physicalProviderDao;
    @Inject
    NetworkOfferingDao _networkOfferingDao;
    @Inject
    VMTemplateDao _templateDao;
    @Inject
    ResourceManager _resourceMgr;
    @Inject
    VMInstanceDao _vmDao;
    @Inject
    NetworkModel _networkModel;
    @Inject
    PhysicalNetworkDao _physicalNetworkDao;
    @Inject
    VlanDao _vlanDao;
    @Inject
    DomainRouterDao _routerDao;

    @Override
    public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, DeployDestination dest, ReservationContext context) {
        //NetScalerLB vm starts up with 3 Nics
        //Nic #1 - NS IP
        //Nic #2 - locallink(guest network)
        //Nic #3 - public nic

        for (final NicProfile nic : profile.getNics()) {
            if(nic.getTrafficType() == TrafficType.Control) {
                nic.setTrafficType(TrafficType.Guest);
            }
        }
        return true;
    }

    @Override
    public boolean finalizeDeployment(Commands cmds, VirtualMachineProfile profile, DeployDestination dest, ReservationContext context) throws ResourceUnavailableException {
        return true;
    }

    @Override
    public boolean finalizeStart(VirtualMachineProfile profile, long hostId, Commands cmds, ReservationContext context) {
        return true;
    }

    @Override
    public boolean finalizeCommandsOnStart(Commands cmds, VirtualMachineProfile profile) {
        return true;
    }

    @Override
    public void finalizeStop(VirtualMachineProfile profile, Answer answer) {
    }

    @Override
    public void finalizeExpunge(VirtualMachine vm) {
    }

    @Override
    public void prepareStop(VirtualMachineProfile profile) {
    }

    @Override
    public void finalizeUnmanage(VirtualMachine vm) {
    }

    @Override
    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
        _itMgr.registerGuru(VirtualMachine.Type.NetScalerVm, this);
        if (logger.isInfoEnabled()) {
            logger.info(getName() + " has been configured");
        }
        return true;
    }

    @Override
    public String getName() {
        return _name;
    }

    protected VirtualRouter stopInternalLbVm(DomainRouterVO internalLbVm, boolean forced, Account caller, long callerUserId) throws ResourceUnavailableException, ConcurrentOperationException {
        logger.debug("Stopping internal lb vm " + internalLbVm);
        try {
            _itMgr.advanceStop(internalLbVm.getUuid(), forced);
            return _internalLbVmDao.findById(internalLbVm.getId());
        } catch (OperationTimedoutException e) {
            throw new CloudRuntimeException("Unable to stop " + internalLbVm, e);
        }
    }

    public VirtualRouterProvider addNetScalerLoadBalancerElement(long ntwkSvcProviderId) {
        VirtualRouterProviderVO element = _vrProviderDao.findByNspIdAndType(ntwkSvcProviderId, com.cloud.network.VirtualRouterProvider.Type.NetScalerVm);
        if (element != null) {
            logger.debug("There is already an " + getName() + " with service provider id " + ntwkSvcProviderId);
            return element;
        }

        PhysicalNetworkServiceProvider provider = _physicalProviderDao.findById(ntwkSvcProviderId);
        if (provider == null || !provider.getProviderName().equalsIgnoreCase(getName())) {
            throw new InvalidParameterValueException("Invalid network service provider is specified");
        }

        element = new VirtualRouterProviderVO(ntwkSvcProviderId, com.cloud.network.VirtualRouterProvider.Type.NetScalerVm);
        element.setEnabled(true);
        element = _vrProviderDao.persist(element);
        return element;
    }

    protected long getNetScalerLbProviderId(long physicalNetworkId) {
        //final long physicalNetworkId = _ntwkModel.getPhysicalNetworkId(guestNetwork);

        final PhysicalNetworkServiceProvider provider = _physicalProviderDao.findByServiceProvider(physicalNetworkId, "Netscaler");
        if (provider == null) {
            throw new CloudRuntimeException("Cannot find service provider " + Provider.Netscaler.toString() + " in physical network " + physicalNetworkId);
        }

        //TODO: get from type
        VirtualRouterProvider netScalerLbProvider = _vrProviderDao.findByNspIdAndType(provider.getId(), com.cloud.network.VirtualRouterProvider.Type.NetScalerVm);
        if (netScalerLbProvider == null) {
            //create the vrp for netscalerVM.
            netScalerLbProvider = addNetScalerLoadBalancerElement(provider.getId());
        }

        return netScalerLbProvider.getId();
    }

    @Override
    public Map<String, Object> deployNsVpx(Account owner, DeployDestination dest, DeploymentPlan plan, long svcOffId, long templateId) throws InsufficientCapacityException {

        VMTemplateVO template = _templateDao.findById(templateId) ;
        long id = _vmDao.getNextInSequence(Long.class, "id");
        Account systemAcct = _accountMgr.getSystemAccount();

        if (template == null) {
            logger.error(" Unable to find the NS VPX template");
            throw new CloudRuntimeException("Unable to find the Template" + templateId);
        }
        long dataCenterId = dest.getDataCenter().getId();
        DataCenterVO dc = _dcDao.findById(dest.getDataCenter().getId());

        String nxVpxName = VirtualMachineName.getSystemVmName(id, "Vpx", NetScalerLbVmNamePrefix);

        ServiceOfferingVO vpxOffering = _serviceOfferingDao.findById(svcOffId); //using 2GB and 2CPU  offering
        if(vpxOffering.getRamSize() < 2048 && vpxOffering.getCpu() <2 ) {
            throw new InvalidParameterValueException(String.format("Specified Service Offering: %s NS Vpx cannot be deployed. Min 2GB Ram and 2 CPU are required", vpxOffering));
        }

        long userId = CallContext.current().getCallingUserId();
        //TODO change the os bits from 142  103 to the actual guest of bits
        if(template.getGuestOSId() !=  103 ) {
            throw new InvalidParameterValueException(String.format("Specified Template %s not suitable for NS VPX Deployment. Please register the template with guest os type as unknown(64-bit)", template));
        }

        NetworkVO defaultNetwork = null;
        NetworkVO defaultPublicNetwork = null;
        if (dc.getNetworkType() == NetworkType.Advanced && dc.isSecurityGroupEnabled()) {
            List<NetworkVO> networks = _networkDao.listByZoneSecurityGroup(dataCenterId);
            if (networks == null || networks.size() == 0) {
                throw new CloudRuntimeException("Can not found security enabled network in SG Zone " + dc);
            }
            defaultNetwork = networks.get(0);
        } else {
            TrafficType defaultTrafficType = TrafficType.Management;
            List<NetworkVO> defaultNetworks = _networkDao.listByZoneAndTrafficType(dataCenterId, defaultTrafficType);

            TrafficType publicTrafficType = TrafficType.Public;
            List<NetworkVO> publicNetworks = _networkDao.listByZoneAndTrafficType(dataCenterId, publicTrafficType);

            // api should never allow this situation to happen
            if (defaultNetworks.size() != 1) {
                throw new CloudRuntimeException("Found " + defaultNetworks.size() + " networks of type " + defaultTrafficType + " when expect to find 1");
            }

            if (publicNetworks.size() != 1) {
                throw new CloudRuntimeException("Found " + defaultNetworks.size() + " networks of type " + defaultTrafficType + " when expect to find 1");
            }
            defaultPublicNetwork = publicNetworks.get(0);
            defaultNetwork = defaultNetworks.get(0);
        }

        LinkedHashMap<Network, List<? extends NicProfile>> networks = new LinkedHashMap<Network, List<? extends NicProfile>>(4);
        NicProfile defaultNic = new NicProfile();
        defaultNic.setDefaultNic(true);
        defaultNic.setDeviceId(0);

        networks.put(_networkMgr.setupNetwork(_accountMgr.getSystemAccount() , _networkOfferingDao.findById(defaultNetwork.getNetworkOfferingId()), plan, null, null, false).get(0), new ArrayList<NicProfile>());

        NicProfile defaultNic1 = new NicProfile();
        defaultNic1.setDefaultNic(false);
        defaultNic1.setDeviceId(1);

        NicProfile defaultNic2 = new NicProfile();
        defaultNic2.setDefaultNic(false);
        defaultNic2.setDeviceId(2);
        defaultNic2.setIPv4Address("");
        defaultNic2.setIPv4Gateway("");
        defaultNic2.setIPv4Netmask("");
        String macAddress = _networkModel.getNextAvailableMacAddressInNetwork(defaultPublicNetwork.getId());
        defaultNic2.setMacAddress(macAddress);

        networks.put(_networkMgr.setupNetwork(_accountMgr.getSystemAccount(), _networkOfferingDao.findByUniqueName(NetworkOffering.SystemPublicNetwork), plan, null, null, false).get(0),
              new ArrayList<NicProfile>(Arrays.asList(defaultNic2)));

        networks.put(_networkMgr.setupNetwork(_accountMgr.getSystemAccount(), _networkOfferingDao.findByUniqueName(NetworkOffering.SystemControlNetwork), plan, null, null, false).get(0),
              new ArrayList<NicProfile>());

        long physicalNetworkId = _networkModel.findPhysicalNetworkId(dataCenterId, _networkOfferingDao.findById(defaultPublicNetwork.getNetworkOfferingId()).getTags(), TrafficType.Public);
        // Validate physical network
        PhysicalNetwork physicalNetwork = _physicalNetworkDao.findById(physicalNetworkId);
        if (physicalNetwork == null) {
            throw new InvalidParameterValueException("Unable to find physical network with id: " + physicalNetworkId + " and tag: "
                  + _networkOfferingDao.findById(defaultPublicNetwork.getNetworkOfferingId()).getTags());
        }
        String guestvnet = physicalNetwork.getVnetString();

        final List<VlanVO> vlans = _vlanDao.listByZone(dataCenterId);
        List<String> pvlan = new ArrayList<String>();
        for (final VlanVO vlan : vlans) {
            pvlan.add(vlan.getVlanTag());
        }

        long netScalerProvider = getNetScalerLbProviderId(physicalNetworkId);
        DomainRouterVO nsVpx = new DomainRouterVO(id, vpxOffering.getId(), netScalerProvider, nxVpxName,
              template.getId(), template.getHypervisorType(),  template.getGuestOSId(), owner.getDomainId(), owner.getId(), userId, false, RedundantState.UNKNOWN, false,
              false, VirtualMachine.Type.NetScalerVm, null);

        nsVpx.setRole(Role.NETSCALER_VM);

        nsVpx = _routerDao.persist(nsVpx);

        VMInstanceVO vmVO= _vmDao.findVMByHostName(nxVpxName);
        _itMgr.allocate(nxVpxName, template, vpxOffering, networks, plan, template.getHypervisorType(), null, null);
        Map<Param, Object> params = new HashMap<VirtualMachineProfile.Param, Object>(1);
        try {
            if (vmVO != null) {
                startNsVpx(vmVO, params);
            } else {
                throw new NullPointerException();
            }
        } catch (StorageUnavailableException e) {
            e.printStackTrace();
        } catch (ConcurrentOperationException e) {
            e.printStackTrace();
        } catch (ResourceUnavailableException e) {
            e.printStackTrace();
        }
        vmVO= _vmDao.findByUuid(nsVpx.getUuid());
        Map<String, Object> deployResponse = new HashMap<String, Object>();
        deployResponse.put("vm", vmVO);
        deployResponse.put("guestvlan", guestvnet);
        deployResponse.put("publicvlan", pvlan);

        return deployResponse;
    }

    protected void startNsVpx(VMInstanceVO nsVpx, Map<Param, Object> params) throws StorageUnavailableException,
    InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException {
        logger.debug("Starting NS Vpx " + nsVpx);
        _itMgr.start(nsVpx.getUuid(), params, null, null);
    }

    @Override
    public Map<String,Object> deployNetscalerServiceVm(DeployNetscalerVpxCmd cmd) {
        DataCenter zone = _dcDao.findById(cmd.getZoneId());
        DeployDestination dest = new DeployDestination(zone, null, null, null);
        VMInstanceVO vmvo = null;
        Map<String,Object> resp = new HashMap<String, Object>();
        Long templateId = cmd.getTemplateId();
        Long serviceOfferingId = cmd.getServiceOfferingId();
        DeploymentPlan plan = new DataCenterDeployment(dest.getDataCenter().getId());

        try {
            resp = deployNsVpx(cmd.getAccount(), dest, plan, serviceOfferingId, templateId);
        } catch (InsufficientCapacityException e) {
            e.printStackTrace();
        }

        return resp;
    }

    protected VirtualRouter stopNetScalerVm(final long vmId, final boolean forced, final Account caller, final long callerUserId) throws ResourceUnavailableException, ConcurrentOperationException {
        final DomainRouterVO netscalerVm = _routerDao.findById(vmId);
        logger.debug("Stopping NetScaler vm " + netscalerVm);

        if (netscalerVm == null || netscalerVm.getRole() != Role.NETSCALER_VM) {
            throw new InvalidParameterValueException("Can't find NetScaler vm by id specified");
        }

        _accountMgr.checkAccess(caller, null, true, netscalerVm);

        try {
            _itMgr.expunge(netscalerVm.getUuid());
            return _routerDao.findById(netscalerVm.getId());
        } catch (final Exception e) {
            throw new CloudRuntimeException("Unable to stop " + netscalerVm, e);
        }
    }

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

    @Override
    public VirtualRouter stopNetScalerVm(Long vmId, boolean forced, Account caller, long callingUserId) {
        final DomainRouterVO netscalerVm = _routerDao.findById(vmId);
        logger.debug("Stopping NetScaler vm " + netscalerVm);

        if (netscalerVm == null || netscalerVm.getRole() != Role.NETSCALER_VM) {
            throw new InvalidParameterValueException("Can't find NetScaler vm by id specified");
        }
        _accountMgr.checkAccess(caller, null, true, netscalerVm);
        try {
            _itMgr.expunge(netscalerVm.getUuid());
            return _routerDao.findById(netscalerVm.getId());
        } catch (final Exception e) {
            throw new CloudRuntimeException("Unable to stop " + netscalerVm, e);
        }
    }
}
