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

import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetVolumesOnStorageAnswer;
import com.cloud.agent.api.GetVolumesOnStorageCommand;
import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk;
import com.cloud.hypervisor.kvm.storage.KVMStoragePool;
import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.storage.Storage.StoragePoolType;
import org.apache.cloudstack.storage.volume.VolumeOnStorageTO;
import org.apache.cloudstack.utils.qemu.QemuImg;
import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat;
import org.apache.cloudstack.utils.qemu.QemuImgException;
import org.apache.cloudstack.utils.qemu.QemuImgFile;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.libvirt.LibvirtException;

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

@ResourceWrapper(handles = GetVolumesOnStorageCommand.class)
public final class LibvirtGetVolumesOnStorageCommandWrapper extends CommandWrapper<GetVolumesOnStorageCommand, Answer, LibvirtComputingResource> {

    static final List<StoragePoolType> STORAGE_POOL_TYPES_SUPPORTED_BY_QEMU_IMG = Arrays.asList(StoragePoolType.NetworkFilesystem,
            StoragePoolType.Filesystem, StoragePoolType.RBD);

    @Override
    public Answer execute(final GetVolumesOnStorageCommand command, final LibvirtComputingResource libvirtComputingResource) {

        final StorageFilerTO pool = command.getPool();
        final String volumePath = command.getVolumePath();
        final String keyword = command.getKeyword();

        final KVMStoragePoolManager storagePoolMgr = libvirtComputingResource.getStoragePoolMgr();
        final KVMStoragePool storagePool = storagePoolMgr.getStoragePool(pool.getType(), pool.getUuid(), true);

        if (StringUtils.isNotBlank(volumePath)) {
            return addVolumeByVolumePath(command, storagePool, volumePath);
        } else {
            return addAllVolumes(command, storagePool, keyword);
        }
    }

    private GetVolumesOnStorageAnswer addVolumeByVolumePath(final GetVolumesOnStorageCommand command, final KVMStoragePool storagePool, String volumePath) {
        List<VolumeOnStorageTO> volumes = new ArrayList<>();

        KVMPhysicalDisk disk = storagePool.getPhysicalDisk(volumePath);
        if (disk != null) {
            if (!volumePath.equals(disk.getPath()) && !volumePath.equals(disk.getName())) {
                String error = String.format("Volume path mismatch. Expected volume path (%s) is not the same as the actual name (%s) and path (%s)", volumePath, disk.getName(), disk.getPath());
                return new GetVolumesOnStorageAnswer(command, false, error);
            }
            if (!isDiskFormatSupported(disk)) {
                return new GetVolumesOnStorageAnswer(command, false, String.format("disk format %s is unsupported", disk.getFormat()));
            }
            Map<String, String> info = getDiskFileInfo(storagePool, disk, true);
            if (info == null) {
                return new GetVolumesOnStorageAnswer(command, false, "failed to get information of disk file. The disk might be locked or unsupported");
            }
            VolumeOnStorageTO volumeOnStorageTO = new VolumeOnStorageTO(Hypervisor.HypervisorType.KVM, disk.getName(), disk.getName(), disk.getPath(),
                    disk.getFormat().toString(), disk.getSize(), disk.getVirtualSize());
            if (disk.getQemuEncryptFormat() != null) {
                volumeOnStorageTO.setQemuEncryptFormat(disk.getQemuEncryptFormat().toString());
            }
            String fileFormat = info.get(QemuImg.FILE_FORMAT);
            if (StringUtils.isNotBlank(fileFormat) && !fileFormat.equalsIgnoreCase(disk.getFormat().toString())) {
                return new GetVolumesOnStorageAnswer(command, false, String.format("The file format is %s, but expected to be %s", fileFormat, disk.getFormat()));
            }
            addDetailsToVolumeOnStorageTO(volumeOnStorageTO, info, storagePool, disk);

            volumes.add(volumeOnStorageTO);
        }
        return new GetVolumesOnStorageAnswer(command, volumes);
    }

    private void addDetailsToVolumeOnStorageTO(VolumeOnStorageTO volumeOnStorageTO, final Map<String, String> info, final KVMStoragePool storagePool, final KVMPhysicalDisk disk) {
        if (MapUtils.isEmpty(info)) {
            return;
        }

        String backingFilePath = info.get(QemuImg.BACKING_FILE);
        if (StringUtils.isNotBlank(backingFilePath)) {
            volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.BACKING_FILE, backingFilePath);
        }
        String backingFileFormat = info.get(QemuImg.BACKING_FILE_FORMAT);
        if (StringUtils.isNotBlank(backingFileFormat)) {
            volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.BACKING_FILE_FORMAT, backingFileFormat);
        }
        String clusterSize = info.get(QemuImg.CLUSTER_SIZE);
        if (StringUtils.isNotBlank(clusterSize)) {
            volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.CLUSTER_SIZE, clusterSize);
        }
        String fileFormat = info.get(QemuImg.FILE_FORMAT);
        if (StringUtils.isNotBlank(fileFormat)) {
            volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.FILE_FORMAT, fileFormat);
        }
        String encrypted = info.get(QemuImg.ENCRYPTED);
        if (StringUtils.isNotBlank(encrypted) && encrypted.equalsIgnoreCase("yes")) {
            volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.IS_ENCRYPTED, String.valueOf(Boolean.TRUE));
        }
        Boolean isLocked = isDiskFileLocked(storagePool, disk);
        volumeOnStorageTO.addDetail(VolumeOnStorageTO.Detail.IS_LOCKED, String.valueOf(isLocked));
    }

    private GetVolumesOnStorageAnswer addAllVolumes(final GetVolumesOnStorageCommand command, final KVMStoragePool storagePool, String keyword) {
        List<VolumeOnStorageTO> volumes = new ArrayList<>();

        List<KVMPhysicalDisk> disks = storagePool.listPhysicalDisks();
        if (StringUtils.isNotBlank(keyword)) {
            disks = disks.stream().filter(disk -> disk.getName().contains(keyword)).collect(Collectors.toList());
        }
        disks.sort(Comparator.comparing(KVMPhysicalDisk::getName));
        for (KVMPhysicalDisk disk: disks) {
            if (!isDiskFormatSupported(disk)) {
                continue;
            }
            Map<String, String> info = getDiskFileInfo(storagePool, disk, true);
            if (info == null) {
                continue;
            }
            VolumeOnStorageTO volumeOnStorageTO = new VolumeOnStorageTO(Hypervisor.HypervisorType.KVM, disk.getName(), disk.getName(), disk.getPath(),
                    disk.getFormat().toString(), disk.getSize(), disk.getVirtualSize());
            if (disk.getQemuEncryptFormat() != null) {
                volumeOnStorageTO.setQemuEncryptFormat(disk.getQemuEncryptFormat().toString());
            }
            String fileFormat = info.get(QemuImg.FILE_FORMAT);
            if (StringUtils.isNotBlank(fileFormat) && !fileFormat.equalsIgnoreCase(disk.getFormat().toString())) {
                continue;
            }
            addDetailsToVolumeOnStorageTO(volumeOnStorageTO, info, storagePool, disk);

            volumes.add(volumeOnStorageTO);
        }
        return new GetVolumesOnStorageAnswer(command, volumes);
    }

    private boolean isDiskFormatSupported(KVMPhysicalDisk disk) {
        return PhysicalDiskFormat.QCOW2.equals(disk.getFormat()) || PhysicalDiskFormat.RAW.equals(disk.getFormat());
    }

    private boolean isDiskFileLocked(KVMStoragePool pool, KVMPhysicalDisk disk) {
        Map<String, String> info = getDiskFileInfo(pool, disk, false);
        return info == null;
    }

    private Map<String, String> getDiskFileInfo(KVMStoragePool pool, KVMPhysicalDisk disk, boolean secure) {
        if (!STORAGE_POOL_TYPES_SUPPORTED_BY_QEMU_IMG.contains(pool.getType())) {
            return new HashMap<>(); // unknown
        }
        try {
            QemuImg qemu = new QemuImg(0);
            QemuImgFile qemuFile = new QemuImgFile(disk.getPath(), disk.getFormat());
            if (StoragePoolType.RBD.equals(pool.getType())) {
                String rbdDestFile = KVMPhysicalDisk.RBDStringBuilder(pool, disk.getPath());
                qemuFile = new QemuImgFile(rbdDestFile, disk.getFormat());
            }
            return qemu.info(qemuFile, secure);
        } catch (QemuImgException | LibvirtException ex) {
            logger.error("Failed to get info of disk file: " + ex.getMessage());
            return null;
        }
    }
}
