package ctbrec.recorder;
import static ctbrec.recorder.NextGenLocalRecorder.*;

import java.io.IOException;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.recorder.download.Download;

public class RecordingPreconditions {

    private static final Logger LOG = LoggerFactory.getLogger(RecordingPreconditions.class);

    private NextGenLocalRecorder recorder;
    private long lastPreconditionMessage = 0;

    RecordingPreconditions(NextGenLocalRecorder recorder) {
        this.recorder = recorder;
    }

    void check(Model model) throws IOException {
        ensureRecorderIsActive();
        ensureModelIsNotSuspended(model);
        ensureModelIsNotMarkedForLaterRecording(model);
        ensureRecordUntilIsInFuture(model);
        ensureNoRecordingRunningForModel(model);
        ensureModelShouldBeRecorded(model);
        ensureEnoughSpaceForRecording();
        ensureDownloadSlotAvailable(model);
        ensureModelIsOnline(model);
    }

    private void ensureModelIsOnline(Model model) {
        try {
            if (!model.isOnline(IGNORE_CACHE)) {
                throw new PreconditionNotMetException(model.getName() + "'s room is not public");
            }
        } catch (IOException | ExecutionException e) {
            throw new PreconditionNotMetException(model.getName() + "'s room is not public");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new PreconditionNotMetException(model.getName() + "'s room is not public");
        }
    }

    private void ensureRecordUntilIsInFuture(Model model) {
        if (Instant.now().isAfter(model.getRecordUntil())) {
            throw new RecordUntilExpiredException(model.getRecordUntil());
        }
    }

    private void ensureEnoughSpaceForRecording() throws IOException {
        if (!recorder.enoughSpaceForRecording()) {
            throw new PreconditionNotMetException("Not enough disk space for recording");
        }
    }

    private void ensureDownloadSlotAvailable(Model model) {
        if (!downloadSlotAvailable()) {
            long now = System.currentTimeMillis();
            if ((now - lastPreconditionMessage) > TimeUnit.MINUTES.toMillis(1)) {
                LOG.info("The number of downloads is maxed out");
            }
            // check, if we can stop a recording for a model with lower priority
            Optional<Recording> lowerPrioRecordingProcess = recordingProcessWithLowerPrio(model.getPriority());
            if (lowerPrioRecordingProcess.isPresent()) {
                Download download = lowerPrioRecordingProcess.get().getDownload();
                Model lowerPrioModel = download.getModel();
                LOG.info("Stopping recording for {}. Prio {} < {}", lowerPrioModel.getName(), lowerPrioModel.getPriority(), model.getPriority());
                recorder.stopRecordingProcess(lowerPrioModel);
            } else {
                throw new PreconditionNotMetException("Other models have higher prio, not starting recording for " + model.getName());
            }
        }
    }

    private Optional<Recording> recordingProcessWithLowerPrio(int priority) {
        Model lowest = null;
        for (Model m : recorder.getRecordingProcesses().keySet()) {
            if (lowest == null || m.getPriority() < lowest.getPriority()) {
                lowest = m;
            }
        }
        if (lowest != null && lowest.getPriority() < priority) {
            return Optional.of(recorder.getRecordingProcesses().get(lowest));
        } else {
            return Optional.empty();
        }
    }

    private void ensureModelShouldBeRecorded(Model model) {
        if (!recorder.getModels().contains(model)) {
            throw new PreconditionNotMetException("Model " + model + " has been removed. Restarting of recording cancelled.");
        }
    }

    private void ensureNoRecordingRunningForModel(Model model) {
        if (recorder.getRecordingProcesses().containsKey(model)) {
            throw new PreconditionNotMetException("A recording for model " + model + " is already running");
        }
    }

    private void ensureModelIsNotSuspended(Model model) {
        if (model.isSuspended()) {
            throw new PreconditionNotMetException("Recording for model " + model + " is suspended");
        }
    }

    private void ensureModelIsNotMarkedForLaterRecording(Model model) {
        if (model.isMarkedForLaterRecording()) {
            throw new PreconditionNotMetException("Model " + model + " is marked for later recording");
        }
    }

    private void ensureRecorderIsActive() {
        if (!recorder.isRecording()) {
            throw new PreconditionNotMetException("Recorder is not in recording mode");
        }
    }

    private boolean downloadSlotAvailable() {
        int concurrentRecordings = Config.getInstance().getSettings().concurrentRecordings;
        return concurrentRecordings == 0 || concurrentRecordings > 0 && recorder.getRecordingProcesses().size() < concurrentRecordings;
    }
}
