package org.ovirt.engine.core.bll;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.ovirt.engine.core.bll.command.utils.StorageDomainSpaceChecker;
import org.ovirt.engine.core.bll.job.ExecutionHandler;
import org.ovirt.engine.core.bll.quota.Quotable;
import org.ovirt.engine.core.bll.quota.StorageQuotaValidationParameter;
import org.ovirt.engine.core.bll.snapshots.SnapshotsValidator;
import org.ovirt.engine.core.bll.storage.StorageDomainCommandBase;
import org.ovirt.engine.core.bll.utils.PermissionSubject;
import org.ovirt.engine.core.bll.utils.VmDeviceUtils;
import org.ovirt.engine.core.bll.utils.WipeAfterDeleteUtils;
import org.ovirt.engine.core.bll.validator.StorageDomainValidator;
import org.ovirt.engine.core.common.AuditLogType;
import org.ovirt.engine.core.common.VdcObjectType;
import org.ovirt.engine.core.common.action.AddDiskParameters;
import org.ovirt.engine.core.common.action.AddImageFromScratchParameters;
import org.ovirt.engine.core.common.action.VdcActionType;
import org.ovirt.engine.core.common.action.VdcReturnValueBase;
import org.ovirt.engine.core.common.businessentities.ActionGroup;
import org.ovirt.engine.core.common.businessentities.Disk;
import org.ovirt.engine.core.common.businessentities.Disk.DiskStorageType;
import org.ovirt.engine.core.common.businessentities.DiskImage;
import org.ovirt.engine.core.common.businessentities.DiskLunMap;
import org.ovirt.engine.core.common.businessentities.LUNs;
import org.ovirt.engine.core.common.businessentities.LunDisk;
import org.ovirt.engine.core.common.businessentities.QuotaEnforcementTypeEnum;
import org.ovirt.engine.core.common.businessentities.Snapshot.SnapshotType;
import org.ovirt.engine.core.common.businessentities.StoragePoolIsoMapId;
import org.ovirt.engine.core.common.businessentities.StorageType;
import org.ovirt.engine.core.common.businessentities.VM;
import org.ovirt.engine.core.common.businessentities.VMStatus;
import org.ovirt.engine.core.common.businessentities.VmDeviceId;
import org.ovirt.engine.core.common.businessentities.VolumeType;
import org.ovirt.engine.core.common.businessentities.permissions;
import org.ovirt.engine.core.common.businessentities.storage_domains;
import org.ovirt.engine.core.common.businessentities.storage_server_connections;
import org.ovirt.engine.core.common.config.Config;
import org.ovirt.engine.core.common.config.ConfigValues;
import org.ovirt.engine.core.common.locks.LockingGroup;
import org.ovirt.engine.core.common.utils.VmDeviceType;
import org.ovirt.engine.core.common.validation.group.UpdateEntity;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.compat.NGuid;
import org.ovirt.engine.core.dal.VdcBllMessages;
import org.ovirt.engine.core.dal.dbbroker.DbFacade;
import org.ovirt.engine.core.dao.BaseDiskDao;
import org.ovirt.engine.core.dao.DiskLunMapDao;
import org.ovirt.engine.core.dao.SnapshotDao;
import org.ovirt.engine.core.dao.StorageDomainStaticDAO;
import org.ovirt.engine.core.utils.transaction.TransactionMethod;
import org.ovirt.engine.core.utils.transaction.TransactionSupport;

@DisableInPrepareMode
@NonTransactiveCommandAttribute(forceCompensation = true)
public class AddDiskCommand<T extends AddDiskParameters> extends AbstractDiskVmCommand<T>
        implements Quotable {

    private static final long serialVersionUID = 4499428315430159917L;

    /**
     * Constructor for command creation when compensation is applied on startup
     *
     * @param commandId
     */
    protected AddDiskCommand(Guid commandId) {
        super(commandId);
    }

    public AddDiskCommand(T parameters) {
        super(parameters);
        parameters.getDiskInfo().setId(Guid.NewGuid());
        parameters.setEntityId(parameters.getDiskInfo().getId());
    }

    @Override
    protected boolean canDoAction() {
        boolean returnValue = isVmExist() && acquireLockInternal();
        VM vm = getVm();

        // if user sent drive check that its not in use
        returnValue = returnValue && (vm == null || isDiskCanBeAddedToVm(getParameters().getDiskInfo()));
        if (returnValue && DiskStorageType.IMAGE == getParameters().getDiskInfo().getDiskStorageType()) {
            returnValue = checkIfImageDiskCanBeAdded(vm);
        }
        if (returnValue && DiskStorageType.LUN == getParameters().getDiskInfo().getDiskStorageType()) {
            returnValue = checkIfLunDiskCanBeAdded();
        }

        return returnValue;
    }

    protected boolean checkIfLunDiskCanBeAdded() {
        LUNs lun = ((LunDisk) getParameters().getDiskInfo()).getLun();
        switch (lun.getLunType()) {
        case UNKNOWN:
            return failCanDoAction(VdcBllMessages.ACTION_TYPE_FAILED_DISK_LUN_HAS_NO_VALID_TYPE);
        case ISCSI:
            if (lun.getLunConnections() == null || lun.getLunConnections().isEmpty()) {
                return failCanDoAction(VdcBllMessages.ACTION_TYPE_FAILED_DISK_LUN_ISCSI_MISSING_CONNECTION_PARAMS);
            }

            for (storage_server_connections conn : lun.getLunConnections()) {
                if (StringUtils.isEmpty(conn.getiqn()) || StringUtils.isEmpty(conn.getconnection())
                        || StringUtils.isEmpty(conn.getport())) {
                    return failCanDoAction(VdcBllMessages.ACTION_TYPE_FAILED_DISK_LUN_ISCSI_MISSING_CONNECTION_PARAMS);
                }
            }
            break;
        }

        if (getDiskLunMapDao().getDiskIdByLunId(lun.getLUN_id()) != null) {
            return failCanDoAction(VdcBllMessages.ACTION_TYPE_FAILED_DISK_LUN_IS_ALREADY_IN_USE);
        }
        return true;
    }

    private boolean checkIfImageDiskCanBeAdded(VM vm) {
        boolean returnValue;
        StorageDomainValidator validator = new StorageDomainValidator(getStorageDomain());
        returnValue = validator.isDomainExistAndActive(getReturnValue().getCanDoActionMessages());
        if (returnValue && vm != null && getStoragePoolIsoMapDao().get(new StoragePoolIsoMapId(
                getStorageDomainId().getValue(), vm.getstorage_pool_id())) == null) {
            returnValue = false;
            addCanDoActionMessage(VdcBllMessages.ACTION_TYPE_FAILED_STORAGE_POOL_OF_VM_NOT_MATCH);
        }
        returnValue = returnValue
                && checkImageConfiguration()
                && (vm == null || isDiskPassPCIAndIDELimit(getParameters().getDiskInfo()));
        returnValue = returnValue && (vm == null || performImagesChecks(vm));

        if (returnValue && !hasFreeSpace(getStorageDomain())) {
            returnValue = false;
            addCanDoActionMessage(VdcBllMessages.ACTION_TYPE_FAILED_DISK_SPACE_LOW);
        }
        if (returnValue && isExceedMaxBlockDiskSize()) {
            addCanDoActionMessage(VdcBllMessages.ACTION_TYPE_FAILED_DISK_MAX_SIZE_EXCEEDED);
            getReturnValue().getCanDoActionMessages().add(
                    String.format("$max_disk_size %1$s", Config.<Integer> GetValue(ConfigValues.MaxBlockDiskSize)));
            returnValue = false;
        }
        if (returnValue && getParameters().getDiskInfo().isShareable()) {
            if (!Config.<Boolean> GetValue(ConfigValues.ShareableDiskEnabled,
                            getStoragePool().getcompatibility_version().getValue())) {
                returnValue = false;
                addCanDoActionMessage(VdcBllMessages.ACTION_NOT_SUPPORTED_FOR_CLUSTER_POOL_LEVEL);
            }
            else if (!isVolumeFormatSupportedForShareable(
                    ((DiskImage) getParameters().getDiskInfo()).getvolume_format())) {
                returnValue = false;
                addCanDoActionMessage(VdcBllMessages.SHAREABLE_DISK_IS_NOT_SUPPORTED_BY_VOLUME_FORMAT);
            }
        }
        return returnValue && (vm == null || validate(getSnapshotValidator().vmNotDuringSnapshot(vm.getId())));
    }

    protected SnapshotsValidator getSnapshotValidator() {
        return new SnapshotsValidator();
    }

    /** Checks if the iamge's configuration is legal */
    protected boolean checkImageConfiguration() {
        return ImagesHandler.CheckImageConfiguration(
                getStorageDomain().getStorageStaticData(),
                getDiskImageInfo(),
                getReturnValue().getCanDoActionMessages());
    }

    protected boolean performImagesChecks(VM vm) {
        return ImagesHandler.PerformImagesChecks(vm,
                getReturnValue().getCanDoActionMessages(),
                vm.getstorage_pool_id(),
                getStorageDomainId().getValue(),
                false,
                true,
                false,
                false,
                true,
                false,
                false,
                true,
                Collections.emptyList());
    }

    private long getRequestDiskSpace() {
        if (getParameters().getDiskInfo().getDiskStorageType() == DiskStorageType.IMAGE) {
            return getDiskImageInfo().getSizeInGigabytes();
        }
        return 0;
    }

    @Override
    protected boolean isVmExist() {
        return getParameters().getVmId() == null || Guid.Empty.equals(getParameters().getVmId()) || super.isVmExist();
    }

    private boolean hasFreeSpace(storage_domains storageDomain) {
        if (getDiskImageInfo().getvolume_type() == VolumeType.Preallocated) {
            return doesStorageDomainhaveSpaceForRequest(storageDomain);
        }
        return isStorageDomainBelowThresholds(storageDomain);
    }

    protected boolean doesStorageDomainhaveSpaceForRequest(storage_domains storageDomain) {
        return StorageDomainSpaceChecker.hasSpaceForRequest(storageDomain, getDiskImageInfo().getSizeInGigabytes());
    }

    protected boolean isStorageDomainBelowThresholds(storage_domains storageDomain) {
        return StorageDomainSpaceChecker.isBelowThresholds(storageDomain);
    }

    /** @return The disk from the parameters, cast to a {@link DiskImage} */
    private DiskImage getDiskImageInfo() {
        return (DiskImage) getParameters().getDiskInfo();
    }

    private boolean isExceedMaxBlockDiskSize() {
        StorageType storageType = getStorageDomain().getstorage_type();
        boolean isBlockStorageDomain = storageType == StorageType.ISCSI || storageType == StorageType.FCP;
        boolean isRequestedLargerThanMaxSize = getRequestDiskSpace() > Config.<Integer> GetValue(ConfigValues.MaxBlockDiskSize);

        return isBlockStorageDomain && isRequestedLargerThanMaxSize;
    }

    /**
     * @return The StorageDomainStaticDAO
     */
    protected StorageDomainStaticDAO getStorageDomainStaticDao() {
        return DbFacade.getInstance().getStorageDomainStaticDAO();
    }

    @Override
    protected SnapshotDao getSnapshotDao() {
        return DbFacade.getInstance().getSnapshotDao();
    }

    protected BaseDiskDao getBaseDiskDao() {
        return DbFacade.getInstance().getBaseDiskDao();
    }

    protected DiskLunMapDao getDiskLunMapDao() {
        return DbFacade.getInstance().getDiskLunMapDao();
    }

    /**
     * @return The ID of the storage domain where the VM's disks reside.
     */
    private Guid getDisksStorageDomainId() {
        return ((DiskImage) getVm().getDiskMap().values().iterator().next()).getstorage_ids().get(0);
    }

    @Override
    public NGuid getStorageDomainId() {
        Guid storageDomainId = getParameters().getStorageDomainId();
        if (Guid.Empty.equals(storageDomainId) &&
                getVm() != null &&
                getVm().getDiskMap() != null &&
                !getVm().getDiskMap().isEmpty()) {
            return getDisksStorageDomainId();
        }

        return storageDomainId == null ? Guid.Empty : storageDomainId;
    }

    @Override
    public List<PermissionSubject> getPermissionCheckSubjects() {
        List<PermissionSubject> listPermissionSubjects;
        if (getParameters().getVmId() == null || Guid.Empty.equals(getParameters().getVmId())) {
            listPermissionSubjects = new ArrayList<PermissionSubject>();
        } else {
            listPermissionSubjects = super.getPermissionCheckSubjects();
        }
        // If the storage domain ID is empty/null, it means we are going to create an external disk
        // In order to do that we need CREATE_DISK permissions on System level
        if (getParameters().getStorageDomainId() == null || Guid.Empty.equals(getParameters().getStorageDomainId())) {
            listPermissionSubjects.add(new PermissionSubject(Guid.SYSTEM,
                    VdcObjectType.System,
                    ActionGroup.CREATE_DISK));
        } else {
            listPermissionSubjects.add(new PermissionSubject(getParameters().getStorageDomainId(),
                    VdcObjectType.Storage,
                    ActionGroup.CREATE_DISK));
        }
        return listPermissionSubjects;
    }

    @Override
    protected void setActionMessageParameters() {
        addCanDoActionMessage(VdcBllMessages.VAR__ACTION__ADD);
        addCanDoActionMessage(VdcBllMessages.VAR__TYPE__VM_DISK);
    }

    @Override
    protected void ExecuteVmCommand() {
        ImagesHandler.setDiskAlias(getParameters().getDiskInfo(), getVm());
        if (DiskStorageType.IMAGE == getParameters().getDiskInfo().getDiskStorageType()) {
            createDiskBasedOnImage();
        } else {
            createDiskBasedOnLun();
        }
    }

    private void createDiskBasedOnLun() {
        final LUNs lun = ((LunDisk) getParameters().getDiskInfo()).getLun();
        TransactionSupport.executeInNewTransaction(new TransactionMethod<Void>() {
            @Override
            public Void runInTransaction() {
                StorageDomainCommandBase.proceedLUNInDb(lun, lun.getLunType());
                getBaseDiskDao().save(getParameters().getDiskInfo());
                getDiskLunMapDao().save(new DiskLunMap(getParameters().getDiskInfo().getId(), lun.getLUN_id()));
                if (getVm() != null) {
                    VmDeviceUtils.addManagedDevice(new VmDeviceId(getParameters().getDiskInfo().getId(), getVmId()),
                            VmDeviceType.DISK,
                            VmDeviceType.DISK,
                            null,
                            getVm().getstatus() == VMStatus.Down,
                            false);
                }
                return null;
            }
        });
        getReturnValue().setActionReturnValue(getParameters().getDiskInfo().getId());
        setSucceeded(true);
    }

    private void createDiskBasedOnImage() {
        if(!getParameters().getDiskInfo().isWipeAfterDeleteSet()) {
            StorageType storageType = getStorageDomain().getstorage_type();
            getParameters().getDiskInfo().setWipeAfterDelete(WipeAfterDeleteUtils.getDefaultWipeAfterDeleteFlag(storageType));
        }
        // create from blank template, create new vm snapshot id
        AddImageFromScratchParameters parameters =
                new AddImageFromScratchParameters(Guid.Empty,
                        getParameters().getVmId(),
                        getDiskImageInfo());
        parameters.setQuotaId(getQuotaId());
        parameters.setDiskAlias(getDiskAlias());
        parameters.setStorageDomainId(getStorageDomainId().getValue());
        parameters.setParentCommand(VdcActionType.AddDisk);
        parameters.setEntityId(getParameters().getEntityId());
        parameters.setStoragePoolId(getStorageDomain().getstorage_pool_id().getValue());
        getParameters().getImagesParameters().add(parameters);
        parameters.setParentParameters(getParameters());
        if (getVm() != null) {
            setVmSnapshotIdForDisk(parameters);
            getCompensationContext().snapshotNewEntity(VmDeviceUtils.addManagedDevice(new VmDeviceId(getParameters().getDiskInfo()
                    .getId(),
                    getVmId()),
                    VmDeviceType.DISK,
                    VmDeviceType.DISK,
                    null,
                    getVm().getstatus() == VMStatus.Down,
                    false));
            getCompensationContext().stateChanged();
        }
        VdcReturnValueBase tmpRetValue =
                Backend.getInstance().runInternalAction(VdcActionType.AddImageFromScratch,
                        parameters,
                        ExecutionHandler.createDefaultContexForTasks(getExecutionContext(), getLock()));
        // Setting lock to null because the lock is released in the child command
        setLock(null);
        getReturnValue().getTaskIdList().addAll(tmpRetValue.getInternalTaskIdList());
        if (tmpRetValue.getActionReturnValue() != null) {
            DiskImage diskImage = (DiskImage) tmpRetValue.getActionReturnValue();
            addDiskPermissions(diskImage);
            getReturnValue().setActionReturnValue(diskImage.getId());
        }
        getReturnValue().setFault(tmpRetValue.getFault());
        setSucceeded(tmpRetValue.getSucceeded());
    }

    /**
     * If disk is not allow to have snapshot no VM snapshot Id should be updated.
     * @param parameters
     */
    private void setVmSnapshotIdForDisk(AddImageFromScratchParameters parameters) {
        if (getParameters().getDiskInfo().isAllowSnapshot()) {
            parameters.setVmSnapshotId(getSnapshotDao().getId(getVmId(), SnapshotType.ACTIVE));
        }
    }

    private void addDiskPermissions(Disk disk) {
        permissions perms =
                new permissions(getCurrentUser().getUserId(),
                        PredefinedRoles.DISK_OPERATOR.getId(),
                        disk.getId(),
                        VdcObjectType.Disk);
        MultiLevelAdministrationHandler.addPermission(perms);
    }

    @Override
    public AuditLogType getAuditLogTypeValue() {
        switch (getActionState()) {
        case EXECUTE:
            return getExecuteAuditLogTypeValue(getSucceeded());

        case END_SUCCESS:
            return getEndSuccessAuditLogTypeValue(getSucceeded());

        default:
            return AuditLogType.USER_ADD_DISK_FINISHED_FAILURE;
        }
    }

    private AuditLogType getExecuteAuditLogTypeValue(boolean successful) {
        boolean isVmNameExist = StringUtils.isNotEmpty(getVmName());
        if (successful) {
            if (isVmNameExist) {
                return AuditLogType.USER_ADD_DISK_TO_VM;
            } else {
                return AuditLogType.USER_ADD_DISK;
            }
        } else {
            if (isVmNameExist) {
                return AuditLogType.USER_FAILED_ADD_DISK_TO_VM;
            } else {
                return AuditLogType.USER_FAILED_ADD_DISK;
            }
        }
    }

    private AuditLogType getEndSuccessAuditLogTypeValue(boolean successful) {
        boolean isVmNameExist = StringUtils.isNotEmpty(getVmName());
        if (successful) {
            if (isVmNameExist) {
                return AuditLogType.USER_ADD_DISK_TO_VM_FINISHED_SUCCESS;
            } else {
                return AuditLogType.USER_ADD_DISK_FINISHED_SUCCESS;
            }
        } else {
            if (isVmNameExist) {
                return AuditLogType.USER_ADD_DISK_TO_VM_FINISHED_FAILURE;
            } else {
                return AuditLogType.USER_ADD_DISK_FINISHED_FAILURE;
            }
        }
    }

    @Override
    protected VdcActionType getChildActionType() {
        return VdcActionType.AddImageFromScratch;
    }

    @Override
    protected List<Class<?>> getValidationGroups() {
        addValidationGroup(UpdateEntity.class);
        return super.getValidationGroups();
    }

    @Override
    protected Map<String, String> getExclusiveLocks() {
        if (getParameters().getDiskInfo().isBoot() && getParameters().getVmId() != null
                && !Guid.Empty.equals(getParameters().getVmId())) {
            return Collections.singletonMap(getParameters().getVmId().toString(), LockingGroup.VM_DISK_BOOT.name());
        }
        return null;
    }

    @Override
    protected Map<String, String> getSharedLocks() {
        if (getParameters().getVmId() != null && !Guid.Empty.equals(getParameters().getVmId())) {
            return Collections.singletonMap(getParameters().getVmId().toString(), LockingGroup.VM.name());
        }
        return null;
    }

    @Override
    public boolean validateAndSetQuota() {
        if (getParameters().getDiskInfo().getDiskStorageType() == DiskStorageType.IMAGE) {
            if (getStoragePool() == null) {
                setStoragePoolId(getStorageDomain().getstorage_pool_id().getValue());
            }
            return getQuotaManager().validateAndSetStorageQuota(getStoragePool(),
                    getStorageQuotaListParameters(),
                    getReturnValue().getCanDoActionMessages());
        }
        return true;
    }

    @Override
    public void rollbackQuota() {
        if (getParameters().getDiskInfo().getDiskStorageType() == DiskStorageType.IMAGE) {
            getQuotaManager().rollbackQuota(getStoragePool(),
                    getQuotaManager().getQuotaListFromParameters(getStorageQuotaListParameters()));
        }
    }

    @Override
    public Guid getQuotaId() {
        if (getParameters().getDiskInfo() != null
                && DiskStorageType.IMAGE == getParameters().getDiskInfo().getDiskStorageType()) {
            return ((DiskImage) getParameters().getDiskInfo()).getQuotaId();
        }
        return null;
    }

    private List<StorageQuotaValidationParameter> getStorageQuotaListParameters() {
        List<StorageQuotaValidationParameter> list = new ArrayList<StorageQuotaValidationParameter>();
        list.add(new StorageQuotaValidationParameter(getQuotaId(),
                getStorageDomainId().getValue(),
                getRequestDiskSpace()));
        return list;
    }

    @Override
    public void addQuotaPermissionSubject(List<PermissionSubject> quotaPermissionList) {
        if (getStoragePool() != null &&
                getQuotaId() != null &&
                !getStoragePool().getQuotaEnforcementType().equals(QuotaEnforcementTypeEnum.DISABLED)) {
            quotaPermissionList.add(new PermissionSubject(getQuotaId(), VdcObjectType.Quota, ActionGroup.CONSUME_QUOTA));
        }
    }

    @Override
    protected void endWithFailure() {
        super.endWithFailure();
        rollbackQuota();
    }

}
