/*
 * Licensed 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.hypervisor.kvm.resource.wrapper;

import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ScaleVmAnswer;
import com.cloud.agent.api.ScaleVmCommand;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.resource.LibvirtVmMemoryDeviceDef;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;

@ResourceWrapper(handles = ScaleVmCommand.class)
public class LibvirtScaleVmCommandWrapper extends CommandWrapper<ScaleVmCommand, Answer, LibvirtComputingResource> {

    @Override
    public Answer execute(ScaleVmCommand command, LibvirtComputingResource libvirtComputingResource) {
        VirtualMachineTO vmSpec = command.getVirtualMachine();
        String vmName = vmSpec.getName();
        Connect conn = null;

        long newMemory = ByteScaleUtils.bytesToKibibytes(vmSpec.getMaxRam());
        int newVcpus = vmSpec.getCpus();
        int newCpuShares = libvirtComputingResource.calculateCpuShares(vmSpec);
        String vmDefinition = vmSpec.toString();
        String scalingDetails = String.format("%s memory to [%s KiB], CPU cores to [%s] and cpu_shares to [%s]", vmDefinition, newMemory, newVcpus, newCpuShares);

        try {
            LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper();

            conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName);
            Domain dm = conn.domainLookupByName(vmName);

            logger.debug(String.format("Scaling %s.", scalingDetails));
            scaleMemory(dm, newMemory, vmDefinition);
            scaleVcpus(dm, newVcpus, vmDefinition);
            updateCpuShares(dm, newCpuShares);

            return new ScaleVmAnswer(command, true, String.format("Successfully scaled %s.", scalingDetails));
        } catch (LibvirtException | CloudRuntimeException e) {
            String message = String.format("Unable to scale %s due to [%s].", scalingDetails, e.getMessage());
            logger.error(message, e);
            return new ScaleVmAnswer(command, false, message);
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (LibvirtException ex) {
                    logger.warn(String.format("Error trying to close libvirt connection [%s]", ex.getMessage()), ex);
                }
            }
        }
    }

    /**
     * Sets the cpu_shares (priority) of the running VM. This is necessary because the priority is only calculated when deploying the VM.
     * To prevent the cpu_shares to be manually updated by using the command virsh schedinfo or restarting the VM. This method updates the cpu_shares of a running VM on the fly.
     * @param dm domain of the VM.
     * @param newCpuShares new priority of the running VM.
     * @throws org.libvirt.LibvirtException
     **/
    protected void updateCpuShares(Domain dm, int newCpuShares) throws LibvirtException {
        int oldCpuShares = LibvirtComputingResource.getCpuShares(dm);

        if (oldCpuShares < newCpuShares) {
            LibvirtComputingResource.setCpuShares(dm, newCpuShares);
            logger.info(String.format("Successfully increased cpu_shares of VM [%s] from [%s] to [%s].", dm.getName(), oldCpuShares, newCpuShares));
        }
    }

    protected void scaleVcpus(Domain dm, int newVcpus, String vmDefinition) throws LibvirtException {
        long runningVcpus = LibvirtComputingResource.countDomainRunningVcpus(dm);

        if (runningVcpus < newVcpus) {
            dm.setVcpus(newVcpus);
            return;
        }

        logger.info(String.format("Not scaling the CPU cores. To scale the CPU cores of the %s, the new CPU count [%s] must be higher than the current CPU count [%s].",
            vmDefinition, newVcpus, runningVcpus));
    }

    protected void scaleMemory(Domain dm, long newMemory, String vmDefinition) throws LibvirtException, CloudRuntimeException {
        long currentMemory = LibvirtComputingResource.getDomainMemory(dm);
        long memoryToAttach = newMemory - currentMemory;

        if (memoryToAttach <= 0) {
            logger.info(String.format("Not scaling the memory. To scale the memory of the %s, the new memory [%s] must be higher than the current memory [%s]. The current "
              + "difference is [%s].", vmDefinition, newMemory, currentMemory, memoryToAttach));
            return;
        }

        if (!dm.getXMLDesc(0).contains("<maxMemory slots='16' unit='KiB'>")) {
            throw new CloudRuntimeException(String.format("The %s is not prepared for dynamic scaling. To be prepared, the VM must be deployed with a dynamic service offering,"
              + " VM dynamic scale enabled and global setting \"enable.dynamic.scale.vm\" as \"true\". If you changed one of these settings after deploying the VM,"
              + " consider stopping and starting it again to prepared it to dynamic scaling.", vmDefinition));
        }

        String memoryDevice = new LibvirtVmMemoryDeviceDef(memoryToAttach).toString();
        logger.debug(String.format("Attaching memory device [%s] to %s.", memoryDevice, vmDefinition));
        dm.attachDevice(memoryDevice);
    }
}
