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

import com.google.common.collect.ImmutableMap;

import org.springframework.beans.factory.annotation.Autowired;
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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import io.delphiplatform.api.util.CollectionUtils;
import io.delphiplatform.api.util.EntityUtils;
import io.delphiplatform.api.v3.model.TrackStateIncludes;
import io.delphiplatform.api.v3.model.charts.ChartTrackLifetimeMetrics;
import io.delphiplatform.api.v3.model.charts.ChartTrackStateMetrics;
import io.delphiplatform.api.v3.model.spotify.ChartTrackMetricsDimension;
import io.delphiplatform.api.v3.model.spotify.SpotifyChart;
import io.delphiplatform.api.v3.model.spotify.SpotifyChartBreakdown;
import io.delphiplatform.api.v3.model.spotify.SpotifyChartTrackMetadata;
import io.delphiplatform.api.v3.model.spotify.SpotifyChartType;
import io.delphiplatform.api.v3.model.spotify.SpotifyTrackChartsSearchResult;
import io.delphiplatform.api.v3.rdb.entity.spotify.IsrcAndChartAndTrackIds;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyChartDateKey;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyChartEntity;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyChartTrackEntity;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyChartTrackLifetimeEntity;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyTrackEntity;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyTrackId;
import io.delphiplatform.api.v3.rdb.repository.spotify.SpotifyChartRepository;
import io.delphiplatform.api.v3.rdb.service.EntityService;
import io.delphiplatform.api.v3.rdb.service.dto.ChartIdDateIsrcKey;
import io.delphiplatform.api.v3.view.util.Params;

import static io.delphiplatform.api.util.CollectionUtils.contains;

@Service
@Transactional(readOnly = true)
public class SpotifyTracksChartsService extends EntityService<SpotifyChartEntity> {

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

    private final SpotifyChartService chartService;
    private final SpotifyChartTrackService chartTrackService;
    private final SpotifyChartTrackLifetimeService trackLifetimeService;
    private final SpotifyTrackService spotifyTrackService;

    @Autowired
    public SpotifyTracksChartsService(
        SpotifyChartRepository repository,
        SpotifyChartTrackService chartTrackService,
        SpotifyChartTrackLifetimeService trackLifetimeService,
        SpotifyTrackService spotifyTrackService,
        SpotifyChartService chartService
    ) {
        super(repository, SORT_BY_TO_ENTITY_FIELD_MAP);
        this.chartTrackService = chartTrackService;
        this.trackLifetimeService = trackLifetimeService;
        this.spotifyTrackService = spotifyTrackService;
        this.chartService = chartService;
    }

    public List<SpotifyTrackChartsSearchResult> getSpotifyTrackCharts(Params params) {
        return getSpotifyTrackCharts(
            params.getDate(),
            params.getTrackIdSet(), params.getIsrcSet(),
            params.getMinPosition(), params.getMaxPosition(),
            params.getMinRank(), params.getMaxRank(),
            params.getCountryCode(),
            params.getBreakdowns(),
            params.getTypes(),
            params.getTrackStateIncludes(),
            params.getChartTrackMetricsDimension()
        );
    }

    private List<SpotifyTrackChartsSearchResult> getSpotifyTrackCharts(
        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,
        Set<TrackStateIncludes> trackStateIncludes,
        ChartTrackMetricsDimension metricsDimension
    ) {
        List<SpotifyChartTrackEntity> chartTrackMetrics;

        Set<SpotifyChartDateKey> chartLastDateKeys = null;

        if (date == null) {
            chartLastDateKeys = chartTrackService.findLastChartDates()
                .stream()
                .map(SpotifyChartDateKey::of)
                .collect(Collectors.toSet());
        }

        if (metricsDimension == null || metricsDimension == ChartTrackMetricsDimension.TRACK_ID) {
            chartTrackMetrics = chartTrackService.findChartTrackAtDateOrChartDateDimensionTrack(
                chartLastDateKeys,
                date, trackIds, isrcs, minPosition, maxPosition,
                minRank, maxRank, countryCode, breakdowns, types
            );
        } else {
            List<SpotifyChartTrackEntity> metricsList = chartTrackService.findChartTrackAtDateOrChartDateDimensionIsrc(
                chartLastDateKeys,
                date, trackIds, isrcs, minPosition, maxPosition,
                minRank, maxRank, countryCode, breakdowns, types
            );

            //de-duplicate result list for case when we have >1 facts for same chartId/chartDate/isrc. Not done in DB to avoid complex query/DB load for rare case
            Map<ChartIdDateIsrcKey, SpotifyChartTrackEntity> metricsByChartKey = new HashMap<>();

            for (SpotifyChartTrackEntity metric : metricsList) {
                ChartIdDateIsrcKey key = ChartIdDateIsrcKey.of(metric);

                if (!metricsByChartKey.containsKey(key) || metricsByChartKey.get(key).getCurrentPosition() > metric.getCurrentPosition()) {
                    metricsByChartKey.put(key, metric);
                }
            }

            chartTrackMetrics = new ArrayList<>(metricsByChartKey.values());
        }

        populateNecessaryLazyFieldsForFactEntity(chartTrackMetrics);

        return getSpotifyTrackChartsSearchResults(trackStateIncludes, metricsDimension, chartTrackMetrics);
    }

    public List<SpotifyTrackChartsSearchResult> getSpotifyTrackChartsSearchResults(
        Set<TrackStateIncludes> trackStateIncludes,
        ChartTrackMetricsDimension metricsDimension,
        List<SpotifyChartTrackEntity> chartTrackMetrics) {
        Map<IsrcAndChartAndTrackIds, SpotifyChartTrackLifetimeEntity> lifetimeMetricsToChartTrackLifetimeId =
            contains(trackStateIncludes, TrackStateIncludes.LIFETIME_METRICS) ?
                findLifetimeMetrics(chartTrackMetrics) :
                Collections.emptyMap();

        return chartTrackMetrics.stream()
            .map(chartTrackMetric -> buildTrackChartsSearchResult(
                chartTrackMetric,
                lifetimeMetricsToChartTrackLifetimeId.getOrDefault(
                    new IsrcAndChartAndTrackIds(
                        chartTrackMetric.getSpotifyChartId(),
                        chartTrackMetric.getSpotifyTrackId(),
                        chartTrackMetric.getIsrc()
                    ), null),
                trackStateIncludes,
                metricsDimension))
            .collect(Collectors.toList());
    }

    public List<SpotifyTrackChartsSearchResult> getSpotifyTracksChartsLifetime(Params params) {
        return getSpotifyTracksChartsLifetime(
            params.getTrackIdSet(), params.getIsrcSet(),
            params.getMinRank(), params.getMaxRank(),
            params.getCountryCode(),
            params.getBreakdowns(),
            params.getTypes(),
            params.getTrackStateIncludes(),
            params.getChartTrackMetricsDimension()
        );
    }

    public List<SpotifyTrackChartsSearchResult> getSpotifyTracksChartsLifetime(
        Set<String> trackIds,
        Set<String> isrcs,
        Integer minRank, Integer maxRank,
        Set<String> countryCode,
        Set<SpotifyChartBreakdown> breakdowns,
        Set<SpotifyChartType> types,
        Set<TrackStateIncludes> trackStateIncludes,
        ChartTrackMetricsDimension metricsDimension
    ) {
        List<SpotifyChartTrackLifetimeEntity> lifetimeMetrics = trackLifetimeService.findChartTrackLifetimeMetrics(
            trackIds, isrcs,
            minRank, maxRank,
            countryCode,
            breakdowns,
            types
        );

        return lifetimeMetrics.stream()
            .map(lifetimeMetric -> buildTrackChartsSearchResultFromLifetime(
                lifetimeMetric,
                trackStateIncludes,
                metricsDimension
            ))
            .collect(Collectors.toList());
    }

    private SpotifyTrackChartsSearchResult buildTrackChartsSearchResultFromLifetime(
        SpotifyChartTrackLifetimeEntity lifetimeMetricEntity,
        Set<TrackStateIncludes> trackStateIncludes,
        ChartTrackMetricsDimension metricsDimension
    ) {
        SpotifyChart chartMeta = contains(trackStateIncludes, TrackStateIncludes.CHART_META) ?
            SpotifyChart.of(lifetimeMetricEntity.getChart()) : null;

        List<String> licensors = contains(trackStateIncludes, TrackStateIncludes.LICENSORS) ?
            Arrays.asList(lifetimeMetricEntity.getTrack().getLicensors()) : null;

        SpotifyChartTrackMetadata chartTrack = contains(trackStateIncludes, TrackStateIncludes.PUBLIC_META) ?
            SpotifyChartTrackMetadata.of(lifetimeMetricEntity.getTrack()) : null;

        ChartTrackLifetimeMetrics lifetimeMetric = contains(trackStateIncludes, TrackStateIncludes.LIFETIME_METRICS) ?
            ChartTrackLifetimeMetrics.of(lifetimeMetricEntity, metricsDimension) : null;

        return SpotifyTrackChartsSearchResult
            .builder()
            .chartMeta(chartMeta)
            .licensors(licensors)
            .publicMeta(chartTrack)
            .lifetimeMetrics(lifetimeMetric)
            .build();
    }

    private SpotifyTrackChartsSearchResult buildTrackChartsSearchResult(
        SpotifyChartTrackEntity chartTrackMetricEntity,
        SpotifyChartTrackLifetimeEntity lifetimeMetricEntity,
        Set<TrackStateIncludes> trackStateIncludes,
        ChartTrackMetricsDimension metricsDimension
    ) {
        SpotifyChart chartMeta = contains(trackStateIncludes, TrackStateIncludes.CHART_META) ?
            SpotifyChart.of(chartTrackMetricEntity.getChart()) : null;

        List<String> licensors = contains(trackStateIncludes, TrackStateIncludes.LICENSORS) ?
            Arrays.asList(chartTrackMetricEntity.getTrack().getLicensors()) : null;

        SpotifyChartTrackMetadata chartTrack = contains(trackStateIncludes, TrackStateIncludes.PUBLIC_META) ?
            SpotifyChartTrackMetadata.of(chartTrackMetricEntity.getTrack()) : null;

        ChartTrackStateMetrics metrics = contains(trackStateIncludes, TrackStateIncludes.METRICS) ?
            ChartTrackStateMetrics.of(chartTrackMetricEntity, metricsDimension) : null;

        ChartTrackLifetimeMetrics lifetimeMetric =
            ChartTrackLifetimeMetrics.of(lifetimeMetricEntity, metricsDimension);

        return SpotifyTrackChartsSearchResult
            .builder()
            .chartMeta(chartMeta)
            .licensors(licensors)
            .publicMeta(chartTrack)
            .metrics(metrics)
            .lifetimeMetrics(lifetimeMetric)
            .build();
    }

    private Map<IsrcAndChartAndTrackIds, SpotifyChartTrackLifetimeEntity> findLifetimeMetrics(
        List<SpotifyChartTrackEntity> chartTrackMetrics
    ) {
        List<IsrcAndChartAndTrackIds> trackLifetimeIds =
            buildSpotifyChartTrackLifetimeIds(chartTrackMetrics);

        return trackLifetimeService.findChartTrackLifetimeMetricsByIds(trackLifetimeIds);
    }

    private List<IsrcAndChartAndTrackIds> buildSpotifyChartTrackLifetimeIds(
        List<SpotifyChartTrackEntity> chartTrackMetrics
    ) {
        return chartTrackMetrics.stream()
            .map(e -> new IsrcAndChartAndTrackIds(e.getSpotifyChartId(), e.getSpotifyTrackId(), e.getIsrc()))
            .collect(Collectors.toList());
    }

    private void populateNecessaryLazyFieldsForFactEntity(List<SpotifyChartTrackEntity> chartTrackMetrics) {
        if (CollectionUtils.isNotEmpty(chartTrackMetrics)) {
            Map<String, SpotifyChartEntity> allChartsById = chartService.findChartsByIdsWithMarketRank(
                chartTrackMetrics.stream()
                    .map(SpotifyChartTrackEntity::getSpotifyChartId)
                    .collect(Collectors.toList())
            );

            Map<SpotifyTrackId, SpotifyTrackEntity> allTracksById = spotifyTrackService.findTrackIdToTrack(
                chartTrackMetrics.stream()
                    .map(EntityUtils::convert)
                    .collect(Collectors.toSet())
            );

            for (SpotifyChartTrackEntity fact : chartTrackMetrics) {
                fact.setChart(allChartsById.get(fact.getSpotifyChartId()));
                fact.setTrack(allTracksById.get(EntityUtils.convert(fact)));
            }
        }
    }
}
