// 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 org.apache.cloudstack.storage.template;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.inject.Inject;

import com.cloud.dc.DataCenter;
import com.cloud.exception.InsufficientAddressCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.IpAddress;
import com.cloud.network.IpAddressManager;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.NetworkService;
import com.cloud.network.VNF;
import com.cloud.network.dao.FirewallRulesDao;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.firewall.FirewallService;
import com.cloud.network.rules.FirewallRule;
import com.cloud.network.rules.FirewallRuleVO;
import com.cloud.network.rules.RulesService;
import com.cloud.network.security.SecurityGroup;
import com.cloud.network.security.SecurityGroupManager;
import com.cloud.network.security.SecurityGroupService;
import com.cloud.network.security.SecurityGroupVO;
import com.cloud.network.security.SecurityRule;
import com.cloud.storage.VnfTemplateDetailVO;
import com.cloud.storage.VnfTemplateNicVO;
import com.cloud.storage.dao.VnfTemplateDetailsDao;
import com.cloud.storage.dao.VnfTemplateNicDao;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.user.Account;
import com.cloud.uservm.UserVm;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallbackWithExceptionNoReturn;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.NicVO;
import com.cloud.vm.dao.NicDao;
import org.apache.cloudstack.api.command.admin.template.ListVnfTemplatesCmdByAdmin;
import org.apache.cloudstack.api.command.admin.template.RegisterVnfTemplateCmdByAdmin;
import org.apache.cloudstack.api.command.admin.template.UpdateVnfTemplateCmdByAdmin;
import org.apache.cloudstack.api.command.admin.vm.DeployVnfApplianceCmdByAdmin;
import org.apache.cloudstack.api.command.admin.vm.ListVnfAppliancesCmdByAdmin;
import org.apache.cloudstack.api.command.user.template.DeleteVnfTemplateCmd;
import org.apache.cloudstack.api.command.user.template.ListVnfTemplatesCmd;
import org.apache.cloudstack.api.command.user.template.RegisterVnfTemplateCmd;
import org.apache.cloudstack.api.command.user.template.UpdateVnfTemplateCmd;
import org.apache.cloudstack.api.command.user.vm.DeployVnfApplianceCmd;
import org.apache.cloudstack.api.command.user.vm.ListVnfAppliancesCmd;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;


public class VnfTemplateManagerImpl extends ManagerBase implements VnfTemplateManager, PluggableService, Configurable {

    public static final String VNF_SECURITY_GROUP_NAME = "VNF_SecurityGroup_";
    public static final String ACCESS_METHOD_SEPARATOR = ",";
    public static final Integer ACCESS_DEFAULT_SSH_PORT = 22;
    public static final Integer ACCESS_DEFAULT_HTTP_PORT = 80;
    public static final Integer ACCESS_DEFAULT_HTTPS_PORT = 443;

    @Inject
    VnfTemplateDetailsDao vnfTemplateDetailsDao;
    @Inject
    VnfTemplateNicDao vnfTemplateNicDao;
    @Inject
    SecurityGroupManager securityGroupManager;
    @Inject
    SecurityGroupService securityGroupService;
    @Inject
    NetworkModel networkModel;
    @Inject
    IpAddressManager ipAddressManager;
    @Inject
    NicDao nicDao;
    @Inject
    NetworkDao networkDao;
    @Inject
    NetworkService networkService;
    @Inject
    RulesService rulesService;
    @Inject
    FirewallRulesDao firewallRulesDao;
    @Inject
    FirewallService firewallService;

    @Override
    public List<Class<?>> getCommands() {
        final List<Class<?>> cmdList = new ArrayList<>();
        if (!VnfTemplateAndApplianceEnabled.value()) {
            return cmdList;
        }
        cmdList.add(RegisterVnfTemplateCmd.class);
        cmdList.add(RegisterVnfTemplateCmdByAdmin.class);
        cmdList.add(ListVnfTemplatesCmd.class);
        cmdList.add(ListVnfTemplatesCmdByAdmin.class);
        cmdList.add(UpdateVnfTemplateCmd.class);
        cmdList.add(UpdateVnfTemplateCmdByAdmin.class);
        cmdList.add(DeleteVnfTemplateCmd.class);
        cmdList.add(DeployVnfApplianceCmd.class);
        cmdList.add(DeployVnfApplianceCmdByAdmin.class);
        cmdList.add(ListVnfAppliancesCmd.class);
        cmdList.add(ListVnfAppliancesCmdByAdmin.class);
        return cmdList;
    }

    @Override
    public void persistVnfTemplate(long templateId, RegisterVnfTemplateCmd cmd) {
        persistVnfTemplateNics(templateId, cmd.getVnfNics());
        persistVnfTemplateDetails(templateId, cmd);
    }

    private void persistVnfTemplateNics(long templateId, List<VNF.VnfNic> nics) {
        for (VNF.VnfNic nic : nics) {
            VnfTemplateNicVO vnfTemplateNicVO = new VnfTemplateNicVO(templateId, nic.getDeviceId(), nic.getName(), nic.isRequired(), nic.isManagement(), nic.getDescription());
            vnfTemplateNicDao.persist(vnfTemplateNicVO);
        }
    }

    private void persistVnfTemplateDetails(long templateId, RegisterVnfTemplateCmd cmd) {
        persistVnfTemplateDetails(templateId, cmd.getVnfDetails());
    }

    private void persistVnfTemplateDetails(long templateId, Map<String, String> vnfDetails) {
        for (Map.Entry<String, String> entry:  vnfDetails.entrySet()) {
            String value = entry.getValue();
            if (VNF.AccessDetail.ACCESS_METHODS.name().equalsIgnoreCase(entry.getKey())) {
                value = Arrays.stream(value.split(ACCESS_METHOD_SEPARATOR)).sorted().collect(Collectors.joining(ACCESS_METHOD_SEPARATOR));
            }
            vnfTemplateDetailsDao.addDetail(templateId, entry.getKey().toLowerCase(), value, true);
        }
    }

    @Override
    public void updateVnfTemplate(long templateId, UpdateVnfTemplateCmd cmd) {
        updateVnfTemplateDetails(templateId, cmd);
        updateVnfTemplateNics(templateId, cmd);
    }

    private void updateVnfTemplateDetails(long templateId, UpdateVnfTemplateCmd cmd) {
        boolean cleanupVnfDetails = cmd.isCleanupVnfDetails();
        if (cleanupVnfDetails) {
            vnfTemplateDetailsDao.removeDetails(templateId);
        } else if (MapUtils.isNotEmpty(cmd.getVnfDetails())) {
            vnfTemplateDetailsDao.removeDetails(templateId);
            persistVnfTemplateDetails(templateId, cmd.getVnfDetails());
        }
    }

    private void updateVnfTemplateNics(long templateId, UpdateVnfTemplateCmd cmd) {
        boolean cleanupVnfNics = cmd.isCleanupVnfNics();
        if (cleanupVnfNics) {
            vnfTemplateNicDao.deleteByTemplateId(templateId);
        } else if (CollectionUtils.isNotEmpty(cmd.getVnfNics())) {
            vnfTemplateNicDao.deleteByTemplateId(templateId);
            persistVnfTemplateNics(templateId, cmd.getVnfNics());
        }
    }

    @Override
    public String getConfigComponentName() {
        return VnfTemplateManager.class.getSimpleName();
    }

    @Override
    public ConfigKey<?>[] getConfigKeys() {
        return new ConfigKey[] { VnfTemplateAndApplianceEnabled };
    }

    @Override
    public void validateVnfApplianceNics(VirtualMachineTemplate template, List<Long> networkIds) {
        if (CollectionUtils.isEmpty(networkIds)) {
            throw new InvalidParameterValueException("VNF nics list is empty");
        }
        List<VnfTemplateNicVO> vnfNics = vnfTemplateNicDao.listByTemplateId(template.getId());
        for (VnfTemplateNicVO vnfNic : vnfNics) {
            if (vnfNic.isRequired() && networkIds.size() <= vnfNic.getDeviceId()) {
                throw new InvalidParameterValueException("VNF nic is required but not found: " + vnfNic);
            }
        }
    }

    protected Set<Integer> getOpenPortsForVnfAppliance(VirtualMachineTemplate template) {
        Set<Integer> ports = new HashSet<>();
        VnfTemplateDetailVO accessMethodsDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.ACCESS_METHODS.name().toLowerCase());
        if (accessMethodsDetail == null || accessMethodsDetail.getValue() == null) {
            return ports;
        }
        String[] accessMethods = accessMethodsDetail.getValue().split(ACCESS_METHOD_SEPARATOR);
        for (String accessMethod : accessMethods) {
            if (VNF.AccessMethod.SSH_WITH_KEY.toString().equalsIgnoreCase(accessMethod)
                    || VNF.AccessMethod.SSH_WITH_PASSWORD.toString().equalsIgnoreCase(accessMethod)) {
                VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.SSH_PORT.name().toLowerCase());
                if (accessDetail == null) {
                    ports.add(ACCESS_DEFAULT_SSH_PORT);
                } else {
                    ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_SSH_PORT));
                }
            } else if (VNF.AccessMethod.HTTP.toString().equalsIgnoreCase(accessMethod)) {
                VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.HTTP_PORT.name().toLowerCase());
                if (accessDetail == null) {
                    ports.add(ACCESS_DEFAULT_HTTP_PORT);
                } else {
                    ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_HTTP_PORT));
                }
            } else if (VNF.AccessMethod.HTTPS.toString().equalsIgnoreCase(accessMethod)) {
                VnfTemplateDetailVO accessDetail = vnfTemplateDetailsDao.findDetail(template.getId(), VNF.AccessDetail.HTTPS_PORT.name().toLowerCase());
                if (accessDetail == null) {
                    ports.add(ACCESS_DEFAULT_HTTPS_PORT);
                } else {
                    ports.add(NumbersUtil.parseInt(accessDetail.getValue(), ACCESS_DEFAULT_HTTPS_PORT));
                }
            }
        }
        return ports;
    }

    private Set<Long> getDeviceIdsOfVnfManagementNics(VirtualMachineTemplate template) {
        Set<Long> deviceIds = new HashSet<>();
        for (VnfTemplateNicVO nic : vnfTemplateNicDao.listByTemplateId(template.getId())) {
            if (nic.isManagement()) {
                deviceIds.add(nic.getDeviceId());
            }
        }
        return deviceIds;
    }

    protected Map<Network, String> getManagementNetworkAndIp(VirtualMachineTemplate template, UserVm vm) {
        Map<Network, String> networkAndIpMap = new HashMap<>();
        Set<Long> managementDeviceIds = getDeviceIdsOfVnfManagementNics(template);
        for (NicVO nic : nicDao.listByVmId(vm.getId())) {
            if (managementDeviceIds.contains((long) nic.getDeviceId()) && nic.getIPv4Address() != null) {
                Network network = networkDao.findById(nic.getNetworkId());
                if (network == null || !Network.GuestType.Isolated.equals(network.getGuestType())) {
                    continue;
                }
                if (!networkModel.areServicesSupportedInNetwork(network.getId(), Network.Service.StaticNat)) {
                    logger.info("Network: {} does not support static nat, " +
                            "skipping this network configuration for VNF appliance", network);
                    continue;
                }
                if (network.getVpcId() != null) {
                    logger.info("Network: {} is a VPC tier, " +
                            "skipping this network configuration for VNF appliance", network);
                    continue;
                }
                if (!networkModel.areServicesSupportedInNetwork(network.getId(), Network.Service.Firewall)) {
                    logger.info("Network: {} does not support firewall, " +
                            "skipping this network configuration for VNF appliance", network);
                    continue;
                }
                networkAndIpMap.put(network, nic.getIPv4Address());
            }
        }
        return networkAndIpMap;
    }

    @Override
    public SecurityGroup createSecurityGroupForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner,
                                                            DeployVnfApplianceCmd cmd) {
        if (zone == null || !(zone.isSecurityGroupEnabled() || networkModel.isSecurityGroupSupportedForZone(zone.getId()))) {
            return null;
        }
        if (!cmd.getVnfConfigureManagement()) {
            return null;
        }
        logger.debug("Creating security group and rules for VNF appliance");
        Set<Integer> ports = getOpenPortsForVnfAppliance(template);
        if (ports.size() == 0) {
            logger.debug("No need to create security group and rules for VNF appliance as there is no ports to be open");
            return null;
        }
        String securityGroupName = VNF_SECURITY_GROUP_NAME.concat(Long.toHexString(System.currentTimeMillis()));
        SecurityGroupVO securityGroupVO = securityGroupManager.createSecurityGroup(securityGroupName,
                "Security group for VNF appliance", owner.getDomainId(), owner.getId(), owner.getAccountName());
        if (securityGroupVO == null) {
            throw new CloudRuntimeException(String.format("Failed to create security group: %s", securityGroupName));
        }
        List<String> cidrList = cmd.getVnfCidrlist();
        for (Integer port : ports) {
            securityGroupService.authorizeSecurityGroupRule(securityGroupVO.getId(), NetUtils.TCP_PROTO, port, port,
                    null, null, cidrList, null, SecurityRule.SecurityRuleType.IngressRule);
        }
        return securityGroupVO;
    }

    @Override
    public void createIsolatedNetworkRulesForVnfAppliance(DataCenter zone, VirtualMachineTemplate template, Account owner,
                                                          UserVm vm, DeployVnfApplianceCmd cmd)
            throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException {

        Map<Network, String> networkAndIpMap = getManagementNetworkAndIp(template, vm);
        Set<Integer> ports = getOpenPortsForVnfAppliance(template);
        for (Map.Entry<Network, String> entry : networkAndIpMap.entrySet()) {
            Network network = entry.getKey();
            logger.debug("Creating network rules for VNF appliance on isolated network {}", network);
            String ip = entry.getValue();
            IpAddress publicIp = networkService.allocateIP(owner, zone.getId(), network.getId(), null, null);
            if (publicIp == null) {
                continue;
            }
            publicIp = ipAddressManager.associateIPToGuestNetwork(publicIp.getId(), network.getId(), false);
            if (publicIp.isSourceNat()) {
                // If isolated network is not implemented, the first acquired Public IP will be Source NAT IP
                publicIp = networkService.allocateIP(owner, zone.getId(), network.getId(), null, null);
                if (publicIp == null) {
                    continue;
                }
                publicIp = ipAddressManager.associateIPToGuestNetwork(publicIp.getId(), network.getId(), false);
            }
            final IpAddress publicIpFinal = publicIp;
            final List<String> cidrList = cmd.getVnfCidrlist();
            try {
                boolean result = rulesService.enableStaticNat(publicIp.getId(), vm.getId(), network.getId(), ip);
                if (!result) {
                    throw new CloudRuntimeException(String.format("Failed to create static nat for vm: %s", vm.getUuid()));
                }
            } catch (NetworkRuleConflictException e) {
                throw new CloudRuntimeException(String.format("Failed to create static nat for vm %s due to %s", vm.getUuid(), e.getMessage()));
            }
            if (network.getVpcId() == null) {
                Transaction.execute(new TransactionCallbackWithExceptionNoReturn<>() {
                    @Override
                    public void doInTransactionWithoutResult(final TransactionStatus status) throws CloudRuntimeException {
                        for (Integer port : ports) {
                            FirewallRuleVO newFirewallRule = new FirewallRuleVO(null, publicIpFinal.getId(), port, port, NetUtils.TCP_PROTO,
                                    network.getId(), owner.getAccountId(), owner.getDomainId(), FirewallRule.Purpose.Firewall,
                                    cidrList, null, null, null, FirewallRule.TrafficType.Ingress);
                            newFirewallRule.setDisplay(true);
                            newFirewallRule.setState(FirewallRule.State.Add);
                            firewallRulesDao.persist(newFirewallRule);
                        }
                    }
                });
                firewallService.applyIngressFwRules(publicIp.getId(), owner);
            }
            logger.debug("Created network rules for VNF appliance on isolated network {}", network);
        }
    }
}
