package be.iminds.ilabt.jfed.experiment.util;

import be.iminds.ilabt.jfed.experiment.Experiment;
import be.iminds.ilabt.jfed.highlevel.model.Slice;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.JFedUtils;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.FileConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

/**
 * User: twalcari
 * Date: 12/6/13
 * Time: 9:40 AM
 */
public class ExperimentRestoreInformation {
    private static final DateTimeFormatter EXPIRATION_DATETIME_FORMAT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
    private static final String KEY_RESERVATION_START_TIME = "reservation-start-time";
    private static final String KEY_EXPIRATION_TIME = "expiration-time";
    private static final String KEY_REQUEST_RSPEC = "request-rspec";

    private static final Logger LOG = LoggerFactory.getLogger(ExperimentRestoreInformation.class);
    private static final String SLICE_RESTORE_INFORMATION_EXT = ".sri"; //Slice Restore Information
    private static final String SLICE_RESTORE_INFORMATION_BASEDIR = JFedUtils.getUserDataDirectory() + "slice_recovery";

    private final FileConfiguration conf;

    private final GeniUrn sliceUrn;

    public ExperimentRestoreInformation(GeniUrn sliceUrn) {
        this.sliceUrn = sliceUrn;
        this.conf = createConfigurationFile(sliceUrn);
    }

    public void bindToExperiment(final Experiment experiment) {
        assert (experiment != null);
        assert (experiment.getSlice() != null);

        updateSliceRequestRspec(experiment.getSlice());
        experiment.getSlice().requestRspecProperty().addListener(observable -> updateSliceRequestRspec(experiment.getSlice()));

        updateSliceExpirationTime(experiment.getSlice());
        experiment.getSlice().expirationDateProperty().addListener(observable -> updateSliceExpirationTime(experiment.getSlice()));

        setReservationStartTime(experiment.getRequestedStartTime());
    }

    private void updateSliceRequestRspec(Slice slice) {
        if (slice.getRequestRspec() != null)
            setRequestRspec(slice.getRequestRspec().getRspecXmlString());
        else
            setRequestRspec(null);
    }

    private void updateSliceExpirationTime(Slice slice) {
        setExpirationTime(slice.getExpirationDate());
    }


    private static FileConfiguration createConfigurationFile(GeniUrn sliceUrn) {

        File file = getFile(sliceUrn);

        if (!file.getParentFile().exists() && !file.getParentFile().mkdirs())
            LOG.warn("Could not make the necessary directories for storing the slice restore information");

        try {
            FileConfiguration conf = new PropertiesConfiguration(file);
            conf.setEncoding("UTF-8");
            conf.setAutoSave(true);

            return conf;
        } catch (ConfigurationException e) {
            LOG.error("Could not initialize file for saving slice backup information", e);
        }

        //If we get here, initializing the configuration file failed.
        //We assume that the file is corrupted, and remove it.

        try {
            Files.delete(file.toPath());
        } catch (IOException e) {
            LOG.error("Could not delete file {}.", file.getAbsolutePath(), e);
        }


        try {
            FileConfiguration conf = new PropertiesConfiguration(file);
            conf.setEncoding("UTF-8");
            conf.setAutoSave(true);

            return conf;
        } catch (ConfigurationException e) {
            LOG.error("Could not initialize file on SECOND TRY for saving slice backup information", e);
            return null;
        }
    }

    public GeniUrn getSliceUrn() {
        return sliceUrn;
    }

    @Nullable
    public Instant getExpirationTime() {
        if (conf == null) {
            LOG.warn("Could not access SliceRestoreInformation-file");
            return null;
        }

        if (conf.containsKey(KEY_EXPIRATION_TIME)) {
            try {
                return ZonedDateTime.parse(conf.getString(KEY_EXPIRATION_TIME), EXPIRATION_DATETIME_FORMAT).toInstant();
            } catch (DateTimeParseException ex) {
                LOG.debug("Could not parse expiration date '{}'", conf.getString(KEY_EXPIRATION_TIME), ex);
                return null;
            }
        } else return null;
    }

    private void setExpirationTime(Instant dateTime) {
        if (conf == null) {
            LOG.warn("Could not access SliceRestoreInformation-file");
            return;
        }
        if (dateTime != null)
            conf.setProperty(KEY_EXPIRATION_TIME, EXPIRATION_DATETIME_FORMAT.format(ZonedDateTime.ofInstant(dateTime, ZoneId.systemDefault())));
        else
            conf.clearProperty(KEY_EXPIRATION_TIME);
    }

    @Nullable
    public Instant getReservationStartTime() {
        if (conf == null) {
            LOG.warn("Could not access SliceRestoreInformation-file");
            return null;
        }

        if (conf.containsKey(KEY_RESERVATION_START_TIME)) {
            try {
                return ZonedDateTime.parse(conf.getString(KEY_RESERVATION_START_TIME), EXPIRATION_DATETIME_FORMAT).toInstant();
            } catch (DateTimeParseException ex) {
                LOG.debug("Could not parse reservation start time '{}'", conf.getString(KEY_RESERVATION_START_TIME), ex);
                return null;
            }
        } else return null;
    }

    private void setReservationStartTime(Instant dateTime) {
        if (conf == null) {
            LOG.warn("Could not access SliceRestoreInformation-file");
            return;
        }
        if (dateTime != null)
            conf.setProperty(KEY_RESERVATION_START_TIME, EXPIRATION_DATETIME_FORMAT.format(ZonedDateTime.ofInstant(dateTime, ZoneId.systemDefault())));
        else
            conf.clearProperty(KEY_RESERVATION_START_TIME);
    }


    public String getRequestRspec() {
        if (conf == null) {
            LOG.warn("Could not access SliceRestoreInformation-file");
            return null;
        }

        return conf.getString(KEY_REQUEST_RSPEC);
    }

    private void setRequestRspec(String rspec) {
        if (conf == null) {
            LOG.warn("Could not access SliceRestoreInformation-file");
            return;
        }

        if (rspec != null)
            conf.setProperty(KEY_REQUEST_RSPEC, rspec.replace(",", "\\,"));
        else
            conf.clearProperty(KEY_REQUEST_RSPEC);
    }

    /**
     * Returns whether slice restore information exists for the given slice on the given authority
     *
     * @param sliceUrn the sliceUrn
     * @return
     */
    public static boolean exists(GeniUrn sliceUrn) {
        return getFile(sliceUrn).exists();
    }

    private static File getFile(GeniUrn urn) {
        StringBuilder sb = new StringBuilder();
        sb.append(SLICE_RESTORE_INFORMATION_BASEDIR);
        sb.append(File.separator);
        sb.append(scrubIllegalPathCharacters(urn.getEncodedTopLevelAuthority_withoutSubAuth()));

        if (urn.hasSubAuthority()) {
            sb.append(File.separator);
            sb.append(scrubIllegalPathCharacters(urn.getEncodedSubAuthName()));
        }
        sb.append(File.separator);
        sb.append(scrubIllegalPathCharacters(urn.getEncodedResourceName()));
        sb.append(SLICE_RESTORE_INFORMATION_EXT);

        return new File(sb.toString());
    }

    public void delete() {
        conf.clear();
    }

    private static String scrubIllegalPathCharacters(String input){
        return input.replaceAll("[^a-zA-Z0-9.-]", "_");

    }
}
