package io.delphiplatform.api.v3.bigtable;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.transaction.NotSupportedException;

import io.delphiplatform.api.util.CollectionUtils;
import io.delphiplatform.api.util.DateUtils;
import io.delphiplatform.api.util.model.ModelUtils;
import io.delphiplatform.api.v3.bigtable.processing.PaginationService;
import io.delphiplatform.api.v3.bigtable.processing.PlaylistTrackPositionAdditionAggregator;
import io.delphiplatform.api.v3.bigtable.processing.PlaylistTrackPositionAdditionSortingService;
import io.delphiplatform.api.v3.bigtable.processing.TrackPositionAggregator;
import io.delphiplatform.api.v3.bigtable.processing.TrackPositionConverter;
import io.delphiplatform.api.v3.bigtable.processing.TrackPositionSortingService;
import io.delphiplatform.api.v3.constant.DspConstants;
import io.delphiplatform.api.v3.model.trackposition.PlaylistTrackPosition;
import io.delphiplatform.api.v3.model.trackposition.PlaylistTrackPositionAddition;
import io.delphiplatform.api.v3.rdb.service.PlaylistManager;
import io.delphiplatform.api.v3.view.util.Params;

@Deprecated
@Service
public class BigtableTrackPositionService {

    private static final int DAYS_SHIFT_TO_SCAN_FOR_LATEST_DATA = 7;

    private final BigtableTrackPositionQueryExecutor trackPositionQueryExecutor;
    private final PaginationService paginationService;

    private final TrackPositionConverter trackPositionConverter;
    private final TrackPositionAggregator trackPositionAggregator;
    private final TrackPositionSortingService trackPositionSortingService;
    private final PlaylistManager playlistManager;
    private final PlaylistTrackPositionAdditionAggregator trackPositionAdditionAggregator;
    private final PlaylistTrackPositionAdditionSortingService trackPositionAdditionSortingService;
    private final BigtableTrackPositionDataEnrichmentService dataEnrichmentService;

    public BigtableTrackPositionService(
        BigtableTrackPositionQueryExecutor trackPositionQueryExecutor,
        PaginationService paginationService,
        TrackPositionConverter trackPositionConverter,
        TrackPositionAggregator trackPositionAggregator,
        TrackPositionSortingService trackPositionSortingService,
        PlaylistManager playlistManager,
        PlaylistTrackPositionAdditionAggregator trackPositionAdditionAggregator,
        PlaylistTrackPositionAdditionSortingService trackPositionAdditionSortingService,
        BigtableTrackPositionDataEnrichmentService dataEnrichmentService
    ) {
        this.trackPositionQueryExecutor = trackPositionQueryExecutor;
        this.paginationService = paginationService;
        this.trackPositionConverter = trackPositionConverter;
        this.trackPositionAggregator = trackPositionAggregator;
        this.trackPositionSortingService = trackPositionSortingService;
        this.playlistManager = playlistManager;
        this.trackPositionAdditionAggregator = trackPositionAdditionAggregator;
        this.trackPositionAdditionSortingService = trackPositionAdditionSortingService;
        this.dataEnrichmentService = dataEnrichmentService;
    }

    @Transactional(readOnly = true)
    public CompletableFuture<List<PlaylistTrackPosition>> getPlaylistTrackPositionModels(Params paramsWithOptionalDates)
        throws NotSupportedException {
        if (CollectionUtils.isNotEmpty(paramsWithOptionalDates.getArtistId())
            && CollectionUtils.isEmpty(paramsWithOptionalDates.getIsrc())) {
            return CompletableFuture.completedFuture(Collections.emptyList());
        }

        /* If start/end date param is empty - set default */
        boolean emptyStartOrEndDate =
            paramsWithOptionalDates.getStartDate() == null || paramsWithOptionalDates.getEndDate() == null;

        Params paramsWithDatesSet;
        if (emptyStartOrEndDate) {
            paramsWithDatesSet = paramsWithOptionalDates.toBuilder()
                .startDate(DateUtils.getCurrentDate().minusDays(DAYS_SHIFT_TO_SCAN_FOR_LATEST_DATA))
                .endDate(DateUtils.getCurrentDate())
                .build();
        } else {
            paramsWithDatesSet = paramsWithOptionalDates;
        }

        /* Process playlistIds passed as param, so that if list contains Amazon full playlistIds (like 'amazon_123_us')
         * - they are cut to base part (like 'amazon_123'). This is needed to query BT.playlists which uses cut playlist id in rowkey
         * (query result PlaylistTrackPosition objects will again have full playlistId set to them as one is expanded into multiple per-country items)
         */
        List<String> requestedPlaylistIds = paramsWithDatesSet.getPlaylistId();
        Optional<List<String>> processedPlaylistIds = ModelUtils.cutFullAmazonPlaylistIdsToBasePart(requestedPlaylistIds);

        //params with Amazon playlistIds cut from 'amazon_123_us' to 'amazon_123'
        Params paramsProcessed = paramsWithDatesSet.toBuilder()
            .playlistId(processedPlaylistIds.orElse(null))
            .build();

        CompletableFuture<Stream<PlaylistTrackPosition>> playlistTrackPositionsFuture =
            trackPositionQueryExecutor.getAllPlaylistPositions(paramsProcessed)
                .thenApply(trackPositionConverter::convertPlaylistPositions)
                .thenApply(trackPositionAggregator::groupBy);

        return playlistTrackPositionsFuture
            .thenApply(models -> filterOnlyLatestDate(models, emptyStartOrEndDate))
            .thenApply(models -> filterAmazonPlaylistPositionsByRequestedFullPlaylistIds(models, requestedPlaylistIds))
            .thenApply(models -> dataEnrichmentService.populatePlaylistStreamsForXDaysPeriods(models, paramsProcessed))
            .thenApply(dataEnrichmentService::populateNumWeeksOnPlaylist)
            .thenApply(models -> trackPositionSortingService
                .sortPlaylistPositions(models, paramsProcessed.getSortBy(), paramsProcessed.getSortOrder()))
            .thenApply(models -> dataEnrichmentService.populatePlaylists(models, paramsProcessed))
            .thenApply(models -> playlistManager.filterPlaylistsByTypes(models, paramsProcessed.getPlaylistTypes()))
            .thenApply(models -> playlistManager.filterPlaylistPositionsByCountryCodes(models, paramsProcessed.getCountryCode()))
            .thenApply(models -> dataEnrichmentService.populateAmazonPlaylistsWithDspPlaylistId(models, paramsProcessed))
            .thenApply(models -> paginationService.getPage(models, paramsProcessed))
            .thenApply(models -> dataEnrichmentService.populatePlaylistPositionTracks(models, paramsProcessed))
            .thenApply(models -> dataEnrichmentService.populatePlaylistPositionAmazonTracks(models, paramsProcessed))
            .thenApply(models -> models.collect(Collectors.toList()));
    }

    @Transactional(readOnly = true)
    public CompletableFuture<List<PlaylistTrackPositionAddition>> getPlaylistTrackPositionAdditionModels(Params params)
        throws NotSupportedException {
        // use day before to determine possible position addition on start date
        Params positionParams = params.toBuilder().startDate(params.getStartDate().minusDays(1)).build();

        CompletableFuture<Stream<PlaylistTrackPosition>> filteredTrackPositions = trackPositionQueryExecutor.getAllPlaylistPositions(
                positionParams)
            .thenApply(trackPositionConverter::convertPlaylistPositions)
            .thenApply(trackPositionAggregator::groupBy)
            .thenApply(models -> trackPositionAdditionAggregator.filterOnlyAdditions(models, positionParams));

        return filteredTrackPositions
            .thenApply(models -> playlistManager.filterPlaylistsByTypes(models, params.getPlaylistTypes()))
            .thenApply(trackPositionAdditionAggregator::groupBy)
            .thenApply(models -> dataEnrichmentService.populatePlaylists(models, params))
            .thenApply(models -> dataEnrichmentService.populateAmazonPlaylistsWithDspPlaylistId(models, params))
            .thenApply(models -> trackPositionAdditionSortingService
                .sortPlaylistPositions(models, params.getSortBy(), params.getSortOrder()))
            .thenApply(models -> paginationService.getPage(models, params))
            .thenApply(models -> models.collect(Collectors.toList()));
    }

    private Stream<PlaylistTrackPosition> filterOnlyLatestDate(Stream<PlaylistTrackPosition> models,
        boolean emptyStartOrEndDate) {
        if (!emptyStartOrEndDate) {
            return models;
        }
        List<PlaylistTrackPosition> modelsList = models.collect(Collectors.toList());

        if (CollectionUtils.isEmpty(modelsList)) {
            return Stream.empty();
        }

        Map<String, LocalDate> dspToMaxDate = modelsList.stream()
            .collect(Collectors.groupingBy(PlaylistTrackPosition::getDsp))
            .entrySet().stream()
            .collect(Collectors.toMap(Entry::getKey,
                e -> e.getValue().stream()
                    .map(PlaylistTrackPosition::getDate)
                    .max(Comparator.naturalOrder())
                    .get()));

        return modelsList.stream()
            .filter(m -> Objects.equals(m.getDate(), dspToMaxDate.get(m.getDsp())));
    }

    /**
     * If any Amazon full playlistIds (like 'amazon_123_us') were passed in params - method ensures that we return only positions with exactly
     * that playlistIds (and not with same base playlist id part but different country)
     * <p>
     * If any Amazon base playlistIds (like 'amazon_123') were passed in params - all playlists found by that base playlist id will be returned
     */
    private Stream<PlaylistTrackPosition> filterAmazonPlaylistPositionsByRequestedFullPlaylistIds(
        Stream<PlaylistTrackPosition> models,
        List<String> requestedPlaylistIds
    ) {
        if (CollectionUtils.isEmpty(requestedPlaylistIds)) {
            return models;
        } else {
            return models.filter(position -> {
                if (DspConstants.AMAZON.equals(position.getDsp())
                    && ModelUtils.isAmazonFullPlaylistId(position.getPlaylistId())) {

                    //return true if:
                    // - full playlistId from this position is among requested playlistIds
                    // - base playlistId from this position (extracted from full playlistId) is among requested playlistIds
                    return requestedPlaylistIds.contains(position.getPlaylistId())
                        || requestedPlaylistIds.contains(position.getBasePlaylistIdFromAmazonFullPlaylistId());
                }

                return true;
            });
        }
    }
}
