/*
 * (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.Aggregates;
import com.mongodb.client.model.Filters;
import com.plutotv.test.lib.LogUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import tv.pluto.grpc.channels.ActiveContent;
import tv.pluto.mongo.entities.episodes.Episodes;
import tv.pluto.mongo.entities.timelines.Timelines;
import tv.pluto.mongo.entities.timelines.TimelinesForActiveContent;
import tv.pluto.utils.CommonUtils;

public class TimelinesUtils extends MongoUtils {

  public TimelinesUtils() {
    this.genericCollection = this.collectionTimelines;
    this.setContentFeedsDates();
  }

  /**
   * Get list of random timelines ids
   *
   * @return list of ids
   */
  public List<String> getRandomTimelinesIds() {
    return getDocuments(MongoUtils.LIMIT).stream()
        .map(timeline -> ((Timelines) timeline).getId())
        .collect(Collectors.toList());
  }

  public String getTimelineIdByAggregatedEpisode(List<Bson> aggregateQueryList) {

    Episodes episode = collectionEpisodes.aggregate(aggregateQueryList).first();

    String episodeId;

    if (episode == null) {
      return "";
    } else {
      episodeId = episode.getId();
    }

    BasicDBObject query = new BasicDBObject();
    query.put("episode", new ObjectId(episodeId));

    Timelines timeline = collectionTimelines.find(query).first();

    if (timeline == null) {
      return "";
    } else {
      return timeline.getId();
    }
  }

  /**
   * Get timeline for active content
   *
   * @param activeContent active content object for query
   * @return list of timelines
   */
  public List<Timelines> getTimelinesForActiveContent(
      ActiveContent activeContent, List<String> projection) {
    List<Timelines> result = new ArrayList<>();
    BasicDBObject query = new BasicDBObject();

    if (activeContent.getChannelId() != null) {
      query.append("channel", new ObjectId(activeContent.getChannelId()));
    }

    if (activeContent.getTo() != null && activeContent.getFrom() != null) {
      query
          .append("start", new BasicDBObject("$lte", activeContent.getTo()))
          .append("stop", new BasicDBObject("$gte", activeContent.getFrom()));
    }

    Bson projectionFields =
        projection != null && !projection.isEmpty() ? include(projection) : null;

    BasicDBObject sorting =
        new BasicDBObject(
            activeContent.getSort() != null ? activeContent.getSort() : "defaultField", 1);

    collectionTimelines
        .find(query)
        .projection(projectionFields)
        .sort(sorting)
        .limit(activeContent.getLimit())
        .forEach(result::add);

    return result;
  }

  /**
   * Get id of Timeline, where episode has series
   *
   * @return timeline id
   */
  public String getIdOfTimelineWithSeries() {
    List<Bson> pipeline =
        Arrays.asList(
            Aggregates.lookup("episodes", "episode", "_id", "matchedEpisode"),
            Aggregates.match(Filters.exists("matchedEpisode.series")),
            Aggregates.project(fields(include("_id"))),
            Aggregates.limit(1));

    Timelines timeline = collectionTimelines.aggregate(pipeline).first();
    return timeline != null ? timeline.getId() : "";
  }

  /**
   * Get id of Timeline, where episode has no series
   *
   * @return timeline id
   */
  public String getIdOfTimelineWithoutSeries() {
    Bson seriesFilter = Filters.not(Filters.exists("series"));

    Bson timelineFilter = Filters.not(Filters.size("timelines", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.match(seriesFilter),
            Aggregates.lookup("timelines", "_id", "episode", "timelines"),
            Aggregates.match(timelineFilter),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return getTimelineIdByAggregatedEpisode(aggregateQueryList);
  }

  /**
   * Get id of Timeline, where episode has no clips in sources
   *
   * @return timeline id
   */
  public String getIdOfTimelineWithoutClips() {
    Bson sourcesFilter = Filters.exists("sources");
    Bson sourcesSizeFilter = Filters.size("sources", 0);

    Bson sizeFilter = Filters.not(Filters.size("timelines", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.match(Filters.and(sourcesFilter, sourcesSizeFilter)),
            Aggregates.lookup("timelines", "_id", "episode", "timelines"),
            Aggregates.match(sizeFilter),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return getTimelineIdByAggregatedEpisode(aggregateQueryList);
  }

  /**
   * Get id of Timeline, where episode has clips in sources
   *
   * @return timeline id
   */
  public String getIdOfTimelineWithClips() {
    Bson sourcesFilter = Filters.exists("sources");
    Bson sourcesSizeFilter = Filters.not(Filters.size("sources", 0));

    Bson timelineFilter = Filters.not(Filters.size("timelines", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.match(Filters.and(sourcesFilter, sourcesSizeFilter)),
            Aggregates.lookup("timelines", "_id", "episode", "timelines"),
            Aggregates.match(timelineFilter),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return getTimelineIdByAggregatedEpisode(aggregateQueryList);
  }

  /**
   * Get id of Timeline: timeline has episode -> episode has clip in sources -> clip has no partner
   *
   * @return timeline id
   */
  public String getIdOfTimelineWithoutPartner() {

    Bson partnerFilter = Filters.eq("clips.partner", null);

    Bson timelineFilter = Filters.not(Filters.size("timelines", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.unwind("$sources"),
            Aggregates.lookup("clips", "sources.clip", "_id", "clips"),
            Aggregates.match(partnerFilter),
            Aggregates.lookup("timelines", "_id", "episode", "timelines"),
            Aggregates.match(timelineFilter),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return getTimelineIdByAggregatedEpisode(aggregateQueryList);
  }

  /**
   * Get id of Timeline: timeline has episode -> episode has clip in sources -> clip has partner
   *
   * @return timeline id
   */
  public String getIdOfTimelineWithPartner() {

    Bson partnerFilter = Filters.exists("clips.partner");

    Bson timelineFilter = Filters.not(Filters.size("timelines", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.unwind("$sources"),
            Aggregates.lookup("clips", "sources.clip", "_id", "clips"),
            Aggregates.match(partnerFilter),
            Aggregates.lookup("timelines", "_id", "episode", "timelines"),
            Aggregates.match(timelineFilter),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return getTimelineIdByAggregatedEpisode(aggregateQueryList);
  }

  /**
   * Get id of Timeline: timeline has episode -> episode has clip in sources -> clip has partner ->
   * partner has no comScore
   *
   * @return timeline id
   */
  public String getIdOfTimelineWithoutComScore() {
    Bson partnerFilter = Filters.ne("clips.partner", null);

    Bson sizeFilter = Filters.not(Filters.size("partners", 0));
    Bson comScoreFilter = Filters.exists("partners.comScore", false);

    Bson timelineFilter = Filters.not(Filters.size("timelines", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.unwind("$sources"),
            Aggregates.lookup("clips", "sources.clip", "_id", "clips"),
            Aggregates.match(partnerFilter),
            Aggregates.unwind("$clips"),
            Aggregates.lookup("partners", "clips.partner", "_id", "partners"),
            Aggregates.match(Filters.and(sizeFilter, comScoreFilter)),
            Aggregates.lookup("timelines", "_id", "episode", "timelines"),
            Aggregates.match(timelineFilter),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return getTimelineIdByAggregatedEpisode(aggregateQueryList);
  }

  /**
   * Get id of Timeline: timeline has episode -> episode has clip in sources -> clip has partner ->
   * partner has comScore
   *
   * @return timeline id
   */
  public String getIdOfTimelineWithComScore() {
    Bson partnerFilter = Filters.ne("clips.partner", null);

    Bson sizeFilter = Filters.not(Filters.size("partners", 0));
    Bson comScoreFilter = Filters.exists("partners.comScore.c2");

    Bson timelineFilter = Filters.not(Filters.size("timelines", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.unwind("$sources"),
            Aggregates.lookup("clips", "sources.clip", "_id", "clips"),
            Aggregates.match(partnerFilter),
            Aggregates.unwind("$clips"),
            Aggregates.lookup("partners", "clips.partner", "_id", "partners"),
            Aggregates.match(Filters.and(sizeFilter, comScoreFilter)),
            Aggregates.lookup("timelines", "_id", "episode", "timelines"),
            Aggregates.match(timelineFilter),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return getTimelineIdByAggregatedEpisode(aggregateQueryList);
  }

  public String getIdOfTimelineWithComScoreAndGenre() {
    Bson partnerFilter = Filters.ne("clips.partner", null);
    Bson genreFilter = Filters.ne("clips.genre", null);

    Bson sizeFilter = Filters.not(Filters.size("partners", 0));
    Bson comScoreFilter = Filters.exists("partners.comScore.c2");

    Bson timelineFilter = Filters.not(Filters.size("timelines", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.unwind("$sources"),
            Aggregates.lookup("clips", "sources.clip", "_id", "clips"),
            Aggregates.match(Filters.and(partnerFilter, genreFilter)),
            Aggregates.unwind("$clips"),
            Aggregates.lookup("partners", "clips.partner", "_id", "partners"),
            Aggregates.match(Filters.and(sizeFilter, comScoreFilter)),
            Aggregates.lookup("timelines", "_id", "episode", "timelines"),
            Aggregates.match(timelineFilter),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return getTimelineIdByAggregatedEpisode(aggregateQueryList);
  }

  public List<Timelines> getDocumentsForContentFeeds(
      LocalDateTime startDate, LocalDateTime stopDate, List<String> ids) {
    List<Timelines> result = new ArrayList<>();

    BasicDBObject query =
        new BasicDBObject("channel", new BasicDBObject("$in", ids))
            .append("start", new BasicDBObject("$gt", startDate))
            .append("stop", new BasicDBObject("$lt", stopDate));

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

    return result;
  }

  public List<Timelines> getTimelinesByChannelsIds(List<String> ids) {
    List<Timelines> result = new ArrayList<>();
    BasicDBObject query = new BasicDBObject();
    query.put("channel", new BasicDBObject("$in", transformStringsToIds(ids)));
    genericCollection.find(query).forEach(e -> result.add((Timelines) e));
    return result;
  }

  public Iterator<?> getTimelinesWithChannelId() {
    BasicDBObject query = new BasicDBObject();
    query.put("channel", new BasicDBObject("$exists", true));
    return genericCollection.find(query).iterator();
  }

  public List<Timelines> getTimelinesForSelectedChannelId(List<String> channelId) {
    List<ObjectId> listObjectsChannelIds =
        channelId.stream().map(ObjectId::new).collect(Collectors.toList());

    BasicDBObject query = new BasicDBObject();
    query.put("channel", new BasicDBObject("$in", listObjectsChannelIds));

    List<Timelines> mongoTimelines = new ArrayList<>();
    Iterator<?> iterator = genericCollection.find(query).iterator();
    while (iterator.hasNext()) {
      Timelines timeline = (Timelines) iterator.next();
      mongoTimelines.add(timeline);
    }
    return mongoTimelines;
  }

  public List<Timelines> getTimelinesBetweenStartEnd(
      String id, Instant startBegin, Instant stopBegin, String[] projectionFields) {
    List<Timelines> result = new ArrayList<>();

    BasicDBObject query =
        new BasicDBObject("start", new BasicDBObject("$eq", startBegin))
            .append("stop", new BasicDBObject("$eq", stopBegin))
            .append("episode", new ObjectId(id));

    if (projectionFields != null) {
      BasicDBObject projection = new BasicDBObject();
      for (String field : projectionFields) {
        projection.put(field, 1);
      }
      genericCollection.find(query).projection(projection).forEach(e -> result.add((Timelines) e));
    } else {
      genericCollection.find(query).forEach(e -> result.add((Timelines) e));
    }

    return result;
  }

  public List getDocumentsBetweenDates(Instant start, Instant stop) {
    LogUtils.logInfoMessage("Start: " + start + ", until: " + stop);

    List<BasicDBObject> basicDBObjects =
        List.of(
            new BasicDBObject("start", new BasicDBObject("$lt", stop)),
            new BasicDBObject("start", new BasicDBObject("$gte", start)));

    BasicDBObject queryBetween = new BasicDBObject("$and", basicDBObjects);
    return CommonUtils.iteratorToList(genericCollection.find(queryBetween).iterator());
  }

  public List<TimelinesForActiveContent> convertToTimelinesForActiveContent(
      List<Timelines> timelinesList) {
    if (timelinesList == null || timelinesList.isEmpty()) {
      return Collections.emptyList();
    }

    return timelinesList.stream()
        .map(this::mapToTimelinesForActiveContent)
        .collect(Collectors.toList());
  }

  private TimelinesForActiveContent mapToTimelinesForActiveContent(Timelines timeline) {
    TimelinesForActiveContent timelinesForActiveContent = new TimelinesForActiveContent();

    if (timeline.getId() != null) {
      timelinesForActiveContent.setId(timeline.getId());
    }
    if (timeline.getChannel() != null) {
      timelinesForActiveContent.setChannel(timeline.getChannel());
    }
    if (timeline.getStart() != null) {
      timelinesForActiveContent.setStart(timeline.getStart());
    }
    if (timeline.getStop() != null) {
      timelinesForActiveContent.setStop(timeline.getStop());
    }

    return timelinesForActiveContent;
  }

  public List<String> getEpisodeIdsByStopBetween(Instant start, Instant stop) {
    BasicDBObject query =
        new BasicDBObject("stop", new BasicDBObject("$lte", stop).append("$gt", start));
    return collectionTimelines
        .distinct("episode", query, ObjectId.class)
        .into(new ArrayList<>())
        .stream()
        .map(ObjectId::toHexString)
        .collect(Collectors.toList());
  }

  public List<Timelines> getTimelinesByStartGteAndChannel(Instant startDate, String channelId) {
    BasicDBObject query =
        new BasicDBObject("start", new BasicDBObject("$gte", startDate))
            .append("channel", new ObjectId(channelId));

    List<Timelines> result = new ArrayList<>();
    genericCollection.find(query).forEach(doc -> result.add((Timelines) doc));
    return result;
  }

  public List<Timelines> getTimelinesByStartNIN(Instant startDate, String channelId) {
    BasicDBObject query =
        new BasicDBObject("start", new BasicDBObject("$nin", Collections.singletonList(startDate)))
            .append("channel", new ObjectId(channelId));

    List<Timelines> result = new ArrayList<>();
    genericCollection.find(query).forEach(doc -> result.add((Timelines) doc));
    return result;
  }

  public List<Timelines> getTimelinesByStartGTAndStopLT(
      Instant startDate, Instant stopDate, String channelId) {

    BasicDBObject query =
        new BasicDBObject()
            .append("start", new BasicDBObject("$gt", startDate))
            .append("stop", new BasicDBObject("$lt", stopDate))
            .append("channel", new ObjectId(channelId));

    List<Timelines> result = new ArrayList<>();
    genericCollection.find(query).forEach(doc -> result.add((Timelines) doc));

    LogUtils.logInfoMessage("Fetched " + result.size() + " timelines.");
    return result;
  }

  public List<Timelines> getTimelinesByStartGTEAndStopLTE(
      Instant startDate, Instant stopDate, String channelId) {

    BasicDBObject query =
        new BasicDBObject()
            .append("start", new BasicDBObject("$gte", startDate))
            .append("stop", new BasicDBObject("$lte", stopDate))
            .append("channel", new ObjectId(channelId));

    List<Timelines> result = new ArrayList<>();
    genericCollection.find(query).forEach(doc -> result.add((Timelines) doc));

    LogUtils.logInfoMessage("Fetched " + result.size() + " timelines.");
    return result;
  }
}
