/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtNetwork module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#ifndef QT_NO_HTTP

#include <qoauth2authorizationcodeflow.h>
#include <private/qoauth2authorizationcodeflow_p.h>

#include <qmap.h>
#include <qurl.h>
#include <qvariant.h>
#include <qurlquery.h>
#include <qjsonobject.h>
#include <qjsondocument.h>
#include <qauthenticator.h>
#include <qoauthhttpserverreplyhandler.h>

#include <functional>

QT_BEGIN_NAMESPACE

QOAuth2AuthorizationCodeFlowPrivate::QOAuth2AuthorizationCodeFlowPrivate(
        QNetworkAccessManager *manager) : QAbstractOAuth2Private(manager)
{}

void QOAuth2AuthorizationCodeFlowPrivate::_q_handleCallback(const QVariantMap &data)
{
    Q_Q(QOAuth2AuthorizationCodeFlow);
    typedef QAbstractOAuth2Private::OAuth2KeyString Key;

    if (status != QAbstractOAuth::Status::NotAuthenticated) {
        qWarning("QOAuth2AuthorizationCodeFlow: Unexpected call");
        return;
    }

    Q_ASSERT(!state.isEmpty());

    const QString error = data.value(Key::error).toString();
    const QString code = data.value(Key::code).toString();
    const QString receivedState = data.value(Key::state).toString();
    if (error.size()) {
        const QString uri = data.value(Key::errorUri).toString();
        const QString description = data.value(Key::errorDescription).toString();
        qCritical("QOAuth2AuthorizationCodeFlow: AuthenticationError: %s(%s): %s",
                  qPrintable(error), qPrintable(uri), qPrintable(description));
        return;
    } else if (code.isEmpty()) {
        qCritical("QOAuth2AuthorizationCodeFlow: AuthenticationError: Code not received");
        return;
    } else if (receivedState.isEmpty()) {
        qCritical("QOAuth2AuthorizationCodeFlow: State not received");
        return;
    } else if (state != receivedState) {
        qCritical("QOAuth2AuthorizationCodeFlow: State mismatch");
        return;
    }

    setStatus(QAbstractOAuth::Status::TemporaryTokenReceived);

    QVariantMap copy(data);
    copy.remove(Key::code);
    extraTokens = copy;
    q->requestAccessToken(code);
}

void QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFinished(QNetworkReply *reply)
{
    Q_Q(QOAuth2AuthorizationCodeFlow);
    typedef QAbstractOAuth2Private::OAuth2KeyString Key;

    if (reply != currentReply)
        return;
    if (reply->error() == QNetworkReply::UnknownNetworkError) {
        qWarning("QOAuth2AuthorizationCodeFlow: %s", qPrintable(reply->errorString()));
        setStatus(QAbstractOAuth::Status::NotAuthenticated);
        return;
    }
    const QByteArray json = reply->readAll();
    Q_EMIT q->replyDataReceived(json);

    if (json.isEmpty()) {
        qCritical("QOAuth2AuthorizationCodeFlowPrivate::accessTokenRequestFinished: Empty answer");
        return;
    }

    qDebug("QOAuth2AuthorizationCodeFlowPrivate::accessTokenRequestFinished: %s", qPrintable(json));
    const auto document = QJsonDocument::fromJson(json);
    Q_ASSERT(document.isObject());
    const auto object = document.object();

    if (object.contains(Key::error)) {
        const QString error = object.value(Key::error).toString();
        qCritical("QOAuth2AuthorizationCodeFlow Error: %s", qPrintable(error));
        return;
    }

    const QString accessToken = object.value(Key::accessToken).toString();
    tokenType = object.value(Key::tokenType).toString();
    const int expiresIn = object.value(Key::expiresIn).toInt(-1);
    refreshToken = object.value(Key::refreshToken).toString();
    scope = object.value(Key::scope).toString();
    if (accessToken.isEmpty()) {
        qWarning("QOAuth2AuthorizationCodeFlow: Access token not received");
        return;
    }
    q->setToken(accessToken);
    expiresAt = expiresIn > 0 ? QDateTime::currentDateTime().addSecs(expiresIn) : QDateTime();

    setStatus(QAbstractOAuth::Status::Granted);
}

void QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate(QNetworkReply *reply,
                                                       QAuthenticator *authenticator)
{
    if (reply == currentReply){
        const auto url = reply->url();
        if (url == accessTokenUrl) {
            authenticator->setUser(clientCredentials.first);
            authenticator->setPassword(QString());
        }
    }
}

QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(QObject *parent) :
    QAbstractOAuth2(*new QOAuth2AuthorizationCodeFlowPrivate, parent)
{}

QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(QNetworkAccessManager *manager,
                                                           QObject *parent) :
    QAbstractOAuth2(*new QOAuth2AuthorizationCodeFlowPrivate(manager), parent)
{}

QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(const QString &clientIdentifier,
                                                           QNetworkAccessManager *manager,
                                                           QObject *parent) :
    QAbstractOAuth2(*new QOAuth2AuthorizationCodeFlowPrivate(manager), parent)
{
    Q_D(QOAuth2AuthorizationCodeFlow);
    d->clientCredentials.first = clientIdentifier;
}

QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(const QUrl &authenticateUrl,
                                                           const QUrl &accessTokenUrl,
                                                           QNetworkAccessManager *manager,
                                                           QObject *parent) :
    QAbstractOAuth2(*new QOAuth2AuthorizationCodeFlowPrivate(manager), parent)
{
    Q_D(QOAuth2AuthorizationCodeFlow);
    d->authorizationUrl = authenticateUrl;
    d->accessTokenUrl = accessTokenUrl;
}

QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(const QString &clientIdentifier,
                                                           const QUrl &authenticateUrl,
                                                           const QUrl &accessTokenUrl,
                                                           QNetworkAccessManager *manager,
                                                           QObject *parent) :
    QAbstractOAuth2(*new QOAuth2AuthorizationCodeFlowPrivate(manager), parent)
{
    Q_D(QOAuth2AuthorizationCodeFlow);
    d->authorizationUrl = authenticateUrl;
    d->accessTokenUrl = accessTokenUrl;
    d->clientCredentials.first = clientIdentifier;
}

QOAuth2AuthorizationCodeFlow::~QOAuth2AuthorizationCodeFlow()
{}

QString QOAuth2AuthorizationCodeFlow::responseType() const
{
    return QStringLiteral("code");
}

QUrl QOAuth2AuthorizationCodeFlow::accessTokenUrl() const
{
    Q_D(const QOAuth2AuthorizationCodeFlow);
    return d->accessTokenUrl;
}

void QOAuth2AuthorizationCodeFlow::setAccessTokenUrl(const QUrl &accessTokenUrl)
{
    Q_D(QOAuth2AuthorizationCodeFlow);
    if (d->accessTokenUrl != accessTokenUrl) {
        d->accessTokenUrl = accessTokenUrl;
        Q_EMIT accessTokenUrlChanged(accessTokenUrl);
    }
}

void QOAuth2AuthorizationCodeFlow::grant()
{
    Q_D(QOAuth2AuthorizationCodeFlow);
    if (d->authorizationUrl.isEmpty()) {
        qCritical("QOAuth2AuthorizationCodeFlow: No authenticate Url set");
        return;
    } else if (d->accessTokenUrl.isEmpty()) {
        qCritical("QOAuth2AuthorizationCodeFlow: No request access token Url set");
        return;
    }

    resourceOwnerAuthorization(d->authorizationUrl);
}

void QOAuth2AuthorizationCodeFlow::refreshAccessToken()
{
    Q_D(QOAuth2AuthorizationCodeFlow);

    if (d->refreshToken.isEmpty()) {
        qWarning("QOAuth2AuthorizationCodeFlow::refreshAccessToken: Cannot refresh access token. "
                 "Empty refresh token");
        return;
    }

    typedef QAbstractOAuth2Private::OAuth2KeyString Key;

    QVariantMap parameters;
    QNetworkRequest request(d->accessTokenUrl);
    QUrlQuery query;
    parameters.insert(Key::grantType, QStringLiteral("refresh_token"));
    parameters.insert(Key::refreshToken, d->refreshToken);
    parameters.insert(Key::redirectUri, QUrl::toPercentEncoding(callback()));
    query = QAbstractOAuthPrivate::createQuery(parameters);
    request.setHeader(QNetworkRequest::ContentTypeHeader,
                      QStringLiteral("application/x-www-form-urlencoded"));

    const QString data = query.toString(QUrl::FullyEncoded);
    d->currentReply = d->networkAccessManager()->post(request, data.toUtf8());

    QObjectPrivate::connect(d->networkAccessManager(), &QNetworkAccessManager::finished,
                            d, &QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFinished);
    QObjectPrivate::connect(d->networkAccessManager(),
                            &QNetworkAccessManager::authenticationRequired,
                            d, &QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate,
                            Qt::UniqueConnection);
}

QUrl QOAuth2AuthorizationCodeFlow::buildAuthenticateUrl(const QVariantMap &parameters)
{
    Q_D(QOAuth2AuthorizationCodeFlow);
    typedef QAbstractOAuth2Private::OAuth2KeyString Key;

    if (d->state.isEmpty())
        d->state = QAbstractOAuth2Private::generateRandomState();
    const QString state = d->state;

    QVariantMap p(parameters);
    QUrl url(d->authorizationUrl);
    p.insert(Key::responseType, responseType());
    p.insert(Key::clientIdentifier, d->clientCredentials.first);
    p.insert(Key::redirectUri, callback());
    p.insert(Key::scope, d->scope);
    p.insert(Key::state, state);
    p.insert(Key::apiKey, QVariant());
    if (d->modifyParametersFunction)
        d->modifyParametersFunction(Stage::RequestingAuthorization, &p);
    url.setQuery(d->createQuery(p));
    connect(d->replyHandler.data(), &QAbstractOAuthReplyHandler::callbackReceived, this,
            &QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived, Qt::UniqueConnection);
    setStatus(QAbstractOAuth::Status::NotAuthenticated);
    qDebug("QOAuth2AuthorizationCodeFlow::buildAuthenticateUrl: %s", qPrintable(url.toString()));
    return url;
}

void QOAuth2AuthorizationCodeFlow::requestAccessToken(const QString &code)
{
    Q_D(QOAuth2AuthorizationCodeFlow);
    typedef QAbstractOAuth2Private::OAuth2KeyString Key;

    QVariantMap parameters;
    QNetworkRequest request(d->accessTokenUrl);
    QUrlQuery query;
    parameters.insert(Key::grantType, QStringLiteral("authorization_code"));
    parameters.insert(Key::code, QUrl::toPercentEncoding(code));
    parameters.insert(Key::redirectUri, QUrl::toPercentEncoding(callback()));
    parameters.insert(Key::clientIdentifier, QUrl::toPercentEncoding(d->clientCredentials.first));
    if (!d->clientCredentials.second.isEmpty())
        parameters.insert(Key::clientSharedSecret, d->clientCredentials.second);
    if (d->modifyParametersFunction)
        d->modifyParametersFunction(Stage::RequestingAccessToken, &parameters);
    query = QAbstractOAuthPrivate::createQuery(parameters);
    request.setHeader(QNetworkRequest::ContentTypeHeader,
                      QStringLiteral("application/x-www-form-urlencoded"));

    const QString data = query.toString(QUrl::FullyEncoded);
    d->currentReply = d->networkAccessManager()->post(request, data.toUtf8());
    QObjectPrivate::connect(d->networkAccessManager(), &QNetworkAccessManager::finished,
                            d, &QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFinished);
    QObjectPrivate::connect(d->networkAccessManager(),
                            &QNetworkAccessManager::authenticationRequired,
                            d, &QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate,
                            Qt::UniqueConnection);
}

void QOAuth2AuthorizationCodeFlow::resourceOwnerAuthorization(const QUrl &url,
                                                              const QVariantMap &parameters)
{
    Q_D(QOAuth2AuthorizationCodeFlow);
    Q_ASSERT(url == d->authorizationUrl);
    const QUrl u = buildAuthenticateUrl(parameters);
    QObjectPrivate::connect(this, &QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived, d,
                            &QOAuth2AuthorizationCodeFlowPrivate::_q_handleCallback,
                            Qt::UniqueConnection);
    Q_EMIT authorizeWithBrowser(u);
}

QT_END_NAMESPACE

#endif // QT_NO_HTTP