/*
 *
 * This file is part of the Hesperides distribution.
 * (https://github.com/voyages-sncf-technologies/hesperides)
 * Copyright (c) 2016 VSCT.
 *
 * Hesperides is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, version 3.
 *
 * Hesperides is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 */
package org.hesperides.test.bdd.platforms;

import lombok.Data;
import lombok.Value;
import org.apache.commons.lang3.SerializationUtils;
import org.hesperides.core.domain.platforms.entities.Platform;
import org.hesperides.core.presentation.io.platforms.ApplicationOutput;
import org.hesperides.core.presentation.io.platforms.ModulePlatformsOutput;
import org.hesperides.test.bdd.platforms.builders.DeployedModuleBuilder;
import org.hesperides.test.bdd.platforms.builders.PlatformBuilder;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.stream.Collectors;

@Component
public class PlatformHistory {

    private List<PlatformTimestampedBuilders> platforms;

    public PlatformHistory() {
        reset();
    }

    public PlatformHistory reset() {
        platforms = new ArrayList<>();
        return this;
    }

    public void addPlatformBuilder(PlatformBuilder platformBuilder) {
        if (platforms.stream().anyMatch(platform -> !platform.isDeleted && platform.getPlatformKey().equals(platformBuilder.buildPlatformKey()))) {
            throw new RuntimeException("Platform " + platformBuilder.getApplicationName() + "-" + platformBuilder.getPlatformName() + " already exists in platform history");
        }
        PlatformBuilder newPlatformBuilder = SerializationUtils.clone(platformBuilder);
        platforms.add(new PlatformTimestampedBuilders(newPlatformBuilder));
    }

    public void removePlatformBuilder(PlatformBuilder platformBuilder) {
        if (platforms.stream().noneMatch(platform -> platform.getPlatformKey().equals(platformBuilder.buildPlatformKey()))) {
            throw new RuntimeException("Can't remove platform " + platformBuilder.getApplicationName() + "-" + platformBuilder.getPlatformName() + " because it doesn't exist in platform history");
        }
        platforms.forEach(platform -> {
            if (platform.getPlatformKey().equals(platformBuilder.buildPlatformKey())) {
                platform.setDeleted(true);
            }
        });
    }

    public void updatePlatformBuilder(PlatformBuilder platformBuilder) {
        if (platforms.stream().noneMatch(platform -> platform.getPlatformKey().equals(platformBuilder.buildPlatformKey()))) {
            throw new RuntimeException("Can't upadte platform " + platformBuilder.getApplicationName() + "-" + platformBuilder.getPlatformName() + " because it doesn't exist in platform history");
        }

        platformBuilder.incrementVersionId();
        platformBuilder.setDeployedModuleIds();
        PlatformBuilder updatedPlatformBuilder = SerializationUtils.clone(platformBuilder);

        TimestampedBuilder timestampedBuilder = new TimestampedBuilder(updatedPlatformBuilder);
        platforms.forEach(platform -> {
            if (platform.getPlatformKey().equals(platformBuilder.buildPlatformKey())) {
                platform.setDeleted(false);
                platform.getTimestampedBuilders().add(timestampedBuilder);
            }
        });
    }

    public List<ModulePlatformsOutput> buildModulePlatforms(DeployedModuleBuilder moduleToLookFor) {
        return platforms.stream()
                .filter(platform -> !platform.isDeleted)
                .map(PlatformTimestampedBuilders::getTimestampedBuilders)
                .map(PlatformHistory::getLastPlatformBuilder)
                .filter(platformBuilder -> platformBuilder.getDeployedModuleBuilders()
                        .stream()
                        .anyMatch(deployedModuleBuilder -> deployedModuleBuilder.equalsByKey(moduleToLookFor)))
                .map(platformBuilder -> new ModulePlatformsOutput(platformBuilder.getApplicationName(), platformBuilder.getPlatformName()))
                .collect(Collectors.toList());
    }

    private static PlatformBuilder getLastPlatformBuilder(List<TimestampedBuilder> timestampedBuilders) {
        return timestampedBuilders.stream()
                .max(Comparator.comparing(TimestampedBuilder::getTimestamp))
                .orElseThrow(() -> new RuntimeException("Can't get last platform builder from timestamped builders"))
                .getPlatformBuilder();
    }

    public PlatformBuilder getPlatformByName(String platformName) {
        List<PlatformBuilder> matchingPlatforms = platforms.stream()
                .filter(platform -> platform.getPlatformKey().getPlatformName().equals(platformName))
                .map(PlatformTimestampedBuilders::getTimestampedBuilders)
                .map(PlatformHistory::getLastPlatformBuilder)
                .collect(Collectors.toList());

        if (matchingPlatforms.size() != 1) {
            throw new RuntimeException("Incorrect matching platforms count: " + matchingPlatforms.size());
        }
        return matchingPlatforms.get(0);
    }

    public ApplicationOutput buildApplicationOutput(String applicationName, boolean withoutModules) {
        List<PlatformBuilder> platformBuilders = platforms.stream()
                .filter(platform -> platform.getPlatformKey().getApplicationName().equals(applicationName)
                        && !platform.isDeleted)
                .map(PlatformTimestampedBuilders::getTimestampedBuilders)
                .map(PlatformHistory::getLastPlatformBuilder)
                .collect(Collectors.toList());

        return buildApplicationOutput(
                applicationName,
                platformBuilders,
                withoutModules);
    }

    private static ApplicationOutput buildApplicationOutput(String applicationName, List<PlatformBuilder> platformBuilders, boolean withoutModules) {
        return new ApplicationOutput(applicationName,
                platformBuilders.stream()
                        .filter(platformBuilder -> platformBuilder.getApplicationName().equals(applicationName))
                        .map(platformBuilder -> platformBuilder.buildOutput(withoutModules))
                        .collect(Collectors.toList()),
                new HashMap<>());
    }

    public List<ApplicationOutput> buildApplicationOutputs() {
        Map<String, List<PlatformBuilder>> applicationPlatformsMap = platforms.stream()
                .filter(platform -> !platform.isDeleted)
                .map(PlatformTimestampedBuilders::getTimestampedBuilders)
                .map(PlatformHistory::getLastPlatformBuilder)
                .collect(Collectors.groupingBy(PlatformBuilder::getApplicationName));

        return applicationPlatformsMap.entrySet().stream()
                .map(entry -> buildApplicationOutput(entry.getKey(), entry.getValue(), false))
                .collect(Collectors.toList());
    }

    public Long getPlatformFirstTimestamp(String applicationName, String platformName) {
        return getPlatformTimestampedBuilderAtIndex(applicationName, platformName, 0).getTimestamp();
    }

    public PlatformBuilder getFirstPlatformBuilder(String applicationName, String platformName) {
        return getPlatformTimestampedBuilderAtIndex(applicationName, platformName, 0).getPlatformBuilder();
    }

    public PlatformBuilder getSecondPlatformBuilder(String applicationName, String platformName) {
        return getPlatformTimestampedBuilderAtIndex(applicationName, platformName, 1).getPlatformBuilder();
    }

    private TimestampedBuilder getPlatformTimestampedBuilderAtIndex(String applicationName, String platformName, int index) {
        return platforms.stream()
                .filter(platform -> platform.getPlatformKey().getApplicationName().equals(applicationName) &&
                        platform.getPlatformKey().getPlatformName().equals(platformName))
                .findFirst()
                .map(PlatformTimestampedBuilders::getTimestampedBuilders)
                .map(timestampedBuilders -> timestampedBuilders.get(index))
                .orElseThrow(() -> new RuntimeException("Can't find platform " + applicationName + "-" + platformName));
    }

    public Long getPenultimatePlatformTimestamp(String applicationName, String platformName) {
        return getPenultimatePlatformTimestampBuilder(applicationName, platformName).getTimestamp();
    }

    // penultimate = second to last = avant dernier
    private TimestampedBuilder getPenultimatePlatformTimestampBuilder(String applicationName, String platformName) {
        return platforms.stream()
                .filter(platform -> platform.getPlatformKey().getApplicationName().equals(applicationName) &&
                        platform.getPlatformKey().getPlatformName().equals(platformName))
                .findFirst()
                .map(PlatformTimestampedBuilders::getTimestampedBuilders)
                .map(timestampedBuilders -> timestampedBuilders.get(timestampedBuilders.size() - 2))
                .orElseThrow(() -> new RuntimeException("Can't find platform " + applicationName + "-" + platformName));
    }

    @Data
    private static class PlatformTimestampedBuilders {
        private Platform.Key platformKey;
        private List<TimestampedBuilder> timestampedBuilders;
        private boolean isDeleted;

        private PlatformTimestampedBuilders(PlatformBuilder platformBuilder) {
            platformKey = platformBuilder.buildPlatformKey();
            timestampedBuilders = new ArrayList<>();
            timestampedBuilders.add(new TimestampedBuilder(platformBuilder));
            isDeleted = false;
        }
    }

    @Value
    public static class TimestampedBuilder {
        Long timestamp;
        PlatformBuilder platformBuilder;

        private TimestampedBuilder(PlatformBuilder platformBuilder) {
            // Les tests faisant référence au timestamp d'un événement passé plantent
            // de manière aléatoire en local. Cela vient du fait que le timestamp
            // sauvegardé dans `PlatformHistory` est parfois défini juste avant la fin
            // de la sauvegarde effective de l'événement. En attendant de trouver mieux...
            try {
                Thread.sleep(10);
            } catch (InterruptedException ignored) {
            }
            timestamp = System.currentTimeMillis();
            this.platformBuilder = platformBuilder;
        }
    }
}