/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.checker;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.checker.factory.CheckerServiceFactory;
import org.dspace.checker.service.ChecksumHistoryService;
import org.dspace.core.Context;
import org.dspace.core.Utils;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;

/**
 * Manages the deletion of results from the checksum history. It uses the
 * dspace.cfg file as the default configuration file for the deletion settings
 * and can use a different configuration file if it is passed in.
 *
 * @author Jim Downing
 * @author Grace Carpenter
 * @author Nathan Sarr
 */
public final class ResultsPruner {

    /**
     * Default logger.
     */
    private static final Logger LOG = LogManager.getLogger(ResultsPruner.class);

    /**
     * Factory method for the default results pruner configuration using
     * dspace.cfg
     *
     * @param context Context
     * @return a ResultsPruner that represent the default retention policy
     */
    public static ResultsPruner getDefaultPruner(Context context) {
        try {
            ConfigurationService configurationService
                    = DSpaceServicesFactory.getInstance().getConfigurationService();
            return getPruner(context, configurationService.getProperties());
        } catch (FileNotFoundException e) {
            throw new IllegalStateException(
                "VeryExceptionalException - config file not there! ", e);
        }
    }


    /**
     * Factory method for ResultsPruners
     *
     * @param context   Context
     * @param propsFile to configure the results pruner.
     * @return the configured results pruner.
     * @throws FileNotFoundException if file doesn't exist
     *                               it the configuration file cannot be found.
     */
    public static ResultsPruner getPruner(Context context, String propsFile)
        throws FileNotFoundException {
        Properties props = new Properties();
        FileInputStream fin = null;
        try {
            fin = new FileInputStream(propsFile);
            props.load(fin);

            return getPruner(context, props);
        } catch (IOException e) {
            throw new IllegalStateException("Problem loading properties file: "
                                                + e.getMessage(), e);
        } finally {
            if (fin != null) {
                try {
                    fin.close();
                } catch (IOException e) {
                    LOG.warn(e);
                }
            }
        }
    }

    /**
     * Factory method for ResultsPruners (used to load ConfigurationManager
     * properties.
     *
     * @param context Context
     * @param props   Properties
     * @return pruner
     * @throws FileNotFoundException if file doesn't exist
     */
    public static ResultsPruner getPruner(Context context, Properties props)
        throws FileNotFoundException {

        ResultsPruner rp = new ResultsPruner(context);
        Pattern retentionPattern = Pattern
            .compile("checker\\.retention\\.(.*)");
        for (Enumeration<String> en = (Enumeration<String>) props.propertyNames(); en.hasMoreElements(); ) {
            String name = en.nextElement();
            Matcher matcher = retentionPattern.matcher(name);
            if (!matcher.matches()) {
                continue;
            }
            String resultCode = matcher.group(1);
            long duration;
            try {
                duration = Utils.parseDuration(props.getProperty(name));
            } catch (ParseException e) {
                throw new IllegalStateException("Problem parsing duration: "
                                                    + e.getMessage(), e);
            }
            if ("default".equals(resultCode)) {
                rp.setDefaultDuration(duration);
            } else {
                ChecksumResultCode code = ChecksumResultCode.valueOf(resultCode);
                if (code == null) {
                    throw new IllegalStateException("Checksum result code not found: " + resultCode);
                }

                rp.addInterested(code, duration);
            }
        }
        return rp;

    }

    /**
     * Ten years
     */
    private long defaultDuration = 31536000000L;

    /**
     * Map of retention durations, keyed by result code name
     */
    Map<ChecksumResultCode, Long> interests = new HashMap<>();


    /**
     * Checksum history database data access.
     */
    private ChecksumHistoryService checksumHistoryService = null;

    private Context context = null;

    /**
     * Default Constructor
     *
     * @param context Context
     */
    public ResultsPruner(Context context) {
        this.checksumHistoryService = CheckerServiceFactory.getInstance().getChecksumHistoryService();
        this.context = context;
    }

    /**
     * Add a result and the length of time before the history with this type of
     * result is removed from the database.
     *
     * @param result   code in the database.
     * @param duration before bitstreams with the specified result type in the
     *                 checksum history is removed.
     */
    public void addInterested(ChecksumResultCode result, long duration) {
        interests.put(result, duration);
    }

    /**
     * Add a result and the length of time before it is removed from the
     * checksum history table.
     *
     * @param result   code in the database.
     * @param duration before bitstreams with the specified result type in the
     *                 checksum history is removed.
     * @throws ParseException if the duration cannot be parsed into a long value.
     */
    public void addInterested(ChecksumResultCode result, String duration)
        throws ParseException {
        addInterested(result, Utils.parseDuration(duration));
    }

    /**
     * The default amount of time before a result is removed.
     *
     * @return the default duration.
     */
    public long getDefaultDuration() {
        return defaultDuration;
    }

    /**
     * Prunes the results retaining results as configured by the interests
     * registered with this object.
     *
     * @return number of results removed.
     * @throws SQLException if database error
     */
    public int prune() throws SQLException {
        ChecksumResultCode[] codes = ChecksumResultCode.values();
        for (ChecksumResultCode code : codes) {
            if (!interests.containsKey(code)) {
                interests.put(code, defaultDuration);
            }

        }
        int result = checksumHistoryService.prune(context, interests);
        return result;
    }

    /**
     * The default duration before records are removed from the checksum history
     * table.
     *
     * @param defaultDuration used before records are removed from the checksum history.
     */
    public void setDefaultDuration(long defaultDuration) {
        this.defaultDuration = defaultDuration;
    }
}
