/*
 * (c) 2020 Pluto Inc.
 */
package tv.pluto.mongo.utils;

import static com.mongodb.client.model.Projections.fields;
import static com.mongodb.client.model.Projections.include;

import com.mongodb.BasicDBObject;
import com.mongodb.client.model.Collation;
import com.plutotv.test.lib.LogUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.bson.conversions.Bson;
import tv.pluto.mongo.entities.channels.Channels;
import tv.pluto.mongo.entities.channels.non_stitched_channels_with_multiple_scheduled_clips.Channel;
import tv.pluto.mongo.entities.clips.Clips;
import tv.pluto.mongo.entities.episodes.Episodes;
import tv.pluto.mongo.utils.episodes.EpisodeUtils;
import tv.pluto.utils.CommonUtils;

public class ChannelsUtils extends MongoUtils {

  public ChannelsUtils() {
    super(new Channels());
    this.setChannelsContentFeedsDates();
  }

  private final EpisodeUtils episodeUtils = new EpisodeUtils();
  private final ClipsUtils clipsUtils = new ClipsUtils();

  /**
   * Get list of random channels ids
   *
   * @return list of ids
   */
  public List<String> getRandomChannelsIds() {
    return getDocuments(MongoUtils.LIMIT).stream()
        .map(channel -> ((Channels) channel).getId())
        .collect(Collectors.toList());
  }

  public List<Channels> getFilteredRandomChannelsId(int filteredNumber) {
    return getDocuments(MongoUtils.LIMIT).stream()
        .filter(channel -> ((Channels) channel).getNumber() != filteredNumber)
        .map(channel -> ((Channels) channel))
        .collect(Collectors.toList());
  }

  /**
   * Get stiched active channel ids for specific active region.
   *
   * @param activeRegion active region code
   * @return list of ids
   */
  public List<String> getStitchedActiveChannelIds(String activeRegion) {
    BasicDBObject query =
        new BasicDBObject("activeRegion", activeRegion)
            .append("isStitched", true)
            .append("published", true);
    List<String> channelIds = new ArrayList<>();
    genericCollection
        .find(query)
        .projection(fields(include("_id")))
        .forEach(channel -> channelIds.add(((Channels) channel).getId()));
    return channelIds;
  }

  /**
   * Get valid active region for StitchedActiveChannel request
   *
   * @return active region code
   */
  public String getValidActiveRegion() {
    BasicDBObject query = new BasicDBObject("isStitched", true).append("published", true);
    List<String> activeRegions = new ArrayList<>();
    genericCollection
        .find(query)
        .projection(fields(include("activeRegion")))
        .forEach(channel -> activeRegions.add(((Channels) channel).getActiveRegion()));

    List<String> uniqueActiveRegions =
        activeRegions.stream().distinct().collect(Collectors.toList());

    return getRandomItem(uniqueActiveRegions);
  }

  /**
   * Get documents from collection by updatedAt field
   *
   * @param startDateTime - Search from this updatedAt date (exclusive).
   * @param endDateTime - Search up to this updatedAt date (inclusive).
   * @return - List of documents
   */
  @SuppressWarnings("unchecked")
  public List<Channels> getDocumentsForContentFeeds(
      LocalDateTime startDateTime, LocalDateTime endDateTime) {
    List<Channels> result = new ArrayList<>();

    List<BasicDBObject> mongoOrList =
        Arrays.asList(
            new BasicDBObject("plutoOfficeOnly", false),
            new BasicDBObject("plutoOfficeOnly", new BasicDBObject("$exists", false)));

    BasicDBObject query =
        new BasicDBObject(
                "updatedAt", new BasicDBObject("$gt", startDateTime).append("$lte", endDateTime))
            .append("archived", false)
            .append("$or", mongoOrList);

    genericCollection
        .find(query)
        .sort(new BasicDBObject("updatedAt", 1))
        .forEach(e -> result.add((Channels) e));

    return result;
  }

  public List<Channels> getDocumentsForFeedsSync(LocalDateTime startDateTime, boolean isPublished) {
    List<Channels> result = new ArrayList<>();

    BasicDBObject query =
        new BasicDBObject("updatedAt", new BasicDBObject("$gte", startDateTime))
            .append("published", isPublished);

    genericCollection.find(query).forEach(e -> result.add((Channels) e));
    return result;
  }

  private List<Channels> getPublishedNonStitchedChannels() {
    List<Channels> result = new ArrayList<>();

    BasicDBObject query = new BasicDBObject("published", true).append("is_stitched", false);

    genericCollection.find(query).forEach(e -> result.add((Channels) e));

    return result;
  }

  private List<
          tv.pluto.mongo.entities.channels.non_stitched_channels_with_multiple_scheduled_clips
              .Clips>
      getMultipleClipsForNonStitchedChannel(Channel channel) {
    List<String> uniqueEpisodeIds =
        episodeUtils.getUniqueEpisodesForFutureTimelines(channel.getId()).stream()
            .map(Episodes::getId)
            .collect(Collectors.toList());

    if (uniqueEpisodeIds.isEmpty()) {
      LogUtils.logInfoMessage("No unique episodeIds found for channel with ID=" + channel.getId());
      return null;
    }

    List<String> uniqueClipIds = episodeUtils.getUniqueClipIdsFromEpisodes(uniqueEpisodeIds);

    if (uniqueClipIds.isEmpty()) {
      LogUtils.logInfoMessage(
          "No unique clipIds found for episode ids=" + uniqueEpisodeIds + " on channel.");
      return null;
    }

    List<Clips> uniqueClips = clipsUtils.getDocumentsByIds(uniqueClipIds);

    List<tv.pluto.mongo.entities.channels.non_stitched_channels_with_multiple_scheduled_clips.Clips>
        mappedClips =
            mapGrpcObjectsToPojo(
                uniqueClips,
                tv.pluto.mongo.entities.channels.non_stitched_channels_with_multiple_scheduled_clips
                    .Clips.class);

    if (mappedClips.size() > 0) {
      return mappedClips;
    }

    return null;
  }

  public List<Channel> getNonStitchedChannelsWithMultipleScheduledClips() {

    List<Channels> publishedNonStitchedChannels = getPublishedNonStitchedChannels();

    List<Channel> channels = mapGrpcObjectsToPojo(publishedNonStitchedChannels, Channel.class);

    channels.forEach(channel -> channel.setClips(getMultipleClipsForNonStitchedChannel(channel)));

    return channels.stream()
        .filter(channel -> channel.getClips() != null)
        .collect(Collectors.toList());
  }

  public List<Channels> getListChannels(BasicDBObject query) {
    List<Channels> result = new ArrayList<>();
    genericCollection.find(query).forEach(e -> result.add((Channels) e));
    return result;
  }

  public BasicDBObject kidsModeObject(Channels channel) {
    BasicDBObject basicDBObject = new BasicDBObject();
    basicDBObject.put("kidsMode", channel.getKidsMode());
    return basicDBObject;
  }

  public List<String> getListIds(List<Channels> channels) {
    return channels.stream().map(Channels::getId).collect(Collectors.toList());
  }

  public List<Channels> getChannelsWithCollation(
      int limit, int offset, Bson sort, boolean numericOrdering) {
    Collation enUs = Collation.builder().locale("en_US").numericOrdering(numericOrdering).build();

    List<Channels> result = new ArrayList<>();
    genericCollection
        .find()
        .limit(limit)
        .skip(offset)
        .sort(sort)
        .collation(enUs)
        .forEach(e -> result.add((Channels) e));
    return result;
  }

  public List<Channels> getChannelsQueryWithCollation(
      int limit, int offset, Bson sort, BasicDBObject query, int num) {
    Collation enUs = Collation.builder().locale("en_US").numericOrdering(true).build();

    List<Channels> result = new ArrayList<>();
    genericCollection
        .find(query)
        .limit(limit)
        .skip(offset)
        .sort(sort)
        .collation(enUs)
        .forEach(e -> result.add((Channels) e));
    return result;
  }

  public Channels getDocumentWithoutSpecialCharactersInName() {
    List<Channels> channels = this.getRandomDocuments(30);

    return channels.stream()
        .filter(channel -> !CommonUtils.containsSpecialCharacters(channel.getName()))
        .findFirst()
        .orElse(null);
  }

  public List<Channels> getDocumentsByRegex(String field, String regex) {
    List<Channels> result = new ArrayList<>();

    BasicDBObject query =
        new BasicDBObject(field, new BasicDBObject("$regex", regex).append("$options", "si"));

    genericCollection.find(query).forEach(e -> result.add((Channels) e));
    return result;
  }

  public Set<Channels> setChannelsCategoryAndVotes(List<Channels> listChannels) {
    return listChannels.stream()
        .peek(
            c -> {
              c.setChatRoomId(null);
              c.setV(0);
            })
        .collect(Collectors.toSet());
  }

  public List<Channels> getChannelsGracenoteSorted(Set<Channels> setChannels) {
    return setChannels.stream()
        .peek(c -> c.setGracenoteIntegration(null))
        .sorted(Comparator.comparing(Channels::getId))
        .collect(Collectors.toList());
  }

  public <T> T getRandomDocumentWithTheFieldSize(String field, int size, int skip) {
    BasicDBObject query = new BasicDBObject(field, new BasicDBObject("$size", size));
    query.append(
        "summary", new BasicDBObject("$exists", true).append("$ne", null)); // summary is required
    query.append(
        "description",
        new BasicDBObject("$exists", true).append("$ne", null)); // description is required
    query.append(
        "number", new BasicDBObject("$exists", true).append("$gte", 1)); // number must be >= 1

    return (T) genericCollection.find(query).skip(skip).first();
  }

  public long getCountOfChannels() {
    return genericCollection.countDocuments();
  }

  public String getSlug() {
    BasicDBObject query = new BasicDBObject("published", true);
    return genericCollection
        .find(query)
        .projection(fields(include("slug")))
        .map(channel -> ((Channels) channel).getSlug())
        .first();
  }
}
