//
//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.snapshot;

import java.util.List;

import javax.inject.Inject;

import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.util.StorPoolUtil;
import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpApiResponse;
import org.apache.cloudstack.storage.datastore.util.StorPoolUtil.SpConnectionDesc;
import org.apache.cloudstack.storage.to.VolumeObjectTO;
import org.apache.cloudstack.storage.vmsnapshot.DefaultVMSnapshotStrategy;
import org.apache.cloudstack.storage.vmsnapshot.VMSnapshotHelper;
import org.springframework.stereotype.Component;

import com.cloud.agent.api.VMSnapshotTO;
import com.cloud.event.EventTypes;
import com.cloud.event.UsageEventUtils;
import com.cloud.hypervisor.kvm.storage.StorPoolStorageAdaptor;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.VolumeDetailVO;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDetailsDao;
import com.cloud.uservm.UserVm;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.fsm.NoTransitionException;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotDetailsVO;
import com.cloud.vm.snapshot.VMSnapshotVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import com.cloud.vm.snapshot.dao.VMSnapshotDetailsDao;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

@Component
public class StorPoolVMSnapshotStrategy extends DefaultVMSnapshotStrategy {

    @Inject
    private VMSnapshotHelper vmSnapshotHelper;
    @Inject
    private UserVmDao userVmDao;
    @Inject
    private VMSnapshotDao vmSnapshotDao;
    @Inject
    private VolumeDao volumeDao;
    @Inject
    private DiskOfferingDao diskOfferingDao;
    @Inject
    private PrimaryDataStoreDao storagePool;
    @Inject
    private VMSnapshotDetailsDao vmSnapshotDetailsDao;
    @Inject
    private VolumeDataFactory volFactory;
    @Inject
    private VolumeDetailsDao volumeDetailsDao;
    @Inject
    private StoragePoolDetailsDao storagePoolDetailsDao;
    @Inject
    private DataStoreManager dataStoreManager;
    int _wait;

    @Override
    public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) {
        logger.info("KVMVMSnapshotStrategy take snapshot");
        UserVm userVm = userVmDao.findById(vmSnapshot.getVmId());
        VMSnapshotVO vmSnapshotVO = (VMSnapshotVO) vmSnapshot;

        try {
            vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshotVO, VMSnapshot.Event.CreateRequested);
        } catch (NoTransitionException e) {
            throw new CloudRuntimeException("No transiontion " + e.getMessage());
        }

        boolean result = false;
        try {

            List<VolumeObjectTO> volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId());
            DataStore dataStore = dataStoreManager.getPrimaryDataStore(volumeTOs.get(0).getDataStore().getUuid());
            SpConnectionDesc conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, storagePool);

            long prev_chain_size = 0;
            long virtual_size = 0;
            for (VolumeObjectTO volume : volumeTOs) {
                virtual_size += volume.getSize();
                VolumeVO volumeVO = volumeDao.findById(volume.getId());
                prev_chain_size += volumeVO.getVmSnapshotChainSize() == null ? 0 : volumeVO.getVmSnapshotChainSize();
            }

            VMSnapshotTO current = null;
            VMSnapshotVO currentSnapshot = vmSnapshotDao.findCurrentSnapshotByVmId(userVm.getId());
            if (currentSnapshot != null) {
                current = vmSnapshotHelper.getSnapshotWithParents(currentSnapshot);
            }

            if (current == null) {
                vmSnapshotVO.setParent(null);
            } else {
                vmSnapshotVO.setParent(current.getId());
            }

            SpApiResponse resp = StorPoolUtil.volumesGroupSnapshot(volumeTOs, userVm.getUuid(), vmSnapshotVO.getUuid(), "group", conn);
            JsonObject obj = resp.fullJson.getAsJsonObject();
            JsonArray snapshots = obj.getAsJsonObject("data").getAsJsonArray("snapshots");
            StorPoolUtil.spLog("Volumes=%s attached to virtual machine", volumeTOs.toString());
            for (VolumeObjectTO vol : volumeTOs) {
                for (JsonElement jsonElement : snapshots) {
                    JsonObject snapshotObject = jsonElement.getAsJsonObject();
                    String snapshot = StorPoolUtil
                            .devPath(snapshotObject.getAsJsonPrimitive(StorPoolUtil.GLOBAL_ID).getAsString());
                    if (snapshotObject.getAsJsonPrimitive("volume").getAsString().equals(StorPoolStorageAdaptor.getVolumeNameFromPath(vol.getPath(), true))
                            || snapshotObject.getAsJsonPrimitive("volumeGlobalId").getAsString().equals(StorPoolStorageAdaptor.getVolumeNameFromPath(vol.getPath(), false))) {
                        VMSnapshotDetailsVO vmSnapshotDetailsVO = new VMSnapshotDetailsVO(vmSnapshot.getId(), vol.getUuid(), snapshot, false);
                        vmSnapshotDetailsDao.persist(vmSnapshotDetailsVO);
                        Long poolId = volumeDao.findById(vol.getId()).getPoolId();
                        if (poolId != null) {
                            VMSnapshotDetailsVO vmSnapshotDetailStoragePoolId = new VMSnapshotDetailsVO(
                                    vmSnapshot.getId(), StorPoolUtil.SP_STORAGE_POOL_ID, String.valueOf(poolId), false);
                            vmSnapshotDetailsDao.persist(vmSnapshotDetailStoragePoolId);
                        }
                        StorPoolUtil.spLog("Snapshot=%s of volume=%s for a group snapshot=%s.", snapshot, vol, vmSnapshot);
                    }
                }
            }

            if (resp.getError() == null) {
                StorPoolUtil.spLog("StorpoolVMSnapshotStrategy.takeSnapshot answer=%s", resp.getError());
                finalizeCreate(vmSnapshotVO, volumeTOs);
                result = vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationSucceeded);
                long new_chain_size = 0;
                for (VolumeObjectTO volumeObjectTO : volumeTOs) {
                    publishUsageEvents(EventTypes.EVENT_VM_SNAPSHOT_CREATE, vmSnapshot, userVm, volumeObjectTO);
                    new_chain_size += volumeObjectTO.getSize();
                    logger.info("EventTypes.EVENT_VM_SNAPSHOT_CREATE publishUsageEvent" + volumeObjectTO);
                }
                publishUsageEvents(EventTypes.EVENT_VM_SNAPSHOT_ON_PRIMARY, vmSnapshot, userVm, new_chain_size - prev_chain_size, virtual_size);
            } else {
                throw new CloudRuntimeException("Could not create vm snapshot");
            }
            return vmSnapshot;
        } catch (Exception e) {
            logger.debug("Could not create VM snapshot:" + e.getMessage());
            throw new CloudRuntimeException("Could not create VM snapshot:" + e.getMessage());
        } finally {
            if (!result) {
                try {
                    vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationFailed);
                    logger.info(String.format("VMSnapshot.Event.OperationFailed vmSnapshot=%s", vmSnapshot));
                } catch (NoTransitionException nte) {
                    logger.error("Cannot set vm state:" + nte.getMessage());
                }
            }
        }
    }

    @Override
    public StrategyPriority canHandle(VMSnapshot vmSnapshot) {
        return areAllVolumesOnStorPool(vmSnapshot.getVmId());
    }

    public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) {
        if (snapshotMemory) {
            return StrategyPriority.CANT_HANDLE;
        }
        return areAllVolumesOnStorPool(vmId);
    }

    private StrategyPriority areAllVolumesOnStorPool(Long vmId) {
        List<VolumeObjectTO> volumeTOs = vmSnapshotHelper.getVolumeTOList(vmId);
        if (volumeTOs == null || volumeTOs.isEmpty()) {
            return StrategyPriority.CANT_HANDLE;
        }
        for (VolumeObjectTO volumeTO : volumeTOs) {
            Long poolId = volumeTO.getPoolId();
            StoragePoolVO pool = storagePool.findById(poolId);
            if (!pool.getStorageProviderName().equals(StorPoolUtil.SP_PROVIDER_NAME)) {
                return StrategyPriority.CANT_HANDLE;
            }
        }
        return StrategyPriority.HIGHEST;
    }

    @Override
    public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) {
        UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId());
        VMSnapshotVO vmSnapshotVO = (VMSnapshotVO) vmSnapshot;
        try {
            vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.ExpungeRequested);
        } catch (NoTransitionException e) {
            logger.debug("Failed to change vm snapshot state with event ExpungeRequested");
            throw new CloudRuntimeException(
                    "Failed to change vm snapshot state with event ExpungeRequested: " + e.getMessage());
        }

        List<VolumeObjectTO> volumeTOs = vmSnapshotHelper.getVolumeTOList(vmSnapshot.getVmId());
        DataStore dataStore = dataStoreManager.getPrimaryDataStore(volumeTOs.get(0).getDataStore().getUuid());
        SpConnectionDesc conn = null;
        try {
            conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, storagePool);
        } catch (CloudRuntimeException e) {
            throw e;
        }

        SpApiResponse resp = null;
        for (VolumeObjectTO volumeObjectTO : volumeTOs) {
            String err = null;
            VMSnapshotDetailsVO snapshotDetailsVO = vmSnapshotDetailsDao.findDetail(vmSnapshot.getId(), volumeObjectTO.getUuid());
            String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(snapshotDetailsVO.getValue(), true);
            if (snapshotName == null) {
                err = String.format("Could not find StorPool's snapshot vm snapshot %s and volume [id: %s, uuid: %s, name: %s]",
                        vmSnapshot, volumeObjectTO.getId(), volumeObjectTO.getUuid(), volumeObjectTO.getName());
                logger.error("Could not delete snapshot for vm:" + err);
            }
            StorPoolUtil.spLog("StorpoolVMSnapshotStrategy.deleteVMSnapshot snapshotName=%s", snapshotName);
            resp = StorPoolUtil.snapshotDelete(snapshotName, conn);
            if (resp.getError() != null) {
                err = String.format("Could not delete storpool vm error=%s", resp.getError());
                logger.error("Could not delete snapshot for vm:" + err);
            } else {
                // do we need to clean database?
                if (snapshotDetailsVO != null) {
                    vmSnapshotDetailsDao.remove(snapshotDetailsVO.getId());
                }
            }
            if (err != null) {
                StorPoolUtil.spLog(
                        "StorpoolVMSnapshotStrategy.deleteVMSnapshot delete snapshot=%s of group snapshot=%s failed due to %s",
                        snapshotName, userVm, err);
                throw new CloudRuntimeException(String.format("Delete vm snapshot %s of vm %s failed due to %s", vmSnapshot, userVm, err));
            }
        }
        vmSnapshotDetailsDao.removeDetails(vmSnapshot.getId());

        finalizeDelete(vmSnapshotVO, volumeTOs);
        vmSnapshotDao.remove(vmSnapshot.getId());

        long full_chain_size = 0;
        for (VolumeObjectTO volumeTo : volumeTOs) {
            publishUsageEvents(EventTypes.EVENT_VM_SNAPSHOT_DELETE, vmSnapshot, userVm, volumeTo);
            full_chain_size += volumeTo.getSize();
        }
        publishUsageEvents(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshot, userVm, full_chain_size, 0L);
        return true;
    }

    @Override
    public boolean revertVMSnapshot(VMSnapshot vmSnapshot) {
        logger.debug("Revert vm snapshot");
        VMSnapshotVO vmSnapshotVO = (VMSnapshotVO) vmSnapshot;
        UserVmVO userVm = userVmDao.findById(vmSnapshot.getVmId());

        if (userVm.getState() == VirtualMachine.State.Running && vmSnapshotVO.getType() == VMSnapshot.Type.Disk) {
            throw new CloudRuntimeException("Virtual machine should be in stopped state for revert operation");
        }

        try {
            vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshotVO, VMSnapshot.Event.RevertRequested);
        } catch (NoTransitionException e) {
            throw new CloudRuntimeException(e.getMessage());
        }

        boolean result = false;
        try {
            List<VolumeObjectTO> volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId());

            DataStore dataStore = dataStoreManager.getPrimaryDataStore(volumeTOs.get(0).getDataStore().getUuid());
            SpConnectionDesc conn = StorPoolUtil.getSpConnection(dataStore.getUuid(), dataStore.getId(), storagePoolDetailsDao, storagePool);
            for (VolumeObjectTO volumeObjectTO : volumeTOs) {
                String err = null;
                VMSnapshotDetailsVO snapshotDetailsVO = vmSnapshotDetailsDao.findDetail(vmSnapshot.getId(),
                        volumeObjectTO.getUuid());
                String snapshotName = StorPoolStorageAdaptor.getVolumeNameFromPath(snapshotDetailsVO.getValue(), true);
                if (snapshotName == null) {
                    err = String.format("Could not find StorPool's snapshot vm snapshot uuid=%s and volume uui=%s",
                            vmSnapshot.getUuid(), volumeObjectTO.getUuid());
                    logger.error("Could not delete snapshot for vm:" + err);
                }
                String volumeName = StorPoolStorageAdaptor.getVolumeNameFromPath(volumeObjectTO.getPath(), true);
                VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeObjectTO.getId(), StorPoolUtil.SP_PROVIDER_NAME);
                if (detail != null) {
                    SpApiResponse updateVolumeResponse = StorPoolUtil.volumeUpdateRename(volumeName, "", StorPoolStorageAdaptor.getVolumeNameFromPath(detail.getValue(), false), conn);

                    if (updateVolumeResponse.getError() != null) {
                        StorPoolUtil.spLog("StorpoolVMSnapshotStrategy.canHandle - Could not update StorPool's volume %s to it's globalId due to %s", volumeName, updateVolumeResponse.getError().getDescr());
                        err = String.format("StorpoolVMSnapshotStrategy.canHandle - Could not update StorPool's volume %s to it's globalId due to %s", volumeName, updateVolumeResponse.getError().getDescr());
                    } else {
                        volumeDetailsDao.remove(detail.getId());
                    }
                }

                SpApiResponse resp = StorPoolUtil.detachAllForced(volumeName, false, conn);
                if (resp.getError() != null) {
                    err = String.format("Could not detach StorPool volume %s from a group snapshot, due to %s",
                            volumeName, resp.getError());
                    throw new CloudRuntimeException(err);
                }
                resp = StorPoolUtil.volumeRevert(volumeName, snapshotName, conn);
                if (resp.getError() != null) {
                    err = String.format("Create Could not complete revert task for volumeName=%s , and snapshotName=%s",
                            volumeName, snapshotName);
                    throw new CloudRuntimeException(err);
                }
                VolumeInfo vinfo = volFactory.getVolume(volumeObjectTO.getId());
                if (vinfo.getMaxIops() != null) {
                    resp = StorPoolUtil.volumeUpdateIopsAndTags(volumeName, null, vinfo.getMaxIops(), conn, null);

                    if (resp.getError() != null) {
                        StorPoolUtil.spLog("Volume was reverted successfully but max iops could not be set due to %s",
                                resp.getError().getDescr());
                    }
                }
            }
            finalizeRevert(vmSnapshotVO, volumeTOs);
            result = vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationSucceeded);
        } catch (CloudRuntimeException | NoTransitionException  e) {
            String errMsg = String.format("Error while finalize create vm snapshot [%s] due to %s", vmSnapshot, e.getMessage());
            logger.error(errMsg, e);
            throw new CloudRuntimeException(errMsg);
        } finally {
            if (!result) {
                try {
                    vmSnapshotHelper.vmSnapshotStateTransitTo(vmSnapshot, VMSnapshot.Event.OperationFailed);
                } catch (NoTransitionException e1) {
                    logger.error("Cannot set vm snapshot state due to: " + e1.getMessage());
                }
            }
        }
        return result;
    }

    private void publishUsageEvents(String type, VMSnapshot vmSnapshot, UserVm userVm, VolumeObjectTO volumeTo) {
        VolumeVO volume = volumeDao.findById(volumeTo.getId());
        Long diskOfferingId = volume.getDiskOfferingId();
        Long offeringId = null;
        if (diskOfferingId != null) {
            DiskOfferingVO offering = diskOfferingDao.findById(diskOfferingId);
            if (offering != null && offering.isComputeOnly()) {
                offeringId = offering.getId();
            }
        }
        UsageEventUtils.publishUsageEvent(type, vmSnapshot.getAccountId(), userVm.getDataCenterId(), userVm.getId(),
                vmSnapshot.getName(), offeringId, volume.getId(), volumeTo.getSize(), VMSnapshot.class.getName(), vmSnapshot.getUuid());
    }

    private void publishUsageEvents(String type, VMSnapshot vmSnapshot, UserVm userVm, Long vmSnapSize, Long virtualSize) {
        try {
            UsageEventUtils.publishUsageEvent(type, vmSnapshot.getAccountId(), userVm.getDataCenterId(), userVm.getId(),
                    vmSnapshot.getName(), 0L, 0L, vmSnapSize, virtualSize, VMSnapshot.class.getName(),
                    vmSnapshot.getUuid());
        } catch (Exception e) {
            logger.error("Failed to publis usage event " + type, e);
        }
    }
}
