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

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.inject.Inject;

import org.apache.cloudstack.storage.datastore.client.ScaleIOGatewayClientConnectionPool;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.util.ScaleIOUtil;
import org.apache.commons.collections.CollectionUtils;
import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.HostScope;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreLifeCycle;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreParameters;
import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
import org.apache.cloudstack.storage.datastore.api.StoragePoolStatistics;
import org.apache.cloudstack.storage.datastore.client.ScaleIOGatewayClient;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper;
import org.apache.log4j.Logger;

import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.DeleteStoragePoolCommand;
import com.cloud.agent.api.StoragePoolInfo;
import com.cloud.capacity.CapacityManager;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.resource.ResourceManager;
import com.cloud.storage.Storage;
import com.cloud.storage.StorageManager;
import com.cloud.storage.StoragePool;
import com.cloud.storage.StoragePoolAutomation;
import com.cloud.storage.StoragePoolHostVO;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.VMTemplateStorageResourceAssoc;
import com.cloud.storage.dao.StoragePoolHostDao;
import com.cloud.template.TemplateManager;
import com.cloud.utils.UriUtils;
import com.cloud.utils.crypt.DBEncryptionUtil;
import com.cloud.utils.exception.CloudRuntimeException;

public class ScaleIOPrimaryDataStoreLifeCycle implements PrimaryDataStoreLifeCycle {
    private static final Logger LOGGER = Logger.getLogger(ScaleIOPrimaryDataStoreLifeCycle.class);

    @Inject
    private ClusterDao clusterDao;
    @Inject
    private PrimaryDataStoreDao primaryDataStoreDao;
    @Inject
    private StoragePoolDetailsDao storagePoolDetailsDao;
    @Inject
    private StoragePoolHostDao storagePoolHostDao;
    @Inject
    private PrimaryDataStoreHelper dataStoreHelper;
    @Inject
    private ResourceManager resourceManager;
    @Inject
    private StorageManager storageMgr;
    @Inject
    private StoragePoolAutomation storagePoolAutomation;
    @Inject
    private CapacityManager capacityMgr;
    @Inject
    private TemplateManager templateMgr;
    @Inject
    private AgentManager agentMgr;

    public ScaleIOPrimaryDataStoreLifeCycle() {
    }

    private org.apache.cloudstack.storage.datastore.api.StoragePool findStoragePool(String url, String username, String password, String storagePoolName) {
        try {
            final int clientTimeout = StorageManager.STORAGE_POOL_CLIENT_TIMEOUT.value();
            final int clientMaxConnections = StorageManager.STORAGE_POOL_CLIENT_MAX_CONNECTIONS.value();
            ScaleIOGatewayClient client = ScaleIOGatewayClient.getClient(url, username, password, false, clientTimeout, clientMaxConnections);
            List<org.apache.cloudstack.storage.datastore.api.StoragePool> storagePools = client.listStoragePools();
            for (org.apache.cloudstack.storage.datastore.api.StoragePool pool : storagePools) {
                if (pool.getName().equals(storagePoolName)) {
                    LOGGER.info("Found PowerFlex storage pool: " + storagePoolName);
                    final org.apache.cloudstack.storage.datastore.api.StoragePoolStatistics poolStatistics = client.getStoragePoolStatistics(pool.getId());
                    pool.setStatistics(poolStatistics);

                    String systemId = client.getSystemId(pool.getProtectionDomainId());
                    pool.setSystemId(systemId);
                    return pool;
                }
            }
        } catch (NoSuchAlgorithmException | KeyManagementException | URISyntaxException e) {
            LOGGER.error("Failed to add storage pool", e);
            throw new CloudRuntimeException("Failed to establish connection with PowerFlex Gateway to find and validate storage pool: " + storagePoolName);
        }
        throw new CloudRuntimeException("Failed to find the provided storage pool name: " + storagePoolName + " in the discovered PowerFlex storage pools");
    }

    @SuppressWarnings("unchecked")
    @Override
    public DataStore initialize(Map<String, Object> dsInfos) {
        String url = (String) dsInfos.get("url");
        Long zoneId = (Long) dsInfos.get("zoneId");
        Long podId = (Long)dsInfos.get("podId");
        Long clusterId = (Long)dsInfos.get("clusterId");
        String dataStoreName = (String) dsInfos.get("name");
        String providerName = (String) dsInfos.get("providerName");
        Long capacityBytes = (Long)dsInfos.get("capacityBytes");
        Long capacityIops = (Long)dsInfos.get("capacityIops");
        String tags = (String)dsInfos.get("tags");
        Map<String, String> details = (Map<String, String>) dsInfos.get("details");

        if (zoneId == null) {
            throw new CloudRuntimeException("Zone Id must be specified.");
        }

        PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters();
        if (clusterId != null) {
            // Primary datastore is cluster-wide, check and set the podId and clusterId parameters
            if (podId == null) {
                throw new CloudRuntimeException("Pod Id must also be specified when the Cluster Id is specified for Cluster-wide primary storage.");
            }

            Hypervisor.HypervisorType hypervisorType = getHypervisorTypeForCluster(clusterId);
            if (!isSupportedHypervisorType(hypervisorType)) {
                throw new CloudRuntimeException("Unsupported hypervisor type: " + hypervisorType.toString());
            }

            parameters.setPodId(podId);
            parameters.setClusterId(clusterId);
        } else if (podId != null) {
            throw new CloudRuntimeException("Cluster Id must also be specified when the Pod Id is specified for Cluster-wide primary storage.");
        }

        URI uri = null;
        try {
            uri = new URI(UriUtils.encodeURIComponent(url));
            if (uri.getScheme() == null || !uri.getScheme().equalsIgnoreCase("powerflex")) {
                throw new InvalidParameterValueException("scheme is invalid for url: " + url + ", should be powerflex://username:password@gatewayhost/pool");
            }
        } catch (Exception ignored) {
            throw new InvalidParameterValueException(url + " is not a valid uri");
        }

        String storagePoolName = null;
        try {
            storagePoolName = URLDecoder.decode(uri.getPath(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            LOGGER.error("[ignored] we are on a platform not supporting \"UTF-8\"!?!", e);
        }
        if (storagePoolName == null) { // if decoding fails, use getPath() anyway
            storagePoolName = uri.getPath();
        }
        storagePoolName = storagePoolName.replaceFirst("/", "");

        final String storageHost = uri.getHost();
        final int port = uri.getPort();
        String gatewayApiURL = null;
        if (port == -1) {
            gatewayApiURL = String.format("https://%s/api", storageHost);
        } else {
            gatewayApiURL = String.format("https://%s:%d/api", storageHost, port);
        }

        final String userInfo = uri.getUserInfo();
        final String gatewayUsername = userInfo.split(":")[0];
        final String gatewayPassword = userInfo.split(":")[1];

        List<StoragePoolVO> storagePoolVO = primaryDataStoreDao.findPoolsByProvider(ScaleIOUtil.PROVIDER_NAME);
        if (CollectionUtils.isNotEmpty(storagePoolVO)) {
            for (StoragePoolVO poolVO : storagePoolVO) {
                Map <String, String> poolDetails = primaryDataStoreDao.getDetails(poolVO.getId());
                String poolUrl = poolDetails.get(ScaleIOGatewayClient.GATEWAY_API_ENDPOINT);
                String poolName = poolDetails.get(ScaleIOGatewayClient.STORAGE_POOL_NAME);

                if (gatewayApiURL.equals(poolUrl) && storagePoolName.equals(poolName)) {
                    throw new IllegalArgumentException("PowerFlex storage pool: " + storagePoolName + " already exists, please specify other storage pool.");
                }
            }
        }

        final org.apache.cloudstack.storage.datastore.api.StoragePool scaleIOPool = this.findStoragePool(gatewayApiURL,
                gatewayUsername, gatewayPassword, storagePoolName);

        parameters.setZoneId(zoneId);
        parameters.setName(dataStoreName);
        parameters.setProviderName(providerName);
        parameters.setManaged(true);
        parameters.setHost(storageHost);
        parameters.setPath(scaleIOPool.getId());
        parameters.setUserInfo(userInfo);
        parameters.setType(Storage.StoragePoolType.PowerFlex);
        parameters.setHypervisorType(Hypervisor.HypervisorType.KVM);
        parameters.setUuid(UUID.randomUUID().toString());
        parameters.setTags(tags);

        StoragePoolStatistics poolStatistics = scaleIOPool.getStatistics();
        if (poolStatistics != null) {
            if (capacityBytes == null) {
                parameters.setCapacityBytes(poolStatistics.getNetMaxCapacityInBytes());
            }
            parameters.setUsedBytes(poolStatistics.getNetUsedCapacityInBytes());
        }

        if (capacityBytes != null) {
            parameters.setCapacityBytes(capacityBytes);
        }

        if (capacityIops != null) {
            parameters.setCapacityIops(capacityIops);
        }

        details.put(ScaleIOGatewayClient.GATEWAY_API_ENDPOINT, gatewayApiURL);
        details.put(ScaleIOGatewayClient.GATEWAY_API_USERNAME, DBEncryptionUtil.encrypt(gatewayUsername));
        details.put(ScaleIOGatewayClient.GATEWAY_API_PASSWORD, DBEncryptionUtil.encrypt(gatewayPassword));
        details.put(ScaleIOGatewayClient.STORAGE_POOL_NAME, storagePoolName);
        details.put(ScaleIOGatewayClient.STORAGE_POOL_SYSTEM_ID, scaleIOPool.getSystemId());
        parameters.setDetails(details);

        return dataStoreHelper.createPrimaryDataStore(parameters);
    }

    @Override
    public boolean attachCluster(DataStore dataStore, ClusterScope scope) {
        final ClusterVO cluster = clusterDao.findById(scope.getScopeId());
        if (!isSupportedHypervisorType(cluster.getHypervisorType())) {
            throw new CloudRuntimeException("Unsupported hypervisor type: " + cluster.getHypervisorType().toString());
        }

        checkConnectedSdcs(dataStore.getId());

        PrimaryDataStoreInfo primaryDataStoreInfo = (PrimaryDataStoreInfo) dataStore;
        List<HostVO> hostsInCluster = resourceManager.listAllUpAndEnabledHosts(Host.Type.Routing, primaryDataStoreInfo.getClusterId(),
                primaryDataStoreInfo.getPodId(), primaryDataStoreInfo.getDataCenterId());
        if (hostsInCluster.isEmpty()) {
            primaryDataStoreDao.expunge(primaryDataStoreInfo.getId());
            throw new CloudRuntimeException("No hosts are Up to associate a storage pool with in cluster: " + primaryDataStoreInfo.getClusterId());
        }

        LOGGER.debug("Attaching the pool to each of the hosts in the cluster: " + primaryDataStoreInfo.getClusterId());
        List<HostVO> poolHosts = new ArrayList<HostVO>();
        for (HostVO host : hostsInCluster) {
            try {
                if (storageMgr.connectHostToSharedPool(host.getId(), primaryDataStoreInfo.getId())) {
                    poolHosts.add(host);
                }
            } catch (Exception e) {
                LOGGER.warn("Unable to establish a connection between " + host + " and " + primaryDataStoreInfo, e);
            }
        }

        if (poolHosts.isEmpty()) {
            LOGGER.warn("No host can access storage pool '" + primaryDataStoreInfo + "' on cluster '" + primaryDataStoreInfo.getClusterId() + "'.");
            primaryDataStoreDao.expunge(primaryDataStoreInfo.getId());
            throw new CloudRuntimeException("Failed to create storage pool in the cluster: " + primaryDataStoreInfo.getClusterId() + " as it is not accessible to hosts");
        }

        dataStoreHelper.attachCluster(dataStore);
        return true;
    }

    @Override
    public boolean attachHost(DataStore store, HostScope scope, StoragePoolInfo existingInfo) {
        return true;
    }

    @Override
    public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.HypervisorType hypervisorType) {
        if (!isSupportedHypervisorType(hypervisorType)) {
            throw new CloudRuntimeException("Unsupported hypervisor type: " + hypervisorType.toString());
        }

        checkConnectedSdcs(dataStore.getId());

        LOGGER.debug("Attaching the pool to each of the hosts in the zone: " + scope.getScopeId());
        List<HostVO> hosts = resourceManager.listAllUpAndEnabledHostsInOneZoneByHypervisor(hypervisorType, scope.getScopeId());
        List<HostVO> poolHosts = new ArrayList<HostVO>();
        for (HostVO host : hosts) {
            try {
                if (storageMgr.connectHostToSharedPool(host.getId(), dataStore.getId())) {
                    poolHosts.add(host);
                }
            } catch (Exception e) {
                LOGGER.warn("Unable to establish a connection between " + host + " and " + dataStore, e);
            }
        }
        if (poolHosts.isEmpty()) {
            LOGGER.warn("No host can access storage pool " + dataStore + " in this zone.");
            primaryDataStoreDao.expunge(dataStore.getId());
            throw new CloudRuntimeException("Failed to create storage pool as it is not accessible to hosts.");
        }

        dataStoreHelper.attachZone(dataStore);
        return true;
    }

    private void checkConnectedSdcs(Long dataStoreId) {
        boolean haveConnectedSdcs = false;
        try {
            ScaleIOGatewayClient client = ScaleIOGatewayClientConnectionPool.getInstance().getClient(dataStoreId, storagePoolDetailsDao);
            haveConnectedSdcs = client.haveConnectedSdcs();
        } catch (NoSuchAlgorithmException | KeyManagementException | URISyntaxException e) {
            LOGGER.error(String.format("Failed to create storage pool for datastore: %s", dataStoreId), e);
            throw new CloudRuntimeException(String.format("Failed to establish connection with PowerFlex Gateway to create storage pool for datastore: %s", dataStoreId));
        }

        if (!haveConnectedSdcs) {
            LOGGER.debug(String.format("No connected SDCs found for the PowerFlex storage pool of datastore: %s", dataStoreId));
            throw new CloudRuntimeException(String.format("Failed to create storage pool as connected SDCs not found for datastore: %s", dataStoreId));
        }
    }

    @Override
    public boolean maintain(DataStore store) {
        storagePoolAutomation.maintain(store);
        dataStoreHelper.maintain(store);
        return true;
    }

    @Override
    public boolean cancelMaintain(DataStore store) {
        dataStoreHelper.cancelMaintain(store);
        storagePoolAutomation.cancelMaintain(store);
        return true;
    }

    @Override
    public void enableStoragePool(DataStore dataStore) {
        dataStoreHelper.enable(dataStore);
    }

    @Override
    public void disableStoragePool(DataStore dataStore) {
        dataStoreHelper.disable(dataStore);
    }

    @Override
    public boolean deleteDataStore(DataStore dataStore) {
        StoragePool storagePool = (StoragePool)dataStore;
        StoragePoolVO storagePoolVO = primaryDataStoreDao.findById(storagePool.getId());
        if (storagePoolVO == null) {
            return false;
        }

        List<VMTemplateStoragePoolVO> unusedTemplatesInPool = templateMgr.getUnusedTemplatesInPool(storagePoolVO);
        for (VMTemplateStoragePoolVO templatePoolVO : unusedTemplatesInPool) {
            if (templatePoolVO.getDownloadState() == VMTemplateStorageResourceAssoc.Status.DOWNLOADED) {
                templateMgr.evictTemplateFromStoragePool(templatePoolVO);
            }
        }

        List<StoragePoolHostVO> poolHostVOs = storagePoolHostDao.listByPoolId(dataStore.getId());
        for (StoragePoolHostVO poolHostVO : poolHostVOs) {
            DeleteStoragePoolCommand deleteStoragePoolCommand = new DeleteStoragePoolCommand(storagePool);
            final Answer answer = agentMgr.easySend(poolHostVO.getHostId(), deleteStoragePoolCommand);
            if (answer != null && answer.getResult()) {
                LOGGER.info("Successfully deleted storage pool: " + storagePool.getId() + " from host: " + poolHostVO.getHostId());
            } else {
                if (answer != null) {
                    LOGGER.error("Failed to delete storage pool: " + storagePool.getId() + " from host: " + poolHostVO.getHostId() + " , result: " + answer.getResult());
                } else {
                    LOGGER.error("Failed to delete storage pool: " + storagePool.getId() + " from host: " + poolHostVO.getHostId());
                }
            }
        }

        ScaleIOGatewayClientConnectionPool.getInstance().removeClient(dataStore.getId());

        return dataStoreHelper.deletePrimaryDataStore(dataStore);
    }

    @Override
    public boolean migrateToObjectStore(DataStore store) {
        return false;
    }

    @Override
    public void updateStoragePool(StoragePool storagePool, Map<String, String> details) {
        String capacityBytes = details.get(PrimaryDataStoreLifeCycle.CAPACITY_BYTES);
        StoragePoolVO storagePoolVO = primaryDataStoreDao.findById(storagePool.getId());

        try {
            if (capacityBytes == null || capacityBytes.isBlank()) {
                return;
            }

            long usedBytes = capacityMgr.getUsedBytes(storagePoolVO);
            if (Long.parseLong(capacityBytes) < usedBytes) {
                throw new CloudRuntimeException("Cannot reduce the number of bytes for this storage pool as it would lead to an insufficient number of bytes");
            }

            primaryDataStoreDao.updateCapacityBytes(storagePool.getId(), Long.parseLong(capacityBytes));
            LOGGER.info("Storage pool successfully updated");
        } catch (Throwable e) {
            throw new CloudRuntimeException("Failed to update the storage pool" + e);
        }
    }

    private Hypervisor.HypervisorType getHypervisorTypeForCluster(long clusterId) {
        ClusterVO cluster = clusterDao.findById(clusterId);
        if (cluster == null) {
            throw new CloudRuntimeException("Unable to locate the specified cluster: " + clusterId);
        }

        return cluster.getHypervisorType();
    }

    private static boolean isSupportedHypervisorType(Hypervisor.HypervisorType hypervisorType) {
        return Hypervisor.HypervisorType.KVM.equals(hypervisorType);
    }
}
