package io.delphiplatform.api.v3.rdb.service.spotify;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import io.delphiplatform.api.util.JpaUtils;
import io.delphiplatform.api.v3.model.spotify.ChartTrackMetricsDimension;
import io.delphiplatform.api.v3.model.spotify.SpotifyChartBreakdown;
import io.delphiplatform.api.v3.model.spotify.SpotifyChartType;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyChartDateKey;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyChartDateProjection;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyChartDateTrackKey;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyChartTrackEntity;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyTrackId;
import io.delphiplatform.api.v3.rdb.repository.spotify.SpotifyChartTrackRepository;
import io.delphiplatform.api.v3.rdb.service.EntityService;
import io.delphiplatform.api.v3.rdb.service.dto.JpaPropertyPath;
import io.delphiplatform.api.v3.rdb.service.specification.SpecificationProvider;

import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.CHART_DATE;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.CHART_DATE_TRACK_KEY;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.CHART_RANK;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.CURRENT_POSITION;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.ID_PROPERTY;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.ISRC;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.MARKET_RANK;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.SPOTIFY_CHART;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.SPOTIFY_CHART_BREAKDOWN;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.SPOTIFY_CHART_ID;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.SPOTIFY_CHART_TYPE;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.SPOTIFY_COUNTRY_ID;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.SPOTIFY_TRACK_ID;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.TRACK;

@Service
@Transactional(readOnly = true)
public class SpotifyChartTrackService extends EntityService<SpotifyChartTrackEntity> {

    private static final ImmutableMap<String, String> SORT_BY_TO_ENTITY_FIELD_MAP =
        ImmutableMap.<String, String>builder().build();

    private final SpotifyChartTrackRepository repository;
    private final SpecificationProvider<SpotifyChartTrackEntity> specificationProvider;

    @Autowired
    public SpotifyChartTrackService(SpotifyChartTrackRepository repository,
        SpecificationProvider<SpotifyChartTrackEntity> specificationProvider) {
        super(repository, SORT_BY_TO_ENTITY_FIELD_MAP);
        this.repository = repository;
        this.specificationProvider = specificationProvider;
    }

    public LocalDate getEarliestDateBefore(LocalDate date, String chartId) {
        return repository.findMaxDateBeforeForChart(date, chartId);
    }

    public LocalDate getEarliestDateBefore(LocalDate date, List<String> isrc, List<String> trackId) {
        return repository.findMaxDateBeforeForTracks(date, isrc, trackId);
    }

    public Set<SpotifyChartDateTrackKey> findTrackKeysWithSameIsrcWithinChartForDate(Set<SpotifyChartDateTrackKey> chartDateTrackKeys,
        LocalDate chartDate) {
        return repository.findTrackKeysWithSameIsrcWithinChartForDate(chartDateTrackKeys, chartDate);
    }

    public Set<SpotifyChartTrackEntity> findTracksWithSameIsrcWithinChartForDate(Set<SpotifyChartDateTrackKey> chartDateTrackKeys,
        LocalDate chartDate) {
        return deduplicateChartTrackPositions(
            repository.findTracksWithSameIsrcWithinChartForDate(chartDateTrackKeys, chartDate)
        );
    }

    public Set<SpotifyChartTrackEntity> findTracksWithSameIsrcWithinChartForDates(
        Collection<SpotifyChartDateTrackKey> dailyChartTrackKeys,
        Collection<SpotifyChartDateTrackKey> weeklyChartTrackKeys,
        LocalDate dailyChartDate,
        LocalDate weeklyChartDate
    ) {
        return deduplicateChartTrackPositions(
            repository.findTracksWithSameIsrcWithinChartForDates(
                dailyChartTrackKeys, weeklyChartTrackKeys, dailyChartDate, weeklyChartDate)
        );
    }

    public Map<SpotifyTrackId, SpotifyChartTrackEntity> findTrackMetricsDistinctTrack(
        Collection<String> spotifyTrackIds, Collection<String> isrcs,
        String chartId, LocalDate date, Integer minPosition, Integer maxPosition,
        ChartTrackMetricsDimension metricsDimension
    ) {
        return findTrackMetrics(spotifyTrackIds, isrcs, chartId, date, minPosition, maxPosition, metricsDimension)
            .entrySet()
            .stream()
            .collect(Collectors.toMap(Entry::getKey, e -> deduplicateChartTrackPositions(
                    e.getValue()
                ).get(0)
            ));
    }

    /**
     * @return tracks on this chart/date. Response is Map<key, List<entity>> instead of Map<key, entity> because same track may occupy multiple
     * positions on the chart at the same time, and this method does NOT deduplicate them
     */
    public Map<SpotifyTrackId, List<SpotifyChartTrackEntity>> findTrackMetrics(
        Collection<String> spotifyTrackIds, Collection<String> isrcs,
        String chartId, LocalDate date, Integer minPosition, Integer maxPosition,
        ChartTrackMetricsDimension metricsDimension
    ) {
        List<String> isrcField = metricsDimension == null || metricsDimension == ChartTrackMetricsDimension.TRACK_ID
            ? List.of(TRACK, ISRC)
            : List.of(ISRC);

        return repository.findAll(JpaUtils.and(Arrays.asList(
                specificationProvider.multiValueSpec(SPOTIFY_TRACK_ID, spotifyTrackIds),
                specificationProvider.multiValueSpec(isrcField, isrcs),
                specificationProvider.singleValueSpec(List.of(ID_PROPERTY, SPOTIFY_CHART_ID), chartId),
                specificationProvider.singleValueSpec(List.of(ID_PROPERTY, CHART_DATE), date),
                specificationProvider.greaterThanOrEqualToValueSpec(CURRENT_POSITION, minPosition),
                specificationProvider.lessThanOrEqualToValueSpec(CURRENT_POSITION, maxPosition)
            )))
            .stream()
            .collect(Collectors.groupingBy(e -> e.getTrack().getId()));
    }

    public List<SpotifyChartTrackEntity> findTrackMetrics(
        Collection<String> spotifyTrackIds, Collection<String> isrcs,
        LocalDate date, Integer minPosition, Integer maxPosition, Integer minRank, Integer maxRank,
        Set<String> countryCode,
        Set<SpotifyChartBreakdown> breakdown,
        Set<SpotifyChartType> type,
        ChartTrackMetricsDimension metricsDimension) {
        return findTrackMetrics(Collections.emptySet(), spotifyTrackIds, isrcs, date, minPosition, maxPosition,
            minRank, maxRank, countryCode, breakdown, type, metricsDimension);
    }

    public List<SpotifyChartTrackEntity> findTrackMetrics(Set<SpotifyChartDateTrackKey> keysToExclude,
        Collection<String> spotifyTrackIds, Collection<String> isrcs,
        LocalDate date, Integer minPosition, Integer maxPosition, Integer minRank, Integer maxRank,
        Set<String> countryCode,
        Set<SpotifyChartBreakdown> breakdowns, Set<SpotifyChartType> types,
        ChartTrackMetricsDimension metricsDimension) {

        return findTrackMetricsByDateConsideringChartBreakdown(
            keysToExclude,
            spotifyTrackIds,
            isrcs,
            date,
            null,
            null,
            minPosition,
            maxPosition,
            minRank,
            maxRank,
            countryCode,
            breakdowns,
            types,
            metricsDimension
        );
    }

    public List<SpotifyChartTrackEntity> findTrackMetricsByDateConsideringChartBreakdown(
        Set<SpotifyChartDateTrackKey> keysToExclude,
        Collection<String> spotifyTrackIds,
        Collection<String> isrcs,
        LocalDate chartDate,
        LocalDate dailyChartDate,
        LocalDate weeklyChartDate,
        Integer minPosition,
        Integer maxPosition,
        Integer minRank,
        Integer maxRank,
        Set<String> countryCode,
        Set<SpotifyChartBreakdown> breakdowns,
        Set<SpotifyChartType> types,
        ChartTrackMetricsDimension metricsDimension
    ) {
        List<String> isrcField = metricsDimension == null || metricsDimension == ChartTrackMetricsDimension.TRACK_ID
            ? List.of(TRACK, ISRC)
            : List.of(ISRC);

        List<Specification<SpotifyChartTrackEntity>> specifications = Lists.newArrayList(
            specificationProvider.multiValueSpec(SPOTIFY_TRACK_ID, spotifyTrackIds),
            specificationProvider.multiValueSpec(isrcField, isrcs),
            specificationProvider.greaterThanOrEqualToValueSpec(CURRENT_POSITION, minPosition),
            specificationProvider.lessThanOrEqualToValueSpec(CURRENT_POSITION, maxPosition),
            specificationProvider.greaterThanOrEqualToValueSpec(CHART_RANK, minRank, List.of(SPOTIFY_CHART, MARKET_RANK)),
            specificationProvider.lessThanOrEqualToValueSpec(CHART_RANK, maxRank, List.of(SPOTIFY_CHART, MARKET_RANK)),
            specificationProvider.multiValueSpec(List.of(SPOTIFY_CHART, SPOTIFY_CHART_TYPE), types),
            specificationProvider.multiValueSpec(List.of(SPOTIFY_CHART, SPOTIFY_CHART_BREAKDOWN), breakdowns),
            specificationProvider.multiValueSpec(List.of(SPOTIFY_CHART, SPOTIFY_COUNTRY_ID), countryCode),
            specificationProvider.notMultiValueSpec(CHART_DATE_TRACK_KEY, keysToExclude),

            specificationProvider.fetchesSpecOfPaths(
                List.of(JpaPropertyPath.ofJoins(SPOTIFY_CHART, MARKET_RANK)))
        );

        //if 'chartDate' is not empty - use it for facts lookup
        //if empty but default dates passed - use default dates to find facts from corresponding charts (DAILY or WEEKLY) separately
        if (chartDate != null) {
            specifications.add(
                specificationProvider.singleValueSpec(List.of(ID_PROPERTY, CHART_DATE), chartDate)
            );
        } else if (dailyChartDate != null || weeklyChartDate != null) {
            List<Specification<SpotifyChartTrackEntity>> chartDateWithTypeSpec = new ArrayList<>();

            chartDateWithTypeSpec.add(
                JpaUtils.and(List.of(
                    specificationProvider.singleValueSpec(List.of(ID_PROPERTY, CHART_DATE), dailyChartDate),
                    specificationProvider.singleValueSpec(List.of(SPOTIFY_CHART, SPOTIFY_CHART_BREAKDOWN), SpotifyChartBreakdown.DAILY)
                ))
            );

            chartDateWithTypeSpec.add(
                JpaUtils.and(List.of(
                    specificationProvider.singleValueSpec(List.of(ID_PROPERTY, CHART_DATE), weeklyChartDate),
                    specificationProvider.singleValueSpec(List.of(SPOTIFY_CHART, SPOTIFY_CHART_BREAKDOWN), SpotifyChartBreakdown.WEEKLY)
                ))
            );

            specifications.add(JpaUtils.or(chartDateWithTypeSpec));
        }

        return deduplicateChartTrackPositions(
            repository.findAll(JpaUtils.and(specifications))
        );
    }

    public Map<SpotifyTrackId, SpotifyChartTrackEntity> findTrackMetricsExcludeTrackIds(
        Set<SpotifyTrackId> notInTrackIds,
        String chartId, LocalDate date, Integer minPosition, Integer maxPosition) {
        return deduplicateChartTrackPositions(
            repository.findAll(JpaUtils.and(Arrays.asList(
                specificationProvider.singleValueSpec(List.of(ID_PROPERTY, SPOTIFY_CHART_ID), chartId),
                specificationProvider.singleValueSpec(List.of(ID_PROPERTY, CHART_DATE), date),
                specificationProvider.greaterThanOrEqualToValueSpec(CURRENT_POSITION, minPosition),
                specificationProvider.lessThanOrEqualToValueSpec(CURRENT_POSITION, maxPosition),
                specificationProvider.notMultiValueSpec(List.of(TRACK, ID_PROPERTY), notInTrackIds)
            )))
        ).stream()
            .collect(Collectors.toMap(e -> e.getTrack().getId(), Function.identity()));
    }

    public LocalDate findMaxDate() {
        return repository.findMaxDate();
    }

    public LocalDate findMaxDateByChartId(String chartId) {
        return repository.findMaxDateByChartId(chartId);
    }

    public Map<String, List<SpotifyChartTrackEntity>> findChartTracks(
        String chartId, LocalDate startDate, LocalDate endDate,
        Collection<String> trackIds, Collection<String> isrcs
    ) {
        return deduplicateChartTrackPositions(repository.findAll(
                JpaUtils.and(Arrays.asList(
                    specificationProvider.singleValueSpec(SPOTIFY_CHART_ID, chartId),
                    specificationProvider.greaterThanOrEqualToValueSpec(CHART_DATE, startDate),
                    specificationProvider.lessThanOrEqualToValueSpec(CHART_DATE, endDate),
                    specificationProvider.multiValueSpec(SPOTIFY_TRACK_ID, trackIds),
                    specificationProvider.multiValueSpec(List.of(TRACK, ISRC), isrcs)
                ))
            )
        ).stream().collect(Collectors.groupingBy(SpotifyChartTrackEntity::getSpotifyTrackId));
    }

    public List<SpotifyChartDateProjection> findLastChartDates() {
        return repository.findLastChartDates();
    }

    public List<SpotifyChartTrackEntity> findChartTrackAtDateOrChartDateDimensionTrack(
        Set<SpotifyChartDateKey> chartDateKeys, LocalDate date,
        Set<String> trackIds, Set<String> isrcs,
        Integer minPosition, Integer maxPosition, Integer minRank, Integer maxRank,
        Set<String> countryCode, Set<SpotifyChartBreakdown> breakdowns, Set<SpotifyChartType> types
    ) {
        return repository.findChartTrackAtDateOrChartDateDimensionTrack(chartDateKeys, date, trackIds, isrcs,
            minPosition, maxPosition, minRank, maxRank, countryCode, breakdowns, types);
    }

    public List<SpotifyChartTrackEntity> findChartTrackAtDateOrChartDateDimensionIsrc(
        Set<SpotifyChartDateKey> chartDateKeys, LocalDate date,
        Set<String> trackIds, Set<String> isrcs,
        Integer minPosition, Integer maxPosition, Integer minRank, Integer maxRank,
        Set<String> countryCode, Set<SpotifyChartBreakdown> breakdowns, Set<SpotifyChartType> types
    ) {
        return repository.findChartTrackAtDateOrChartDateDimensionIsrc(chartDateKeys, date, trackIds, isrcs,
            minPosition, maxPosition, minRank, maxRank, countryCode, breakdowns, types);
    }

    //more than one record with same trackId at single chart/date is possible in rare cases (DAPD-1638)
    private SpotifyChartTrackEntity mergeDuplicates(SpotifyChartTrackEntity e1, SpotifyChartTrackEntity e2) {
        if (e1.getCreatedAt().equals(e2.getCreatedAt())) {
            return e1.getCurrentPosition() < e2.getCurrentPosition()
                ? e1
                : e2;
        }

        return e1.getCreatedAt().isAfter(e2.getCreatedAt())
            ? e1
            : e2;
    }

    private List<SpotifyChartTrackEntity> deduplicateChartTrackPositions(List<SpotifyChartTrackEntity> list) {
        return new ArrayList<>(
            deduplicateChartTrackPositions(new HashSet<>(list))
        );
    }

    private Set<SpotifyChartTrackEntity> deduplicateChartTrackPositions(Set<SpotifyChartTrackEntity> set) {
        return new HashSet<>(
            set.stream()
                //deduplicate possibly repeated records for same day/chart/track (so same track on multiple positions)
                .collect(Collectors.toMap(
                    SpotifyChartTrackEntity::getChartDateTrackKey, Function.identity(), this::mergeDuplicates))
                .values()
        );
    }
}
