#include "msaauthservice.h"

#include <QDateTime>
#include <QJsonArray>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrlQuery>

namespace {

// ── Azure-приложение лаунчера ───────────────────────────────────────────────
// Временно используется client_id официального лаунчера Mojang: это старое
// приложение Live Connect, поэтому и конечные точки здесь от login.live.com.
// При переходе на собственное приложение (портал Azure -> регистрация
// приложения -> «Allow public client flows») меняются шесть констант ниже:
//   kClientId        — id своего приложения;
//   kAuthorizeUrl    — https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
//   kTokenUrl        — https://login.microsoftonline.com/consumers/oauth2/v2.0/token
//   kRedirectUri     — тот, что зарегистрирован в Azure; для встроенного окна
//                      удобен https://login.microsoftonline.com/common/oauth2/nativeclient
//   kScope           — XboxLive.signin offline_access
//   kRpsTicketPrefix — "d="; у старого приложения в RpsTicket идёт сырой токен,
//                      у нового — с этой приставкой.
const QString kClientId = QStringLiteral("00000000402b5328");
const QString kAuthorizeUrl = QStringLiteral("https://login.live.com/oauth20_authorize.srf");
const QString kTokenUrl = QStringLiteral("https://login.live.com/oauth20_token.srf");
const QString kRedirectUri = QStringLiteral("https://login.live.com/oauth20_desktop.srf");
const QString kScope = QStringLiteral("service::user.auth.xboxlive.com::MBI_SSL");
const QString kRpsTicketPrefix = QString();

const QString kXboxAuthUrl = QStringLiteral("https://user.auth.xboxlive.com/user/authenticate");
const QString kXstsUrl = QStringLiteral("https://xsts.auth.xboxlive.com/xsts/authorize");
const QString kMinecraftLoginUrl =
    QStringLiteral("https://api.minecraftservices.com/authentication/login_with_xbox");
const QString kEntitlementsUrl =
    QStringLiteral("https://api.minecraftservices.com/entitlements/mcstore");
const QString kProfileUrl = QStringLiteral("https://api.minecraftservices.com/minecraft/profile");

const int kTransferTimeoutMs = 20000;

QString normalizeUuid(const QString &uuid)
{
    return QString(uuid).remove(u'-');
}

// Первый элемент DisplayClaims.xui — единственный, который отдают и Xbox Live,
// и XSTS: в нём лежат uhs (хеш пользователя) и xid (он же XUID).
QJsonObject displayClaim(const QJsonObject &response)
{
    return response.value(QStringLiteral("DisplayClaims"))
        .toObject()
        .value(QStringLiteral("xui"))
        .toArray()
        .first()
        .toObject();
}

// XSTS отказывает не текстом, а числовым кодом — без расшифровки пользователь
// видит просто «401».
QString describeXstsError(const QJsonObject &response)
{
    const qint64 code = static_cast<qint64>(response.value(QStringLiteral("XErr")).toDouble());
    switch (code) {
    case 2148916233:
        return MsaAuthService::tr("К аккаунту Microsoft не привязан профиль Xbox. "
                                  "Создайте его на xbox.com и повторите вход.");
    case 2148916235:
        return MsaAuthService::tr("Xbox Live недоступен в стране, указанной в аккаунте");
    case 2148916236:
    case 2148916237:
        return MsaAuthService::tr("Аккаунту нужна проверка возраста на xbox.com");
    case 2148916238:
        return MsaAuthService::tr("Детский аккаунт: добавьте его в семейную группу "
                                  "Microsoft, иначе вход в Xbox Live невозможен");
    default:
        break;
    }
    const QString message = response.value(QStringLiteral("Message")).toString();
    return message.isEmpty() ? MsaAuthService::tr("Xbox Live отклонил вход (XErr %1)").arg(code)
                             : message;
}

QString describeOauthError(const QJsonObject &response, int status)
{
    const QString description = response.value(QStringLiteral("error_description")).toString();
    if (!description.isEmpty())
        return description;
    const QString error = response.value(QStringLiteral("error")).toString();
    if (!error.isEmpty())
        return error;
    return MsaAuthService::tr("Microsoft вернул код %1").arg(status);
}

} // namespace

MsaAuthService::MsaAuthService(QObject *parent)
    : QObject(parent)
    , m_network(new QNetworkAccessManager(this))
{
}

QString MsaAuthService::clientId()
{
    return kClientId;
}

QUrl MsaAuthService::authorizationUrl()
{
    QUrl url(kAuthorizeUrl);
    QUrlQuery query;
    query.addQueryItem(QStringLiteral("client_id"), kClientId);
    query.addQueryItem(QStringLiteral("response_type"), QStringLiteral("code"));
    query.addQueryItem(QStringLiteral("redirect_uri"), kRedirectUri);
    query.addQueryItem(QStringLiteral("scope"), kScope);
    // Без этого Microsoft молча пускает по прошлой сессии браузера, и добавить
    // второй аккаунт становится нечем.
    query.addQueryItem(QStringLiteral("prompt"), QStringLiteral("select_account"));
    url.setQuery(query);
    return url;
}

bool MsaAuthService::matchRedirect(const QUrl &url, QString *code, QString *error)
{
    const QUrl redirect(kRedirectUri);
    if (url.host().compare(redirect.host(), Qt::CaseInsensitive) != 0
        || url.path() != redirect.path())
        return false;

    // Код приходит в query, но часть приложений Azure настроена на response_mode
    // с фрагментом — разбираем оба места.
    QUrlQuery query(url.query());
    if (!query.hasQueryItem(QStringLiteral("code")) && !query.hasQueryItem(QStringLiteral("error")))
        query = QUrlQuery(url.fragment());

    if (code)
        *code = query.queryItemValue(QStringLiteral("code"), QUrl::FullyDecoded);
    if (error) {
        const QString description =
            query.queryItemValue(QStringLiteral("error_description"), QUrl::FullyDecoded);
        *error = description.isEmpty()
                     ? query.queryItemValue(QStringLiteral("error"), QUrl::FullyDecoded)
                     : description;
    }
    return true;
}

void MsaAuthService::postForm(const QUrl &url, const QByteArray &body, JsonHandler handler)
{
    QNetworkRequest request{url};
    request.setHeader(QNetworkRequest::ContentTypeHeader,
                      QStringLiteral("application/x-www-form-urlencoded"));
    request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                         QNetworkRequest::NoLessSafeRedirectPolicy);
    request.setTransferTimeout(kTransferTimeoutMs);

    QNetworkReply *reply = m_network->post(request, body);
    connect(reply, &QNetworkReply::finished, this, [reply, handler = std::move(handler)]() {
        reply->deleteLater();
        const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        QString transportError;
        if (status == 0 && reply->error() != QNetworkReply::NoError)
            transportError = reply->errorString();
        handler(status, QJsonDocument::fromJson(reply->readAll()).object(), transportError);
    });
}

void MsaAuthService::postJson(const QUrl &url,
                              const QJsonObject &body,
                              const QString &bearer,
                              JsonHandler handler)
{
    QNetworkRequest request{url};
    request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
    request.setRawHeader("Accept", "application/json");
    if (!bearer.isEmpty())
        request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(bearer).toUtf8());
    request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                         QNetworkRequest::NoLessSafeRedirectPolicy);
    request.setTransferTimeout(kTransferTimeoutMs);

    QNetworkReply *reply = m_network->post(request, QJsonDocument(body).toJson(QJsonDocument::Compact));
    connect(reply, &QNetworkReply::finished, this, [reply, handler = std::move(handler)]() {
        reply->deleteLater();
        const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        QString transportError;
        if (status == 0 && reply->error() != QNetworkReply::NoError)
            transportError = reply->errorString();
        handler(status, QJsonDocument::fromJson(reply->readAll()).object(), transportError);
    });
}

void MsaAuthService::getJson(const QUrl &url, const QString &bearer, JsonHandler handler)
{
    QNetworkRequest request{url};
    request.setRawHeader("Accept", "application/json");
    if (!bearer.isEmpty())
        request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(bearer).toUtf8());
    request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
                         QNetworkRequest::NoLessSafeRedirectPolicy);
    request.setTransferTimeout(kTransferTimeoutMs);

    QNetworkReply *reply = m_network->get(request);
    connect(reply, &QNetworkReply::finished, this, [reply, handler = std::move(handler)]() {
        reply->deleteLater();
        const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        QString transportError;
        if (status == 0 && reply->error() != QNetworkReply::NoError)
            transportError = reply->errorString();
        handler(status, QJsonDocument::fromJson(reply->readAll()).object(), transportError);
    });
}

void MsaAuthService::loginWithCode(const QString &code, Callback callback)
{
    if (code.isEmpty()) {
        AuthResult result;
        result.error = tr("Microsoft не вернул код авторизации");
        callback(result);
        return;
    }

    QUrlQuery form;
    form.addQueryItem(QStringLiteral("client_id"), kClientId);
    form.addQueryItem(QStringLiteral("code"), code);
    form.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("authorization_code"));
    form.addQueryItem(QStringLiteral("redirect_uri"), kRedirectUri);

    emit progress(tr("Обмен кода Microsoft на токен…"));
    exchangeToken(form.toString(QUrl::FullyEncoded).toUtf8(), std::move(callback));
}

void MsaAuthService::loginWithRefreshToken(const QString &refreshToken, Callback callback)
{
    if (refreshToken.isEmpty()) {
        AuthResult result;
        result.error = tr("Профиль не подключён к аккаунту Microsoft");
        callback(result);
        return;
    }

    QUrlQuery form;
    form.addQueryItem(QStringLiteral("client_id"), kClientId);
    form.addQueryItem(QStringLiteral("refresh_token"), refreshToken);
    form.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("refresh_token"));
    form.addQueryItem(QStringLiteral("redirect_uri"), kRedirectUri);
    form.addQueryItem(QStringLiteral("scope"), kScope);

    emit progress(tr("Обновление сессии Microsoft…"));
    exchangeToken(form.toString(QUrl::FullyEncoded).toUtf8(), std::move(callback));
}

void MsaAuthService::exchangeToken(const QByteArray &form, Callback callback)
{
    postForm(QUrl(kTokenUrl), form,
             [this, callback](int status, const QJsonObject &response, const QString &transportError) {
                 if (!transportError.isEmpty()) {
                     AuthResult result;
                     result.error = tr("Нет связи с сервером Microsoft: %1").arg(transportError);
                     callback(result);
                     return;
                 }
                 if (status != 200) {
                     AuthResult result;
                     result.error = describeOauthError(response, status);
                     callback(result);
                     return;
                 }

                 const QString accessToken = response.value(QStringLiteral("access_token")).toString();
                 // Refresh-токен приходит и при продлении: Microsoft выдаёт новый
                 // и обесценивает прежний, поэтому сохранять надо свежий.
                 const QString refreshToken = response.value(QStringLiteral("refresh_token")).toString();
                 if (accessToken.isEmpty()) {
                     AuthResult result;
                     result.error = tr("Microsoft не вернул токен доступа");
                     callback(result);
                     return;
                 }
                 authenticateXbox(accessToken, refreshToken, callback);
             });
}

void MsaAuthService::authenticateXbox(const QString &microsoftToken,
                                      const QString &refreshToken,
                                      Callback callback)
{
    emit progress(tr("Вход в Xbox Live…"));

    QJsonObject properties{{QStringLiteral("AuthMethod"), QStringLiteral("RPS")},
                           {QStringLiteral("SiteName"), QStringLiteral("user.auth.xboxlive.com")},
                           {QStringLiteral("RpsTicket"), kRpsTicketPrefix + microsoftToken}};
    const QJsonObject body{{QStringLiteral("Properties"), properties},
                           {QStringLiteral("RelyingParty"), QStringLiteral("http://auth.xboxlive.com")},
                           {QStringLiteral("TokenType"), QStringLiteral("JWT")}};

    postJson(QUrl(kXboxAuthUrl), body, {},
             [this, refreshToken, callback](int status, const QJsonObject &response,
                                            const QString &transportError) {
                 AuthResult result;
                 if (!transportError.isEmpty()) {
                     result.error = tr("Нет связи с Xbox Live: %1").arg(transportError);
                     callback(result);
                     return;
                 }
                 if (status != 200) {
                     result.error = tr("Xbox Live отклонил токен Microsoft (код %1)").arg(status);
                     callback(result);
                     return;
                 }

                 const QString token = response.value(QStringLiteral("Token")).toString();
                 if (token.isEmpty()) {
                     result.error = tr("Xbox Live не вернул токен");
                     callback(result);
                     return;
                 }
                 authorizeXsts(token, refreshToken, callback);
             });
}

void MsaAuthService::authorizeXsts(const QString &xblToken,
                                   const QString &refreshToken,
                                   Callback callback)
{
    emit progress(tr("Проверка прав XSTS…"));

    const QJsonObject properties{{QStringLiteral("SandboxId"), QStringLiteral("RETAIL")},
                                 {QStringLiteral("UserTokens"), QJsonArray{xblToken}}};
    const QJsonObject body{{QStringLiteral("Properties"), properties},
                           {QStringLiteral("RelyingParty"),
                            QStringLiteral("rp://api.minecraftservices.com/")},
                           {QStringLiteral("TokenType"), QStringLiteral("JWT")}};

    postJson(QUrl(kXstsUrl), body, {},
             [this, refreshToken, callback](int status, const QJsonObject &response,
                                            const QString &transportError) {
                 AuthResult result;
                 if (!transportError.isEmpty()) {
                     result.error = tr("Нет связи с сервером XSTS: %1").arg(transportError);
                     callback(result);
                     return;
                 }
                 if (status == 401) {
                     result.error = describeXstsError(response);
                     callback(result);
                     return;
                 }
                 if (status != 200) {
                     result.error = tr("XSTS отклонил запрос (код %1)").arg(status);
                     callback(result);
                     return;
                 }

                 const QString token = response.value(QStringLiteral("Token")).toString();
                 const QJsonObject claim = displayClaim(response);
                 const QString userHash = claim.value(QStringLiteral("uhs")).toString();
                 if (token.isEmpty() || userHash.isEmpty()) {
                     result.error = tr("XSTS не вернул данные пользователя");
                     callback(result);
                     return;
                 }
                 loginWithXbox(userHash, token, claim.value(QStringLiteral("xid")).toString(),
                               refreshToken, callback);
             });
}

void MsaAuthService::loginWithXbox(const QString &userHash,
                                   const QString &xstsToken,
                                   const QString &xuid,
                                   const QString &refreshToken,
                                   Callback callback)
{
    emit progress(tr("Авторизация в Minecraft Services…"));

    const QJsonObject body{{QStringLiteral("identityToken"),
                            QStringLiteral("XBL3.0 x=%1;%2").arg(userHash, xstsToken)}};

    postJson(QUrl(kMinecraftLoginUrl), body, {},
             [this, xuid, refreshToken, callback](int status, const QJsonObject &response,
                                                  const QString &transportError) {
                 AuthResult result;
                 if (!transportError.isEmpty()) {
                     result.error = tr("Нет связи с api.minecraftservices.com: %1").arg(transportError);
                     callback(result);
                     return;
                 }
                 if (status != 200) {
                     result.error = tr("Minecraft Services отклонили вход (код %1)").arg(status);
                     callback(result);
                     return;
                 }

                 result.accessToken = response.value(QStringLiteral("access_token")).toString();
                 if (result.accessToken.isEmpty()) {
                     result.error = tr("Minecraft Services не вернули токен доступа");
                     callback(result);
                     return;
                 }

                 const int lifetime = response.value(QStringLiteral("expires_in")).toInt(86400);
                 result.expiresAt = QDateTime::currentDateTimeUtc().addSecs(lifetime);
                 result.refreshToken = refreshToken;
                 result.xuid = xuid;
                 result.clientToken = kClientId;
                 result.userType = QStringLiteral("msa");
                 checkLicense(result, callback);
             });
}

void MsaAuthService::checkLicense(AuthResult result, Callback callback)
{
    emit progress(tr("Проверка лицензии Minecraft…"));

    getJson(QUrl(kEntitlementsUrl), result.accessToken,
            [this, result, callback](int status, const QJsonObject &response, const QString &) {
                // Сетевую ошибку здесь не считаем отказом: окончательный ответ
                // про лицензию даёт профиль, а список покупок нужен только
                // затем, чтобы точнее объяснить отказ.
                const bool owns = status == 200
                                  && !response.value(QStringLiteral("items")).toArray().isEmpty();
                fetchProfile(result, owns, callback);
            });
}

void MsaAuthService::fetchProfile(AuthResult result, bool storeEntitlement, Callback callback)
{
    getJson(QUrl(kProfileUrl), result.accessToken,
            [result, storeEntitlement, callback](int status, const QJsonObject &response,
                                                 const QString &transportError) mutable {
                if (!transportError.isEmpty()) {
                    result.error = MsaAuthService::tr("Нет связи с api.minecraftservices.com: %1")
                                       .arg(transportError);
                    callback(result);
                    return;
                }

                // 404 здесь — штатный ответ для аккаунта, на котором нет копии
                // игры: профиль игрока создаётся только вместе с покупкой.
                if (status == 404 || status == 401) {
                    result.licenseMissing = true;
                    result.error =
                        storeEntitlement
                            ? MsaAuthService::tr("На аккаунте Microsoft есть покупка, но игровой "
                                                 "профиль не создан. Зайдите один раз в "
                                                 "официальный лаунчер и выберите ник.")
                            : MsaAuthService::tr("На этом аккаунте Microsoft нет копии "
                                                 "Minecraft: Java Edition");
                    callback(result);
                    return;
                }
                if (status != 200) {
                    result.error =
                        MsaAuthService::tr("Не удалось получить профиль Minecraft (код %1)").arg(status);
                    callback(result);
                    return;
                }

                result.playerName = response.value(QStringLiteral("name")).toString();
                result.uuid = normalizeUuid(response.value(QStringLiteral("id")).toString());
                result.ok = !result.playerName.isEmpty() && !result.uuid.isEmpty();
                if (!result.ok) {
                    result.licenseMissing = true;
                    result.error = MsaAuthService::tr("Minecraft Services не вернули игровой профиль");
                }
                callback(result);
            });
}
