package ch.zhaw.gartenverwaltung.io;

import ch.zhaw.gartenverwaltung.types.Crop;
import ch.zhaw.gartenverwaltung.types.Task;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Implements the {@link TaskDatabase} interface for loading and writing {@link Task} objects
 * from and to a JSON file.
 * The reads are cached to minimize file-io operations.
 */
public class JsonTaskDatabase implements TaskDatabase{
    IdProvider idProvider;
    private final URL dataSource = getClass().getResource("taskdb.json");
    private final static String INVALID_DATASOURCE_MSG = "Invalid datasource specified!";

    private Map<Long, Task> taskMap = Collections.emptyMap();

    /**
     * Creating constant objects required to deserialize the {@link LocalDate} classes
     */
    private final static JavaTimeModule timeModule = new JavaTimeModule();
    static {
        DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDateDeserializer dateDeserializer = new LocalDateDeserializer(dateFormat);
        LocalDateSerializer dateSerializer = new LocalDateSerializer(dateFormat);

        timeModule.addDeserializer(LocalDate.class, dateDeserializer);
        timeModule.addSerializer(LocalDate.class, dateSerializer);
    }

    /**
     * If no data is currently loaded, data is loaded from {@link #dataSource}.
     * In any case, the values of {@link #taskMap} are returned.
     *
     * @see TaskDatabase#getTaskList(LocalDate, LocalDate)
     */
    @Override
    public List<Task> getTaskList(LocalDate start, LocalDate end) throws IOException{
        if(taskMap.isEmpty()) {
            loadTaskListFromFile();
        }
        return taskMap.values().stream().filter(task -> task.isInTimePeriode(start, end)).toList();
    }

    /**
     * Method get all Tasks for a specific Crop
     * @param cropId the cropId
     * @return List of Tasks for given Crop
     */
    @Override
    public List<Task> getTaskForCrop(long cropId) throws IOException {
        if(taskMap.isEmpty()) {
            loadTaskListFromFile();
        }
        return taskMap.values().stream().filter(task -> task.getCropId() == cropId).toList();
    }

    /**
     * Method remove all Tasks for a specific Crop
     * @param cropId the crop
     */
    @Override
    public void removeTasksForCrop(long cropId) throws IOException {
        if(taskMap.isEmpty()) {
            loadTaskListFromFile();
        }
        taskMap.values().removeIf(task -> task.getCropId() == cropId);
    }

    /**
     * If no data is currently loaded, data is loaded from {@link #dataSource}.
     * If the {@link Task} has an id than the task is added to the {@link #taskMap}
     * otherwise the id is generated with the {@link IdProvider} before adding
     * it to the {@link #taskMap}. In any case, the {@link #taskMap} is written
     * to the {@link #dataSource}.
     *
     * @see TaskDatabase#saveTask(Task)
     */
    @Override
    public void saveTask(Task task) throws IOException {
        if(taskMap.isEmpty()) {
            loadTaskListFromFile();
        }
        if(task.getId() == 0) {
            task.withId(idProvider.incrementAndGet());
        }
        writeTaskListToFile();
    }

    /**
     * If no data is currently loaded, data is loaded from {@link #dataSource}.
     * If the {@link Task}s id is found in the {@link #taskMap}, the Task is removed
     * from the  {@link #taskMap}. Then the Task are written to the {@link #dataSource}.
     *
     * @see TaskDatabase#removeTask(Task)
     */
    @Override
    public void removeTask(Task task) throws IOException {
        if(taskMap.isEmpty()) {
            loadTaskListFromFile();
        }
        if(taskMap.containsKey(task.getId())){
            taskMap.remove(task.getId());
            writeTaskListToFile();
        }
    }

    /**
     * Writes cached data to the {@link #dataSource}.
     *
     * @throws IOException If the database cannot be accessed
     */
    private void writeTaskListToFile() throws IOException {
        if(dataSource != null) {
            try {
                new ObjectMapper()
                        .registerModule(timeModule)
                        .registerModule(new Jdk8Module())
                        .writeValue(new File(dataSource.toURI()), taskMap.values());

            } catch (URISyntaxException e) {
                throw new IOException(INVALID_DATASOURCE_MSG, e);
            }
        }
    }

    /**
     * Loads the database from {@link #dataSource} and updates the cached data.
     *
     * @throws IOException If the database cannot be accessed
     */
    private void loadTaskListFromFile() throws IOException {
        if (dataSource != null) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.registerModule(timeModule)
                    .registerModule(new Jdk8Module());

            List<Task> result;
            result = mapper.readerForListOf(Task.class).readValue(dataSource);

            taskMap = result.stream()
                    .collect(HashMap::new,
                        (res, task) -> res.put(task.getId(), task),
                        (existing, replacement) -> {});
        }

        Long maxId = taskMap.isEmpty() ? 0L : Collections.max(taskMap.keySet());
        idProvider = new IdProvider(maxId);
    }
}
