// For license of this file, see <project-root-folder>/LICENSE.md.

////////////////////////////////////////////////////////////////////////////////

//                                                                            //
// This file is part of QOAuth2.                                              //
// Copyright (c) 2014 Jacob Dawid <jacob@omg-it.works>                        //
//                                                                            //
// QOAuth2 is free software: you can redistribute it and/or modify            //
// it under the terms of the GNU Affero General Public License as             //
// published by the Free Software Foundation, either version 3 of the         //
// License, or (at your option) any later version.                            //
//                                                                            //
// QOAuth2 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 Affero General Public License for more details.                        //
//                                                                            //
// You should have received a copy of the GNU Affero General Public           //
// License along with QOAuth2.                                                //
// If not, see <http://www.gnu.org/licenses/>.                                //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

#include "network-web/oauth2service.h"

#include "definitions/definitions.h"
#include "miscellaneous/application.h"
#include "network-web/networkfactory.h"
#include "network-web/oauthhttphandler.h"
#include "network-web/webfactory.h"

#include <cstdlib>
#include <utility>

#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRandomGenerator>

OAuth2Service::OAuth2Service(const QString& auth_url,
                             const QString& token_url,
                             const QString& client_id,
                             const QString& client_secret,
                             const QString& scope,
                             QObject* parent)
  : QObject(parent), m_id(QString::number(QRandomGenerator::global()->generate())), m_timerId(-1),
    m_redirectionHandler(new OAuthHttpHandler(tr("You can close this window now. Go back to %1.").arg(QSL(APP_NAME)),
                                              this)),
    m_functorOnLogin(std::function<void()>()) {
  m_tokenGrantType = QSL("authorization_code");
  m_tokenUrl = QUrl(token_url);
  m_authUrl = auth_url;

  m_clientId = client_id;
  m_clientSecret = client_secret;
  m_clientSecretId = m_clientSecretSecret = QString();
  m_scope = scope;
  m_useHttpBasicAuthWithClientData = false;

  connect(&m_networkManager, &QNetworkAccessManager::finished, this, &OAuth2Service::tokenRequestFinished);
  connect(m_redirectionHandler, &OAuthHttpHandler::authGranted, [this](const QString& auth_code, const QString& id) {
    if (id.isEmpty() || id == m_id) {
      // We process this further only if handler (static singleton) responded to our original request.
      retrieveAccessToken(auth_code);
    }
  });
  connect(m_redirectionHandler,
          &OAuthHttpHandler::authRejected,
          [this](const QString& error_description, const QString& id) {
            Q_UNUSED(error_description)

            if (id.isEmpty() || id == m_id) {
              // We process this further only if handler (static singleton) responded to our original request.
              emit authFailed();
            }
          });
}

OAuth2Service::~OAuth2Service() {
  qDebugNN << LOGSEC_OAUTH << "Destroying OAuth2Service instance.";
}

QString OAuth2Service::bearer() {
  if (!isFullyLoggedIn()) {
    qApp->showGuiMessage(Notification::Event::LoginFailure,
                         {tr("You have to login first"),
                          tr("Click here to login."),
                          QSystemTrayIcon::MessageIcon::Critical},
                         {},
                         {tr("Login"), [this]() {
                            login();
                          }});
    return {};
  }
  else {
    return QSL("Bearer %1").arg(accessToken());
  }
}

bool OAuth2Service::isFullyLoggedIn() const {
  bool is_expiration_valid = tokensExpireIn() > QDateTime::currentDateTime();
  bool do_tokens_exist = !refreshToken().isEmpty() && !accessToken().isEmpty();

  return is_expiration_valid && do_tokens_exist;
}

void OAuth2Service::setOAuthTokenGrantType(QString grant_type) {
  m_tokenGrantType = std::move(grant_type);
}

QString OAuth2Service::oAuthTokenGrantType() {
  return m_tokenGrantType;
}

void OAuth2Service::timerEvent(QTimerEvent* event) {
  if (m_timerId >= 0 && event->timerId() == m_timerId) {
    event->accept();

    QDateTime window_about_expire = tokensExpireIn().addSecs(-60 * 15);

    if (window_about_expire < QDateTime::currentDateTime()) {
      // We try to refresh access token, because it probably expires soon.
      qDebugNN << LOGSEC_OAUTH << "Refreshing automatically access token.";
      refreshAccessToken();
    }
    else {
      qDebugNN << LOGSEC_OAUTH << "Access token is not expired yet.";
    }
  }

  QObject::timerEvent(event);
}

bool OAuth2Service::useHttpBasicAuthWithClientData() const {
  return m_useHttpBasicAuthWithClientData;
}

void OAuth2Service::setUseHttpBasicAuthWithClientData(bool use_auth) {
  m_useHttpBasicAuthWithClientData = use_auth;
}

QString OAuth2Service::clientSecretSecret() const {
  return m_clientSecretSecret;
}

void OAuth2Service::setClientSecretSecret(const QString& client_secret_secret) {
  m_clientSecretSecret = client_secret_secret;
}

QString OAuth2Service::clientSecretId() const {
  return m_clientSecretId;
}

void OAuth2Service::setClientSecretId(const QString& client_secret_id) {
  m_clientSecretId = client_secret_id;
}

QString OAuth2Service::id() const {
  return m_id;
}

void OAuth2Service::setId(const QString& id) {
  m_id = id;
}

void OAuth2Service::retrieveAccessToken(const QString& auth_code) {
  QNetworkRequest network_request;

  network_request.setUrl(m_tokenUrl);
  network_request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");

  if (m_useHttpBasicAuthWithClientData) {
    auto basic_auth = NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic,
                                                              properClientId(),
                                                              properClientSecret());

    network_request.setRawHeader(basic_auth.first, basic_auth.second);
  }

  QString content = QString("client_id=%1&"
                            "client_secret=%2&"
                            "code=%3&"
                            "redirect_uri=%5&"
                            "grant_type=%4")
                      .arg(properClientId(),
                           properClientSecret(),
                           auth_code,
                           m_tokenGrantType,
                           m_redirectionHandler->listenAddressPort());

  qDebugNN << LOGSEC_OAUTH << "Posting data for access token retrieval:" << QUOTE_W_SPACE_DOT(content);
  m_networkManager.post(network_request, content.toUtf8());
}

void OAuth2Service::refreshAccessToken(const QString& refresh_token) {
  auto real_refresh_token = refresh_token.isEmpty() ? refreshToken() : refresh_token;
  QNetworkRequest network_request;

  network_request.setUrl(m_tokenUrl);
  network_request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded");

  if (m_useHttpBasicAuthWithClientData) {
    auto basic_auth = NetworkFactory::generateBasicAuthHeader(NetworkFactory::NetworkAuthentication::Basic,
                                                              properClientId(),
                                                              properClientSecret());

    network_request.setRawHeader(basic_auth.first, basic_auth.second);
  }

  QString content = QString("client_id=%1&"
                            "client_secret=%2&"
                            "refresh_token=%3&"
                            "grant_type=%4")
                      .arg(properClientId(), properClientSecret(), real_refresh_token, QSL("refresh_token"));

  qApp->showGuiMessage(Notification::Event::LoginDataRefreshed,
                       {tr("Logging in via OAuth 2.0..."),
                        tr("Refreshing login tokens for '%1'...").arg(m_tokenUrl.toString()),
                        QSystemTrayIcon::MessageIcon::Information},
                       {true, false, true});

  qDebugNN << LOGSEC_OAUTH << "Posting data for access token refreshing:" << QUOTE_W_SPACE_DOT(content);
  m_networkManager.post(network_request, content.toUtf8());
}

void OAuth2Service::tokenRequestFinished(QNetworkReply* network_reply) {
  QByteArray repl = network_reply->readAll();
  QJsonDocument json_document = QJsonDocument::fromJson(repl);
  QJsonObject root_obj = json_document.object();

  qDebugNN << LOGSEC_OAUTH << "Token response:" << QUOTE_W_SPACE_DOT(QString::fromUtf8(json_document.toJson()));

  if (network_reply->error() != QNetworkReply::NetworkError::NoError) {
    qWarningNN << LOGSEC_OAUTH
               << "Network error when obtaining token response:" << QUOTE_W_SPACE_DOT(network_reply->error());

    emit tokensRetrieveError(QString(), NetworkFactory::networkErrorText(network_reply->error()));
  }
  else if (root_obj.keys().contains(QSL("error"))) {
    QString error = root_obj.value(QSL("error")).toString();
    QString error_description = root_obj.value(QSL("error_description")).toString();

    qWarningNN << LOGSEC_OAUTH << "JSON error when obtaining token response:" << QUOTE_W_SPACE(error)
               << QUOTE_W_SPACE_DOT(error_description);

    logout();

    emit tokensRetrieveError(error, error_description);
  }
  else {
    int expires = root_obj.value(QL1S("expires_in")).toInt();

    setTokensExpireIn(QDateTime::currentDateTime().addSecs(expires));
    setAccessToken(root_obj.value(QL1S("access_token")).toString());

    const QString refresh_token = root_obj.value(QL1S("refresh_token")).toString();

    if (!refresh_token.isEmpty()) {
      setRefreshToken(refresh_token);
    }

    qDebugNN << LOGSEC_OAUTH << "Obtained refresh token" << QUOTE_W_SPACE(refreshToken()) << "- expires on date/time"
             << QUOTE_W_SPACE_DOT(tokensExpireIn());

    if (m_functorOnLogin != nullptr) {
      qDebugNN << LOGSEC_OAUTH << "Running custom after-login code.";

      m_functorOnLogin();
    }

    emit tokensRetrieved(accessToken(), refreshToken(), expires);
  }

  network_reply->deleteLater();
}

QString OAuth2Service::properClientId() const {
  return m_clientId.simplified().isEmpty() ? m_clientSecretId : m_clientId;
}

QString OAuth2Service::properClientSecret() const {
  return m_clientSecret.simplified().isEmpty() ? m_clientSecretSecret : m_clientSecret;
}

QString OAuth2Service::accessToken() const {
  return m_accessToken;
}

void OAuth2Service::setAccessToken(const QString& access_token) {
  m_accessToken = access_token;
}

QDateTime OAuth2Service::tokensExpireIn() const {
  return m_tokensExpireIn;
}

void OAuth2Service::setTokensExpireIn(const QDateTime& tokens_expire_in) {
  m_tokensExpireIn = tokens_expire_in;
}

QString OAuth2Service::clientSecret() const {
  return m_clientSecret;
}

void OAuth2Service::setClientSecret(const QString& client_secret) {
  m_clientSecret = client_secret;
}

QString OAuth2Service::clientId() const {
  return m_clientId;
}

void OAuth2Service::setClientId(const QString& client_id) {
  m_clientId = client_id;
}

QString OAuth2Service::redirectUrl() const {
  return m_redirectionHandler->listenAddressPort();
}

void OAuth2Service::setRedirectUrl(const QString& redirect_url, bool start_handler) {
  m_redirectionHandler->setListenAddressPort(redirect_url, start_handler);
}

QString OAuth2Service::refreshToken() const {
  return m_refreshToken;
}

void OAuth2Service::setRefreshToken(const QString& refresh_token) {
  killRefreshTimer();
  m_refreshToken = refresh_token;
  startRefreshTimer();
}

bool OAuth2Service::login(const std::function<void()>& functor_when_logged_in) {
  m_functorOnLogin = functor_when_logged_in;

  if (!m_redirectionHandler->isListening()) {
    qCriticalNN << LOGSEC_OAUTH << "Cannot log-in because OAuth redirection handler is not listening.";

    emit tokensRetrieveError(QString(),
                             tr("Failed to start OAuth "
                                "redirection listener. Maybe your "
                                "rights are not high enough."));

    return false;
  }

  bool did_token_expire = tokensExpireIn().isNull() || tokensExpireIn() < QDateTime::currentDateTime().addSecs(-120);
  bool does_token_exist = !refreshToken().isEmpty();

  // We refresh current tokens only if:
  //   1. We have some existing refresh token.
  //   AND
  //   2. We do not know its expiration date or it passed.
  if (does_token_exist && did_token_expire) {
    refreshAccessToken();
    return false;
  }
  else if (!does_token_exist) {
    retrieveAuthCode();
    return false;
  }
  else {
    functor_when_logged_in();
    return true;
  }
}

void OAuth2Service::logout(bool stop_redirection_handler) {
  setTokensExpireIn(QDateTime());
  setAccessToken(QString());
  setRefreshToken(QString());

  qDebugNN << LOGSEC_OAUTH << "Clearing tokens.";

  if (stop_redirection_handler) {
    m_redirectionHandler->stop();
  }
}

void OAuth2Service::startRefreshTimer() {
  if (!refreshToken().isEmpty()) {
    m_timerId = startTimer(1000 * 60 * 15, Qt::TimerType::VeryCoarseTimer);
  }
}

void OAuth2Service::killRefreshTimer() {
  if (m_timerId > 0) {
    killTimer(m_timerId);
  }
}

void OAuth2Service::retrieveAuthCode() {
  QString auth_url = m_authUrl + QString("?client_id=%1&"
                                         "scope=%2&"
                                         "redirect_uri=%3&"
                                         "response_type=code&"
                                         "state=%4&"
                                         "prompt=consent&"
                                         "duration=permanent&"
                                         "access_type=offline")
                                   .arg(properClientId(), m_scope, m_redirectionHandler->listenAddressPort(), m_id);

  // We run login URL in external browser, response is caught by light HTTP server.
  qApp->web()->openUrlInExternalBrowser(auth_url);
}
