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

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

import com.mongodb.BasicDBObject;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.Aggregates;
import com.mongodb.client.model.Filters;
import com.plutotv.test.lib.LogUtils;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import net.datafaker.Faker;
import org.bson.Document;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.junit.Assert;
import tv.pluto.grpc.episodes.EpisodesClient;
import tv.pluto.mongo.entities.common.ObjectIdContentIdSet;
import tv.pluto.mongo.entities.episodes.Episodes;
import tv.pluto.mongo.utils.MongoUtils;
import tv.pluto.mongo.utils.TimelinesUtils;

public class EpisodeUtils extends MongoUtils {

  private TimelinesUtils timelinesUtils = new TimelinesUtils();

  public EpisodeUtils() {
    this.genericCollection = this.collectionEpisodes;
    this.setContentFeedsDates();
  }

  /**
   * Get list of random episodes ids
   *
   * @return list of ids
   */
  public List<String> getRandomEpisodesIds() {
    return getDocuments(MongoUtils.LIMIT).stream()
        .map(episode -> ((Episodes) episode).getId())
        .collect(Collectors.toList());
  }

  public List<String> getUniqueActiveRegionFromEpisodes() {
    return getDocuments(MongoUtils.LIMIT).stream()
        .map(episode -> ((Episodes) episode).getActiveRegion())
        .distinct()
        .collect(Collectors.toList());
  }

  /**
   * Get id of Episode that has no series
   *
   * @return episode id
   */
  public String getIdOfEpisodeWithoutSeries() {
    Bson filter = Filters.eq("series", null);

    Episodes episode = collectionEpisodes.find(filter).projection(fields(include("_id"))).first();

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

  /**
   * Get id of Episode that has series
   *
   * @return episode id
   */
  public String getIdOfEpisodeWithSeries() {
    Bson filter = Filters.ne("series", null);

    Episodes episode =
        collectionEpisodes
            .find(filter)
            .projection(fields(include("_id")))
            .limit(-1)
            .skip(5)
            .first();

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

  public Episodes getRandomEpisodeWithSeriesSeasonAndNumber() {
    Bson filter =
        Filters.and(
            Filters.ne("series", null), Filters.ne("season", null), Filters.ne("number", null));

    return collectionEpisodes.find(filter).limit(1).first();
  }

  /**
   * Get id of Episode that has no sources
   *
   * @return episode id
   */
  public String getIdOfEpisodeWithoutSources() {
    BasicDBObject query = new BasicDBObject("sources", new BasicDBObject("$size", 0));

    Episodes episode = collectionEpisodes.find(query).projection(fields(include("_id"))).first();

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

  /**
   * Get id of Episode that has sources
   *
   * @return episode id
   */
  public String getIdOfEpisodeWithSources() {
    BasicDBObject query =
        new BasicDBObject("sources", new BasicDBObject("$not", new BasicDBObject("$size", 0)));

    Episodes episode = collectionEpisodes.find(query).projection(fields(include("_id"))).first();

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

  /**
   * Get id of Episode: episode has clip in sources -> clip has no partner
   *
   * @return episode id
   */
  public String getIdOfEpisodeWithoutPartner() {
    Bson filter = Filters.eq("clips.partner", null);

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

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

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

  /**
   * Get id of Episode: episode has clip in sources -> clip has partner
   *
   * @return episode id
   */
  public String getIdOfEpisodeWithPartner() {
    Bson filter = Filters.ne("clips.partner", null);
    /* 51fad8c02e077a7c6e0000f2 has a bad data and failing */
    Bson filterEpisode = Filters.ne("_id", "ObjectId('51fad8c02e077a7c6e0000f2')");

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

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

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

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

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

    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.limit(1),
            Aggregates.project(fields(include("_id"))));

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

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

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

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

    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.limit(1),
            Aggregates.project(fields(include("_id"))));

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

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

  /**
   * Get id of an Episode where a Clip in its sources has a null or non-null contentStatus.
   *
   * @return episode id
   */
  public String getIdOfEpisodeWithContentStatus(boolean withContentStatus) {

    Bson contentStatusFilter = Filters.exists("clips.contentStatus", withContentStatus);

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

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

    return episode != null ? episode.getId() : "";
  }

  /**
   * Get id of an Episode where a Clip in its sources has a null or non-null duration.
   *
   * @return episode id
   */
  public String getIdOfEpisodeWithDuration() {
    Bson contentStatusFilter = Filters.exists("clips.duration", true);

    Bson subClipsNotEmptyFilter =
        Filters.and(
            Filters.exists("clips.fragments.subClips", true),
            Filters.not(Filters.size("clips.fragments.subClips", 0)));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.unwind("$sources"),
            Aggregates.lookup("clips", "sources.clip", "_id", "clips"),
            Aggregates.unwind("$clips"),
            Aggregates.match(Filters.and(contentStatusFilter, subClipsNotEmptyFilter)),
            Aggregates.limit(1),
            Aggregates.project(fields(include("_id"))));

    return Objects.requireNonNull(collectionEpisodes.aggregate(aggregateQueryList).first()).getId();
  }

  public List<Episodes> getEpisodesByClipId(
      String clipId, int limit, int offset, Bson sort, Bson projection) {
    BasicDBObject query = new BasicDBObject("sources.clip", new ObjectId(clipId));

    List<Episodes> episodes = new ArrayList<>();
    collectionEpisodes
        .find(query)
        .projection(projection)
        .limit(limit)
        .sort(sort)
        .skip(offset)
        .forEach(episodes::add);

    return episodes;
  }

  public List<Episodes> getUniqueEpisodesForFutureTimelines(String channelId) {
    BasicDBObject query =
        new BasicDBObject("channel", new ObjectId(channelId))
            .append("start", new BasicDBObject("$gte", LocalDateTime.now(ZoneOffset.UTC)));

    Set<String> episodeIds = new HashSet<>();

    timelinesUtils
        .collectionTimelines
        .find(query)
        .forEach(timeline -> episodeIds.add(timeline.getEpisode()));

    return getDocumentsByIds(new ArrayList<>(episodeIds));
  }

  public List<String> getUniqueClipIdsFromEpisodes(List<String> episodeIds) {
    List<Episodes> episodes = getDocumentsByIds(episodeIds);

    Set<String> uniqueClipIds = new HashSet<>();

    episodes.forEach(
        episode -> episode.getSources().forEach(source -> uniqueClipIds.add(source.getClip())));

    if (uniqueClipIds.isEmpty()) {
      LogUtils.logInfoMessage("No unique clip ids found.");
    }

    return new ArrayList<>(uniqueClipIds);
  }

  public List<String> getIdsOfEpisodesWithoutAgeRange(int limit) {
    List<String> result = new ArrayList<>();

    BasicDBObject query = new BasicDBObject("ageRange", new BasicDBObject("$exists", false));

    collectionEpisodes.find(query).limit(limit).forEach(e -> result.add(e.getId().toString()));

    return result;
  }

  public List<String> getIdsOfEpisodesWithAgeRange(int limit) {
    List<String> result = new ArrayList<>();

    BasicDBObject query = new BasicDBObject("ageRange", new BasicDBObject("$exists", true));

    collectionEpisodes.find(query).limit(limit).forEach(e -> result.add(e.getId()));

    return result;
  }

  public List<Episodes> getEpisodesWithUUID(int limit) {
    List<Episodes> result = new ArrayList<>();

    BasicDBObject query = new BasicDBObject("contentUUID", new BasicDBObject("$exists", true));

    genericCollection.find(query).limit(limit).forEach(episode -> result.add((Episodes) episode));

    return result;
  }

  public List<Episodes> getEpisodesWithoutUUID(int limit) {
    List<Episodes> result = new ArrayList<>();

    BasicDBObject query = new BasicDBObject("contentUUID", new BasicDBObject("$exists", false));

    genericCollection.find(query).limit(limit).forEach(episode -> result.add((Episodes) episode));

    return result;
  }

  public List<ObjectIdContentIdSet> extractIdsAndUUID(List<Episodes> episodes) {
    List<ObjectIdContentIdSet> result = new ArrayList<>();

    Faker faker = new Faker();

    if (episodes != null && !episodes.isEmpty()) {
      episodes.forEach(
          episode ->
              result.add(
                  ObjectIdContentIdSet.builder()
                      .id(episode.getId())
                      .contentUUID(faker.internet().uuid())
                      .build()));
    }

    return result;
  }

  public List<String> getEpisodeIdWithFields() {
    List<String> result = new ArrayList<>();

    Bson filter =
        Filters.and(
            Filters.ne("name", null),
            Filters.ne("number", null),
            Filters.ne("season", null),
            Filters.ne("duration", null),
            Filters.ne("allotment", null),
            Filters.ne("description", null),
            Filters.ne("summary", null),
            Filters.ne("category", null),
            Filters.ne("subCategory", null),
            Filters.ne("genre", null),
            Filters.ne("subGenre", null),
            Filters.ne("subGenre", "Kids’ Music"), // avoid data issue on preprod
            Filters.ne("subGenre", "Kids’ TV"), // avoid data issue on preprod
            Filters.ne("tags", null),
            Filters.ne("metadataLanguage", null));

    genericCollection
        .find()
        .filter(filter)
        .limit(10)
        .forEach(episode -> result.add(((Episodes) episode).getId()));

    return result;
  }

  public String getClipFromSources() {
    String clipId;

    BasicDBObject elemMatch = new BasicDBObject("$elemMatch", Filters.ne("clip", null));
    BasicDBObject query = new BasicDBObject("sources", elemMatch);

    Episodes episode = (Episodes) genericCollection.find(query).first();

    clipId =
        episode.getSources().stream()
            .filter(source -> source.getClip() != null)
            .findFirst()
            .get()
            .getClip();

    return clipId;
  }

  /**
   * Retrieves a clip ID from the first episode that meets the following conditions: - The specified
   * field does not exist in the document, OR - The specified field has a specific value. - The
   * episode must contain at least one source where the "clip" field is not null.
   *
   * @param field The field to check for existence or a specific value.
   * @param value The value to match if the field exists.
   * @return The clip ID from the first matching episode's sources where "clip" is not null.
   */
  public String getClipFromSourcesWithNonExistentOrCondition(String field, String value) {
    String clipId;

    BasicDBObject elemMatch = new BasicDBObject("$elemMatch", Filters.ne("clip", null));

    BasicDBObject query =
        new BasicDBObject(
                "$or",
                Arrays.asList(
                    new BasicDBObject(field, new BasicDBObject("$exists", false)),
                    new BasicDBObject(field, value)))
            .append("sources", elemMatch);

    Episodes episode = (Episodes) genericCollection.find(query).first();

    clipId =
        episode.getSources().stream()
            .filter(source -> source.getClip() != null)
            .findFirst()
            .get()
            .getClip();

    return clipId;
  }

  public List<String> getSetActiveRegionFromEpisodes() {
    return getDocuments(MongoUtils.LIMIT).stream()
        .map(episode -> ((Episodes) episode).getActiveRegion())
        .distinct()
        .collect(Collectors.toList());
  }

  public Long getCountClipsInEpisode(String clipId) {
    BasicDBObject query = new BasicDBObject("sources.clip", new ObjectId(clipId));
    return collectionEpisodes.countDocuments(query);
  }

  public Episodes dashEnabledFromEpisode(int limit, boolean isPromotional) {
    BasicDBObject matchObject =
        new BasicDBObject("dashEnabled", false)
            .append("cli.sources.type", "DASH")
            .append("cli.promotional", isPromotional)
            .append("cli", new BasicDBObject("$size", 1));

    BasicDBObject projectObject =
        new BasicDBObject("cli.sources.type", 1).append("cli._id", 1).append("dashEnabled", 1);

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.lookup("clips", "sources.clip", "_id", "cli"),
            Aggregates.match(matchObject),
            Aggregates.project(projectObject),
            Aggregates.limit(limit));

    return collectionEpisodes.aggregate(aggregateQueryList).first();
  }

  public List<String> getEpisodeIdsBySeriesIds(List<String> ids) {
    List<String> result = new ArrayList<>();

    BasicDBObject query =
        new BasicDBObject("series", new BasicDBObject("$in", transformStringsToIds(ids)));

    genericCollection.find(query).forEach(episode -> result.add(((Episodes) episode).getId()));

    return result;
  }

  public List<String> getEpisodesWithSeriesIds() {
    List<String> result = new ArrayList<>();

    MongoCursor<ObjectId> iterator =
        genericCollection
            .distinct("series", ObjectId.class)
            .filter(Filters.ne("series", null))
            .iterator();

    while (result.size() < LIMIT) {
      String value = iterator.next().toString();
      // this series has data - double. In Episode 55a5ce1c5616b6240c26ddf8
      if (!value.contains("55af18d927f18b7f8bc2709a")) result.add(iterator.next().toString());
    }

    return result;
  }

  public List<String> getEpisodesWithSeriesIdsAndClips() {
    List<String> result = new ArrayList<>();

    Bson seriesFilter = Filters.ne("series", null);
    Bson clipFilter = Filters.and(Filters.exists("sources.clip"), Filters.ne("sources.clip", null));

    MongoCursor<ObjectId> iterator =
        genericCollection
            .distinct("series", ObjectId.class)
            .filter(Filters.and(seriesFilter, clipFilter))
            .iterator();

    while (result.size() < LIMIT) {
      String value = iterator.next().toString();
      // this series has data - double. In Episode 55a5ce1c5616b6240c26ddf8
      if (!value.contains("55af18d927f18b7f8bc2709a")) result.add(iterator.next().toString());
    }

    return result;
  }

  public List<Episodes> getEpisodesWithNeNullField(String field, int limit) {
    BasicDBObject query = new BasicDBObject().append(field, new BasicDBObject("$ne", null));
    List<Episodes> list = new ArrayList<>();
    try {
      genericCollection.find(query).limit(limit).forEach(e -> list.add((Episodes) e));
    } catch (CodecConfigurationException e) {
      e.printStackTrace();
      Assert.fail("Not expected data was in the collection");
    }

    return list;
  }

  public Episodes getRandomEpisodeWithClips() {
    BasicDBObject elemMatch = new BasicDBObject("$elemMatch", Filters.ne("clip", null));
    BasicDBObject query = new BasicDBObject("sources", elemMatch);

    return (Episodes) genericCollection.find(query).first();
  }

  public Episodes getRandomEpisodeWithDuration() {
    BasicDBObject objectQuery =
        new BasicDBObject("$exists", true).append("$not", new BasicDBObject("$size", 0));

    BasicDBObject query = new BasicDBObject("sources.adPods.duration", objectQuery);
    return (Episodes) genericCollection.find(query).first();
  }

  public String getUpdatedEpisodeName(String mongoName) {
    String testMongoName = null;
    List<String> listScenarios = List.of("normal", "partial", "lowerCase");
    String scenario = listScenarios.get(new Random().nextInt(listScenarios.size()));

    switch (scenario) {
      case "normal":
        testMongoName = mongoName;
        break;
      case "partial":
        if (mongoName.length() > 5) testMongoName = mongoName.substring(0, mongoName.length() - 2);
        else testMongoName = mongoName.toLowerCase();
        break;
      case "lowerCase":
        testMongoName = mongoName.toLowerCase();
        break;
    }
    LogUtils.logInfoMessage("Updated Mongo name:" + testMongoName);
    return testMongoName;
  }

  public String getSeriesWithEpisodeAndVodCategoryEntries(int skip) {
    BasicDBObject query = new BasicDBObject();
    query.put(
        "vodCategoryEntries",
        new BasicDBObject("$exists", true).append("$not", new BasicDBObject("$size", 0)));
    query.put("series", new BasicDBObject("$exists", true));
    return collectionEpisodes.find(query).skip(skip).first().getSeries();
  }

  public String getSeriesWithEpisodeTimelinesAfter() {
    BasicDBObject query = new BasicDBObject();
    query.put("nextTimeline.start", new BasicDBObject("$exists", true).append("$gt", new Date()));
    query.put("series", new BasicDBObject("$exists", true));
    return collectionEpisodes.find(query).first().getSeries();
  }

  public String createNewEpisodeId(EpisodesClient episodesClientAuth) {
    String testValue = "DALValue";
    tv.pluto.ip.dal.episode.Episodes.Episode updateGrpcEpisode =
        tv.pluto.ip.dal.episode.Episodes.Episode.newBuilder()
            .setName(testValue)
            .setSubGenre("Cartoons")
            .setGenre("Children & Family")
            .setSubCategory("Cartoons")
            .setCategory("Pop Culture")
            .setSummary(testValue)
            .setMetadataLanguage(testValue)
            .setDescription(testValue)
            .setAllotment(5)
            .addAllTags(List.of("us"))
            .setActiveRegion("us")
            .build();

    tv.pluto.ip.dal.episode.Episodes.EpisodeCreate createResponse =
        episodesClientAuth.create(updateGrpcEpisode).getDocument();
    org.testng.Assert.assertFalse(createResponse.getId().isEmpty());
    return createResponse.getId();
  }

  public long updateClipIdInSources(String episodeID, String invalidIdValue) {
    ObjectId episodeId = new ObjectId(episodeID);
    ObjectId clipId = new ObjectId(invalidIdValue);

    Document update =
        new Document("$set", new Document("sources", List.of(new Document("clip", clipId))));

    return collectionEpisodes.updateOne(new Document("_id", episodeId), update).getMatchedCount();
  }

  public Episodes getEpisodeByNameSeriesAndActiveRegion(
      String name, String series, String activeRegion) {
    BasicDBObject query = new BasicDBObject();
    query.put("name", name);
    query.put("series", new ObjectId(series));
    query.put("activeRegion", activeRegion);
    return (Episodes) genericCollection.find(query).first();
  }

  public Episodes getEpisodeBySeriesSeasonAndNumber(String series, int season, int number) {
    BasicDBObject query = new BasicDBObject();
    query.put("series", new ObjectId(series));
    query.put("season", season);
    query.put("number", number);
    return (Episodes) genericCollection.find(query).first();
  }

  public List<Episodes> getEpisodesExcludingIdsAndMatchingSeries(
      List<String> episodeIds, List<String> seriesIds) {

    List<ObjectId> episodeIdsObjectId = episodeIds.stream().map(ObjectId::new).toList();
    List<ObjectId> seriesIdsObjectId = seriesIds.stream().map(ObjectId::new).toList();

    BasicDBObject query =
        new BasicDBObject()
            .append("_id", new BasicDBObject("$nin", episodeIdsObjectId))
            .append("series", new BasicDBObject("$in", seriesIdsObjectId));

    return collectionEpisodes.find(query).into(new ArrayList<>());
  }

  public List<Episodes> getPublishedEpisodesByIds(List<String> episodeIds) {
    List<ObjectId> episodeIdsObjectId = episodeIds.stream().map(ObjectId::new).toList();

    BasicDBObject query =
        new BasicDBObject()
            .append("_id", new BasicDBObject("$in", episodeIdsObjectId))
            .append("published", true);

    return collectionEpisodes.find(query).into(new ArrayList<>());
  }

  public Episodes getEpisodeForPastTimelines() {
    return getDocumentById(
        timelinesUtils
            .collectionTimelines
            .find(new BasicDBObject("start", new BasicDBObject("$lte", new Date())))
            .sort(new BasicDBObject("start", -1))
            .first()
            .getEpisode());
  }

  public Episodes getSingleEpisodeWithoutTimelines() {

    List<ObjectId> episodeIdsInTimelines =
        timelinesUtils
            .collectionTimelines
            .distinct("episode", ObjectId.class)
            .into(new ArrayList<>());

    // Query for an episode NOT in episodeIdsInTimelines
    BasicDBObject query =
        new BasicDBObject("_id", new BasicDBObject("$nin", episodeIdsInTimelines));

    return collectionEpisodes.find(query).first();
  }

  public List<Episodes> getEpisodesBySeriesIdWithProjection(String seriesId) {
    BasicDBObject query = new BasicDBObject("series", new org.bson.types.ObjectId(seriesId));
    BasicDBObject projection = new BasicDBObject();
    projection.put("_id", 1);
    projection.put("rating", 1);
    projection.put("updatedAt", 1);
    projection.put("sources.clip", 1);

    List<Episodes> result = new ArrayList<>();
    genericCollection.find(query).projection(projection).forEach(e -> result.add((Episodes) e));
    return result;
  }
}
