/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.streams.youtube.provider;

import org.apache.streams.core.StreamsDatum;
import org.apache.streams.google.gplus.configuration.UserInfo;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.util.api.requests.backoff.BackOffStrategy;
import org.apache.streams.youtube.YoutubeConfiguration;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.services.youtube.YouTube;
import com.google.api.services.youtube.model.ActivityListResponse;
import com.google.api.services.youtube.model.Video;
import com.google.api.services.youtube.model.VideoListResponse;
import com.google.gson.Gson;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;

/**
 * YoutubeDataCollector for YoutubeUserActivityProvider.
 */
public class YoutubeUserActivityCollector extends YoutubeDataCollector {

  /**
   * Max results allowed per request
   * https://developers.google.com/+/api/latest/activities/list
   */
  private static final long MAX_RESULTS = 50;
  private static final int MAX_ATTEMPTS = 5;
  private static final Logger LOGGER = LoggerFactory.getLogger(YoutubeUserActivityCollector.class);
  private static final ObjectMapper MAPPER = StreamsJacksonMapper.getInstance();

  static { //set up mapper for Google Activity Object
    SimpleModule simpleModule = new SimpleModule();
    MAPPER.registerModule(simpleModule);
    MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  }

  private Gson gson = new Gson();
  private BlockingQueue<StreamsDatum> datumQueue;
  private BackOffStrategy backOff;
  private YouTube youtube;
  private UserInfo userInfo;
  private YoutubeConfiguration config;

  /**
   * YoutubeUserActivityCollector constructor.
   * @param youtube YouTube
   * @param datumQueue BlockingQueue of StreamsDatum
   * @param backOff BackOffStrategy
   * @param userInfo UserInfo
   * @param config YoutubeConfiguration
   */
  public YoutubeUserActivityCollector(
      YouTube youtube,
      BlockingQueue<StreamsDatum> datumQueue,
      BackOffStrategy backOff,
      UserInfo userInfo,
      YoutubeConfiguration config) {
    this.youtube = youtube;
    this.datumQueue = datumQueue;
    this.backOff = backOff;
    this.userInfo = userInfo;
    this.config = config;
  }

  @Override
  public void run() {
    collectActivityData();
  }

  /**
   * Iterate through all users in the Youtube configuration and collect all videos
   * associated with their accounts.
   */
  protected void collectActivityData() {

    int item_count = 0;
    int last_count = 0;
    int page_count = 0;

    try {
      YouTube.Activities.List request = null;
      ActivityListResponse feed = null;

      boolean tryAgain = false;
      int attempt = 0;
      DateTime afterDate = userInfo.getAfterDate();
      DateTime beforeDate = userInfo.getBeforeDate();

      do {
        try {
          if (request == null) {
            request = this.youtube.activities().list("contentDetails")
                .setChannelId(userInfo.getUserId())
                .setMaxResults(MAX_RESULTS)
                .setKey(config.getApiKey());
            feed = request.execute();
          } else {
            request = this.youtube.activities().list("contentDetails")
                .setChannelId(userInfo.getUserId())
                .setMaxResults(MAX_RESULTS)
                .setPageToken(feed.getNextPageToken())
                .setKey(config.getApiKey());
            feed = request.execute();
          }
          this.backOff.reset(); //successful pull reset api.
          last_count = feed.getItems().size();
          processActivityFeed(feed, afterDate, beforeDate);
          item_count += last_count;
          page_count++;
        } catch (GoogleJsonResponseException gjre) {
          tryAgain = backoffAndIdentifyIfRetry(gjre, this.backOff);
          ++attempt;
        }
      }
      while ((tryAgain || (feed != null && feed.getNextPageToken() != null)) && attempt < MAX_ATTEMPTS);
    } catch (Throwable throwable) {
      if (throwable instanceof InterruptedException) {
        Thread.currentThread().interrupt();
      }
      throwable.printStackTrace();
      LOGGER.warn("Unable to pull Activities for user={} : {}", this.userInfo.getUserId(), throwable);
    }

    LOGGER.info("item_count: {} last_count: {} page_count: {} ", item_count, last_count, page_count);

  }

  /**
   * Given a feed and an after and before date, fetch all relevant user videos
   * and place them into the datumQueue for post-processing.
   * @param feed ActivityListResponse
   * @param afterDate DateTime
   * @param beforeDate DateTime
   * @throws IOException IOException
   * @throws InterruptedException InterruptedException
   */
  void processActivityFeed(ActivityListResponse feed, DateTime afterDate, DateTime beforeDate) throws IOException, InterruptedException {
    for (com.google.api.services.youtube.model.Activity activity : feed.getItems()) {
      try {
        List<Video> videos = new ArrayList<>();

        if (activity.getContentDetails().getUpload() != null) {
          videos.addAll(getVideoList(activity.getContentDetails().getUpload().getVideoId()));
        }
        if (activity.getContentDetails().getPlaylistItem() != null && activity.getContentDetails().getPlaylistItem().getResourceId() != null) {
          videos.addAll(getVideoList(activity.getContentDetails().getPlaylistItem().getResourceId().getVideoId()));
        }

        processVideos(videos, afterDate, beforeDate, activity, feed);
      } catch (Exception ex) {
        LOGGER.error("Error while trying to process activity: {}, {}", activity, ex);
      }
    }
  }

  /**
   * Process a list of Video objects.
   * @param videos     List of Video
   * @param afterDate  afterDate
   * @param beforeDate beforeDate
   * @param activity com.google.api.services.youtube.model.Activity
   * @param feed ActivityListResponse
   */
  void processVideos(List<Video> videos, DateTime afterDate, DateTime beforeDate, com.google.api.services.youtube.model.Activity activity, ActivityListResponse feed) {
    try {
      for (Video video : videos) {
        if (video != null) {
          org.joda.time.DateTime published = new org.joda.time.DateTime(video.getSnippet().getPublishedAt().getValue());
          if ((afterDate == null && beforeDate == null)
              || (beforeDate == null && afterDate.isBefore(published))
              || (afterDate == null && beforeDate.isAfter(published))
              || ((afterDate != null && beforeDate != null) && (afterDate.isAfter(published) && beforeDate.isBefore(published)))) {
            LOGGER.debug("Providing Youtube Activity: {}", MAPPER.writeValueAsString(video));
            this.datumQueue.put(new StreamsDatum(gson.toJson(video), activity.getId()));
          } else if (afterDate != null && afterDate.isAfter(published)) {
            feed.setNextPageToken(null); // do not fetch next page
            break;
          }
        }
      }
    } catch (Exception ex) {
      LOGGER.error("Exception while trying to process video list: {}, {}", videos, ex);
    }
  }

  /**
   * Given a Youtube videoId, return the relevant Youtube Video object.
   * @param videoId videoId
   * @return List of Videos
   * @throws IOException
   */
  List<Video> getVideoList(String videoId) throws IOException {
    VideoListResponse videosListResponse = this.youtube.videos().list("snippet,statistics")
        .setId(videoId)
        .setKey(config.getApiKey())
        .execute();

    if (videosListResponse.getItems().size() == 0) {
      LOGGER.debug("No Youtube videos found for videoId: {}", videoId);
      return new ArrayList<>();
    }

    return videosListResponse.getItems();
  }

  BlockingQueue<StreamsDatum> getDatumQueue() {
    return this.datumQueue;
  }
}