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

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.inject.Inject;

import net.nuage.vsp.acs.client.api.model.NetworkRelatedVsdIds;
import net.nuage.vsp.acs.client.api.model.VspAclRule;
import net.nuage.vsp.acs.client.api.model.VspAddressRange;
import net.nuage.vsp.acs.client.api.model.VspDhcpDomainOption;
import net.nuage.vsp.acs.client.api.model.VspDhcpVMOption;
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.VspNetwork;
import net.nuage.vsp.acs.client.api.model.VspNic;
import net.nuage.vsp.acs.client.api.model.VspStaticNat;
import net.nuage.vsp.acs.client.api.model.VspVm;
import net.nuage.vsp.acs.client.common.model.Pair;

import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao;

import com.cloud.dc.Vlan;
import com.cloud.dc.VlanVO;
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.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkDetailsDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.manager.NuageVspManager;
import com.cloud.network.rules.FirewallRule;
import com.cloud.network.vpc.NetworkACLItem;
import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.VpcDao;
import com.cloud.offering.NetworkOffering;
import com.cloud.offerings.NetworkOfferingVO;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.NicProfile;
import com.cloud.vm.NicVO;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.NicSecondaryIpDao;
import com.cloud.vm.dao.NicSecondaryIpVO;
import com.cloud.vm.dao.VMInstanceDao;

public class NuageVspEntityBuilder {
    private static final Logger s_logger = Logger.getLogger(NuageVspEntityBuilder.class);

    @Inject
    NetworkDao _networkDao;
    @Inject
    VpcDao _vpcDao;
    @Inject
    DomainDao _domainDao;
    @Inject
    AccountDao _accountDao;
    @Inject
    NetworkOfferingDao _networkOfferingDao;
    @Inject
    NetworkOfferingServiceMapDao _networkOfferingServiceMapDao;
    @Inject
    NetworkModel _networkModel;
    @Inject
    VlanDao _vlanDao;
    @Inject
    VlanDetailsDao _vlanDetailsDao;
    @Inject
    IPAddressDao _ipAddressDao;
    @Inject
    NetworkDetailsDao _networkDetailsDao;
    @Inject
    VpcDetailsDao _vpcDetailsDao;
    @Inject
    VMInstanceDao _vmInstanceDao;
    @Inject
    NuageVspManager _nuageVspManager;
    @Inject
    NicDao _nicDao;
    @Inject
    NicSecondaryIpDao _nicSecondaryIpDao;
    @Inject
    NetworkOfferingServiceMapDao _ntwkOfferingSrvcDao;


    public VspDomain buildVspDomain(Domain domain) {
        return new VspDomain.Builder()
                .uuid(domain.getUuid())
                .name(domain.getName())
                .path(domain.getPath())
                .build();
    }

    public VspDomainCleanUp buildVspDomainCleanUp(Domain domain) {
        VspDomainCleanUp.Builder vspDomainCleanUpBuilder = new VspDomainCleanUp.Builder().uuid(domain.getUuid());

        Map<String, List<String>> sharedNetworkUuids = Maps.newHashMap();
        List<NetworkVO> allSharedNetworks = _networkDao.listByGuestType(Network.GuestType.Shared);
        for (NetworkVO sharedNetwork : allSharedNetworks) {
            if (_networkModel.isNetworkAvailableInDomain(sharedNetwork.getId(), domain.getId())) {
                String preConfiguredDomainTemplateName = _nuageVspManager.getPreConfiguredDomainTemplateName(sharedNetwork);
                if (!sharedNetworkUuids.containsKey(preConfiguredDomainTemplateName)) {
                    sharedNetworkUuids.put(preConfiguredDomainTemplateName, Lists.<String>newArrayList());
                }
                sharedNetworkUuids.get(preConfiguredDomainTemplateName).add(sharedNetwork.getUuid());
            }
        }
        vspDomainCleanUpBuilder.sharedNetworkUuids(sharedNetworkUuids);

        return vspDomainCleanUpBuilder.build();
    }

    public VspNetwork buildVspNetwork(Network network) {
        return buildVspNetwork(network.getDomainId(), network, null);
    }

    public VspNetwork buildVspNetwork(Network network, String vsdSubnetId) {
        return buildVspNetwork(network.getDomainId(), network, vsdSubnetId);
    }

    public VspNetwork buildVspNetwork(long domainId, Network network) {
        return buildVspNetwork(domainId, network, null);
    }

    public VspNetwork buildVspNetwork(long domainId, Network network, String vsdSubnetId) {
        VspNetwork.Builder vspNetworkBuilder = new VspNetwork.Builder()
                .id(network.getId())
                .uuid(network.getUuid())
                .name(network.getName())
                .cidr(network.getCidr())
                .gateway(network.getGateway());

        DomainVO domain = _domainDao.findById(domainId);
        VspDomain vspDomain = buildVspDomain(domain);
        vspNetworkBuilder.domain(vspDomain);


        AccountVO account = _accountDao.findById(network.getAccountId());
        if (account != null) {
            vspNetworkBuilder.accountUuid(account.getUuid()).accountName(account.getAccountName());
        }

        NetworkOfferingVO networkOffering = _networkOfferingDao.findById(network.getNetworkOfferingId());
        vspNetworkBuilder.egressDefaultPolicy(networkOffering.getEgressDefaultPolicy())
                         .publicAccess(networkOffering.getSupportsPublicAccess());

        Map<String, String> networkDetails = _networkDetailsDao.listDetailsKeyPairs(network.getId(), false);

        final NetworkRelatedVsdIds.Builder relatedVsdIdsBuilder = new NetworkRelatedVsdIds.Builder();

        if (MapUtils.isNotEmpty(networkDetails)) {
            relatedVsdIdsBuilder.vsdSubnetId(networkDetails.get(NuageVspManager.NETWORK_METADATA_VSD_SUBNET_ID))
                                .withVsdManaged("true".equals(networkDetails.get(NuageVspManager.NETWORK_METADATA_VSD_MANAGED)));
        } else if (vsdSubnetId != null) {
            relatedVsdIdsBuilder.vsdSubnetId(vsdSubnetId)
                                .withVsdManaged("true".equals(networkDetails.get(NuageVspManager.NETWORK_METADATA_VSD_MANAGED)));
        }

        if (network.getVpcId() != null) {
            long vpcId = network.getVpcId();
            VpcVO vpc = _vpcDao.findById(vpcId);
            vspNetworkBuilder.vpcUuid(vpc.getUuid())
                    .vpcName(vpc.getName())
                    .networkType(VspNetwork.NetworkType.Vpc);
            Map<String, String> vpcDetails = _vpcDetailsDao.listDetailsKeyPairs(vpcId, false);
            applyDomainAndZoneId(relatedVsdIdsBuilder, vpcDetails);
        } else {
            applyDomainAndZoneId(relatedVsdIdsBuilder, networkDetails);

            if (networkOffering.getGuestType() == Network.GuestType.Shared) {
                List<VlanVO> vlans = _vlanDao.listVlansByNetworkIdIncludingRemoved(network.getId());
                List<VspAddressRange> vspAddressRanges =
                        vlans.stream()
                             .map(vlan -> new VspAddressRange.Builder().gateway(vlan.getVlanGateway()).netmask(vlan.getVlanNetmask()).build())
                             .collect(Collectors.toList());


                vspNetworkBuilder.networkType(VspNetwork.NetworkType.Shared)
                                 .addressRanges(vspAddressRanges);
            } else if (_networkOfferingServiceMapDao.areServicesSupportedByNetworkOffering(network.getNetworkOfferingId(), Network.Service.SourceNat)
                    || _networkOfferingServiceMapDao.areServicesSupportedByNetworkOffering(network.getNetworkOfferingId(), Network.Service.StaticNat)) {
                vspNetworkBuilder.networkType(VspNetwork.NetworkType.L3);
            } else {
                vspNetworkBuilder.networkType(VspNetwork.NetworkType.L2);
            }

        }

        NetworkRelatedVsdIds networkRelatedVsdIds = relatedVsdIdsBuilder.build();
        vspNetworkBuilder.networkRelatedVsdIds(networkRelatedVsdIds);

        boolean firewallServiceSupported = _networkModel.areServicesSupportedByNetworkOffering(network.getNetworkOfferingId(), Network.Service.Firewall);
        vspNetworkBuilder.firewallServiceSupported(firewallServiceSupported);

        String preConfiguredDomainTemplateName = _nuageVspManager.getPreConfiguredDomainTemplateName(network);
        vspNetworkBuilder.domainTemplateName(preConfiguredDomainTemplateName);

        if (usesVirtualRouter(networkOffering.getId())) {
                String virtualRouterIp = getVirtualRouterIP(network);
                vspNetworkBuilder.virtualRouterIp(virtualRouterIp);
        }

        return vspNetworkBuilder.build();
    }

    private void applyDomainAndZoneId(NetworkRelatedVsdIds.Builder relatedVsdIdsBuilder, Map<String, String> details) {
        if (MapUtils.isNotEmpty(details)) {
            relatedVsdIdsBuilder
                    .vsdDomainId(details.get(NuageVspManager.NETWORK_METADATA_VSD_DOMAIN_ID))
                    .vsdZoneId(details.get(NuageVspManager.NETWORK_METADATA_VSD_ZONE_ID));
        }
    }

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

    public VspNetwork updateVspNetworkByPublicIp(VspNetwork vspNetwork, Network network, String publicIp) {
        List<VlanVO> vlans = _vlanDao.listVlansByNetworkId(network.getId());
        final long ip = NetUtils.ip2Long(publicIp);
        VlanVO matchingVlan = vlans.stream()
                                   .filter(vlan -> isVlanContainingIp(vlan, ip))
                                   .findFirst()
                                   .get();

        boolean underlayEnabled = NuageVspUtil.isUnderlayEnabledForVlan(_vlanDetailsDao, matchingVlan);
        return new VspNetwork.Builder().fromObject(vspNetwork)
                .gateway(matchingVlan.getVlanGateway())
                .cidr(NetUtils.getCidrFromGatewayAndNetmask(matchingVlan.getVlanGateway(), matchingVlan.getVlanNetmask()))
                .vlanUnderlay(underlayEnabled)
                .build();
    }

    private boolean isVlanContainingIp(Vlan vlan, long ip) {
        Pair<String, String> ipAddressRange =  NuageVspUtil.getIpAddressRange(vlan);
        long startIp = NetUtils.ip2Long(ipAddressRange.getLeft());
        long endIp = NetUtils.ip2Long(ipAddressRange.getRight());
        return startIp <= ip && ip <= endIp;
    }

    private String getVirtualRouterIP(Network network) {
        return network.getBroadcastUri() != null ? network.getBroadcastUri().getPath().substring(1) : null;
    }


    public VspVm buildVspVm(VirtualMachine vm, Network network) {
        VspVm.Builder vspVmBuilder = new VspVm.Builder()
                .uuid(vm.getUuid())
                .name(vm.getInstanceName())
                .state(getEnumValue(vm.getState(), VspVm.State.Unknown));

        boolean isDomainRouter = vm.getType().equals(VirtualMachine.Type.DomainRouter);
        vspVmBuilder.domainRouter(isDomainRouter);

        if (network.getBroadcastUri() != null) {
            String domainRouterIp = network.getBroadcastUri().getPath().substring(1);
            vspVmBuilder.domainRouterIp(domainRouterIp);
        }

        return vspVmBuilder.build();
    }

    public VspNic buildVspNic(String nicUuid, NicProfile nicProfile) {
        return buildVspNic(nicUuid, nicProfile.getMacAddress(), nicProfile.getIPv4Address(), nicProfile.getNetworkId(), null);
    }

    public VspNic buildVspNic(NicVO nic) {
        return buildVspNic(nic.getUuid(), nic.getMacAddress(), nic.getIPv4Address(), nic.getNetworkId(), null);
    }

    public VspNic buildVspNic(NicVO nic, NicSecondaryIpVO nicSecondaryIp) {
        return buildVspNic(nic.getUuid(), nic.getMacAddress(), nic.getIPv4Address(), nic.getNetworkId(), nicSecondaryIp);
    }

    private VspNic buildVspNic(String uuid, String macAddress, String ip, long networkId, NicSecondaryIpVO nicSecondaryIp) {
        VspNic.Builder vspNicBuilder = new VspNic.Builder()
                .uuid(uuid)
                .macAddress(macAddress)
                .useStaticIp(true)
                .ip(ip);

        if (nicSecondaryIp != null) {
            vspNicBuilder.secondaryIpUuid(nicSecondaryIp.getUuid()).secondaryIpAddress(nicSecondaryIp.getIp4Address());
        }

        Network network = _networkDao.findById(networkId);
        NetworkOffering networkOffering = _networkOfferingDao.findById(network.getNetworkOfferingId());

        return vspNicBuilder.build();
    }

    public VspStaticNat buildVspStaticNat(Boolean forRevoke, IPAddressVO staticNatIp, VlanVO staticNatVlan, VspNic vspNic) {
        VspStaticNat.Builder vspStaticNatBuilder = new VspStaticNat.Builder()
                .ipUuid(staticNatIp.getUuid())
                .ipAddress(staticNatIp.getAddress().addr())
                .revoke(forRevoke)
                .oneToOneNat(staticNatIp.isOneToOneNat())
                .state(getEnumValue(staticNatIp.getState(), VspStaticNat.State.class))
                .vlanUuid(staticNatVlan.getUuid())
                .vlanGateway(staticNatVlan.getVlanGateway())
                .vlanNetmask(staticNatVlan.getVlanNetmask())
                .vlanUnderlay(NuageVspUtil.isUnderlayEnabledForVlan(_vlanDetailsDao, staticNatVlan));

        if (staticNatIp.getVmIp() != null) {
            vspStaticNatBuilder.destinationIp(staticNatIp.getVmIp() + "/32");
        }

        if (vspNic != null) {
            vspStaticNatBuilder.nic(vspNic);
        }

        return vspStaticNatBuilder.build();
    }

    public VspStaticNat buildVspStaticNat(Boolean forRevoke, IPAddressVO staticNatIp, VlanVO staticNatVlan, NicVO nic) {
        NicSecondaryIpVO nicSecondaryIp = null;

        if (nic == null && staticNatIp.getAssociatedWithVmId() != null && staticNatIp.getVmIp() != null) {
            nicSecondaryIp = _nicSecondaryIpDao.findByIp4AddressAndInstanceId(staticNatIp.getAssociatedWithVmId(), staticNatIp.getVmIp());
            if (nicSecondaryIp != null) {
                nic = _nicDao.findById(nicSecondaryIp.getNicId());
            }
        }

        VspNic vspNic = (nic != null) ? buildVspNic(nic, nicSecondaryIp) : null;
        return buildVspStaticNat(forRevoke, staticNatIp, staticNatVlan, vspNic);
    }

    public VspAclRule buildVspAclRule(FirewallRule firewallRule, Network network) {
        return buildVspAclRule(firewallRule, network, null);
    }

    public VspAclRule buildVspAclRule(FirewallRule firewallRule, Network network, IPAddressVO staticNat) {
        VspAclRule.Builder vspAclRuleBuilder = new VspAclRule.Builder()
                .uuid(firewallRule.getUuid())
                .protocol(firewallRule.getProtocol())
                .startPort(firewallRule.getSourcePortStart())
                .endPort(firewallRule.getSourcePortEnd())
                .sourceCidrList(firewallRule.getSourceCidrList())
                .priority(-1)
                .type(VspAclRule.ACLType.Firewall)
                .state(getEnumValue(firewallRule.getState(), VspAclRule.ACLState.class))
                .trafficType(getEnumValue(firewallRule.getTrafficType(), VspAclRule.ACLTrafficType.class));

        NetworkOfferingVO networkOffering = _networkOfferingDao.findById(network.getNetworkOfferingId());
        if (firewallRule.getTrafficType() == FirewallRule.TrafficType.Egress && networkOffering.getEgressDefaultPolicy()) {
            vspAclRuleBuilder.deny();
        } else {
            vspAclRuleBuilder.allow();
        }

        if (staticNat == null && firewallRule.getSourceIpAddressId() != null) {
            IPAddressVO staticNatIp = _ipAddressDao.findById(firewallRule.getSourceIpAddressId());

            if (staticNatIp != null) {
                VlanVO staticNatVlan = _vlanDao.findById(staticNatIp.getVlanId());
                NicVO nic = _nicDao.findByIp4AddressAndNetworkId(staticNatIp.getVmIp(), staticNatIp.getAssociatedWithNetworkId());

                vspAclRuleBuilder.staticNat(buildVspStaticNat(null, staticNatIp, staticNatVlan, nic));
            }
        }

        return vspAclRuleBuilder.build();
    }

    public VspAclRule buildVspAclRule(NetworkACLItem networkAcl) {
        return new VspAclRule.Builder()
                .uuid(networkAcl.getUuid())
                .protocol(networkAcl.getProtocol())
                .startPort(networkAcl.getSourcePortStart())
                .endPort(networkAcl.getSourcePortEnd())
                .sourceIpAddress(null)
                .sourceCidrList(networkAcl.getSourceCidrList())
                .priority(networkAcl.getNumber())
                .type(VspAclRule.ACLType.NetworkACL)
                .state(getEnumValue(networkAcl.getState(), VspAclRule.ACLState.class))
                .trafficType(getEnumValue(networkAcl.getTrafficType(), VspAclRule.ACLTrafficType.class))
                .action(getEnumValue(networkAcl.getAction(), VspAclRule.ACLAction.class))
                .build();
    }

    /** Build VspDhcpVMOption to put on the VM interface */
    public VspDhcpVMOption buildVmDhcpOption (NicVO userNic, boolean defaultHasDns, boolean networkHasDns) {
        VMInstanceVO userVm  = _vmInstanceDao.findById(userNic.getInstanceId());
        VspDhcpVMOption.Builder vspDhcpVMOptionBuilder = new VspDhcpVMOption.Builder()
                .nicUuid(userNic.getUuid())
                .defaultHasDns(defaultHasDns)
                .hostname(userVm.getHostName())
                .networkHasDns(networkHasDns)
                .isDefaultInterface(userNic.isDefaultNic())
                .domainRouter(VirtualMachine.Type.DomainRouter.equals(userNic.getVmType()));
        return vspDhcpVMOptionBuilder.build();
    }

    /** Build VspDhcpVMOption to put on the subnet */
    public VspDhcpDomainOption buildNetworkDhcpOption(Network network, NetworkOffering offering) {
        List<String> dnsProvider = _ntwkOfferingSrvcDao.listProvidersForServiceForNetworkOffering(offering.getId(), Network.Service.Dns);
        boolean isVrDnsProvider = dnsProvider.contains("VirtualRouter") || dnsProvider.contains("VpcVirtualRouter");
        VspDhcpDomainOption.Builder vspDhcpDomainBuilder = new VspDhcpDomainOption.Builder()
                .dnsServers(_nuageVspManager.getDnsDetails(network.getDataCenterId()))
                .vrIsDnsProvider(isVrDnsProvider);

        if (isVrDnsProvider) {
            vspDhcpDomainBuilder.networkDomain(network.getVpcId() != null ? _vpcDao.findById(network.getVpcId()).getNetworkDomain() : network.getNetworkDomain());
        }

        return vspDhcpDomainBuilder.build();
    }

    private <E extends Enum<E>> E getEnumValue(Enum cloudstackValue, Class<E> target) {
        try {
            return Enum.valueOf(target, cloudstackValue.name());
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

    private <E extends Enum<E>> E getEnumValue(Enum cloudstackValue, E defaultValue) {
        try {
            return Enum.valueOf(defaultValue.getDeclaringClass(), cloudstackValue.name());
        } catch (IllegalArgumentException e) {
            return defaultValue;
        }
    }
}
