/*
 * 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.datastore.driver;

import static com.cloud.utils.NumbersUtil.toHumanReadableSize;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.inject.Inject;

import com.cloud.agent.api.to.DiskTO;
import com.cloud.storage.VolumeVO;
import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.StorageAction;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.storage.command.CommandResult;
import org.apache.cloudstack.storage.command.CopyCmdAnswer;
import org.apache.cloudstack.storage.command.CopyCommand;
import org.apache.cloudstack.storage.command.CreateObjectCommand;
import org.apache.cloudstack.storage.command.DeleteCommand;
import org.apache.cloudstack.storage.command.RevertSnapshotCommand;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
import org.apache.cloudstack.storage.volume.VolumeObject;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import com.cloud.agent.api.Answer;
import com.cloud.agent.api.storage.ResizeVolumeAnswer;
import com.cloud.agent.api.storage.ResizeVolumeCommand;
import com.cloud.agent.api.to.DataObjectType;
import com.cloud.agent.api.to.DataStoreTO;
import com.cloud.agent.api.to.DataTO;
import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.exception.StorageUnavailableException;
import com.cloud.host.Host;
import com.cloud.host.dao.HostDao;
import com.cloud.storage.CreateSnapshotPayload;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.Storage;
import com.cloud.storage.Storage.StoragePoolType;
import com.cloud.storage.StorageManager;
import com.cloud.storage.StoragePool;
import com.cloud.storage.Volume;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.SnapshotDao;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.snapshot.SnapshotManager;
import com.cloud.template.TemplateManager;
import com.cloud.utils.Pair;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.VMInstanceDao;

public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDriver {
    @Override
    public Map<String, String> getCapabilities() {
        Map<String, String> caps = new HashMap<String, String>();
        caps.put(DataStoreCapabilities.VOLUME_SNAPSHOT_QUIESCEVM.toString(), Boolean.FALSE.toString());
        return caps;
    }

    protected Logger logger = LogManager.getLogger(getClass());
    private static final String NO_REMOTE_ENDPOINT_WITH_ENCRYPTION = "No remote endpoint to send command, unable to find a valid endpoint. Requires encryption support: %s";

    @Inject
    DiskOfferingDao diskOfferingDao;
    @Inject
    VMTemplateDao templateDao;
    @Inject
    VolumeDao volumeDao;
    @Inject
    HostDao hostDao;
    @Inject
    StorageManager storageMgr;
    @Inject
    VMInstanceDao vmDao;
    @Inject
    SnapshotDao snapshotDao;
    @Inject
    PrimaryDataStoreDao primaryStoreDao;
    @Inject
    SnapshotManager snapshotMgr;
    @Inject
    EndPointSelector epSelector;
    @Inject
    ConfigurationDao configDao;
    @Inject
    TemplateManager templateManager;
    @Inject
    TemplateDataFactory templateDataFactory;
    @Inject
    VolumeDataFactory volFactory;

    @Inject
    private VolumeOrchestrationService volumeOrchestrationService;

    @Override
    public DataTO getTO(DataObject data) {
        return null;
    }

    @Override
    public DataStoreTO getStoreTO(DataStore store) {
        return null;
    }

    public Answer createVolume(VolumeInfo volume) throws StorageUnavailableException {
        if (logger.isDebugEnabled()) {
            logger.debug("Creating volume: " + volume);
        }

        CreateObjectCommand cmd = new CreateObjectCommand(volume.getTO());
        boolean encryptionRequired = anyVolumeRequiresEncryption(volume);
        EndPoint ep = epSelector.select(volume, encryptionRequired);
        Answer answer = null;
        if (ep == null) {
            String errMsg = String.format(NO_REMOTE_ENDPOINT_WITH_ENCRYPTION, encryptionRequired);
            logger.error(errMsg);
            answer = new Answer(cmd, false, errMsg);
        } else {
            answer = ep.sendMessage(cmd);
        }
        return answer;
    }

    @Override
    public ChapInfo getChapInfo(DataObject dataObject) {
        return null;
    }

    @Override
    public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore) {
        return false;
    }

    @Override
    public void revokeAccess(DataObject dataObject, Host host, DataStore dataStore) {
    }

    @Override
    public long getUsedBytes(StoragePool storagePool) {
        return 0;
    }

    @Override
    public long getUsedIops(StoragePool storagePool) {
        return 0;
    }

    @Override
    public long getDataObjectSizeIncludingHypervisorSnapshotReserve(DataObject dataObject, StoragePool pool) {
        return dataObject.getSize();
    }

    @Override
    public long getBytesRequiredForTemplate(TemplateInfo templateInfo, StoragePool storagePool) {
        return 0;
    }

    @Override
    public void createAsync(DataStore dataStore, DataObject data, AsyncCompletionCallback<CreateCmdResult> callback) {
        String errMsg = null;
        Answer answer = null;
        CreateCmdResult result = new CreateCmdResult(null, null);
        if (data.getType() == DataObjectType.VOLUME) {
            try {
                answer = createVolume((VolumeInfo) data);
                if ((answer == null) || (!answer.getResult())) {
                    result.setSuccess(false);
                    if (answer != null) {
                        result.setResult(answer.getDetails());
                    }
                } else {
                    result.setAnswer(answer);
                }
            } catch (Exception e) {
                logger.debug("failed to create volume", e);
                errMsg = e.toString();
            }
        }
        if (errMsg != null) {
            result.setResult(errMsg);
        }

        if (callback != null) {
            callback.complete(result);
        }
    }

    private boolean commandCanBypassHostMaintenance(DataObject data) {
        if (DataObjectType.VOLUME.equals(data.getType())) {
            Volume volume = (Volume)data;
            if (volume.getInstanceId() != null) {
                VMInstanceVO vm = vmDao.findById(volume.getInstanceId());
                return vm != null && (VirtualMachine.Type.SecondaryStorageVm.equals(vm.getType()) ||
                        VirtualMachine.Type.ConsoleProxy.equals(vm.getType()));
            }
        }
        return false;
    }

    @Override
    public void deleteAsync(DataStore dataStore, DataObject data, AsyncCompletionCallback<CommandResult> callback) {
        DeleteCommand cmd = new DeleteCommand(data.getTO());
        cmd.setBypassHostMaintenance(commandCanBypassHostMaintenance(data));
        CommandResult result = new CommandResult();
        try {
            EndPoint ep;
            if (data.getType() == DataObjectType.VOLUME) {
                ep = epSelector.select(data, StorageAction.DELETEVOLUME);
            } else if (data.getType() == DataObjectType.SNAPSHOT) {
                ep = epSelector.select(data, StorageAction.DELETESNAPSHOT);
            } else {
                ep = epSelector.select(data);
            }
            if (ep == null) {
                String errMsg = "No remote endpoint to send DeleteCommand, check if host or ssvm is down?";
                logger.error(errMsg);
                result.setResult(errMsg);
            } else {
                Answer answer = ep.sendMessage(cmd);
                if (answer != null && !answer.getResult()) {
                    result.setResult(answer.getDetails());
                }
            }
        } catch (Exception ex) {
            logger.debug(String.format(
                    "Unable to destroy volume [id: %d, uuid: %s]",
                    data.getId(), data.getUuid()), ex);
            result.setResult(ex.toString());
        }
        callback.complete(result);
    }

    @Override
    public void copyAsync(DataObject srcdata, DataObject destData, AsyncCompletionCallback<CopyCommandResult> callback) {
        logger.debug("Copying volume [{}] to [{}]", srcdata, destData);
        boolean encryptionRequired = anyVolumeRequiresEncryption(srcdata, destData);
        DataStore store = destData.getDataStore();
        if (store.getRole() == DataStoreRole.Primary) {
            if ((srcdata.getType() == DataObjectType.TEMPLATE && destData.getType() == DataObjectType.TEMPLATE)) {
                //For CLVM, we need to copy template to primary storage at all, just fake the copy result.
                TemplateObjectTO templateObjectTO = new TemplateObjectTO();
                templateObjectTO.setPath(UUID.randomUUID().toString());
                templateObjectTO.setSize(srcdata.getSize());
                templateObjectTO.setPhysicalSize(srcdata.getSize());
                templateObjectTO.setFormat(Storage.ImageFormat.RAW);
                CopyCmdAnswer answer = new CopyCmdAnswer(templateObjectTO);
                CopyCommandResult result = new CopyCommandResult("", answer);
                callback.complete(result);
            } else if (srcdata.getType() == DataObjectType.TEMPLATE && destData.getType() == DataObjectType.VOLUME) {
                //For CLVM, we need to pass template on secondary storage to hypervisor
                int primaryStorageDownloadWait = StorageManager.PRIMARY_STORAGE_DOWNLOAD_WAIT.value();
                StoragePoolVO storagePoolVO = primaryStoreDao.findById(store.getId());
                DataStore imageStore = templateManager.getImageStore(storagePoolVO.getDataCenterId(), srcdata.getId());
                DataObject srcData = templateDataFactory.getTemplate(srcdata.getId(), imageStore);

                CopyCommand cmd = new CopyCommand(srcData.getTO(), destData.getTO(), primaryStorageDownloadWait, true);
                EndPoint ep = epSelector.select(srcData, destData, encryptionRequired);
                Answer answer = null;
                if (ep == null) {
                    String errMsg = String.format(NO_REMOTE_ENDPOINT_WITH_ENCRYPTION, encryptionRequired);
                    logger.error(errMsg);
                    answer = new Answer(cmd, false, errMsg);
                } else {
                    logger.debug(String.format("Sending copy command to endpoint %s, where encryption support is %s", ep.getHostAddr(), encryptionRequired ? "required" : "not required"));
                    answer = ep.sendMessage(cmd);
                }
                CopyCommandResult result = new CopyCommandResult("", answer);
                callback.complete(result);
            } else if (srcdata.getType() == DataObjectType.SNAPSHOT && destData.getType() == DataObjectType.VOLUME) {
                SnapshotObjectTO srcTO = (SnapshotObjectTO) srcdata.getTO();
                CopyCommand cmd = new CopyCommand(srcTO, destData.getTO(), StorageManager.PRIMARY_STORAGE_DOWNLOAD_WAIT.value(), true);
                EndPoint ep = epSelector.select(srcdata, destData, encryptionRequired);
                CopyCmdAnswer answer = null;
                if (ep == null) {
                    String errMsg = String.format(NO_REMOTE_ENDPOINT_WITH_ENCRYPTION, encryptionRequired);
                    logger.error(errMsg);
                    answer = new CopyCmdAnswer(errMsg);
                } else {
                    answer = (CopyCmdAnswer) ep.sendMessage(cmd);
                }
                CopyCommandResult result = new CopyCommandResult("", answer);
                callback.complete(result);
            }
        }
    }

    @Override
    public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback<CopyCommandResult> callback) {
        copyAsync(srcData, destData, callback);
    }

    @Override
    public boolean canCopy(DataObject srcData, DataObject destData) {
        //BUG fix for CLOUDSTACK-4618
        DataStore destStore = destData.getDataStore();
        if (destStore.getRole() == DataStoreRole.Primary && srcData.getType() == DataObjectType.TEMPLATE
                && (destData.getType() == DataObjectType.TEMPLATE || destData.getType() == DataObjectType.VOLUME)) {
            StoragePoolVO storagePoolVO = primaryStoreDao.findById(destStore.getId());
            if (storagePoolVO != null && storagePoolVO.getPoolType() == Storage.StoragePoolType.CLVM) {
                return true;
            }
        } else if (DataObjectType.SNAPSHOT.equals(srcData.getType()) && DataObjectType.VOLUME.equals(destData.getType())) {
            DataStore srcStore = srcData.getDataStore();
            if (DataStoreRole.Primary.equals(srcStore.getRole()) && DataStoreRole.Primary.equals(destStore.getRole())) {
                StoragePoolVO srcStoragePoolVO = primaryStoreDao.findById(srcStore.getId());
                StoragePoolVO dstStoragePoolVO = primaryStoreDao.findById(destStore.getId());
                if (srcStoragePoolVO != null && StoragePoolType.RBD.equals(srcStoragePoolVO.getPoolType())
                        && dstStoragePoolVO != null && (StoragePoolType.RBD.equals(dstStoragePoolVO.getPoolType())
                        || StoragePoolType.NetworkFilesystem.equals(dstStoragePoolVO.getPoolType()))) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback<CreateCmdResult> callback) {
        CreateCmdResult result = null;
        logger.debug("Taking snapshot of "+ snapshot);
        try {
            SnapshotObjectTO snapshotTO = getAndUpdateSnapshotObjectTO(snapshot);

            boolean encryptionRequired = anyVolumeRequiresEncryption(snapshot);
            CreateObjectCommand cmd = new CreateObjectCommand(snapshotTO);
            EndPoint ep = epSelector.select(snapshot, StorageAction.TAKESNAPSHOT, encryptionRequired);
            Answer answer;

            logger.debug("Taking snapshot of "+ snapshot + " and encryption required is " + encryptionRequired);

            if (ep == null) {
                String errMsg = "No remote endpoint to send createObjectCommand, check if host or ssvm is down?";
                logger.error(errMsg);
                answer = new Answer(cmd, false, errMsg);
            } else {
                answer = ep.sendMessage(cmd);
            }

            result = new CreateCmdResult(null, answer);
            if (answer != null && !answer.getResult()) {
                result.setResult(answer.getDetails());
            }

            callback.complete(result);
            return;
        } catch (Exception e) {
            logger.debug("Failed to take snapshot: {}", snapshot, e);
            result = new CreateCmdResult(null, null);
            result.setResult(e.toString());
        }
        callback.complete(result);
    }

    private SnapshotObjectTO getAndUpdateSnapshotObjectTO(SnapshotInfo snapshot) {
        SnapshotObjectTO snapshotTO = (SnapshotObjectTO) snapshot.getTO();
        Object payload = snapshot.getPayload();
        if (payload instanceof CreateSnapshotPayload) {
            CreateSnapshotPayload snapshotPayload = (CreateSnapshotPayload) payload;
            snapshotTO.setQuiescevm(snapshotPayload.getQuiescevm());
            snapshotTO.setKvmIncrementalSnapshot(snapshotPayload.isKvmIncrementalSnapshot());
        }

        if (snapshot.getImageStore() != null) {
            snapshotTO.setImageStore(snapshot.getImageStore().getTO());
        }
        if (snapshot.getParent() != null) {
            snapshotTO.setParentStore(snapshot.getParent().getDataStore().getTO());
        }

        return snapshotTO;
    }


    @Override
    public void revertSnapshot(SnapshotInfo snapshot, SnapshotInfo snapshotOnPrimaryStore, AsyncCompletionCallback<CommandResult> callback) {
        SnapshotObjectTO dataOnPrimaryStorage = null;
        if (snapshotOnPrimaryStore != null) {
            dataOnPrimaryStorage = (SnapshotObjectTO)snapshotOnPrimaryStore.getTO();
        }
        RevertSnapshotCommand cmd = new RevertSnapshotCommand((SnapshotObjectTO)snapshot.getTO(), dataOnPrimaryStorage);

        CommandResult result = new CommandResult();
        try {
            EndPoint ep = null;
            if (snapshotOnPrimaryStore != null) {
                ep = epSelector.select(snapshotOnPrimaryStore);
            } else {
                VolumeInfo volumeInfo = volFactory.getVolume(snapshot.getVolumeId(), DataStoreRole.Primary);
                ep = epSelector.select(volumeInfo);
            }
            if ( ep == null ){
                String errMsg = "No remote endpoint to send RevertSnapshotCommand, check if host or ssvm is down?";
                logger.error(errMsg);
                result.setResult(errMsg);
            } else {
                Answer answer = ep.sendMessage(cmd);
                if (answer != null && !answer.getResult()) {
                    result.setResult(answer.getDetails());
                }
            }
        } catch (Exception ex) {
            logger.debug("Unable to revert snapshot {}", snapshot, ex);
            result.setResult(ex.toString());
        }
        callback.complete(result);
    }

    @Override
    public void resize(DataObject data, AsyncCompletionCallback<CreateCmdResult> callback) {
        VolumeObject vol = (VolumeObject) data;
        StoragePool pool = (StoragePool) data.getDataStore();
        ResizeVolumePayload resizeParameter = (ResizeVolumePayload) vol.getpayload();
        boolean encryptionRequired = anyVolumeRequiresEncryption(vol);
        long [] endpointsToRunResize = resizeParameter.hosts;

        CreateCmdResult result = new CreateCmdResult(null, null);

        // if hosts are provided, they are where the VM last ran. We can use that.
        if (endpointsToRunResize == null || endpointsToRunResize.length == 0) {
            EndPoint ep = epSelector.select(data, encryptionRequired);
            if (ep == null) {
                String errMsg = String.format(NO_REMOTE_ENDPOINT_WITH_ENCRYPTION, encryptionRequired);
                logger.error(errMsg);
                result.setResult(errMsg);
                callback.complete(result);
                return;
            }
            endpointsToRunResize = new long[] {ep.getId()};
        }
        ResizeVolumeCommand resizeCmd = new ResizeVolumeCommand(vol.getPath(), new StorageFilerTO(pool), vol.getSize(),
                resizeParameter.newSize, resizeParameter.shrinkOk, resizeParameter.instanceName, vol.getChainInfo(), vol.getPassphrase(), vol.getEncryptFormat());
        if (pool.getParent() != 0) {
            resizeCmd.setContextParam(DiskTO.PROTOCOL_TYPE, Storage.StoragePoolType.DatastoreCluster.toString());
        }
        try {
            ResizeVolumeAnswer answer = (ResizeVolumeAnswer) storageMgr.sendToPool(pool, endpointsToRunResize, resizeCmd);
            if (answer != null && answer.getResult()) {
                long finalSize = answer.getNewSize();
                logger.debug("Resize: volume started at size: " + toHumanReadableSize(vol.getSize()) + " and ended at size: " + toHumanReadableSize(finalSize));

                vol.setSize(finalSize);
                vol.update();

                updateVolumePathDetails(vol, answer);
            } else if (answer != null) {
                result.setResult(answer.getDetails());
            } else {
                logger.debug("return a null answer, mark it as failed for unknown reason");
                result.setResult("return a null answer, mark it as failed for unknown reason");
            }
        } catch (Exception e) {
            logger.debug("sending resize command failed", e);
            result.setResult(e.toString());
        } finally {
            resizeCmd.clearPassphrase();
        }

        callback.complete(result);
    }

    private void updateVolumePathDetails(VolumeObject vol, ResizeVolumeAnswer answer) {
        VolumeVO volumeVO = volumeDao.findById(vol.getId());
        String datastoreUUID = answer.getContextParam("datastoreUUID");
        if (datastoreUUID != null) {
            StoragePoolVO storagePoolVO = primaryStoreDao.findByUuid(datastoreUUID);
            if (storagePoolVO != null) {
                volumeVO.setPoolId(storagePoolVO.getId());
                volumeVO.setPoolType(storagePoolVO.getPoolType());
            } else {
                logger.warn("Unable to find datastore {} while updating the new datastore of the volume {}", datastoreUUID, vol);
            }
        }

        String volumePath = answer.getContextParam("volumePath");
        if (volumePath != null) {
            volumeVO.setPath(volumePath);
        }

        String chainInfo = answer.getContextParam("chainInfo");
        if (chainInfo != null) {
            volumeVO.setChainInfo(chainInfo);
        }

        volumeDao.update(volumeVO.getId(), volumeVO);
    }

    @Override
    public void handleQualityOfServiceForVolumeMigration(VolumeInfo volumeInfo, QualityOfServiceState qualityOfServiceState) {}

    @Override
    public boolean canProvideStorageStats() {
        return false;
    }

    @Override
    public Pair<Long, Long> getStorageStats(StoragePool storagePool) {
        return null;
    }

    @Override
    public boolean canProvideVolumeStats() {
        return false;
    }

    @Override
    public Pair<Long, Long> getVolumeStats(StoragePool storagePool, String volumeId) {
        return null;
    }

    @Override
    public boolean canHostAccessStoragePool(Host host, StoragePool pool) {
        return true;
    }

    @Override
    public boolean isVmInfoNeeded() {
        return false;
    }

    @Override
    public void provideVmInfo(long vmId, long volumeId) {
    }

    @Override
    public boolean isVmTagsNeeded(String tagKey) {
        return false;
    }

    @Override
    public void provideVmTags(long vmId, long volumeId, String tagValue) {
    }

    /**
     * Does any object require encryption support?
     */
    private boolean anyVolumeRequiresEncryption(DataObject ... objects) {
        for (DataObject o : objects) {
            // this fails code smell for returning true twice, but it is more readable than combining all tests into one statement
            if (o instanceof VolumeInfo && ((VolumeInfo) o).getPassphraseId() != null) {
                return true;
            } else if (o instanceof SnapshotInfo && ((SnapshotInfo) o).getBaseVolume().getPassphraseId() != null) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean isStorageSupportHA(StoragePoolType type) {
        return StoragePoolType.NetworkFilesystem == type;
    }

    @Override
    public void detachVolumeFromAllStorageNodes(Volume volume) {
    }
}
