/*
 * (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.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import net.datafaker.Faker;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.codecs.BsonArrayCodec;
import org.bson.codecs.DecoderContext;
import org.bson.conversions.Bson;
import org.bson.json.JsonObject;
import org.bson.json.JsonReader;
import tv.pluto.core.MongoDbManager;
import tv.pluto.grpc.clips.ClipsClient;
import tv.pluto.mongo.entities.clips.Clips;
import tv.pluto.mongo.entities.clips.IdUpdated;
import tv.pluto.mongo.entities.common.ObjectIdContentIdSet;
import tv.pluto.utils.CommonUtils;

public class ClipsUtils extends MongoUtils {

  private final String SOURCE_TYPE_HLS = "HLS";
  private static final String SOURCE_TYPE_DASH = "DASH";
  private final String BACKFILL_KIDS_TAG = "plutotv_backfill_kids";
  private final String BACKFILL_NONKIDS_TAG = "plutotv_backfill_nonkids";
  // private final String BACKFILL_GENERIC_TAG = "plutotv_backfill"; // Will be used once CMS has
  // ability to identify Kids content on a clip level.
  private final List<String> BACKFILL_TAGS = Arrays.asList(BACKFILL_KIDS_TAG, BACKFILL_NONKIDS_TAG);
  private final List<String> ERROR_TAGS = Collections.singletonList("plutotv_error");

  public ClipsUtils() {
    super(new Clips());
    this.setContentFeedsDates();
  }

  /**
   * Get list of random timelines ids
   *
   * @return list of ids
   */
  public List<String> getRandomClipsIds() {
    return getDocuments(MongoUtils.LIMIT).stream()
        .filter(clip -> !clip.getIsArchived())
        .map(clip -> clip.getId())
        .collect(Collectors.toList());
  }

  public List<String> getPartnersIds(List<Clips> clips) {
    if (clips != null) {
      return clips.stream().map(Clips::getPartner).collect(Collectors.toList());
    } else {
      return null;
    }
  }

  /**
   * Get id of the clip that has no partner
   *
   * @return id of the clip
   */
  public List<String> getIdsOfClipsWithPartner(boolean withPartner) {
    BasicDBObject query = new BasicDBObject();
    query.append("partner", new BasicDBObject("$exists", withPartner));
    List<String> ids = new ArrayList<>();
    genericCollection
        .find(query)
        .projection(fields(include("_id")))
        .forEach(clip -> ids.add(((Clips) clip).getId()));
    return ids;
  }

  /**
   * Get IDs of clips which are present in episodes
   *
   * @param limit - limit for search. 0 means all documents
   * @return - List of IDs
   */
  public List<String> getIdsOfClipsInEpisodes(int limit) {
    Bson filter = Filters.not(Filters.size("episodesWithClips", 0));

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.lookup("episodes", "_id", "sources.clip", "episodesWithClips"),
            Aggregates.match(filter),
            Aggregates.project(fields(include("_id"))),
            Aggregates.limit(limit));
    List<String> ids = new ArrayList<>();
    genericCollection
        .aggregate(aggregateQueryList)
        .forEach(clip -> ids.add(((Clips) clip).getId()));
    return ids;
  }

  public List<String> getIdsOfClipsInMoreThanOneEpisode(int limit) {
    Bson filter =
        Filters.expr(
            new Document("$gt", Arrays.asList(new Document("$size", "$episodesWithClips"), 1)));

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

    List<String> ids = new ArrayList<>();
    genericCollection
        .aggregate(aggregateQueryList)
        .forEach(clip -> ids.add(((Clips) clip).getId()));
    return ids;
  }

  /**
   * Get IDs of clips which are not present in episodes
   *
   * @param limit - limit for search. 0 means all documents
   * @return - List of IDs
   */
  public List<String> getIdsOfClipsIsNotInEpisodes(int limit) {
    Bson filter = Filters.size("episodesWithoutClips", 0);

    List<Bson> aggregateQueryList =
        Arrays.asList(
            Aggregates.lookup("episodes", "_id", "sources.clip", "episodesWithoutClips"),
            Aggregates.match(filter),
            Aggregates.project(fields(include("_id"))),
            Aggregates.limit(limit));
    List<String> ids = new ArrayList<>();
    genericCollection
        .aggregate(aggregateQueryList)
        .forEach(
            clip -> {
              Clips clips = (Clips) clip;
              if (!clips.getIsArchived()) { // ooo
                ids.add(clips.getId());
              }
            });
    return ids;
  }

  /**
   * Get list of backfill clip Ids
   *
   * @return List of ids
   */
  public List<IdUpdated> getBackfillClips() {
    List<IdUpdated> result = new ArrayList<>();

    BasicDBObject query = new BasicDBObject();

    query
        .append("tags", new BasicDBObject("$in", BACKFILL_TAGS))
        .append("contentStatus", "ready")
        .append("isArchived", new BasicDBObject("$in", Arrays.asList(false, null)))
        .append(
            "sources.type",
            new BasicDBObject("$in", Arrays.asList(SOURCE_TYPE_HLS, SOURCE_TYPE_DASH)));

    genericCollection
        .find(query)
        .projection(fields(include("_id", "updatedAt")))
        .forEach(
            clip -> {
              String id = ((Clips) clip).getId();
              LocalDateTime updatedAt = ((Clips) clip).getUpdatedAt();
              result.add(new IdUpdated(id, updatedAt));
            });

    return result;
  }

  /**
   * Get list of error clip Ids
   *
   * @return List of clips
   */
  public List<IdUpdated> getErrorClips() {
    List<IdUpdated> result = new ArrayList<>();

    BasicDBObject query = new BasicDBObject();

    query
        .append("tags", new BasicDBObject("$in", ERROR_TAGS))
        .append("contentStatus", "ready")
        .append("sources.type", SOURCE_TYPE_HLS);

    genericCollection
        .find(query)
        .projection(fields(include("_id", "updatedAt")))
        .forEach(
            clip -> {
              String id = ((Clips) clip).getId();
              LocalDateTime updatedAt = ((Clips) clip).getUpdatedAt();
              result.add(new IdUpdated(id, updatedAt));
            });

    return result;
  }

  public List<JsonObject> getDataForAvailableVodContentRecommendations() {
    List<JsonObject> result;

    // TODO rework using build-in methods
    //        Bson filterCc = and(exists("cc", true), not(size("cc",0)));
    //        Bson programmingType = regex("programmingType" , "Full Movie|Full Episode");
    //        Bson activeRegion = eq("activeRegion", "us");
    //
    //        Bson transcodeState = eq("transcode.state", "ready");
    //        Bson transcodes = exists("transcodes", true);
    //        Bson endDate = gte("availabilityWindows.AVOD.endDate", new Date());
    //
    //        List<BasicDBObject> mongoOrList = Arrays.asList(
    //                new BasicDBObject("transcode.state", new BasicDBObject("$eq", "ready")),
    //                new BasicDBObject("transcodes", new BasicDBObject("$exists", true)),
    //                new BasicDBObject("availabilityWindows.AVOD.endDate", new BasicDBObject("gte",
    // new Date()))
    //        );

    //        BasicDBObject query = new BasicDBObject("cc",
    //                new BasicDBObject("$exists", true).append("$not", new BasicDBObject("$size",
    // 0)))
    //                .append("programmingType", new BasicDBObject("$regex" , "Full Movie|Full
    // Episode"))
    //                .append("activeRegion", "us")
    //                .append("$or", mongoOrList);

    //        Bson filterOr = or( and(filterCc, programmingType,activeRegion) , and(transcodeState,
    // transcodes, endDate));

    //        String[] projectionFields = {
    //                "name",
    //                "programmingType",
    //                "actors",
    //                "directors",
    //                "producers",
    //                "writers",
    //                "authors",
    //                "cc",
    //                "tmsid",
    //                "duration",
    //                "summary",
    //                "thumbnail",
    //                "createdAt",
    //                "episodes._id",
    //                "episodes.series",
    //                "episodes.season",
    //                "episodes.genre",
    //                "episodes.subGenre",
    //                "episodes.category",
    //                "episodes.subCategory",
    //                "episodes.tmsid",
    //                "episodes.distrubutedAs",
    //                "series._id",
    //                "series.distrubtedAs",
    //                "series.name",
    //                "series.genre",
    //                "series.tmsid",
    //                "series.type"
    //        };

    //        List<Bson> aggregateQueryList =  Arrays.asList(
    //                Aggregates.match(filterOr),
    //                Aggregates.lookup("episodes", "_id" ,"content.clip", "episodes"),
    //                Aggregates.lookup("series", "episodes.series" ,"_id", "series"),
    //                Aggregates.project(fields(include(projectionFields)))
    //        );

    String json = "";
    try {
      json =
          new String(
              Files.readAllBytes(Paths.get("src/main/resources/recommendation.json")),
              StandardCharsets.UTF_8);
    } catch (IOException e) {
      LogUtils.logSevereMessageThenFail(e.getMessage());
    }

    List<BsonDocument> pipeline =
        new BsonArrayCodec()
            .decode(new JsonReader(json), DecoderContext.builder().build()).stream()
                .map(BsonValue::asDocument)
                .collect(Collectors.toList());

    result =
        MongoDbManager.getDatabase()
            .getCollection("clips")
            .withDocumentClass(JsonObject.class)
            .aggregate(pipeline)
            .into(new ArrayList<>());

    return result;
  }

  public List<Clips> getClipsWithUUID(int limit) {
    List<Clips> result = new ArrayList<>();

    BasicDBObject query =
        new BasicDBObject("contentUUID", new BasicDBObject("$exists", true))
            .append("isArchived", new BasicDBObject("$in", Arrays.asList(false, null)));

    genericCollection.find(query).limit(limit).forEach(clip -> result.add((Clips) clip));

    return result;
  }

  public List<Clips> getClipsWithoutUUID(int limit) {
    List<Clips> result = new ArrayList<>();

    BasicDBObject query =
        new BasicDBObject("contentUUID", new BasicDBObject("$exists", false))
            .append("isArchived", new BasicDBObject("$in", Arrays.asList(false, null)));

    genericCollection.find(query).limit(limit).forEach(clip -> result.add((Clips) clip));

    return result;
  }

  public List<ObjectIdContentIdSet> extractIdsAndUUID(List<Clips> clips) {
    List<ObjectIdContentIdSet> result = new ArrayList<>();

    Faker faker = new Faker();

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

    return result;
  }

  public Map<String, String> getIdsFromContentSet(List<ObjectIdContentIdSet> contentIdSets) {
    Map<String, String> result = new HashMap<>();

    contentIdSets.forEach(content -> result.put(content.getId(), content.getContentUUID()));

    return result;
  }

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

    BasicDBObject rdeRangeSize =
        new BasicDBObject(
            "ratingDescriptors",
            new BasicDBObject("$exists", true).append("$ne", new ArrayList<>()));

    genericCollection
        .find(rdeRangeSize)
        .projection(fields(include("_id")))
        .forEach(
            clip -> {
              if (!((Clips) clip).getIsArchived()) {
                String id = ((Clips) clip).getId();
                result.add(id);
              }
            });

    return result;
  }

  // TODO optimize
  public List<String> getClipsIdsWithoutRd() {
    List<String> result = new ArrayList<>();

    BasicDBObject size = new BasicDBObject("$exists", false);

    BasicDBObject rdeRangeSize = new BasicDBObject("ratingDescriptors", size);

    genericCollection
        .find(rdeRangeSize)
        .projection(fields(include("_id")))
        .forEach(
            clip -> {
              String id = ((Clips) clip).getId();
              result.add(id);
            });

    return result;
  }

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

    BasicDBObject size = new BasicDBObject("$exists", true).append("$ne", new ArrayList<>());

    BasicDBObject rdeRangeSize = new BasicDBObject("ratingDescriptors", size);

    genericCollection
        .find(rdeRangeSize)
        .projection(fields(include("_id")))
        .forEach(
            clip -> {
              if (!((Clips) clip).getIsArchived()) {
                String id = ((Clips) clip).getId();
                result.add(id);
              }
            });

    return result;
  }

  public Clips getClipsIdWithFieldInSources(String sourceField) {
    BasicDBObject clipSearch =
        new BasicDBObject("sources." + sourceField, new BasicDBObject("$exists", true))
            .append("isArchived", new BasicDBObject("$in", Arrays.asList(false, null)));

    return (Clips) (genericCollection.find(clipSearch).first());
  }

  public Clips getClipsSourcesTypeAndField(String sourceType, String field, boolean value) {
    BasicDBObject clipSearch =
        new BasicDBObject(
                "sources",
                new BasicDBObject("$exists", true).append("$not", new BasicDBObject("$size", 0)))
            .append(
                "sources", new BasicDBObject("$elemMatch", new BasicDBObject("type", sourceType)))
            .append("isArchived", new BasicDBObject("$in", Arrays.asList(false, null)))
            .append("origin.type", new BasicDBObject("$ne", "application/vnd.apple.mpegurl"));

    if (field != null) {
      clipSearch.append(field, value);
    }

    return (Clips) (genericCollection.find(clipSearch).first());
  }

  public Set<Clips> setCategoryAndVotes(List<Clips> listClips) {
    return listClips.stream().peek(c -> c.setMeta(null)).collect(Collectors.toSet());
  }

  public Clips getRandomDocumentWithTheFieldSize(String field, int size) {
    // DAL does not show isArchived = true ooo
    BasicDBObject query =
        new BasicDBObject(field, new BasicDBObject("$size", size))
            .append("isArchived", new BasicDBObject("$in", Arrays.asList(false, null)));

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

  public Clips getRandomDocument() {
    return getRandomItem(getDocuments(MongoUtils.LIMIT));
  }

  public List<Clips> getDocuments(int limit) {
    List<Clips> result = new ArrayList<>();
    genericCollection.find().limit(limit).forEach(e -> result.add((Clips) e));

    // DAL does not return Archived values
    return result.stream().filter(c -> !c.getIsArchived()).collect(Collectors.toList());
  }

  public Clips createClip(ClipsClient clipsClientAuth) {
    tv.pluto.ip.dal.clip.Clips.Clip createdClip =
        clipsClientAuth.createLanguageOverridesTranscode().getDocument();

    LogUtils.logInfoMessage("Created clip with id: " + createdClip.getId());
    return this.getDocumentById(createdClip.getId());
  }

  public Clips getDocumentWithoutSpecialCharactersInName() {
    List<Clips> clips = this.getRandomDocuments(30);

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

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

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

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

  public List<Clips> getPromotionalClips() {
    List<Clips> result = new ArrayList<>();

    BasicDBObject query =
        new BasicDBObject("promotional", true)
            .append("provider", "jwplatform")
            .append("contentStatus", "ready")
            .append("duration", new BasicDBObject("$gte", 1))
            .append("published", true)
            .append("isArchived", new BasicDBObject("$in", Arrays.asList(false, null)));

    genericCollection.find(query).forEach(clip -> result.add((Clips) clip));

    return result;
  }

  public List<Clips> getClipsByIdsWithStandardProjection(List<String> clipIds) {
    List<org.bson.types.ObjectId> objectIds =
        clipIds.stream().map(org.bson.types.ObjectId::new).collect(Collectors.toList());
    BasicDBObject query = new BasicDBObject("_id", new BasicDBObject("$in", objectIds));

    BasicDBObject projection = new BasicDBObject();
    projection.put("_id", 1);
    projection.put("name", 1);
    projection.put("rating", 1);
    projection.put("promotional", 1);
    projection.put("programmingType", 1);
    projection.put("activeRegion", 1);

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