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

import com.google.common.collect.ImmutableMap;

import org.openapitools.jackson.nullable.JsonNullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
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.Collection;
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 javax.annotation.Nullable;

import io.delphiplatform.api.util.CollectionUtils;
import io.delphiplatform.api.v3.model.IncludeTracks;
import io.delphiplatform.api.v3.model.Track;
import io.delphiplatform.api.v3.model.TrackMediaType;
import io.delphiplatform.api.v3.model.TrackSimple;
import io.delphiplatform.api.v3.model.TracksGroupByField;
import io.delphiplatform.api.v3.model.service.ImageService;
import io.delphiplatform.api.v3.rdb.entity.RelatedIsrcProjection;
import io.delphiplatform.api.v3.rdb.entity.TrackEntity;
import io.delphiplatform.api.v3.rdb.repository.TrackRepository;
import io.delphiplatform.api.v3.rdb.service.dto.JpaPropertyPath;
import io.delphiplatform.api.v3.rdb.service.specification.SpecificationProvider;
import io.delphiplatform.api.v3.view.util.Params;

@Service
public class TrackService extends EntityService<TrackEntity> {

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

    private static final Set<JpaPropertyPath> FETCHES = Set.of(
        JpaPropertyPath.ofJoins("product"),
        JpaPropertyPath.ofJoin("trackFamily"),
        JpaPropertyPath.ofJoin("repertoireOwner")
    );

    public static final Set<JpaPropertyPath> FETCHES_PRODUCT_LABEL_AND_ARTISTS = Set.of(
        JpaPropertyPath.ofJoins("product", "label"),
        JpaPropertyPath.ofJoin("artists")
    );

    public static final Set<JpaPropertyPath> FETCHES_FOR_TRACK_SIMPLE = Set.of(
        JpaPropertyPath.ofJoins("repertoireOwner", "repertoireOwnerCompany"),
        JpaPropertyPath.ofJoins("product", "label"),
        JpaPropertyPath.ofJoins("product", "productVersion", "configurationCategory"),
        JpaPropertyPath.ofJoins("product", "productVersion", "repertoireOwner", "repertoireOwnerCompany")
    );

    public static final Set<JpaPropertyPath> FETCHES_FOR_AUDIO_TRACK = Set.of(
        JpaPropertyPath.ofJoins("repertoireOwner", "repertoireOwnerCompany")
    );

    private final SpecificationProvider<TrackEntity> specificationProvider;
    private final TrackRepository trackRepository;
    private final ImageService imageService;
    private final ArtistService artistService;

    @Autowired
    public TrackService(TrackRepository trackRepository, SpecificationProvider<TrackEntity> specificationProvider,
        ImageService imageService, ArtistService artistService) {
        super(trackRepository, SORT_BY_TO_ENTITY_FIELD_MAP);
        this.trackRepository = trackRepository;
        this.specificationProvider = specificationProvider;
        this.imageService = imageService;
        this.artistService = artistService;
    }

    @Transactional(readOnly = true)
    public List<TrackSimple> find(Params params) {
        List<TrackEntity> tracks = find(
            TracksGroupByField.ISRC == params.getTracksGroupByField() ? GroupByType.ISRC : null,
            params.getIsrc(),
            params.getProductId(),
            params.getArtistId(),
            params.getMediaTypeKeys(),
            params.getProductFamilyNo(),
            params.getConfigCatKey(),
            params.getConfigKey(),
            getPageRequestSortByToCamelCase(params),
            FETCHES_FOR_TRACK_SIMPLE);
        List<TrackSimple> models = tracks.stream().map(TrackSimple::from).collect(Collectors.toList());
        if (CollectionUtils.contains(params.getIncludeTracks(), IncludeTracks.RELATED_ISRCS)) {
            populateRelatedIsrcs(models);
        }

        return models;
    }

    private void populateRelatedIsrcs(List<TrackSimple> models) {
        Set<String> isrcs = models.stream().map(TrackSimple::getIsrc).collect(Collectors.toSet());

        Map<String, Set<String>> isrcToRelated = trackRepository.findIsrcsWithRelated(isrcs).stream()
            .collect(Collectors.toMap(RelatedIsrcProjection::getIsrc, e -> Set.of(e.getRelatedIsrc())));

        models.forEach(t -> t.setRelatedIsrcs(JsonNullable.of(isrcToRelated.getOrDefault(t.getIsrc(), null))));
    }

    @Transactional(readOnly = true)
    public Set<String> findIsrcsForVideo(Collection<String> trackIds, Set<String> artistIds) {
        Specification<TrackEntity> resultSpec = and(Arrays.asList(
            specificationProvider.multiValueSpec("trackId", trackIds),
            specificationProvider.multiValueSpec("artistId", artistIds, "artists")
        ));

        return find(resultSpec, defaultPageable).stream()
            .map(TrackEntity::getIsrc)
            .collect(Collectors.toSet());
    }

    @Transactional(readOnly = true)
    public Set<String> findIsrcsByArtist(Set<String> artistIds, Pageable pageable) {
        Specification<TrackEntity> resultSpec = and(Collections.singletonList(
            specificationProvider.multiValueSpec("artistId", artistIds, "artists")
        ));
        return find(resultSpec, pageable == null ? defaultPageable : pageable).stream()
            .map(TrackEntity::getIsrc)
            .collect(Collectors.toSet());
    }

    @Transactional(readOnly = true)
    public List<TrackEntity> find(GroupByType groupBy, Collection<String> isrcs, String productId, String artistId,
        Collection<TrackMediaType> mediaTypes, Integer productFamilyNo, String configCatKey,
        String configKey, Pageable pageable, @Nullable Set<JpaPropertyPath> fetches) {

        Collection<String> artistIdsWithParticipants = artistService.findParticipantIdsByArtistId(artistId);

        Specification<TrackEntity> baseFilterSpec;
        if (groupBy == GroupByType.ISRC) {
            if (artistId != null) {
                baseFilterSpec = and(Arrays.asList(
                    specificationProvider.multiValueSpec("artistId", artistIdsWithParticipants, "artists"),
                    specificationProvider.singleValueSpec("isMainProduct", true)
                ));
            } else {
                baseFilterSpec = and(Arrays.asList(
                    specificationProvider.multiValueSpec("isrc", isrcs),
                    specificationProvider.singleValueSpec("productId", productId, "product"),
                    specificationProvider.singleValueSpec("isMainProduct", true)
                ));
            }
        } else {
            baseFilterSpec = and(Arrays.asList(
                specificationProvider.multiValueSpec("isrc", isrcs),
                specificationProvider.singleValueSpec("productId", productId, "product"),
                specificationProvider.multiValueSpec("artistId", artistIdsWithParticipants, "artists")
            ));
        }

        Set<Integer> mediaTypeKeys = mediaTypes.stream()
            .map(TrackMediaType::getKey)
            .collect(Collectors.toSet());
        Specification<TrackEntity> resultSpec = and(Arrays.asList(baseFilterSpec,
            specificationProvider.singleValueSpec("configCatKey", configCatKey, List.of("configurationCategory")),
            specificationProvider.singleValueSpec("configKey", configKey, List.of("product", "configuration")),
            specificationProvider.multiValueSpec("mediaTypeKey", mediaTypeKeys, "trackMediaType"),
            specificationProvider.singleValueSpec("prodFamNo", productFamilyNo,
                List.of("product", "productVersion")),
            specificationProvider.fetchesSpecOfPaths(fetches != null ? fetches : FETCHES)
        ));

        Pageable pager = pageable == null ? defaultPageable : pageable;
        return find(resultSpec, pager);
    }

    @Transactional(readOnly = true)
    public List<TrackEntity> findAudioTracks(GroupByType groupBy, String artistId, Pageable pageable) {
        return find(groupBy, null, null, artistId, TrackMediaType.AUDIO_TYPES, null,
            null, null, pageable, FETCHES_FOR_AUDIO_TRACK);
    }

    @Transactional(readOnly = true)
    public Track findOne(String trackId, Set<IncludeTracks> include) {
        return trackRepository.findById(trackId)
            .map(t -> Track.from(t, imageService))
            .map(track -> {
                track.setRelatedIsrcs(
                    JsonNullable.of(trackRepository.findRelatedIsrcs(Collections.singleton(track.getIsrc()))));
                return track;
            })
            .orElseThrow(NOT_FOUND);
    }

    @Transactional(readOnly = true)
    public Set<String> findRelatedIsrc(Collection<String> isrcs) {
        // ensure we don't end up querying the entire data set.
        if (CollectionUtils.isEmpty(isrcs)) {
            return Collections.emptySet();
        }
        return trackRepository.findRelatedIsrcs(isrcs);
    }

    @Transactional(readOnly = true)
    public Map<String, TrackEntity> findMainProductTracksByIsrcs(
        Collection<String> trackIsrcs,
        @Nullable Set<JpaPropertyPath> fetches
    ) {
        List<TrackEntity> mainProductTrackForEachIsrcs = find(
            GroupByType.ISRC,
            trackIsrcs,
            null,
            null,
            List.of(TrackMediaType.values()),
            null,
            null,
            null,
            null,
            fetches
        );

        return mainProductTrackForEachIsrcs.stream()
            .collect(Collectors.toMap(
                TrackEntity::getIsrc,
                Function.identity(),
                (e1, e2) -> e1  //pick 1st available track in case there are >1 for same isrc (should be very rare case)
            ));
    }

    @Transactional(readOnly = true)
    public Map<String, TrackSimple> findMainProductSimpleTracksByIsrcs(Collection<String> trackIsrcs) {
        return findMainProductTracksByIsrcs(trackIsrcs, FETCHES_FOR_TRACK_SIMPLE).entrySet().stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> TrackSimple.from(e.getValue())
            ));
    }
}
