// Copyright (c) 2025-2025 Manuel Schneider

#include "systemutil.h"
#include "logging.h"
#include "networkutil.h"
#include "oauth.h"
#include <QByteArray>
#include <QCryptographicHash>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRandomGenerator>
#include <QTimer>
#include <QUrl>
#include <QUrlQuery>
using enum albert::OAuth2::State;
using namespace albert;
using namespace std;

static QString generateRandomString(int length) {
    static const QString chars = QStringLiteral("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                                                "abcdefghijklmnopqrstuvwxyz"
                                                "0123456789");
    QString result;
    result.reserve(length);
    for (int i = 0; i < length; ++i) {
        int idx = QRandomGenerator::global()->bounded(chars.size());
        result.append(chars[idx]);
    }
    return result;
}

static QString generateCodeChallenge(const QString &code_verifier) {
    QByteArray hash = QCryptographicHash::hash(code_verifier.toUtf8(), QCryptographicHash::Sha256);
    QByteArray b64 = hash.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
    return QString::fromUtf8(b64);
}

// -------------------------------------------------------------------------------------------------

class OAuth2::Private
{
public:
    OAuth2 *q;

    QString client_id;
    QString client_secret;
    QString scope;
    QString token_url;

    // Stage 1 Grant permissions

    QString auth_url;
    QString redirect_uri;
    QString code_verifier;
    QString state_string;
    bool pkce = true;

    // Stage 2 Authorize access

    QString refresh_token;
    QString access_token;
    QDateTime token_expiration;
    QTimer token_refresh_timer;

    QString error;

    void requestAuthorization(); // 4.1.1
    void handleAutorizationResponse(const QUrl &callback_url); // 4.1.2
    void requestAccessToken(const QString &code); // 4.1.3
    void handleAccessTokenResponse(QNetworkReply *reply); // 4.1.4

    void refreshAccessToken();
    void parseTokenReply(QNetworkReply *reply);
};

void OAuth2::Private::requestAuthorization()
{
    // Authorization Request - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
    //
    // response_type
    //         REQUIRED.  Value MUST be set to "code".
    // client_id
    //         REQUIRED.  The client identifier as described in Section 2.2.
    // redirect_uri
    //         OPTIONAL.  As described in Section 3.1.2.
    // scope
    //         OPTIONAL.  The scope of the access request as described by
    //         Section 3.3.
    // state
    //         RECOMMENDED.  An opaque value used by the client to maintain
    //         state between the request and callback.  The authorization
    //         server includes this value when redirecting the user-agent back
    //         to the client.  The parameter SHOULD be used for preventing
    //         cross-site request forgery as described in Section 10.12.

    state_string = generateRandomString(8);

    QUrlQuery query;
    query.addQueryItem("response_type", "code");
    query.addQueryItem("client_id", client_id);
    query.addQueryItem("scope", scope);
    query.addQueryItem("redirect_uri", redirect_uri);
    query.addQueryItem("state", state_string);
    if (pkce)
    {
        code_verifier = generateRandomString(64);
        auto code_challenge = generateCodeChallenge(code_verifier);
        query.addQueryItem("code_challenge_method", "S256");
        query.addQueryItem("code_challenge", code_challenge);
    }

    QUrl url(auth_url);
    url.setQuery(query);
    open(url);

    emit q->stateChanged(Awaiting);
}

void OAuth2::Private::handleAutorizationResponse(const QUrl &callback_url)
{
    // Authorization Response
    //
    // SUCCESS - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
    //
    // code
    //          REQUIRED.  The authorization code generated by the
    //          authorization server.  The authorization code MUST expire
    //          shortly after it is issued to mitigate the risk of leaks.  A
    //          maximum authorization code lifetime of 10 minutes is
    //          RECOMMENDED.  The client MUST NOT use the authorization code
    //          more than once.  If an authorization code is used more than
    //          once, the authorization server MUST deny the request and SHOULD
    //          revoke (when possible) all tokens previously issued based on
    //          that authorization code.  The authorization code is bound to
    //          the client identifier and redirection URI.
    // state
    //          REQUIRED if the "state" parameter was present in the client
    //          authorization request.  The exact value received from the
    //          client.
    //
    // ERROR - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
    //
    // error
    //          REQUIRED.  A single ASCII [USASCII] error code from the
    //          following:
    //
    //      invalid_request
    //               The request is missing a required parameter, includes an
    //               invalid parameter value, includes a parameter more than
    //               once, or is otherwise malformed.
    //      unauthorized_client
    //               The client is not authorized to request an authorization
    //               code using this method.
    //      access_denied
    //               The resource owner or authorization server denied the
    //               request.
    //      unsupported_response_type
    //               The authorization server does not support obtaining an
    //               authorization code using this method.
    //      invalid_scope
    //               The requested scope is invalid, unknown, or malformed.
    //      server_error
    //               The authorization server encountered an unexpected
    //               condition that prevented it from fulfilling the request.
    //               (This error code is needed because a 500 Internal Server
    //               Error HTTP status code cannot be returned to the client
    //               via an HTTP redirect.)
    //      temporarily_unavailable
    //               The authorization server is currently unable to handle
    //               the request due to a temporary overloading or maintenance
    //               of the server.  (This error code is needed because a 503
    //               Service Unavailable HTTP status code cannot be returned
    //               to the client via an HTTP redirect.)
    //
    // error_description
    //          OPTIONAL.  Human-readable ASCII [USASCII] text providing
    //          additional information, used to assist the client developer in
    //          understanding the error that occurred.
    //          Values for the "error_description" parameter MUST NOT include
    //          characters outside the set %x20-21 / %x23-5B / %x5D-7E.
    // error_uri
    //          OPTIONAL.  A URI identifying a human-readable web page with
    //          information about the error, used to provide the client
    //          developer with additional information about the error.
    //          Values for the "error_uri" parameter MUST conform to the
    //          URI-reference syntax and thus MUST NOT include characters
    //          outside the set %x21 / %x23-5B / %x5D-7E.
    // state
    //          REQUIRED if a "state" parameter was present in the client
    //          authorization request.  The exact value received from the
    //          client.

    QUrlQuery url_query(callback_url.query());

    if (state_string.isEmpty() || url_query.queryItemValue("state") != state_string)
    {
        WARN << "Received unexpected authorization response.";
        return;
    }

    state_string.clear();

    if (url_query.hasQueryItem("code"))
        requestAccessToken(url_query.queryItemValue("code"));

    else if (url_query.hasQueryItem("error"))
    {
        error = url_query.queryItemValue("error");
        if (url_query.hasQueryItem("error_description"))
            error += ": " + url_query.queryItemValue("error_description");
        if (url_query.hasQueryItem("error_uri"))
            error += " (" + url_query.queryItemValue("error_uri") + ")";
        emit q->stateChanged(NotAuthorized);
    }

    else
    {
        error = "Neither 'code' nor 'error' set authorization response.";
        emit q->stateChanged(NotAuthorized);
    }
}

void OAuth2::Private::requestAccessToken(const QString & code)
{
    // Access Token Request - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
    //
    // grant_type
    //       REQUIRED.  Value MUST be set to "authorization_code".
    // code
    //       REQUIRED.  The authorization code received from the
    //       authorization server.
    // redirect_uri
    //       REQUIRED, if the "redirect_uri" parameter was included in the
    //       authorization request as described in Section 4.1.1, and their
    //       values MUST be identical.
    // client_id
    //       REQUIRED, if the client is not authenticating with the
    //       authorization server as described in Section 3.2.1.
    QUrlQuery params;
    params.addQueryItem("grant_type", "authorization_code");
    params.addQueryItem("code", code);
    params.addQueryItem("redirect_uri", redirect_uri);
    QNetworkRequest request(token_url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
    request.setRawHeader("Accept", "application/json");
    if (pkce)
    {
        params.addQueryItem("client_id", client_id);
        params.addQueryItem("code_verifier", code_verifier);
    }
    else
    {
        const auto base64 = QString("%1:%2").arg(client_id, client_secret).toUtf8().toBase64();
        request.setRawHeader(QByteArray("Authorization"), QString("Basic %1").arg(base64).toUtf8());
    }

    QNetworkReply *reply = network().post(request, params.toString(QUrl::FullyEncoded).toUtf8());

    QObject::connect(reply, &QNetworkReply::finished, q, [this, reply] {
        handleAccessTokenResponse(reply);
        reply->deleteLater();
    });
}

void OAuth2::Private::handleAccessTokenResponse(QNetworkReply *reply)
{
    // Access Token Response - https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4
    //
    // SUCCESS - https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
    //
    // {
    //     access_token: 'BQBLuPRYBQ...BP8stIv5xr-Iwaf4l8eg',
    //     token_type: 'Bearer',
    //     expires_in: 3600,
    //     refresh_token: 'AQAQfyEFmJJuCvAFh...cG_m-2KTgNDaDMQqjrOa3',
    //     scope: 'user-read-email user-read-private'
    // }
    //
    // access_token
    //       REQUIRED.  The access token issued by the authorization server.
    // token_type
    //       REQUIRED.  The type of the token issued as described in
    //       Section 7.1.  Value is case insensitive.
    // expires_in
    //       RECOMMENDED.  The lifetime in seconds of the access token.  For
    //       example, the value "3600" denotes that the access token will
    //       expire in one hour from the time the response was generated.
    //       If omitted, the authorization server SHOULD provide the
    //       expiration time via other means or document the default value.
    // refresh_token
    //       OPTIONAL.  The refresh token, which can be used to obtain new
    //       access tokens using the same authorization grant as described
    //       in Section 6.
    // scope
    //       OPTIONAL, if identical to the scope requested by the client;
    //       otherwise, REQUIRED.  The scope of the access token as
    //       described by Section 3.3.
    //
    // ERROR - https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
    //
    // {
    //     "error":"incorrect_client_credentials",
    //     "error_description":"The client_id and/or client_secret passed are incorrect.",
    //     "error_uri":"https:…",
    //     "token_type":null
    // }
    //
    // error
    //       REQUIRED.  A single ASCII [USASCII] error code from the
    //       following:
    //       invalid_request
    //             The request is missing a required parameter, includes an
    //             unsupported parameter value (other than grant type),
    //             repeats a parameter, includes multiple credentials,
    //             utilizes more than one mechanism for authenticating the
    //             client, or is otherwise malformed.
    //       invalid_client
    //             Client authentication failed (e.g., unknown client, no
    //             client authentication included, or unsupported
    //             authentication method).  The authorization server MAY
    //             return an HTTP 401 (Unauthorized) status code to indicate
    //             which HTTP authentication schemes are supported.  If the
    //             client attempted to authenticate via the "Authorization"
    //             request header field, the authorization server MUST
    //             respond with an HTTP 401 (Unauthorized) status code and
    //             include the "WWW-Authenticate" response header field
    //             matching the authentication scheme used by the client.
    //       invalid_grant
    //             The provided authorization grant (e.g., authorization
    //             code, resource owner credentials) or refresh token is
    //             invalid, expired, revoked, does not match the redirection
    //             URI used in the authorization request, or was issued to
    //             another client.
    //       unauthorized_client
    //             The authenticated client is not authorized to use this
    //             authorization grant type.
    //       unsupported_grant_type
    //             The authorization grant type is not supported by the
    //             authorization server.
    //       invalid_scope
    //             The requested scope is invalid, unknown, malformed, or
    //             exceeds the scope granted by the resource owner.
    //       Values for the "error" parameter MUST NOT include characters
    //       outside the set %x20-21 / %x23-5B / %x5D-7E.
    // error_description
    //       OPTIONAL.  Human-readable ASCII [USASCII] text providing
    //       additional information, used to assist the client developer in
    //       understanding the error that occurred.
    //       Values for the "error_description" parameter MUST NOT include
    //       characters outside the set %x20-21 / %x23-5B / %x5D-7E.
    // error_uri
    //       OPTIONAL.  A URI identifying a human-readable web page with
    //       information about the error, used to provide the client
    //       developer with additional information about the error.
    //       Values for the "error_uri" parameter MUST conform to the
    //       URI-reference syntax and thus MUST NOT include characters
    //       outside the set %x21 / %x23-5B / %x5D-7E.


    error.clear();
    QJsonParseError parseError;

    if ((int)reply->error() == 302)  // Grant invalid/revoked
    {
        refresh_token.clear();

        error = QString("%1 (%2) - %3 %4")
                    .arg((int)reply->error())
                    .arg(reply->error())
                    .arg(reply->errorString(), reply->readAll());
    }

    else if (reply->error() != QNetworkReply::NoError)
    {
        error = QString("%1 (%2) - %3 %4")
                    .arg((int)reply->error())
                    .arg(reply->error())
                    .arg(reply->errorString(), reply->readAll());
    }

    else if (auto doc = QJsonDocument::fromJson(reply->readAll(), &parseError);
             parseError.error != QJsonParseError::NoError)
        error = QString("Failed parsing response: ") + parseError.errorString();

    else if (auto obj = doc.object();
             obj.contains("error"))
    {
        error = obj["error"].toString();
        if (auto desc = obj["error_description"].toString(); !desc.isEmpty())
            error += ": " + desc;
        if (auto url = obj["error_uri"].toString(); !url.isEmpty())
            error += " (" + url + ")";
    }

    else if (obj.contains("access_token") && obj.contains("token_type"))
    {
        // {
        //     access_token: 'BQBLuPRYBQ...BP8stIv5xr-Iwaf4l8eg',
        //     token_type: 'Bearer',
        //     expires_in: 3600,
        //     refresh_token: 'AQAQfyEFmJJuCvAFh...cG_m-2KTgNDaDMQqjrOa3',
        //     scope: 'user-read-email user-read-private'
        // }
        //
        // access_token
        //       REQUIRED.  The access token issued by the authorization server.
        // token_type
        //       REQUIRED.  The type of the token issued as described in
        //       Section 7.1.  Value is case insensitive.
        // expires_in
        //       RECOMMENDED.  The lifetime in seconds of the access token.  For
        //       example, the value "3600" denotes that the access token will
        //       expire in one hour from the time the response was generated.
        //       If omitted, the authorization server SHOULD provide the
        //       expiration time via other means or document the default value.
        // refresh_token
        //       OPTIONAL.  The refresh token, which can be used to obtain new
        //       access tokens using the same authorization grant as described
        //       in Section 6.
        // scope
        //       OPTIONAL, if identical to the scope requested by the client;
        //       otherwise, REQUIRED.  The scope of the access token as
        //       described by Section 3.3.

        if (const auto type = obj["token_type"].toString();
            QString::compare(type, QStringLiteral("bearer"), Qt::CaseInsensitive))
            error = QString("Unsupported token type: %1.").arg(type);
        else
        {
            q->setTokens(obj["access_token"].toString(),
                         obj["refresh_token"].toString(),
                         QDateTime::currentDateTime().addSecs(obj["expires_in"].toInt()));

            emit q->stateChanged(Granted);

            return;  // success
        }
    }

    else
        error = "Neither 'error' nor 'access_token' and 'token_type' in access token response.";

    error = QString("Access token request failed: %1").arg(error);



    access_token.clear();
    refresh_token.clear();
    token_expiration = {};
    emit q->tokensChanged();
    emit q->stateChanged(NotAuthorized);
}

void OAuth2::Private::refreshAccessToken()
{
    // Refreshing an Access Token - https://datatracker.ietf.org/doc/html/rfc6749#section-6
    //
    // grant_type
    //       REQUIRED.  Value MUST be set to "refresh_token".
    // refresh_token
    //       REQUIRED.  The refresh token issued to the client.
    // scope
    //       OPTIONAL.  The scope of the access request as described by
    //       Section 3.3.  The requested scope MUST NOT include any scope
    //       not originally granted by the resource owner, and if omitted is
    //       treated as equal to the scope originally granted by the
    //       resource owner.

    QUrlQuery params;
    params.addQueryItem("grant_type", "refresh_token");
    params.addQueryItem("refresh_token", refresh_token);
    QNetworkRequest request(token_url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
    if (pkce)
        params.addQueryItem("client_id", client_id);
    else
    {
        const auto base64 = QString("%1:%2").arg(client_id, client_secret).toUtf8().toBase64();
        request.setRawHeader(QByteArray("Authorization"), QString("Basic %1").arg(base64).toUtf8());
    }

    QNetworkReply *reply = network().post(request, params.toString(QUrl::FullyEncoded).toUtf8());
    QObject::connect(reply, &QNetworkReply::finished, q, [this, reply] {
        // If valid and authorized, the authorization server issues an access
        // token as described in Section 5.1.  If the request failed
        // verification or is invalid, the authorization server returns an error
        // response as described in Section 5.2.
        handleAccessTokenResponse(reply);
        reply->deleteLater();
    });
}

// -------------------------------------------------------------------------------------------------

OAuth2::OAuth2() : d(make_unique<Private>(this))
{
    connect(&d->token_refresh_timer, &QTimer::timeout, this, &OAuth2::updateTokens);
    d->token_refresh_timer.setSingleShot(true);
}

OAuth2::~OAuth2() {}

void OAuth2::requestAccess() { d->requestAuthorization(); }

void OAuth2::handleCallback(const QUrl &callback_url)
{ d->handleAutorizationResponse(callback_url); }

void OAuth2::updateTokens() { d->refreshAccessToken(); }

const QString &OAuth2::clientId() const { return d->client_id; }

void OAuth2::setClientId(const QString &v)
{
    if (v != d->client_id)
    {
        d->client_id = v;
        emit clientIdChanged(v);
    }
}

const QString &OAuth2::clientSecret() const { return d->client_secret; }

void OAuth2::setClientSecret(const QString &v)
{
    if (v != d->client_secret)
    {
        d->client_secret = v;
        emit clientSecretChanged(v);
    }
}

const QString &OAuth2::scope() const { return d->scope; }

void OAuth2::setScope(const QString &v)
{
    if (v != d->scope)
    {
        d->scope = v;
        emit scopeChanged(v);
    }
}

const QString &OAuth2::authUrl() const { return d->auth_url; }

void OAuth2::setAuthUrl(const QString &v)
{
    if (v != d->auth_url)
    {
        d->auth_url = v;
        emit authUrlChanged(v);
    }
}

const QString &OAuth2::redirectUri() const { return d->redirect_uri; }

void OAuth2::setRedirectUri(const QString &v)
{
    if (v != d->redirect_uri)
    {
        d->redirect_uri = v;
        emit redirectUriChanged(v);
    }
}

bool OAuth2::isPkceEnabled() const { return d->pkce; }

void OAuth2::setPkceEnabled(bool v)
{
    if (v != d->pkce)
    {
        d->pkce = v;
    }
}

const QString &OAuth2::tokenUrl() const { return d->token_url; }

void OAuth2::setTokenUrl(const QString &v)
{
    if (v != d->token_url)
    {
        d->token_url = v;
        emit tokenUrlChanged(v);
    }
}

const QString &OAuth2::accessToken() const { return d->access_token; }

const QString &OAuth2::refreshToken() const { return d->refresh_token; }

const QDateTime &OAuth2::tokenExpiration() const { return d->token_expiration; }

void OAuth2::setTokens(const QString &access_token,
                       const QString &refresh_token,
                       const QDateTime &expiration)
{
    const auto state_before = state();

    d->access_token = access_token;
    d->refresh_token = refresh_token;
    d->token_refresh_timer.stop();
    d->token_expiration = expiration;
    if (!refresh_token.isEmpty())
    {
        if (expiration.isNull())
        {
            WARN << "Got 'refresh_token' but no valid expiration. Refreshing immediately.";
            d->refreshAccessToken();
        }
        else
        {
            if (const auto expires_in = QDateTime::currentDateTime().secsTo(expiration);
                expires_in > 0)
                d->token_refresh_timer.start((expires_in - 30) * 1000);
            else
                d->refreshAccessToken();
        }
    }

    emit tokensChanged();

    const auto state_after = state();
    if (state_before != state_after)
        emit stateChanged(state_after);
}

const QString &OAuth2::error() const { return d->error; }

OAuth2::State OAuth2::state() const
{
    using enum State;
    if (!d->access_token.isEmpty())
        return Granted;
    else if (d->state_string.isEmpty())
        return NotAuthorized;
    else
        return Awaiting;
}
