package ctbrec.recorder;

import static ctbrec.Recording.State.*;
import static ctbrec.io.IoUtils.*;
import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;

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

import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;

import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.Recording.State;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.sites.Site;

public class RecordingManager {
    private static final Logger LOG = LoggerFactory.getLogger(RecordingManager.class);

    private Config config;
    private Moshi moshi;
    private JsonAdapter<Recording> adapter;
    private List<Recording> recordings = new ArrayList<>();
    private ReentrantLock recordingsLock = new ReentrantLock();

    public RecordingManager(Config config, List<Site> sites) throws IOException {
        this.config = config;
        moshi = new Moshi.Builder()
                .add(Model.class, new ModelJsonAdapter(sites))
                .add(Instant.class, new InstantJsonAdapter())
                .add(File.class, new FileJsonAdapter())
                .build();
        adapter = moshi.adapter(Recording.class).indent("  ");

        loadRecordings();
    }

    public void add(Recording rec) throws IOException {
        File recordingsMetaDir = getDir();
        String filename = rec.toString() + ".json";
        File recordingMetaData = new File(recordingsMetaDir, filename);
        rec.setMetaDataFile(recordingMetaData.getCanonicalPath());
        saveRecording(rec);
        recordingsLock.lock();
        try {
            recordings.add(rec);
        } finally {
            recordingsLock.unlock();
        }
    }

    public void saveRecording(Recording rec) throws IOException {
        if (rec.getMetaDataFile() != null) {
            File recordingMetaData = new File(rec.getMetaDataFile());
            String json = adapter.toJson(rec);
            rec.setMetaDataFile(recordingMetaData.getAbsolutePath());
            Files.createDirectories(recordingMetaData.getParentFile().toPath());
            Files.write(recordingMetaData.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING);
        }
    }

    private void loadRecordings() throws IOException {
        File recordingsMetaDir = getDir();
        File[] metaFiles = recordingsMetaDir.listFiles((file, name) -> name.endsWith(".json"));
        if (metaFiles != null) {
            for (File file : metaFiles) {
                String json = new String(Files.readAllBytes(file.toPath()), UTF_8);
                try {
                    Recording recording = adapter.fromJson(json);
                    if (recording.getStatus() == RECORDING || recording.getStatus() == GENERATING_PLAYLIST || recording.getStatus() == POST_PROCESSING) {
                        recording.setStatus(WAITING);
                    }
                    if (recording.getId() == null) {
                        recording.setId(UUID.randomUUID().toString());
                        saveRecording(recording);
                    }
                    if (recordingExists(recording)) {
                        recordings.add(recording);
                    } else {
                        LOG.info("Recording {} does not exist anymore -> deleting meta data", recording);
                        Files.deleteIfExists(file.toPath());
                    }
                } catch(Exception e) {
                    LOG.error("Couldn't load recording {}", file, e);
                }
            }
        }
    }

    private boolean recordingExists(Recording recording) {
        return recording.getAbsoluteFile().exists();
    }

    private File getDir() {
        File configDir = config.getConfigDir();
        File recordingsMetaDir = new File(configDir, "recordings");
        return recordingsMetaDir;
    }

    /**
     * Deletes a recording from disk and removes it from ctbrec
     * @param recording
     * @throws IOException
     */
    public void delete(Recording recording) throws IOException {
        if (recording.isPinned()) {
            throw new RecordingPinnedException(recording);
        }

        recordingsLock.lock();
        try {
            int idx = recordings.indexOf(recording);
            recording = recordings.get(idx);

            recording.setStatus(State.DELETING);
            File path = recording.getAbsoluteFile();
            boolean isFile = path.isFile();
            LOG.debug("Deleting {}", path);

            // delete the video files
            if (isFile) {
                Files.delete(path.toPath());
            } else {
                deleteDirectory(path);
            }

            // delete files associated with this recording
            for (String associated : recording.getAssociatedFiles()) {
                File f = new File(associated);
                if (f.isFile()) {
                    Files.delete(f.toPath());
                    deleteEmptyParents(f.getParentFile());
                } else {
                    deleteDirectory(f);
                    deleteEmptyParents(f);
                }
            }

            // delete the meta data
            Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());

            // delete empty parent files
            if (isFile) {
                deleteEmptyParents(path.getParentFile());
            } else {
                deleteEmptyParents(path);
            }

            // remove from data structure
            recordings.remove(recording);
            recording.setStatus(State.DELETED);
        } finally {
            recordingsLock.unlock();
        }
    }

    /**
     * Removes the recording from ctbrec, but does not touch the actual file
     * @param recording
     * @throws IOException
     */
    public void remove(Recording recording) throws IOException {
        recordingsLock.lock();
        try {
            int idx = recordings.indexOf(recording);
            recording = recordings.get(idx);
            File path = recording.getAbsoluteFile();
            deleteEmptyParents(path.getParentFile());
            // delete the meta data
            Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());
            // remove from data structure
            recordings.remove(recording);
        } finally {
            recordingsLock.unlock();
        }
    }

    public List<Recording> getAll() {
        recordingsLock.lock();
        try {
            Instant start = Instant.now();
            for (Recording recording : recordings) {
                recording.refresh();
            }
            Instant finish = Instant.now();
            long timeElapsed = Duration.between(start, finish).toMillis();
            LOG.trace("Recordings list refreshed in {} ms", timeElapsed);
            return new ArrayList<>(recordings);
        } finally {
            recordingsLock.unlock();
        }
    }

    public void pin(Recording recording) throws IOException {
        recordingsLock.lock();
        try {
            Recording local = getRecording(recording);
            local.setPinned(true);
            saveRecording(local);
        } finally {
            recordingsLock.unlock();
        }
    }

    public void unpin(Recording recording) throws IOException {
        recordingsLock.lock();
        try {
            Recording local = getRecording(recording);
            local.setPinned(false);
            saveRecording(local);
        } finally {
            recordingsLock.unlock();
        }
    }

    public void setNote(Recording rec, String note) throws IOException {
        recordingsLock.lock();
        try {
            Recording local = getRecording(rec);
            local.setNote(note);
            saveRecording(local);
        } finally {
            recordingsLock.unlock();
        }
    }

    private Recording getRecording(Recording rec) {
        for (Recording r : getAll()) {
            if(Objects.equals(r, rec)) {
                return r;
            }
        }
        throw new NoSuchElementException("Recording not found");
    }
}
