//
// 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 java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

import org.apache.cloudstack.backup.BackupAnswer;
import org.apache.cloudstack.backup.RestoreBackupCommand;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;

import com.cloud.agent.api.Answer;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.script.Script;
import com.cloud.vm.VirtualMachine;

@ResourceWrapper(handles = RestoreBackupCommand.class)
public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper<RestoreBackupCommand, Answer, LibvirtComputingResource> {
    private static final String BACKUP_TEMP_FILE_PREFIX = "csbackup";
    private static final String FILE_PATH_PLACEHOLDER = "%s/%s";

    @Override
    public Answer execute(RestoreBackupCommand command, LibvirtComputingResource serverResource) {
        String vmName = command.getVmName();
        String backupPath = command.getBackupPath();
        String backupRepoAddress = command.getBackupRepoAddress();
        String backupRepoType = command.getBackupRepoType();
        String mountOptions = command.getMountOptions();
        Boolean vmExists = command.isVmExists();
        String diskType = command.getDiskType();
        List<String> volumePaths = command.getVolumePaths();
        List<String> backupFiles = command.getBackupFiles();

        String newVolumeId = null;
        try {
            if (Objects.isNull(vmExists)) {
                String volumePath = volumePaths.get(0);
                String backupFile = backupFiles.get(0);
                int lastIndex = volumePath.lastIndexOf("/");
                newVolumeId = volumePath.substring(lastIndex + 1);
                restoreVolume(backupPath, backupRepoType, backupRepoAddress, volumePath, diskType, backupFile,
                        new Pair<>(vmName, command.getVmState()), mountOptions);
            } else if (Boolean.TRUE.equals(vmExists)) {
                restoreVolumesOfExistingVM(volumePaths, backupPath, backupFiles, backupRepoType, backupRepoAddress, mountOptions);
            } else {
                restoreVolumesOfDestroyedVMs(volumePaths, vmName, backupPath, backupFiles, backupRepoType, backupRepoAddress, mountOptions);
            }
        } catch (CloudRuntimeException e) {
            String errorMessage = "Failed to restore backup for VM: " + vmName + ".";
            if (e.getMessage() != null && !e.getMessage().isEmpty()) {
                errorMessage += " Details: " + e.getMessage();
            }
            logger.error(errorMessage);
            return new BackupAnswer(command, false, errorMessage);
        }

        return new BackupAnswer(command, true, newVolumeId);
    }

    private void restoreVolumesOfExistingVM(List<String> volumePaths, String backupPath, List<String> backupFiles,
                                             String backupRepoType, String backupRepoAddress, String mountOptions) {
        String diskType = "root";
        String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
        try {
            for (int idx = 0; idx < volumePaths.size(); idx++) {
                String volumePath = volumePaths.get(idx);
                String backupFile = backupFiles.get(idx);
                String bkpPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType);
                diskType = "datadisk";
                if (!replaceVolumeWithBackup(volumePath, bkpPath)) {
                    throw new CloudRuntimeException(String.format("Unable to restore backup from volume [%s].", volumePath));
                }
            }
        } finally {
            unmountBackupDirectory(mountDirectory);
            deleteTemporaryDirectory(mountDirectory);
        }

    }

    private void restoreVolumesOfDestroyedVMs(List<String> volumePaths, String vmName, String backupPath, List<String> backupFiles,
                                              String backupRepoType, String backupRepoAddress, String mountOptions) {
        String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
        String diskType = "root";
        try {
            for (int idx = 0; idx < volumePaths.size(); idx++) {
                String volumePath = volumePaths.get(idx);
                String backupFile = backupFiles.get(idx);
                String bkpPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType);
                diskType = "datadisk";
                if (!replaceVolumeWithBackup(volumePath, bkpPath)) {
                    throw new CloudRuntimeException(String.format("Unable to restore backup from volume [%s].", volumePath));
                }
            }
        } finally {
            unmountBackupDirectory(mountDirectory);
            deleteTemporaryDirectory(mountDirectory);
        }
    }

    private void restoreVolume(String backupPath, String backupRepoType, String backupRepoAddress, String volumePath,
                               String diskType, String backupFile, Pair<String, VirtualMachine.State> vmNameAndState, String mountOptions) {
        String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions);
        String bkpPath;
        try {
            bkpPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType);
            if (!replaceVolumeWithBackup(volumePath, bkpPath)) {
                throw new CloudRuntimeException(String.format("Unable to restore backup from volume [%s].", volumePath));
            }
            if (VirtualMachine.State.Running.equals(vmNameAndState.second())) {
                if (!attachVolumeToVm(vmNameAndState.first(), volumePath)) {
                    throw new CloudRuntimeException(String.format("Failed to attach volume to VM: %s", vmNameAndState.first()));
                }
            }
        } catch (Exception e) {
            throw new CloudRuntimeException("Failed to restore volume", e);
        } finally {
            unmountBackupDirectory(mountDirectory);
            deleteTemporaryDirectory(mountDirectory);
        }
    }


    private String mountBackupDirectory(String backupRepoAddress, String backupRepoType, String mountOptions) {
        String randomChars = RandomStringUtils.random(5, true, false);
        String mountDirectory = String.format("%s.%s",BACKUP_TEMP_FILE_PREFIX , randomChars);
        try {
            mountDirectory = Files.createTempDirectory(mountDirectory).toString();
            String mountPath = Script.getExecutableAbsolutePath("mount");
            List<String> mountCmd = new ArrayList<>();
            mountCmd.add("sudo");
            mountCmd.add(mountPath);
            mountCmd.add("-t");
            mountCmd.add(backupRepoType);
            mountCmd.add(backupRepoAddress);
            mountCmd.add(mountDirectory);
            if ("cifs".equals(backupRepoType)) {
                if (StringUtils.isBlank(mountOptions)) {
                    mountOptions = "nobrl";
                } else {
                    mountOptions += ",nobrl";
                }
            }
            if (StringUtils.isNotBlank(mountOptions)) {
                mountCmd.add("-o");
                mountCmd.add(mountOptions);
            }
            Script.executeCommand(mountCmd.toArray(new String[0]));
        } catch (Exception e) {
            throw new CloudRuntimeException(String.format("Failed to mount %s to %s", backupRepoType, backupRepoAddress), e);
        }
        return mountDirectory;
    }

    private void unmountBackupDirectory(String backupDirectory) {
        try {
            String umountPath = Script.getExecutableAbsolutePath("umount");
            String[] umountCmd = new String[] { "sudo", umountPath, backupDirectory };
            Script.executeCommand(umountCmd);
        } catch (Exception e) {
            throw new CloudRuntimeException(String.format("Failed to unmount backup directory: %s", backupDirectory), e);
        }
    }

    private void deleteTemporaryDirectory(String backupDirectory) {
        try {
            Files.deleteIfExists(Paths.get(backupDirectory));
        } catch (IOException e) {
            throw new CloudRuntimeException(String.format("Failed to delete backup directory: %s", backupDirectory), e);
        }
    }

    private String getBackupPath(String mountDirectory, String backupPath, String backupFile, String diskType) {
        String bkpPath = String.format(FILE_PATH_PLACEHOLDER, mountDirectory, backupPath);
        String backupFileName = String.format("%s.%s.qcow2", diskType.toLowerCase(Locale.ROOT), backupFile);
        bkpPath = String.format(FILE_PATH_PLACEHOLDER, bkpPath, backupFileName);
        return bkpPath;
    }

    private boolean replaceVolumeWithBackup(String volumePath, String backupPath) {
        String[] rsyncCmd = new String[] { Script.getExecutableAbsolutePath("rsync"), "-az", backupPath, volumePath };
        int exitValue = Script.executeCommandForExitValue(rsyncCmd);
        return exitValue == 0;
    }

    private boolean attachVolumeToVm(String vmName, String volumePath) {
        String deviceToAttachDiskTo = getDeviceToAttachDisk(vmName);
        String[] attachCmd = new String[] { Script.getExecutableAbsolutePath("virsh"), "attach-disk", vmName, volumePath, deviceToAttachDiskTo,
                "--driver", "qemu", "--subdriver", "qcow2", "--cache", "none" };
        int exitValue = Script.executeCommandForExitValue(attachCmd);
        return exitValue == 0;
    }

    private String getDeviceToAttachDisk(String vmName) {
        String[] domblkCmd = new String[] { Script.getExecutableAbsolutePath("virsh"), "domblklist", "--domain", vmName };
        String[] tailCmd = new String[] { Script.getExecutableAbsolutePath("tail"), "-n", "3" };
        String[] headCmd = new String[] { Script.getExecutableAbsolutePath("head"), "-n", "1" };
        String[] awkCmd = new String[] { Script.getExecutableAbsolutePath("awk"), "'{print $1}'" };
        Pair<Integer, String> result = Script.executePipedCommands(Arrays.asList(domblkCmd, tailCmd, headCmd, awkCmd), 0);
        String currentDevice = result.second();
        char lastChar = currentDevice.charAt(currentDevice.length() - 1);
        char incrementedChar = (char) (lastChar + 1);
        return currentDevice.substring(0, currentDevice.length() - 1) + incrementedChar;
    }
}
