/* This file is part of Clementine.
   Copyright 2013-2014, John Maguire <john.maguire@gmail.com>
   Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>

   Clementine is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   Clementine is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "boxservice.h"

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrlQuery>

#include "core/application.h"
#include "core/player.h"
#include "core/waitforsignal.h"
#include "internet/box/boxurlhandler.h"
#include "internet/core/oauthenticator.h"
#include "library/librarybackend.h"
#include "ui/iconloader.h"

const char* BoxService::kServiceName = "Box";
const char* BoxService::kSettingsGroup = "Box";

namespace {

static const char* kClientId = "gbswb9wp7gjyldc3qrw68h2rk68jaf4h";
static const char* kClientSecret = "pZ6cUCQz5X0xaWoPVbCDg6GpmfTtz73s";

static const char* kOAuthEndpoint = "https://api.box.com/oauth2/authorize";
static const char* kOAuthTokenEndpoint = "https://api.box.com/oauth2/token";

static const char* kUserInfo = "https://api.box.com/2.0/users/me";
static const char* kFolderItems = "https://api.box.com/2.0/folders/%1/items";
static const int kRootFolderId = 0;

static const char* kFileContent = "https://api.box.com/2.0/files/%1/content";

static const char* kEvents = "https://api.box.com/2.0/events";
}  // namespace

BoxService::BoxService(Application* app, InternetModel* parent)
    : CloudFileService(app, parent, kServiceName, kSettingsGroup,
                       IconLoader::Load("box", IconLoader::Provider),
                       SettingsDialog::Page_Box) {
  app->player()->RegisterUrlHandler(new BoxUrlHandler(this, this));
}

bool BoxService::has_credentials() const { return !refresh_token().isEmpty(); }

QString BoxService::refresh_token() const {
  QSettings s;
  s.beginGroup(kSettingsGroup);

  return s.value("refresh_token").toString();
}

bool BoxService::is_authenticated() const {
  return !access_token_.isEmpty() &&
         QDateTime::currentDateTime().secsTo(expiry_time_) > 0;
}

void BoxService::EnsureConnected() {
  if (is_authenticated()) {
    return;
  }

  Connect();
  WaitForSignal(this, SIGNAL(Connected()));
}

void BoxService::Connect() {
  OAuthenticator* oauth = new OAuthenticator(
      kClientId, kClientSecret, OAuthenticator::RedirectStyle::REMOTE, this);
  if (!refresh_token().isEmpty()) {
    oauth->RefreshAuthorisation(kOAuthTokenEndpoint, refresh_token());
  } else {
    oauth->StartAuthorisation(kOAuthEndpoint, kOAuthTokenEndpoint, QString());
  }

  NewClosure(oauth, SIGNAL(Finished()), this,
             SLOT(ConnectFinished(OAuthenticator*)), oauth);
}

void BoxService::ConnectFinished(OAuthenticator* oauth) {
  oauth->deleteLater();

  QSettings s;
  s.beginGroup(kSettingsGroup);
  s.setValue("refresh_token", oauth->refresh_token());

  access_token_ = oauth->access_token();
  expiry_time_ = oauth->expiry_time();

  if (s.value("name").toString().isEmpty()) {
    QUrl url(kUserInfo);
    QNetworkRequest request(url);
    AddAuthorizationHeader(&request);

    QNetworkReply* reply = network_->get(request);
    NewClosure(reply, SIGNAL(finished()), this,
               SLOT(FetchUserInfoFinished(QNetworkReply*)), reply);
  } else {
    emit Connected();
  }
  UpdateFiles();
}

void BoxService::AddAuthorizationHeader(QNetworkRequest* request) const {
  request->setRawHeader("Authorization",
                        QString("Bearer %1").arg(access_token_).toUtf8());
}

void BoxService::FetchUserInfoFinished(QNetworkReply* reply) {
  reply->deleteLater();

  QJsonObject json_response =
      QJsonDocument::fromJson(reply->readAll()).object();

  QString name = json_response["name"].toString();
  if (!name.isEmpty()) {
    QSettings s;
    s.beginGroup(kSettingsGroup);
    s.setValue("name", name);
  }

  emit Connected();
}

void BoxService::ForgetCredentials() {
  QSettings s;
  s.beginGroup(kSettingsGroup);

  s.remove("refresh_token");
  s.remove("name");
}

void BoxService::UpdateFiles() {
  QSettings s;
  s.beginGroup(kSettingsGroup);

  if (!s.value("cursor").toString().isEmpty()) {
    // Use events API to fetch changes.
    UpdateFilesFromCursor(s.value("cursor").toString());
    return;
  }

  // First run we scan as events may not cover everything.
  FetchRecursiveFolderItems(kRootFolderId);
  InitialiseEventsCursor();
}

void BoxService::InitialiseEventsCursor() {
  QUrl url(kEvents);
  QUrlQuery url_query;
  url_query.addQueryItem("stream_position", "now");
  url.setQuery(url_query);
  QNetworkRequest request(url);
  AddAuthorizationHeader(&request);
  QNetworkReply* reply = network_->get(request);
  NewClosure(reply, SIGNAL(finished()), this,
             SLOT(InitialiseEventsFinished(QNetworkReply*)), reply);
}

void BoxService::InitialiseEventsFinished(QNetworkReply* reply) {
  reply->deleteLater();
  QJsonObject json_response =
      QJsonDocument::fromJson(reply->readAll()).object();
  if (json_response.contains("next_stream_position")) {
    QSettings s;
    s.beginGroup(kSettingsGroup);
    s.setValue("cursor", json_response["next_stream_position"].toString());
  }
}

void BoxService::FetchRecursiveFolderItems(const int folder_id,
                                           const int offset) {
  QUrl url(QString(kFolderItems).arg(folder_id));
  QStringList fields;
  fields << "etag"
         << "size"
         << "created_at"
         << "modified_at"
         << "name";
  QString fields_list = fields.join(",");
  QUrlQuery url_query(url);
  url_query.addQueryItem("fields", fields_list);
  url_query.addQueryItem("limit", "1000");  // Maximum according to API docs.
  url_query.addQueryItem("offset", QString::number(offset));
  url.setQuery(url_query);
  QNetworkRequest request(url);
  AddAuthorizationHeader(&request);
  QNetworkReply* reply = network_->get(request);
  NewClosure(reply, SIGNAL(finished()), this,
             SLOT(FetchFolderItemsFinished(QNetworkReply*, int)), reply,
             folder_id);
}

void BoxService::FetchFolderItemsFinished(QNetworkReply* reply,
                                          const int folder_id) {
  reply->deleteLater();

  QJsonObject json_response =
      QJsonDocument::fromJson(reply->readAll()).object();

  QJsonArray entries = json_response["entries"].toArray();
  const int total_entries = json_response["total_count"].toInt();
  const int offset = json_response["offset"].toInt();
  if (entries.size() + offset < total_entries) {
    // Fetch the next page if necessary.
    FetchRecursiveFolderItems(folder_id, offset + entries.size());
  }

  for (const QJsonValue& e : entries) {
    QJsonObject entry = e.toObject();
    if (entry["type"].toString() == "folder") {
      FetchRecursiveFolderItems(entry["id"].toInt());
    } else {
      MaybeAddFileEntry(entry);
    }
  }
}

void BoxService::MaybeAddFileEntry(const QJsonObject& entry) {
  QString mime_type = GuessMimeTypeForFile(entry["name"].toString());
  QUrl url;
  url.setScheme("box");
  url.setPath("/" + entry["id"].toString());

  Song song;
  song.set_url(url);
  song.set_ctime(
      QDateTime::fromString(entry["created_at"].toString()).toTime_t());
  song.set_mtime(
      QDateTime::fromString(entry["modified_at"].toString()).toTime_t());
  song.set_filesize(entry["size"].toInt());
  song.set_title(entry["name"].toString());

  // This is actually a redirect. Follow it now.
  QNetworkReply* reply = FetchContentUrlForFile(entry["id"].toString());
  NewClosure(reply, SIGNAL(finished()), this,
             SLOT(RedirectFollowed(QNetworkReply*, Song, QString)), reply, song,
             mime_type);
}

QNetworkReply* BoxService::FetchContentUrlForFile(const QString& file_id) {
  QUrl content_url(QString(kFileContent).arg(file_id));
  QNetworkRequest request(content_url);
  AddAuthorizationHeader(&request);
  QNetworkReply* reply = network_->get(request);
  return reply;
}

void BoxService::RedirectFollowed(QNetworkReply* reply, const Song& song,
                                  const QString& mime_type) {
  reply->deleteLater();
  QVariant redirect =
      reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
  if (!redirect.isValid()) {
    return;
  }

  QUrl real_url = redirect.toUrl();
  MaybeAddFileToDatabase(song, mime_type, real_url,
                         QString("Bearer %1").arg(access_token_));
}

void BoxService::UpdateFilesFromCursor(const QString& cursor) {
  QUrl url(kEvents);
  QUrlQuery url_query;
  url_query.addQueryItem("stream_position", cursor);
  url_query.addQueryItem("limit", "5000");
  url.setQuery(url_query);
  QNetworkRequest request(url);
  AddAuthorizationHeader(&request);
  QNetworkReply* reply = network_->get(request);
  NewClosure(reply, SIGNAL(finished()), this,
             SLOT(FetchEventsFinished(QNetworkReply*)), reply);
}

void BoxService::FetchEventsFinished(QNetworkReply* reply) {
  // TODO(John Maguire): Page through events.
  reply->deleteLater();
  QJsonObject json_response =
      QJsonDocument::fromJson(reply->readAll()).object();

  QSettings s;
  s.beginGroup(kSettingsGroup);
  s.setValue("cursor", json_response["next_stream_position"].toString());

  QJsonArray entries = json_response["entries"].toArray();
  for (const QJsonValue& e : entries) {
    QJsonObject event = e.toObject();
    QString type = event["event_type"].toString();
    QJsonObject source = event["source"].toObject();
    if (source["type"] == "file") {
      if (type == "ITEM_UPLOAD") {
        // Add file.
        MaybeAddFileEntry(source);
      } else if (type == "ITEM_TRASH") {
        // Delete file.
        QUrl url;
        url.setScheme("box");
        url.setPath("/" + source["id"].toString());
        Song song = library_backend_->GetSongByUrl(url);
        if (song.is_valid()) {
          library_backend_->DeleteSongs(SongList() << song);
        }
      }
    }
  }
}

QUrl BoxService::GetStreamingUrlFromSongId(const QString& id) {
  EnsureConnected();
  QNetworkReply* reply = FetchContentUrlForFile(id);
  WaitForSignal(reply, SIGNAL(finished()));
  reply->deleteLater();
  QUrl real_url =
      reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
  return real_url;
}
