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

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.GsonBuilder;
import com.opencsv.CSVReader;
import com.opencsv.CSVReaderBuilder;
import com.opencsv.exceptions.CsvValidationException;
import com.plutotv.test.lib.LogUtils;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bson.types.ObjectId;
import org.junit.Assert;
import tv.pluto.mongo.entities.clips.Clips;
import tv.pluto.mongo.entities.comscore.ComScore;
import tv.pluto.mongo.entities.comscore.ComScoreMetadata;
import tv.pluto.mongo.entities.episodes.AgeRange;
import tv.pluto.mongo.entities.episodes.Episodes;
import tv.pluto.mongo.entities.partners.Partners;
import tv.pluto.mongo.entities.sequencer.Clip;
import tv.pluto.mongo.entities.sequencer.ClipSource;
import tv.pluto.mongo.entities.sequencer.Episode;
import tv.pluto.mongo.entities.sequencer.Fragment;
import tv.pluto.mongo.entities.sequencer.HydratedComScore;
import tv.pluto.mongo.entities.sequencer.Nielsen;
import tv.pluto.mongo.entities.sequencer.Partner;
import tv.pluto.mongo.entities.sequencer.Source;
import tv.pluto.mongo.entities.sequencer.SubClip;
import tv.pluto.mongo.entities.sequencer.Timeline;
import tv.pluto.mongo.entities.subclips.SubClips;
import tv.pluto.mongo.entities.timelines.Timelines;
import tv.pluto.mongo.utils.episodes.AgeRangeUtils;
import tv.pluto.mongo.utils.episodes.EpisodeUtils;
import tv.pluto.mongo.utils.json.SkipDeserialisation;

public class SequencerUtils extends MongoUtils {
  private static final Logger logger = Logger.getLogger(SequencerUtils.class.getName());

  public SequencerUtils() {
    this.gson =
        new GsonBuilder()
            .addDeserializationExclusionStrategy(
                new ExclusionStrategy() {
                  @Override
                  public boolean shouldSkipField(FieldAttributes f) {
                    return f.getAnnotation(SkipDeserialisation.class) != null;
                  }

                  @Override
                  public boolean shouldSkipClass(Class<?> clazz) {
                    return false;
                  }
                })
            .setPrettyPrinting()
            .create();
  }

  private final EpisodeUtils episodeUtils = new EpisodeUtils();
  private final ClipsUtils clipsUtils = new ClipsUtils();
  private final SubClipsUtils subClipsUtils = new SubClipsUtils();
  private final PartnersUtils partnersUtils = new PartnersUtils();
  private final ComScoreUtils comScoreUtils = new ComScoreUtils();
  private final SeriesUtils seriesUtils = new SeriesUtils();
  private final TimelinesUtils timelinesUtils = new TimelinesUtils();
  private final AgeRangeUtils ageRangeUtils = new AgeRangeUtils();

  /**
   * Hydrate episode by joining objects retrieved. - Populate episode. - Populate clips on episode.
   * - Populate partners on clips. - Populate series on episode.
   *
   * @param id episode id
   * @return Episodes object
   */
  public Episode hydrateBaseEpisode(String id, String playbackType) {

    if (playbackType.isEmpty()) {
      playbackType = "vod";
    }

    Episode hydratedEpisode;

    Episodes episode = episodeUtils.getDocumentById(id);

    if (episode == null) {
      logger.log(
          Level.SEVERE,
          "Could not hydrate episode for sequencer. Episode with id=" + id + " does not exist.");
      return null;
    }

    hydratedEpisode = this.mapGrpcObjectToPojo(episode, Episode.class);

    // Hydrate series
    if (!episode.getSeries().isEmpty()) {
      hydratedEpisode.setSeries(
          seriesUtils.mapGrpcObjectToPojo(
              seriesUtils.getDocumentById(episode.getSeries()),
              tv.pluto.mongo.entities.sequencer.Series.class));
    }

    // Hydrate ageRanges
    if (!episode.getAgeRange().isEmpty()) {
      String[] projection = {"countryCode", "name", "displayName", "min", "max"};
      List<String> ageRangeIds = transformIdsToStrings(episode.getAgeRange());
      List<AgeRange> ageRanges = ageRangeUtils.getDocumentsByIds(ageRangeIds, projection);

      hydratedEpisode.setAgeRange(ageRanges);
    }

    Nielsen nielsen = getNielsenData(episode.getId());

    List<tv.pluto.mongo.entities.episodes.Source> initialSources = episode.getSources();

    List<Source> mappedSources = new ArrayList<>();

    // Hydrate clip -> sources
    for (tv.pluto.mongo.entities.episodes.Source initSource : initialSources) {
      Source internalSource;
      List<Fragment> mappedFragments = new ArrayList<>();
      internalSource = this.mapGrpcObjectToPojo(initSource, Source.class);

      // Hydrate clip data
      String clipId = initSource.getClip();
      Clips internalClip = clipsUtils.getDocumentById(clipId);
      Assert.assertNotNull(internalClip);

      internalSource.setClip(this.mapGrpcObjectToPojo(internalClip, Clip.class));

      // Hydrate clip -> partner
      if (!internalClip.getPartner().isEmpty()) {
        Partners partner = partnersUtils.getDocumentById(internalClip.getPartner());
        Partner mappedPartner = this.mapGrpcObjectToPojo(partner, Partner.class);
        internalSource.getClip().setPartner(mappedPartner);
      }

      // Hydrate clip -> author
      if (internalClip.getAuthor() != null && !internalClip.getAuthor().getName().isEmpty()) {
        internalSource.getClip().getAuthor().setName(internalClip.getAuthor().getName());
      }

      List<tv.pluto.mongo.entities.common.Fragment> internalFragments = internalClip.getFragments();

      // Hydrate clip -> fragments
      if (!internalFragments.isEmpty()) {
        for (tv.pluto.mongo.entities.common.Fragment initFragment : internalFragments) {
          Fragment mappedFragment = this.mapGrpcObjectToPojo(initFragment, Fragment.class);

          List<ObjectId> subClipsIds = initFragment.getSubClips();
          List<SubClip> mappedSubClips = new ArrayList<>();

          // Htdrate clip -> fragment -> subClips
          if (!subClipsIds.isEmpty()) {
            List<SubClips> internalSubClips =
                subClipsUtils.getDocumentsByIds(transformIdsToStrings(subClipsIds));

            internalSubClips.forEach(
                subClip -> mappedSubClips.add(this.mapGrpcObjectToPojo(subClip, SubClip.class)));
          }

          mappedFragment.setSubClips(mappedSubClips);

          mappedFragments.add(mappedFragment);
        }
        internalSource.getClip().setFragments(mappedFragments);
      }

      List<tv.pluto.mongo.entities.common.Source> internalClipSources = internalClip.getSources();

      List<ClipSource> mappedClipsSources =
          clipsUtils.mapGrpcObjectsToPojo(internalClipSources, ClipSource.class);

      internalSource.getClip().setSources(mappedClipsSources);

      // Set nielsen
      internalSource.getClip().setNielsen(nielsen);

      // Set comScore
      HydratedComScore hydratedComScore = null;
      if (internalSource.getClip().getPartner() != null) {
        ComScore comScore =
            comScoreUtils.getComScoreForClip(
                internalSource.getClip().getId(), playbackType, episode.getGenre());

        if (comScore != null) {
          hydratedComScore = new HydratedComScore();
          hydratedComScore.setStreamingTag(comScore.getStreamingTag());
          hydratedComScore.setMetadata(comScore.getMetadata());
        }
      }
      internalSource.getClip().setComScore(hydratedComScore);

      mappedSources.add(internalSource);
    }

    // Set Sources
    hydratedEpisode.setSources(mappedSources);

    return hydratedEpisode;
  }

  /**
   * Get a hydrated episode.
   *
   * @param id episode id
   * @return Episodes object
   */
  public Episode getHydratedEpisode(String id) {
    return hydrateBaseEpisode(id, "");
  }

  /**
   * * Hydrate timeline by joining objects retrieved. - Use timeline id to get timeline. - Populate
   * episode on timeline. - Populate clips on episode. - Populate partners on clips. - Populate
   * series on episode. - Populated comScore data. - Populate neilsen data.
   *
   * @param id timeline id
   * @return hydrated timeline
   */
  public Timeline getHydratedTimeline(String id) {
    Timeline hydratedTimeline;

    Timelines timeline = timelinesUtils.getDocumentById(id);

    if (timeline == null) {
      LogUtils.logSevereMessage(
          "Could not hydrate timeline for sequencer. Timeline with id=" + id + " does not exist.");
      return null;
    }

    hydratedTimeline = this.mapGrpcObjectToPojo(timeline, Timeline.class);

    Episode mappedEpisode = hydrateBaseEpisode(timeline.getEpisode(), "linear");

    // Hydrate episode
    hydratedTimeline.setEpisode(mappedEpisode);

    return hydratedTimeline;
  }

  public Nielsen getNielsenData(String episodeId) {
    Nielsen nielsen = new Nielsen();

    Episodes episode = episodeUtils.getDocumentById(episodeId);
    tv.pluto.mongo.entities.series.Series series = new tv.pluto.mongo.entities.series.Series();
    String genre;

    if (!episode.getSeries().isEmpty()) {
      series = seriesUtils.getDocumentById(episode.getSeries());
    }

    String name = series.getName().isEmpty() ? "unknown" : series.getName();

    if (!episode.getSubGenre().isEmpty()) {
      genre = episode.getSubGenre();
    } else if (!series.getSubGenre().isEmpty()) {
      genre = series.getSubGenre();
    } else {
      genre = "unknown";
    }
    // TODO https://plutotv.atlassian.net/browse/ARCH-420
    nielsen.setDuration(episode.getDuration() - episode.getAdPodsDuration());
    nielsen.setGenre(plutoToNielsenGenre(genre));
    nielsen.setName(name);

    return nielsen;
  }

  /**
   * Return genre map from src/main/resources/genreMap.csv
   *
   * @return genre map
   */
  public Map<String, String> getNielsenGenreMap() {
    Map<String, String> genreMap = new HashMap<>();
    try (CSVReader reader =
        new CSVReaderBuilder(new FileReader("src/main/resources/genreMap.csv"))
            .withSkipLines(1) // skip the first line, header info
            .build()) {
      String[] nextLine;
      while ((nextLine = reader.readNext()) != null) {
        genreMap.put(nextLine[1], nextLine[6]);
      }
    } catch (CsvValidationException | IOException e) {
      LogUtils.logSevereMessage(e.getMessage());
    }

    return genreMap;
  }

  /**
   * Return Nielsen Genre for a Pluto Genre
   *
   * @param plutoGenre pluto genre
   * @return nielsen genre or GV if genre not present in the map
   */
  public String plutoToNielsenGenre(String plutoGenre) {
    return getNielsenGenreMap().getOrDefault(plutoGenre, "GV");
  }

  public void sortInnerLists(Episode episode) {
    if (episode != null) {

      if (!episode.getSources().isEmpty()) {
        // Sort episode sources
        episode.getSources().sort(Comparator.comparing(source -> source.getClip().getId()));

        episode
            .getSources()
            .forEach(
                source -> {
                  if (!source.getClip().getSources().isEmpty()) {
                    // Sort clips
                    source.getClip().getSources().sort(Comparator.comparing(ClipSource::getId));
                  }

                  if (!source.getClip().getFragments().isEmpty()) {
                    source
                        .getClip()
                        .getFragments()
                        .forEach(
                            fragment -> {
                              if (!fragment.getSubClips().isEmpty()) {
                                // Sort subClips in fragments
                                fragment.getSubClips().sort(Comparator.comparing(SubClip::getId));

                                fragment
                                    .getSubClips()
                                    .forEach(
                                        subClip -> {
                                          if (!subClip.getSources().isEmpty()) {
                                            // Sort clipSources in subClips
                                            subClip
                                                .getSources()
                                                .sort(Comparator.comparing(ClipSource::getId));
                                          }
                                        });
                              }
                            });
                  }
                });
      }

      // Sort age ranges
      if (!episode.getAgeRange().isEmpty()) {
        episode.getAgeRange().sort(Comparator.comparing(AgeRange::getId));
      }
    }
  }

  public Timeline getTimelinesNotNulls(Timeline timeline) {
    for (Source source : timeline.getEpisode().getSources()) {
      ComScoreMetadata metadata = source.getClip().getComScore().getMetadata();
      replaceNullFields(metadata);
    }
    return timeline;
  }

  private void replaceNullFields(ComScoreMetadata metadata) {
    Field[] fields = metadata.getClass().getDeclaredFields();
    for (Field field : fields) {
      field.setAccessible(true);
      try {
        Object value = field.get(metadata);
        if (value instanceof String && ((String) value).contains("*null")) {
          field.set(metadata, "");
        }
      } catch (IllegalAccessException e) {
        LogUtils.logSevereMessageThenFail(e.getLocalizedMessage());
      }
    }
  }
}
