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

import com.cloud.api.ApiDBUtils;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.storage.StorageManager;
import com.cloud.storage.StorageStats;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.VolumeVO;
import com.cloud.user.AccountVO;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.exception.CloudRuntimeException;
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.SnapshotInfo;
import org.apache.cloudstack.secstorage.HeuristicVO;
import org.apache.cloudstack.secstorage.dao.SecondaryStorageHeuristicDao;
import org.apache.cloudstack.secstorage.heuristics.HeuristicType;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
import org.apache.cloudstack.storage.heuristics.presetvariables.Account;
import org.apache.cloudstack.storage.heuristics.presetvariables.Domain;
import org.apache.cloudstack.storage.heuristics.presetvariables.PresetVariables;
import org.apache.cloudstack.storage.heuristics.presetvariables.SecondaryStorage;
import org.apache.cloudstack.storage.heuristics.presetvariables.Snapshot;
import org.apache.cloudstack.storage.heuristics.presetvariables.Template;
import org.apache.cloudstack.storage.heuristics.presetvariables.Volume;
import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Helper class for building and injecting the heuristics preset variables into the JS script.
 */
public class HeuristicRuleHelper {

    protected Logger logger = LogManager.getLogger(HeuristicRuleHelper.class);

    private static final Long HEURISTICS_SCRIPT_TIMEOUT = StorageManager.HEURISTICS_SCRIPT_TIMEOUT.value();

    @Inject
    private DataStoreManager dataStoreManager;

    @Inject
    private ImageStoreDao imageStoreDao;

    @Inject
    private SecondaryStorageHeuristicDao secondaryStorageHeuristicDao;

    @Inject
    private DomainDao domainDao;

    @Inject
    private AccountDao accountDao;

    @Inject
    private DataCenterDao zoneDao;

    /**
     * Returns the {@link DataStore} object if the zone, specified by the ID, has an active heuristic rule for the given {@link HeuristicType}.
     * It returns null otherwise.
     * @param zoneId used to search for the heuristic rules.
     * @param heuristicType used for checking if there is a heuristic rule for the given {@link HeuristicType}.
     * @param obj can be from the following classes: {@link VMTemplateVO}, {@link SnapshotInfo} and {@link VolumeVO}.
     *           They are used to retrieve attributes for injecting in the JS rule.
     * @return the corresponding {@link DataStore} if there is a heuristic rule, returns null otherwise.
     */
    public DataStore getImageStoreIfThereIsHeuristicRule(Long zoneId, HeuristicType heuristicType, Object obj) {
        HeuristicVO heuristicsVO = secondaryStorageHeuristicDao.findByZoneIdAndType(zoneId, heuristicType);

        if (heuristicsVO == null) {
            logger.debug("No heuristic rules found for zone [{}] and heuristic type [{}]. Returning null.", () -> zoneDao.findById(zoneId), heuristicType::toString);
            return null;
        } else {
            logger.debug("Found the heuristic rule {} to apply for zone [{}].", heuristicsVO::toString, () -> zoneDao.findById(zoneId));
            return interpretHeuristicRule(heuristicsVO.getHeuristicRule(), heuristicType, obj, zoneId);
        }
    }

    /**
     * Build the preset variables ({@link Template}, {@link Snapshot} and {@link Volume}) for the JS script.
     */
    protected void buildPresetVariables(JsInterpreter jsInterpreter, HeuristicType heuristicType, long zoneId, Object obj) {
        PresetVariables presetVariables = new PresetVariables();
        Long accountId = null;

        switch (heuristicType) {
            case TEMPLATE:
            case ISO:
                presetVariables.setTemplate(setTemplatePresetVariable((VMTemplateVO) obj));
                accountId = ((VMTemplateVO) obj).getAccountId();
                break;
            case SNAPSHOT:
                presetVariables.setSnapshot(setSnapshotPresetVariable((SnapshotInfo) obj));
                accountId = ((SnapshotInfo) obj).getAccountId();
                break;
            case VOLUME:
                presetVariables.setVolume(setVolumePresetVariable((VolumeVO) obj));
                accountId = ((VolumeVO) obj).getAccountId();
                break;
        }
        presetVariables.setAccount(setAccountPresetVariable(accountId));
        presetVariables.setSecondaryStorages(setSecondaryStoragesVariable(zoneId));

        injectPresetVariables(jsInterpreter, presetVariables);
    }

    /**
     * Inject the {@link PresetVariables} ({@link Template}, {@link Snapshot} and {@link Volume}) into the JS {@link JsInterpreter}.
     * For each type, they can be accessed using the following variables in the script:
     * <ul>
     *     <li>ISO/Template: using the variable <b>iso</b> or <b>template</b>, e.g. <b>iso.format</b> </li>
     *     <li>Snapshot: using the variable <b>snapshot</b>, e.g. <b>snapshot.size</b></li>
     *     <li>Volume: using the variable <b>volume</b>, e.g. <b>volume.format</b></li>
     * </ul>
     * @param jsInterpreter the JS script
     * @param presetVariables used for injecting in the JS interpreter.
     */
    protected void injectPresetVariables(JsInterpreter jsInterpreter, PresetVariables presetVariables) {
        jsInterpreter.injectVariable("secondaryStorages", presetVariables.getSecondaryStorages().toString());

        if (presetVariables.getTemplate() != null) {
            jsInterpreter.injectVariable("template", presetVariables.getTemplate().toString());
            jsInterpreter.injectVariable("iso", presetVariables.getTemplate().toString());
        }

        if (presetVariables.getSnapshot() != null) {
            jsInterpreter.injectVariable("snapshot", presetVariables.getSnapshot().toString());
        }

        if (presetVariables.getVolume() != null) {
            jsInterpreter.injectVariable("volume", presetVariables.getVolume().toString());
        }

        if (presetVariables.getAccount() != null) {
            jsInterpreter.injectVariable("account", presetVariables.getAccount().toString());
        }
    }

    protected List<SecondaryStorage> setSecondaryStoragesVariable(long zoneId) {
        List<SecondaryStorage> secondaryStorageList = new ArrayList<>();
        List<ImageStoreVO> imageStoreVOS = imageStoreDao.listStoresByZoneId(zoneId);

        for (ImageStoreVO imageStore : imageStoreVOS) {
            SecondaryStorage secondaryStorage = new SecondaryStorage();

            secondaryStorage.setName(imageStore.getName());
            secondaryStorage.setId(imageStore.getUuid());
            secondaryStorage.setProtocol(imageStore.getProtocol());
            StorageStats storageStats = ApiDBUtils.getSecondaryStorageStatistics(imageStore.getId());

            if (storageStats != null) {
                secondaryStorage.setUsedDiskSize(storageStats.getByteUsed());
                secondaryStorage.setTotalDiskSize(storageStats.getCapacityBytes());
            }

            secondaryStorageList.add(secondaryStorage);
        }
        return secondaryStorageList;
    }

    protected Template setTemplatePresetVariable(VMTemplateVO templateVO) {
        Template template = new Template();

        template.setName(templateVO.getName());
        template.setFormat(templateVO.getFormat());
        template.setHypervisorType(templateVO.getHypervisorType());

        return template;
    }

    protected Volume setVolumePresetVariable(VolumeVO volumeVO) {
        Volume volume = new Volume();

        volume.setName(volumeVO.getName());
        volume.setFormat(volumeVO.getFormat());
        volume.setSize(volumeVO.getSize());

        return volume;
    }

    protected Snapshot setSnapshotPresetVariable(SnapshotInfo snapshotInfo) {
        Snapshot snapshot = new Snapshot();

        snapshot.setName(snapshotInfo.getName());
        snapshot.setSize(snapshotInfo.getSize());
        snapshot.setHypervisorType(snapshotInfo.getHypervisorType());

        return snapshot;
    }

    protected Account setAccountPresetVariable(Long accountId) {
        if (accountId == null) {
            return null;
        }

        AccountVO account = accountDao.findById(accountId);
        if (account == null) {
            return null;
        }

        Account accountVariable = new Account();
        accountVariable.setName(account.getName());
        accountVariable.setId(account.getUuid());

        accountVariable.setDomain(setDomainPresetVariable(account.getDomainId()));

        return accountVariable;
    }

    protected Domain setDomainPresetVariable(long domainId) {
        DomainVO domain = domainDao.findById(domainId);
        if (domain == null) {
            return null;
        }
        Domain domainVariable = new Domain();
        domainVariable.setName(domain.getName());
        domainVariable.setId(domain.getUuid());

        return domainVariable;
    }

    /**
     * This method calls {@link HeuristicRuleHelper#buildPresetVariables(JsInterpreter, HeuristicType, long, Object)} for building the preset variables and
     * execute the JS script specified in the <b>rule</b> ({@link String}) parameter. The script is pre-injected with the preset variables, to allow the JS script to reference them
     * in the code scope.
     * <br>
     * <br>
     * The JS script needs to return a valid UUID ({@link String}) of a secondary storage, otherwise a {@link CloudRuntimeException} is thrown.
     * @param rule the {@link String} representing the JS script.
     * @param heuristicType used for building the preset variables accordingly to the  {@link HeuristicType} specified.
     * @param obj can be from the following classes: {@link VMTemplateVO}, {@link SnapshotInfo} and {@link VolumeVO}.
     *           They are used to retrieve attributes for injecting in the JS rule.
     * @param zoneId used for injecting the {@link SecondaryStorage} preset variables.
     * @return the {@link DataStore} returned by the script.
     */
    public DataStore interpretHeuristicRule(String rule, HeuristicType heuristicType, Object obj, long zoneId) {
        try (JsInterpreter jsInterpreter = new JsInterpreter(HEURISTICS_SCRIPT_TIMEOUT)) {
            buildPresetVariables(jsInterpreter, heuristicType, zoneId, obj);
            Object scriptReturn = jsInterpreter.executeScript(rule);

            if (!(scriptReturn instanceof String)) {
                throw new CloudRuntimeException(String.format("Error while interpreting heuristic rule [%s], the rule did not return a String.", rule));
            }

            DataStore dataStore = dataStoreManager.getImageStoreByUuid((String) scriptReturn);

            if (dataStore == null) {
                throw new CloudRuntimeException(String.format("Unable to find a secondary storage with the UUID [%s] returned by the heuristic rule [%s]. Check if the rule is " +
                        "returning a valid UUID.", scriptReturn, rule));
            }

            return dataStore;
        } catch (IOException ex) {
            String message = String.format("Error while executing script [%s].", rule);
            logger.error(message, ex);
            throw new CloudRuntimeException(message, ex);
        }
    }

}
