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

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;

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.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.Set;
import java.util.stream.Collectors;

import io.delphiplatform.api.util.CollectionUtils;
import io.delphiplatform.api.util.OffsetBasedPageRequest;
import io.delphiplatform.api.v3.model.service.ImageService;
import io.delphiplatform.api.v3.model.video.ExpandTo;
import io.delphiplatform.api.v3.model.video.Video;
import io.delphiplatform.api.v3.model.video.YoutubeContentType;
import io.delphiplatform.api.v3.rdb.entity.video.VideoEntity;
import io.delphiplatform.api.v3.rdb.repository.VideoRepository;
import io.delphiplatform.api.v3.rdb.service.specification.SpecificationProvider;
import io.delphiplatform.api.v3.view.util.Params;

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

@Service
public class VideoService extends EntityService<VideoEntity> {

    private static final ImmutableMap<String, String> SORT_BY_TO_ENTITY_FIELD_MAP = ImmutableMap.<String, String>builder()
        .put("video_title", "title")
        .put("channel_id", "channel_channel_id")
        .build();
    private static final Set<String> FETCHES = Set.of("channel");
    private final VideoRepository videoRepository;
    private final SpecificationProvider<VideoEntity> specificationProvider;
    private final ImageService imageService;
    private final TrackService trackService;
    private final ArtistService artistService;

    @Autowired
    public VideoService(VideoRepository videoRepository,
        SpecificationProvider<VideoEntity> specificationProvider,
        ImageService imageService, TrackService trackService,
        ArtistService artistService) {
        super(videoRepository);
        this.videoRepository = videoRepository;
        this.specificationProvider = specificationProvider;
        this.imageService = imageService;
        this.trackService = trackService;
        this.artistService = artistService;
    }

    @Transactional(readOnly = true)
    public List<String> getVideoIds(Params params) {
        if (CollectionUtils.isNotEmpty(params.getVideoIds())) {
            return new ArrayList<>(params.getVideoIds());
        }
        List<Video> videos = getAllVideosForFurtherProcessing(params);
        List<String> sonyVideoIds = videos.stream().map(Video::getVideoId).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(sonyVideoIds)
            && CollectionUtils.isNotEmpty(params.getVideoIds())
            && !existsInfoAboutVideoInDb(params.getVideoIds())) {
            // still trying to search for chartmetric unique video ids
            return new ArrayList<>(params.getVideoIds());
        }
        return sonyVideoIds;
    }

    private boolean existsInfoAboutVideoInDb(String videoId) {
        if (CollectionUtils.isEmpty(videoId)) {
            return false;
        }
        return videoRepository.existsById(videoId);
    }

    private boolean existsInfoAboutVideoInDb(Set<String> videoId) {
        if (CollectionUtils.isEmpty(videoId)) {
            return false;
        }
        return videoRepository.existsByVideoIdIn(videoId);
    }

    /**
     * Gets videos by params for further processing (bigtable requests, other internal requests to PG). Does not use pagination.
     */
    @Transactional(readOnly = true)
    public List<Video> getAllVideosForFurtherProcessing(Params params) {
        Params videosByIsrcsParams = params.copyVideoParams();
        return find(videosByIsrcsParams);
    }

    @Transactional(readOnly = true)
    public List<Video> find(Params params) {

        Specification<VideoEntity> spec = and(Arrays.asList(
            specificationProvider.singleValueSpec("channelId", params.getChannelId(), "channel"),
            specificationProvider.singleValueSpec("dsp", params.getVideoDsp()),
            specificationProvider
                .videosByContentTypeSpec(params.getYouTubeContentTypes(), params.getMinPremiumUgcViews()),
            specificationProvider.multiValueSpec("videoId", params.getVideoIds()),

            specificationProvider.fetchesSpec(FETCHES)
        ));

        if (isEmpty(params.getTrackId()) && isNotEmpty(params.getArtistId())) {
            Set<String> videoIds = params.getYouTubeContentTypes().stream()
                .map(ct -> videoRepository.findVideoIdsWithArtistIdAndContentType(
                    artistService.findParticipantIdsByArtistId(params.getArtistId()),
                    ct.getValue().toUpperCase(),
                    YoutubeContentType.PREMIUM_UGC.equals(ct) ? params.getMinPremiumUgcViews() : 0)
                )
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());

            if (isNotEmpty(videoIds)) {
                spec = and(Arrays.asList(spec,
                    specificationProvider.multiValueSpec("videoId", videoIds)
                ));
            } else {
                return Collections.emptyList();
            }
        } else {
            Set<String> isrcs = getIsrcsForVideos(params);
            // ensure we don't end up querying the entire data set.
            if (isNotEmpty(isrcs)) {
                spec = and(Arrays.asList(spec,
                    specificationProvider.videosByIsrcsSpec(isrcs, params.getYouTubeContentTypes(),
                        params.getMinPremiumUgcViews())
                ));
            } else if (isEmpty(params.getChannelId())) {
                return Collections.emptyList();
            }
        }

        String sortBy = convertToEntityField(params.getSortBy());
        return find(spec, OffsetBasedPageRequest
            .ofParamsToCamelCase(params.getOffset(), params.getLimit(), params.getSortOrder(), sortBy))
            .stream()
            .map(entity -> Video.from(entity, imageService))
            .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public Set<String> getIsrcsForVideos(Params params) {
        Set<String> artistIdsWithParticipants = artistService.findParticipantIdsByArtistId(params.getArtistId());

        Set<String> isrcsFromParams;
        if (isNotEmpty(params.getIsrc()) && contains(params.getExpandTo(), ExpandTo.RELATED_ISRCS)) {
            isrcsFromParams = trackService.findRelatedIsrc(params.getIsrc());
        } else if (isNotEmpty(params.getIsrc())) {
            isrcsFromParams = Sets.newHashSet(params.getIsrc());
        } else {
            isrcsFromParams = new HashSet<>();
        }

        if (isNotEmpty(params.getVideoIds())) {
            isrcsFromParams.addAll(getIsrcsByVideoIds(params.getVideoIds()));
        }

        Set<String> isrcsFromTrackAndArtist;
        if (isEmpty(params.getTrackId()) && isEmpty(artistIdsWithParticipants)) {
            return isrcsFromParams;
        } else if (isEmpty(params.getTrackId()) && isNotEmpty(artistIdsWithParticipants)) {
            isrcsFromTrackAndArtist = trackService
                .findIsrcsByArtist(artistIdsWithParticipants, defaultPageable);
        } else {
            isrcsFromTrackAndArtist = trackService.findIsrcsForVideo(params.getTrackId(), artistIdsWithParticipants);
        }

        if (isEmpty(isrcsFromParams)) {
            return isrcsFromTrackAndArtist;
        } else {
            isrcsFromParams.retainAll(isrcsFromTrackAndArtist);
            return isrcsFromParams;
        }
    }

    @Transactional(readOnly = true)
    public Video findOne(String videoId) {
        return videoRepository.findById(videoId).map(p -> Video.from(p, imageService)).orElseThrow(NOT_FOUND);
    }

    protected String convertToEntityField(String inputSortBy) {
        if (isEmpty(inputSortBy)) {
            return null;
        }
        return SORT_BY_TO_ENTITY_FIELD_MAP.getOrDefault(inputSortBy, inputSortBy);
    }

    @Transactional(readOnly = true)
    public Map<String, ImmutableSet<String>> getVideoIdToIsrcs(Set<String> ids) {
        List<VideoEntity> allById = videoRepository.findByVideoIdIn(ids);
        return allById.stream()
            .collect(Collectors.toMap(VideoEntity::getVideoId,
                e -> ImmutableSet.copyOf(e.getIsrcs())));
    }

    @Transactional(readOnly = true)
    public Set<String> getIsrcsByVideoIds(Set<String> videoId) {
        List<VideoEntity> videos = videoRepository.findByVideoIdIn(videoId);
        if (videos.isEmpty()) {
            return Set.of();
        }

        return videos.stream().flatMap(elem -> elem.getIsrcs().stream()).collect(Collectors.toSet());
    }
}
