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

import com.google.common.collect.ImmutableMap;

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.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import io.delphiplatform.api.util.CollectionUtils;
import io.delphiplatform.api.util.JpaUtils;
import io.delphiplatform.api.v3.model.JunoSpotifyStreamsIncludes;
import io.delphiplatform.api.v3.model.service.ImageService;
import io.delphiplatform.api.v3.model.spotify.JunoSpotifyStreamsGrasTrackMeta;
import io.delphiplatform.api.v3.model.spotify.JunoSpotifyStreamsMetrics;
import io.delphiplatform.api.v3.model.spotify.JunoSpotifyStreamsTrackMeta;
import io.delphiplatform.api.v3.model.spotify.LatestJunoSpotifyStreamsResult;
import io.delphiplatform.api.v3.rdb.entity.TrackEntity;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyJunoTopStreamsDailyEntity;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyJunoTopStreamsDailyEntityAggregatedProjection;
import io.delphiplatform.api.v3.rdb.entity.spotify.SpotifyJunoTopStreamsDailyId;
import io.delphiplatform.api.v3.rdb.repository.spotify.SpotifyJunoTopStreamsDailyRepository;
import io.delphiplatform.api.v3.rdb.service.EntityService;
import io.delphiplatform.api.v3.rdb.service.TrackService;
import io.delphiplatform.api.v3.rdb.service.specification.SpecificationProvider;
import io.delphiplatform.api.v3.view.util.Params;

import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.COUNTRY_CODE;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.ISRC_COUNTRY_CODE;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.PRODUCT_SALE_DATE;
import static io.delphiplatform.api.v3.rdb.entity.CommonJpaPropertyNames.STREAMS_CHANGE_PERCENT_7_TO_14_DAYS;

@Service
public class SpotifyJunoStreamsService extends EntityService<SpotifyJunoTopStreamsDailyEntity> {

    public static final ImmutableMap<String, String> SORT_BY_TO_ENTITY_FIELD_MAP =
        ImmutableMap.<String, String>builder()
            .put("streams_days_7", "streams_1_7_days_total")
            .put("streams_days_8_14", "streams_8_14_days_total")
            .build();

    private final SpecificationProvider<SpotifyJunoTopStreamsDailyEntity> specificationProvider;
    private final SpotifyJunoTopStreamsDailyRepository spotifyJunoTopStreamsDailyRepository;
    private final TrackService trackService;
    private final ImageService imageService;

    @Autowired
    public SpotifyJunoStreamsService(
        SpotifyJunoTopStreamsDailyRepository spotifyJunoTopStreamsDailyRepository,
        SpecificationProvider<SpotifyJunoTopStreamsDailyEntity> specificationProvider,
        TrackService trackService,
        ImageService imageService
    ) {
        super(spotifyJunoTopStreamsDailyRepository, SORT_BY_TO_ENTITY_FIELD_MAP);
        this.spotifyJunoTopStreamsDailyRepository = spotifyJunoTopStreamsDailyRepository;
        this.specificationProvider = specificationProvider;
        this.trackService = trackService;
        this.imageService = imageService;
    }

    @Transactional(readOnly = true)
    public List<LatestJunoSpotifyStreamsResult> findJunoSpotifyTopStreamsMetrics(Params params) {
        Set<String> streamsCountryCodes = params.getStreamsCountryCode();

        if (CollectionUtils.isEmpty(streamsCountryCodes)) {
            throw new IllegalArgumentException("empty streamsCountryCode param");
        } else if (streamsCountryCodes.size() == 1) {
            //if single countryCode was requested - this method works is chosen as it works faster than aggregating one
            return findJunoSpotifyTopStreamsMetricsSingleStreamCountry(params);
        } else {
            return findJunoSpotifyTopStreamsMetricsMultipleStreamCountries(params);
        }
    }

    private List<LatestJunoSpotifyStreamsResult> findJunoSpotifyTopStreamsMetricsSingleStreamCountry(Params params) {
        Specification<SpotifyJunoTopStreamsDailyEntity> specification = and(Arrays.asList(
            //here 'countryCode' field means actually 'streams' country code
            specificationProvider.multiValueSpec(COUNTRY_CODE, params.getStreamsCountryCode()),
            specificationProvider.multiValueSpec(ISRC_COUNTRY_CODE, params.getCountryCode()),
            specificationProvider.greaterThanOrEqualToValueSpec(STREAMS_CHANGE_PERCENT_7_TO_14_DAYS, params.getPercentChange()),
            or(Arrays.asList(
                specificationProvider.nullValueSpec(PRODUCT_SALE_DATE, true),
                and(Arrays.asList(
                    specificationProvider.nullValueSpec(PRODUCT_SALE_DATE, false),
                    specificationProvider.greaterThanOrEqualToValueSpec(PRODUCT_SALE_DATE, params.getStartDate()),
                    specificationProvider.lessThanOrEqualToValueSpec(PRODUCT_SALE_DATE, params.getEndDate())
                ))
            ))
        ));

        List<SpotifyJunoTopStreamsDailyEntity> topStreamsEntities = find(
            specification,
            getPageRequestSortByToCamelCase(params)
        );

        if (topStreamsEntities.isEmpty()) {
            return Collections.emptyList();
        }

        Map<String, TrackEntity> mainProductTrackByIsrcs = trackService.findMainProductTracksByIsrcs(
            topStreamsEntities.stream()
                .map(SpotifyJunoTopStreamsDailyEntity::getIsrc)
                .collect(Collectors.toSet()),
            TrackService.FETCHES_PRODUCT_LABEL_AND_ARTISTS
        );

        return topStreamsEntities.stream()
            .map(entity -> LatestJunoSpotifyStreamsResult.builder()
                .isrc(entity.getIsrc())
                .latestEntryDate(entity.getLatestEntryDate())
                .productSaleDate(entity.getProductSaleDate())
                .countryCode(CollectionUtils.contains(params.getJunoSpotifyStreamsIncludes(), JunoSpotifyStreamsIncludes.COUNTRY_CODES)
                    ? List.of(entity.getCountryCode())
                    : null
                )
                .isrcCountryCode(entity.getIsrcCountryCode())
                .spotifyTopTrackMeta(JunoSpotifyStreamsTrackMeta.from(entity))
                .grasTrackMeta(
                    JunoSpotifyStreamsGrasTrackMeta.from(mainProductTrackByIsrcs.get(entity.getIsrc()), imageService)
                )
                .metrics(JunoSpotifyStreamsMetrics.from(entity))
                .build()
            )
            .collect(Collectors.toList());
    }

    private List<LatestJunoSpotifyStreamsResult> findJunoSpotifyTopStreamsMetricsMultipleStreamCountries(Params params) {
        List<SpotifyJunoTopStreamsDailyEntityAggregatedProjection> topStreamsAggregatedByIsrc =
            spotifyJunoTopStreamsDailyRepository.findStreamCountriesAggregatedByIsrc(
                params.getStreamsCountryCode(), //here 'countryCode' field means actually 'streams' country code
                params.getCountryCode() != null ? params.getCountryCode() : Collections.emptySet(),
                params.getPercentChange(),
                params.getStartDate(),
                params.getEndDate(),
                getPageRequestSortByKeepCase(params)
            );

        if (topStreamsAggregatedByIsrc.isEmpty()) {
            return Collections.emptyList();
        }

        //find 1 entity per isrc to get track info (use any countryCode as track info should be same)
        Set<SpotifyJunoTopStreamsDailyId> singleIdPerIsrc = topStreamsAggregatedByIsrc.stream()
            .map(prj -> new SpotifyJunoTopStreamsDailyId(prj.getIsrc(), prj.getCountryCode()[0]))
            .collect(Collectors.toSet());

        Map<String, SpotifyJunoTopStreamsDailyEntity> singleEntityByIsrc =
            JpaUtils.splitRepositoryMethodCall(
                singleIdPerIsrc,
                spotifyJunoTopStreamsDailyRepository::findAllById,
                //should not contain any duplicate by isrc due to group by isrc in above aggregation query
                Collectors.toMap(SpotifyJunoTopStreamsDailyEntity::getIsrc, Function.identity())
            );

        Map<String, TrackEntity> mainProductTrackByIsrcs = trackService.findMainProductTracksByIsrcs(
            topStreamsAggregatedByIsrc.stream()
                .map(SpotifyJunoTopStreamsDailyEntityAggregatedProjection::getIsrc)
                .collect(Collectors.toSet()),
            TrackService.FETCHES_PRODUCT_LABEL_AND_ARTISTS
        );

        return topStreamsAggregatedByIsrc.stream()
            .map(prj -> LatestJunoSpotifyStreamsResult.builder()
                .isrc(prj.getIsrc())
                .latestEntryDate(prj.getLatestEntryDate())
                .productSaleDate(prj.getProductSaleDate())
                .countryCode(CollectionUtils.contains(params.getJunoSpotifyStreamsIncludes(), JunoSpotifyStreamsIncludes.COUNTRY_CODES)
                    ? Arrays.asList(prj.getCountryCode())
                    : null
                )
                .isrcCountryCode(prj.getIsrcCountryCode())
                .spotifyTopTrackMeta(JunoSpotifyStreamsTrackMeta.from(singleEntityByIsrc.get(prj.getIsrc())))
                .grasTrackMeta(
                    JunoSpotifyStreamsGrasTrackMeta.from(mainProductTrackByIsrcs.get(prj.getIsrc()), imageService)
                )
                .metrics(JunoSpotifyStreamsMetrics.from(prj))
                .build()
            )
            .collect(Collectors.toList());
    }
}
