/*
 * This file is part of dependency-check-core.
 *
 * Licensed 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.
 *
 * Copyright (c) 2014 Jeremy Long. All Rights Reserved.
 */
package org.owasp.dependencycheck.data.update;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import javax.annotation.concurrent.ThreadSafe;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.data.nvdcve.CveDB;
import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
import org.owasp.dependencycheck.data.nvdcve.DatabaseProperties;
import org.owasp.dependencycheck.data.update.exception.UpdateException;
import org.owasp.dependencycheck.utils.DateUtil;
import org.owasp.dependencycheck.utils.DependencyVersion;
import org.owasp.dependencycheck.utils.Downloader;
import org.owasp.dependencycheck.utils.ResourceNotFoundException;
import org.owasp.dependencycheck.utils.Settings;
import org.owasp.dependencycheck.utils.TooManyRequestsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Checks the gh-pages dependency-check site to determine the current released
 * version number. If the released version number is greater than the running
 * version number a warning is printed recommending that an upgrade be
 * performed.
 *
 * @author Jeremy Long
 */
@ThreadSafe
public class EngineVersionCheck implements CachedWebDataSource {

    /**
     * Static logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(EngineVersionCheck.class);
    /**
     * The property key indicating when the last version check occurred.
     */
    public static final String ENGINE_VERSION_CHECKED_ON = "VersionCheckOn";
    /**
     * The property key indicating when the last version check occurred.
     */
    public static final String CURRENT_ENGINE_RELEASE = "CurrentEngineRelease";
    /**
     * The version retrieved from the database properties or web to check
     * against.
     */
    private String updateToVersion;
    /**
     * The configured settings.
     */
    private Settings settings;

    /**
     * Constructs a new engine version check utility for testing.
     *
     * @param settings the configured settings
     */
    protected EngineVersionCheck(Settings settings) {
        this.settings = settings;
    }

    /**
     * Constructs a new engine version check utility.
     */
    public EngineVersionCheck() {
    }

    /**
     * Getter for updateToVersion - only used for testing. Represents the
     * version retrieved from the database.
     *
     * @return the version to test
     */
    protected String getUpdateToVersion() {
        return updateToVersion;
    }

    /**
     * Setter for updateToVersion - only used for testing. Represents the
     * version retrieved from the database.
     *
     * @param version the version to test
     */
    protected void setUpdateToVersion(String version) {
        updateToVersion = version;
    }

    /**
     * Downloads the current released version number and compares it to the
     * running engine's version number. If the released version number is newer
     * a warning is printed recommending an upgrade.
     *
     * @return returns false as no updates are made to the database that would
     * require compaction
     * @throws UpdateException thrown if the local database properties could not
     * be updated
     */
    @Override
    public boolean update(Engine engine) throws UpdateException {
        this.settings = engine.getSettings();
        try {
            final CveDB db = engine.getDatabase();
            final boolean autoupdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
            final boolean enabled = settings.getBoolean(Settings.KEYS.UPDATE_VERSION_CHECK_ENABLED, true);
            final String datafeed = settings.getString(Settings.KEYS.NVD_API_DATAFEED_URL);
            /*
             * Only update if auto-update is enabled, the engine check is
             * enabled, and the NVD DataFeed is being used (i.e. the user
             * is likely on a private network). This check is not really needed
             * so we are okay skipping it.
             */
            if (enabled && autoupdate && datafeed != null) {
                LOGGER.debug("Begin Engine Version Check");

                final DatabaseProperties properties = db.getDatabaseProperties();

                final long lastChecked = DateUtil.getEpochValueInSeconds(properties.getProperty(ENGINE_VERSION_CHECKED_ON, "0"));
                final long now = System.currentTimeMillis() / 1000;
                updateToVersion = properties.getProperty(CURRENT_ENGINE_RELEASE, "");
                final String currentVersion = settings.getString(Settings.KEYS.APPLICATION_VERSION, "0.0.0");
                LOGGER.debug("Last checked: {}", lastChecked);
                LOGGER.debug("Now: {}", now);
                LOGGER.debug("Current version: {}", currentVersion);
                final boolean updateNeeded = shouldUpdate(lastChecked, now, properties, currentVersion);
                if (updateNeeded) {
                    LOGGER.warn("A new version of dependency-check is available. Consider updating to version {}.",
                            updateToVersion);
                }
            }
        } catch (DatabaseException ex) {
            LOGGER.debug("Database Exception opening databases to retrieve properties", ex);
            throw new UpdateException("Error occurred updating database properties.");
        }
        return false;
    }

    /**
     * Determines if a new version of the dependency-check engine has been
     * released.
     *
     * @param lastChecked the epoch time of the last version check
     * @param now the current epoch time
     * @param properties the database properties object
     * @param currentVersion the current version of dependency-check
     * @return <code>true</code> if a newer version of the database has been
     * released; otherwise <code>false</code>
     * @throws UpdateException thrown if there is an error connecting to the
     * github documentation site or accessing the local database.
     */
    protected boolean shouldUpdate(final long lastChecked, final long now, final DatabaseProperties properties,
            String currentVersion) throws UpdateException {
        //check every 30 days if we know there is an update, otherwise check every 7 days
        final int checkRange = 30;
        if (!DateUtil.withinDateRange(lastChecked, now, checkRange)) {
            LOGGER.debug("Checking web for new version.");
            final String currentRelease = getCurrentReleaseVersion();
            if (currentRelease != null) {
                final DependencyVersion v = new DependencyVersion(currentRelease);
                if (v.getVersionParts() != null && v.getVersionParts().size() >= 3) {
                    updateToVersion = v.toString();
                    if (!currentRelease.equals(updateToVersion)) {
                        properties.save(CURRENT_ENGINE_RELEASE, updateToVersion);
                    }
                    properties.save(ENGINE_VERSION_CHECKED_ON, Long.toString(now));
                }
            }
            LOGGER.debug("Current Release: {}", updateToVersion);
        }
        if (updateToVersion == null) {
            LOGGER.debug("Unable to obtain current release");
            return false;
        }
        final DependencyVersion running = new DependencyVersion(currentVersion);
        final DependencyVersion released = new DependencyVersion(updateToVersion);
        if (running.compareTo(released) < 0) {
            LOGGER.debug("Upgrade recommended");
            return true;
        }
        LOGGER.debug("Upgrade not needed");
        return false;
    }

    /**
     * Retrieves the current released version number from the github
     * documentation site.
     *
     * @return the current released version number
     */
    protected String getCurrentReleaseVersion() {
        try {
            final String str = settings.getString(Settings.KEYS.ENGINE_VERSION_CHECK_URL, "https://jeremylong.github.io/DependencyCheck/current.txt");
            final URL url = new URL(str);
            String releaseVersion = null;
            releaseVersion = Downloader.getInstance().fetchContent(url, StandardCharsets.UTF_8);
            return releaseVersion.trim();
        } catch (TooManyRequestsException ex) {
            LOGGER.debug("Unable to retrieve current release version of dependency-check - downloader failed on HTTP 429 Too many requests");
        } catch (ResourceNotFoundException ex) {
            LOGGER.debug("Unable to retrieve current release version of dependency-check - downloader  failed on HTTP 404 ResourceNotFound");
        } catch (MalformedURLException ex) {
            LOGGER.debug("Unable to retrieve current release version of dependency-check - malformed url?");
        } catch (IOException ex) {
            LOGGER.debug("Unable to retrieve current release version of dependency-check - i/o exception");
        }
        return null;
    }

    @Override
    public boolean purge(Engine engine) {
        return true;
    }
}
