package eu.eudat.logic.managers;

import eu.eudat.data.dao.criteria.DataManagementPlanCriteria;
import eu.eudat.data.dao.criteria.DatasetCriteria;
import eu.eudat.data.dao.criteria.GrantCriteria;
import eu.eudat.data.dao.criteria.OrganisationCriteria;
import eu.eudat.data.dao.entities.DMPDao;
import eu.eudat.data.dao.entities.DatasetDao;
import eu.eudat.data.dao.entities.GrantDao;
import eu.eudat.data.dao.entities.OrganisationDao;
import eu.eudat.data.entities.DMP;
import eu.eudat.data.entities.Dataset;
import eu.eudat.data.entities.Grant;
import eu.eudat.data.entities.UserInfo;
import eu.eudat.data.query.items.table.dataset.DatasetTableRequest;
import eu.eudat.data.query.items.table.dmp.DataManagementPlanTableRequest;
import eu.eudat.logic.builders.model.models.RecentActivityDataBuilder;
import eu.eudat.logic.services.ApiContext;
import eu.eudat.logic.services.operations.DatabaseRepository;
import eu.eudat.models.data.dashboard.recent.RecentActivity;
import eu.eudat.models.data.dashboard.recent.RecentActivityData;
import eu.eudat.models.data.dashboard.recent.model.RecentActivityModel;
import eu.eudat.models.data.dashboard.recent.model.RecentDatasetModel;
import eu.eudat.models.data.dashboard.recent.model.RecentDmpModel;
import eu.eudat.models.data.dashboard.recent.tablerequest.RecentActivityTableRequest;
import eu.eudat.models.data.dashboard.searchbar.SearchBarItem;
import eu.eudat.models.data.dashboard.statistics.DashBoardStatistics;
import eu.eudat.models.data.helpers.common.DataTableData;
import eu.eudat.models.data.listingmodels.DataManagementPlanListingModel;
import eu.eudat.models.data.listingmodels.DatasetListingModel;
import eu.eudat.models.data.security.Principal;
import eu.eudat.types.searchbar.SearchBarItemType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.transaction.Transactional;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Component
public class DashBoardManager {
    private static final Logger logger = LoggerFactory.getLogger(DashBoardManager.class);

    private final Map<String, Comparator<RecentActivityModel>> comparators = Stream.of(new Object[][] {
            { "modified", Comparator.comparing(o -> ((RecentActivityModel)o).getModified()).reversed()},
            { "created", Comparator.comparing(o -> ((RecentActivityModel)o).getCreated()).reversed()},
            { "label", Comparator.comparing(o -> ((RecentActivityModel)o).getTitle())},
            { "status", Comparator.comparing(o -> ((RecentActivityModel)o).getStatus()).reversed()},
            { "finalizedAt", Comparator.comparing(o -> ((RecentActivityModel)o).getFinalizedAt(), Comparator.nullsLast(Comparator.naturalOrder())).reversed()},
            { "publishedAt", Comparator.comparing(o -> ((RecentActivityModel)o).getPublishedAt(), Comparator.nullsLast(Comparator.naturalOrder())).reversed()}
    }).collect(Collectors.toMap(data -> (String) data[0], data -> (Comparator<RecentActivityModel>)data[1]));

    private ApiContext apiContext;
    private DatabaseRepository databaseRepository;
    private final DataManagementPlanManager dataManagementPlanManager;
    private final DatasetManager datasetManager;

    @Autowired
    public DashBoardManager(ApiContext apiContext, DataManagementPlanManager dataManagementPlanManager, DatasetManager datasetManager) {
        this.apiContext = apiContext;
        this.databaseRepository = apiContext.getOperationsContext().getDatabaseRepository();
        this.dataManagementPlanManager = dataManagementPlanManager;
        this.datasetManager = datasetManager;
    }

    public DashBoardStatistics getStatistics() {
        DashBoardStatistics statistics = new DashBoardStatistics();

        DataManagementPlanCriteria publicCriteria = new DataManagementPlanCriteria();
        publicCriteria.setIsPublic(true);
        publicCriteria.setOnlyPublic(true);
        publicCriteria.setAllVersions(false);

        List<DMP> dmps = apiContext.getOperationsContext().getDatabaseRepository().getDmpDao().getWithCriteria(publicCriteria).toList();

//        DataManagementPlanCriteria dataManagementPlanCriteria = new DataManagementPlanCriteria();
        OrganisationCriteria organisationCriteria = new OrganisationCriteria();
        /*dataManagementPlanCriteria.setAllVersions(false);
        dataManagementPlanCriteria.setIsPublic(true);*/
        organisationCriteria.setPublic(true);

//        List<DMP> dmps = databaseRepository.getDmpDao().getWithCriteria(dataManagementPlanCriteria).toList();
        long numberOfDatasets = 0;
        LinkedList<Grant> grants = new LinkedList<>();
        for (DMP dmp : dmps) {
            numberOfDatasets = numberOfDatasets + dmp.getDataset().stream()
                    .filter(item -> item.getStatus() == Dataset.Status.FINALISED.getValue()).count();
            grants.add(dmp.getGrant());
        }

        statistics.setTotalDataManagementPlanCount((long) dmps.size());
        statistics.setTotalDataSetCount(numberOfDatasets);
        statistics.setTotalGrantCount(grants.stream().distinct().count());
        statistics.setTotalOrganisationCount(databaseRepository.getOrganisationDao().getWithCriteria(organisationCriteria).count());

        return statistics;
    }

    public DashBoardStatistics getMeStatistics(Principal principal) throws IOException {
        Long datasets = 0L;
        Long dmps = 0L;
        DashBoardStatistics statistics = new DashBoardStatistics();
        DMPDao dataManagementPlanRepository = databaseRepository.getDmpDao();
        DatasetDao datasetRepository = databaseRepository.getDatasetDao();
        GrantDao grantRepository = databaseRepository.getGrantDao();
        OrganisationDao organisationRepository = databaseRepository.getOrganisationDao();
        UserInfo user = new UserInfo();
        user.setId(principal.getId());
        DatasetCriteria datasetCriteria = new DatasetCriteria();
        if (apiContext.getOperationsContext().getElasticRepository().getDatasetRepository() != null) {
            try {
                eu.eudat.elastic.criteria.DatasetCriteria datasetElasticCriteria = new eu.eudat.elastic.criteria.DatasetCriteria();
                datasetElasticCriteria.setAllowAllVersions(false);
                datasetElasticCriteria.setPublic(false);
                datasetElasticCriteria.setCollaborators(Collections.singletonList(principal.getId()));
                datasets = apiContext.getOperationsContext().getElasticRepository().getDatasetRepository().count(datasetElasticCriteria);
            }catch (Exception e) {
                logger.warn(e.getMessage(), e);
                datasets = null;
            }
        }
        datasetCriteria.setAllVersions(false);
        datasetCriteria.setIsPublic(false);
        DataManagementPlanCriteria dataManagementPlanCriteria = new DataManagementPlanCriteria();
        if (apiContext.getOperationsContext().getElasticRepository().getDmpRepository() != null) {
            try {
                eu.eudat.elastic.criteria.DmpCriteria dmpElasticCriteria = new eu.eudat.elastic.criteria.DmpCriteria();
                dmpElasticCriteria.setAllowAllVersions(false);
                dmpElasticCriteria.setPublic(false);
                dmpElasticCriteria.setCollaborators(Collections.singletonList(principal.getId()));
                dmps = apiContext.getOperationsContext().getElasticRepository().getDmpRepository().count(dmpElasticCriteria);
            }catch (Exception e) {
                logger.warn(e.getMessage(), e);
                dmps = null;
            }
        }
        dataManagementPlanCriteria.setAllVersions(false);
        dataManagementPlanCriteria.setOnlyPublic(false);
        dataManagementPlanCriteria.setIsPublic(false);

        GrantCriteria grantCriteria = new GrantCriteria();
        grantCriteria.setActive(true);

        OrganisationCriteria organisationCriteria = new OrganisationCriteria();
        organisationCriteria.setActive(true);

        List<Integer> roles = new LinkedList<>();
        if ((dmps == null || dmps == 0L) && (datasets == null || datasets == 0L)) {
            CompletableFuture dmpFuture = dataManagementPlanRepository.getAuthenticated(dataManagementPlanRepository.getWithCriteria(dataManagementPlanCriteria), principal.getId(), roles).distinct().countAsync()
                    .whenComplete((dmpsStats, throwable) -> statistics.setTotalDataManagementPlanCount(dmpsStats));
            CompletableFuture datasetFuture = datasetRepository.getAuthenticated( datasetRepository.getWithCriteria(datasetCriteria), user, roles).distinct().countAsync()
                    .whenComplete((datasetsStats, throwable) -> statistics.setTotalDataSetCount(datasetsStats));
            CompletableFuture.allOf(dmpFuture, datasetFuture).join();
        } else {
            statistics.setTotalDataManagementPlanCount(dmps);
            statistics.setTotalDataSetCount(datasets);
        }
        CompletableFuture grantFuture = grantRepository.getAuthenticated(grantRepository.getWithCriteria(grantCriteria), user).countAsync()
                .whenComplete((grantsStats, throwable) -> statistics.setTotalGrantCount(grantsStats));
        CompletableFuture orgnanisationFuture = organisationRepository.getAuthenticated(organisationRepository.getWithCriteria(organisationCriteria).withHint("organisationRecentActivity"), user).countAsync()
                .whenComplete((organisationStats, throwable) -> statistics.setTotalOrganisationCount(organisationStats));

        CompletableFuture.allOf( grantFuture, orgnanisationFuture).join();
        return statistics;
    }

    @Deprecated
    public RecentActivity getRecentActivity(Principal principal, Integer numberofactivities) {
        RecentActivity activity = new RecentActivity();
        DMPDao dataManagementPlanRepository = databaseRepository.getDmpDao();
        DatasetDao datasetRepository = databaseRepository.getDatasetDao();
        GrantDao grantRepository = databaseRepository.getGrantDao();
        UserInfo user = new UserInfo();
        user.setId(principal.getId());
        DatasetCriteria datasetCriteria = new DatasetCriteria();
        datasetCriteria.setAllVersions(false);
        DataManagementPlanCriteria dataManagementPlanCriteria = new DataManagementPlanCriteria();
        dataManagementPlanCriteria.setAllVersions(false);
        GrantCriteria grantCriteria = new GrantCriteria();
        RecentActivityDataBuilder recentActivityDataBuilder = apiContext.getOperationsContext().getBuilderFactory().getBuilder(RecentActivityDataBuilder.class);

        List<Integer> roles = new LinkedList<>();
        CompletableFuture<List<RecentActivityData>> dmps = dataManagementPlanRepository.getAuthenticated(dataManagementPlanRepository.getWithCriteria(dataManagementPlanCriteria), principal.getId(), roles)
                .withHint("dmpRecentActivity")
                .orderBy((builder, root) -> builder.desc(root.get("modified")))
                .take(numberofactivities)
                .selectAsync(item -> recentActivityDataBuilder.label(item.getLabel()).timestamp(item.getModified()).id(item.getId().toString()).build())
                .whenComplete((dmpActivities, throwable) -> activity.setRecentDmpActivities(dmpActivities));

        CompletableFuture<List<RecentActivityData>> datasets = datasetRepository.getAuthenticated(datasetRepository.getWithCriteria(datasetCriteria), user, roles).distinct()
                .withHint("datasetRecentActivity")
                .orderBy((builder, root) -> builder.desc(root.get("modified")))
                .take(numberofactivities)
                .selectAsync(item -> recentActivityDataBuilder.label(item.getLabel()).timestamp(item.getModified()).id(item.getId().toString()).build())
                .whenComplete((datasetActivities, throwable) -> activity.setRecentDatasetActivities(datasetActivities));

        CompletableFuture<List<RecentActivityData>> grants = grantRepository.getAuthenticated(grantRepository.getWithCriteria(grantCriteria), user)
                .withHint("grantRecentActivity")
                .orderBy((builder, root) -> builder.desc(root.get("modified")))
                .take(numberofactivities)
                .selectAsync(item -> recentActivityDataBuilder.label(item.getLabel()).timestamp(item.getModified()).id(item.getId().toString()).build())
                .whenComplete((grantActivities, throwable) -> activity.setRecentGrantActivities(grantActivities));

        CompletableFuture.allOf(grants, dmps, datasets).join();
        return activity;
    }

    @Transactional
    public List<RecentActivityModel> getNewRecentActivity(RecentActivityTableRequest tableRequest, Principal principal) throws Exception {
        boolean isAuthenticated = principal.getId() != null;
        List<RecentActivityModel> recentActivityModels = new ArrayList<>();
        UserInfo user = new UserInfo();
        if (isAuthenticated) {
            user.setId(principal.getId());
        }
        DatasetCriteria datasetCriteria = new DatasetCriteria();
        datasetCriteria.setLike(tableRequest.getCriteria().getLike());
        datasetCriteria.setAllVersions(false);
        datasetCriteria.setIsPublic(!isAuthenticated);
        DataManagementPlanCriteria dataManagementPlanCriteria = new DataManagementPlanCriteria();
        dataManagementPlanCriteria.setAllVersions(false);
        dataManagementPlanCriteria.setLike(tableRequest.getCriteria().getLike());
        dataManagementPlanCriteria.setIsPublic(!isAuthenticated);
        dataManagementPlanCriteria.setOnlyPublic(!isAuthenticated);

        //GK: Use the managers to get the data in order to be better synced with other lists
        DataManagementPlanTableRequest dataManagementPlanTableRequest = new DataManagementPlanTableRequest();
        dataManagementPlanTableRequest.setCriteria(dataManagementPlanCriteria);
        dataManagementPlanTableRequest.setOrderings(tableRequest.getOrderings());
        dataManagementPlanTableRequest.setLength(tableRequest.getLength());
        dataManagementPlanTableRequest.setOffset(tableRequest.getDmpOffset());
        DataTableData<DataManagementPlanListingModel> dmps = this.dataManagementPlanManager.getPaged(dataManagementPlanTableRequest, principal, "listing");
        recentActivityModels.addAll(dmps.getData().stream().map(dataManagementPlanListingModel -> new RecentDmpModel().fromDataModel(dataManagementPlanListingModel.toDataModel())).collect(Collectors.toList()));
        DatasetTableRequest datasetTableRequest = new DatasetTableRequest();
        datasetCriteria.setCollaborators(new ArrayList<>());
        datasetTableRequest.setCriteria(datasetCriteria);
        datasetTableRequest.setOrderings(tableRequest.getOrderings());
        datasetTableRequest.getOrderings().getFields().addAll(datasetTableRequest.getOrderings().getFields().stream().filter(s -> s.contains("publishedAt")).map(s -> s.charAt(0) + "dmp:" + s.substring(1) + "|join|").collect(Collectors.toList()));
        datasetTableRequest.getOrderings().getFields().removeIf(s -> s.contains("publishedAt") && !s.endsWith("|join|"));
        datasetTableRequest.setLength(tableRequest.getLength());
        datasetTableRequest.setOffset(tableRequest.getDatasetOffset());
        DataTableData<DatasetListingModel> datasets = this.datasetManager.getPaged(datasetTableRequest, principal);
        recentActivityModels.addAll(datasets.getData().stream().map(datasetListingModel -> new RecentDatasetModel().fromDataModel(datasetListingModel.toDataModel())).collect(Collectors.toList()));

        //GK: Shuffle the deck otherwise we will summon the DMPodia when sorting with status
        /*int pos = -1;
        for (int i = (recentActivityModels.size() / 2); i < recentActivityModels.size(); i++) {
            RecentActivityModel recentActivityModel = recentActivityModels.remove(i);
            while (pos < recentActivityModels.size()) {
                pos++;
                if (pos % 2 != 0) {
                    break;
                }
            }
            recentActivityModels.add(pos, recentActivityModel);
        }*/
        //GK: No one likes to play shuffle with the recent activities. So just re-sort them based on how they have been sorted already

        String order = tableRequest.getOrderings().getFields().get(0).toCharArray()[0] + "";
        String field = tableRequest.getOrderings().getFields().get(0).substring(1);
        if (field.contains(":") && field.contains("|")) {
            field = field.substring(field.lastIndexOf(":") + 1, field.indexOf("|"));
        }
        field = field.equals("label") ? "title" : field;
        field = field.substring(0, 1).toUpperCase() + field.substring(1);
        String finalField = field;
        recentActivityModels = recentActivityModels.stream().sorted((o1, o2) -> {
            try {
                return (order.equals("+") ? 1 : -1 ) * ((Comparable)o1.getClass().getMethod("get" + finalField).invoke(o1)).compareTo(o2.getClass().getMethod("get" + finalField).invoke(o2));
                } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
                    logger.error(e.getLocalizedMessage(), e);
                }
            return 0;
        }).collect(Collectors.toList());

        return recentActivityModels;
    }

    public List<SearchBarItem> searchUserData(String like, Principal principal) {
        UserInfo user = new UserInfo();
        user.setId(principal.getId());
        DMPDao dataManagementPlanRepository = databaseRepository.getDmpDao();
        DatasetDao datasetRepository = databaseRepository.getDatasetDao();
        GrantDao grantRepository = databaseRepository.getGrantDao();

        List<Integer> roles = new LinkedList<>();
        List<SearchBarItem> searchBarItems = new LinkedList<>();

        CompletableFuture<List<SearchBarItem>> publicDmps = dataManagementPlanRepository.asQueryable()
                .where((builder, root) -> builder.like(builder.upper(root.get("label")), "%" + like.toUpperCase() + "%"))
                .where((builder, root) -> builder.notEqual(root.get("status"), DMP.DMPStatus.DELETED.getValue()))
                .where((builder, root) -> builder.equal(root.get("isPublic"), true))
                .orderBy((builder, root) -> builder.desc(root.get("modified")))
                .selectAsync(item -> new SearchBarItem(item.getId().toString(), item.getLabel(), SearchBarItemType.DMP.getValue(), true))
                .whenComplete((dmpItems, throwable) -> searchBarItems.addAll(dmpItems));

        CompletableFuture<List<SearchBarItem>> publicDatasets = datasetRepository.asQueryable()
                .where((builder, root) -> builder.like(builder.upper(root.get("label")), "%" + like.toUpperCase() + "%"))
                .where((builder, root) -> builder.equal(root.get("status"), Dataset.Status.FINALISED.getValue()))
                .where((builder, root) -> builder.equal(root.get("dmp").get("isPublic"), true))
                .orderBy((builder, root) -> builder.desc(root.get("modified")))
                .selectAsync(item -> new SearchBarItem(item.getId().toString(), item.getLabel(), SearchBarItemType.DATASET.getValue(), true))
                .whenComplete((dataSetItems, throwable) -> searchBarItems.addAll(dataSetItems));

        if (principal.getId() != null) {
            CompletableFuture<List<SearchBarItem>> dmps = dataManagementPlanRepository.getAuthenticated(dataManagementPlanRepository.asQueryable(), principal.getId(), roles)
                    .withHint("dmpRecentActivity")
                    .where((builder, root) -> builder.like(builder.upper(root.get("label")), "%" + like.toUpperCase() + "%"))
                    .where((builder, root) -> builder.notEqual(root.get("status"), DMP.DMPStatus.DELETED.getValue()))
                    .orderBy((builder, root) -> builder.desc(root.get("modified")))
                    .selectAsync(item -> new SearchBarItem(item.getId().toString(), item.getLabel(), SearchBarItemType.DMP.getValue(), false))
                    .whenComplete((dmpItems, throwable) -> searchBarItems.addAll(dmpItems));

            CompletableFuture<List<SearchBarItem>> datasets = datasetRepository.getAuthenticated(datasetRepository.asQueryable(), user, roles)
                    .withHint("datasetRecentActivity")
                    .where((builder, root) -> builder.like(builder.upper(root.get("label")), "%" + like.toUpperCase() + "%"))
                    .where((builder, root) -> builder.notEqual(root.get("status"), Dataset.Status.DELETED.getValue()))
                    .where((builder, root) -> builder.notEqual(root.get("status"), Dataset.Status.CANCELED.getValue()))
                    .orderBy((builder, root) -> builder.desc(root.get("modified")))
                    .selectAsync(item -> new SearchBarItem(item.getId().toString(), item.getLabel(), SearchBarItemType.DATASET.getValue(), false))
                    .whenComplete((dataSetItems, throwable) -> searchBarItems.addAll(dataSetItems));

            CompletableFuture<List<SearchBarItem>> grants = grantRepository.getAuthenticated(grantRepository.asQueryable(), user)
                    .withHint("grantRecentActivity")
                    .where((builder, root) -> builder.like(builder.upper(root.get("label")), "%" + like.toUpperCase() + "%"))
                    .orderBy((builder, root) -> builder.desc(root.get("modified")))
                    .selectAsync(item -> new SearchBarItem(item.getId().toString(), item.getLabel(), SearchBarItemType.GRANT.getValue(), false))
                    .whenComplete((grantItems, throwable) -> searchBarItems.addAll(grantItems));

            CompletableFuture.allOf(grants, dmps, datasets, publicDmps, publicDatasets).join();
        } else {
            CompletableFuture.allOf(publicDmps, publicDatasets).join();
        }

        return searchBarItems;
    }
}
